统一微信壳桥接目录
新增 miniprogram/shell 承接微信 Page 生命周期与 wx 容器调用 收窄微信页面入口为 shell 页面工厂装配 更新三端 host-bridge/shell 目录门禁与认证 smoke 补充微信 shell 测试与 CommonJS 测试加载工具 同步宿主壳架构文档和项目共享记忆 补齐 api-server 外部生成测试 fixture 更新时间字段
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user