@screenly/edge-apps 1.4.0 → 26.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/test/screenshots.d.ts +61 -6
- package/dist/test/screenshots.d.ts.map +1 -1
- package/dist/test/screenshots.js +31 -0
- package/dist/utils/color.d.ts +14 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +72 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
- package/scripts/create-template/e2e/screenshots.spec.ts +6 -20
package/README.md
CHANGED
|
@@ -105,6 +105,10 @@ signalReady()
|
|
|
105
105
|
- `setupBrandingLogo()` - Fetch and process branding logo
|
|
106
106
|
- `setupBranding()` - Setup complete branding (colors and logo)
|
|
107
107
|
|
|
108
|
+
### Color
|
|
109
|
+
|
|
110
|
+
- `isLightColor(color)` - Check whether a color needs dark text on top of it
|
|
111
|
+
|
|
108
112
|
### Settings
|
|
109
113
|
|
|
110
114
|
- `getSettings()` - Get all settings
|
|
@@ -201,6 +205,29 @@ async function loadWeather() {
|
|
|
201
205
|
}
|
|
202
206
|
```
|
|
203
207
|
|
|
208
|
+
## Color Contrast
|
|
209
|
+
|
|
210
|
+
Customers supply their own accent color through `screenly_color_accent`, so an
|
|
211
|
+
app that paints anything on top of it cannot hardcode the text color. A pale
|
|
212
|
+
brand needs dark text, a deep one needs light text.
|
|
213
|
+
|
|
214
|
+
`isLightColor()` answers that question using the WCAG relative luminance
|
|
215
|
+
formula, so hues are ranked the way an eye ranks them rather than by averaging
|
|
216
|
+
raw channels. Pure yellow and pure blue have similar RGB totals but land on
|
|
217
|
+
opposite sides of the threshold.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { isLightColor, setupTheme } from '@screenly/edge-apps'
|
|
221
|
+
|
|
222
|
+
const { primary } = setupTheme()
|
|
223
|
+
document.body.classList.toggle('on-light-brand', isLightColor(primary))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
It accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
227
|
+
strings, ignoring any alpha. Percentage channels and named colors are not
|
|
228
|
+
supported. Anything it cannot parse returns `false`, so a malformed setting
|
|
229
|
+
leaves an unattended screen rendering instead of throwing.
|
|
230
|
+
|
|
204
231
|
## Web Components
|
|
205
232
|
|
|
206
233
|
This library includes reusable web components for building consistent Edge Apps. See the [components documentation](https://github.com/Screenly/edge-apps-library/blob/main/docs/components.md) for usage details.
|
|
@@ -315,6 +342,43 @@ afterEach(() => {
|
|
|
315
342
|
})
|
|
316
343
|
```
|
|
317
344
|
|
|
345
|
+
### Screenshot Testing
|
|
346
|
+
|
|
347
|
+
`captureScreenshot()` consolidates the Playwright boilerplate (browser
|
|
348
|
+
context/page setup, clock and `screenly.js` mocking, navigation, and cleanup)
|
|
349
|
+
that's otherwise duplicated across every resolution in an Edge App's e2e
|
|
350
|
+
screenshot spec. Keep `test()` in your spec file so Playwright still reports
|
|
351
|
+
the correct source location; pass app-specific route mocks through the
|
|
352
|
+
optional `setupMocks` callback, omitting it when there's nothing app-specific
|
|
353
|
+
to mock.
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { test } from '@playwright/test'
|
|
357
|
+
import {
|
|
358
|
+
captureScreenshot,
|
|
359
|
+
createMockScreenlyForScreenshots,
|
|
360
|
+
RESOLUTIONS,
|
|
361
|
+
} from '@screenly/edge-apps/test/screenshots'
|
|
362
|
+
|
|
363
|
+
const { screenlyJsContent } = createMockScreenlyForScreenshots()
|
|
364
|
+
|
|
365
|
+
for (const { width, height } of RESOLUTIONS) {
|
|
366
|
+
test(`screenshot ${width}x${height}`, async ({ browser }) => {
|
|
367
|
+
await captureScreenshot(browser, {
|
|
368
|
+
width,
|
|
369
|
+
height,
|
|
370
|
+
filenamePrefix: 'my-edge-app',
|
|
371
|
+
screenlyJsContent,
|
|
372
|
+
setupMocks: async (page) => {
|
|
373
|
+
await page.route('**/api**', async (route) => {
|
|
374
|
+
await route.fulfill({ status: 200, body: '{}' })
|
|
375
|
+
})
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
318
382
|
## Types
|
|
319
383
|
|
|
320
384
|
```typescript
|
|
@@ -18,6 +18,37 @@ interface PlaywrightRouteFulfillOptions {
|
|
|
18
18
|
interface PlaywrightRoute {
|
|
19
19
|
fulfill(options: PlaywrightRouteFulfillOptions): Promise<void>;
|
|
20
20
|
}
|
|
21
|
+
interface PlaywrightRequest {
|
|
22
|
+
url(): string;
|
|
23
|
+
method(): string;
|
|
24
|
+
}
|
|
25
|
+
type PlaywrightRouteHandler = (route: PlaywrightRoute, request: PlaywrightRequest) => Promise<void> | void;
|
|
26
|
+
interface PlaywrightRoutable {
|
|
27
|
+
route(url: string | RegExp, handler: PlaywrightRouteHandler): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
interface PlaywrightPage extends PlaywrightRoutable {
|
|
30
|
+
clock: {
|
|
31
|
+
setFixedTime(time: Date | number | string): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
goto(url: string): Promise<unknown>;
|
|
34
|
+
waitForLoadState(state: 'networkidle'): Promise<void>;
|
|
35
|
+
screenshot(options: {
|
|
36
|
+
path: string;
|
|
37
|
+
fullPage: boolean;
|
|
38
|
+
}): Promise<Buffer>;
|
|
39
|
+
}
|
|
40
|
+
interface PlaywrightBrowserContext {
|
|
41
|
+
newPage(): Promise<PlaywrightPage>;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
interface PlaywrightBrowser {
|
|
45
|
+
newContext(options: {
|
|
46
|
+
viewport: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
}): Promise<PlaywrightBrowserContext>;
|
|
51
|
+
}
|
|
21
52
|
/**
|
|
22
53
|
* Standard resolutions for screenshot testing
|
|
23
54
|
* Covers all supported Screenly player resolutions
|
|
@@ -92,17 +123,13 @@ export interface OpenWeatherMocks {
|
|
|
92
123
|
* @param page - Playwright page object
|
|
93
124
|
* @param mocks - Mock data for OpenWeather API endpoints
|
|
94
125
|
*/
|
|
95
|
-
export declare function setupOpenWeatherMocks(page:
|
|
96
|
-
route: (url: string, handler: (route: PlaywrightRoute) => Promise<void>) => Promise<void>;
|
|
97
|
-
}, mocks: OpenWeatherMocks): Promise<void>;
|
|
126
|
+
export declare function setupOpenWeatherMocks(page: PlaywrightRoutable, mocks: OpenWeatherMocks): Promise<void>;
|
|
98
127
|
/**
|
|
99
128
|
* Setup screenly.js route mock in Playwright
|
|
100
129
|
* @param page - Playwright page object
|
|
101
130
|
* @param screenlyJsContent - JavaScript content string for screenly.js
|
|
102
131
|
*/
|
|
103
|
-
export declare function setupScreenlyJsMock(page:
|
|
104
|
-
route: (url: string, handler: (route: PlaywrightRoute) => Promise<void>) => Promise<void>;
|
|
105
|
-
}, screenlyJsContent: string): Promise<void>;
|
|
132
|
+
export declare function setupScreenlyJsMock(page: PlaywrightRoutable, screenlyJsContent: string): Promise<void>;
|
|
106
133
|
/**
|
|
107
134
|
* Freeze the browser clock to a fixed point in time before page load.
|
|
108
135
|
* Must be called before `page.goto()` to ensure the clock is set before
|
|
@@ -117,4 +144,32 @@ export declare function setupClockMock(page: {
|
|
|
117
144
|
setFixedTime(time: Date | number | string): Promise<void>;
|
|
118
145
|
};
|
|
119
146
|
}, date?: Date | number | string): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Options for {@link captureScreenshot}.
|
|
149
|
+
*/
|
|
150
|
+
export interface CaptureScreenshotOptions {
|
|
151
|
+
/** Viewport width */
|
|
152
|
+
width: number;
|
|
153
|
+
/** Viewport height */
|
|
154
|
+
height: number;
|
|
155
|
+
/** Prefix used for the output screenshot filename */
|
|
156
|
+
filenamePrefix: string;
|
|
157
|
+
/** JavaScript content string for screenly.js mocking */
|
|
158
|
+
screenlyJsContent: string;
|
|
159
|
+
/** Callback for app-specific route mocks, called before `page.goto()` */
|
|
160
|
+
setupMocks?: (page: PlaywrightPage) => Promise<void> | void;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Captures a single screenshot at the given viewport size, handling the
|
|
164
|
+
* common boilerplate shared across Edge App screenshot specs: browser
|
|
165
|
+
* context/page setup, clock and screenly.js mocking, navigation, and
|
|
166
|
+
* cleanup. App-specific route mocks are supplied via `setupMocks`.
|
|
167
|
+
*
|
|
168
|
+
* Callers should keep `test()` in their spec files (rather than wrapping it
|
|
169
|
+
* in this helper) so Playwright reports the correct source location.
|
|
170
|
+
*
|
|
171
|
+
* @param browser - Playwright browser object
|
|
172
|
+
* @param options - Capture options
|
|
173
|
+
*/
|
|
174
|
+
export declare function captureScreenshot(browser: PlaywrightBrowser, { width, height, filenamePrefix, screenlyJsContent, setupMocks, }: CaptureScreenshotOptions): Promise<void>;
|
|
120
175
|
//# sourceMappingURL=screenshots.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/test/screenshots.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAE9C,OAAO,EAAE,kBAAkB,EAAE,CAAA;AAE7B;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,MAAmC,CAAA;AAErE,UAAU,6BAA6B;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWd,CAAA;AAEV;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAI1C;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,cAAc,GAC7B,MAAM,CAKR;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,GAAE,OAAO,CAAC,gBAAgB,CAAM,EACxC,QAAQ,GAAE,OAAO,CAAC,gBAAgB,CAAM,EACxC,YAAY,SAA0B,GACrC;IAAE,YAAY,EAAE,cAAc,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAS7D;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE
|
|
1
|
+
{"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/test/screenshots.ts"],"names":[],"mappings":"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"}
|
package/dist/test/screenshots.js
CHANGED
|
@@ -125,3 +125,34 @@ export async function setupScreenlyJsMock(page, screenlyJsContent) {
|
|
|
125
125
|
export async function setupClockMock(page, date = FIXED_SCREENSHOT_DATE) {
|
|
126
126
|
await page.clock.setFixedTime(date);
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Captures a single screenshot at the given viewport size, handling the
|
|
130
|
+
* common boilerplate shared across Edge App screenshot specs: browser
|
|
131
|
+
* context/page setup, clock and screenly.js mocking, navigation, and
|
|
132
|
+
* cleanup. App-specific route mocks are supplied via `setupMocks`.
|
|
133
|
+
*
|
|
134
|
+
* Callers should keep `test()` in their spec files (rather than wrapping it
|
|
135
|
+
* in this helper) so Playwright reports the correct source location.
|
|
136
|
+
*
|
|
137
|
+
* @param browser - Playwright browser object
|
|
138
|
+
* @param options - Capture options
|
|
139
|
+
*/
|
|
140
|
+
export async function captureScreenshot(browser, { width, height, filenamePrefix, screenlyJsContent, setupMocks = async () => { }, }) {
|
|
141
|
+
const screenshotsDir = getScreenshotsDir();
|
|
142
|
+
const context = await browser.newContext({ viewport: { width, height } });
|
|
143
|
+
try {
|
|
144
|
+
const page = await context.newPage();
|
|
145
|
+
await setupClockMock(page);
|
|
146
|
+
await setupScreenlyJsMock(page, screenlyJsContent);
|
|
147
|
+
await setupMocks(page);
|
|
148
|
+
await page.goto('/');
|
|
149
|
+
await page.waitForLoadState('networkidle');
|
|
150
|
+
await page.screenshot({
|
|
151
|
+
path: path.join(screenshotsDir, `${filenamePrefix}-${width}x${height}.png`),
|
|
152
|
+
fullPage: false,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await context.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether a color is light enough to need dark text on top of it.
|
|
3
|
+
*
|
|
4
|
+
* Uses the WCAG relative luminance formula rather than raw RGB averages, so
|
|
5
|
+
* hues of the same nominal brightness are ranked the way an eye ranks them.
|
|
6
|
+
* Accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
7
|
+
* strings; any alpha component is ignored. Percentage channels and named
|
|
8
|
+
* colors are not supported.
|
|
9
|
+
*
|
|
10
|
+
* Returns `false` for values it cannot parse, so an unattended screen keeps
|
|
11
|
+
* rendering instead of throwing on a malformed setting.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isLightColor(color: string): boolean;
|
|
14
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../src/utils/color.ts"],"names":[],"mappings":"AAkEA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKnD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relative luminance at which text contrast against black and against white
|
|
3
|
+
* is equal. Colors above it read better with dark text, colors below it with
|
|
4
|
+
* light text.
|
|
5
|
+
*/
|
|
6
|
+
const LIGHT_THRESHOLD = 0.179;
|
|
7
|
+
function clampChannel(value) {
|
|
8
|
+
return Math.min(255, Math.max(0, value));
|
|
9
|
+
}
|
|
10
|
+
function parseHex(color) {
|
|
11
|
+
const digits = color.slice(1);
|
|
12
|
+
const expanded = digits.length === 3 || digits.length === 4
|
|
13
|
+
? digits.replace(/./g, (digit) => digit + digit)
|
|
14
|
+
: digits;
|
|
15
|
+
if (expanded.length !== 6 && expanded.length !== 8)
|
|
16
|
+
return null;
|
|
17
|
+
if (!/^[0-9a-f]+$/i.test(expanded))
|
|
18
|
+
return null;
|
|
19
|
+
return [
|
|
20
|
+
parseInt(expanded.slice(0, 2), 16),
|
|
21
|
+
parseInt(expanded.slice(2, 4), 16),
|
|
22
|
+
parseInt(expanded.slice(4, 6), 16),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function parseRgb(color) {
|
|
26
|
+
const match = color.match(/^rgba?\(([^)]+)\)$/i);
|
|
27
|
+
if (!match)
|
|
28
|
+
return null;
|
|
29
|
+
const channels = match[1]
|
|
30
|
+
.split(/[\s,/]+/)
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.slice(0, 3)
|
|
33
|
+
.map(Number);
|
|
34
|
+
if (channels.length !== 3 || channels.some(Number.isNaN))
|
|
35
|
+
return null;
|
|
36
|
+
return [
|
|
37
|
+
clampChannel(channels[0]),
|
|
38
|
+
clampChannel(channels[1]),
|
|
39
|
+
clampChannel(channels[2]),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
function parseColor(color) {
|
|
43
|
+
const value = color.trim();
|
|
44
|
+
if (value.startsWith('#'))
|
|
45
|
+
return parseHex(value);
|
|
46
|
+
return parseRgb(value);
|
|
47
|
+
}
|
|
48
|
+
function toLinear(channel) {
|
|
49
|
+
const value = channel / 255;
|
|
50
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
}
|
|
52
|
+
function getRelativeLuminance([red, green, blue]) {
|
|
53
|
+
return (0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check whether a color is light enough to need dark text on top of it.
|
|
57
|
+
*
|
|
58
|
+
* Uses the WCAG relative luminance formula rather than raw RGB averages, so
|
|
59
|
+
* hues of the same nominal brightness are ranked the way an eye ranks them.
|
|
60
|
+
* Accepts hex (`#abc`, `#aabbcc`, `#aabbccdd`) and numeric `rgb()` / `rgba()`
|
|
61
|
+
* strings; any alpha component is ignored. Percentage channels and named
|
|
62
|
+
* colors are not supported.
|
|
63
|
+
*
|
|
64
|
+
* Returns `false` for values it cannot parse, so an unattended screen keeps
|
|
65
|
+
* rendering instead of throwing on a malformed setting.
|
|
66
|
+
*/
|
|
67
|
+
export function isLightColor(color) {
|
|
68
|
+
const rgb = parseColor(color);
|
|
69
|
+
if (!rgb)
|
|
70
|
+
return false;
|
|
71
|
+
return getRelativeLuminance(rgb) > LIGHT_THRESHOLD;
|
|
72
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA"}
|
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
import { test } from '@playwright/test'
|
|
2
2
|
import {
|
|
3
|
+
captureScreenshot,
|
|
3
4
|
createMockScreenlyForScreenshots,
|
|
4
|
-
getScreenshotsDir,
|
|
5
5
|
RESOLUTIONS,
|
|
6
|
-
setupClockMock,
|
|
7
|
-
setupScreenlyJsMock,
|
|
8
6
|
} from '@screenly/edge-apps/test/screenshots'
|
|
9
|
-
import path from 'path'
|
|
10
7
|
|
|
11
8
|
const { screenlyJsContent } = createMockScreenlyForScreenshots()
|
|
12
9
|
|
|
13
10
|
for (const { width, height } of RESOLUTIONS) {
|
|
14
11
|
test(`screenshot ${width}x${height}`, async ({ browser }) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
await setupClockMock(page)
|
|
21
|
-
await setupScreenlyJsMock(page, screenlyJsContent)
|
|
22
|
-
|
|
23
|
-
await page.goto('/')
|
|
24
|
-
await page.waitForLoadState('networkidle')
|
|
25
|
-
|
|
26
|
-
await page.screenshot({
|
|
27
|
-
path: path.join(screenshotsDir, `${width}x${height}.png`),
|
|
28
|
-
fullPage: false,
|
|
12
|
+
await captureScreenshot(browser, {
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
filenamePrefix: '{{APP_NAME}}',
|
|
16
|
+
screenlyJsContent,
|
|
29
17
|
})
|
|
30
|
-
|
|
31
|
-
await context.close()
|
|
32
18
|
})
|
|
33
19
|
}
|