@juangadm/pre-post 1.3.0 → 1.5.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.
Files changed (106) hide show
  1. package/README.md +43 -288
  2. package/assets/labels/post.png +0 -0
  3. package/assets/labels/pre.png +0 -0
  4. package/dist/attach.d.ts +59 -0
  5. package/dist/attach.d.ts.map +1 -0
  6. package/dist/attach.js +137 -0
  7. package/dist/attach.js.map +1 -0
  8. package/dist/baseline.d.ts +26 -1
  9. package/dist/baseline.d.ts.map +1 -1
  10. package/dist/baseline.js +92 -19
  11. package/dist/baseline.js.map +1 -1
  12. package/dist/bin/cli.js +40 -16
  13. package/dist/bin/cli.js.map +1 -1
  14. package/dist/browser.d.ts +79 -3
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +179 -38
  17. package/dist/browser.js.map +1 -1
  18. package/dist/commands/compare.d.ts.map +1 -1
  19. package/dist/commands/compare.js +5 -5
  20. package/dist/commands/compare.js.map +1 -1
  21. package/dist/commands/detect.d.ts +5 -0
  22. package/dist/commands/detect.d.ts.map +1 -1
  23. package/dist/commands/detect.js +3 -0
  24. package/dist/commands/detect.js.map +1 -1
  25. package/dist/commands/doctor.d.ts +6 -0
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +21 -0
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/pr.d.ts +5 -0
  30. package/dist/commands/pr.d.ts.map +1 -1
  31. package/dist/commands/pr.js +167 -113
  32. package/dist/commands/pr.js.map +1 -1
  33. package/dist/comparison.d.ts +5 -0
  34. package/dist/comparison.d.ts.map +1 -1
  35. package/dist/comparison.js +5 -5
  36. package/dist/comparison.js.map +1 -1
  37. package/dist/compose.d.ts +72 -0
  38. package/dist/compose.d.ts.map +1 -0
  39. package/dist/compose.js +237 -0
  40. package/dist/compose.js.map +1 -0
  41. package/dist/diff-worker.d.ts +1 -0
  42. package/dist/diff-worker.d.ts.map +1 -1
  43. package/dist/diff-worker.js +1 -0
  44. package/dist/diff-worker.js.map +1 -1
  45. package/dist/diff.d.ts +42 -0
  46. package/dist/diff.d.ts.map +1 -1
  47. package/dist/diff.js +142 -24
  48. package/dist/diff.js.map +1 -1
  49. package/dist/doctor.d.ts +4 -1
  50. package/dist/doctor.d.ts.map +1 -1
  51. package/dist/doctor.js +5 -1
  52. package/dist/doctor.js.map +1 -1
  53. package/dist/drift.d.ts +29 -0
  54. package/dist/drift.d.ts.map +1 -0
  55. package/dist/drift.js +106 -0
  56. package/dist/drift.js.map +1 -0
  57. package/dist/errors.d.ts +6 -0
  58. package/dist/errors.d.ts.map +1 -1
  59. package/dist/errors.js +6 -0
  60. package/dist/errors.js.map +1 -1
  61. package/dist/ffmpeg.d.ts +35 -0
  62. package/dist/ffmpeg.d.ts.map +1 -0
  63. package/dist/ffmpeg.js +143 -0
  64. package/dist/ffmpeg.js.map +1 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/moments.d.ts +83 -0
  70. package/dist/moments.d.ts.map +1 -0
  71. package/dist/moments.js +170 -0
  72. package/dist/moments.js.map +1 -0
  73. package/dist/record.d.ts +69 -0
  74. package/dist/record.d.ts.map +1 -0
  75. package/dist/record.js +223 -0
  76. package/dist/record.js.map +1 -0
  77. package/dist/report.d.ts +29 -1
  78. package/dist/report.d.ts.map +1 -1
  79. package/dist/report.js +174 -42
  80. package/dist/report.js.map +1 -1
  81. package/dist/routes/imports.d.ts +21 -1
  82. package/dist/routes/imports.d.ts.map +1 -1
  83. package/dist/routes/imports.js +32 -2
  84. package/dist/routes/imports.js.map +1 -1
  85. package/dist/routes.d.ts +18 -0
  86. package/dist/routes.d.ts.map +1 -1
  87. package/dist/routes.js +110 -29
  88. package/dist/routes.js.map +1 -1
  89. package/dist/run.d.ts +48 -12
  90. package/dist/run.d.ts.map +1 -1
  91. package/dist/run.js +140 -18
  92. package/dist/run.js.map +1 -1
  93. package/dist/timeline.d.ts +46 -0
  94. package/dist/timeline.d.ts.map +1 -0
  95. package/dist/timeline.js +80 -0
  96. package/dist/timeline.js.map +1 -0
  97. package/dist/types.d.ts +95 -1
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +8 -0
  100. package/dist/types.js.map +1 -1
  101. package/dist/video.d.ts +19 -0
  102. package/dist/video.d.ts.map +1 -0
  103. package/dist/video.js +109 -0
  104. package/dist/video.js.map +1 -0
  105. package/package.json +4 -2
  106. package/skill/SKILL.md +38 -10
