156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
import { demoScenes as scenes, demoDesign } from "./scenes";
|
|
import { z } from "zod";
|
|
import { test, expect } from "@playwright/test";
|
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import {
|
|
nativeConversationStream,
|
|
nativeImageStream,
|
|
neutralTurn,
|
|
} from "../../src/apps/product/fixtures/native-conversation-stream";
|
|
|
|
// A scripted product walkthrough using imagery generated by Rev Animated in capture.ts or generate-images.ts.
|
|
// All playback goes through the real /chat page, native streams, persistence and reveal.
|
|
|
|
for (const scene of scenes)
|
|
test(`${scene.id} product walkthrough`, async ({ page }, testInfo) => {
|
|
const dir = `reports/image-reveal-demo/${scene.id}`;
|
|
await mkdir(dir, { recursive: true });
|
|
const started = Date.now();
|
|
await page.addInitScript(() => {
|
|
sessionStorage.setItem(
|
|
"stx:onboarding:v2:product-demo",
|
|
JSON.stringify({
|
|
version: 2,
|
|
locale: "fr",
|
|
intensity: "beginner",
|
|
adultConfirmed: true,
|
|
warningsAccepted: true,
|
|
participationConfirmed: true,
|
|
}),
|
|
);
|
|
localStorage.setItem("stx:metrics-tour:completed:v2", "true");
|
|
});
|
|
const image = z
|
|
.object({ images: z.array(z.object({ url: z.string() })).min(1) })
|
|
.parse(JSON.parse(await readFile(`${dir}/provider-image.json`, "utf8"))).images[0]!.url;
|
|
const turn = neutralTurn(scene.id, 0.2, 2, {
|
|
modality: "text-image",
|
|
visualPrompt: demoDesign(scene.id).title[0],
|
|
});
|
|
turn.fragments = [...scene.lines];
|
|
turn.statePatch.shortTitleSummary = demoDesign(scene.id).title[0];
|
|
turn.suggestions = scene.replies.map((text) => ({
|
|
displayText: text,
|
|
promptValue: text,
|
|
style: "default",
|
|
}));
|
|
turn.explanations = { context: "Une rencontre onirique entre paysage, matière et lumière." };
|
|
const body = await nativeConversationStream(turn);
|
|
await page.route("**/api/ai/chat", (route) =>
|
|
route.fulfill({ contentType: "text/event-stream", body }),
|
|
);
|
|
await page.route("**/api/ai/image", async (route) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
await route.fulfill({
|
|
contentType: "text/event-stream",
|
|
body: await nativeImageStream("success", image),
|
|
});
|
|
});
|
|
await page.goto(
|
|
`/chat?persona-id=${scene.id}&model-id=gemini&gateway-id=nanogpt&session-id=product-demo`,
|
|
);
|
|
const input = page.getByRole("textbox");
|
|
await input.fill("Ouvre un paysage de rêve, dans la brume.");
|
|
await page.waitForTimeout(500);
|
|
const profileRate = Number(process.env["PROFILE_REVEAL"] ?? 0);
|
|
const profiler = profileRate ? await page.context().newCDPSession(page) : null;
|
|
if (profiler) {
|
|
await profiler.send("Emulation.setCPUThrottlingRate", { rate: profileRate });
|
|
await profiler.send("Performance.enable");
|
|
}
|
|
await input.press("Enter");
|
|
await expect(page.locator('[data-image-reveal="summon"]')).toBeVisible({ timeout: 15000 });
|
|
const summon = Date.now() - started;
|
|
await expect(page.locator(".image-reveal__scene")).toHaveAttribute(
|
|
"data-reveal-renderer",
|
|
"webgpu",
|
|
);
|
|
const ink = await page
|
|
.locator("[data-image-reveal]")
|
|
.evaluate((element) => getComputedStyle(element).getPropertyValue("--reveal-ink").trim());
|
|
if (profiler) {
|
|
const before = await profiler.send("Performance.getMetrics");
|
|
const frames = await page.evaluate(
|
|
() =>
|
|
new Promise<{ count: number; medianMs: number; p95Ms: number; over33ms: number }>(
|
|
(resolve) => {
|
|
const intervals: number[] = [];
|
|
let previous = performance.now();
|
|
const sample = (now: number) => {
|
|
intervals.push(now - previous);
|
|
previous = now;
|
|
if (document.querySelector("[data-image-reveal]")) requestAnimationFrame(sample);
|
|
else {
|
|
intervals.sort((a, b) => a - b);
|
|
resolve({
|
|
count: intervals.length,
|
|
medianMs: intervals[Math.floor(intervals.length / 2)] ?? 0,
|
|
p95Ms: intervals[Math.floor(intervals.length * 0.95)] ?? 0,
|
|
over33ms: intervals.filter((ms) => ms > 33.4).length,
|
|
});
|
|
}
|
|
};
|
|
requestAnimationFrame(sample);
|
|
},
|
|
),
|
|
);
|
|
const after = await profiler.send("Performance.getMetrics");
|
|
const delta = (name: string) =>
|
|
(after.metrics.find((metric) => metric.name === name)?.value ?? 0) -
|
|
(before.metrics.find((metric) => metric.name === name)?.value ?? 0);
|
|
const report = {
|
|
persona: scene.id,
|
|
cpuThrottle: profileRate,
|
|
viewport: page.viewportSize(),
|
|
frames,
|
|
layoutCount: delta("LayoutCount"),
|
|
layoutMs: delta("LayoutDuration") * 1000,
|
|
scriptMs: delta("ScriptDuration") * 1000,
|
|
taskMs: delta("TaskDuration") * 1000,
|
|
};
|
|
await writeFile(`${dir}/performance-${profileRate}x.json`, JSON.stringify(report, null, 2));
|
|
console.log(JSON.stringify(report));
|
|
await expect(page.locator("[data-image-reveal]")).toHaveCount(0);
|
|
return;
|
|
}
|
|
// Capture the transient pixels immediately; Playwright screenshots wait for web fonts.
|
|
const screenshots = await page.context().newCDPSession(page);
|
|
await page.waitForTimeout(150);
|
|
const coarse = await screenshots.send("Page.captureScreenshot", { format: "png" });
|
|
await writeFile(`${dir}/product-summon.png`, Buffer.from(coarse.data, "base64"));
|
|
await page.waitForTimeout(300);
|
|
const pixels = await screenshots.send("Page.captureScreenshot", { format: "png" });
|
|
await writeFile(`${dir}/product-pixels.png`, Buffer.from(pixels.data, "base64"));
|
|
await expect(page.locator('[data-image-reveal="reveal"]')).toBeVisible();
|
|
await page.waitForTimeout(300);
|
|
const clear = await screenshots.send("Page.captureScreenshot", { format: "png" });
|
|
await writeFile(`${dir}/product-reveal.png`, Buffer.from(clear.data, "base64"));
|
|
await screenshots.detach();
|
|
await expect(page.locator("[data-image-reveal]")).toHaveCount(0, { timeout: 10000 });
|
|
await page.waitForTimeout(600);
|
|
await page.screenshot({ path: `${dir}/product-dialogue.png` });
|
|
const savedImage = await page.locator("[data-image-slot] img").last().getAttribute("src");
|
|
if (savedImage?.startsWith("data:"))
|
|
await writeFile(`${dir}/generated.webp`, Buffer.from(savedImage.split(",")[1]!, "base64"));
|
|
await page.waitForTimeout(1600);
|
|
await writeFile(
|
|
`${dir}/product-timing.json`,
|
|
JSON.stringify({
|
|
summon,
|
|
ink,
|
|
title: demoDesign(scene.id).title[0],
|
|
source: "Scripted dialogue; live Rev Animated image replay",
|
|
video: testInfo.outputPath("video.webm"),
|
|
}),
|
|
);
|
|
});
|