Merge pull request 'fix(ui): 完成啡跃主题与浏览器图标的生产接入' (#34) from codex/brand-ui into main
Reviewed-on: #34
This commit was merged in pull request #34.
This commit is contained in:
+3
-3
@@ -13,9 +13,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate
|
||||
COPY apps/waterfall-wiki/pnpm-lock.yaml ./
|
||||
COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml
|
||||
COPY packages/brand/ /repo/packages/brand/
|
||||
RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
COPY apps/waterfall-wiki/package.json ./
|
||||
RUN pnpm --filter @prompt/waterfall-wiki install --offline --frozen-lockfile
|
||||
RUN pnpm --filter @prompt/waterfall-wiki install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
|
||||
FROM dependencies AS build
|
||||
# .dockerignore excludes docs and operational notes without extra legacy COPY snapshots.
|
||||
@@ -27,9 +26,10 @@ USER node
|
||||
CMD ["npm", "run", "test:integration"]
|
||||
|
||||
FROM base AS runtime
|
||||
WORKDIR /app/apps/waterfall-wiki
|
||||
ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 \
|
||||
FFMPEG_PATH=/usr/bin/ffmpeg FFPROBE_PATH=/usr/bin/ffprobe
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ ./
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ /app/
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/dist/worker.cjs /repo/apps/waterfall-wiki/dist/bootstrap.cjs /repo/apps/waterfall-wiki/dist/publish-learning-series.cjs /repo/apps/waterfall-wiki/dist/auth-cli.cjs ./
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/db/ ./db/
|
||||
COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./
|
||||
|
||||
@@ -4,9 +4,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate
|
||||
COPY apps/studio/pnpm-lock.yaml ./
|
||||
COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml
|
||||
COPY packages/brand/ /repo/packages/brand/
|
||||
RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
COPY apps/studio/package.json ./
|
||||
RUN pnpm --filter @prompt/studio install --offline --frozen-lockfile
|
||||
RUN pnpm --filter @prompt/studio install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
COPY apps/studio/ .
|
||||
# Compare the two public parameter validators inside the isolated CI build.
|
||||
COPY apps/waterfall-wiki/src/lib/layout-parameters.ts /repo/apps/waterfall-wiki/src/lib/layout-parameters.ts
|
||||
@@ -14,9 +13,9 @@ COPY apps/waterfall-wiki/src/lib/series.ts /repo/apps/waterfall-wiki/src/lib/ser
|
||||
COPY apps/waterfall-wiki/src/lib/canvas-document.ts /repo/apps/waterfall-wiki/src/lib/canvas-document.ts
|
||||
RUN pnpm typecheck && pnpm test && pnpm build && pnpm test:smoke
|
||||
FROM node:24-bookworm-slim
|
||||
WORKDIR /app
|
||||
WORKDIR /app/apps/studio
|
||||
ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000
|
||||
COPY --from=build --chown=node:node /repo/apps/studio/.next/standalone/ ./
|
||||
COPY --from=build --chown=node:node /repo/apps/studio/.next/standalone/ /app/
|
||||
COPY --from=build --chown=node:node /repo/apps/studio/.next/static/ ./.next/static/
|
||||
COPY --from=build --chown=node:node /repo/apps/studio/public/ ./public/
|
||||
USER node
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolve } from "node:path";
|
||||
import type { NextConfig } from "next";
|
||||
const config: NextConfig = { basePath: "/studio", output: "standalone", outputFileTracingRoot: process.cwd(), poweredByHeader: false };
|
||||
const config: NextConfig = { turbopack: { root: resolve(process.cwd(), "../..") }, basePath: "/studio", output: "standalone", outputFileTracingRoot: resolve(process.cwd(), "../.."), poweredByHeader: false };
|
||||
export default config;
|
||||
|
||||
@@ -9,7 +9,7 @@ const assetId='00000000-0000-4000-8000-000000000005';doc.sources=[{assetId,autho
|
||||
let reads=0,mediaReads=0;
|
||||
const wiki=createHttpServer((req,res)=>{assert.equal(req.headers.cookie,'session=smoke-test');if(/^\/api\/media\/[0-9a-f-]{36}$/.test(req.url)){mediaReads++;res.setHeader('Content-Type','image/png');return res.end(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j3ioAAAAASUVORK5CYII=','base64'))}reads++;res.setHeader('Content-Type','application/json');if(req.url.endsWith(seriesId))return res.end(JSON.stringify({design:{id:seriesId,name:'完整三页作品',layoutId:'standard-01',revision:1,url:'/studio/template-layout?id='+seriesId,parameters:doc.pages[0].parameters,document:doc}}));const standard=req.url.endsWith(standardId);if(!standard&&!req.url.endsWith(designId)){res.writeHead(404);return res.end('{}')}res.end(JSON.stringify({design:{id:standard?standardId:designId,name:'已保存的实际页面',layoutId:standard?'standard-01':'ref-01',revision:3,url:'/studio/designs/'+designId,parameters:{added:[{id:'user-smoke',kind:'text',role:'Added text',box:[10,10,20,10],direction:'horizontal',text:'新增元素已恢复',reading:'custom'}],layers:{T01:{x:12,rotation:25,opacity:.7},'user-smoke':{z:12}},texts:{T01:{value:'确定的页面文字',color:'#123456'}},images:{I01:{color:'#abcdef'}},decorations:{D01:{hidden:true}},paper:'#eeeeee'}}}))});
|
||||
wiki.listen(0,'127.0.0.1');await once(wiki,'listening');
|
||||
const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const port=socket.address().port;await new Promise(r=>socket.close(r));const child=spawn(process.execPath,[process.env.STUDIO_STANDALONE_SERVER || '.next/standalone/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1',STUDIO_WIKI_ORIGIN:'http://127.0.0.1:'+wiki.address().port},stdio:'inherit'}),base='http://127.0.0.1:'+port;
|
||||
const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const port=socket.address().port;await new Promise(r=>socket.close(r));const child=spawn(process.execPath,[process.env.STUDIO_STANDALONE_SERVER || '.next/standalone/apps/studio/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1',STUDIO_WIKI_ORIGIN:'http://127.0.0.1:'+wiki.address().port},stdio:'inherit'}),base='http://127.0.0.1:'+port;
|
||||
try{let ready=false;for(let i=0;i<60;i++){try{if((await fetch(base+'/studio/api/health')).ok){ready=true;break}}catch{}await setTimeout(250)}assert.ok(ready);const entry=await fetch(base+'/studio',{redirect:'manual'});assert.equal(entry.status,307);assert.equal(entry.headers.get('location'),'/studio/graphic');const library=await fetch(base+'/studio/graphic').then(r=>r.text());assert.ok(library.includes('GRAPHIC'));assert.ok(!library.includes('ref-27')); const catalog=await fetch(base+'/studio/api/layouts').then(r=>r.json());assert.equal(catalog.layouts.length,28);
|
||||
for(const l of catalog.layouts){const page=await fetch(base+l.url);assert.equal(page.status,200,l.id);const html=await page.text();assert.ok(html.includes(l.name));assert.ok(html.includes('data-layout-id="'+l.id+'"'));assert.ok(!/<svg|<img|data:image|trial-controls|page-tools|workbench-top|contenteditable|编辑页面/.test(html),'Default page must use native elements, not source imagery');const schema=await fetch(base+l.api).then(r=>r.json());assert.equal(schema.id,l.id);for(const s of schema.elements)assert.ok(html.includes('data-element="'+s.id+'"'));if(l.family==='standard'){assert.equal((await fetch(base+l.api+'?format=svg')).status,400);continue;}const original=await fetch(base+l.api+'?format=svg',{headers:{cookie:'session=smoke-test'}}).then(r=>r.text());assert.ok(original.includes('data-layout-id="'+l.id+'"'));const text=schema.elements.find(e=>e.kind==='text');const result=await fetch(base+l.api,{method:'POST',headers:{'Content-Type':'application/json',cookie:'session=smoke-test'},body:JSON.stringify({texts:{[text.id]:{value:'API render <test>'}}})});assert.equal(result.status,200);assert.ok((await result.text()).includes('<'));}
|
||||
assert.equal(reads,0,'Default pages never load a saved design');assert.ok(mediaReads>0,'SVG exports load isolated media fixtures');
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
.layer[data-selected=true],.tabs button[aria-pressed=true]{background:var(--selection);color:var(--foreground)}
|
||||
.element[data-selected=true],.corner{outline-color:var(--selection-border);border-color:var(--selection-border)}
|
||||
/* The artwork board is deliberately independent of dark UI chrome. */
|
||||
.blankHint{color:#49372E}
|
||||
.blankHint{color:var(--el-espresso)}
|
||||
|
||||
@@ -11,9 +11,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate
|
||||
COPY apps/waterfall-wiki/pnpm-lock.yaml ./
|
||||
COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml
|
||||
COPY packages/brand/ /repo/packages/brand/
|
||||
RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
COPY apps/waterfall-wiki/package.json ./
|
||||
RUN pnpm --filter @prompt/waterfall-wiki install --offline --frozen-lockfile
|
||||
RUN pnpm --filter @prompt/waterfall-wiki install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8
|
||||
|
||||
FROM dependencies AS build
|
||||
# .dockerignore excludes docs and operational notes without extra legacy COPY snapshots.
|
||||
@@ -25,9 +24,10 @@ USER node
|
||||
CMD ["npm", "run", "test:integration"]
|
||||
|
||||
FROM base AS runtime
|
||||
WORKDIR /app/apps/waterfall-wiki
|
||||
ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 \
|
||||
FFMPEG_PATH=/usr/bin/ffmpeg FFPROBE_PATH=/usr/bin/ffprobe
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ ./
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ /app/
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/dist/worker.cjs /repo/apps/waterfall-wiki/dist/bootstrap.cjs /repo/apps/waterfall-wiki/dist/publish-learning-series.cjs /repo/apps/waterfall-wiki/dist/auth-cli.cjs ./
|
||||
COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/db/ ./db/
|
||||
COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { resolve } from "node:path";
|
||||
import type { NextConfig } from "next";
|
||||
import managedMedia from "./src/content/managed-media.json";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
turbopack: { root: resolve(process.cwd(), "../..") },
|
||||
serverExternalPackages: ["playwright-core"],
|
||||
async redirects(){return [
|
||||
{source:'/studio/graphic',destination:'/graphic',permanent:true},
|
||||
@@ -20,7 +22,7 @@ const nextConfig: NextConfig = {
|
||||
{source:"/agent/index.md",destination:"/api/agent-index"},
|
||||
{source:"/skills/:skill/:doc.md",destination:"/api/skill-docs/:skill/:doc"},
|
||||
]},
|
||||
output: "standalone", outputFileTracingRoot: process.cwd(),
|
||||
output: "standalone", outputFileTracingRoot: resolve(process.cwd(), "../.."),
|
||||
outputFileTracingIncludes: {"/*":["./node_modules/playwright-core/**/*"]},
|
||||
poweredByHeader: false,
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ async function main() {
|
||||
const request = (path:string,init:RequestInit={}) => fetch(base+path,{...init,redirect:'manual'});
|
||||
const buildId = readFileSync('.next/BUILD_ID','utf8').trim();
|
||||
for(const path of ['/','/workspace','/documents','/account'])assert.equal((await request(path)).status,200,'Public shell '+path);
|
||||
for(const path of ['/favicon.ico','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/apple-touch-icon.png']) assert.equal((await request(path)).status,200,'Public brand asset '+path);
|
||||
const studio=await request('/studio');assert.equal(studio.status,307);assert.equal(studio.headers.get('location'),'/workspace');
|
||||
for(const [from,to] of [['/studio/graphic','/graphic'],['/studio/infinite_canvas','/infinite_canvas']]){const r=await request(from);assert.equal(r.status,308);assert.equal(r.headers.get('location'),to)}
|
||||
assert.equal((await request('/account')).status,200,'Public account page');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cp,mkdir,access } from 'node:fs/promises';
|
||||
await mkdir('.next/standalone/.next',{recursive:true});
|
||||
await cp('.next/static','.next/standalone/.next/static',{recursive:true});
|
||||
try{await access('public');await cp('public','.next/standalone/public',{recursive:true});}catch(e){if(e.code!=='ENOENT')throw e;}
|
||||
await mkdir('.next/standalone/apps/waterfall-wiki/.next',{recursive:true});
|
||||
await cp('.next/static','.next/standalone/apps/waterfall-wiki/.next/static',{recursive:true});
|
||||
try{await access('public');await cp('public','.next/standalone/apps/waterfall-wiki/public',{recursive:true});}catch(e){if(e.code!=='ENOENT')throw e;}
|
||||
|
||||
@@ -17,9 +17,9 @@ async function availablePort() {
|
||||
|
||||
const port = process.env.SMOKE_PORT || await availablePort();
|
||||
const release = "smoke-test";
|
||||
const previewFixture='.next/standalone/public/__smoke-preview.png';
|
||||
const previewFixture='.next/standalone/apps/waterfall-wiki/public/__smoke-preview.png';
|
||||
await writeFile(previewFixture,await sharp({create:{width:48,height:48,channels:3,background:'#eee'}}).png().toBuffer());
|
||||
const child = spawn(process.execPath, [".next/standalone/server.js"], {
|
||||
const child = spawn(process.execPath, [".next/standalone/apps/waterfall-wiki/server.js"], {
|
||||
// This loopback-only child checks retained resources without production credentials.
|
||||
// Production keeps failing closed; the bypass requires both explicit test settings.
|
||||
env: { ...process.env, AUTH_CONFIG_FILE: "", AUTH_DISABLED_FOR_TESTS: "true", PORT: port, HOSTNAME: "127.0.0.1", RELEASE_SHA: release },
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { authConfig, SESSION_COOKIE, validSession } from './lib/auth';
|
||||
export function proxy(request: NextRequest) {
|
||||
const path = request.nextUrl.pathname;
|
||||
const publicBrandAssets = ['/favicon.ico','/apple-touch-icon.png','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/studio/favicon.ico','/studio/apple-touch-icon.png','/studio/brand/logo-light.svg','/studio/brand/logo-dark.svg','/studio/brand/icon-dark.svg'];
|
||||
if (['GET','HEAD'].includes(request.method) && publicBrandAssets.includes(path)) return NextResponse.next({headers:{'Referrer-Policy':'same-origin'}});
|
||||
const responseHeaders = {'Cache-Control':'private, no-store', 'Referrer-Policy':'same-origin'};
|
||||
// Public knowledge pages are read-only. Account handlers enforce their own origin and session checks.
|
||||
const publicRead = ['GET','HEAD'].includes(request.method) && (path === '/' || ['/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api'].includes(path) || path.startsWith('/studio/_next/') || path === '/protocol' || ['/llms.txt','/index.md','/agent','/agent/index.md','/api/agent-index','/api/guides'].includes(path) || path.startsWith('/guide/') || path.startsWith('/api/guides/'));
|
||||
|
||||
@@ -19,3 +19,5 @@ input,textarea,select{accent-color:var(--primary)}
|
||||
|
||||
.primary-button,.auth-submit{background:var(--primary);color:var(--primary-foreground)}
|
||||
.primary-button:hover,.auth-submit:hover{background:var(--primary-hover)}
|
||||
/* The legacy library rail collapses on tablets, while its mobile drawer expands. */
|
||||
@media(min-width:651px) and (max-width:950px){.sidebar .endoleap-wordmark{display:none!important}}
|
||||
|
||||
@@ -71,8 +71,8 @@
|
||||
top: 8px;
|
||||
left: 250px;
|
||||
transform: translateY(-200%);
|
||||
background: var(--ink);
|
||||
color: var(--foreground) !important;
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground) !important;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@ test('proxy exposes creative entry pages while protecting APIs, data and Studio
|
||||
for(const path of ['/library/import','/skills/example.png','/_next/data/build/library/import.json','/studio/layouts/ref-01']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
|
||||
for(const path of ['/api/upload','/studio/api/export','/studio/api/layouts','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path);
|
||||
for(const path of ['/','/_next/data/build/index.json','/llms.txt','/index.md','/agent','/agent/index.md','/api/agent-index','/api/guides','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
|
||||
const creativeEntries=['/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api','/studio/_next/static/chunk.js'];
|
||||
const creativeEntries=['/favicon.ico','/apple-touch-icon.png','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/studio/favicon.ico','/studio/apple-touch-icon.png','/studio/brand/logo-light.svg','/studio/brand/logo-dark.svg','/studio/brand/icon-dark.svg','/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api','/studio/_next/static/chunk.js'];
|
||||
for(const path of creativeEntries){
|
||||
for(const method of ['GET','HEAD'])assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method})).status,200,`${method} ${path}`);
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method:'POST'})).status,401,`POST ${path} must not bypass authentication`);
|
||||
|
||||
@@ -17,10 +17,12 @@ export async function verifyBrandUI(base: string) {
|
||||
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({ viewport: { width: 1440, height: 960 } });
|
||||
page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000);
|
||||
const visit = async (path: string, theme: string) => {
|
||||
await page.goto(base + path, { waitUntil: 'commit' });
|
||||
await page.locator('button[data-theme-ready="true"]').first().waitFor();
|
||||
await page.waitForFunction(expected => document.documentElement.dataset.theme === expected, theme);
|
||||
console.log('Brand UI route ready:', path, theme);
|
||||
};
|
||||
const background = (selector: string) => page.locator(selector).first().evaluate(el => getComputedStyle(el).backgroundColor);
|
||||
await visit('/', 'home-light');
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function verifyCanvasAgentUI(base:string){
|
||||
page.on('requestfailed',request=>{pending.delete(request);network.push(requestName(request)+': '+request.failure()?.errorText)});
|
||||
page.on('response',response=>{const url=new URL(response.url());if(url.pathname.startsWith('/api/canvases'))canvasRequests.push(`${response.request().method()} ${url.pathname} ${response.status()}`)});
|
||||
try{
|
||||
page.setDefaultTimeout(20000);page.on('pageerror',e=>errors.push(e.message));
|
||||
mark('open landing');await page.goto(base+'/',{waitUntil:'domcontentloaded'});
|
||||
page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000);page.on('pageerror',e=>errors.push(e.message));
|
||||
mark('open landing');await page.goto(base+'/',{waitUntil:'commit'});
|
||||
await page.getByRole('heading',{name:/想法不该停在对话里/,level:1}).waitFor();
|
||||
const accountSession=page.waitForResponse(response=>new URL(response.url()).pathname==='/api/account/session'&&response.request().method()==='GET');
|
||||
const [,sessionResponse]=await Promise.all([page.getByRole('link',{name:'开始创作',exact:true}).click(),accountSession]);
|
||||
@@ -97,7 +97,7 @@ export async function verifyCanvasAgentUI(base:string){
|
||||
assert.equal((await fetch(base+root,{headers:{authorization:second.authorization}})).status,404);
|
||||
await page.setViewportSize({width:390,height:844});
|
||||
const bounds=await panel.boundingBox();assert.ok(bounds&&bounds.x>=0&&bounds.x+bounds.width<=390,'Agent panel stays within the mobile viewport');
|
||||
await page.reload({waitUntil:'domcontentloaded'});
|
||||
await page.reload({waitUntil:'commit'});
|
||||
await page.getByRole('link',{name:email,exact:true,includeHidden:true}).waitFor({state:'attached'});
|
||||
await page.getByRole('button',{name:'Agent',exact:true}).click();
|
||||
const reopened=page.getByRole('region',{name:'Agent 连接',exact:true});
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function verifyGuidancePublishing(base:string){
|
||||
// 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();
|
||||
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));
|
||||
@@ -46,30 +46,30 @@ export async function verifyGuidancePublishing(base:string){
|
||||
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 page.goto(base+'/documents',{waitUntil:'domcontentloaded'});record('directory HTTP '+navigation?.status());
|
||||
const navigation=await page.goto(base+'/documents',{waitUntil:'commit'});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);
|
||||
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 page.reload({waitUntil:'domcontentloaded'});
|
||||
await page.reload({waitUntil:'commit'});
|
||||
await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor();
|
||||
assert.equal(await search.inputValue(),httpDoc.title);
|
||||
await page.getByRole('tab',{name:/事实依据/}).click();
|
||||
await page.waitForURL(url=>url.searchParams.get('kind')==='evidence');
|
||||
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==='');
|
||||
await page.waitForURL(url=>url.search==='',{waitUntil:'commit'});
|
||||
await page.locator(link).waitFor({state:'visible'});
|
||||
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');
|
||||
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 page.goto(base+'/write',{waitUntil:'domcontentloaded'});assert.equal(await page.locator('h1').textContent(),'发布独立文档');
|
||||
await page.goto(base+'/write',{waitUntil:'commit'});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);
|
||||
|
||||
@@ -89,7 +89,8 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
|
||||
route.fulfill({ json: { document: authored } }),
|
||||
);
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(10000);
|
||||
page.setDefaultTimeout(60000);
|
||||
page.setDefaultNavigationTimeout(60000);
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("crash", () => errors.push("Browser renderer crashed"));
|
||||
const pending = new Set<string>(), network: string[] = [];
|
||||
@@ -99,7 +100,7 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
|
||||
async function navigate(url: string, reload = false) {
|
||||
const previousSessionReplies = sessionReplies;
|
||||
try {
|
||||
const navigation = await (reload ? page.reload({waitUntil:"domcontentloaded"}) : page.goto(url,{waitUntil:"domcontentloaded"}));
|
||||
const navigation = await (reload ? page.reload({waitUntil:"commit"}) : page.goto(url,{waitUntil:"commit"}));
|
||||
assert.ok(navigation?.ok(), "Navigation returns a successful document");
|
||||
await page.getByText(signedIn ? "editor@example.com" : "登录你的账户",{exact:true}).first().waitFor({state:"attached"});
|
||||
assert.ok(sessionReplies > previousSessionReplies, "The new document requests its account fixture and renders the resulting account state");
|
||||
|
||||
@@ -23,18 +23,25 @@ import {verifyFlatNoteMigration} from './flat-note-migration';
|
||||
import {staticEntries} from '../src/lib/explore';
|
||||
import {publishLearningSeries} from '../src/lib/publish-learning-series';
|
||||
|
||||
// Browser acceptance gets its own deadline; it must not consume the existing business-flow budget.
|
||||
test('Endoleap icons, persisted themes and artwork isolation in the real browser', {timeout:300000,skip:!process.env.PROMPT_TEST_BASE_URL}, async()=>{
|
||||
assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1');
|
||||
const base=process.env.PROMPT_TEST_BASE_URL;assert.ok(base);
|
||||
await verifyBrandUI(base);
|
||||
});
|
||||
|
||||
test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processing', {timeout:600000}, async()=>{
|
||||
assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1','Run only against an explicitly isolated test database');
|
||||
const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const address=socket.address();assert.ok(address&&typeof address!=='string');const port=address.port;await new Promise<void>(r=>socket.close(()=>r()));
|
||||
const external=process.env.PROMPT_TEST_BASE_URL;
|
||||
const base=external||`http://127.0.0.1:${port}`;
|
||||
const server=external?null:spawn(process.execPath,['.next/standalone/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1'},stdio:'inherit'});
|
||||
const server=external?null:spawn(process.execPath,['.next/standalone/apps/waterfall-wiki/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1'},stdio:'inherit'});
|
||||
const worker=external?null:spawn(process.execPath,['dist/worker.cjs'],{env:process.env,stdio:'inherit'});const dir=await mkdtemp(join(tmpdir(),'prompt-test-'));
|
||||
const request=async(path:string,body?:unknown)=>{const res=await fetch(base+path,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:undefined);assert.ok(res.ok,`${path}: ${res.status} ${res.ok?'':await res.text()}`);return res.json();};
|
||||
try{
|
||||
let ready=false;for(let i=0;i<60;i++){try{if((await fetch(base+'/api/health')).ok){ready=true;break;}}catch{}await setTimeout(250);}assert.ok(ready);
|
||||
for(const route of ['/','/images','/videos','/audio','/articles','/motion','/library/import','/tools/doodle','/tools/skills','/tools/skills/street-people','/tools/skills/shadow-puppet'])assert.equal((await fetch(base+route)).status,200,route);
|
||||
await verifyBrandUI(base);
|
||||
if(!external)await verifyBrandUI(base);
|
||||
await verifyEmailAccounts();
|
||||
await verifyCanvasWorkspace(base);
|
||||
if(process.env.VERIFY_CANVAS_UI==='1')await verifyCanvasAgentUI(base);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
在消费应用的 `package.json` 声明 `"@endoleap/brand": "workspace:*"`,按仓库独立锁文件策略更新该应用锁文件。包为私有内部资源,无需 npm 发布、React peer dependency 或运行时主题库。
|
||||
|
||||
Studio / Wiki 已声明 `workspace:*` 依赖。Docker 构建在 `/repo/apps/<app>` 保留与 `/repo/packages/brand` 的相对位置;运行镜像仍在 `/app` 执行 standalone 服务。保持消费应用与品牌包的相对位置,不恢复原有孤立安装流程。
|
||||
Studio / Wiki 已声明 `workspace:*` 依赖。Docker 构建在 `/repo/apps/<app>` 保留与 `/repo/packages/brand` 的相对位置;Next.js 的模块解析与文件追踪都以 `/repo` 为根,standalone 保留 `apps/<app>` 层次。运行镜像复制完整 standalone 到 `/app`,在 `/app/apps/<app>` 执行服务;静态文件、图标、worker 与健康检查文件都放在对应应用目录。保持消费应用与品牌包的相对位置,不恢复原有孤立安装流程。
|
||||
|
||||
## Tailwind CSS v4
|
||||
|
||||
|
||||
Reference in New Issue
Block a user