package/dist/ffmpeg.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Encoding with the ffmpeg Playwright already knows how to install.
3
+ *
4
+ * Playwright ships a 2.5 MB ffmpeg for its own video recording, installed the
5
+ * same way as the headless browser, so nobody has to install anything. It is a
6
+ * minimal build (measured, revision 1011): libvpx VP8 encode and decode, mjpeg
7
+ * decode, image2pipe in, WebM out, pad/crop/scale — and no `hstack`, no
8
+ * drawtext, no `-` alias for stdin (it has to be `pipe:0`, as Playwright's own
9
+ * recorder writes it). That is why frames are composited in a canvas and piped
10
+ * in as JPEGs, and why the output is WebM.
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { spawn, spawnSync } from 'child_process';
15
+ import { installBrowser, playwrightCacheDir } from './browser.js';
16
+ import { onPath } from './baseline.js';
17
+ const BINARY = process.platform === 'darwin' ? 'ffmpeg-mac'
18
+ : process.platform === 'win32' ? 'ffmpeg-win64.exe'
19
+ : 'ffmpeg-linux';
20
+ /**
21
+ * The largest clip that is sure to upload. GitHub takes 10 MB of video on a
22
+ * Free plan (100 MB on paid ones, but nothing here can tell which), so a clip
23
+ * over this is re-encoded smaller rather than refused at upload.
24
+ */
25
+ export const MAX_CLIP_BYTES = 8 * 1024 * 1024;
26
+ /** Playwright's ffmpeg, newest revision first, when one is installed. */
27
+ function findCached() {
28
+ const dir = playwrightCacheDir();
29
+ let entries;
30
+ try {
31
+ entries = fs.readdirSync(dir).filter(e => /^ffmpeg-\d+$/.test(e));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ entries.sort((a, b) => Number(b.slice(7)) - Number(a.slice(7)));
37
+ for (const entry of entries) {
38
+ const full = path.join(dir, entry, BINARY);
39
+ if (fs.existsSync(full))
40
+ return full;
41
+ }
42
+ return null;
43
+ }
44
+ /** An ffmpeg on PATH that can encode VP8, the last resort. */
45
+ function findSystem() {
46
+ if (!onPath('ffmpeg'))
47
+ return null;
48
+ const probe = spawnSync('ffmpeg', ['-hide_banner', '-encoders'], { encoding: 'utf-8' });
49
+ return probe.status === 0 && /libvpx/.test(probe.stdout) ? 'ffmpeg' : null;
50
+ }
51
+ /** Where ffmpeg is, without installing anything. For `doctor`. */
52
+ export function findFfmpeg() {
53
+ const override = process.env.PRE_POST_FFMPEG;
54
+ if (override)
55
+ return fs.existsSync(override) ? override : null;
56
+ return findCached() ?? findSystem();
57
+ }
58
+ let ready = null;
59
+ /** ffmpeg, installing Playwright's (one time, ~2 MB) when there is none. */
60
+ export function ensureFfmpeg() {
61
+ ready ??= (async () => {
62
+ const found = findFfmpeg();
63
+ if (found)
64
+ return found;
65
+ if (await installBrowser('ffmpeg')) {
66
+ const installed = findCached();
67
+ if (installed)
68
+ return installed;
69
+ }
70
+ throw new Error('Video needs ffmpeg and it could not be installed. Run: npx playwright-core install ffmpeg');
71
+ })().catch(err => { ready = null; throw err; });
72
+ return ready;
73
+ }
74
+ const VP8 = ['-c:v', 'libvpx', '-qmin', '0', '-qmax', '50', '-deadline', 'realtime', '-cpu-used', '8', '-auto-alt-ref', '0'];
75
+ /**
76
+ * Stream JPEG frames at `fps` into a WebM at `out`.
77
+ *
78
+ * `-crf 10` is what sets the quality; the bitrate only caps it. Measured on a
79
+ * dropdown clip, 1M and 3M came out within 15% of each other and both kept
80
+ * 16px text readable in an 800px pane.
81
+ */
82
+ /** Run ffmpeg; `closed` resolves when it exits cleanly, rejects with its last error line. */
83
+ function run(ffmpeg, args, stdin) {
84
+ const child = spawn(ffmpeg, ['-loglevel', 'error', '-y', ...args], { stdio: [stdin, 'ignore', 'pipe'] });
85
+ let stderr = '';
86
+ child.stderr.on('data', d => { stderr += d; });
87
+ const closed = new Promise((resolve, reject) => {
88
+ child.on('error', reject);
89
+ child.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg failed (exit ${code}): ${stderr.trim().split('\n').pop() ?? ''}`)));
90
+ });
91
+ return { child, closed };
92
+ }
93
+ export function startEncoder(ffmpeg, out, fps) {
94
+ const { child, closed } = run(ffmpeg, [
95
+ '-f', 'image2pipe', '-avioflags', 'direct', '-fpsprobesize', '0', '-probesize', '32', '-analyzeduration', '0',
96
+ '-framerate', String(fps), '-c:v', 'mjpeg', '-i', 'pipe:0',
97
+ '-an', ...VP8, '-crf', '10', '-b:v', '2M', out,
98
+ ], 'pipe');
99
+ const stdin = child.stdin;
100
+ // stdin errors (EPIPE when ffmpeg dies early) surface through `closed`.
101
+ stdin.on('error', () => undefined);
102
+ let exited = false;
103
+ closed.catch(() => undefined).finally(() => { exited = true; });
104
+ return {
105
+ async write(jpeg) {
106
+ // A dead encoder never drains, so waiting on 'drain' alone would hang
107
+ // the run forever; the spike did exactly that.
108
+ if (exited)
109
+ return closed;
110
+ if (!stdin.write(jpeg)) {
111
+ await Promise.race([new Promise(r => stdin.once('drain', r)), closed]);
112
+ }
113
+ },
114
+ async finish() {
115
+ stdin.end();
116
+ await closed;
117
+ },
118
+ };
119
+ }
120
+ /**
121
+ * Bring a clip under `maxBytes` by re-encoding it smaller, once. Returns the
122
+ * final size. Re-encodes from the WebM itself, so nothing has to keep every
123
+ * frame in memory in case the clip turns out large.
124
+ */
125
+ export async function fitSize(ffmpeg, file, durationMs, maxBytes = MAX_CLIP_BYTES) {
126
+ const size = fs.statSync(file).size;
127
+ if (size <= maxBytes)
128
+ return size;
129
+ const seconds = Math.max(1, durationMs / 1000);
130
+ // 90% of the budget, leaving room for the container.
131
+ const kbps = Math.max(200, Math.floor((maxBytes * 8 * 0.9) / seconds / 1000));
132
+ const tmp = file.replace(/\.webm$/, '.fit.webm');
133
+ // Asynchronous: the other Moments are still being drawn and encoded, and a
134
+ // re-encode takes seconds.
135
+ await run(ffmpeg, [
136
+ '-i', file,
137
+ '-vf', 'scale=trunc(iw*0.75/2)*2:trunc(ih*0.75/2)*2',
138
+ '-an', ...VP8, '-crf', '30', '-b:v', `${kbps}k`, '-maxrate', `${kbps}k`, '-bufsize', `${kbps * 2}k`, tmp,
139
+ ], 'ignore').closed.catch(err => { throw new Error(`Could not shrink the clip: ${err.message}`); });
140
+ fs.renameSync(tmp, file);
141
+ return fs.statSync(file).size;
142
+ }
143
+ //# sourceMappingURL=ffmpeg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../src/ffmpeg.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -22,4 +22,8 @@ export { runLogin } from './commands/login.js';
22
22
  export { runPrune } from './commands/prune.js';
23
23
  export { runDoctor } from './commands/doctor.js';
24
24
  export { runDetect } from './commands/detect.js';
25
+ export { recordMoments } from './video.js';
26
+ export type { RecordMomentsOptions } from './video.js';
27
+ export { parseMoments, loadMomentsFile, MomentError, MAX_MOMENTS } from './moments.js';
28
+ export type { Moment, Step } from './moments.js';
25
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvF,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -17,4 +17,6 @@ export { runLogin } from './commands/login.js';
17
17
  export { runPrune } from './commands/prune.js';
18
18
  export { runDoctor } from './commands/doctor.js';
19
19
  export { runDetect } from './commands/detect.js';
20
+ export { recordMoments } from './video.js';
21
+ export { parseMoments, loadMomentsFile, MomentError, MAX_MOMENTS } from './moments.js';
20
22
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAElG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAElG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Moments: short, named interactions recorded on Pre and Post and played side
3
+ * by side in one clip.
4
+ *
5
+ * A screenshot cannot show a menu opening or a hover state; a recording of a
6
+ * page sitting still shows nothing either (that is why the first GIF attempt
7
+ * was parked). So the unit of video is an interaction a person would do —
8
+ * "Open the account menu" — written as a few plain steps that target what the
9
+ * person sees, not CSS.
10
+ *
11
+ * Everything here is pure: parsing, validation, choosing which Moments run,
12
+ * and the sentences a reviewer reads. The browser side is `record.ts`.
13
+ */
14
+ declare const VERBS: readonly ["click", "hover", "type", "press", "scroll", "wait"];
15
+ export type StepVerb = typeof VERBS[number];
16
+ export type Step = {
17
+ verb: 'click' | 'hover';
18
+ target: string;
19
+ } | {
20
+ verb: 'type';
21
+ target: string;
22
+ text: string;
23
+ } | {
24
+ verb: 'press';
25
+ key: string;
26
+ } | {
27
+ verb: 'scroll';
28
+ target?: string;
29
+ px?: number;
30
+ } | {
31
+ verb: 'wait';
32
+ ms: number;
33
+ };
34
+ export interface Moment {
35
+ /** What a reviewer reads above the clip, e.g. "Open the account menu". */
36
+ name: string;
37
+ route: string;
38
+ /** Viewport preset or WxH. Default desktop. */
39
+ viewport: string;
40
+ steps: Step[];
41
+ }
42
+ /** As written in `.pre-post.json` or a `--moments` file. */
43
+ export interface MomentInput {
44
+ name?: unknown;
45
+ route?: unknown;
46
+ viewport?: unknown;
47
+ steps?: unknown;
48
+ }
49
+ /**
50
+ * Moments per run. A big PR gets video for the few changes a reviewer most
51
+ * needs to see move; every changed page still gets screenshots. Order is
52
+ * priority: the first three run, the rest are named in one line.
53
+ */
54
+ export declare const MAX_MOMENTS = 3;
55
+ /**
56
+ * Past this a clip still ships, with a nudge to split it. Not a cap: length
57
+ * comes only from the steps someone wrote, so nothing records for minutes on
58
+ * its own, and a clip a reviewer can take in on one play is the goal.
59
+ */
60
+ export declare const LONG_CLIP_MS = 30000;
61
+ export declare class MomentError extends Error {
62
+ }
63
+ /** Validate one Moment. `where` names it in errors, e.g. "Moment 2". */
64
+ export declare function parseMoment(raw: MomentInput, where: string): Moment;
65
+ /** Validate a list of Moments, as found in config or a file. */
66
+ export declare function parseMoments(raw: unknown, source: string): Moment[];
67
+ /** Read a `--moments` file. */
68
+ export declare function loadMomentsFile(file: string, cwd?: string): Moment[];
69
+ /** The Moments that run, in priority order, and the names of the rest. */
70
+ export declare function selectMoments(moments: Moment[]): {
71
+ run: Moment[];
72
+ skipped: string[];
73
+ };
74
+ /** A name as a reviewer reads it: “Open the account menu”. */
75
+ export declare function quote(s: string): string;
76
+ /** A step as a reviewer would say it: `Click “Save”`. Shown under the clip. */
77
+ export declare function describeStep(step: Step): string;
78
+ /** One line for a clip that ran long, or null. */
79
+ export declare function longClipNote(name: string, durationMs: number): string | null;
80
+ /** One line naming the Moments over the limit, or null. */
81
+ export declare function skippedNote(skipped: string[]): string | null;
82
+ export {};
83
+ //# sourceMappingURL=moments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moments.d.ts","sourceRoot":"","sources":["../src/moments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,QAAA,MAAM,KAAK,gEAAiE,CAAC;AAC7E,MAAM,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,MAAM,IAAI,GACZ;IAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAS,CAAC;AAQnC,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAmDzC,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBnE;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnE;AAED,+BAA+B;AAC/B,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAgB,GAAG,MAAM,EAAE,CAe3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAErF;AAED,8DAA8D;AAC9D,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvC;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAS/C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAG5D"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Moments: short, named interactions recorded on Pre and Post and played side
3
+ * by side in one clip.
4
+ *
5
+ * A screenshot cannot show a menu opening or a hover state; a recording of a
6
+ * page sitting still shows nothing either (that is why the first GIF attempt
7
+ * was parked). So the unit of video is an interaction a person would do —
8
+ * "Open the account menu" — written as a few plain steps that target what the
9
+ * person sees, not CSS.
10
+ *
11
+ * Everything here is pure: parsing, validation, choosing which Moments run,
12
+ * and the sentences a reviewer reads. The browser side is `record.ts`.
13
+ */
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { parseViewport } from './viewport.js';
17
+ const VERBS = ['click', 'hover', 'type', 'press', 'scroll', 'wait'];
18
+ /**
19
+ * Moments per run. A big PR gets video for the few changes a reviewer most
20
+ * needs to see move; every changed page still gets screenshots. Order is
21
+ * priority: the first three run, the rest are named in one line.
22
+ */
23
+ export const MAX_MOMENTS = 3;
24
+ /**
25
+ * Past this a clip still ships, with a nudge to split it. Not a cap: length
26
+ * comes only from the steps someone wrote, so nothing records for minutes on
27
+ * its own, and a clip a reviewer can take in on one play is the goal.
28
+ */
29
+ export const LONG_CLIP_MS = 30_000;
30
+ /** What a clip should aim for; used in the nudge. */
31
+ const TARGET_CLIP_S = 15;
32
+ /** A `wait` longer than this is almost certainly a mistake (ms, not s). */
33
+ const MAX_WAIT_MS = 10_000;
34
+ export class MomentError extends Error {
35
+ }
36
+ function fail(where, message) {
37
+ throw new MomentError(`${where}: ${message}`);
38
+ }
39
+ function parseStep(raw, where) {
40
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
41
+ fail(where, `expected a step like { "click": "Save" }, got ${JSON.stringify(raw)}`);
42
+ }
43
+ const keys = Object.keys(raw);
44
+ if (keys.length !== 1 || !VERBS.includes(keys[0])) {
45
+ fail(where, `a step has exactly one of ${VERBS.join(', ')} (got ${keys.join(', ') || 'nothing'})`);
46
+ }
47
+ const verb = keys[0];
48
+ const value = raw[verb];
49
+ const text = (v) => typeof v === 'string' && v.trim() ? v : null;
50
+ switch (verb) {
51
+ case 'click':
52
+ case 'hover': {
53
+ const target = text(value);
54
+ if (!target)
55
+ fail(where, `"${verb}" needs the text of what to ${verb}, e.g. { "${verb}": "Save" }`);
56
+ return { verb, target };
57
+ }
58
+ case 'type': {
59
+ if (!Array.isArray(value) || value.length !== 2 || !text(value[0]) || typeof value[1] !== 'string') {
60
+ fail(where, '"type" needs [field, text], e.g. { "type": ["Email", "ada@example.com"] }');
61
+ }
62
+ return { verb, target: value[0], text: value[1] };
63
+ }
64
+ case 'press': {
65
+ const key = text(value);
66
+ if (!key)
67
+ fail(where, '"press" needs a key, e.g. { "press": "Escape" }');
68
+ return { verb, key };
69
+ }
70
+ case 'scroll': {
71
+ if (typeof value === 'number' && Number.isFinite(value))
72
+ return { verb, px: value };
73
+ const target = text(value);
74
+ if (!target)
75
+ fail(where, '"scroll" needs the text to scroll to, or a number of pixels');
76
+ return { verb, target };
77
+ }
78
+ case 'wait': {
79
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
80
+ fail(where, '"wait" needs milliseconds, e.g. { "wait": 600 }');
81
+ }
82
+ if (value > MAX_WAIT_MS)
83
+ fail(where, `"wait" is in milliseconds; ${value} is over ${MAX_WAIT_MS / 1000}s`);
84
+ return { verb, ms: value };
85
+ }
86
+ }
87
+ }
88
+ /** Validate one Moment. `where` names it in errors, e.g. "Moment 2". */
89
+ export function parseMoment(raw, where) {
90
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
91
+ fail(where, 'expected an object with name, route and steps');
92
+ const name = typeof raw.name === 'string' ? raw.name.trim() : '';
93
+ if (!name)
94
+ fail(where, 'needs a "name" — the caption a reviewer reads, e.g. "Open the account menu"');
95
+ const label = `${where} ("${name}")`;
96
+ const route = typeof raw.route === 'string' ? raw.route.trim() : '';
97
+ if (!route.startsWith('/'))
98
+ fail(label, 'needs a "route" starting with /, e.g. "/settings"');
99
+ const viewport = raw.viewport === undefined ? 'desktop' : typeof raw.viewport === 'string' ? raw.viewport.trim() : '';
100
+ // Checked here, with the rest of the file, so a typo stops the run before
101
+ // any capture instead of costing one Moment after all of them were recorded.
102
+ try {
103
+ parseViewport(viewport);
104
+ }
105
+ catch {
106
+ fail(label, `"viewport" is desktop, tablet, mobile or WxH (got ${JSON.stringify(raw.viewport)})`);
107
+ }
108
+ if (raw.steps !== undefined && !Array.isArray(raw.steps))
109
+ fail(label, '"steps" is a list');
110
+ const steps = (raw.steps ?? []).map((s, i) => parseStep(s, `${label} step ${i + 1}`));
111
+ return { name, route, viewport, steps };
112
+ }
113
+ /** Validate a list of Moments, as found in config or a file. */
114
+ export function parseMoments(raw, source) {
115
+ const list = Array.isArray(raw) ? raw
116
+ : raw && typeof raw === 'object' && Array.isArray(raw.moments) ? raw.moments
117
+ : fail(source, 'expected a list of Moments, or { "moments": [...] }');
118
+ return list.map((m, i) => parseMoment(m, `${source}, Moment ${i + 1}`));
119
+ }
120
+ /** Read a `--moments` file. */
121
+ export function loadMomentsFile(file, cwd = process.cwd()) {
122
+ const full = path.resolve(cwd, file);
123
+ let text;
124
+ try {
125
+ text = fs.readFileSync(full, 'utf-8');
126
+ }
127
+ catch {
128
+ throw new MomentError(`Cannot read the Moments file ${file}.`);
129
+ }
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(text);
133
+ }
134
+ catch (err) {
135
+ throw new MomentError(`${file} is not valid JSON: ${err.message}`);
136
+ }
137
+ return parseMoments(parsed, path.basename(file));
138
+ }
139
+ /** The Moments that run, in priority order, and the names of the rest. */
140
+ export function selectMoments(moments) {
141
+ return { run: moments.slice(0, MAX_MOMENTS), skipped: moments.slice(MAX_MOMENTS).map(m => m.name) };
142
+ }
143
+ /** A name as a reviewer reads it: “Open the account menu”. */
144
+ export function quote(s) {
145
+ return `“${s}”`;
146
+ }
147
+ /** A step as a reviewer would say it: `Click “Save”`. Shown under the clip. */
148
+ export function describeStep(step) {
149
+ switch (step.verb) {
150
+ case 'click': return `Click ${quote(step.target)}`;
151
+ case 'hover': return `Hover ${quote(step.target)}`;
152
+ case 'type': return `Type ${quote(step.text)} into ${quote(step.target)}`;
153
+ case 'press': return `Press ${step.key}`;
154
+ case 'scroll': return step.target ? `Scroll to ${quote(step.target)}` : `Scroll ${step.px >= 0 ? 'down' : 'up'} ${Math.abs(step.px)}px`;
155
+ case 'wait': return 'Wait';
156
+ }
157
+ }
158
+ /** One line for a clip that ran long, or null. */
159
+ export function longClipNote(name, durationMs) {
160
+ if (durationMs <= LONG_CLIP_MS)
161
+ return null;
162
+ return `${quote(name)} is ${Math.round(durationMs / 1000)}s — clips under ${TARGET_CLIP_S}s get watched; consider splitting it into smaller Moments.`;
163
+ }
164
+ /** One line naming the Moments over the limit, or null. */
165
+ export function skippedNote(skipped) {
166
+ if (!skipped.length)
167
+ return null;
168
+ return `Recorded the first ${MAX_MOMENTS} Moments; also listed, not recorded: ${skipped.map(quote).join(', ')}.`;
169
+ }
170
+ //# sourceMappingURL=moments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moments.js","sourceRoot":"","sources":["../src/moments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Record one side of a Moment: load the page, then play its steps while the
3
+ * browser streams frames.
4
+ *
5
+ * Frames come from CDP `Page.startScreencast`, which sends a frame only when
6
+ * pixels change — about every 17 ms during a transition, nothing while the
7
+ * page sits still (measured; docs/internal/video-spike.md). Playwright's own
8
+ * `recordVideo` was the alternative: fixed at 25 fps and recording from the
9
+ * moment the context opens, load and all.
10
+ *
11
+ * Every step's start time is marked, so `timeline.ts` can line the two sides
12
+ * up step by step. A step that fails ends the recording at the moment it
13
+ * began: the seconds spent looking for a button that is not there are not
14
+ * something a reviewer should watch.
15
+ */
16
+ import { AuthOptions, ViewportSize } from './types.js';
17
+ import { Moment } from './moments.js';
18
+ import { SideTiming } from './timeline.js';
19
+ /** Still frames before the first step, so the viewer sees the page first. */
20
+ export declare const LEAD_IN_MS = 500;
21
+ /** After each step, so its result is on screen before the next one starts. */
22
+ export declare const BREATH_MS = 400;
23
+ /** After the last step, so the clip ends on the result. */
24
+ export declare const TAIL_MS = 1000;
25
+ /** A Moment with no steps records this much of the page loading, for entrance animations. */
26
+ export declare const LOAD_ONLY_MS = 3000;
27
+ /**
28
+ * How long a step looks for its target. Short, because a missing target is
29
+ * the normal case on Pre for a new feature, and every second spent looking is
30
+ * a second the whole run waits.
31
+ */
32
+ export declare const FIND_TIMEOUT_MS = 3000;
33
+ /** How long a found target's action (click, hover, typing) may take. */
34
+ export declare const STEP_TIMEOUT_MS = 5000;
35
+ export interface Frame {
36
+ t: number;
37
+ data: Buffer;
38
+ }
39
+ /** Where a pointer step acted, in viewport CSS pixels, for the click marker. */
40
+ export interface PointerMark {
41
+ step: number;
42
+ x: number;
43
+ y: number;
44
+ }
45
+ export interface SideRecording extends SideTiming {
46
+ viewport: ViewportSize;
47
+ frames: Frame[];
48
+ pointers: PointerMark[];
49
+ /** HTTP status of the page. */
50
+ status?: number;
51
+ /** The step that could not be done, and why, in plain words. */
52
+ failure?: {
53
+ step: number;
54
+ message: string;
55
+ };
56
+ }
57
+ export declare class StepError extends Error {
58
+ }
59
+ /**
60
+ * Record `moment` on `url`. Throws NavigationError / HttpStatusError when the
61
+ * page cannot be loaded; returns a recording with `failure` set when a step
62
+ * cannot be done. A page that answers 4xx/5xx comes back with its status and
63
+ * no frames, for the caller to phrase.
64
+ */
65
+ export declare function recordSide(url: string, moment: Moment, viewport: ViewportSize, opts: {
66
+ auth?: AuthOptions;
67
+ maxWidth: number;
68
+ }): Promise<SideRecording>;
69
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,EAAE,MAAM,EAAQ,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,6EAA6E;AAC7E,eAAO,MAAM,UAAU,MAAM,CAAC;AAC9B,8EAA8E;AAC9E,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,2DAA2D;AAC3D,eAAO,MAAM,OAAO,OAAO,CAAC;AAC5B,6FAA6F;AAC7F,eAAO,MAAM,YAAY,OAAO,CAAC;AACjC;;;;GAIG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,wEAAwE;AACxE,eAAO,MAAM,eAAe,OAAO,CAAC;AAOpC,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,qBAAa,SAAU,SAAQ,KAAK;CAAG;AAwHvC;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAwD5J"}