screenci 0.0.69 → 0.0.70

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 (145) hide show
  1. package/README.md +45 -122
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +12 -4
  4. package/dist/cli.js.map +1 -1
  5. package/dist/docs/manifest.d.ts +54 -23
  6. package/dist/docs/manifest.d.ts.map +1 -1
  7. package/dist/docs/manifest.js +18 -7
  8. package/dist/docs/manifest.js.map +1 -1
  9. package/dist/docs/video-sources/cli.screenci.d.ts +2 -0
  10. package/dist/docs/video-sources/cli.screenci.d.ts.map +1 -0
  11. package/dist/docs/video-sources/{cli.video.js → cli.screenci.js} +1 -1
  12. package/dist/docs/video-sources/cli.screenci.js.map +1 -0
  13. package/dist/docs/video-sources/installation.screenci.d.ts +2 -0
  14. package/dist/docs/video-sources/installation.screenci.d.ts.map +1 -0
  15. package/dist/docs/video-sources/{installation.video.js → installation.screenci.js} +1 -1
  16. package/dist/docs/video-sources/installation.screenci.js.map +1 -0
  17. package/dist/docs/video-sources/landing.screenci.d.ts +2 -0
  18. package/dist/docs/video-sources/landing.screenci.d.ts.map +1 -0
  19. package/dist/docs/video-sources/{landing.video.js → landing.screenci.js} +1 -1
  20. package/dist/docs/video-sources/landing.screenci.js.map +1 -0
  21. package/dist/docs/video-sources/locator-highlight-animated.screenci.d.ts +2 -0
  22. package/dist/docs/video-sources/locator-highlight-animated.screenci.d.ts.map +1 -0
  23. package/dist/docs/video-sources/locator-highlight-animated.screenci.js +53 -0
  24. package/dist/docs/video-sources/locator-highlight-animated.screenci.js.map +1 -0
  25. package/dist/docs/video-sources/locator-highlight-still.screenci.d.ts +2 -0
  26. package/dist/docs/video-sources/locator-highlight-still.screenci.d.ts.map +1 -0
  27. package/dist/docs/video-sources/locator-highlight-still.screenci.js +24 -0
  28. package/dist/docs/video-sources/locator-highlight-still.screenci.js.map +1 -0
  29. package/dist/docs/video-sources/product-pitch.screenci.d.ts +2 -0
  30. package/dist/docs/video-sources/product-pitch.screenci.d.ts.map +1 -0
  31. package/dist/docs/video-sources/product-pitch.screenci.js +136 -0
  32. package/dist/docs/video-sources/product-pitch.screenci.js.map +1 -0
  33. package/dist/docs/video-sources/public-urls.screenci.d.ts +2 -0
  34. package/dist/docs/video-sources/public-urls.screenci.d.ts.map +1 -0
  35. package/dist/docs/video-sources/public-urls.screenci.js +34 -0
  36. package/dist/docs/video-sources/public-urls.screenci.js.map +1 -0
  37. package/dist/docs/video-sources/studio.screenci.d.ts +2 -0
  38. package/dist/docs/video-sources/studio.screenci.d.ts.map +1 -0
  39. package/dist/docs/video-sources/{studio.video.js → studio.screenci.js} +1 -1
  40. package/dist/docs/video-sources/studio.screenci.js.map +1 -0
  41. package/dist/docs/videos.d.ts +36 -15
  42. package/dist/docs/videos.d.ts.map +1 -1
  43. package/dist/docs/videos.js +27 -2
  44. package/dist/docs/videos.js.map +1 -1
  45. package/dist/e2e/screenshot.e2e.d.ts +2 -0
  46. package/dist/e2e/screenshot.e2e.d.ts.map +1 -0
  47. package/dist/e2e/screenshot.e2e.js +73 -0
  48. package/dist/e2e/screenshot.e2e.js.map +1 -0
  49. package/dist/index.d.ts +7 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/src/asset.d.ts +119 -15
  54. package/dist/src/asset.d.ts.map +1 -1
  55. package/dist/src/asset.js +310 -124
  56. package/dist/src/asset.js.map +1 -1
  57. package/dist/src/audio.d.ts +14 -0
  58. package/dist/src/audio.d.ts.map +1 -1
  59. package/dist/src/audio.js +4 -1
  60. package/dist/src/audio.js.map +1 -1
  61. package/dist/src/config.d.ts.map +1 -1
  62. package/dist/src/config.js +9 -3
  63. package/dist/src/config.js.map +1 -1
  64. package/dist/src/contextOptions.d.ts +48 -0
  65. package/dist/src/contextOptions.d.ts.map +1 -0
  66. package/dist/src/contextOptions.js +71 -0
  67. package/dist/src/contextOptions.js.map +1 -0
  68. package/dist/src/crop.d.ts +93 -0
  69. package/dist/src/crop.d.ts.map +1 -0
  70. package/dist/src/crop.js +133 -0
  71. package/dist/src/crop.js.map +1 -0
  72. package/dist/src/cue.d.ts +17 -4
  73. package/dist/src/cue.d.ts.map +1 -1
  74. package/dist/src/cue.js +31 -3
  75. package/dist/src/cue.js.map +1 -1
  76. package/dist/src/defaults.d.ts +6 -0
  77. package/dist/src/defaults.d.ts.map +1 -1
  78. package/dist/src/defaults.js +6 -0
  79. package/dist/src/defaults.js.map +1 -1
  80. package/dist/src/dimensions.d.ts +6 -0
  81. package/dist/src/dimensions.d.ts.map +1 -1
  82. package/dist/src/dimensions.js +15 -0
  83. package/dist/src/dimensions.js.map +1 -1
  84. package/dist/src/events.d.ts +152 -10
  85. package/dist/src/events.d.ts.map +1 -1
  86. package/dist/src/events.js +61 -3
  87. package/dist/src/events.js.map +1 -1
  88. package/dist/src/htmlRasterizer.d.ts +43 -0
  89. package/dist/src/htmlRasterizer.d.ts.map +1 -1
  90. package/dist/src/htmlRasterizer.js +32 -10
  91. package/dist/src/htmlRasterizer.js.map +1 -1
  92. package/dist/src/init.js +7 -7
  93. package/dist/src/init.js.map +1 -1
  94. package/dist/src/overlayFlush.d.ts +18 -0
  95. package/dist/src/overlayFlush.d.ts.map +1 -0
  96. package/dist/src/overlayFlush.js +56 -0
  97. package/dist/src/overlayFlush.js.map +1 -0
  98. package/dist/src/overlayRect.d.ts +69 -0
  99. package/dist/src/overlayRect.d.ts.map +1 -0
  100. package/dist/src/overlayRect.js +55 -0
  101. package/dist/src/overlayRect.js.map +1 -0
  102. package/dist/src/recording.d.ts +3 -2
  103. package/dist/src/recording.d.ts.map +1 -1
  104. package/dist/src/recordingData.d.ts +20 -0
  105. package/dist/src/recordingData.d.ts.map +1 -1
  106. package/dist/src/runtimeContext.d.ts +19 -1
  107. package/dist/src/runtimeContext.d.ts.map +1 -1
  108. package/dist/src/runtimeContext.js +15 -0
  109. package/dist/src/runtimeContext.js.map +1 -1
  110. package/dist/src/screenshot.d.ts +105 -0
  111. package/dist/src/screenshot.d.ts.map +1 -0
  112. package/dist/src/screenshot.js +188 -0
  113. package/dist/src/screenshot.js.map +1 -0
  114. package/dist/src/stillCapture.d.ts +68 -0
  115. package/dist/src/stillCapture.d.ts.map +1 -0
  116. package/dist/src/stillCapture.js +119 -0
  117. package/dist/src/stillCapture.js.map +1 -0
  118. package/dist/src/studio.d.ts +8 -11
  119. package/dist/src/studio.d.ts.map +1 -1
  120. package/dist/src/studio.js +2 -25
  121. package/dist/src/studio.js.map +1 -1
  122. package/dist/src/types.d.ts +100 -8
  123. package/dist/src/types.d.ts.map +1 -1
  124. package/dist/src/types.js.map +1 -1
  125. package/dist/src/video.d.ts +20 -0
  126. package/dist/src/video.d.ts.map +1 -1
  127. package/dist/src/video.js +71 -24
  128. package/dist/src/video.js.map +1 -1
  129. package/dist/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +1 -1
  131. package/skills/screenci/SKILL.md +10 -10
  132. package/skills/screenci/references/init.md +1 -1
  133. package/skills/screenci/references/record.md +2 -2
  134. package/dist/docs/video-sources/cli.video.d.ts +0 -2
  135. package/dist/docs/video-sources/cli.video.d.ts.map +0 -1
  136. package/dist/docs/video-sources/cli.video.js.map +0 -1
  137. package/dist/docs/video-sources/installation.video.d.ts +0 -2
  138. package/dist/docs/video-sources/installation.video.d.ts.map +0 -1
  139. package/dist/docs/video-sources/installation.video.js.map +0 -1
  140. package/dist/docs/video-sources/landing.video.d.ts +0 -2
  141. package/dist/docs/video-sources/landing.video.d.ts.map +0 -1
  142. package/dist/docs/video-sources/landing.video.js.map +0 -1
  143. package/dist/docs/video-sources/studio.video.d.ts +0 -2
  144. package/dist/docs/video-sources/studio.video.d.ts.map +0 -1
  145. package/dist/docs/video-sources/studio.video.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestDetails, TestInfo } from '@playwright/test';
