screenci 0.0.4 → 0.0.6

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 (94) hide show
  1. package/README.md +227 -0
  2. package/cli.ts +1111 -0
  3. package/dist/cli.d.ts +4 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +896 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/e2e/instrument.e2e.d.ts +2 -0
  8. package/dist/e2e/instrument.e2e.d.ts.map +1 -0
  9. package/dist/e2e/instrument.e2e.js +661 -0
  10. package/dist/e2e/instrument.e2e.js.map +1 -0
  11. package/dist/index.d.ts +18 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/playwright.config.d.ts +3 -0
  16. package/dist/playwright.config.d.ts.map +1 -0
  17. package/dist/playwright.config.js +21 -0
  18. package/dist/playwright.config.js.map +1 -0
  19. package/dist/reporter.d.ts +9 -0
  20. package/dist/reporter.d.ts.map +1 -0
  21. package/dist/reporter.js +49 -0
  22. package/dist/reporter.js.map +1 -0
  23. package/dist/src/asset.d.ts +90 -0
  24. package/dist/src/asset.d.ts.map +1 -0
  25. package/dist/src/asset.js +74 -0
  26. package/dist/src/asset.js.map +1 -0
  27. package/dist/src/autoZoom.d.ts +40 -0
  28. package/dist/src/autoZoom.d.ts.map +1 -0
  29. package/dist/src/autoZoom.js +88 -0
  30. package/dist/src/autoZoom.js.map +1 -0
  31. package/dist/src/caption.d.ts +152 -0
  32. package/dist/src/caption.d.ts.map +1 -0
  33. package/dist/src/caption.js +240 -0
  34. package/dist/src/caption.js.map +1 -0
  35. package/dist/src/caption.test-d.d.ts +2 -0
  36. package/dist/src/caption.test-d.d.ts.map +1 -0
  37. package/dist/src/caption.test-d.js +50 -0
  38. package/dist/src/caption.test-d.js.map +1 -0
  39. package/dist/src/config.d.ts +42 -0
  40. package/dist/src/config.d.ts.map +1 -0
  41. package/dist/src/config.js +147 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/defaults.d.ts +63 -0
  44. package/dist/src/defaults.d.ts.map +1 -0
  45. package/dist/src/defaults.js +66 -0
  46. package/dist/src/defaults.js.map +1 -0
  47. package/dist/src/dimensions.d.ts +29 -0
  48. package/dist/src/dimensions.d.ts.map +1 -0
  49. package/dist/src/dimensions.js +47 -0
  50. package/dist/src/dimensions.js.map +1 -0
  51. package/dist/src/events.d.ts +203 -0
  52. package/dist/src/events.d.ts.map +1 -0
  53. package/dist/src/events.js +227 -0
  54. package/dist/src/events.js.map +1 -0
  55. package/dist/src/hide.d.ts +27 -0
  56. package/dist/src/hide.d.ts.map +1 -0
  57. package/dist/src/hide.js +49 -0
  58. package/dist/src/hide.js.map +1 -0
  59. package/dist/src/instrument.d.ts +15 -0
  60. package/dist/src/instrument.d.ts.map +1 -0
  61. package/dist/src/instrument.js +910 -0
  62. package/dist/src/instrument.js.map +1 -0
  63. package/dist/src/logger.d.ts +7 -0
  64. package/dist/src/logger.d.ts.map +1 -0
  65. package/dist/src/logger.js +13 -0
  66. package/dist/src/logger.js.map +1 -0
  67. package/dist/src/reporter.d.ts +9 -0
  68. package/dist/src/reporter.d.ts.map +1 -0
  69. package/dist/src/reporter.js +50 -0
  70. package/dist/src/reporter.js.map +1 -0
  71. package/dist/src/sanitize.d.ts +5 -0
  72. package/dist/src/sanitize.d.ts.map +1 -0
  73. package/dist/src/sanitize.js +11 -0
  74. package/dist/src/sanitize.js.map +1 -0
  75. package/dist/src/types.d.ts +544 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +2 -0
  78. package/dist/src/types.js.map +1 -0
  79. package/dist/src/video.d.ts +138 -0
  80. package/dist/src/video.d.ts.map +1 -0
  81. package/dist/src/video.js +415 -0
  82. package/dist/src/video.js.map +1 -0
  83. package/dist/src/voices.d.ts +60 -0
  84. package/dist/src/voices.d.ts.map +1 -0
  85. package/dist/src/voices.js +42 -0
  86. package/dist/src/voices.js.map +1 -0
  87. package/dist/src/xvfb.d.ts +22 -0
  88. package/dist/src/xvfb.d.ts.map +1 -0
  89. package/dist/src/xvfb.js +87 -0
  90. package/dist/src/xvfb.js.map +1 -0
  91. package/dist/tsconfig.tsbuildinfo +1 -0
  92. package/package.json +45 -4
  93. package/bin/index.js +0 -3
  94. package/index.js +0 -1
