feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// Actual portal components and routing, deterministic dates and synthetic API
|
||||
// metadata only. Backend aggregation is tested separately against local PG.
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { installCustomerFixture, households, subjects, devices, 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 work=await mkdtemp(join(tmpdir(),'story-usage-boundaries-'));
|
||||
const output=resolve(process.env.USAGE_BOUNDARY_OUTPUT ?? '../.local-evidence/usage-chat-boundaries-20261006/browser');
|
||||
await mkdir(join(work,'empty-env')); await mkdir(output,{recursive:true});
|
||||
const server=await createServer({configFile:false,root:resolve('web'),envDir:join(work,'empty-env'),cacheDir:join(work,'vite-cache'),oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:0},plugins:[{
|
||||
name:'usage-boundary-fixture',
|
||||
configureServer(vite){vite.middlewares.use('/__usage-boundary',(_q,response)=>{response.setHeader('Content-Type','text/html');response.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/__usage-boundary.tsx"></script>');});},
|
||||
resolveId(name){if(name==='__usage-boundary.tsx')return '\0__usage-boundary.tsx';},
|
||||
load(name){if(name==='\0__usage-boundary.tsx')return `import React from 'react';import {createRoot} from 'react-dom/client';import {AuthProvider} from '/src/auth/useAuth.ts';import {CustomerPortal} from '/src/pages/CustomerPortal.tsx';import '/src/styles.css';import '/src/cal-theme.css';import '/src/customer-responsive.css';createRoot(document.getElementById('root')).render(React.createElement(AuthProvider,null,React.createElement(CustomerPortal)));`;},
|
||||
}]});
|
||||
const now='2026-10-06T04:00:00.000Z', familyA=households[0],familyB=households[1];
|
||||
const roster=[...subjects.slice(0,2),{...subjects[2],id:id(410),household_id:familyB.id,preferred_name:'合成空家庭长者'}];
|
||||
const fleet=devices.slice(0,2).map((d,index)=>({...d,model:`合成设备${index+1}`}));
|
||||
const iso=ms=>new Date(ms).toISOString();
|
||||
const beijingDay=at=>iso(Date.parse(at)+8*3600000).slice(0,10);
|
||||
const mainStarts={30:'2026-09-06T16:00:00.000Z',90:'2026-07-08T16:00:00.000Z',365:'2025-10-06T16:00:00.000Z'};
|
||||
const usageRows=[];
|
||||
function addUsage(at,deviceIndex){const device=fleet[deviceIndex],duration=(deviceIndex+1)*60000;usageRows.push({id:id(2100+usageRows.length),household_id:familyA.id,device_id:device.id,subject_name:device.subject_name,device_name:device.model,started_at:at,ended_at:iso(Date.parse(at)+duration),duration_ms:duration,status:'COMPLETED'});}
|
||||
addUsage('2026-10-05T15:59:59.999Z',0);addUsage('2026-10-05T16:00:00.000Z',1);
|
||||
for(let i=0;i<23;i++)addUsage(iso(Date.parse('2026-10-05T10:00:00Z')-i*60000),i%2);
|
||||
for(const [days,deviceIndex] of [[30,0],[90,1],[365,0]]){addUsage(iso(Date.parse(mainStarts[days])-1),1-deviceIndex);addUsage(mainStarts[days],deviceIndex);}
|
||||
addUsage(now,0);addUsage(iso(Date.parse(now)+1),1);
|
||||
const legacyRows=[];
|
||||
function addLegacy(at,subjectID=subjects[0].id){legacyRows.push({id:id(3000+legacyRows.length),subject_id:subjectID,device_id:fleet[0].id,created_at:at,updated_at:at,started_at:at,ended_at:iso(Date.parse(at)+60000),duration_ms:60000,status:'FAILED',engine_mode:'s2s_hybrid',runtime_type:'legacy_voicechat',retention_mode:'none'});}
|
||||
for(let i=0;i<35;i++)addLegacy(iso(Date.parse('2026-10-06T03:00:00Z')-i*60000));
|
||||
for(const at of ['2026-10-05T15:59:59.999Z','2026-10-05T16:00:00.000Z','2026-10-06T15:59:59.999Z','2026-10-06T16:00:00.000Z'])addLegacy(at);
|
||||
for(let i=0;i<16;i++)addLegacy(iso(Date.parse('2026-10-06T02:00:00Z')-i*60000),subjects[1].id);
|
||||
const modernRows=roster.flatMap((s,index)=>Array.from({length:index===0?55:index===1?21:0},(_,i)=>({id:id(4000+index*100+i),elder_id:s.id,business_session_id:id(4000+index*100+i),created_at:iso(Date.parse('2026-10-06T03:00:00Z')-i*60000),status:'ENDED',transcript_state:'EMPTY',duration_ms:60000})));
|
||||
const reads=[],mutations=[],external=[],textRequests=[],errors=[],results=[];
|
||||
let browser,origin;
|
||||
function aggregate(household,params){
|
||||
const deviceID=params.get('device_id'),from=Date.parse(params.get('from')),to=Date.parse(params.get('to'));
|
||||
if(deviceID)assert(fleet.some(d=>d.id===deviceID&&d.household_id===household),'foreign device reached usage request');
|
||||
const rows=usageRows.filter(r=>r.household_id===household&&(!deviceID||r.device_id===deviceID)&&Date.parse(r.started_at)>=from&&Date.parse(r.started_at)<to).sort((a,b)=>b.started_at.localeCompare(a.started_at));
|
||||
const daily=new Map(),byDevice=new Map();
|
||||
for(const r of rows){const date=beijingDay(r.started_at),d=daily.get(date)??{date,duration_ms:0,conversation_count:0,cost_micros:0};d.duration_ms+=r.duration_ms;d.conversation_count++;daily.set(date,d);const item=byDevice.get(r.device_id)??{id:r.device_id,name:r.device_name,conversation_count:0,duration_ms:0,cost_micros:0};item.duration_ms+=r.duration_ms;item.conversation_count++;byDevice.set(r.device_id,item);}
|
||||
const total=rows.reduce((sum,r)=>sum+r.duration_ms,0);
|
||||
return{from:params.get('from'),to:params.get('to'),timezone:'Asia/Shanghai',scope:{household_id:household,household_name:households.find(h=>h.id===household).name,...(deviceID?{device_id:deviceID}:{})},total_duration_ms:total,average_duration_ms:rows.length?Math.floor(total/rows.length):0,conversation_count:rows.length,daily:[...daily.values()].sort((a,b)=>a.date.localeCompare(b.date)),devices:[...byDevice.values()],recent_conversations:rows.slice(0,20),currency:'CNY',cost_available:false};
|
||||
}
|
||||
async function start(width,section='billing',instant=now,timezoneId='America/Los_Angeles'){
|
||||
const context=await browser.newContext({viewport:{width,height:844},timezoneId,serviceWorkers:'block'});
|
||||
const unexpected=await installCustomerFixture(context);
|
||||
const page=await context.newPage();page.setDefaultTimeout(10000);await page.clock.setFixedTime(new Date(instant));page.on('pageerror',e=>errors.push(e.message));
|
||||
const control={holdLegacyMore:false,holdUsage:false,pending:[],usageAnswers:[]};
|
||||
await page.route('**/*',async route=>{
|
||||
const request=route.request(),url=new URL(request.url()),path=url.pathname;
|
||||
if(url.origin!==origin){external.push(url.origin+path);return route.abort();}
|
||||
if(!path.startsWith('/api/'))return route.continue();
|
||||
const answer=data=>route.fulfill({json:{data,request_id:'synthetic-boundary'}});
|
||||
if(path==='/api/v1/memories/calls/query'&&request.method()==='POST'){
|
||||
const body=request.postDataJSON(),offset=body.cursor?Number(body.cursor.split(':').at(-1)):0;
|
||||
reads.push({kind:'modern',...body});const all=modernRows.filter(r=>r.elder_id===body.elder_id),limit=body.limit;
|
||||
return answer({items:all.slice(offset,offset+limit),next_cursor:all.length>offset+limit?`${body.elder_id}:${offset+limit}`:''});
|
||||
}
|
||||
if(/\/(turns|summaries)$|\/calls\/turns$/.test(path)){textRequests.push(path);return route.abort();}
|
||||
if(request.method()!=='GET'){mutations.push(request.method()+' '+path);return route.abort();}
|
||||
if(path==='/api/v1/devices')return answer({items:fleet});
|
||||
const subjectMatch=path.match(/\/households\/([^/]+)\/subjects$/);
|
||||
if(subjectMatch)return answer({items:roster.filter(s=>s.household_id===subjectMatch[1])});
|
||||
const usage=path.match(/\/households\/([^/]+)\/usage-overview$/);
|
||||
if(usage){
|
||||
reads.push({kind:'usage',householdID:usage[1],...Object.fromEntries(url.searchParams)});
|
||||
const result=aggregate(usage[1],url.searchParams);control.usageAnswers.push(result);
|
||||
if(control.holdUsage){control.holdUsage=false;await new Promise(release=>control.pending.push(release));}
|
||||
return answer(result);
|
||||
}
|
||||
const legacy=path.match(/\/subjects\/([^/]+)\/conversations$/);
|
||||
if(legacy){
|
||||
const params=url.searchParams,offset=params.has('cursor')?Number(params.get('cursor').split(':').at(-1)):0,limit=Number(params.get('limit'));
|
||||
reads.push({kind:'legacy',subjectID:legacy[1],...Object.fromEntries(params)});
|
||||
const all=legacyRows.filter(r=>r.subject_id===legacy[1]&&(!params.has('from')||Date.parse(r.created_at)>=Date.parse(params.get('from')))&&(!params.has('to')||Date.parse(r.created_at)<Date.parse(params.get('to')))&&(!params.has('status')||r.status===params.get('status'))&&(!params.has('engine_mode')||r.engine_mode===params.get('engine_mode'))).sort((a,b)=>b.created_at.localeCompare(a.created_at));
|
||||
if(control.holdLegacyMore&¶ms.has('cursor')){control.holdLegacyMore=false;await new Promise(release=>control.pending.push(release));}
|
||||
return answer({items:all.slice(offset,offset+limit),next_cursor:all.length>offset+limit?`${legacy[1]}:${offset+limit}`:''});
|
||||
}
|
||||
if(/\/(stories|memories|facts|events\/history)$/.test(path))return answer({items:[]});
|
||||
if(path.endsWith('/home-overview'))return answer({daily:[],conversation_count:0,total_duration_ms:0,week_start:'2026-10-05',week_end:'2026-10-12'});
|
||||
return route.fallback();
|
||||
});
|
||||
await page.goto(origin+'/__usage-boundary?section='+section);
|
||||
return{page,context,control,unexpected};
|
||||
}
|
||||
const usageDevice=page=>page.locator('.cp-usage-filters label select');
|
||||
const usagePeriod=page=>page.getByRole('combobox',{name:'用量时间范围',exact:true});
|
||||
async function usageReady(page,count){await page.locator('.cp-usage-metrics').getByText(String(count)+' 次',{exact:true}).waitFor();}
|
||||
async function firstPage(page,label){await page.locator(`[aria-label="${label}第 1 页"][aria-current="page"]`).waitFor({state:'attached'});}
|
||||
async function openEarly(page){
|
||||
// Wait for the initial response before opening this native details element.
|
||||
await page.locator('.conversation-filter__summary').getByText(/^已加载 20 条/).waitFor({state:'attached'});
|
||||
const summary=page.locator('summary').filter({hasText:'查看早期聊天记录'});
|
||||
if(!await summary.evaluate(element=>element.parentElement.open))await summary.click();
|
||||
await page.getByRole('navigation',{name:'聊天时间线分页',exact:true}).waitFor();
|
||||
}
|
||||
async function nav(page,label,width){if(width<900)await page.getByRole('button',{name:'打开导航菜单',exact:true}).click();await page.getByRole('link',{name:label,exact:true}).click();}
|
||||
async function switchFamily(page,width){if(width<900)await page.getByRole('button',{name:'打开导航菜单',exact:true}).click();await page.getByRole('button',{name:'切换家庭',exact:true}).click();await page.getByRole('menuitem',{name:familyB.name,exact:true}).click();if(width<900)await page.locator('.ch-mobile-drawer .ant-drawer-close').click();}
|
||||
async function check(name,run){try{await run();results.push({name,status:'passed'});}catch(error){results.push({name,status:'failed',error:error.message});}console.log(JSON.stringify(results.at(-1)));}
|
||||
async function withPage(width,section,run,instant=now,timezone){const state=await start(width,section,instant,timezone);try{await run(state);}catch(error){await state.page.screenshot({path:join(output,`failure-${results.length}-${width}.png`)});await writeFile(join(output,`failure-${results.length}-${width}.txt`),await state.page.locator('body').innerText());throw error;}finally{state.control.pending.splice(0).forEach(release=>release());await state.context.close();assert.deepEqual(state.unexpected,[]);}}
|
||||
try{
|
||||
await server.listen();origin=`http://127.0.0.1:${server.httpServer.address().port}`;
|
||||
browser=await chromium.launch({headless:true,...(process.env.CHROME_EXECUTABLE?{executablePath:process.env.CHROME_EXECUTABLE}:{})});
|
||||
for(const width of [1188,389]){
|
||||
await check(`${width}px usage: 30/90/365 boundaries, two independent devices and all-device totals, paging/filter reset`,()=>withPage(width,'billing',async({page,control})=>{
|
||||
await usageReady(page,26);assert.match(await page.locator('.cp-usage-metrics').innerText(),/38\s*分钟/);
|
||||
assert.match(await page.locator('.cp-table tbody tr').nth(0).innerText(),/10\/6.*00:00/);
|
||||
assert.match(await page.locator('.cp-table tbody tr').nth(1).innerText(),/10\/5.*23:59/);
|
||||
await page.getByRole('button',{name:'最近通话下一页',exact:true}).click();
|
||||
assert.equal(await page.locator('.cp-table tbody tr').count(),10);
|
||||
await usageDevice(page).selectOption(fleet[0].id);await usageReady(page,14);await firstPage(page,'最近通话');
|
||||
assert.match(await page.locator('.cp-usage-metrics').innerText(),/14\s*分钟/);
|
||||
assert((await page.locator('.cp-table tbody').innerText()).includes('合成设备1'));assert(!(await page.locator('.cp-table tbody').innerText()).includes('合成设备2'));
|
||||
await page.getByRole('button',{name:'最近通话下一页',exact:true}).click();assert.equal(await page.locator('.cp-table tbody tr').count(),4);
|
||||
await usageDevice(page).selectOption(fleet[1].id);await usageReady(page,12);await firstPage(page,'最近通话');
|
||||
assert.match(await page.locator('.cp-usage-metrics').innerText(),/24\s*分钟/);
|
||||
await usageDevice(page).selectOption('');await usageReady(page,26);await firstPage(page,'最近通话');
|
||||
const answer=control.usageAnswers.at(-1);assert.equal(answer.total_duration_ms,2280000);assert.equal(answer.devices.length,2);assert.equal(answer.recent_conversations.length,20);
|
||||
assert.equal(answer.daily.find(d=>d.date==='2026-10-06').conversation_count,1,'exact midnight starts the new Beijing date');
|
||||
assert.equal(answer.daily.find(d=>d.date==='2026-10-05').conversation_count,24,'pre-midnight session belongs to previous date');
|
||||
const periodCounts={30:26,90:28,365:30};
|
||||
for(const days of [90,365,30]){await page.getByRole('button',{name:'最近通话下一页',exact:true}).click();await usagePeriod(page).selectOption(String(days));await usageReady(page,periodCounts[days]);await firstPage(page,'最近通话');const request=reads.filter(r=>r.kind==='usage').at(-1);assert.equal(request.from,mainStarts[days]);assert.equal(request.to,now);assert.equal(request.timezone,'Asia/Shanghai');}
|
||||
await page.getByRole('combobox',{name:'最近通话每页条数',exact:true}).selectOption('5');await firstPage(page,'最近通话');assert.equal(await page.locator('.cp-table tbody tr').count(),5);
|
||||
if(width>=640){await page.getByRole('textbox',{name:'最近通话跳转页码',exact:true}).fill('4');await page.getByRole('button',{name:'最近通话跳转',exact:true}).click();}
|
||||
else for(let i=0;i<3;i++)await page.getByRole('button',{name:'最近通话下一页',exact:true}).click();
|
||||
assert.equal(await page.locator('[aria-current=page][aria-label="最近通话第 4 页"]').count(),1);
|
||||
await usagePeriod(page).selectOption('90');await usageReady(page,28);await firstPage(page,'最近通话');
|
||||
await page.screenshot({path:join(output,`usage-boundaries-${width}.png`)});
|
||||
}));
|
||||
await check(`${width}px usage: empty family clears selected device/results and ignores a delayed old-family response`,()=>withPage(width,'billing',async({page,control})=>{
|
||||
await usageReady(page,26);await usageDevice(page).selectOption(fleet[0].id);await usageReady(page,14);
|
||||
control.holdUsage=true;await page.getByRole('button',{name:'刷新',exact:true}).click();await page.waitForFunction(()=>document.querySelector('.cp-usage-metrics')?.textContent.includes('—'));
|
||||
await switchFamily(page,width);await usageReady(page,0);assert.equal(await usageDevice(page).inputValue(),'');assert.equal(await usageDevice(page).locator('option').count(),1);
|
||||
control.pending.splice(0).forEach(release=>release());await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
|
||||
await usageReady(page,0);assert.equal(await page.locator('.cp-table tbody tr').count(),0);assert.equal(await page.getByRole('group',{name:'每日聊天用量',exact:true}).count(),0);
|
||||
assert(!reads.filter(r=>r.kind==='usage'&&r.householdID===familyB.id).some(r=>r.device_id));await page.screenshot({path:join(output,`empty-family-${width}.png`)});
|
||||
}));
|
||||
await check(`${width}px device chats: API cursor, local page/size, elder switch, refresh and empty family reset`,()=>withPage(width,'conversations',async({page})=>{
|
||||
const pager=page.getByRole('navigation',{name:'聊天记录分页',exact:true});await pager.getByText('30',{exact:true}).waitFor();assert.equal(await page.locator('.cp-memory-row').count(),10);
|
||||
await pager.getByRole('button',{name:'聊天记录下一页',exact:true}).click();await page.getByRole('combobox',{name:'聊天记录每页条数',exact:true}).selectOption('5');await firstPage(page,'聊天记录');assert.equal(await page.locator('.cp-memory-row').count(),5);
|
||||
await pager.getByRole('button',{name:/^读取后续记录/}).click();await pager.getByText('55',{exact:true}).waitFor();assert.equal(await pager.getByRole('button',{name:/^读取后续记录/}).count(),0);
|
||||
await pager.getByRole('button',{name:'聊天记录下一页',exact:true}).click();await page.locator('.cp-subject-picker .cp-elder-roster button').filter({hasText:subjects[1].preferred_name}).click();await pager.getByText('21',{exact:true}).waitFor();await firstPage(page,'聊天记录');
|
||||
await pager.getByRole('button',{name:'聊天记录下一页',exact:true}).click();await page.getByRole('button',{name:'刷新记录',exact:true}).click();await firstPage(page,'聊天记录');
|
||||
await switchFamily(page,width);await page.locator('.cp-elder-roster button[aria-pressed=true]').filter({hasText:roster[2].preferred_name}).waitFor();await page.locator('section[aria-label="设备聊天记录"]').getByRole('heading',{name:'还没有聊天记录',exact:true}).waitFor();assert.equal(await page.locator('.cp-memory-row').count(),0);assert.equal(await pager.count(),0);
|
||||
}));
|
||||
await check(`${width}px early chats: inclusive Beijing date inputs, exclusive next midnight, paging/filter reset`,()=>withPage(width,'conversations',async({page})=>{
|
||||
await openEarly(page);const pager=page.getByRole('navigation',{name:'聊天时间线分页',exact:true});await pager.getByText('20',{exact:true}).waitFor();
|
||||
await pager.getByRole('button',{name:/^读取后续记录/}).click();await pager.getByText('39',{exact:true}).waitFor();await pager.getByRole('button',{name:'聊天时间线下一页',exact:true}).click();
|
||||
await page.getByRole('button',{name:'筛选日期与状态',exact:true}).click();const dates=page.locator('.conversation-filter input[type=date]');await dates.nth(0).fill('2026-10-06');await dates.nth(1).fill('2026-10-06');await pager.getByText('20',{exact:true}).waitFor();await firstPage(page,'聊天时间线');
|
||||
const query=reads.filter(r=>r.kind==='legacy').at(-1);assert.equal(query.from,'2026-10-05T16:00:00.000Z');assert.equal(query.to,'2026-10-06T16:00:00.000Z');assert(!query.cursor);
|
||||
await pager.getByRole('button',{name:/^读取后续记录/}).click();await pager.getByText('37',{exact:true}).waitFor();
|
||||
const expected=legacyRows.filter(r=>r.subject_id===subjects[0].id&&r.created_at>=query.from&&r.created_at<query.to);assert.equal(expected.length,37);assert(!expected.some(r=>r.created_at==='2026-10-06T16:00:00.000Z'));
|
||||
await pager.getByRole('button',{name:'聊天时间线下一页',exact:true}).click();await page.locator('.conversation-filter').getByLabel(/^通话状态/).selectOption('ACTIVE');await page.locator('.conversation-filter__summary').getByText('已加载 0 条',{exact:true}).waitFor();assert.equal(await page.locator('.conversation-row').count(),0);
|
||||
await page.getByRole('button',{name:'清除筛选',exact:true}).click();await pager.getByText('20',{exact:true}).waitFor();await firstPage(page,'聊天时间线');assert.deepEqual(await dates.evaluateAll(nodes=>nodes.map(n=>n.value)),['','']);
|
||||
await page.screenshot({path:join(output,`early-chat-dates-${width}.png`)});
|
||||
}));
|
||||
await check(`${width}px early chats: delayed cursor page cannot leak into an empty different family`,()=>withPage(width,'conversations',async({page,control})=>{
|
||||
await openEarly(page);const pager=page.getByRole('navigation',{name:'聊天时间线分页',exact:true});await pager.getByText('20',{exact:true}).waitFor();
|
||||
control.holdLegacyMore=true;await pager.getByRole('button',{name:/^读取后续记录/}).click();
|
||||
await page.waitForFunction(()=>document.querySelector('nav[aria-label="聊天时间线分页"]')?.getAttribute('aria-busy')==='true');assert.equal(control.pending.length,1);
|
||||
await switchFamily(page,width);await page.locator('.cp-elder-roster button[aria-pressed=true]').filter({hasText:roster[2].preferred_name}).waitFor();await page.locator('.conversation-filter__summary').getByText('已加载 0 条',{exact:true}).waitFor({state:'attached'});
|
||||
const delivered=page.waitForResponse(response=>/\/subjects\/[^/]+\/conversations/.test(response.url())&&new URL(response.url()).searchParams.has('cursor'));
|
||||
control.pending.splice(0).forEach(release=>release());await (await delivered).finished();
|
||||
await page.waitForFunction(()=>!document.querySelector('.conversation-filter select:disabled'));
|
||||
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
|
||||
assert.equal(await page.locator('.conversation-row').count(),0,'old-family cursor response revived conversation rows');
|
||||
assert.match(await page.locator('.conversation-filter__summary').innerText(),/已加载 0 条/);
|
||||
}));
|
||||
}
|
||||
for(const [instant,starts] of [
|
||||
['2026-10-05T15:59:59.999Z',{30:'2026-09-05T16:00:00.000Z',90:'2026-07-07T16:00:00.000Z',365:'2025-10-05T16:00:00.000Z'}],
|
||||
['2026-10-05T16:00:00.000Z',mainStarts],['2026-10-05T16:00:00.001Z',mainStarts],
|
||||
])await check(`Beijing midnight ${instant}: actual 30/90/365 request bounds in UTC browser`,()=>withPage(1188,'billing',async({page,control})=>{
|
||||
for(const days of [30,90,365]){await usagePeriod(page).waitFor();if(days!==30)await usagePeriod(page).selectOption(String(days));await page.waitForFunction(()=>!document.querySelector('.cp-usage-filters button')?.disabled);const response=control.usageAnswers.at(-1);assert.equal(response.from,starts[days]);assert.equal(response.to,instant);assert.equal(response.timezone,'Asia/Shanghai');}
|
||||
},instant,'Etc/UTC'));
|
||||
assert.deepEqual(mutations,[]);assert.deepEqual(external,[]);assert.deepEqual(textRequests,[]);assert.deepEqual(errors,[]);
|
||||
}finally{
|
||||
const result={passed:results.every(r=>r.status==='passed'),results,reads,mutations,external,textRequests,browserErrors:errors,isolation:{fixedSyntheticDates:true,freshEmptyViteEnv:true,productionRequests:0,businessMutations:0},scope:'Browser fixtures exercise UI and request wiring; actual backend SQL is covered by the separate Go/PostgreSQL test.'};
|
||||
await writeFile(join(output,'checks.json'),JSON.stringify(result,null,2)+'\n');console.log(JSON.stringify({...result,reads:reads.length},null,2));
|
||||
await browser?.close();await server.close();assert.equal(dirname(work),resolve(tmpdir()));await rm(work,{recursive:true,force:true});
|
||||
}
|
||||
if(results.some(r=>r.status==='failed'))process.exitCode=1;
|
||||
Reference in New Issue
Block a user