118 lines
10 KiB
JavaScript
118 lines
10 KiB
JavaScript
// Real customer workspace, synthetic responses only. No production requests.
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { createRequire } from 'node:module';
|
||
|
|
import { mkdir } from 'node:fs/promises';
|
||
|
|
import { resolve } from 'node:path';
|
||
|
|
import { pathToFileURL } from 'node:url';
|
||
|
|
import { installCustomerFixture, currentUser, households, id } from './fixtures/customer-mobile.mjs';
|
||
|
|
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
|
||
|
|
const { chromium } = require('playwright');
|
||
|
|
const { createServer } = await import(pathToFileURL(resolve('web/node_modules/vite/dist/node/index.js')));
|
||
|
|
const output = resolve('.design-evidence/consent-guidance');
|
||
|
|
await mkdir(output, { recursive: true });
|
||
|
|
const server = await createServer({ configFile: false, root: resolve('web'), oxc: { jsx: { runtime: 'automatic' } }, logLevel: 'error', server: { host: '127.0.0.1', port: 5340, strictPort: true }, plugins: [{
|
||
|
|
name: 'consent-guidance-harness',
|
||
|
|
configureServer(s) { s.middlewares.use('/__consent-test', (_q, r) => { r.setHeader('Content-Type', 'text/html'); r.end('<!doctype html><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><div id="root"></div><script type="module" src="/@id/__consent-module.tsx"></script>'); }); },
|
||
|
|
resolveId(name) { if (name === '__consent-module.tsx') return '\0__consent-module.tsx'; },
|
||
|
|
load(name) { if (name === '\0__consent-module.tsx') return `import React from 'react';import {createRoot} from 'react-dom/client';import {SaaSWorkspace} from '/src/pages/SaaSWorkspace.tsx';import '/src/styles.css';import '/src/cal-theme.css';function App(){const [section,setSection]=React.useState('conversations');return React.createElement(React.Fragment,null,React.createElement('nav',null,['conversations','stories','subjects','privacy','sharing'].map(value=>React.createElement('button',{key:value,onClick:()=>setSection(value)},'测试'+value))),React.createElement(SaaSWorkspace,{accessToken:'synthetic-consent-token',accounts:${JSON.stringify(currentUser.accounts)},section,onSectionChange:setSection,redesignedHome:true,showNavigation:false}));};createRoot(document.getElementById('root')).render(React.createElement(React.StrictMode,null,React.createElement(App)));`; },
|
||
|
|
}] });
|
||
|
|
await server.listen();
|
||
|
|
let browser;
|
||
|
|
try {
|
||
|
|
browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
|
||
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||
|
|
await installCustomerFixture(context, { authenticated: false });
|
||
|
|
const page = await context.newPage(), errors = [], writes = [], grants = new Map();
|
||
|
|
const session = '11111111-1111-4111-8111-111111111111';
|
||
|
|
let failed = false, viewer = false, transcriptDenied = false, recordingState = 'CONSENT_REQUIRED', holdSubject = '', releaseSubject;
|
||
|
|
const response = (route, data, status = 200, code = 'OK') => route.fulfill({ status, json: { ok: status === 200, data, code, message: code, request_id: 'synthetic-consent' } });
|
||
|
|
page.on('pageerror', e => errors.push(e.message));
|
||
|
|
await page.route('**/api/v1/**', async route => {
|
||
|
|
const req = route.request(), path = new URL(req.url()).pathname;
|
||
|
|
if (path === '/api/v1/households') return response(route, { items: households.map(h => viewer ? { ...h, role: 'viewer', capabilities: ['household.read', 'subject.read', 'conversation.read'] } : h) });
|
||
|
|
if (/\/subjects\/[^/]+\/consents$/.test(path)) {
|
||
|
|
const subjectID = path.split('/')[4];
|
||
|
|
if (req.method() === 'POST') {
|
||
|
|
const body = req.postDataJSON(); writes.push({ subjectID, body });
|
||
|
|
const item = { ...body, id: id(999), subject_id: subjectID, actor_user_id: id(100), granted_at: new Date().toISOString(), effective: true, version: 1 };
|
||
|
|
grants.set(subjectID, [...(grants.get(subjectID) ?? []), item]); return response(route, item);
|
||
|
|
}
|
||
|
|
if (subjectID === holdSubject) await new Promise(resolve => { releaseSubject = resolve; });
|
||
|
|
return response(route, { items: grants.get(subjectID) ?? [] }, failed ? 503 : 200, failed ? 'UNAVAILABLE' : 'OK');
|
||
|
|
}
|
||
|
|
if (path.startsWith('/api/v1/memories/')) {
|
||
|
|
const op = path.split('/memories/')[1];
|
||
|
|
if (op === 'calls/query') return response(route, { items: [{ id: session, status: 'ENDED', transcript_state: 'PARTIAL', created_at: new Date().toISOString(), ended_at: new Date().toISOString(), duration_ms: 30000 }], next_cursor: '' });
|
||
|
|
if (op === 'calls/turns') return response(route, { items: [], next_cursor: '' }, transcriptDenied ? 403 : 200, transcriptDenied ? 'TRANSCRIPT_ACCESS_DENIED' : 'OK');
|
||
|
|
if (op === 'calls/recordings') return response(route, { items: [], state: recordingState, can_play: false, can_download: false, playback_ready: true, can_manage: !viewer, requester_id: id(100) });
|
||
|
|
if (op === 'query') return response(route, { items: [], mutations: [], next_cursor: '', can_write: false, can_manage_access: false });
|
||
|
|
if (op === 'sync/status') return response(route, { memory_revision: 0, viking_visible_revision: 0, pending_count: 0, blocked_count: 0, upload_pending_count: 0, extracting_count: 0, sync_state: 'EMPTY' });
|
||
|
|
throw new Error(`Unexpected memory operation ${op}`);
|
||
|
|
}
|
||
|
|
if (path.endsWith('/events') || path.endsWith('/events/history') || path.endsWith('/firmware') || path.endsWith('/commands') || path.endsWith('/home-overview')) return response(route, { items: [] });
|
||
|
|
return route.fallback();
|
||
|
|
});
|
||
|
|
const guidance = page.getByRole('region', { name: '当前功能的授权提示' });
|
||
|
|
await page.goto('http://127.0.0.1:5340/__consent-test');
|
||
|
|
await guidance.getByText(/目前无法保存新聊天的文字/).waitFor();
|
||
|
|
await page.screenshot({ path: resolve(output, 'owner-shortcuts.png'), fullPage: true });
|
||
|
|
await page.getByText('文字不完整或未保存', { exact: false }).first().waitFor();
|
||
|
|
assert.equal(await page.getByText('已保存部分文字', { exact: true }).count(), 0);
|
||
|
|
await page.getByRole('button', { name: /30 秒/ }).click();
|
||
|
|
await page.getByText(/如果聊天时未授权,文字不会保存/).waitFor();
|
||
|
|
await page.getByRole('region', { name: '本次聊天录音' }).getByRole('button', { name: '去授权保存原始录音' }).waitFor();
|
||
|
|
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
|
||
|
|
let dialog = page.getByRole('dialog');
|
||
|
|
assert(await dialog.getByRole('button', { name: '确认授权', exact: true }).isDisabled());
|
||
|
|
await page.waitForTimeout(300); // Capture the settled modal after its entrance animation.
|
||
|
|
await page.screenshot({ path: resolve(output, 'grant-dialog.png'), fullPage: true });
|
||
|
|
await dialog.getByRole('button', { name: /^取\s*消$/ }).click();
|
||
|
|
assert.equal(writes.length, 0);
|
||
|
|
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
|
||
|
|
dialog = page.getByRole('dialog');
|
||
|
|
await dialog.getByRole('checkbox').check();
|
||
|
|
await dialog.getByRole('button', { name: '确认授权', exact: true }).click();
|
||
|
|
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor({ state: 'hidden' });
|
||
|
|
assert.equal(writes.length, 1); assert.equal(writes[0].subjectID, id(400));
|
||
|
|
assert.equal(writes[0].body.type, 'transcript_storage'); assert.equal(writes[0].body.policy_version, '2026-09'); assert.match(writes[0].body.notice_digest, /^[0-9a-f]{64}$/);
|
||
|
|
assert.equal(grants.get(id(400)).length, 1);
|
||
|
|
await page.getByRole('button', { name: '测试stories', exact: true }).click();
|
||
|
|
await guidance.getByText(/无法在新聊天中使用长期记忆/).waitFor();
|
||
|
|
await guidance.getByText(/不会自动整理为故事候选或摘要/).waitFor();
|
||
|
|
await page.getByText(/当前账号只有查看权限/).waitFor();
|
||
|
|
await guidance.getByRole('button', { name: '去授权个性化记忆', exact: true }).click();
|
||
|
|
await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click();
|
||
|
|
await page.getByRole('button', { name: '测试conversations', exact: true }).click();
|
||
|
|
await page.getByRole('dialog').waitFor({ state: 'hidden' });
|
||
|
|
holdSubject = id(401);
|
||
|
|
await page.getByRole('combobox', { name: '选择长者', exact: true }).selectOption(id(401));
|
||
|
|
await page.getByText('正在核对数据授权…', { exact: true }).waitFor();
|
||
|
|
assert.equal(await page.getByRole('button', { name: '去授权保存文字记录', exact: true }).count(), 0);
|
||
|
|
while (!releaseSubject) await new Promise(resolve => setTimeout(resolve, 20));
|
||
|
|
holdSubject = ''; releaseSubject();
|
||
|
|
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor();
|
||
|
|
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
|
||
|
|
await page.getByRole('dialog').getByText('陈建国', { exact: true }).waitFor();
|
||
|
|
await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click();
|
||
|
|
failed = true; await page.reload();
|
||
|
|
await page.getByText('暂时无法读取授权状态,请重试后核对。', { exact: false }).waitFor();
|
||
|
|
assert.equal(await page.getByText(/因为尚未授权“保存文字记录”/).count(), 0);
|
||
|
|
failed = false; await page.getByRole('button', { name: '重试授权状态', exact: true }).click();
|
||
|
|
await guidance.waitFor();
|
||
|
|
viewer = true; grants.clear(); recordingState = 'READY'; transcriptDenied = true;
|
||
|
|
await page.reload();
|
||
|
|
await guidance.getByText(/当前账号不能新增数据授权/).waitFor();
|
||
|
|
assert.equal(await guidance.getByRole('button', { name: /^去授权/ }).count(), 0);
|
||
|
|
await page.getByRole('button', { name: /30 秒/ }).click();
|
||
|
|
await page.getByText(/查看设备聊天文字需要/).waitFor();
|
||
|
|
await page.getByText(/当前账号没有播放权限/).waitFor();
|
||
|
|
await page.getByText(/当前账号没有下载权限/).waitFor();
|
||
|
|
assert.equal(await page.getByRole('button', { name: /^授权本账号/ }).count(), 0);
|
||
|
|
await page.screenshot({ path: resolve(output, 'desktop.png'), fullPage: true });
|
||
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
||
|
|
await page.screenshot({ path: resolve(output, 'mobile.png'), fullPage: true });
|
||
|
|
assert((await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)) < 3);
|
||
|
|
assert.deepEqual(errors, []);
|
||
|
|
console.log('PASS: missing consent and historical gaps; explicit single-item grant/cancel; dependent features; subject isolation; fetch failure/retry; viewer restrictions; transcript/recording access instructions; mobile layout');
|
||
|
|
} finally { await browser?.close(); await server.close(); }
|