2
+ import type { RecordOptions, RenderOptions, ScreenCIPage } from './types.js';
3
+ import type { StudioRenderOptionsSentinel } from './studio.js';
4
+ import type { CropTarget, CropOptions } from './crop.js';
5
+ /**
6
+ * The `crop` fixture argument. Call it inside a `screenshot()` body to crop the
7
+ * implicit end-of-body capture to a locator or pixel region. Replaces the
8
+ * old module-level `crop()` function; the crop is recorded per test.
9
+ */
10
+ export type CropFixture = (target: CropTarget, options?: CropOptions) => Promise<void>;
11
+ type ScreenshotFixtureOptions = {
12
+ recordOptions: RecordOptions;
13
+ renderOptions: RenderOptions | StudioRenderOptionsSentinel | undefined;
14
+ };
15
+ type ScreenshotFixtures = {
16
+ crop: CropFixture;
17
+ };
18
+ type ScreenshotType = TestType<PlaywrightTestArgs & PlaywrightTestOptions & ScreenshotFixtureOptions & ScreenshotFixtures & PlaywrightWorkerArgs & PlaywrightWorkerOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
19
+ type ScreenshotArgs = Omit<PlaywrightTestArgs, 'page'> & {
20
+ page: ScreenCIPage;
21
+ } & PlaywrightTestOptions & ScreenshotFixtureOptions & ScreenshotFixtures & PlaywrightWorkerArgs & PlaywrightWorkerOptions;
22
+ type ScreenshotBody = (args: ScreenshotArgs, testInfo: TestInfo) => void | Promise<void>;
23
+ /** Conditional overloads shared by skip / fixme / fail */
24
+ type ConditionalOverloads = ((condition?: boolean, description?: string) => void) & ((condition?: boolean, callback?: () => string) => void);
25
+ interface ScreenshotCallSignatures {
26
+ /**
27
+ * Declares a ScreenCI screenshot test.
28
+ *
29
+ * Drives the page like a `video()` test, then captures the final state as a
30
+ * still image. The capture is framed on the configured background with the
31
+ * branded frame, cropped with the `crop` fixture argument, and decorated with
32
+ * overlays at render time. The viewport is derived from
33
+ * `recordOptions.aspectRatio` and `recordOptions.quality`;
34
+ * `recordOptions.deviceScaleFactor` raises the DPI.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { screenshot } from 'screenci'
39
+ *
40
+ * screenshot('Dashboard hero', async ({ page, crop }) => {
41
+ * await page.goto('https://app.example.com/dashboard')
42
+ * await crop(page.getByTestId('revenue-card'), { padding: 48 })
43
+ * })
44
+ * ```
45
+ */
46
+ (title: string, body: ScreenshotBody): void;
47
+ /**
48
+ * Declares a ScreenCI screenshot test with additional details (tags,
49
+ * annotations, etc.).
50
+ */
51
+ (title: string, details: TestDetails, body: ScreenshotBody): void;
52
+ }
53
+ /**
54
+ * Recursive interface so `.only`, `.skip`, `.fixme`, `.fail`, and `.slow`
55
+ * surface `page: ScreenCIPage` instead of the raw Playwright `page: Page`.
56
+ */
57
+ interface Screenshot extends ScreenshotCallSignatures {
58
+ only: Screenshot;
59
+ skip: Screenshot & ConditionalOverloads;
60
+ fixme: Screenshot & ConditionalOverloads;
61
+ fail: Screenshot & ConditionalOverloads;
62
+ slow: Screenshot & ((condition?: boolean, description?: string) => void);
63
+ beforeEach(inner: (args: ScreenshotArgs, testInfo: TestInfo) => Promise<void> | void): void;
64
+ beforeEach(title: string, inner: (args: ScreenshotArgs, testInfo: TestInfo) => Promise<void> | void): void;
65
+ afterEach(inner: (args: ScreenshotArgs, testInfo: TestInfo) => Promise<void> | void): void;
66
+ afterEach(title: string, inner: (args: ScreenshotArgs, testInfo: TestInfo) => Promise<void> | void): void;
67
+ describe: ScreenshotType['describe'];
68
+ beforeAll: ScreenshotType['beforeAll'];
69
+ afterAll: ScreenshotType['afterAll'];
70
+ use: ScreenshotType['use'];
71
+ extend: ScreenshotType['extend'];
72
+ step: ScreenshotType['step'];
73
+ info: ScreenshotType['info'];
74
+ expect: ScreenshotType['expect'];
75
+ setTimeout: ScreenshotType['setTimeout'];
76
+ }
77
+ /**
78
+ * ScreenCI screenshot test fixture.
79
+ *
80
+ * Extended Playwright test that captures a branded still image of the final page
81
+ * state. Configure capture options with `screenshot.use()` or in your config.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import { screenshot, createOverlays } from 'screenci'
86
+ *
87
+ * const overlays = createOverlays({
88
+ * badge: { path: '../assets/new-badge.png', x: 0.72, y: 0.06, width: 0.2 },
89
+ * })
90
+ *
91
+ * screenshot.use({
92
+ * colorScheme: 'dark',
93
+ * recordOptions: { quality: '1440p', deviceScaleFactor: 2 },
94
+ * })
95
+ *
96
+ * screenshot('Dashboard hero', async ({ page, crop }) => {
97
+ * await page.goto('https://app.example.com/dashboard')
98
+ * await overlays.badge()
99
+ * await crop(page.getByTestId('revenue-card'), { padding: 0.06 })
100
+ * })
101
+ * ```
102
+ */
103
+ export declare const screenshot: Screenshot;
104
+ export {};
105
+ //# sourceMappingURL=screenshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/screenshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ import { test as base } from '@playwright/test';
2
+ import { mkdir, rm } from 'fs/promises';
3
+ import { join, relative } from 'path';
4
+ import { getDimensions } from './dimensions.js';
5
+ import { DEFAULT_VIDEO_OPTIONS, DEFAULT_ASPECT_RATIO, DEFAULT_QUALITY, DEFAULT_SCREENSHOT_DEVICE_SCALE_FACTOR, } from './defaults.js';
6
+ import { EventRecorder } from './events.js';
7
+ import { instrumentBrowser, instrumentContext } from './instrument.js';
8
+ import { getChromiumLaunchOptions } from './browserLaunchOptions.js';
9
+ import { createScreenCIRuntimeContext } from './runtimeContext.js';
10
+ import { escapeFileSystemPathSegment } from './fileSystemName.js';
11
+ import { buildScreenCIContextOptions, resolveDeviceScaleFactor, } from './contextOptions.js';
12
+ import { resolveCrop } from './crop.js';
13
+ import { getRuntimePage, setRuntimeCrop } from './runtimeContext.js';
14
+ import { ScreenciError } from './errors.js';
15
+ import { withActiveRecordingContext } from './video.js';
16
+ /** File name of the raw page capture written beside `data.json`. */
17
+ const SCREENSHOT_FILE_NAME = 'screenshot.png';
18
+ const _screenshotBase = base.extend({
19
+ recordOptions: [DEFAULT_VIDEO_OPTIONS, { option: true }],
20
+ renderOptions: [undefined, { option: true }],
21
+ crop: async ({}, use) => {
22
+ await use(async (target, options) => {
23
+ const page = getRuntimePage();
24
+ if (page === null) {
25
+ throw new ScreenciError('crop() requires an active ScreenCI page. Call it inside a screenshot() body.');
26
+ }
27
+ setRuntimeCrop(await resolveCrop(target, page, options));
28
+ });
29
+ },
30
+ browser: async ({ playwright }, use) => {
31
+ const shouldRecord = process.env.SCREENCI_RECORDING === 'true';
32
+ const launchOptions = getChromiumLaunchOptions(shouldRecord);
33
+ const browser = await playwright.chromium.launch(launchOptions);
34
+ instrumentBrowser(browser);
35
+ await use(browser);
36
+ if (browser.isConnected()) {
37
+ await browser.close();
38
+ }
39
+ },
40
+ context: async ({ browser, recordOptions, colorScheme, locale, timezoneId, userAgent, geolocation, permissions, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors, offline, storageState, baseURL, bypassCSP, acceptDownloads, javaScriptEnabled, hasTouch, isMobile, deviceScaleFactor, }, use) => {
41
+ const aspectRatio = recordOptions.aspectRatio ?? DEFAULT_ASPECT_RATIO;
42
+ const quality = recordOptions.quality ?? DEFAULT_QUALITY;
43
+ const dimensions = getDimensions(aspectRatio, quality);
44
+ const shouldRecord = process.env.SCREENCI_RECORDING === 'true';
45
+ // Screenshots honor deviceScaleFactor for higher-DPI stills.
46
+ const context = await browser.newContext(buildScreenCIContextOptions({
47
+ dimensions,
48
+ applyLocaleDefault: shouldRecord,
49
+ deviceScaleFactor: resolveDeviceScaleFactor(recordOptions, deviceScaleFactor, DEFAULT_SCREENSHOT_DEVICE_SCALE_FACTOR),
50
+ forwarded: {
51
+ colorScheme,
52
+ locale,
53
+ timezoneId,
54
+ userAgent,
55
+ geolocation,
56
+ permissions,
57
+ extraHTTPHeaders,
58
+ httpCredentials,
59
+ ignoreHTTPSErrors,
60
+ offline,
61
+ storageState,
62
+ baseURL,
63
+ bypassCSP,
64
+ acceptDownloads,
65
+ javaScriptEnabled,
66
+ hasTouch,
67
+ isMobile,
68
+ },
69
+ }));
70
+ instrumentContext(context);
71
+ try {
72
+ await use(context);
73
+ }
74
+ finally {
75
+ await context.close();
76
+ }
77
+ },
78
+ page: async ({ context, recordOptions, renderOptions, deviceScaleFactor }, use, testInfo) => {
79
+ const shouldRecord = process.env.SCREENCI_RECORDING === 'true';
80
+ const recorder = new EventRecorder(renderOptions, recordOptions);
81
+ if (!shouldRecord) {
82
+ // Preview run (`screenci test`): exercise the body without capturing.
83
+ const page = await context.newPage();
84
+ const runtimeContext = createScreenCIRuntimeContext({
85
+ recorder,
86
+ page,
87
+ testFilePath: testInfo.file,
88
+ });
89
+ recorder.start();
90
+ await withActiveRecordingContext({
91
+ runtimeContext,
92
+ page,
93
+ recorder,
94
+ unendedOverlays: 'autoEnd',
95
+ fn: async () => {
96
+ await use(page);
97
+ },
98
+ });
99
+ await page.close();
100
+ return;
101
+ }
102
+ const aspectRatio = recordOptions.aspectRatio ?? DEFAULT_ASPECT_RATIO;
103
+ const quality = recordOptions.quality ?? DEFAULT_QUALITY;
104
+ const dimensions = getDimensions(aspectRatio, quality);
105
+ const dsf = resolveDeviceScaleFactor(recordOptions, deviceScaleFactor, DEFAULT_SCREENSHOT_DEVICE_SCALE_FACTOR);
106
+ const directoryName = escapeFileSystemPathSegment(testInfo.title);
107
+ const screenshotDir = join(process.cwd(), '.screenci', directoryName);
108
+ // Start fresh.
109
+ try {
110
+ await rm(screenshotDir, { recursive: true, force: true });
111
+ }
112
+ catch {
113
+ // Ignore if it does not exist.
114
+ }
115
+ await mkdir(screenshotDir, { recursive: true });
116
+ const page = await context.newPage();
117
+ const runtimeContext = createScreenCIRuntimeContext({
118
+ recorder,
119
+ page,
120
+ testFilePath: testInfo.file,
121
+ recordingDir: screenshotDir,
122
+ });
123
+ recorder.start();
124
+ try {
125
+ await withActiveRecordingContext({
126
+ runtimeContext,
127
+ page,
128
+ recorder,
129
+ unendedOverlays: 'autoEnd',
130
+ fn: async () => {
131
+ await use(page);
132
+ },
133
+ });
134
+ // The body completed successfully: capture the final page state. Capturing
135
+ // here (not in `finally`) mirrors the video fixture's passed-only upload:
136
+ // a thrown body skips capture and fails the test.
137
+ await page.screenshot({
138
+ path: join(screenshotDir, SCREENSHOT_FILE_NAME),
139
+ });
140
+ const crop = runtimeContext.crop ?? undefined;
141
+ const screenshot = {
142
+ path: SCREENSHOT_FILE_NAME,
143
+ width: Math.round(dimensions.width * dsf),
144
+ height: Math.round(dimensions.height * dsf),
145
+ deviceScaleFactor: dsf,
146
+ };
147
+ const configDir = process.env.SCREENCI_CONFIG_DIR ?? process.cwd();
148
+ // The crop (from the `crop` fixture) is a render option, so it goes into
149
+ // renderOptions.screenshot.crop (editable in Studio), not ScreenshotInfo.
150
+ await recorder.writeToFile(screenshotDir, testInfo.title, relative(configDir, testInfo.file), {
151
+ output: 'screenshot',
152
+ screenshot,
153
+ ...(crop !== undefined && { crop }),
154
+ });
155
+ }
156
+ finally {
157
+ await page.close();
158
+ }
159
+ },
160
+ });
161
+ /**
162
+ * ScreenCI screenshot test fixture.
163
+ *
164
+ * Extended Playwright test that captures a branded still image of the final page
165
+ * state. Configure capture options with `screenshot.use()` or in your config.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * import { screenshot, createOverlays } from 'screenci'
170
+ *
171
+ * const overlays = createOverlays({
172
+ * badge: { path: '../assets/new-badge.png', x: 0.72, y: 0.06, width: 0.2 },
173
+ * })
174
+ *
175
+ * screenshot.use({
176
+ * colorScheme: 'dark',
177
+ * recordOptions: { quality: '1440p', deviceScaleFactor: 2 },
178
+ * })
179
+ *
180
+ * screenshot('Dashboard hero', async ({ page, crop }) => {
181
+ * await page.goto('https://app.example.com/dashboard')
182
+ * await overlays.badge()
183
+ * await crop(page.getByTestId('revenue-card'), { padding: 0.06 })
184
+ * })
185
+ * ```
186
+ */
187
+ export const screenshot = _screenshotBase;
188
+ //# sourceMappingURL=screenshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/screenshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { Page } from '@playwright/test';
2
+ import type { IEventRecorder } from './events.js';
3
+ import type { CropTarget, ScreenshotCropRecord } from './crop.js';
4
+ import type { RecordOptions, RenderOptions } from './types.js';
5
+ import type { StudioRenderOptionsSentinel } from './studio.js';
6
+ /**
7
+ * The screenci-specific options added to `page.screenshot()` inside a `video()`.
8
+ * Everything else is forwarded to Playwright's native screenshot.
9
+ */
10
+ export type StillScreenshotOptions = {
11
+ /**
12
+ * Names the still recording produced from this capture. The recording's title
13
+ * is `"<video title> - <name>"`. Defaults to the `path` basename, then an
14
+ * auto-indexed `screenshot`, `screenshot 2`, ...
15
+ */
16
+ name?: string;
17
+ /** Crop the still to a locator or pixel region (applied by the compositor). */
18
+ crop?: CropTarget;
19
+ };
20
+ /**
21
+ * Resolve the still's name: explicit `name`, else the `path` basename (without
22
+ * extension), else an auto-indexed fallback that stays unique within `used`.
23
+ * Pure and exported for testing.
24
+ */
25
+ export declare function resolveStillName(name: string | undefined, path: string | undefined, used: Set<string>): string;
26
+ /** Injectable side effects, so the writer can be unit tested without real I/O. */
27
+ export type StillCaptureDeps = {
28
+ /** Captures the page to `pngPath` and returns the raw bytes. */
29
+ capture: (pngPath: string) => Promise<Buffer>;
30
+ fs?: {
31
+ rm: (path: string) => Promise<void>;
32
+ mkdir: (path: string) => Promise<void>;
33
+ };
34
+ makeRecorder?: (renderOptions: RenderOptions | StudioRenderOptionsSentinel | undefined, recordOptions: RecordOptions | undefined) => IEventRecorder;
35
+ };
36
+ /**
37
+ * Write a standalone screenshot recording (its own `.screenci/<title>/` dir with
38
+ * `output: 'screenshot'`). Discovered and uploaded by the CLI exactly like a
39
+ * `screenshot()` fixture capture, so no rendering/backend changes are needed.
40
+ * Returns the captured bytes so the wrapped `page.screenshot()` can return them.
41
+ */
42
+ export declare function writeStillRecording(params: {
43
+ name: string;
44
+ screenciDir: string;
45
+ dimensions: {
46
+ width: number;
47
+ height: number;
48
+ };
49
+ deviceScaleFactor: number;
50
+ crop?: ScreenshotCropRecord;
51
+ testFilePath: string | null;
52
+ configDir: string;
53
+ recordOptions: RecordOptions | undefined;
54
+ renderOptions: RenderOptions | StudioRenderOptionsSentinel | undefined;
55
+ deps: StillCaptureDeps;
56
+ }): Promise<Buffer>;
57
+ /**
58
+ * Wrap `page.screenshot()` so that, while recording a `video()`, each call also
59
+ * writes a branded still as a separate screenshot recording, then returns the
60
+ * native Buffer. Outside recording it strips the screenci-specific keys and
61
+ * delegates straight to Playwright.
62
+ *
63
+ * Bound only to the `video()` fixture's page (never the `screenshot()` fixture's),
64
+ * so the screenshot fixture's own implicit capture and the overlay rasterizer
65
+ * keep the native behavior.
66
+ */
67
+ export declare function bindStillCaptureToPage(page: Page): void;
68
+ //# sourceMappingURL=stillCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stillCapture.d.ts","sourceRoot":"","sources":["../../src/stillCapture.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAI5C,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,aAAa,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAOjE,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AAK9D;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB,CAAA;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAChB,MAAM,CAqBR;AAED,kFAAkF;AAClF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gEAAgE;IAChE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7C,EAAE,CAAC,EAAE;QACH,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACvC,CAAA;IACD,YAAY,CAAC,EAAE,CACb,aAAa,EAAE,aAAa,GAAG,2BAA2B,GAAG,SAAS,EACtE,aAAa,EAAE,aAAa,GAAG,SAAS,KACrC,cAAc,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,oBAAoB,CAAA;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,aAAa,GAAG,SAAS,CAAA;IACxC,aAAa,EAAE,aAAa,GAAG,2BAA2B,GAAG,SAAS,CAAA;IACtE,IAAI,EAAE,gBAAgB,CAAA;CACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmDlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAqDvD"}
@@ -0,0 +1,119 @@
1
+ import { mkdir as fsMkdir, rm as fsRm } from 'fs/promises';
2
+ import { join, relative } from 'path';
3
+ import { getDimensions } from './dimensions.js';
4
+ import { escapeFileSystemPathSegment } from './fileSystemName.js';
5
+ import { EventRecorder } from './events.js';
6
+ import { resolveCrop } from './crop.js';
7
+ import { getScreenCIRuntimeContext } from './runtimeContext.js';
8
+ import { DEFAULT_VIDEO_OPTIONS, DEFAULT_ASPECT_RATIO, DEFAULT_QUALITY, } from './defaults.js';
9
+ /** File name of the raw page capture written beside a still's `data.json`. */
10
+ const SCREENSHOT_FILE_NAME = 'screenshot.png';
11
+ /**
12
+ * Resolve the still's name: explicit `name`, else the `path` basename (without
13
+ * extension), else an auto-indexed fallback that stays unique within `used`.
14
+ * Pure and exported for testing.
15
+ */
16
+ export function resolveStillName(name, path, used) {
17
+ let base = name?.trim() || undefined;
18
+ if (base === undefined && typeof path === 'string') {
19
+ base =
20
+ path
21
+ .split(/[\\/]/)
22
+ .pop()
23
+ ?.replace(/\.[^.]+$/, '') || undefined;
24
+ }
25
+ if (base === undefined) {
26
+ base = 'screenshot';
27
+ }
28
+ let candidate = base;
29
+ let n = 2;
30
+ while (used.has(candidate)) {
31
+ candidate = `${base} ${n}`;
32
+ n++;
33
+ }
34
+ used.add(candidate);
35
+ return candidate;
36
+ }
37
+ /**
38
+ * Write a standalone screenshot recording (its own `.screenci/<title>/` dir with
39
+ * `output: 'screenshot'`). Discovered and uploaded by the CLI exactly like a
40
+ * `screenshot()` fixture capture, so no rendering/backend changes are needed.
41
+ * Returns the captured bytes so the wrapped `page.screenshot()` can return them.
42
+ */
43
+ export async function writeStillRecording(params) {
44
+ const { name, screenciDir, dimensions, deviceScaleFactor, crop, testFilePath, configDir, recordOptions, renderOptions, deps, } = params;
45
+ const rm = deps.fs?.rm ??
46
+ ((path) => fsRm(path, { recursive: true, force: true }));
47
+ const mkdir = deps.fs?.mkdir ??
48
+ ((path) => fsMkdir(path, { recursive: true }).then(() => undefined));
49
+ const makeRecorder = deps.makeRecorder ?? ((ro, rec) => new EventRecorder(ro, rec));
50
+ // The still is named exactly by `name`; its directory and recording title both
51
+ // use it directly (like a standalone screenshot() test's title).
52
+ const dir = join(screenciDir, escapeFileSystemPathSegment(name));
53
+ await rm(dir);
54
+ await mkdir(dir);
55
+ const buffer = await deps.capture(join(dir, SCREENSHOT_FILE_NAME));
56
+ const screenshot = {
57
+ path: SCREENSHOT_FILE_NAME,
58
+ width: Math.round(dimensions.width * deviceScaleFactor),
59
+ height: Math.round(dimensions.height * deviceScaleFactor),
60
+ deviceScaleFactor,
61
+ };
62
+ const recorder = makeRecorder(renderOptions, recordOptions);
63
+ recorder.start();
64
+ // The crop is a render option (renderOptions.screenshot.crop), passed through
65
+ // writeToFile rather than stored on ScreenshotInfo.
66
+ await recorder.writeToFile(dir, name, testFilePath !== null ? relative(configDir, testFilePath) : undefined, { output: 'screenshot', screenshot, ...(crop !== undefined && { crop }) });
67
+ return buffer;
68
+ }
69
+ /**
70
+ * Wrap `page.screenshot()` so that, while recording a `video()`, each call also
71
+ * writes a branded still as a separate screenshot recording, then returns the
72
+ * native Buffer. Outside recording it strips the screenci-specific keys and
73
+ * delegates straight to Playwright.
74
+ *
75
+ * Bound only to the `video()` fixture's page (never the `screenshot()` fixture's),
76
+ * so the screenshot fixture's own implicit capture and the overlay rasterizer
77
+ * keep the native behavior.
78
+ */
79
+ export function bindStillCaptureToPage(page) {
80
+ const original = page.screenshot.bind(page);
81
+ const usedNames = new Set();
82
+ const wrapped = async (options) => {
83
+ const { name, crop: cropTarget, ...playwrightOptions } = options ?? {};
84
+ if (process.env.SCREENCI_RECORDING !== 'true') {
85
+ return original(playwrightOptions);
86
+ }
87
+ const ctx = getScreenCIRuntimeContext();
88
+ const recordOptions = ctx.recordOptions ?? DEFAULT_VIDEO_OPTIONS;
89
+ const aspectRatio = recordOptions.aspectRatio ?? DEFAULT_ASPECT_RATIO;
90
+ const quality = recordOptions.quality ?? DEFAULT_QUALITY;
91
+ const dimensions = getDimensions(aspectRatio, quality);
92
+ const resolvedName = resolveStillName(name, typeof playwrightOptions.path === 'string'
93
+ ? playwrightOptions.path
94
+ : undefined, usedNames);
95
+ let crop;
96
+ if (cropTarget !== undefined && ctx.page !== null) {
97
+ crop = await resolveCrop(cropTarget, ctx.page);
98
+ }
99
+ const configDir = process.env.SCREENCI_CONFIG_DIR ?? process.cwd();
100
+ return writeStillRecording({
101
+ name: resolvedName,
102
+ screenciDir: join(process.cwd(), '.screenci'),
103
+ dimensions,
104
+ // The video context never applies deviceScaleFactor (video.ts), so a still
105
+ // captured mid-recording is at the viewport resolution (DSF 1).
106
+ deviceScaleFactor: 1,
107
+ ...(crop !== undefined && { crop }),
108
+ testFilePath: ctx.testFilePath,
109
+ configDir,
110
+ recordOptions,
111
+ renderOptions: ctx.renderOptions,
112
+ deps: {
113
+ capture: (pngPath) => original({ ...playwrightOptions, path: pngPath }),
114
+ },
115
+ });
116
+ };
117
+ page.screenshot = wrapped;
118
+ }
119
+ //# sourceMappingURL=stillCapture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stillCapture.js","sourceRoot":"","sources":["../../src/stillCapture.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * Code opts in per concern:
6
6
  * - `createStudioNarration('intro', 'outro')` declares cue keys whose text and
