middlewright 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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * video-mode: slow down and highlight actions so recorded videos are watchable.
3
+ *
4
+ * Extracted from the iterate monorepo's internal Playwright test
5
+ * infrastructure (github.com/iterate/iterate, private). Modification from the
6
+ * original: the hardcoded skip for iterate's test-helpers file is now the
7
+ * `skipStackFrames` option.
8
+ */
9
+ import type { Locator } from "@playwright/test";
10
+ import type { Plugin, OverrideableMethod } from "../plugin-system.ts";
11
+
12
+ export type VideoModeOptions = {
13
+ /** Pause duration before action (ms). Default: 1000 */
14
+ pauseBefore?: number;
15
+ /** Pause duration after test (ms). Default: 3000 */
16
+ pauseAfterTest?: number;
17
+ /** Highlight style. Default: '3px solid gold' */
18
+ highlightStyle?: string;
19
+ /** Methods to skip highlighting. Default: ['waitFor'] */
20
+ skipMethods?: OverrideableMethod[];
21
+ /**
22
+ * Skip highlighting for actions triggered from these files (matched as
23
+ * substrings of stack frames). Useful for internal helpers like login
24
+ * flows that shouldn't be slowed down. Default: []
25
+ */
26
+ skipStackFrames?: string[];
27
+ };
28
+
29
+ /** Highlight element, pause, return disposable that unhighlights */
30
+ const setupHighlight = async (locator: Locator, style: string, pauseMs: number) => {
31
+ try {
32
+ await locator.evaluate((el, s) => {
33
+ const prev = el.getAttribute("style") || "";
34
+ el.setAttribute("data-video-prev-style", prev);
35
+ el.setAttribute(
36
+ "style",
37
+ `${prev}; outline: ${s} !important; outline-offset: 2px !important;`,
38
+ );
39
+ }, style);
40
+ } catch {
41
+ // Element may not be ready yet, ignore
42
+ }
43
+ await new Promise((resolve) => setTimeout(resolve, pauseMs));
44
+
45
+ return {
46
+ [Symbol.dispose]: () => {
47
+ // Fire-and-forget cleanup - don't wait for it
48
+ locator
49
+ .evaluate((el) => {
50
+ const prev = el.getAttribute("data-video-prev-style");
51
+ if (typeof prev === "string") {
52
+ el.setAttribute("style", prev);
53
+ el.removeAttribute("data-video-prev-style");
54
+ }
55
+ })
56
+ .catch(() => {
57
+ // Element may be gone or not actionable, ignore
58
+ });
59
+ },
60
+ };
61
+ };
62
+
63
+ /**
64
+ * Highlights elements before actions and pauses for video recording.
65
+ * Also pauses after tests complete for better video endings.
66
+ */
67
+ export const videoMode = (options: VideoModeOptions = {}): Plugin => {
68
+ const pauseBefore = options.pauseBefore || 1000;
69
+ const pauseAfterTest = options.pauseAfterTest || 3000;
70
+ const highlightStyle = options.highlightStyle || "3px solid gold";
71
+ const skipMethods = options.skipMethods || ["waitFor"];
72
+ const skipStackFrames = options.skipStackFrames || [];
73
+
74
+ return {
75
+ name: "video-mode",
76
+
77
+ middleware: async ({ locator, method }, next) => {
78
+ if (skipMethods.includes(method)) return next();
79
+
80
+ // Skip if called from internal helpers (navigation, login flows etc)
81
+ if (skipStackFrames.length > 0) {
82
+ const stack = new Error().stack || "";
83
+ if (skipStackFrames.some((frame) => stack.includes(frame))) return next();
84
+ }
85
+
86
+ using _ = await setupHighlight(locator, highlightStyle, pauseBefore);
87
+ return await next();
88
+ },
89
+
90
+ testLifecycle: (emitter) => {
91
+ return emitter.on("afterTest", async ({ testInfo }) => {
92
+ await new Promise((resolve) => setTimeout(resolve, pauseAfterTest));
93
+ console.log(`video will be written to ${testInfo.outputDir}/video.webm`);
94
+ });
95
+ },
96
+ };
97
+ };