Files
story/web/test/home-page-render.test.mjs
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

121 lines
7.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const webRoot = fileURLToPath(new URL('..', import.meta.url));
// Allows the copied source to use installed dependencies without copying them
// or writing to the original checkout. Normal repo tests need no override.
const dependencyRoot = process.env.HOME_TEST_WEB_ROOT || webRoot;
const requireDependency = createRequire(path.join(dependencyRoot, 'package.json'));
const ts = requireDependency('typescript');
const React = requireDependency('react');
const { renderToStaticMarkup } = requireDependency('react-dom/server');
const modules = new Map();
function loadSource(file) {
if (modules.has(file)) return modules.get(file).exports;
const module = { exports: {} };
modules.set(file, module);
const compiled = ts.transpileModule(fs.readFileSync(file, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2022, esModuleInterop: true } });
const requireSource = name => {
if (name.endsWith('.css') || name.startsWith('@fontsource')) return {};
// Icon drawings do not affect these state/copy assertions. Avoid loading
// the vendor's browser-oriented CJS entry in Node's ESM test runner.
if (name === '@phosphor-icons/react') return new Proxy({}, { get: () => () => React.createElement('svg', { 'aria-hidden': true }) });
if (!name.startsWith('.')) return requireDependency(name);
const relative = path.resolve(path.dirname(file), name);
const fallback = path.join(dependencyRoot, path.relative(webRoot, relative));
const target = [relative + '.ts', relative + '.tsx', fallback + '.ts', fallback + '.tsx'].find(candidate => fs.existsSync(candidate));
if (!target) throw new Error(`Missing local test source: ${name}`);
return loadSource(target);
};
new Function('require', 'module', 'exports', compiled.outputText)(requireSource, module, module.exports);
return module.exports;
}
const { CustomerHomePage } = loadSource(path.join(webRoot, 'src/components/home/CustomerHomePage.tsx'));
const check = (code, status, blocking, title, detail, action) => ({ code, status, blocking, title, detail, action });
const ready = { subject_id: 'elder-a', ready: true, evaluated_at: '2026-10-05T00:00:00Z', checks: [check('SUBJECT_ACTIVE', 'ready', true, '长者档案已启用', '档案可用于通话'), check('VOICE_CONSENT', 'ready', true, '语音通话授权已具备', '语音授权已具备')], devices: [], start_time_checks: [] };
const props = {
households: [{ id: 'household-a', name: '测试家庭' }], householdID: 'household-a',
subjects: [{ id: 'elder-a', household_id: 'household-a', preferred_name: '测试长者', timezone: 'Asia/Shanghai' }],
subject: { id: 'elder-a', household_id: 'household-a', preferred_name: '测试长者', timezone: 'Asia/Shanghai' },
devices: [{ id: 'device-a', subject_id: 'elder-a' }], presence: { 'device-a': { online: true } },
conversations: [], insights: {}, stories: [], privacyRequests: [], readiness: ready,
now: new Date('2026-10-05T00:00:00Z'), onHousehold() {}, onSubject() {}, onNavigate() {}, onRetry() {}, onLogout() {},
};
const render = extra => renderToStaticMarkup(React.createElement(CustomerHomePage, { ...props, ...extra })).replace(/<[^>]+>/g, '');
test('home shows the actual missing consent and completed checks, not online equals ready', () => {
const checks = [ready.checks[0], check('VOICE_CONSENT', 'blocked', true, '缺少语音通话授权', '核对语音通话授权凭证', 'MANAGE_CONSENTS')];
const text = render({ readiness: { ...ready, ready: false, checks } });
assert.match(text, /当前下一步缺少语音通话授权/);
assert.match(text, /核对语音通话授权凭证/);
assert.match(text, /已完成 1 项 · 待完成 1 项/);
assert.doesNotMatch(text, /通话准备项已完成/);
});
test('a readiness 502 leaves the navigation available and does not claim missing setup', () => {
const text = render({ readinessState: 'error', readinessError: '准备服务暂时不可用(HTTP 502)' });
assert.match(text, /陪伴准备状态待确认/);
assert.match(text, /重新核对准备状态/);
assert.match(text, /设置与权限/);
assert.doesNotMatch(text, /查看准备清单|通话准备项已完成|待完成 \d+ 项/);
});
test('overview failure stays local and is not presented as zero chats or no history', () => {
const text = render({ useOverview: true, overviewError: '陪伴近况暂时不可用(HTTP 502)' });
assert.match(text, /通话准备项已完成/);
assert.match(text, /陪伴近况暂时未能读取/);
assert.match(text, /重新加载陪伴近况/);
assert.doesNotMatch(text, /0 次聊天|还没有聊天记录|还没有新的家庭动态/);
});
test('overview loading is distinct from an empty conversation history', () => {
const text = render({ useOverview: true, overviewLoading: true });
assert.match(text, /正在载入陪伴近况/);
assert.doesNotMatch(text, /0 次聊天|还没有聊天记录/);
});
test('changing elder does not show the previous elder data requests or preparation', () => {
const text = render({
subject: { ...props.subject, id: 'elder-b', preferred_name: '另一位长者' },
useOverview: true, overviewLoading: true,
privacyRequests: [{ id: 'request-a', subject_id: 'elder-a', status: 'EXECUTING', request_type: 'ACCESS' }],
});
assert.match(text, /另一位长者的陪伴近况/);
assert.match(text, /陪伴准备状态待确认/);
assert.doesNotMatch(text, /通话准备项已完成|1项数据申请正在处理中/);
});
test('nonblocking suggestions say they do not block conversation', () => {
const text = render({ readiness: { ...ready, checks: [...ready.checks, check('CONFIG_SYNC', 'attention', false, '设备配置待同步', '设备可在下次连接时同步', 'OPEN_DEVICES')] } });
assert.match(text, /建议检查 · 不阻止通话/);
assert.match(text, /已完成 2 项 · 待完成 0 项 · 建议 1 项/);
});
test('home displays latest time in Beijing despite browser or historical profile timezone', () => {
const previous = process.env.TZ;
process.env.TZ = 'America/Los_Angeles';
try {
const text = render({
subject: { ...props.subject, timezone: 'America/Los_Angeles' }, useOverview: true,
overview: {
subject_id: 'elder-a', timezone: 'America/Los_Angeles', week_start: '2026-10-05', week_end: '2026-10-12',
conversation_count: 1, total_duration_ms: 12000,
daily: [{ date: '2026-10-05', conversation_count: 1, duration_ms: 12000 }],
latest_conversation: { id: 'synthetic-call', source: 'call_memory', status: 'FAILED', started_at: '2026-10-05T12:16:08Z', ended_at: '2026-10-05T12:16:20Z', duration_ms: 12000, summary_state: 'NO_SUMMARY' },
},
});
assert.match(text, /20:16(北京时间)/);
assert.match(text, /12秒/);
assert.match(text, /设备通话须已结束并有有效文字/);
assert.match(text, /服务用量还包含其他会话记录/);
assert.doesNotMatch(text, /05:16/);
assert.match(text, /通话准备项已完成/);
} finally {
if (previous === undefined) delete process.env.TZ; else process.env.TZ = previous;
}
});