121 lines
7.2 KiB
JavaScript
121 lines
7.2 KiB
JavaScript
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;
|
|||
|
|
}
|
|||
|
|
});
|