import { visitBrowserDocument } from "./brand-navigation.mjs"; import assert from 'node:assert/strict'; import {verifyGuidanceWorkspaceUI} from './guidance-workspace-ui'; import {chromium} from 'playwright-core'; import {randomUUID} from 'node:crypto'; import {newToken,tokenDigest,ACCOUNT_COOKIE} from '../src/lib/account-crypto'; import {guidanceXml,type Guidance} from '../src/content/guides/catalog'; import {db} from '../src/lib/db'; import {publishDocument,resolveDocument,withdrawDocument,discoverDocuments} from '../src/lib/guidance-store'; export async function verifyGuidancePublishing(base:string){ const owner=randomUUID(),other=randomUUID(),id='user-'+owner+'-document'; for(const uid of [owner,other])await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,'test',now())",[uid,uid+'@example.com']); const doc:Guidance={id,revision:1,kind:'context',title:'实际数据库文档',summary:'摘要',applies:'条件',body:'正文',steps:[],checks:[],links:[],tags:[]}; await assert.rejects(()=>publishDocument(other,doc),/INVALID_ID/); await publishDocument(other,{...doc,id:'user-'+other+'-document'}); await publishDocument(owner,doc);assert.equal((await resolveDocument(id,1))?.title,doc.title); await assert.rejects(()=>publishDocument(other,{...doc,revision:2}),/INVALID_ID/); const concurrent=await Promise.allSettled([publishDocument(owner,{...doc,revision:2,body:'新正文'}),publishDocument(owner,{...doc,revision:2})]);assert.equal(concurrent.filter(r=>r.status==='fulfilled').length,1); assert.equal((await resolveDocument(id,1))?.body,'正文');assert.equal(await resolveDocument(id,999),null); await assert.rejects(()=>db().query('UPDATE guidance_revisions SET content=$2 WHERE document_id=$1',[id,{}]),/immutable/); assert.equal(await withdrawDocument(other,id),false);assert.equal(await withdrawDocument(owner,id),true);assert.equal(await resolveDocument(id),null);assert.ok(await resolveDocument(id,1));assert.ok(!(await discoverDocuments()).some(d=>d.id===id)); const token=newToken();await db().query("INSERT INTO account_tokens(digest,user_id,kind,expires_at) VALUES($1,$2,'session',now()+interval '1 hour')",[tokenDigest(token),owner]); const headers={'Content-Type':'application/json',origin:process.env.ACCOUNT_PUBLIC_ORIGIN||base,cookie:ACCOUNT_COOKIE+'='+token}; const httpDoc={...doc,id:'user-'+owner+'-http',tags:['往返标签']}; const request=(path:string,method:string,body?:unknown,extra:Record={})=>fetch(base+'/api/documents'+path,{method,headers:{...headers,...extra},...(body?{body:typeof body==='string'?body:JSON.stringify(body)}:{})}); assert.equal((await request('','POST',httpDoc,{cookie:''})).status,401); assert.equal((await request('','POST',httpDoc,{origin:'https://untrusted.example'})).status,403); const preview=await request('/preview','POST',httpDoc);assert.equal(preview.status,200,await preview.clone().text());assert.equal(await resolveDocument(httpDoc.id,1),null); const created=await request('','POST',guidanceXml(httpDoc),{'Content-Type':'application/xml'});assert.equal(created.status,201,await created.clone().text()); assert.equal((await request('','POST',httpDoc)).status,409); assert.equal((await request('','POST',{...httpDoc,id:'user-'+other+'-http'})).status,400); const own=await fetch(base+'/api/documents?author='+owner).then(r=>r.json());assert.ok(own.documents.some((d:{id:string})=>d.id===httpDoc.id));assert.ok(own.documents.every((d:{author:string})=>d.author===owner));assert.equal((await fetch(base+'/api/documents?author=invalid')).status,400); const xml=await fetch(base+'/api/documents/'+httpDoc.id+'?revision=1&format=xml');assert.equal(xml.status,200);assert.match(await xml.text(),/往返标签<\/tag>/); const page=await fetch(base+'/documents/'+httpDoc.id+'?revision=1');assert.equal(page.status,200);assert.ok((await page.text()).includes('实际数据库文档')); const directory=await fetch(base+'/api/documents');assert.equal(directory.status,200);assert.ok((await directory.json()).documents.some((d:{id:string})=>d.id===httpDoc.id),'Published document must appear in the public API'); // The document discovery list hydrates from the real API; the homepage is the product landing page. const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); try{ const page=await browser.newPage();page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000); const navigationDiagnostics:string[]=[],inflight=new Set(); const record=(value:string)=>{navigationDiagnostics.push(value.slice(0,1800));if(navigationDiagnostics.length>24)navigationDiagnostics.shift();}; page.on('pageerror',error=>record('pageerror: '+error.message)); page.on('console',message=>{if(message.type()==='error')record('console: '+message.text());}); page.on('request',request=>inflight.add(request.url())); page.on('requestfinished',request=>inflight.delete(request.url())); page.on('requestfailed',request=>{inflight.delete(request.url());record('requestfailed: '+new URL(request.url()).pathname+' '+request.failure()?.errorText)}); page.on('response',response=>{const path=new URL(response.url()).pathname;if(path.startsWith('/api/')||path.includes('/_next/data/'))void response.text().then(body=>record(`data ${response.status()} ${path}: ${body.slice(0,1400)}`)).catch(()=>{});else if(response.status()>=400||response.request().resourceType()==='script')record(`resource ${response.status()} ${path}`);}); try{ const link=`a[href="/documents/${httpDoc.id}?revision=1"]`; const navigation=await visitBrowserDocument(page,base+'/documents');record('directory HTTP '+navigation?.status()); await page.locator(link).waitFor({state:'visible'});assert.equal(await page.locator(link).getByRole('heading').textContent(),httpDoc.title); // Published matches must not inherit the examples' empty state; submitted filters survive reload. const search=page.getByRole('textbox',{name:'搜索文档'}); await search.fill(httpDoc.title);await search.press('Enter'); await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title,{waitUntil:'commit'}); await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); assert.equal(await page.getByText('没有匹配文档',{exact:false}).count(),0); await visitBrowserDocument(page,page.url(),true); await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); // The matching count can also describe the unfiltered list before router hydration. await page.waitForFunction(title=>document.querySelector('input[aria-label="搜索文档"]')?.value===title,httpDoc.title); assert.equal(await search.inputValue(),httpDoc.title); await page.getByRole('tab',{name:/事实依据/}).click(); await page.waitForURL(url=>url.searchParams.get('kind')==='evidence',{waitUntil:'commit'}); await page.getByRole('heading',{name:'没有匹配文档',exact:true}).waitFor(); await page.getByRole('button',{name:'清除所有筛选'}).click(); await page.waitForURL(url=>url.search==='',{waitUntil:'commit'}); await page.locator(link).waitFor({state:'visible'}); await page.waitForFunction(()=>document.querySelector('input[aria-label="搜索文档"]')?.value===''); assert.equal(await search.inputValue(),''); await page.getByRole('navigation',{name:'主导航',exact:true}).getByRole('link',{name:'社区文档',exact:true}).click();await page.locator(link).waitFor({state:'visible'}); await page.locator(link).click(); await page.waitForURL(url=>url.pathname==='/documents/'+httpDoc.id&&url.searchParams.get('revision')==='1',{waitUntil:'commit'}); await page.getByRole('heading',{name:httpDoc.title,exact:true,level:1}).waitFor(); assert.equal(await page.locator('h1').textContent(),httpDoc.title); await visitBrowserDocument(page,base+'/write');await page.getByRole('heading',{name:'发布独立文档',exact:true,level:1}).waitFor();assert.equal(await page.locator('h1').textContent(),'发布独立文档'); }catch(error){console.error('Document navigation diagnostics',JSON.stringify({url:page.url(),headings:await page.locator('h1').allTextContents(),body:(await page.locator('body').innerText()).slice(0,2200),inflight:[...inflight].map(url=>new URL(url).pathname),events:navigationDiagnostics}));throw error;} await page.close(); await verifyGuidanceWorkspaceUI(base,browser); console.log('Workspace desktop/mobile, filters, reading, account and publishing assertions passed in a fresh browser context'); }finally{await browser.close();} assert.equal((await request('/'+httpDoc.id,'DELETE',undefined,{cookie:''})).status,401); assert.equal((await request('/'+httpDoc.id,'DELETE')).status,200); assert.equal((await fetch(base+'/api/documents/'+httpDoc.id)).status,404); assert.equal((await fetch(base+'/api/documents/'+httpDoc.id+'?revision=1')).status,200); }