@@ -0,0 +1,152 @@
1
+ import type { IEventRecorder } from './events.js';
2
+ import type { VoiceKey, VoiceForLang, Lang } from './voices.js';
3
+ export declare const ONE_FRAME_MS: number;
4
+ /** A percentage string, e.g. `'50%'` or `'100%'`. */
5
+ export type Percentage = `${number}%`;
6
+ export declare function setSleepFn(fn: (ms: number) => void): void;
7
+ export declare function setActiveCaptionRecorder(recorder: IEventRecorder | null): void;
8
+ export declare function resetCaptionChain(): void;
9
+ export interface CaptionController {
10
+ /**
11
+ * Begins voiceover audio and shows the caption overlay.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * await captions.intro.start()
16
+ * await page.goto('/dashboard')
17
+ * await captions.intro.end()
18
+ * ```
19
+ */
20
+ start(): Promise<void>;
21
+ /**
22
+ * Pauses execution until the voiceover audio reaches the given playback position.
23
+ *
24
+ * Use this to time a page interaction to a specific moment in the narration —
25
+ * for example, clicking a button right as the voiceover mentions it.
26
+ *
27
+ * @param progress - Playback position as a percentage string, e.g. `'50%'`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * await captions.intro.start()
32
+ * await captions.intro.waitUntil('70%') // wait until 70% of audio has played
33
+ * await page.locator('#cta').click() // then click
34
+ * await captions.intro.end()
35
+ * ```
36
+ */
37
+ waitUntil(progress: string): Promise<void>;
38
+ /**
39
+ * Hides the caption overlay and stops voiceover playback.
40
+ * Always call this after every `start()`.
41
+ */
42
+ end(): Promise<void>;
43
+ }
44
+ /** A single caption value in a multi-language map: either TTS text or a file-based entry. */
45
+ export type CaptionMapValue = string | {
46
+ path: string;
47
+ subtitle?: string;
48
+ };
49
+ export type Captions<T extends Record<string, CaptionMapValue>> = {
50
+ [K in keyof T]: CaptionController;
51
+ };
52
+ export type VideoCaptionEntry = string | {
53
+ path: string;
54
+ subtitle?: string;
55
+ };
56
+ export type VideoCaptions<T extends Record<string, VideoCaptionEntry>> = {
57
+ [K in keyof T]: CaptionController;
58
+ };
59
+ /** Converts a union type to an intersection: `A | B` → `A & B` */
60
+ type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
61
+ /**
62
+ * Produces a record requiring every key that appears in any language's captions.
63
+ * Uses each language's key set with CaptionMapValue values before intersecting,
64
+ * so value types don't conflict (e.g. string vs { path, subtitle } for the same key).
65
+ */
66
+ type AllCaptions<M extends Partial<Record<Lang, {
67
+ captions: Record<string, CaptionMapValue>;
68
+ }>>> = UnionToIntersection<{
69
+ [L in keyof M]: M[L] extends {
70
+ captions: infer C;
71
+ } ? Record<keyof C & string, CaptionMapValue> : never;
72
+ }[keyof M]> & Record<string, CaptionMapValue>;
73
+ /**
74
+ * Creates a set of typed caption controllers, one per key in the map.
75
+ *
76
+ * Each controller has `start()`, `waitUntil(percent)`, and `end()`.
77
+ * At render time screenci sends the caption text to ElevenLabs, generates
78
+ * a voiceover, and syncs the audio to the recording. You write text; the
79
+ * voice is handled for you.
80
+ *
81
+ * TypeScript enforces that every language has the same caption keys.
82
+ * Forget a translation key → compile error.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const captions = createCaptions({
87
+ * en: { voice: voices.en.Jude, captions: { intro: 'Welcome.' } },
88
+ * fi: { voice: voices.fi.Martti, captions: { intro: 'Tervetuloa.' } },
89
+ * })
90
+ * ```
91
+ */
92
+ export declare function createCaptions<M extends Partial<Record<Lang, {
93
+ voice: VoiceKey;
94
+ captions: Record<string, CaptionMapValue>;
95
+ }>>>(languagesMap: M & {
96
+ [L in keyof M]: {
97
+ voice: VoiceForLang<L & string>;
98
+ captions: AllCaptions<M>;
99
+ };
100
+ }): Captions<AllCaptions<M>>;
101
+ type RequireAllSameVideoKeys<M extends Partial<Record<Lang, {
102
+ captions: Record<string, VideoCaptionEntry>;
103
+ }>>> = {
104
+ [L in keyof M]: {
105
+ captions: UnionToIntersection<M[keyof M] extends {
106
+ captions: infer C;
107
+ } ? C : never>;
108
+ };
109
+ };
110
+ /**
111
+ * Creates caption controllers backed by pre-recorded asset files instead of
112
+ * TTS-generated audio. Each entry maps a name to either an asset path string
113
+ * or an object with `assetPath` and an optional `subtitle` text.
114
+ *
115
+ * At render time the asset file is used directly as the voiceover audio.
116
+ * If `subtitle` is provided, words are spread with equal timing across the
117
+ * audio duration (no word-level TTS data available).
118
+ *
119
+ * Same constraints as `createCaptions`: cannot overlap with other captions,
120
+ * and cannot fall inside input events.
121
+ *
122
+ * Two overloads:
123
+ *
124
+ * **1. Single-language:**
125
+ * ```ts
126
+ * const captions = createVideoCaptions({
127
+ * intro: '/assets/intro.mp3',
128
+ * demo: { assetPath: '/assets/demo.mp3', subtitle: 'Watch the demo.' },
129
+ * })
130
+ * ```
131
+ *
132
+ * **2. Multi-language (type-safe):**
133
+ * TypeScript enforces that every language has the same caption keys.
134
+ * ```ts
135
+ * const captions = createVideoCaptions({
136
+ * en: { captions: { intro: '/assets/en/intro.mp3' } },
137
+ * fi: { captions: { intro: '/assets/fi/intro.mp3' } },
138
+ * })
139
+ * ```
140
+ */
141
+ export declare function createVideoCaptions<T extends Record<string, VideoCaptionEntry>>(captionsMap: T): VideoCaptions<T>;
142
+ export declare function createVideoCaptions<L extends Lang, T extends Record<string, VideoCaptionEntry>>(languagesMap: {
143
+ [K in L]: {
144
+ captions: T;
145
+ };
146
+ } & RequireAllSameVideoKeys<{
147
+ [K in L]: {
148
+ captions: Record<string, VideoCaptionEntry>;
149
+ };
150
+ }>): VideoCaptions<T & Record<string, VideoCaptionEntry>>;
151
+ export {};
152
+ //# sourceMappingURL=caption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caption.d.ts","sourceRoot":"","sources":["../../src/caption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,240 @@
1
+ import { isInsideHide } from './hide.js';
2
+ // One frame at 24fps — ensures at least one rendered frame captures each caption state.
3
+ export const ONE_FRAME_MS = 1000 / 24;
4
+ function parsePercentage(percent) {
5
+ const value = parseFloat(percent);
6
+ if (!isFinite(value) || value < 0 || value > 100) {
7
+ throw new Error(`Invalid percentage: "${percent}". Must be a finite number between 0 and 100 followed by %.`);
8
+ }
9
+ return value;
10
+ }
11
+ // Blocking sleep — spin until the elapsed time has passed
12
+ let sleepFn = (ms) => {
13
+ const end = performance.now() + ms;
14
+ while (performance.now() < end) {
15
+ /* spin */
16
+ }
17
+ };
18
+ export function setSleepFn(fn) {
19
+ sleepFn = fn;
20
+ }
21
+ let activeRecorder = null;
22
+ let captionStarted = false;
23
+ export function setActiveCaptionRecorder(recorder) {
24
+ activeRecorder = recorder;
25
+ }
26
+ export function resetCaptionChain() {
27
+ captionStarted = false;
28
+ }
29
+ function captionWaitEnd() {
30
+ if (activeRecorder === null)
31
+ return;
32
+ if (isInsideHide())
33
+ throw new Error('Cannot call caption.waitEnd inside hide()');
34
+ if (!captionStarted)
35
+ throw new Error('No caption has been started');
36
+ activeRecorder.addCaptionEnd();
37
+ sleepFn(2 * ONE_FRAME_MS);
38
+ }
39
+ function captionWaitUntil(percent) {
40
+ if (activeRecorder === null)
41
+ return;
42
+ if (isInsideHide())
43
+ throw new Error('Cannot call caption.waitUntil inside hide()');
44
+ if (!captionStarted)
45
+ throw new Error('No caption has been started');
46
+ const percentage = parsePercentage(percent);
47
+ activeRecorder.addCaptionUntil(percentage);
48
+ sleepFn(2 * ONE_FRAME_MS);
49
+ }
50
+ /**
51
+ * Creates a set of typed caption controllers, one per key in the map.
52
+ *
53
+ * Each controller has `start()`, `waitUntil(percent)`, and `end()`.
54
+ * At render time screenci sends the caption text to ElevenLabs, generates
55
+ * a voiceover, and syncs the audio to the recording. You write text; the
56
+ * voice is handled for you.
57
+ *
58
+ * TypeScript enforces that every language has the same caption keys.
59
+ * Forget a translation key → compile error.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const captions = createCaptions({
64
+ * en: { voice: voices.en.Jude, captions: { intro: 'Welcome.' } },
65
+ * fi: { voice: voices.fi.Martti, captions: { intro: 'Tervetuloa.' } },
66
+ * })
67
+ * ```
68
+ */
69
+ export function createCaptions(languagesMap) {
70
+ return buildMultiLangCaptions(languagesMap);
71
+ }
72
+ function buildMultiLangCaptions(languagesMap) {
73
+ const langs = Object.keys(languagesMap);
74
+ const firstLang = langs[0];
75
+ if (firstLang === undefined)
76
+ return {};
77
+ const result = {};
78
+ for (const key in languagesMap[firstLang].captions) {
79
+ const keyStr = key;
80
+ // Determine if any language uses a file-based value for this key.
81
+ // If so, use videoCaptionStart with mixed translations (file or TTS per lang).
82
+ // Otherwise use captionStart with text translations (existing behaviour).
83
+ const hasFileEntry = langs.some((lang) => {
84
+ const val = languagesMap[lang].captions[keyStr];
85
+ return val !== undefined && typeof val !== 'string';
86
+ });
87
+ if (hasFileEntry) {
88
+ const videoTranslations = {};
89
+ for (const lang of langs) {
90
+ const val = languagesMap[lang].captions[keyStr];
91
+ if (val === undefined)
92
+ continue;
93
+ if (typeof val === 'string') {
94
+ videoTranslations[lang] = {
95
+ text: val,
96
+ voice: languagesMap[lang].voice,
97
+ };
98
+ }
99
+ else {
100
+ const fileTrans = {
101
+ assetPath: val.path,
102
+ ...(val.subtitle !== undefined && { subtitle: val.subtitle }),
103
+ };
104
+ videoTranslations[lang] = fileTrans;
105
+ }
106
+ }
107
+ result[key] = {
108
+ async start() {
109
+ if (activeRecorder === null)
110
+ return;
111
+ if (isInsideHide())
112
+ throw new Error('Cannot call caption.start inside hide()');
113
+ captionStarted = true;
114
+ sleepFn(2 * ONE_FRAME_MS);
115
+ activeRecorder.addVideoCaptionStart(keyStr, undefined, undefined, videoTranslations);
116
+ },
117
+ async waitUntil(progress) {
118
+ captionWaitUntil(progress);
119
+ },
120
+ async end() {
121
+ captionWaitEnd();
122
+ },
123
+ };
124
+ }
125
+ else {
126
+ const textTranslations = {};
127
+ for (const lang of langs) {
128
+ const val = languagesMap[lang].captions[keyStr];
129
+ if (val !== undefined && typeof val === 'string') {
130
+ textTranslations[lang] = {
131
+ text: val,
132
+ voice: languagesMap[lang].voice,
133
+ };
134
+ }
135
+ }
136
+ result[key] = {
137
+ async start() {
138
+ if (activeRecorder === null)
139
+ return;
140
+ if (isInsideHide())
141
+ throw new Error('Cannot call caption.start inside hide()');
142
+ captionStarted = true;
143
+ sleepFn(2 * ONE_FRAME_MS);
144
+ activeRecorder.addCaptionStart('', keyStr, undefined, textTranslations);
145
+ },
146
+ async waitUntil(progress) {
147
+ captionWaitUntil(progress);
148
+ },
149
+ async end() {
150
+ captionWaitEnd();
151
+ },
152
+ };
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+ function entryToVideoTranslation(entry) {
158
+ if (typeof entry === 'string')
159
+ return { assetPath: entry };
160
+ return {
161
+ assetPath: entry.path,
162
+ ...(entry.subtitle !== undefined && { subtitle: entry.subtitle }),
163
+ };
164
+ }
165
+ export function createVideoCaptions(firstArg) {
166
+ // Distinguish single-language vs multi-language by checking if any value has a `captions` key
167
+ const firstValue = Object.values(firstArg)[0];
168
+ if (firstValue !== undefined &&
169
+ typeof firstValue === 'object' &&
170
+ !Array.isArray(firstValue) &&
171
+ 'captions' in firstValue &&
172
+ typeof firstValue.captions === 'object') {
173
+ return buildMultiLangVideoCaptions(firstArg);
174
+ }
175
+ return buildSingleLangVideoCaptions(firstArg);
176
+ }
177
+ function buildSingleLangVideoCaptions(captionsMap) {
178
+ const result = {};
179
+ for (const key in captionsMap) {
180
+ const entry = captionsMap[key];
181
+ if (entry === undefined)
182
+ continue;
183
+ const assetPath = typeof entry === 'string' ? entry : entry.path;
184
+ const subtitle = typeof entry === 'string' ? undefined : entry.subtitle;
185
+ result[key] = {
186
+ async start() {
187
+ if (activeRecorder === null)
188
+ return;
189
+ if (isInsideHide())
190
+ throw new Error('Cannot call videoCaption.start inside hide()');
191
+ captionStarted = true;
192
+ sleepFn(2 * ONE_FRAME_MS);
193
+ activeRecorder.addVideoCaptionStart(key, assetPath, subtitle);
194
+ },
195
+ async waitUntil(progress) {
196
+ captionWaitUntil(progress);
197
+ },
198
+ async end() {
199
+ captionWaitEnd();
200
+ },
201
+ };
202
+ }
203
+ return result;
204
+ }
205
+ function buildMultiLangVideoCaptions(languagesMap) {
206
+ const langs = Object.keys(languagesMap);
207
+ const firstLang = langs[0];
208
+ if (firstLang === undefined)
209
+ return {};
210
+ const result = {};
211
+ for (const key in languagesMap[firstLang].captions) {
212
+ const keyStr = key;
213
+ const translations = {};
214
+ for (const lang of langs) {
215
+ const entry = languagesMap[lang].captions[keyStr];
216
+ if (entry !== undefined) {
217
+ translations[lang] = entryToVideoTranslation(entry);
218
+ }
219
+ }
220
+ result[key] = {
221
+ async start() {
222
+ if (activeRecorder === null)
223
+ return;
224
+ if (isInsideHide())
225
+ throw new Error('Cannot call videoCaption.start inside hide()');
226
+ captionStarted = true;
227
+ sleepFn(2 * ONE_FRAME_MS);
228
+ activeRecorder.addVideoCaptionStart(keyStr, undefined, undefined, translations);
229
+ },
230
+ async waitUntil(progress) {
231
+ captionWaitUntil(progress);
232
+ },
233
+ async end() {
234
+ captionWaitEnd();
235
+ },
236
+ };
237
+ }
238
+ return result;
239
+ }
240
+ //# sourceMappingURL=caption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caption.js","sourceRoot":"","sources":["../../src/caption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=caption.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caption.test-d.d.ts","sourceRoot":"","sources":["../../src/caption.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ import { describe, it, assertType } from 'vitest';
2
+ import { createCaptions } from './caption.js';
3
+ import { voices } from './voices.js';
4
+ describe('createCaptions type constraints', () => {
5
+ it('accepts matching keys across all languages', () => {
6
+ createCaptions({
7
+ en: {
8
+ voice: voices.en.Jude,
9
+ captions: { intro: 'Hello', outro: 'Bye' },
10
+ },
11
+ fi: {
12
+ voice: voices.fi.Martti,
13
+ captions: { intro: 'Hei', outro: 'Näkemiin' },
14
+ },
15
+ });
16
+ });
17
+ it('accepts mixed value types (string vs file object) for the same key', () => {
18
+ createCaptions({
19
+ en: {
20
+ voice: voices.en.Jude,
21
+ captions: {
22
+ intro: 'Hello',
23
+ clip: { path: '/clip.mp4', subtitle: 'Watch' },
24
+ },
25
+ },
26
+ fi: {
27
+ voice: voices.fi.Martti,
28
+ captions: { intro: 'Hei', clip: 'Katso' },
29
+ },
30
+ });
31
+ });
32
+ it('rejects a language with a missing caption key', () => {
33
+ createCaptions({
34
+ en: {
35
+ voice: voices.en.Jude,
36
+ captions: { intro: 'Hello', outro: 'Bye' },
37
+ },
38
+ fi: {
39
+ voice: voices.fi.Martti,
40
+ // @ts-expect-error — 'outro' is missing
41
+ captions: { intro: 'Hei' },
42
+ },
43
+ });
44
+ });
45
+ it('rejects a wrong-language voice', () => {
46
+ // @ts-expect-error — fi voice is not assignable to VoiceForLang<'en'>
47
+ assertType(voices.fi.Martti);
48
+ });
49
+ });
50
+ //# sourceMappingURL=caption.test-d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caption.test-d.js","sourceRoot":"","sources":["../../src/caption.test-d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,cAAc,CAAC;YACb,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI;gBACrB,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE;aAC3C;YACD,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM;gBACvB,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE;aAC9C;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,cAAc,CAAC;YACb,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI;gBACrB,QAAQ,EAAE;oBACR,KAAK,EAAE,OAAO;oBACd,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE;iBAC/C;aACF;YACD,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM;gBACvB,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE;aAC1C;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,cAAc,CAAC;YACb,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI;gBACrB,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE;aAC3C;YACD,EAAE,EAAE;gBACF,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM;gBACvB,wCAAwC;gBACxC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;aAC3B;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,sEAAsE;QACtE,UAAU,CAAqB,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,42 @@
1
+ import type { ScreenCIConfig, ExtendedScreenCIConfig } from './types.js';
2
+ /**
3
+ * Defines a screenci configuration file.
4
+ *
5
+ * Extends Playwright's config with screenci-specific options and enforces
6
+ * settings required for reliable video recording. Some Playwright options
7
+ * are locked and cannot be set — `workers`, `fullyParallel`, `retries`,
8
+ * `testDir`, and `testMatch`. Attempting to set them throws at startup.
9
+ *
10
+ * @example
11
+ * Minimal — all options have sensible defaults:
12
+ * ```ts
13
+ * import { defineConfig } from 'screenci'
14
+ * export default defineConfig({})
15
+ * ```
16
+ *
17
+ * @example
18
+ * Full config:
19
+ * ```ts
20
+ * import { defineConfig } from 'screenci'
21
+ *
22
+ * export default defineConfig({
23
+ * projectName: 'my-project',
24
+ * videoDir: './videos',
25
+ * use: {
26
+ * baseURL: 'https://app.example.com',
27
+ * recordOptions: {
28
+ * aspectRatio: '16:9', // '16:9' | '9:16' | '1:1' | '4:3' | ...
29
+ * quality: '1080p', // '720p' | '1080p' | '1440p' | '2160p'
30
+ * fps: 30, // 24 | 30 | 60
31
+ * },
32
+ * trace: 'retain-on-failure',
33
+ * sendTraces: true,
34
+ * },
35
+ * })
36
+ * ```
37
+ *
38
+ * @param config - screenci configuration options
39
+ * @returns Extended Playwright configuration with enforced sequential execution settings
40
+ */
41
+ export declare function defineConfig(config: ScreenCIConfig): ExtendedScreenCIConfig;
42
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAyBxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,sBAAsB,CA+H3E"}
@@ -0,0 +1,147 @@
1
+ import { DEFAULT_VIDEO_DIR, DEFAULT_TRACE, DEFAULT_SEND_TRACES, DEFAULT_TIMEOUT, DEFAULT_ACTION_TIMEOUT, DEFAULT_NAVIGATION_TIMEOUT, } from './defaults.js';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, resolve } from 'path';
4
+ import { existsSync } from 'fs';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ // Resolve reporter path - try .js first (production), then .ts (development)
8
+ const reporterPathJs = resolve(__dirname, 'reporter.js');
9
+ const reporterPathTs = resolve(__dirname, 'reporter.ts');
10
+ const reporterPath = existsSync(reporterPathJs)
11
+ ? reporterPathJs
12
+ : reporterPathTs;
13
+ /**
14
+ * Defines a screenci configuration file.
15
+ *
16
+ * Extends Playwright's config with screenci-specific options and enforces
17
+ * settings required for reliable video recording. Some Playwright options
18
+ * are locked and cannot be set — `workers`, `fullyParallel`, `retries`,
19
+ * `testDir`, and `testMatch`. Attempting to set them throws at startup.
20
+ *
21
+ * @example
22
+ * Minimal — all options have sensible defaults:
23
+ * ```ts
24
+ * import { defineConfig } from 'screenci'
25
+ * export default defineConfig({})
26
+ * ```
27
+ *
28
+ * @example
29
+ * Full config:
30
+ * ```ts
31
+ * import { defineConfig } from 'screenci'
32
+ *
33
+ * export default defineConfig({
34
+ * projectName: 'my-project',
35
+ * videoDir: './videos',
36
+ * use: {
37
+ * baseURL: 'https://app.example.com',
38
+ * recordOptions: {
39
+ * aspectRatio: '16:9', // '16:9' | '9:16' | '1:1' | '4:3' | ...
40
+ * quality: '1080p', // '720p' | '1080p' | '1440p' | '2160p'
41
+ * fps: 30, // 24 | 30 | 60
42
+ * },
43
+ * trace: 'retain-on-failure',
44
+ * sendTraces: true,
45
+ * },
46
+ * })
47
+ * ```
48
+ *
49
+ * @param config - screenci configuration options
50
+ * @returns Extended Playwright configuration with enforced sequential execution settings
51
+ */
52
+ export function defineConfig(config) {
53
+ // Add the video name validator reporter if not already present
54
+ const existingReporters = config.reporter
55
+ ? Array.isArray(config.reporter)
56
+ ? config.reporter
57
+ : [config.reporter]
58
+ : ['list'];
59
+ // Check if our validator is already added
60
+ const hasValidator = existingReporters.some((r) => {
61
+ if (Array.isArray(r)) {
62
+ return r[0] === reporterPath || r[0]?.toString().endsWith('reporter.js');
63
+ }
64
+ return r === reporterPath || r?.toString().endsWith('reporter.js');
65
+ });
66
+ // Convert all reporters to tuple format for Playwright validation
67
+ const normalizedReporters = existingReporters.map((r) => Array.isArray(r) ? r : [r]);
68
+ // Check if list reporter is present for CLI output
69
+ const hasListReporter = normalizedReporters.some((r) => {
70
+ const name = Array.isArray(r) ? r[0] : r;
71
+ return name === 'list' || name === 'line' || name === 'dot';
72
+ });
73
+ // Build reporters array with proper typing - all reporters must be tuples
74
+ // Add list reporter if no CLI reporter is present
75
+ const reportersWithCLI = hasListReporter
76
+ ? normalizedReporters
77
+ : [['list'], ...normalizedReporters];
78
+ const reporters = hasValidator
79
+ ? reportersWithCLI
80
+ : [[reporterPath, {}], ...reportersWithCLI];
81
+ // Runtime check for viewport (check before testDir since test objects may not have testDir defined)
82
+ if (config.use && 'viewport' in config.use) {
83
+ throw new Error('screenci does not support "viewport" option. ' +
84
+ 'The viewport is automatically set based on the recordOptions.resolution. ' +
85
+ 'Use recordOptions.resolution to control the viewport size.');
86
+ }
87
+ // Runtime check for viewport in projects
88
+ if (config.projects) {
89
+ for (const project of config.projects) {
90
+ if (project.use && 'viewport' in project.use) {
91
+ throw new Error(`screenci does not support "viewport" option in project "${project.name}". ` +
92
+ 'The viewport is automatically set based on the recordOptions.resolution. ' +
93
+ 'Use recordOptions.resolution to control the viewport size.');
94
+ }
95
+ }
96
+ }
97
+ // Runtime check for testDir
98
+ if (Object.prototype.hasOwnProperty.call(config, 'testDir') &&
99
+ config.testDir !== undefined) {
100
+ throw new Error('screenci does not support "testDir" option. ' +
101
+ 'Use "videoDir" instead to specify the directory containing your *.video.* files. ' +
102
+ 'Defaults to "./videos".');
103
+ }
104
+ // Runtime check for testMatch
105
+ if ('testMatch' in config) {
106
+ throw new Error('screenci does not support "testMatch" option. ' +
107
+ 'screenci automatically configures tests to only run *.video.* files.');
108
+ }
109
+ // Runtime check for workers
110
+ if ('workers' in config) {
111
+ throw new Error('screenci does not support "workers" option. ' +
112
+ 'Tests must run sequentially to ensure proper video recording. ' +
113
+ 'screenci automatically configures workers to 1.');
114
+ }
115
+ // Runtime check for fullyParallel
116
+ if ('fullyParallel' in config) {
117
+ throw new Error('screenci does not support "fullyParallel" option. ' +
118
+ 'Tests must run sequentially to ensure proper video recording. ' +
119
+ 'screenci automatically sets fullyParallel to false.');
120
+ }
121
+ // Runtime check for retries
122
+ if ('retries' in config) {
123
+ throw new Error('screenci does not support "retries" option. ' +
124
+ 'Tests must succeed immediately or fail to ensure proper video recording. ' +
125
+ 'screenci automatically sets retries to 0.');
126
+ }
127
+ const { videoDir, ...rest } = config;
128
+ // Force sequential execution with single worker and no retries, map videoDir to testDir
129
+ return {
130
+ testDir: videoDir ?? DEFAULT_VIDEO_DIR,
131
+ testMatch: '**/*.video.?(c|m)[jt]s?(x)',
132
+ ...rest,
133
+ reporter: reporters,
134
+ use: {
135
+ ...rest.use,
136
+ trace: rest.use?.trace ?? DEFAULT_TRACE,
137
+ sendTraces: rest.use?.sendTraces ?? DEFAULT_SEND_TRACES,
138
+ actionTimeout: rest.use?.actionTimeout ?? DEFAULT_ACTION_TIMEOUT,
139
+ navigationTimeout: rest.use?.navigationTimeout ?? DEFAULT_NAVIGATION_TIMEOUT,
140
+ },
141
+ timeout: rest.timeout ?? DEFAULT_TIMEOUT,
142
+ fullyParallel: false,
143
+ workers: 1,
144
+ retries: 0,
145
+ };
146
+ }
147
+ //# sourceMappingURL=config.js.map