统一微信壳桥接目录
新增 miniprogram/shell 承接微信 Page 生命周期与 wx 容器调用 收窄微信页面入口为 shell 页面工厂装配 更新三端 host-bridge/shell 目录门禁与认证 smoke 补充微信 shell 测试与 CommonJS 测试加载工具 同步宿主壳架构文档和项目共享记忆 补齐 api-server 外部生成测试 fixture 更新时间字段
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/* global wx */
|
||||
|
||||
const {
|
||||
notifyPreviousWebView,
|
||||
parsePayParams,
|
||||
requestWechatPayment,
|
||||
} = require('../host-bridge/wechatHostBridgePayment');
|
||||
|
||||
function createWechatPayPage(pageContext) {
|
||||
return {
|
||||
data: {
|
||||
title: '正在拉起支付',
|
||||
errorMessage: '',
|
||||
},
|
||||
|
||||
async onLoad(query) {
|
||||
const requestId = String(query.requestId || '');
|
||||
const orderId = String(query.orderId || '');
|
||||
const payParams = parsePayParams(query.payParams);
|
||||
if (!requestId || !payParams) {
|
||||
const page = pageContext ?? this;
|
||||
page.setData({
|
||||
title: '支付失败',
|
||||
errorMessage: '缺少支付参数。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payResult = await requestWechatPayment(payParams);
|
||||
notifyPreviousWebView(requestId, orderId, payResult);
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
handleBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createWechatPayPage,
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const payBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgePayment.js',
|
||||
);
|
||||
const paymentShellPath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/shell/wechatShellPayment.js',
|
||||
);
|
||||
|
||||
describe('wechat payment shell page', () => {
|
||||
let createWechatPayPage;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
getSystemInfoSync: vi.fn(() => ({ SDKVersion: '2.32.0' })),
|
||||
navigateBack: vi.fn(),
|
||||
requestPayment: vi.fn(),
|
||||
requestVirtualPayment: vi.fn(),
|
||||
setStorageSync: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => []);
|
||||
const wechatPayBridge = loadCommonJsModule(payBridgePath);
|
||||
const wechatPaymentShell = loadCommonJsModule(paymentShellPath, {
|
||||
'../host-bridge/wechatHostBridgePayment': wechatPayBridge,
|
||||
});
|
||||
createWechatPayPage = wechatPaymentShell.createWechatPayPage;
|
||||
});
|
||||
|
||||
test('stores payment result and returns to previous web-view', async () => {
|
||||
const previousPage = {
|
||||
data: { webViewUrl: 'https://web.test/#tab=profile' },
|
||||
setData: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => [previousPage, {}]);
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
const page = createWechatPayPage({ setData: vi.fn() });
|
||||
|
||||
await page.onLoad({
|
||||
requestId: 'request-1',
|
||||
orderId: 'order-1',
|
||||
payParams: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
mode: 'short_series_coin',
|
||||
signData: '{}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
|
||||
'genarrative:wechat-pay-result',
|
||||
'request-1:success:order-1',
|
||||
);
|
||||
expect(previousPage.setData).toHaveBeenCalledWith({
|
||||
webViewUrl:
|
||||
'https://web.test/#tab=profile&wx_pay_result=request-1%3Asuccess%3Aorder-1',
|
||||
});
|
||||
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/* global wx */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const {
|
||||
buildShareGridTileFileName,
|
||||
buildShareGridTilePlan,
|
||||
normalizeShareGridQuery,
|
||||
} = require('../host-bridge/wechatHostBridgeShareGrid');
|
||||
|
||||
function downloadImage(imageUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.downloadFile({
|
||||
url: imageUrl,
|
||||
success(response) {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
resolve(response.tempFilePath);
|
||||
return;
|
||||
}
|
||||
reject(new Error(`封面下载失败:${response.statusCode}`));
|
||||
},
|
||||
fail(error) {
|
||||
reject(new Error(error.errMsg || '封面下载失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getImageInfo(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: resolve,
|
||||
fail(error) {
|
||||
reject(new Error(error.errMsg || '读取封面失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCanvasNode(page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery()
|
||||
.in(page)
|
||||
.select('#share-grid-canvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((results) => {
|
||||
const canvas = results && results[0] && results[0].node;
|
||||
if (canvas) {
|
||||
resolve(canvas);
|
||||
return;
|
||||
}
|
||||
reject(new Error('切图画布初始化失败'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function canvasToTempFilePath(canvas, width, height) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
fileType: 'png',
|
||||
success(response) {
|
||||
resolve(response.tempFilePath);
|
||||
},
|
||||
fail(error) {
|
||||
reject(new Error(error.errMsg || '导出切图失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveImageToAlbum(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
fail(error) {
|
||||
reject(new Error(error.errMsg || '保存到相册失败'));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyTempFileWithName(tempFilePath, fileName) {
|
||||
const fileSystem = wx.getFileSystemManager && wx.getFileSystemManager();
|
||||
const userDataPath = wx.env && wx.env.USER_DATA_PATH;
|
||||
if (!fileSystem || !userDataPath || typeof fileSystem.copyFile !== 'function') {
|
||||
return Promise.resolve(tempFilePath);
|
||||
}
|
||||
|
||||
const targetPath = `${userDataPath}/${fileName}`;
|
||||
return new Promise((resolve) => {
|
||||
fileSystem.copyFile({
|
||||
srcPath: tempFilePath,
|
||||
destPath: targetPath,
|
||||
success() {
|
||||
resolve(targetPath);
|
||||
},
|
||||
fail() {
|
||||
resolve(tempFilePath);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveGridTiles(page, params, localImagePath, imageInfo) {
|
||||
const canvas = await getCanvasNode(page);
|
||||
const context = canvas.getContext('2d');
|
||||
const image = canvas.createImage();
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = resolve;
|
||||
image.onerror = () => reject(new Error('封面绘制失败'));
|
||||
image.src = localImagePath;
|
||||
});
|
||||
|
||||
const plan = buildShareGridTilePlan(imageInfo.width, imageInfo.height);
|
||||
for (const tile of plan) {
|
||||
canvas.width = tile.sourceWidth;
|
||||
canvas.height = tile.sourceHeight;
|
||||
context.clearRect(0, 0, tile.sourceWidth, tile.sourceHeight);
|
||||
context.drawImage(
|
||||
image,
|
||||
tile.sourceX,
|
||||
tile.sourceY,
|
||||
tile.sourceWidth,
|
||||
tile.sourceHeight,
|
||||
0,
|
||||
0,
|
||||
tile.sourceWidth,
|
||||
tile.sourceHeight,
|
||||
);
|
||||
|
||||
const tempFilePath = await canvasToTempFilePath(
|
||||
canvas,
|
||||
tile.sourceWidth,
|
||||
tile.sourceHeight,
|
||||
);
|
||||
const namedFilePath = await copyTempFileWithName(
|
||||
tempFilePath,
|
||||
buildShareGridTileFileName(params, tile.index),
|
||||
);
|
||||
await saveImageToAlbum(namedFilePath);
|
||||
page.setData({
|
||||
savedCount: tile.index + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createWechatShareGridPage() {
|
||||
return {
|
||||
data: {
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
savedCount: 0,
|
||||
title: '九宫切图',
|
||||
},
|
||||
|
||||
async onLoad(query = {}) {
|
||||
const params = normalizeShareGridQuery(query);
|
||||
this._shareGridParams = params;
|
||||
this.setData({
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
savedCount: 0,
|
||||
title: params.title,
|
||||
});
|
||||
|
||||
if (!params.imageUrl) {
|
||||
this.setData({
|
||||
errorMessage: '缺少封面图。',
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const localImagePath = await downloadImage(params.imageUrl);
|
||||
const imageInfo = await getImageInfo(localImagePath);
|
||||
await saveGridTiles(this, params, localImagePath, imageInfo);
|
||||
this.setData({
|
||||
loading: false,
|
||||
savedCount: 9,
|
||||
});
|
||||
wx.showToast({
|
||||
title: '已保存',
|
||||
icon: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[share-grid] save failed', error);
|
||||
this.setData({
|
||||
errorMessage:
|
||||
error && error.message ? error.message : '九宫切图保存失败。',
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createWechatShareGridPage,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const shareGridBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgeShareGrid.js',
|
||||
);
|
||||
const shareGridShellPath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/shell/wechatShellShareGrid.js',
|
||||
);
|
||||
|
||||
let createWechatShareGridPage;
|
||||
|
||||
function createPage() {
|
||||
const page = createWechatShareGridPage();
|
||||
return {
|
||||
...page,
|
||||
data: { ...page.data },
|
||||
setData(patch) {
|
||||
Object.assign(this.data, patch);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('wechat share-grid shell page', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
navigateBack: vi.fn(),
|
||||
};
|
||||
const shareGridBridge = loadCommonJsModule(shareGridBridgePath);
|
||||
const shareGridShell = loadCommonJsModule(shareGridShellPath, {
|
||||
'../host-bridge/wechatHostBridgeShareGrid': shareGridBridge,
|
||||
});
|
||||
createWechatShareGridPage = shareGridShell.createWechatShareGridPage;
|
||||
});
|
||||
|
||||
test('shows a clear error when imageUrl is missing', async () => {
|
||||
const page = createPage();
|
||||
|
||||
await page.onLoad({});
|
||||
|
||||
expect(page.data.loading).toBe(false);
|
||||
expect(page.data.errorMessage).toBe('缺少封面图。');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
const {
|
||||
createSubscribeMessagePageController,
|
||||
} = require('../host-bridge/wechatHostBridgeSubscribeMessage');
|
||||
|
||||
function createSubscribeMessagePage(pageContext, options = {}) {
|
||||
return createSubscribeMessagePageController(pageContext, options);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSubscribeMessagePage,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const TEST_TEMPLATE_ID = 'm5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU';
|
||||
|
||||
const subscribeBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgeSubscribeMessage.js',
|
||||
);
|
||||
const subscribeShellPath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/shell/wechatShellSubscribeMessage.js',
|
||||
);
|
||||
|
||||
describe('wechat subscribe-message shell page', () => {
|
||||
let createSubscribeMessagePage;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
navigateBack: vi.fn(),
|
||||
requestSubscribeMessage: vi.fn(),
|
||||
setStorageSync: vi.fn(),
|
||||
};
|
||||
const subscribeMessageBridge = loadCommonJsModule(subscribeBridgePath);
|
||||
const subscribeMessageShell = loadCommonJsModule(subscribeShellPath, {
|
||||
'../host-bridge/wechatHostBridgeSubscribeMessage': subscribeMessageBridge,
|
||||
});
|
||||
createSubscribeMessagePage =
|
||||
subscribeMessageShell.createSubscribeMessagePage;
|
||||
});
|
||||
|
||||
test('requests generation-result subscribe template and stores result', () => {
|
||||
globalThis.wx.requestSubscribeMessage.mockImplementationOnce((options) => {
|
||||
options.success?.({
|
||||
[TEST_TEMPLATE_ID]: 'accept',
|
||||
});
|
||||
});
|
||||
const page = createSubscribeMessagePage(
|
||||
{ setData: vi.fn() },
|
||||
{ templateId: TEST_TEMPLATE_ID },
|
||||
);
|
||||
page.onLoad({ requestId: 'request-1' });
|
||||
|
||||
page.requestSubscribe();
|
||||
|
||||
expect(globalThis.wx.requestSubscribeMessage).toHaveBeenCalledWith({
|
||||
tmplIds: [TEST_TEMPLATE_ID],
|
||||
success: expect.any(Function),
|
||||
fail: expect.any(Function),
|
||||
});
|
||||
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
|
||||
'genarrative:wechat-subscribe-result',
|
||||
'request-1:success',
|
||||
);
|
||||
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const webViewBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgeWebView.js',
|
||||
);
|
||||
const webViewShellPath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/shell/wechatShellWebView.js',
|
||||
);
|
||||
const config = {
|
||||
API_BASE_URL: 'https://www.genarrative.world',
|
||||
DEV_API_BASE_URL: 'https://dev.genarrative.world',
|
||||
DEV_WEB_VIEW_ENTRY_URL: 'https://dev.genarrative.world',
|
||||
MINI_PROGRAM_APP_ID: 'wx-test-app',
|
||||
MINI_PROGRAM_ENV: 'release',
|
||||
WEB_VIEW_ENTRY_URL: 'https://www.genarrative.world',
|
||||
WEB_VIEW_SOURCE_QUERY: {
|
||||
clientType: 'mini_program',
|
||||
clientRuntime: 'wechat_mini_program',
|
||||
},
|
||||
};
|
||||
|
||||
let createWechatWebViewPage;
|
||||
|
||||
function createPage() {
|
||||
const page = createWechatWebViewPage();
|
||||
return {
|
||||
...page,
|
||||
data: { ...page.data },
|
||||
setData(patch) {
|
||||
Object.assign(this.data, patch);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('wechat web-view shell page', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
getAccountInfoSync: vi.fn(() => ({
|
||||
miniProgram: { envVersion: 'release' },
|
||||
})),
|
||||
getStorageSync: vi.fn(() => ''),
|
||||
getSystemInfoSync: vi.fn(() => ({ platform: 'ios' })),
|
||||
login: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
removeStorageSync: vi.fn(),
|
||||
request: vi.fn(),
|
||||
setStorageSync: vi.fn(),
|
||||
showShareMenu: vi.fn(),
|
||||
};
|
||||
const webViewBridge = loadCommonJsModule(webViewBridgePath);
|
||||
const webViewShell = loadCommonJsModule(webViewShellPath, {
|
||||
'../config': config,
|
||||
'../host-bridge/wechatHostBridgeWebView': webViewBridge,
|
||||
});
|
||||
createWechatWebViewPage = webViewShell.createWechatWebViewPage;
|
||||
});
|
||||
|
||||
test('opens anonymous H5 web-view without eager login', async () => {
|
||||
const page = createPage();
|
||||
|
||||
await page.onLoad({});
|
||||
|
||||
expect(globalThis.wx.login).not.toHaveBeenCalled();
|
||||
expect(globalThis.wx.request).not.toHaveBeenCalled();
|
||||
expect(page.data.loading).toBe(false);
|
||||
expect(page.data.webViewUrl).toBe(
|
||||
'https://www.genarrative.world?clientType=mini_program&clientRuntime=wechat_mini_program',
|
||||
);
|
||||
expect(globalThis.wx.showShareMenu).toHaveBeenCalledWith({
|
||||
withShareTicket: true,
|
||||
menus: ['shareAppMessage', 'shareTimeline'],
|
||||
});
|
||||
});
|
||||
|
||||
test('stores latest H5 share target for native share menu', () => {
|
||||
const page = createPage();
|
||||
page.handleWebViewMessage({
|
||||
detail: {
|
||||
data: [
|
||||
{
|
||||
data: {
|
||||
type: 'genarrative:share-target',
|
||||
payload: {
|
||||
targetPath: '/works/detail',
|
||||
work: 'BB-12345678',
|
||||
title: '汪汪声浪',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(page.onShareAppMessage()).toEqual({
|
||||
title: '陶泥儿',
|
||||
path: '/pages/web-view/index?targetPath=%2Fworks%2Fdetail&work=BB-12345678',
|
||||
});
|
||||
expect(page.onShareTimeline()).toEqual({
|
||||
title: '陶泥儿',
|
||||
query: 'targetPath=%2Fworks%2Fdetail&work=BB-12345678',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user