7
7
  * voice are filled in on the Studio page.
8
- * - `renderOptions: STUDIO_RENDER_OPTIONS` defers render options to Studio.
8
+ * - `renderOptions: 'studio'` defers render options to Studio.
9
9
  *
10
10
  * On the first upload of a studio-mode video, rendering is held until the
11
11
  * video is configured in Studio; later uploads reuse the saved configuration.
@@ -14,23 +14,20 @@
14
14
  * Sentinel value for the `renderOptions` fixture/config option meaning
15
15
  * "render options are configured in Studio".
16
16
  *
17
- * Implemented as a frozen, JSON-safe branded object (not a string or symbol)
18
- * so it survives Playwright's serialization of `use` options between the
19
- * config file and test workers, and cannot collide with a real
20
- * {@link import('./types.js').RenderOptions} value.
17
+ * A plain string literal (not an object or symbol) so it survives Playwright's
18
+ * serialization of `use` options between the config file and test workers, and
19
+ * cannot collide with a real {@link import('./types.js').RenderOptions} value,
20
+ * which is always an object.
21
21
  *
22
22
  * @example
23
23
  * ```ts
24
- * import { defineConfig, STUDIO_RENDER_OPTIONS } from 'screenci'
24
+ * import { defineConfig } from 'screenci'
25
25
  *
26
26
  * export default defineConfig({
27
- * use: { renderOptions: STUDIO_RENDER_OPTIONS },
27
+ * use: { renderOptions: 'studio' },
28
28
  * })
29
29
  * ```
