88 lines
8.8 KiB
TypeScript
88 lines
8.8 KiB
TypeScript
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<string,string>={})=>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>往返标签<\/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<string>();
|
|
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<HTMLInputElement>('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<HTMLInputElement>('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);
|
|
|
|
}
|