统一微信壳桥接目录

新增 miniprogram/shell 承接微信 Page 生命周期与 wx 容器调用

收窄微信页面入口为 shell 页面工厂装配

更新三端 host-bridge/shell 目录门禁与认证 smoke

补充微信 shell 测试与 CommonJS 测试加载工具

同步宿主壳架构文档和项目共享记忆

补齐 api-server 外部生成测试 fixture 更新时间字段
This commit is contained in:
2026-06-18 16:32:25 +08:00
parent 00b41b6a29
commit 7609c37d5f
24 changed files with 1449 additions and 1022 deletions
+42
View File
@@ -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();
});
});
+212
View File
@@ -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',
});
});
});