import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {mkdir,writeFile} from 'node:fs/promises'; import {resolve} from 'node:path'; import {pathToFileURL} from 'node:url'; 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('.tmp-tos-recordings/evidence');await mkdir(output,{recursive:true}); const server=await createServer({configFile:false,root:resolve('.tmp-tos-recordings/candidate/web'),oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:5325,strictPort:true},plugins:[{ name:'recording-test',configureServer(server){server.middlewares.use('/__test',(req,res)=>{res.setHeader('Content-Type','text/html');res.end('
');});},resolveId(id){if(id==='__recording-test.tsx')return '\0__recording-test.tsx';},load(id){if(id==='\0__recording-test.tsx')return ` import React from 'react';import {createRoot} from 'react-dom/client';import {CallRecordings} from '/src/components/CallRecordings.tsx';import '/src/components/home/customer-portal.css'; function App(){const [sid,set]=React.useState('call1');return React.createElement('main',{className:'companion-portal',style:{maxWidth:700,margin:'30px auto',padding:20}},React.createElement('button',{onClick:()=>set('call2')},'切换通话'),React.createElement(CallRecordings,{key:sid,sessionID:sid,elderID:'elder',accessToken:'test-token'}))};createRoot(document.getElementById('root')).render(React.createElement(App));`;}}]}); await server.listen();const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH??'C:/Program Files/Google/Chrome/Application/chrome.exe'});const page=await browser.newPage({viewport:{width:1100,height:850}});const problems=[],checks=[];page.on('pageerror',e=>problems.push(e.message)); let canPlay=false,canDownload=false,slow=false,denied=false,playbackReady=true;const requests=[]; await page.route('**/api/v1/**',async route=>{const path=new URL(route.request().url()).pathname,body=route.request().postDataJSON();requests.push({path,body}); if(path.endsWith('recording-access-grants')){if(body.permission==='PLAY')canPlay=true;else canDownload=true;return route.fulfill({json:{data:{}}});} if(path.endsWith('calls/recordings'))return route.fulfill({json:{data:{items:body.business_session_id==='call1'?[{id:'user',speaker:'USER',duration_seconds:58,bytes:1045859},{id:'bot',speaker:'ASSISTANT',duration_seconds:103,bytes:1845555}]:[],can_play:canPlay,can_download:canDownload,can_manage:true,consent:true,requester_id:'self',state:'READY',playback_ready:playbackReady}}}); if(path.endsWith('calls/recording-access')){if(slow)await new Promise(r=>setTimeout(r,450));if(denied)return route.fulfill({status:403,json:{code:'RECORDING_ACCESS_DENIED',message:'denied'}});return route.fulfill({json:{data:{access_url:'https://memory927.tos-cn-beijing.volces.com/test.mp4?signature=synthetic',access_until:new Date(Date.now()+300000).toISOString()}}});} return route.fulfill({status:404,json:{}});}); // Synthetic metadata only. No actual customer audio is loaded in this test. await page.route('https://memory927.tos-cn-beijing.volces.com/**',route=>route.abort()); try{ await page.goto('http://127.0.0.1:5325/__test');await page.getByText('长者',{exact:true}).waitFor();assert(await page.getByRole('button',{name:'播放录音'}).first().isDisabled());checks.push('separate speakers and authorization required'); await page.getByRole('button',{name:'授权本账号播放 24 小时'}).click();await page.getByRole('button',{name:'播放录音'}).first().waitFor();await page.getByRole('button',{name:'播放录音'}).first().click();await page.locator('audio').waitFor();assert.match(await page.locator('audio').getAttribute('src'),/^https:\/\/memory927\.tos-cn-beijing\.volces\.com\//);assert.equal(await page.locator('audio').getAttribute('preload'),'none');checks.push('player uses direct TOS URL'); await page.getByRole('button',{name:'授权本账号下载 24 小时'}).click();await page.getByRole('button',{name:'获取下载链接'}).first().click();await page.getByRole('link',{name:'从火山下载录音'}).waitFor();assert.match(await page.getByRole('link',{name:'从火山下载录音'}).getAttribute('href'),/^https:\/\/memory927\.tos-cn-beijing\.volces\.com\//);checks.push('download is a direct link, not a platform blob'); await page.screenshot({path:resolve(output,'recordings-desktop.png'),fullPage:true});await page.setViewportSize({width:390,height:844});assert(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)<3);await page.screenshot({path:resolve(output,'recordings-mobile.png'),fullPage:true});checks.push('mobile layout'); denied=true;await page.getByRole('button',{name:'播放录音'}).first().click();await page.getByRole('alert').waitFor();assert.equal(await page.locator('audio').count(),0);assert.equal(await page.getByRole('link',{name:'从火山下载录音'}).count(),0);checks.push('revoked access clears prior addresses'); denied=false;slow=true;await page.getByRole('button',{name:'播放录音'}).first().click();await page.getByRole('button',{name:'切换通话'}).click();await page.getByText(/暂未找到录音/).waitFor();await page.waitForTimeout(650);assert.equal(await page.locator('audio').count(),0);checks.push('late URL cannot leak into another call'); playbackReady=false;await page.reload();await page.getByText(/在线播放尚未开通/).waitFor();assert.equal(await page.getByRole('button',{name:'播放录音'}).count(),0);assert.equal(await page.getByRole('button',{name:'获取下载链接'}).count(),2);checks.push('default TOS domain exposes download without a broken play button'); assert.deepEqual(problems,[]);await writeFile(resolve(output,'result.json'),JSON.stringify({checks,problems},null,2));console.log(JSON.stringify({checks,problems})); }catch(e){console.log(JSON.stringify({problems,requests,body:await page.locator('body').innerText()}));throw e;}finally{await browser.close();await server.close();}