统一微信壳桥接目录
新增 miniprogram/shell 承接微信 Page 生命周期与 wx 容器调用 收窄微信页面入口为 shell 页面工厂装配 更新三端 host-bridge/shell 目录门禁与认证 smoke 补充微信 shell 测试与 CommonJS 测试加载工具 同步宿主壳架构文档和项目共享记忆 补齐 api-server 外部生成测试 fixture 更新时间字段
This commit is contained in:
@@ -85,6 +85,7 @@
|
||||
- 2026-06-18 移动壳 WebView 安全开关:Expo 移动壳 WebView 必须显式禁用 JS 自动开窗、多窗口、文件访问、file URL 跨源访问、HTTPS 混合内容、第三方 Cookie、共享 Cookie 和 WebView 远程调试;同源主站页面才能留在带 HostBridge 的 WebView 内,外链只通过受控协议离开容器交给系统。配置检查和移动壳导航测试会拒绝这些边界被放宽。
|
||||
- 2026-06-18 移动壳 WebView 默认下载边界:Expo WebView 内网页自动下载和 `<a download>` 直接落盘默认关闭;壳层注入脚本阻断 download 链接,iOS `onFileDownload` 只丢弃不落盘,Android 包配置通过 `blockedPermissions` 移除外部存储读写、管理外部存储和请求安装包权限。移动端文本、图片、音频保存只能通过 `file.exportText`、`file.exportImage`、`file.exportAudio` 等 HostBridge 受控导出能力进入系统分享 / 保存面板。
|
||||
- 2026-06-18 移动壳 HostBridge 消息来源校验:Expo 移动壳 `onMessage` 必须根据 `event.nativeEvent.url` 校验消息来源,只有同源主站页面能进入 `handleMobileHostBridgeMessage`;`about:blank`、外域、协议降级和危险协议页面消息直接丢弃,不返回宿主能力错误细节。该规则与 WebView 导航留壳规则共用同源判断,配置检查和移动壳导航测试会拒绝移除。
|
||||
- 2026-06-18 三端桥接层目录同构:微信小程序、Expo 移动壳和 Tauri 桌面壳都按 `host-bridge / shell` 两层管理宿主桥接代码。微信 `miniprogram/host-bridge/wechatHostBridge*.js` 只放协议归一、支付 / 订阅 / 分享结果编解码和可测试桥接函数,`miniprogram/shell/wechatShell*.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂;页面目录只保留 `Page(createWechat...Page())` 装配。`npm run check:native-shells` 会校验微信、移动和桌面三端目录清单,新增宿主能力必须按同一边界落文件和测试。
|
||||
- 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。
|
||||
- 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。
|
||||
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。
|
||||
|
||||
@@ -50,7 +50,8 @@ apps/
|
||||
mobile-shell/ # Expo + React Native App 壳
|
||||
desktop-shell/ # Tauri 桌面 App 壳
|
||||
miniprogram/
|
||||
host-bridge/ # 微信小程序宿主桥接逻辑,页面只做装配
|
||||
host-bridge/ # 微信小程序协议归一、支付 / 订阅 / 分享结果编解码
|
||||
shell/ # 微信小程序 Page 生命周期、wx.* 容器调用和页面工厂
|
||||
packages/
|
||||
shared/
|
||||
src/contracts/
|
||||
@@ -63,7 +64,7 @@ src/
|
||||
|
||||
已落地:`packages/shared/src/contracts/hostBridge.ts` 保存消息 envelope、method、payload 和错误码,H5、Expo 壳与 Tauri 壳共享同一份协议类型。
|
||||
|
||||
三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。
|
||||
三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,协议归一、支付 / 订阅 / 分享结果编解码统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`,Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/wechatShell*.js`,页面入口只做 `Page(createWechat...Page())` 装配;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。
|
||||
|
||||
## HostBridge 消息协议
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ AI H5 sandbox
|
||||
-> parent HostBridge adapter
|
||||
```
|
||||
|
||||
桥接层文件结构按宿主统一为“协议 / 分发 / 宿主容器行为”三类职责。微信小程序的可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留页面装配;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。
|
||||
桥接层文件结构按宿主统一为“协议 / 分发 / 宿主容器行为”三类职责。微信小程序的协议与可测试桥接函数统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`,`miniprogram/shell/wechatShell*.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。
|
||||
|
||||
## 首批能力
|
||||
|
||||
|
||||
@@ -176,44 +176,13 @@ function notifyPreviousWebView(requestId, orderId, payResult) {
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
canUseVirtualPayment,
|
||||
PAY_RESULT_STORAGE_KEY,
|
||||
appendPayResult,
|
||||
buildPayResultValue,
|
||||
createWechatPayPage,
|
||||
normalizePayError,
|
||||
notifyPreviousWebView,
|
||||
parsePayParams,
|
||||
safeCompareVersion,
|
||||
requestWechatPayment,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import wechatPayBridge from './wechatHostBridgePayment.js';
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const {
|
||||
appendPayResult,
|
||||
createWechatPayPage,
|
||||
parsePayParams,
|
||||
requestWechatPayment,
|
||||
} = wechatPayBridge;
|
||||
const payBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgePayment.js',
|
||||
);
|
||||
|
||||
describe('wechat-pay mini program payment bridge', () => {
|
||||
let wechatPayBridge;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
globalThis.wx = {
|
||||
@@ -20,9 +22,11 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
navigateBack: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => []);
|
||||
wechatPayBridge = loadCommonJsModule(payBridgePath);
|
||||
});
|
||||
|
||||
test('routes virtual payloads to wx.requestVirtualPayment', async () => {
|
||||
const { requestWechatPayment } = wechatPayBridge;
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
@@ -49,6 +53,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
});
|
||||
|
||||
test('routes goods virtual payloads to wx.requestVirtualPayment', async () => {
|
||||
const { requestWechatPayment } = wechatPayBridge;
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
@@ -75,6 +80,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
});
|
||||
|
||||
test('keeps ordinary requestPayment payloads on wx.requestPayment', async () => {
|
||||
const { requestWechatPayment } = wechatPayBridge;
|
||||
globalThis.wx.requestPayment.mockImplementationOnce((options) => {
|
||||
options.success?.();
|
||||
});
|
||||
@@ -101,6 +107,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
});
|
||||
|
||||
test('maps virtual payment cancel errCode to cancel result', async () => {
|
||||
const { requestWechatPayment } = wechatPayBridge;
|
||||
const payError = {
|
||||
errCode: -2,
|
||||
errMsg: 'requestVirtualPayment:fail cancel',
|
||||
@@ -129,30 +136,17 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('page notifies previous web-view after virtual payment', async () => {
|
||||
test('notifies previous web-view after virtual payment', () => {
|
||||
const { notifyPreviousWebView } = wechatPayBridge;
|
||||
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',
|
||||
}),
|
||||
),
|
||||
notifyPreviousWebView('request-1', 'order-1', {
|
||||
status: 'success',
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
|
||||
@@ -162,10 +156,10 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
expect(previousPage.setData).toHaveBeenCalledWith({
|
||||
webViewUrl: 'https://web.test/#tab=profile&wx_pay_result=request-1%3Asuccess%3Aorder-1',
|
||||
});
|
||||
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('parsePayParams and appendPayResult keep existing behavior', () => {
|
||||
const { appendPayResult, parsePayParams } = wechatPayBridge;
|
||||
expect(parsePayParams(encodeURIComponent('{"paySign":"sig"}'))).toEqual({
|
||||
paySign: 'sig',
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ function resolveSubscribeStatus(result, templateId) {
|
||||
: 'skip';
|
||||
}
|
||||
|
||||
function createSubscribeMessagePage(pageContext, options = {}) {
|
||||
function createSubscribeMessagePageController(pageContext, options = {}) {
|
||||
const templateId = String(options.templateId || '').trim();
|
||||
const notifyPageResult = (methodThis, status, reason) => {
|
||||
const page = pageContext ?? methodThis;
|
||||
@@ -123,6 +123,6 @@ module.exports = {
|
||||
SUBSCRIBE_RESULT_STORAGE_KEY,
|
||||
appendSubscribeResult,
|
||||
buildSubscribeResultValue,
|
||||
createSubscribeMessagePage,
|
||||
createSubscribeMessagePageController,
|
||||
resolveSubscribeStatus,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import subscribeMessageBridge from './wechatHostBridgeSubscribeMessage.js';
|
||||
import { loadCommonJsModule } from '../test-utils/loadCommonJsModule.js';
|
||||
|
||||
const TEST_TEMPLATE_ID = 'm5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU';
|
||||
|
||||
const {
|
||||
SUBSCRIBE_RESULT_STORAGE_KEY,
|
||||
appendSubscribeResult,
|
||||
buildSubscribeResultValue,
|
||||
createSubscribeMessagePage,
|
||||
} = subscribeMessageBridge;
|
||||
const subscribeBridgePath = path.resolve(
|
||||
process.cwd(),
|
||||
'miniprogram/host-bridge/wechatHostBridgeSubscribeMessage.js',
|
||||
);
|
||||
|
||||
describe('subscribe-message mini program bridge', () => {
|
||||
let subscribeMessageBridge;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
requestSubscribeMessage: vi.fn(),
|
||||
@@ -19,9 +21,14 @@ describe('subscribe-message mini program bridge', () => {
|
||||
navigateBack: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => []);
|
||||
subscribeMessageBridge = loadCommonJsModule(subscribeBridgePath);
|
||||
});
|
||||
|
||||
test('requests subscribe message and stores result before returning', () => {
|
||||
const {
|
||||
SUBSCRIBE_RESULT_STORAGE_KEY,
|
||||
createSubscribeMessagePageController,
|
||||
} = subscribeMessageBridge;
|
||||
const previousPage = {
|
||||
data: { webViewUrl: 'https://web.test/#tab=create' },
|
||||
setData: vi.fn(),
|
||||
@@ -32,7 +39,7 @@ describe('subscribe-message mini program bridge', () => {
|
||||
m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU: 'accept',
|
||||
});
|
||||
});
|
||||
const page = createSubscribeMessagePage(
|
||||
const page = createSubscribeMessagePageController(
|
||||
{
|
||||
setData: vi.fn(),
|
||||
},
|
||||
@@ -56,12 +63,16 @@ describe('subscribe-message mini program bridge', () => {
|
||||
});
|
||||
|
||||
test('skip action notifies previous web-view', () => {
|
||||
const {
|
||||
SUBSCRIBE_RESULT_STORAGE_KEY,
|
||||
createSubscribeMessagePageController,
|
||||
} = subscribeMessageBridge;
|
||||
const previousPage = {
|
||||
data: { webViewUrl: 'https://web.test/' },
|
||||
setData: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => [previousPage, {}]);
|
||||
const page = createSubscribeMessagePage(
|
||||
const page = createSubscribeMessagePageController(
|
||||
{
|
||||
setData: vi.fn(),
|
||||
},
|
||||
@@ -80,6 +91,8 @@ describe('subscribe-message mini program bridge', () => {
|
||||
});
|
||||
|
||||
test('appendSubscribeResult replaces stale subscribe hash', () => {
|
||||
const { appendSubscribeResult, buildSubscribeResultValue } =
|
||||
subscribeMessageBridge;
|
||||
expect(
|
||||
appendSubscribeResult(
|
||||
'https://web.test/#old=1&wx_subscribe_result=old',
|
||||
|
||||
@@ -1,206 +1,5 @@
|
||||
/* global Page, wx */
|
||||
/* eslint-disable no-console */
|
||||
/* global Page */
|
||||
|
||||
const {
|
||||
buildShareGridTileFileName,
|
||||
buildShareGridTilePlan,
|
||||
normalizeShareGridQuery,
|
||||
} = require('../../host-bridge/wechatHostBridgeShareGrid');
|
||||
const { createWechatShareGridPage } = require('../../shell/wechatShellShareGrid');
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
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();
|
||||
},
|
||||
});
|
||||
Page(createWechatShareGridPage());
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID } = require('../../config');
|
||||
const {
|
||||
createSubscribeMessagePage,
|
||||
} = require('../../host-bridge/wechatHostBridgeSubscribeMessage');
|
||||
} = require('../../shell/wechatShellSubscribeMessage');
|
||||
|
||||
Page(
|
||||
createSubscribeMessagePage(null, {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
const { createWechatPayPage } = require('../../host-bridge/wechatHostBridgePayment');
|
||||
/* global Page */
|
||||
|
||||
const { createWechatPayPage } = require('../../shell/wechatShellPayment');
|
||||
|
||||
Page(createWechatPayPage());
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
export function loadCommonJsModule(filePath, requireMap = {}) {
|
||||
const module = { exports: {} };
|
||||
const sandbox = {
|
||||
clearTimeout,
|
||||
console,
|
||||
getCurrentPages(...args) {
|
||||
return globalThis.getCurrentPages(...args);
|
||||
},
|
||||
module,
|
||||
exports: module.exports,
|
||||
require(requestPath) {
|
||||
if (Object.prototype.hasOwnProperty.call(requireMap, requestPath)) {
|
||||
return requireMap[requestPath];
|
||||
}
|
||||
throw new Error(`Unexpected require: ${requestPath}`);
|
||||
},
|
||||
setTimeout,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
wx: globalThis.wx,
|
||||
};
|
||||
|
||||
vm.runInNewContext(readFileSync(filePath, 'utf8'), sandbox, {
|
||||
filename: filePath,
|
||||
});
|
||||
|
||||
return module.exports;
|
||||
}
|
||||
@@ -17,6 +17,16 @@ const expectedWechatHostBridgeFiles = [
|
||||
'wechatHostBridgeWebView.js',
|
||||
'wechatHostBridgeWebView.test.js',
|
||||
];
|
||||
const expectedWechatShellFiles = [
|
||||
'wechatShellPayment.js',
|
||||
'wechatShellPayment.test.js',
|
||||
'wechatShellShareGrid.js',
|
||||
'wechatShellShareGrid.test.js',
|
||||
'wechatShellSubscribeMessage.js',
|
||||
'wechatShellSubscribeMessage.test.js',
|
||||
'wechatShellWebView.js',
|
||||
'wechatShellWebView.test.js',
|
||||
];
|
||||
const expectedMobileHostBridgeFiles = [
|
||||
'mobileHostBridge.test.ts',
|
||||
'mobileHostBridge.ts',
|
||||
@@ -210,15 +220,33 @@ function assertHostBridgeLayerLayout() {
|
||||
'wechat host bridge files',
|
||||
);
|
||||
|
||||
for (const pagePath of [
|
||||
const wechatShellFiles = fs
|
||||
.readdirSync('miniprogram/shell', { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
assertSameList(
|
||||
wechatShellFiles,
|
||||
expectedWechatShellFiles,
|
||||
'wechat shell files',
|
||||
);
|
||||
|
||||
const wechatPagePaths = [
|
||||
'miniprogram/pages/web-view/index.js',
|
||||
'miniprogram/pages/wechat-pay/index.js',
|
||||
'miniprogram/pages/share-grid/index.js',
|
||||
'miniprogram/pages/subscribe-message/index.js',
|
||||
]) {
|
||||
];
|
||||
for (const pagePath of wechatPagePaths) {
|
||||
const source = fs.readFileSync(pagePath, 'utf8');
|
||||
if (source.includes("require('./index.shared')")) {
|
||||
throw new Error(`${pagePath} must import from miniprogram/host-bridge`);
|
||||
if (!source.includes("require('../../shell/")) {
|
||||
throw new Error(`${pagePath} must import from miniprogram/shell`);
|
||||
}
|
||||
if (
|
||||
source.includes("require('../../host-bridge/") ||
|
||||
source.includes("require('./index.shared')")
|
||||
) {
|
||||
throw new Error(`${pagePath} must not import bridge logic directly`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,22 +54,27 @@ if (failures.length > 0) {
|
||||
console.log('\n[wechat-miniprogram-auth-smoke] 通过');
|
||||
|
||||
function checkMiniProgramShell() {
|
||||
const shellPath = join(repoRoot, 'miniprogram', 'pages', 'web-view', 'index.js');
|
||||
const shellPath = join(repoRoot, 'miniprogram', 'shell', 'wechatShellWebView.js');
|
||||
const webViewBridgePath = join(repoRoot, 'miniprogram', 'host-bridge', 'wechatHostBridgeWebView.js');
|
||||
const pagePath = join(repoRoot, 'miniprogram', 'pages', 'web-view', 'index.js');
|
||||
const shellTemplatePath = join(repoRoot, 'miniprogram', 'pages', 'web-view', 'index.wxml');
|
||||
const authServiceTestPath = join(repoRoot, 'src', 'services', 'authService.test.ts');
|
||||
|
||||
ensureNeedles(pagePath, ["require('../../shell/wechatShellWebView')"]);
|
||||
ensureNeedles(shellPath, [
|
||||
'/api/auth/wechat/miniprogram-login',
|
||||
'/api/auth/wechat/bind-phone',
|
||||
"'x-client-type': MINI_PROGRAM_CLIENT_TYPE",
|
||||
"'x-client-runtime': MINI_PROGRAM_CLIENT_RUNTIME",
|
||||
'auth_provider',
|
||||
'auth_token',
|
||||
'auth_binding_status',
|
||||
'bindingStatus',
|
||||
'pending_bind_phone',
|
||||
'wechatPhoneCode',
|
||||
]);
|
||||
ensureNeedles(webViewBridgePath, [
|
||||
'auth_provider',
|
||||
'auth_token',
|
||||
'auth_binding_status',
|
||||
]);
|
||||
|
||||
ensureNeedles(shellTemplatePath, ['getPhoneNumber', 'bindgetphonenumber']);
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ import vm from 'node:vm';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pageScriptPath = join(
|
||||
const shellScriptPath = join(
|
||||
repoRoot,
|
||||
'miniprogram',
|
||||
'pages',
|
||||
'web-view',
|
||||
'index.js',
|
||||
'shell',
|
||||
'wechatShellWebView.js',
|
||||
);
|
||||
const webViewHostBridgePath = join(
|
||||
repoRoot,
|
||||
'miniprogram',
|
||||
'host-bridge',
|
||||
'wechatHostBridgeWebView.js',
|
||||
);
|
||||
|
||||
type MiniProgramPage = {
|
||||
@@ -43,45 +48,28 @@ function loadWebViewPage(
|
||||
wxMock: ReturnType<typeof createWxMock>,
|
||||
configOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
let pageConfig: Record<string, unknown> | null = null;
|
||||
const source = readFileSync(pageScriptPath, 'utf8');
|
||||
const sandbox = {
|
||||
console,
|
||||
getCurrentPages: () => [],
|
||||
module: { exports: {} },
|
||||
Page(config: Record<string, unknown>) {
|
||||
pageConfig = config;
|
||||
(globalThis as unknown as { wx: ReturnType<typeof createWxMock> }).wx =
|
||||
wxMock;
|
||||
const webViewBridge = loadCommonJsModule(webViewHostBridgePath, {});
|
||||
const shellModule = loadCommonJsModule(shellScriptPath, {
|
||||
'../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',
|
||||
},
|
||||
...configOverrides,
|
||||
},
|
||||
setTimeout(callback: () => void) {
|
||||
callback();
|
||||
return 1;
|
||||
},
|
||||
require(requestPath: string) {
|
||||
if (requestPath === '../../config') {
|
||||
return {
|
||||
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',
|
||||
},
|
||||
...configOverrides,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected require: ${requestPath}`);
|
||||
},
|
||||
wx: wxMock,
|
||||
'../host-bridge/wechatHostBridgeWebView': webViewBridge,
|
||||
}) as {
|
||||
createWechatWebViewPage: () => Record<string, unknown>;
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, sandbox, { filename: pageScriptPath });
|
||||
|
||||
if (!pageConfig) {
|
||||
throw new Error('web-view page did not call Page()');
|
||||
}
|
||||
const pageConfig = shellModule.createWechatWebViewPage();
|
||||
|
||||
const page = {
|
||||
...pageConfig,
|
||||
@@ -94,6 +82,34 @@ function loadWebViewPage(
|
||||
return page;
|
||||
}
|
||||
|
||||
function loadCommonJsModule(
|
||||
filePath: string,
|
||||
requireMap: Record<string, unknown>,
|
||||
) {
|
||||
const source = readFileSync(filePath, 'utf8');
|
||||
const module = { exports: {} as Record<string, unknown> };
|
||||
const sandbox = {
|
||||
console,
|
||||
getCurrentPages: () => [],
|
||||
module,
|
||||
exports: module.exports,
|
||||
setTimeout(callback: () => void) {
|
||||
callback();
|
||||
return 1;
|
||||
},
|
||||
require(requestPath: string) {
|
||||
if (Object.prototype.hasOwnProperty.call(requireMap, requestPath)) {
|
||||
return requireMap[requestPath];
|
||||
}
|
||||
throw new Error(`Unexpected require: ${requestPath}`);
|
||||
},
|
||||
wx: globalThis.wx,
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, sandbox, { filename: filePath });
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
describe('mini-program web-view auth page', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -264,6 +280,10 @@ describe('mini-program web-view auth page', () => {
|
||||
data: {
|
||||
token: 'jwt-pending-wechat',
|
||||
bindingStatus: 'pending_bind_phone',
|
||||
user: {
|
||||
displayName: '陶泥玩家',
|
||||
publicUserCode: 'SY-12345678',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -744,6 +744,7 @@ mod tests {
|
||||
started_at: Some("2026-06-03T00:00:00Z".to_string()),
|
||||
completed_at: None,
|
||||
updated_at: "2026-06-03T00:00:00Z".to_string(),
|
||||
updated_at_micros: 1_780_444_800_000_000,
|
||||
lease_token: lease_token.map(ToOwned::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user