demowright 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/register.cjs ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CJS preload for NODE_OPTIONS="--require demowright/register" (zero test changes).
3
+ *
4
+ * NODE_OPTIONS="--require demowright/register" npx playwright test
5
+ *
6
+ * Patches Browser.newContext() to automatically inject demowright.
7
+ *
8
+ * Config via env vars: QA_HUD=0, QA_HUD_CURSOR=0, QA_HUD_KEYBOARD=0,
9
+ * QA_HUD_DELAY=200, QA_HUD_CURSOR_STYLE=dot, QA_HUD_KEY_FADE=2000
10
+ */
11
+ "use strict";
12
+
13
+ if (process.env.QA_HUD === "0") {
14
+ // Explicitly disabled
15
+ } else {
16
+ const Module = require("node:module");
17
+ const originalLoad = Module._load;
18
+ let contextProtoPatched = false;
19
+
20
+ Module._load = function qaHudLoad(request, parent, isMain) {
21
+ const result = originalLoad.call(this, request, parent, isMain);
22
+
23
+ // We need to patch BrowserContext.prototype, but it's not exported.
24
+ // Instead, we patch the BrowserType launch methods to intercept
25
+ // the Browser they return, then patch Browser.newContext.
26
+ if (!contextProtoPatched && request === "playwright-core") {
27
+ contextProtoPatched = true;
28
+
29
+ // Patch each browser type's launch method
30
+ for (const browserType of [result.chromium, result.firefox, result.webkit]) {
31
+ if (!browserType?.launch) continue;
32
+
33
+ const origLaunch = browserType.launch.bind(browserType);
34
+ browserType.launch = async function (...args) {
35
+ const browser = await origLaunch(...args);
36
+ patchBrowser(browser);
37
+ return browser;
38
+ };
39
+ }
40
+ }
41
+
42
+ return result;
43
+ };
44
+
45
+ function patchBrowser(browser) {
46
+ if (browser.__qaHudPatched) return;
47
+ browser.__qaHudPatched = true;
48
+
49
+ const origNewContext = browser.newContext.bind(browser);
50
+ browser.newContext = async function (...args) {
51
+ const context = await origNewContext(...args);
52
+ await applyHudToContext(context);
53
+ return context;
54
+ };
55
+ }
56
+
57
+ let applyHudCache = null;
58
+ async function applyHudToContext(context) {
59
+ try {
60
+ if (!applyHudCache) {
61
+ const mod = await import("./src/setup.ts");
62
+ applyHudCache = mod.applyHud;
63
+ }
64
+ const ttsUrl = process.env.QA_HUD_TTS || false;
65
+ let tts = ttsUrl;
66
+ // Built-in Gemini TTS provider
67
+ if (process.env.GEMINI_API_KEY && !ttsUrl) {
68
+ const apiKey = process.env.GEMINI_API_KEY;
69
+ tts = async function geminiTts(text) {
70
+ const res = await fetch(
71
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`,
72
+ {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({
76
+ contents: [{ parts: [{ text }] }],
77
+ generationConfig: {
78
+ responseModalities: ["AUDIO"],
79
+ speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: "Kore" } } },
80
+ },
81
+ }),
82
+ },
83
+ );
84
+ if (!res.ok) throw new Error(`Gemini TTS ${res.status}`);
85
+ const json = await res.json();
86
+ const b64 = json.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
87
+ if (!b64) throw new Error("No audio in Gemini response");
88
+ const pcm = Buffer.from(b64, "base64");
89
+ // Wrap raw PCM (s16le 24kHz mono) in WAV header
90
+ const hdr = Buffer.alloc(44);
91
+ hdr.write("RIFF", 0); hdr.writeUInt32LE(36 + pcm.length, 4);
92
+ hdr.write("WAVE", 8); hdr.write("fmt ", 12);
93
+ hdr.writeUInt32LE(16, 16); hdr.writeUInt16LE(1, 20);
94
+ hdr.writeUInt16LE(1, 22); hdr.writeUInt32LE(24000, 24);
95
+ hdr.writeUInt32LE(48000, 28); hdr.writeUInt16LE(2, 32);
96
+ hdr.writeUInt16LE(16, 34); hdr.write("data", 36);
97
+ hdr.writeUInt32LE(pcm.length, 40);
98
+ return Buffer.concat([hdr, pcm]);
99
+ };
100
+ }
101
+
102
+ // Fallback: espeak-ng (free, no API key needed)
103
+ if (!tts) {
104
+ try {
105
+ const { execFileSync } = require("node:child_process");
106
+ execFileSync("espeak-ng", ["--version"], { stdio: "ignore" });
107
+ tts = async function espeakTts(text) {
108
+ const wav = execFileSync("espeak-ng", [
109
+ "--stdout", "-s", "160", "-p", "50", text,
110
+ ], { maxBuffer: 10 * 1024 * 1024 });
111
+ return wav;
112
+ };
113
+ } catch { /* espeak-ng not installed — skip */ }
114
+ }
115
+
116
+ // Output directory
117
+ const outputDir = process.env.QA_HUD_OUTPUT_DIR || ".demowright";
118
+
119
+ // Audio capture: QA_HUD_AUDIO can be a path template or "1" for auto-path
120
+ let audio = false;
121
+ if (process.env.QA_HUD_AUDIO) {
122
+ const val = process.env.QA_HUD_AUDIO;
123
+ if (val === "1") {
124
+ audio = true;
125
+ } else {
126
+ audio = val;
127
+ }
128
+ }
129
+
130
+ const opts = {
131
+ cursor: process.env.QA_HUD_CURSOR !== "0",
132
+ keyboard: process.env.QA_HUD_KEYBOARD !== "0",
133
+ cursorStyle: process.env.QA_HUD_CURSOR_STYLE || "default",
134
+ keyFadeMs: Number(process.env.QA_HUD_KEY_FADE) || 1500,
135
+ actionDelay: Number(process.env.QA_HUD_DELAY) || 120,
136
+ tts,
137
+ audio,
138
+ outputDir,
139
+ };
140
+ await applyHudCache(context, opts);
141
+ } catch (e) {
142
+ console.warn("[demowright] Failed to apply HUD:", e.message);
143
+ }
144
+ }
145
+ }