Files
story/web/test/home-page-render.test.mjs
T

121 lines
7.2 KiB
JavaScript
Raw Normal View History

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;
}
});