// Real login/router/API clients; synthetic delivery and business responses. 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 server=await createServer({configFile:false,root:resolve('web'),oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:5325,strictPort:true}}); await server.listen();const output=resolve('.design-evidence/operator-phone');await mkdir(output,{recursive:true}); let browser;const checks=[],errors=[]; try { 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:1280,height:1000}}); page.on('pageerror',e=>errors.push(e.message)); let customer=true,operator=false,recentSMS=false,captcha=0,opReads=0,writes=0,sends=0; const tenant='10000000-0000-0000-0000-000000000011'; await page.route('**/*',async route=>{ const r=route.request(),path=new URL(r.url()).pathname; if(!path.startsWith('/api/')&&!path.startsWith('/health/'))return route.continue(); const body=r.method()==='POST'?r.postDataJSON():undefined; let status=200,data={},code='OK',message=''; if(path.startsWith('/health/'))data={status:path.endsWith('/live')?'alive':'ready',service:'api',checks:{database:'ready'}}; else if(path==='/api/v1/version')data={version:'test'}; else if(path==='/api/v1/web/operator/account/session'){if(operator)data={authenticated:true,csrf_token:'operator-csrf'};else{status=401;code='LOGIN_REQUIRED';}} else if(path==='/api/v1/web/account/session'){assert(customer);data={authenticated:true,csrf_token:'customer-csrf'};} else if(path==='/api/v1/web/operator/login/session')data={sms_enabled:true,password_enabled:true,signup_enabled:false,csrf_token:'operator-login-csrf'}; else if(path==='/api/v1/web/operator/login/captcha'){ assert.equal(r.headers()['x-csrf-token'],'operator-login-csrf');captcha++; data={captcha_id:`image-${captcha}`,image:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=',expires_at_ms:Date.now()+120000}; } else if(path==='/api/v1/web/operator/login/password'){ assert.equal(r.headers()['x-csrf-token'],'operator-login-csrf');assert.equal(body.phone_e164,'+8618941657635');assert.equal(body.captcha_id,`image-${captcha}`); if(body.captcha_code!=='654321'){status=401;code='CAPTCHA_INVALID';message='图片验证码错误或已过期,请重新输入';} else{assert.equal(body.password,'OperatorOnly123!');operator=true;recentSMS=false;data={csrf_token:'operator-csrf'};} } else if(path==='/api/v1/web/operator/login/sms/send'){ assert.equal(r.headers()['x-csrf-token'],'operator-login-csrf');assert.equal(body.captcha_id,`image-${captcha}`);assert.equal(body.captcha_code,'654321');sends++; data={sms_challenge_id:'sms-op',expires_at_ms:Date.now()+300000,resend_after_ms:60000}; } else if(path==='/api/v1/web/operator/login/sms/verify'){assert.equal(body.code,'654321');operator=true;recentSMS=true;data={csrf_token:'operator-csrf'};} else if(path==='/api/v1/me'){ const op=r.headers()['x-login-portal']==='operator'; if(op&&!operator){status=401;code='LOGIN_REQUIRED';} else data={identity:{subject:op?'admin':'owner',display_name:op?'测试管理员':'测试家庭用户'},accounts:op?[{tenant_id:tenant,tenant_name:'测试组织',user_id:'admin',platform_role:'ops_admin',account_kind:'operator',household_memberships:[]}]:[]}; } else if(path==='/api/v1/web/operator/account/password'){ assert(operator);if(r.method()==='POST'){assert.equal(r.headers()['x-csrf-token'],'operator-csrf');assert.equal(body.current_password,'OperatorOnly123!');writes++;} data={available:true,phone_masked:'+86189****7635',password_configured:true,sms_verified_recently:recentSMS}; } else if(path==='/api/v1/web/account/password')data={available:false}; else if(path==='/api/v1/web/operator/auth/logout'){assert.equal(r.headers()['x-csrf-token'],'operator-csrf');operator=false;} else if(path.startsWith('/api/v1/ops/')){ assert(operator);assert.equal(r.headers()['x-login-portal'],'operator');assert.equal(r.headers().authorization,undefined);assert.equal(r.headers()['x-tenant-id'],tenant); if(r.method()==='POST'){assert.equal(r.headers()['x-csrf-token'],'operator-csrf');writes++;}else opReads++; data={items:[]}; } else if(['/api/v1/households','/api/v1/devices'].includes(path))data={items:[]}; else throw new Error('Unexpected API '+path); await route.fulfill({status,contentType:'application/json',body:JSON.stringify({ok:status===200,code,message,data})}); }); const origin='http://127.0.0.1:5325',target='/maintenance?area=devices&record=123#detail'; await page.goto(origin+target);await page.getByRole('heading',{name:'维护中心登录'}).waitFor(); assert.equal(opReads,0);assert.equal(await page.locator('.maintenance-layout').count(),0); const phone=page.getByLabel('手机号',{exact:true});await phone.fill('18941657635'); assert(await page.getByRole('button',{name:'获取验证码',exact:true}).isDisabled()); const bounds=await page.locator('.login-panel').boundingBox(); await page.getByRole('button',{name:'密码登录',exact:true}).click();await page.getByRole('img',{name:'图片验证码',exact:true}).waitFor(); assert.deepEqual(await page.locator('.login-panel').boundingBox(),bounds);assert.equal(await phone.inputValue(),'18941657635'); checks.push('customer session cannot enter management; both admin login modes share one card'); await page.getByLabel('密码',{exact:true}).fill('OperatorOnly123!');await page.getByLabel('图片验证码',{exact:true}).fill('000000');await page.locator('form.sms-login-form button[type=submit]').click(); await page.getByText('图片验证码错误或已过期,请重新输入',{exact:true}).waitFor();await page.waitForFunction(()=>!document.querySelector('#customer-image-code').disabled); await page.getByLabel('图片验证码',{exact:true}).fill('654321');await page.locator('form.sms-login-form button[type=submit]').click(); await page.getByRole('button',{name:'打开管理账户菜单'}).waitFor();assert.equal(page.url(),origin+target); await page.reload();await page.getByRole('button',{name:'打开管理账户菜单'}).waitFor();assert(opReads>0); checks.push('operator password/captcha login, API reads and reload retain original destination'); await page.evaluate(async tenant=>{const {authenticatedJSON}=await import('/src/api/client.ts');await authenticatedJSON('POST','/api/v1/ops/devices','',undefined,{}, {'X-Tenant-Id':tenant});const {onboardingRequest}=await import('/src/api/onboarding.ts');await onboardingRequest('/api/v1/ops/device-inventory',{}, {tenant,token:''});},tenant); assert.equal(writes,2);checks.push('both operator API clients send operator CSRF and tenant without an empty Bearer'); await page.getByRole('button',{name:'打开管理账户菜单'}).click();await page.getByRole('menuitem',{name:'登录与安全'}).click();await page.getByText('手机号:+86189****7635').waitFor(); await page.getByLabel('当前密码',{exact:true}).fill('OperatorOnly123!');await page.getByLabel('新密码',{exact:true}).fill('ChangedOperator123!');await page.getByLabel('确认新密码',{exact:true}).fill('ChangedOperator123!');await page.getByRole('button',{name:'保存密码'}).click();await page.getByText('密码已更新').waitFor(); await page.locator('.ant-drawer-close').click(); checks.push('management account menu offers isolated password settings'); await page.getByRole('button',{name:'打开管理账户菜单'}).click();await page.getByRole('menuitem',{name:'退出登录'}).click();await page.getByRole('heading',{name:'维护中心登录'}).waitFor();assert(customer); await page.evaluate(async()=>{const {navigateTo}=await import('/src/auth/navigation.ts');navigateTo('/?section=profile');});await page.getByRole('button',{name:'打开账户菜单'}).waitFor(); await page.evaluate(async target=>{const {navigateTo}=await import('/src/auth/navigation.ts');navigateTo(target);},target);await page.getByRole('heading',{name:'维护中心登录'}).waitFor(); checks.push('operator logout preserves customer session; switching portals reloads the correct identity'); await phone.fill('18941657635');await page.getByLabel('图片验证码',{exact:true}).fill('654321');await page.getByRole('button',{name:'获取验证码',exact:true}).click();await page.getByText(/验证码已发送/).waitFor();assert.equal(sends,1); await page.getByLabel('短信验证码',{exact:true}).fill('654321');await page.locator('form.sms-login-form button[type=submit]').click();await page.getByRole('button',{name:'打开管理账户菜单'}).waitFor(); checks.push('operator SMS login requires image verification and returns to management'); operator=false;await page.evaluate(async()=>{const {platformApi}=await import('/src/api/client.ts');await platformApi.currentUser('').catch(()=>{});});await page.getByRole('heading',{name:'维护中心登录'}).waitFor();assert(customer); for(const width of [375,320]){await page.setViewportSize({width,height:920});await page.waitForFunction(()=>!document.querySelector('#customer-phone').disabled);assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));} await page.screenshot({path:resolve(output,'mobile.png'),fullPage:true});checks.push('expired management session returns to its login page; 320px and 375px layouts fit'); assert.deepEqual(errors,[]);await writeFile(resolve(output,'result.json'),JSON.stringify({checks,errors},null,2));console.log(JSON.stringify({checks,errors})); }finally{await browser?.close();await server.close();}