diff --git a/calliope-web/src/lib/components/shell/ThemeChoices.svelte b/calliope-web/src/lib/components/shell/ThemeChoices.svelte index abd152e6..e61bc4b2 100644 --- a/calliope-web/src/lib/components/shell/ThemeChoices.svelte +++ b/calliope-web/src/lib/components/shell/ThemeChoices.svelte @@ -57,10 +57,14 @@ > {item.name}{#if tiles && item.id === "night"} - 默认{/if}{tiles ? item.description.split(" · ")[0] : item.english}{#if !tiles}{item.description}{/if}{tiles && item.id === "night" + ? item.name + " " + : item.name}{#if tiles && item.id === "night"}默认{/if}{tiles ? item.tone : item.english}{#if !tiles}{item.description}{/if} { + try { + const p = decodeURIComponent(new URL(req.url, "http://local").pathname); + if (p.startsWith("/api/")) { + apiLog.push(`${req.method} ${p}`); + let body = []; + if (p === "/api/settings") + body = { + queue_concurrency: 2, + queue_poll_timeout_sec: 1800, + comfyui_base_url: "http://127.0.0.1:8188", + projects_root: "D:\\AIGC\\projects", + dry_run: false, + }; + else if ( + p === "/api/projects" || + p === "/api/workflows" || + p === "/api/library/media" || + p === "/api/agent/sessions" || + p === "/api/agent/agents" || + p === "/api/canvas" || + p.startsWith("/api/tasks") || + p.startsWith("/api/models") + ) + body = []; + else if (p === "/api/jobs/queue-status") body = { paused: false }; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(body)); + return; + } + if (p.startsWith("/sample")) { + const bytes = await fs.readFile(SAMPLE); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.end(bytes); + return; + } + let f = path.join(BUILD, p); + if (!path.extname(f)) f = path.join(BUILD, "index.html"); + const bytes = await fs.readFile(f); + res.setHeader( + "Content-Type", + f.endsWith(".js") + ? "text/javascript" + : f.endsWith(".css") + ? "text/css" + : f.endsWith(".html") + ? "text/html" + : "application/octet-stream", + ); + res.end(bytes); + } catch { + res.writeHead(404); + res.end(); + } +}); +await new Promise((r) => (server.listing = server.listen(0, "127.0.0.1", r))); +const base = `http://127.0.0.1:${server.address().port}`; +console.log("serving at", base); + +const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] }); + +async function freshContext(width) { + const ctx = await browser.newContext({ + viewport: { width, height: width === 1440 ? 900 : width === 1366 ? 768 : 720 }, + timezoneId: "Asia/Shanghai", + deviceScaleFactor: 1, + }); + await ctx.addInitScript(() => { + localStorage.setItem("aigc-lang", "zh"); + localStorage.setItem("aigc-reduce-motion", "1"); + window.__sse = []; + window.EventSource = class { + constructor() { + window.__sse.push(this); + setTimeout(() => this.onopen?.(), 0); + } + addEventListener(t, f) { + (this.handlers ??= {})[t] = f; + } + close() {} + }; + }); + await ctx.route("**/*", async (route) => { + const u = new URL(route.request().url()); + if (["data:", "blob:", "file:"].includes(u.protocol)) return route.continue(); + if (u.origin !== base) return route.abort(); + route.continue(); + }); + return ctx; +} + +/** Shared DOM measurement: given a card element handle's root, measure all internals. */ +const MEASURE = (cardSel) => { + const card = document.querySelector(cardSel); + if (!card) return null; + const num = (v) => parseFloat(v) || 0; + const r = (el) => { + const b = el.getBoundingClientRect(); + return { x: +b.x.toFixed(3), y: +b.y.toFixed(3), w: +b.width.toFixed(3), h: +b.height.toFixed(3) }; + }; + const st = (el) => { + const s = getComputedStyle(el); + return { + rect: r(el), + background: s.backgroundColor, + backgroundImage: s.backgroundImage, + borderTopWidth: s.borderTopWidth, + borderRightWidth: s.borderRightWidth, + borderBottomWidth: s.borderBottomWidth, + borderLeftWidth: s.borderLeftWidth, + borderTopColor: s.borderTopColor, + borderRightColor: s.borderRightColor, + borderBottomColor: s.borderBottomColor, + borderLeftColor: s.borderLeftColor, + borderTopLeftRadius: s.borderTopLeftRadius, + borderTopRightRadius: s.borderTopRightRadius, + borderBottomLeftRadius: s.borderBottomLeftRadius, + borderBottomRightRadius: s.borderBottomRightRadius, + boxShadow: s.boxShadow, + paddingTop: s.paddingTop, + paddingRight: s.paddingRight, + paddingBottom: s.paddingBottom, + paddingLeft: s.paddingLeft, + opacity: s.opacity, + color: s.color, + overflow: s.overflow, + position: s.position, + }; + }; + const cardS = st(card); + const tag = card.querySelector(".theme-default, .tag"); + const prev = card.querySelector(".preview") || card.querySelector(".tprev"); + const rail = prev?.querySelector(".rail") || prev?.querySelector(".r"); + const list = prev?.querySelector(".list") || prev?.querySelector(".l"); + const items = list ? [...list.querySelectorAll("i")] : []; + const stage = prev?.querySelector(".stage") || prev?.querySelector(".m"); + const art = stage?.querySelector("i") || stage?.querySelector(".art"); + const dock = stage?.querySelector("b") || stage?.querySelector(".dk"); + const badge = dock?.querySelector("em") || dock?.querySelector("span"); + const textLine = prev?.querySelector(".preview-text") || prev?.querySelector(".tx"); + const strong = card.querySelector(".copy strong") || card.querySelector("b"); + const small = card.querySelector(".copy small") || card.querySelector("small"); + const dot = rail ? getComputedStyle(rail, "::before") : null; + return { + card: cardS, + defaultTag: tag ? st(tag) : null, + cardClass: card.className, + ariaChecked: card.getAttribute("aria-checked") ?? card.getAttribute("aria-pressed"), + dataTheme: prev?.getAttribute("data-theme"), + preview: prev ? st(prev) : null, + rail: rail ? st(rail) : null, + railDot: dot + ? { w: dot.width, h: dot.height, bg: dot.backgroundColor, mt: dot.marginTop, mr: dot.marginRight, ml: dot.marginLeft, mb: dot.marginBottom, radius: dot.borderRadius } + : null, + list: list ? st(list) : null, + listItemCount: items.length, + listItems: items.map((i) => st(i)), + stage: stage ? st(stage) : null, + art: art ? st(art) : null, + dock: dock ? st(dock) : null, + badge: badge ? st(badge) : null, + textLine: textLine ? st(textLine) : null, + copyStrong: strong + ? { text: strong.textContent.trim().replace(/\s+/g, " "), fontSize: getComputedStyle(strong).fontSize, lineHeight: getComputedStyle(strong).lineHeight, fontWeight: getComputedStyle(strong).fontWeight } + : null, + copySmall: small + ? { text: small.textContent.trim(), fontSize: getComputedStyle(small).fontSize, lineHeight: getComputedStyle(small).lineHeight, color: getComputedStyle(small).color, fontFamily: getComputedStyle(small).fontFamily.slice(0, 40) } + : null, + }; +}; + +const results = { groups: [], errors: [], apiCalls: [] }; +try { + for (const width of VIEWPORTS) { + const ctx = await freshContext(width); + const app = await ctx.newPage(), ref = await ctx.newPage(); + for (const p of [app, ref]) p.on("pageerror", e => results.errors.push(e.message)); + await app.goto(`${base}/settings`); + await app.locator(".choices.tiles").waitFor(); + await ref.goto(`${base}/sample`); + await ref.evaluate(() => { go("settings"); document.documentElement.classList.add("capture", "reduce"); }); + for (const theme of THEMES) { + await app.evaluate(t => { localStorage.setItem("aigc-theme", t); window.dispatchEvent(new StorageEvent("storage", {key:"aigc-theme", newValue:t})); }, theme); + await ref.evaluate(t => setTheme(t), theme); + await app.mouse.move(1200, 600); await ref.mouse.move(1200, 600); + await app.waitForTimeout(350); + const key = `${theme}-${width}`; + await app.screenshot({path:path.join(OUT, `${key}-full-app.png`)}); + await ref.screenshot({path:path.join(OUT, `${key}-full-sample.png`)}); + for (let i=0;i<4;i++) { + const a = `.choices.tiles .choice:nth-child(${i+1})`, b = `#themeTiles .ttile:nth-child(${i+1})`; + for (const state of ["normal", "selected", "hover"]) { + // Hold page theme fixed to test every card/state. Class changes are fixture-only; + // natural click/selection/persistence are exercised by verify-hel769.py separately. + await app.locator(a).evaluate((e,on)=>e.classList.toggle("selected",on), state === "selected"); + await ref.locator(b).evaluate((e,on)=>e.classList.toggle("on",on), state === "selected"); + await app.mouse.move(1200,600); await ref.mouse.move(1200,600); + if (state === "hover") { await app.locator(a).hover(); await ref.locator(b).hover(); } + await app.waitForTimeout(300); + const name = `${key}-card${i}-${state}`; + results.groups.push({name, theme, width, card:i, state, app:await app.evaluate(MEASURE,a), sample:await ref.evaluate(MEASURE,b)}); + await app.locator(a).screenshot({path:path.join(OUT,`${name}-app.png`)}); + await ref.locator(b).screenshot({path:path.join(OUT,`${name}-sample.png`)}); + } + await app.locator(a).evaluate((e,on)=>e.classList.toggle("selected",on), THEMES[i]===theme); + await ref.locator(b).evaluate((e,on)=>e.classList.toggle("on",on), THEMES[i]===theme); + } + } + await ctx.close(); + } +} finally { + results.apiCalls = [...new Set(apiLog)]; + await fs.writeFile(path.join(OUT,"measurements.json"), JSON.stringify(results,null,2)); + await browser.close(); + await new Promise(r=>server.close(r)); +} +if(results.errors.length || results.groups.length !== 144) throw Error("Incomplete matrix or page errors"); +console.log(`Collected ${results.groups.length} card/state pairs; zero page errors; browser and server closed.`);