mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:08:46 +02:00
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { test, type Page, type TestInfo } from "@playwright/test";
|
|
import * as narration from "./narration";
|
|
import { renderFinalVideo } from "./video-render";
|
|
import type { Clip } from "./subtitles";
|
|
|
|
const DEMO = !!process.env.PW_DEMO;
|
|
|
|
/**
|
|
* Wraps test.step() so every step of a demo spec produces a captioned
|
|
* screenshot, and — in PW_DEMO mode — a native Playwright screencast
|
|
* recording with cursor annotations, chapter-title cards, burned-in
|
|
* subtitles and (when `say` is available) spoken Spanish narration.
|
|
* Screenshots/video/subtitles land in e2e-docs/<slug>/ and are also
|
|
* attached to the Playwright HTML report; save() stitches everything into
|
|
* a Markdown walkthrough that can be published as-is.
|
|
*/
|
|
export class FeatureDoc {
|
|
private steps: { title: string; caption: string; file: string }[] = [];
|
|
private clips: Clip[] = [];
|
|
private readonly dir: string;
|
|
private recordingStartedAt = 0;
|
|
|
|
constructor(
|
|
private page: Page,
|
|
private testInfo: TestInfo,
|
|
private slug: string,
|
|
private title: string,
|
|
private intro: string,
|
|
) {
|
|
this.dir = path.resolve("e2e-docs", slug);
|
|
fs.rmSync(this.dir, { recursive: true, force: true });
|
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
}
|
|
|
|
/** Starts the native screencast recording. Call before the first step(). */
|
|
async start() {
|
|
if (!DEMO) return;
|
|
this.recordingStartedAt = Date.now();
|
|
await this.page.screencast.start({
|
|
path: path.join(this.dir, "raw.webm"),
|
|
size: { width: 1920, height: 1080 },
|
|
});
|
|
await this.page.screencast.showActions({ cursor: "pointer" });
|
|
}
|
|
|
|
async step(title: string, caption: string, fn: () => Promise<void>) {
|
|
const narrationPromise = DEMO
|
|
? narration.synthesize(caption, this.testInfo.outputDir, this.clips.length)
|
|
: Promise.resolve(null);
|
|
|
|
const stepStart = Date.now();
|
|
if (DEMO) {
|
|
await this.page.screencast.showChapter(title, { description: caption, duration: 1500 });
|
|
}
|
|
|
|
await test.step(title, fn);
|
|
|
|
const narrationResult = await narrationPromise;
|
|
if (DEMO) {
|
|
const elapsed = Date.now() - stepStart;
|
|
const target = (narrationResult?.durationMs ?? 0) + 300;
|
|
if (target > elapsed) await this.page.waitForTimeout(target - elapsed);
|
|
}
|
|
|
|
const file = `${String(this.steps.length + 1).padStart(2, "0")}-${title
|
|
.toLowerCase()
|
|
.normalize("NFD")
|
|
.replace(/[̀-ͯ]+/g, "")
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "")}.png`;
|
|
const body = await this.page.screenshot({
|
|
path: path.join(this.dir, file),
|
|
animations: "disabled",
|
|
});
|
|
await this.testInfo.attach(title, { body, contentType: "image/png" });
|
|
this.steps.push({ title, caption, file });
|
|
|
|
if (DEMO) {
|
|
this.clips.push({
|
|
title,
|
|
caption,
|
|
file,
|
|
startMs: stepStart - this.recordingStartedAt,
|
|
endMs: Date.now() - this.recordingStartedAt,
|
|
narration: narrationResult,
|
|
});
|
|
}
|
|
}
|
|
|
|
async save() {
|
|
let videoName = "video.mp4";
|
|
if (DEMO) {
|
|
await this.page.screencast.stop();
|
|
const { videoFile, degraded } = await renderFinalVideo({
|
|
rawWebmPath: path.join(this.dir, "raw.webm"),
|
|
clips: this.clips,
|
|
outDir: this.dir,
|
|
});
|
|
for (const warning of degraded) console.warn(`[${this.slug}] ${warning}`);
|
|
videoName = path.basename(videoFile);
|
|
await this.testInfo.attach("video", {
|
|
path: videoFile,
|
|
contentType: videoName.endsWith(".mp4") ? "video/mp4" : "video/webm",
|
|
});
|
|
}
|
|
|
|
const md = [
|
|
`# ${this.title}`,
|
|
"",
|
|
this.intro,
|
|
"",
|
|
`> Generado automáticamente por \`${path.basename(this.testInfo.file)}\` — ${this.steps.length} pasos. El video completo del recorrido está en [${videoName}](${videoName}).`,
|
|
"",
|
|
...this.steps.flatMap((s, i) => [
|
|
`## ${i + 1}. ${s.title}`,
|
|
"",
|
|
s.caption,
|
|
"",
|
|
``,
|
|
"",
|
|
]),
|
|
].join("\n");
|
|
fs.writeFileSync(path.join(this.dir, "index.md"), md);
|
|
}
|
|
}
|