30
30
  */
31
- export declare const STUDIO_RENDER_OPTIONS: Readonly<{
32
- readonly __screenciStudioRenderOptions: true;
33
- }>;
34
- export type StudioRenderOptionsSentinel = typeof STUDIO_RENDER_OPTIONS;
31
+ export type StudioRenderOptionsSentinel = 'studio';
35
32
  export declare function isStudioRenderOptions(value: unknown): value is StudioRenderOptionsSentinel;
36
33
  //# sourceMappingURL=studio.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB;;EAEvB,CAAA;AAEX,MAAM,MAAM,2BAA2B,GAAG,OAAO,qBAAqB,CAAA;AAEtE,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,2BAA2B,CAMtC"}
1
+ {"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAA;AAElD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,2BAA2B,CAEtC"}
@@ -5,35 +5,12 @@
5
5
  * Code opts in per concern:
6
6
  * - `createStudioNarration('intro', 'outro')` declares cue keys whose text and
7
7
  * voice are filled in on the Studio page.
8
- * - `renderOptions: STUDIO_RENDER_OPTIONS` defers render options to Studio.
8
+ * - `renderOptions: 'studio'` defers render options to Studio.
9
9
  *
10
10
  * On the first upload of a studio-mode video, rendering is held until the
11
11
  * video is configured in Studio; later uploads reuse the saved configuration.
