feat(miniapp): align UI with H5 and harden device debugging

五个页面按 h5/css 的 style.css → ui-20261009 → ui-20261012 覆盖顺序逐条重排,
换算规则固定为 calc(N * var(--u)) → 2N rpx,固定 px 的字号保持 px。

- app.acss:换回 H5 设计 token(方角小倒角按钮 10rpx、卡片 14rpx、.chev 折线箭头)
- splash/profile/history:底图与按钮文字图改用 H5 资源,隐私/政策弹窗按 H5 重排
- report:环形分用 conic-gradient、人脸裁切窗口叠加 face_maps、标记点百分比盒、
  侧脸皱纹 pending/unavailable 文案;用 Playwright 抓 H5 94 个选择器几何比对,
  整页高 2438.7 vs 2438.67
- camera:淘宝 <camera> 组件在真机整页崩溃(嵌了微信专有的 <cover-view>),
  改为默认系统相机 + `config.nativeCamera` 开关 + pages/camtest 冒烟页验证组件
- permissions:淘宝用带前缀的 scope.camera(裸名返回 5022 Scope:camera信息不存在),
  先试带前缀再退回裸名,拒绝后不再重复弹窗
- voice:非 HTTPS 资源导致的异常不再冒泡成整页崩溃;app.js 加开发版错误 toast
- 开发版入口:权限诊断(启动页/相机页)、相机组件测试
- 测试 20/20(新增权限与相机能力用例)
This commit is contained in:
xsl
2026-11-01 23:30:38 +08:00
parent f5c416dd9d
commit fed52930cc
33 changed files with 3061 additions and 876 deletions
+3 -2
View File
@@ -30,7 +30,7 @@ test('offline camera debug skips identity/profile, never uploads or plays remote
'../../utils/development': { cameraDebugEnabled: () => true },
'../../utils/api': { user() { authReads++; return null; }, analyze() { backendCalls++; throw Error('debug must not upload'); } },
'../../utils/voice': voice,
'../../utils/permissions': { settings: async () => ({}), state: () => undefined, ensure: async () => {} },
'../../utils/permissions': { settings: async () => ({}), state: () => undefined, ensure: async () => {}, cameraSupported: () => true },
'../../utils/image': { compressToBase64: async (photo, p) => {
p.setData({ compressWidth: 1280, compressHeight: 960 }); return 'data:image/jpeg;base64,/9j/';
} }
@@ -89,7 +89,8 @@ test('camera blocks duplicate shots/confirm, cancels late callbacks and submits
const api = { user: () => ({ id: 'a' }), analyze: (photos) => { submitted++; ordered = photos; return new Promise(resolve => { resolver = resolve; }); } };
const voice = { play() {}, stop() {}, VOICE: { step: [], shot: '', analyzing: '', fail: '' } };
const camera = page('pages/camera/camera.js', { '../../utils/api': api, '../../utils/voice': voice,
'../../utils/permissions': { settings: async () => ({ 'scope.camera': true }), state: (s, key) => s[key], ensure: async () => {} },
'../../config': { environment: 'development', nativeCamera: true },
'../../utils/permissions': { settings: async () => ({ 'scope.camera': true }), state: (s, key) => s[key], ensure: async () => {}, cameraSupported: () => true },
'../../utils/image': { compressToBase64: async src => src + '-base64' } }, {
getApp: () => app, my: { createCameraContext(id) { assert.equal(id, 'skin-camera'); return { takePhoto(opts) { captured = opts; calls++; } }; }, showToast() {}, redirectTo() { navigated++; } }
});
+51 -12
View File
@@ -5,26 +5,65 @@ function setup(my) {
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../utils/permissions.js'), 'utf8'), { module, Promise, Error, my });
return module.exports;
}
test('camera and album authorization are separate from login; deny never loops and only explicit settings can restore', async () => {
// 2026-11-01 真机实测(SDK 2.9.4):淘宝用带前缀的 scope.camera(裸名 camera 返回 5022 "Scope:camera信息不存在")。
// 所以先试 scope.camera,失败再退到裸名,两者都失败才拒绝,且拒绝后不再重复弹授权。
test('deny never loops: tries prefixed then bare once, then requires explicit settings', async () => {
let granted = { 'scope.userInfo': true }, calls = [], openCalls = 0;
const permissions = setup({
getSetting({ success }) { success({ authSetting: granted }); },
authorize({ scopes, success, fail }) { calls.push(scopes); if (scopes === 'scope.camera') fail(); else success(); },
authorize({ scopes, success, fail }) { calls.push(scopes); fail({ error: 11, errorMessage: 'no such scope' }); },
openSetting({ success }) { openCalls++; granted['scope.camera'] = true; success({ authSetting: granted }); }
});
await assert.rejects(permissions.ensure('scope.camera'), e => e.permission === 'scope.camera');
await assert.rejects(permissions.ensure('scope.camera'), e => e.permission === 'scope.camera');
assert.deepEqual(calls, ['scope.camera']); assert.equal(openCalls, 0);
await permissions.ensure('scope.album'); assert.deepEqual(calls, ['scope.camera', 'scope.album']);
assert.deepEqual(calls, ['scope.camera', 'camera'], '第二次不再尝试授权');
assert.equal(openCalls, 0);
await permissions.open(); await permissions.ensure('scope.camera');
assert.equal(calls.length, 2); assert.equal(openCalls, 1);
assert.equal(calls.length, 2, '授权设置里开启后不再调用 authorize');
assert.equal(openCalls, 1);
});
test('existing denial does not reauthorize; short-form settings keys work and unknown scope requests on demand', async () => {
test('prefixed scope is used first and succeeds without falling back', async () => {
let calls = [];
const permissions = setup({
getSetting({ success }) { success({ authSetting: {} }); },
authorize({ scopes, success }) { calls.push(scopes); success(); }
});
await permissions.ensure('scope.camera');
assert.deepEqual(calls, ['scope.camera']);
await permissions.ensure('scope.album');
assert.deepEqual(calls, ['scope.camera', 'scope.album']);
});
test('existing denial does not reauthorize; short-form settings keys work', async () => {
let calls = 0;
const permissions = setup({ getSetting({ success }) { success({ authSetting: { camera: false, album: true } }); },
authorize() { calls++; } });
await assert.rejects(permissions.ensure('scope.camera')); await permissions.ensure('scope.album'); assert.equal(calls, 0);
const first = setup({ getSetting({ success }) { success({ authSetting: {} }); },
authorize({ scopes, success }) { assert.equal(scopes, 'scope.camera'); calls++; success(); } });
await first.ensure('scope.camera'); assert.equal(calls, 1);
const permissions = setup({
getSetting({ success }) { success({ authSetting: { camera: false, album: true } }); },
authorize() { calls++; }
});
await assert.rejects(permissions.ensure('scope.camera'));
await permissions.ensure('scope.album');
assert.equal(calls, 0);
});
test('diagnose reports capability and authorization outcomes', async () => {
const permissions = setup({
SDKVersion: '2.9.4',
canIUse() { return true; },
getSetting({ success }) { success({ authSetting: { 'scope.camera': true } }); },
authorize({ scopes, success, fail }) { scopes === 'scope.camera' ? success() : fail({ errorCode: '5022' }); },
createCameraContext() {}, chooseImage() {}, getImageInfo() {}, createCanvasContext() {}, openSetting() {}
});
const info = await permissions.diagnose();
const text = info.join('\n');
assert.match(text, /SDKVersion=2\.9\.4/);
assert.match(text, /createCameraContext: typeof=function canIUse=yes/);
assert.match(text, /authSetting=\{"scope\.camera":true\}/);
assert.match(text, /authorize\(scope\.camera\)=ok/);
});
test('cameraSupported requires the native camera API and canIUse', () => {
assert.equal(setup({ canIUse: () => true }).cameraSupported(), false, '没有 createCameraContext 就不支持');
assert.equal(setup({ canIUse: () => true, createCameraContext() {} }).cameraSupported(), true);
assert.equal(setup({ canIUse: (api) => api !== 'camera', createCameraContext() {} }).cameraSupported(), false);
});
+16
View File
@@ -19,3 +19,19 @@ test('cloud transport initializes once, preserves subpath/auth, caps timeout and
config.transport = 'direct';
await assert.rejects(transport.request('/api/taobao/me', {}), /仅用于本机/);
});
test('聚石塔 tower transport omits domain and does not require HTTPS', async () => {
const config = { transport: 'cloud', cloudType: 'tower', baseUrl: 'http://127.0.0.1:8080', cloudAppId: '', cloudEnv: 'test', basePath: '' };
let options;
const cloud = { init: async ({ env }) => { assert.equal(env, 'test'); return true; },
application: { httpRequest: async input => { options = input; return { success: true, data: { ok: true } }; } } };
const module = { exports: {} };
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../utils/transport.js'), 'utf8'), {
module, Promise, require(name) { return name === '../config' ? config : { default: cloud }; }
});
const transport = module.exports;
const data = await transport.request('/api/taobao/ping', {});
assert.equal(data.ok, true);
assert.equal(options.path, '/api/taobao/ping');
assert.equal(options.exts.domain, undefined);
assert.equal(options.exts.cloudAppId, undefined);
});