Files
Genarrative/scripts/miniprogram-web-view-auth.test.ts
T
kdletters 1cf9e6bcc6 锁定微信手机号授权取消提示
微信手机号授权取消不再向页面透出原生错误细节

微信认证测试覆盖手机号授权取消稳定提示

native shell 总门禁反查手机号授权取消边界
2026-06-20 13:36:49 +08:00

385 lines
11 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import vm from 'node:vm';
import { beforeEach, describe, expect, test, vi } from 'vitest';
const repoRoot = process.cwd();
const shellScriptPath = join(
repoRoot,
'miniprogram',
'shell',
'webView.js',
);
const webViewHostBridgePath = join(
repoRoot,
'miniprogram',
'host-bridge',
'webView.js',
);
type MiniProgramPage = {
data: Record<string, unknown>;
setData: (patch: Record<string, unknown>) => void;
onLoad: (query?: Record<string, string>) => Promise<void>;
onShareAppMessage: () => Record<string, unknown>;
onShareTimeline: () => Record<string, unknown>;
onShow: () => void;
consumePayResult: () => void;
handleGetPhoneNumber: (event: {
detail?: Record<string, unknown>;
}) => Promise<void>;
};
function createWxMock() {
return {
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(),
showShareMenu: vi.fn(),
setStorageSync: vi.fn(),
};
}
function loadWebViewPage(
wxMock: ReturnType<typeof createWxMock>,
configOverrides: Record<string, unknown> = {},
) {
(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,
},
'../host-bridge/webView': webViewBridge,
}) as {
createWechatWebViewPage: () => Record<string, unknown>;
};
const pageConfig = shellModule.createWechatWebViewPage();
const page = {
...pageConfig,
data: { ...(pageConfig.data as Record<string, unknown>) },
setData(patch: Record<string, unknown>) {
Object.assign(this.data, patch);
},
} as MiniProgramPage;
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();
vi.spyOn(console, 'error').mockImplementation(() => {});
});
test('默认进入时不预登录,直接打开未登录 web-view', async () => {
const wxMock = createWxMock();
wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' });
});
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 200,
data: {
token: 'jwt-active-wechat',
bindingStatus: 'active',
},
});
});
const page = loadWebViewPage(wxMock);
await page.onLoad({});
expect(wxMock.login).not.toHaveBeenCalled();
expect(wxMock.request).not.toHaveBeenCalled();
expect(page.data.loading).toBe(false);
expect(page.data.phoneBindingRequired).toBe(false);
expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
);
expect(wxMock.showShareMenu).toHaveBeenCalledWith({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
});
test('默认进入时即便微信新身份待绑手机号,也不弹出绑定手机号页', async () => {
const wxMock = createWxMock();
wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' });
});
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 200,
data: {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
},
});
});
const page = loadWebViewPage(wxMock);
await page.onLoad({});
expect(wxMock.login).not.toHaveBeenCalled();
expect(wxMock.request).not.toHaveBeenCalled();
expect(page.data.loading).toBe(false);
expect(page.data.phoneBindingRequired).toBe(false);
expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
);
});
test('web-view 页面分享好友和朋友圈都回到小程序 web-view 入口', () => {
const wxMock = createWxMock();
const page = loadWebViewPage(wxMock);
expect(page.onShareAppMessage()).toEqual({
title: '陶泥儿',
path: '/pages/web-view/index',
});
expect(page.onShareTimeline()).toEqual({
title: '陶泥儿',
query: '',
});
});
test('默认匿名进入 web-view 仍不依赖 API_BASE_URL 配置', async () => {
const wxMock = createWxMock();
const page = loadWebViewPage(wxMock, {
API_BASE_URL: '',
});
await page.onLoad({});
expect(wxMock.login).not.toHaveBeenCalled();
expect(wxMock.request).not.toHaveBeenCalled();
expect(page.data.errorMessage).toBe('');
expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
);
});
test('体验版自动切到 dev 子域名并透传 trial 环境', async () => {
const wxMock = createWxMock();
wxMock.getAccountInfoSync.mockReturnValue({
miniProgram: { envVersion: 'trial' },
});
const page = loadWebViewPage(wxMock);
await page.onLoad({});
expect(page.data.webViewUrl).toBe(
'https://dev.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program&miniProgramEnv=trial',
);
});
test('开发版自动切到 dev 子域名并把 develop 规整为 dev', async () => {
const wxMock = createWxMock();
wxMock.getAccountInfoSync.mockReturnValue({
miniProgram: { envVersion: 'develop' },
});
wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' });
});
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 200,
data: {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
},
});
});
const page = loadWebViewPage(wxMock);
await page.onLoad({ authAction: 'login', returnTo: 'previous' });
expect(wxMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://dev.genarrative.world/api/auth/wechat/miniprogram-login',
header: expect.objectContaining({
'x-mini-program-env': 'dev',
}),
}),
);
});
test('onShow 二次检查支付结果并写回 web-view hash', () => {
const wxMock = createWxMock();
wxMock.getStorageSync.mockImplementation((key) =>
key === 'genarrative:wechat-pay-result'
? 'request-1:success:order-1'
: '',
);
const page = loadWebViewPage(wxMock);
page.data.webViewUrl =
'https://www.genarrative.world/?clientType=mini_program#tab=profile';
page.onShow();
expect(wxMock.removeStorageSync).toHaveBeenCalledWith(
'genarrative:wechat-pay-result',
);
expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program#tab=profile&wx_pay_result=request-1%3Asuccess%3Aorder-1',
);
});
test('H5 请求登录时才启动微信小程序登录并进入手机号授权态', async () => {
const wxMock = createWxMock();
wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' });
});
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 200,
data: {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
user: {
displayName: '陶泥玩家',
publicUserCode: 'SY-12345678',
},
},
});
});
const page = loadWebViewPage(wxMock);
await page.onLoad({ authAction: 'login', returnTo: 'previous' });
expect(wxMock.login).toHaveBeenCalledTimes(1);
expect(wxMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://www.genarrative.world/api/auth/wechat/miniprogram-login',
method: 'POST',
data: { code: 'wx-login-code' },
}),
);
expect(page.data.webViewUrl).toBe('');
expect(page.data.loading).toBe(false);
expect(page.data.phoneBindingRequired).toBe(true);
});
test('微信登录失败不向页面透出原生错误', async () => {
const wxMock = createWxMock();
const loginError = { errMsg: 'login:fail private native detail' };
wxMock.login.mockImplementation(({ fail }) => {
fail(loginError);
});
const page = loadWebViewPage(wxMock);
await page.onLoad({ authAction: 'login', returnTo: 'previous' });
expect(page.data.errorMessage).toBe('微信登录失败,请稍后重试。');
expect(console.error).toHaveBeenCalledWith(
'[web-view] wx.login failed',
loginError,
);
expect(page.data.phoneBindingRequired).toBe(false);
});
test('绑定手机号失败不向页面透出后端错误体', async () => {
const wxMock = createWxMock();
const page = loadWebViewPage(wxMock);
page.data.authResult = {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
};
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 500,
data: {
error: {
message: 'private backend detail',
},
},
});
});
await page.handleGetPhoneNumber({
detail: {
code: 'wechat-phone-code',
},
});
expect(page.data.errorMessage).toBe('绑定手机号失败,请稍后重试。');
expect(console.error).toHaveBeenCalledWith(
'[web-view] mini program bind phone failed',
expect.objectContaining({
statusCode: 500,
}),
);
});
test('拒绝手机号授权不向页面透出微信原生错误', async () => {
const wxMock = createWxMock();
const page = loadWebViewPage(wxMock);
page.data.authResult = {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
};
const authDeclined = {
errMsg: 'getPhoneNumber:fail private native detail',
};
await page.handleGetPhoneNumber({
detail: authDeclined,
});
expect(page.data.errorMessage).toBe('需要授权手机号后才能完成绑定。');
expect(page.data.errorMessage).not.toContain('private native detail');
expect(console.error).toHaveBeenCalledWith(
'[web-view] bind phone auth declined',
authDeclined,
);
});
});