12
12
  */
13
- /**
14
- * Sentinel value for the `renderOptions` fixture/config option meaning
15
- * "render options are configured in Studio".
16
- *
17
- * Implemented as a frozen, JSON-safe branded object (not a string or symbol)
18
- * so it survives Playwright's serialization of `use` options between the
19
- * config file and test workers, and cannot collide with a real
20
- * {@link import('./types.js').RenderOptions} value.
21
- *
22
- * @example
23
- * ```ts
24
- * import { defineConfig, STUDIO_RENDER_OPTIONS } from 'screenci'
25
- *
26
- * export default defineConfig({
27
- * use: { renderOptions: STUDIO_RENDER_OPTIONS },
28
- * })
29
- * ```
30
- */
31
- export const STUDIO_RENDER_OPTIONS = Object.freeze({
32
- __screenciStudioRenderOptions: true,
33
- });
34
13
  export function isStudioRenderOptions(value) {
35
- return (typeof value === 'object' &&
36
- value !== null &&
37
- value.__screenciStudioRenderOptions === true);
14
+ return value === 'studio';
38
15
  }
39
16
  //# sourceMappingURL=studio.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"studio.js","sourceRoot":"","sources":["../../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IACjD,6BAA6B,EAAE,IAAI;CAC3B,CAAC,CAAA;AAIX,MAAM,UAAU,qBAAqB,CACnC,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAiC,CAAC,6BAA6B,KAAK,IAAI,CAC1E,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"studio.js","sourceRoot":"","sources":["../../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsBH,MAAM,UAAU,qBAAqB,CACnC,KAAc;IAEd,OAAO,KAAK,KAAK,QAAQ,CAAA;AAC3B,CAAC"}