creevey 0.10.0-beta.5 → 0.10.0-beta.7
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/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -0
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/assets/{index-DkmZfG9C.js → index-DB8lHlJw.js} +2 -2
- package/dist/client/web/index.html +1 -1
- package/dist/creevey.js +12 -5
- package/dist/creevey.js.map +1 -1
- package/dist/server/config.js +2 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/docker.js +2 -2
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +4 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +2 -1
- package/dist/server/logger.js +7 -3
- package/dist/server/logger.js.map +1 -1
- package/dist/server/master/api.js +1 -1
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/pool.d.ts +3 -3
- package/dist/server/master/pool.js +10 -65
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/queue.d.ts +13 -0
- package/dist/server/master/queue.js +64 -0
- package/dist/server/master/queue.js.map +1 -0
- package/dist/server/master/runner.d.ts +1 -0
- package/dist/server/master/runner.js +4 -1
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -1
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.js +4 -4
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/internal.js +21 -20
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.js +1 -1
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/providers/browser.js +2 -1
- package/dist/server/providers/browser.js.map +1 -1
- package/dist/server/providers/hybrid.js +1 -1
- package/dist/server/providers/hybrid.js.map +1 -1
- package/dist/server/reporter.js +4 -4
- package/dist/server/reporter.js.map +1 -1
- package/dist/server/selenium/internal.d.ts +1 -2
- package/dist/server/selenium/internal.js +95 -75
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/webdriver.js +1 -1
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/utils.d.ts +2 -1
- package/dist/server/utils.js +11 -0
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +2 -3
- package/dist/server/webdriver.js +9 -9
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/chai-image.d.ts +1 -2
- package/dist/server/worker/chai-image.js +4 -3
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/start.js +7 -10
- package/dist/server/worker/start.js.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.js.map +1 -1
- package/package.json +30 -30
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/creevey.ts +12 -6
- package/src/server/config.ts +2 -1
- package/src/server/docker.ts +2 -2
- package/src/server/index.ts +4 -4
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +18 -58
- package/src/server/master/queue.ts +64 -0
- package/src/server/master/runner.ts +4 -1
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +7 -4
- package/src/server/playwright/internal.ts +22 -20
- package/src/server/playwright/webdriver.ts +1 -1
- package/src/server/providers/browser.ts +2 -1
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporter.ts +4 -3
- package/src/server/selenium/internal.ts +98 -76
- package/src/server/selenium/webdriver.ts +1 -1
- package/src/server/utils.ts +12 -1
- package/src/server/webdriver.ts +9 -15
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/start.ts +7 -11
- package/src/types.ts +6 -2
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
@@ -105,7 +105,7 @@ async function buildWebdriver(
|
|
105
105
|
const url = new URL(gridUrl);
|
106
106
|
url.username = url.username ? '********' : '';
|
107
107
|
url.password = url.password ? '********' : '';
|
108
|
-
logger.debug(`
|
108
|
+
logger().debug(`Connecting to Selenium ${chalk.magenta(url.toString())}`);
|
109
109
|
|
110
110
|
// TODO Define some capabilities explicitly and define typings
|
111
111
|
const capabilities = new Capabilities({
|
@@ -125,33 +125,61 @@ async function buildWebdriver(
|
|
125
125
|
// TODO Validate browsers, versions, and platform
|
126
126
|
// TODO Use `customizeBuilder`
|
127
127
|
|
128
|
-
let webdriver: WebDriver;
|
128
|
+
let webdriver: WebDriver | null;
|
129
129
|
|
130
130
|
try {
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
131
|
+
const maxRetries = 5;
|
132
|
+
let retries = 0;
|
133
|
+
do {
|
134
|
+
webdriver = await Promise.race([
|
135
|
+
new Promise<null>((resolve) => {
|
136
|
+
setTimeout(() => {
|
137
|
+
retries += 1;
|
138
|
+
resolve(null);
|
139
|
+
}, 120_000);
|
140
|
+
}),
|
141
|
+
(async () => {
|
142
|
+
if (retries > 0) {
|
143
|
+
logger().debug(`Trying to initialize session to Selenium Grid: retried ${retries} of ${maxRetries}`);
|
144
|
+
}
|
145
|
+
const retry = retries;
|
146
|
+
// const ie = new IeOptions();
|
147
|
+
// const edge = new EdgeOptions();
|
148
|
+
// const chrome = new ChromeOptions();
|
149
|
+
// const safari = new SafariOptions();
|
150
|
+
// const firefox = new FirefoxOptions();
|
151
|
+
// edge.enableBidi();
|
152
|
+
// chrome.enableBidi();
|
153
|
+
// firefox.enableBidi();
|
154
|
+
|
155
|
+
const driver = await new Builder()
|
156
|
+
// .setIeOptions(ie)
|
157
|
+
// .setEdgeOptions(edge)
|
158
|
+
// .setChromeOptions(chrome)
|
159
|
+
// .setSafariOptions(safari)
|
160
|
+
// .setFirefoxOptions(firefox)
|
161
|
+
.usingServer(gridUrl)
|
162
|
+
.withCapabilities(capabilities)
|
163
|
+
.setLoggingPrefs(prefs) // NOTE: Should go last
|
164
|
+
.build();
|
165
|
+
|
166
|
+
// const id = await driver.getWindowHandle();
|
167
|
+
// context = await BrowsingContext(driver, { browsingContextId: id });
|
168
|
+
|
169
|
+
if (retry != retries) {
|
170
|
+
void driver.quit();
|
171
|
+
return null;
|
172
|
+
}
|
173
|
+
|
174
|
+
return driver;
|
175
|
+
})(),
|
176
|
+
]);
|
177
|
+
if (webdriver) break;
|
178
|
+
} while (retries < maxRetries);
|
179
|
+
|
180
|
+
if (!webdriver) throw new Error('Failed to initialize session to Selenium Grid due to many retries');
|
153
181
|
} catch (error) {
|
154
|
-
logger.error(`
|
182
|
+
logger().error(`Failed to start browser:`, error);
|
155
183
|
return null;
|
156
184
|
}
|
157
185
|
|
@@ -163,13 +191,11 @@ export class InternalBrowser {
|
|
163
191
|
#browser: WebDriver;
|
164
192
|
#serverHost: string | null = null;
|
165
193
|
#serverPort: number;
|
166
|
-
#logger: Logger.Logger;
|
167
194
|
#unsubscribe: () => void = noop;
|
168
195
|
#keepAliveInterval: NodeJS.Timeout | null = null;
|
169
|
-
constructor(browser: WebDriver, port: number
|
196
|
+
constructor(browser: WebDriver, port: number) {
|
170
197
|
this.#browser = browser;
|
171
198
|
this.#serverPort = port;
|
172
|
-
this.#logger = logger;
|
173
199
|
this.#unsubscribe = subscribeOn('shutdown', () => {
|
174
200
|
void this.closeBrowser();
|
175
201
|
});
|
@@ -198,7 +224,7 @@ export class InternalBrowser {
|
|
198
224
|
|
199
225
|
const ignoreStyles = await this.insertIgnoreStyles(ignoreElements);
|
200
226
|
|
201
|
-
if (
|
227
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
202
228
|
const { innerWidth, innerHeight } = await this.#browser.executeScript<{
|
203
229
|
innerWidth: number;
|
204
230
|
innerHeight: number;
|
@@ -208,16 +234,16 @@ export class InternalBrowser {
|
|
208
234
|
innerHeight: window.innerHeight,
|
209
235
|
};
|
210
236
|
});
|
211
|
-
|
237
|
+
logger().debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
|
212
238
|
}
|
213
239
|
|
214
240
|
try {
|
215
241
|
if (!captureElement) {
|
216
|
-
|
242
|
+
logger().debug('Capturing viewport screenshot');
|
217
243
|
screenshot = await this.#browser.takeScreenshot();
|
218
|
-
|
244
|
+
logger().debug('Viewport screenshot is captured');
|
219
245
|
} else {
|
220
|
-
|
246
|
+
logger().debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
|
221
247
|
const rects = await this.#browser.executeScript<
|
222
248
|
{ elementRect: ElementRect; windowRect: ElementRect } | undefined
|
223
249
|
>(function (selector: string): { elementRect: ElementRect; windowRect: ElementRect } | undefined {
|
@@ -256,11 +282,11 @@ export class InternalBrowser {
|
|
256
282
|
elementRect.height + elementRect.top <= windowRect.height;
|
257
283
|
|
258
284
|
if (isFitIntoViewport) {
|
259
|
-
|
285
|
+
logger().debug(
|
260
286
|
`Capturing ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
|
261
287
|
);
|
262
288
|
} else
|
263
|
-
|
289
|
+
logger().debug(
|
264
290
|
`Capturing composite screenshot image of ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
|
265
291
|
);
|
266
292
|
|
@@ -276,7 +302,7 @@ export class InternalBrowser {
|
|
276
302
|
: // TODO pointer-events: none, need to research
|
277
303
|
await this.takeCompositeScreenshot(windowRect, elementRect);
|
278
304
|
|
279
|
-
|
305
|
+
logger().debug(`${chalk.cyan(captureElement)} is captured`);
|
280
306
|
}
|
281
307
|
} finally {
|
282
308
|
await this.removeIgnoreStyles(ignoreStyles);
|
@@ -298,7 +324,7 @@ export class InternalBrowser {
|
|
298
324
|
await this.updateBrowserGlobalVariables();
|
299
325
|
await this.resetMousePosition();
|
300
326
|
|
301
|
-
|
327
|
+
logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
|
302
328
|
|
303
329
|
const result = await this.#browser.executeAsyncScript<[error?: string | null, isCaptureCalled?: boolean] | null>(
|
304
330
|
function (
|
@@ -360,14 +386,14 @@ export class InternalBrowser {
|
|
360
386
|
}
|
361
387
|
|
362
388
|
async afterTest(test: ServerTest): Promise<void> {
|
363
|
-
if (
|
389
|
+
if (logger().getLevel() === Logger.levels.TRACE) {
|
364
390
|
const output: string[] = [];
|
365
391
|
const types = await this.#browser.manage().logs().getAvailableLogTypes();
|
366
392
|
for (const type of types) {
|
367
393
|
const logs = await this.#browser.manage().logs().get(type);
|
368
394
|
output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n'));
|
369
395
|
}
|
370
|
-
|
396
|
+
logger().debug(
|
371
397
|
'----------',
|
372
398
|
getTestPath(test).join('/'),
|
373
399
|
'----------\n',
|
@@ -391,7 +417,7 @@ export class InternalBrowser {
|
|
391
417
|
|
392
418
|
if (!browser) return null;
|
393
419
|
|
394
|
-
const internalBrowser = new InternalBrowser(browser, options.port
|
420
|
+
const internalBrowser = new InternalBrowser(browser, options.port);
|
395
421
|
|
396
422
|
try {
|
397
423
|
if (isShuttingDown.current) return null;
|
@@ -409,11 +435,12 @@ export class InternalBrowser {
|
|
409
435
|
} catch (originalError) {
|
410
436
|
void internalBrowser.closeBrowser();
|
411
437
|
|
412
|
-
const message =
|
413
|
-
|
438
|
+
const message =
|
439
|
+
originalError instanceof Error ? originalError.message : ((originalError ?? 'Unknown error') as string);
|
440
|
+
const error = new Error(`Can't load storybook root page: ${message}`);
|
414
441
|
if (originalError instanceof Error) error.stack = originalError.stack;
|
415
442
|
|
416
|
-
logger.error(error);
|
443
|
+
logger().error(error);
|
417
444
|
|
418
445
|
return null;
|
419
446
|
}
|
@@ -444,16 +471,14 @@ export class InternalBrowser {
|
|
444
471
|
/* noop */
|
445
472
|
}
|
446
473
|
|
447
|
-
|
448
|
-
|
449
|
-
prefix.apply(this.#logger, {
|
474
|
+
prefix.apply(logger(), {
|
450
475
|
format(level) {
|
451
476
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
452
|
-
return `[${browserName}:${chalk.gray(
|
477
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
|
453
478
|
},
|
454
479
|
});
|
455
480
|
|
456
|
-
|
481
|
+
logger().debug(`Connected successful with ${chalk.green(browserHost)}`);
|
457
482
|
|
458
483
|
return await runSequence(
|
459
484
|
[
|
@@ -482,20 +507,20 @@ export class InternalBrowser {
|
|
482
507
|
|
483
508
|
try {
|
484
509
|
if (resolver) {
|
485
|
-
|
510
|
+
logger().debug('Resolving storybook url with custom resolver');
|
486
511
|
|
487
512
|
const resolvedUrl = await resolver();
|
488
513
|
|
489
|
-
|
514
|
+
logger().debug(`Resolver storybook url ${resolvedUrl}`);
|
490
515
|
|
491
516
|
await this.#browser.get(appendIframePath(resolvedUrl));
|
492
517
|
} else {
|
493
518
|
// TODO Pageload timeout 10s
|
494
519
|
// NOTE: getUrlChecker already calls `browser.get` so we don't need another one
|
495
|
-
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url)
|
520
|
+
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
|
496
521
|
}
|
497
522
|
} catch (error) {
|
498
|
-
|
523
|
+
logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
|
499
524
|
throw error;
|
500
525
|
}
|
501
526
|
}
|
@@ -503,13 +528,13 @@ export class InternalBrowser {
|
|
503
528
|
private async checkUrl(url: string): Promise<boolean> {
|
504
529
|
try {
|
505
530
|
// NOTE: Before trying a new url, reset the current one
|
506
|
-
|
531
|
+
logger().debug(`Opening ${chalk.magenta('about:blank')} page`);
|
507
532
|
await openUrlAndWaitForPageSource(
|
508
533
|
this.#browser,
|
509
534
|
'about:blank',
|
510
535
|
(source: string) => !source.includes('<body></body>'),
|
511
536
|
);
|
512
|
-
|
537
|
+
logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
|
513
538
|
const source = await openUrlAndWaitForPageSource(
|
514
539
|
this.#browser,
|
515
540
|
url,
|
@@ -521,7 +546,7 @@ export class InternalBrowser {
|
|
521
546
|
// because other add significant delay and some of them don't work in earlier chrome versions
|
522
547
|
// Browsers always load page successful even it's failed
|
523
548
|
// So we just check `root` element
|
524
|
-
|
549
|
+
logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
|
525
550
|
return source.includes(`id="${storybookRootID}"`);
|
526
551
|
} catch {
|
527
552
|
return false;
|
@@ -529,7 +554,7 @@ export class InternalBrowser {
|
|
529
554
|
}
|
530
555
|
|
531
556
|
private async waitForStorybook(): Promise<void> {
|
532
|
-
|
557
|
+
logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
|
533
558
|
|
534
559
|
const isTimeout = await Promise.race([
|
535
560
|
new Promise<boolean>((resolve) => {
|
@@ -540,19 +565,15 @@ export class InternalBrowser {
|
|
540
565
|
(async () => {
|
541
566
|
let wait = true;
|
542
567
|
do {
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
}, StorybookEvents.SET_GLOBALS);
|
553
|
-
} catch (e: unknown) {
|
554
|
-
this.#logger.debug('An error has been caught during the script:', e);
|
555
|
-
}
|
568
|
+
// TODO Research a different way to ensure storybook is initiated
|
569
|
+
wait = await this.#browser.executeScript<boolean>(function (SET_GLOBALS: string): boolean {
|
570
|
+
// TODO Maybe use
|
571
|
+
// import { global } from '@storybook/global';
|
572
|
+
// global.IS_STORYBOOK
|
573
|
+
if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
|
574
|
+
if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
|
575
|
+
return false;
|
576
|
+
}, StorybookEvents.SET_GLOBALS);
|
556
577
|
} while (wait);
|
557
578
|
return false;
|
558
579
|
})(),
|
@@ -565,7 +586,7 @@ export class InternalBrowser {
|
|
565
586
|
private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
|
566
587
|
if (!globals) return;
|
567
588
|
|
568
|
-
|
589
|
+
logger().debug('Applying storybook globals');
|
569
590
|
await this.#browser.executeScript(function (globals: StorybookGlobals) {
|
570
591
|
window.__CREEVEY_UPDATE_GLOBALS__(globals);
|
571
592
|
}, globals);
|
@@ -635,7 +656,7 @@ export class InternalBrowser {
|
|
635
656
|
},
|
636
657
|
);
|
637
658
|
|
638
|
-
|
659
|
+
logger().debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
|
639
660
|
|
640
661
|
const dWidth = windowRect.width - innerWidth;
|
641
662
|
const dHeight = windowRect.height - innerHeight;
|
@@ -649,7 +670,7 @@ export class InternalBrowser {
|
|
649
670
|
}
|
650
671
|
|
651
672
|
private async resetMousePosition(): Promise<void> {
|
652
|
-
|
673
|
+
logger().debug('Resetting mouse position to the top-left corner');
|
653
674
|
const browserName = (await this.#browser.getCapabilities()).getBrowserName();
|
654
675
|
const [browserVersion] =
|
655
676
|
(await this.#browser.getCapabilities()).getBrowserVersion()?.split('.') ??
|
@@ -677,8 +698,9 @@ export class InternalBrowser {
|
|
677
698
|
y: Math.ceil((-1 * height) / 2) - top,
|
678
699
|
})
|
679
700
|
.perform();
|
680
|
-
} else if (browserName == 'firefox'
|
701
|
+
} else if (browserName == 'firefox') {
|
681
702
|
// NOTE Firefox for some reason moving by 0 x 0 move cursor in bottom left corner :sad:
|
703
|
+
// NOTE In recent versions (eg 128.0) moving by 0 x 0 doesn't work at all
|
682
704
|
await this.#browser.actions().move({ origin: Origin.VIEWPORT, x: 0, y: 1 }).perform();
|
683
705
|
} else {
|
684
706
|
// NOTE IE don't emit move events until force window focus or connect by RDP on virtual machine
|
@@ -690,7 +712,7 @@ export class InternalBrowser {
|
|
690
712
|
const ignoreSelectors = Array.prototype.concat(ignoreElements).filter(Boolean);
|
691
713
|
if (!ignoreSelectors.length) return null;
|
692
714
|
|
693
|
-
|
715
|
+
logger().debug('Hiding ignored elements before capturing');
|
694
716
|
|
695
717
|
return await this.#browser.executeScript(function (ignoreSelectors: string[]) {
|
696
718
|
return window.__CREEVEY_INSERT_IGNORE_STYLES__(ignoreSelectors);
|
@@ -771,7 +793,7 @@ export class InternalBrowser {
|
|
771
793
|
|
772
794
|
private async removeIgnoreStyles(ignoreStyles: WebElement | null): Promise<void> {
|
773
795
|
if (ignoreStyles) {
|
774
|
-
|
796
|
+
logger().debug('Revert hiding ignored elements');
|
775
797
|
await this.#browser.executeScript(function (ignoreStyles: HTMLStyleElement) {
|
776
798
|
window.__CREEVEY_REMOVE_IGNORE_STYLES__(ignoreStyles);
|
777
799
|
}, ignoreStyles);
|
@@ -810,7 +832,7 @@ export class InternalBrowser {
|
|
810
832
|
this.#keepAliveInterval = setInterval(() => {
|
811
833
|
// NOTE Simple way to keep session alive
|
812
834
|
void this.#browser.getCurrentUrl().then((url) => {
|
813
|
-
logger.debug('current url', chalk.magenta(url));
|
835
|
+
logger().debug('current url', chalk.magenta(url));
|
814
836
|
});
|
815
837
|
}, 10 * 1000);
|
816
838
|
}
|
package/src/server/utils.ts
CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
6
|
import { createRequire } from 'module';
|
7
7
|
import { register as esmRegister } from 'tsx/esm/api';
|
8
8
|
import { register as cjsRegister } from 'tsx/cjs/api';
|
9
|
-
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
|
9
|
+
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
|
10
10
|
import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
|
11
11
|
|
12
12
|
const importMetaUrl = pathToFileURL(__filename).href;
|
@@ -96,6 +96,17 @@ export async function shutdownWorkers(): Promise<void> {
|
|
96
96
|
emitShutdownMessage();
|
97
97
|
}
|
98
98
|
|
99
|
+
export function gracefullyKill(worker: Worker): void {
|
100
|
+
worker.isShuttingDown = true;
|
101
|
+
const timeout = setTimeout(() => {
|
102
|
+
worker.kill();
|
103
|
+
}, 10000);
|
104
|
+
worker.on('exit', () => {
|
105
|
+
clearTimeout(timeout);
|
106
|
+
});
|
107
|
+
sendShutdownMessage(worker);
|
108
|
+
}
|
109
|
+
|
99
110
|
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
111
|
const { default: findCacheDir } = await import('find-cache-dir');
|
101
112
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
package/src/server/webdriver.ts
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
import Logger from 'loglevel';
|
2
1
|
import chalk from 'chalk';
|
3
2
|
import { networkInterfaces } from 'os';
|
4
|
-
import { logger
|
3
|
+
import { logger } from './logger.js';
|
5
4
|
import { Args } from '@storybook/csf';
|
6
5
|
import {
|
7
6
|
isDefined,
|
@@ -22,16 +21,15 @@ const DOCKER_INTERNAL = 'host.docker.internal';
|
|
22
21
|
export async function resolveStorybookUrl(
|
23
22
|
storybookUrl: string,
|
24
23
|
checkUrl: (url: string) => Promise<boolean>,
|
25
|
-
logger: Logger.Logger = defaultLogger,
|
26
24
|
): Promise<string> {
|
27
|
-
logger.debug('Resolving storybook url');
|
25
|
+
logger().debug('Resolving storybook url');
|
28
26
|
const addresses = getAddresses();
|
29
27
|
// TODO Use Promise.race?
|
30
28
|
for (const ip of addresses) {
|
31
29
|
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
32
|
-
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
30
|
+
logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
33
31
|
if (await checkUrl(resolvedUrl)) {
|
34
|
-
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
32
|
+
logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
35
33
|
return resolvedUrl;
|
36
34
|
}
|
37
35
|
}
|
@@ -75,11 +73,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
75
73
|
|
76
74
|
abstract afterTest(test: ServerTest): Promise<void>;
|
77
75
|
|
78
|
-
async switchStory(
|
79
|
-
story: StoryInput,
|
80
|
-
context: BaseCreeveyTestContext,
|
81
|
-
logger: Logger.Logger,
|
82
|
-
): Promise<CreeveyTestContext> {
|
76
|
+
async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
|
83
77
|
const { id, title, name, parameters } = story;
|
84
78
|
const {
|
85
79
|
captureElement = `#${storybookRootID}`,
|
@@ -87,7 +81,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
87
81
|
ignoreElements,
|
88
82
|
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
89
83
|
|
90
|
-
logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
84
|
+
logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
91
85
|
|
92
86
|
let storyPlayResolver: (isCompleted: boolean) => void;
|
93
87
|
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
@@ -108,7 +102,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
108
102
|
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
109
103
|
|
110
104
|
if (isCaptureCalled) {
|
111
|
-
logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
105
|
+
logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
112
106
|
while (!(await waitForComplete)) {
|
113
107
|
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
114
108
|
}
|
@@ -116,8 +110,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
116
110
|
|
117
111
|
unsubscribe();
|
118
112
|
|
119
|
-
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
120
|
-
else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
113
|
+
if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
|
114
|
+
else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
121
115
|
|
122
116
|
return Object.assign(
|
123
117
|
{
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import
|
1
|
+
import { logger } from '../logger';
|
2
|
+
|
2
3
|
export default function (
|
3
4
|
matchImage: (image: Buffer, imageName?: string) => Promise<void>,
|
4
5
|
matchImages: (images: Record<string, Buffer>) => Promise<void>,
|
5
|
-
logger: Logger.Logger,
|
6
6
|
) {
|
7
7
|
let isWarningShown = false;
|
8
8
|
return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
|
@@ -11,7 +11,7 @@ export default function (
|
|
11
11
|
'matchImage',
|
12
12
|
async function (this: Record<string, unknown>, imageName?: string) {
|
13
13
|
if (!isWarningShown) {
|
14
|
-
logger.warn(
|
14
|
+
logger().warn(
|
15
15
|
'`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
|
16
16
|
);
|
17
17
|
isWarningShown = true;
|
@@ -23,7 +23,7 @@ export default function (
|
|
23
23
|
|
24
24
|
utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
|
25
25
|
if (!isWarningShown) {
|
26
|
-
logger.warn(
|
26
|
+
logger().warn(
|
27
27
|
'`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
|
28
28
|
);
|
29
29
|
isWarningShown = true;
|
@@ -1,6 +1,4 @@
|
|
1
1
|
import chai from 'chai';
|
2
|
-
import chalk from 'chalk';
|
3
|
-
import Logger from 'loglevel';
|
4
2
|
import EventEmitter from 'events';
|
5
3
|
import {
|
6
4
|
BaseCreeveyTestContext,
|
@@ -86,7 +84,7 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
|
|
86
84
|
|
87
85
|
async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
|
88
86
|
if ((await webdriver.openBrowser(true)) == null) {
|
89
|
-
logger.error('Failed to start browser');
|
87
|
+
logger().error('Failed to start browser');
|
90
88
|
emitWorkerMessage({
|
91
89
|
type: 'error',
|
92
90
|
payload: { subtype: 'browser', error: 'Failed to start browser' },
|
@@ -125,8 +123,6 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
125
123
|
|
126
124
|
if (!webdriver || !sessionId) return;
|
127
125
|
|
128
|
-
const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
|
129
|
-
|
130
126
|
const reporterOptions = {
|
131
127
|
...config.reporterOptions,
|
132
128
|
creevey: {
|
@@ -150,13 +146,13 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
150
146
|
const { matchImage, matchImages } = options.odiff
|
151
147
|
? getOdiffMatchers(imagesContext, config)
|
152
148
|
: await getMatchers(imagesContext, config);
|
153
|
-
chai.use(chaiImage(matchImage, matchImages
|
149
|
+
chai.use(chaiImage(matchImage, matchImages));
|
154
150
|
|
155
151
|
const tests = await (async () => {
|
156
152
|
try {
|
157
153
|
return await getTestsFromStories(config, browser, webdriver);
|
158
154
|
} catch (error) {
|
159
|
-
|
155
|
+
logger().error('Failed to get tests from stories:', error);
|
160
156
|
emitWorkerMessage({
|
161
157
|
type: 'error',
|
162
158
|
payload: { subtype: 'browser', error: serializeError(error) },
|
@@ -174,7 +170,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
174
170
|
|
175
171
|
if (!test) {
|
176
172
|
const error = `Test with id ${message.payload.id} not found`;
|
177
|
-
|
173
|
+
logger().error(error);
|
178
174
|
emitWorkerMessage({
|
179
175
|
type: 'error',
|
180
176
|
payload: { subtype: 'test', error },
|
@@ -235,7 +231,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
235
231
|
}, config.testTimeout),
|
236
232
|
),
|
237
233
|
(async () => {
|
238
|
-
const context = await webdriver.switchStory(test.story, baseContext
|
234
|
+
const context = await webdriver.switchStory(test.story, baseContext);
|
239
235
|
await test.fn(context);
|
240
236
|
})(),
|
241
237
|
]);
|
@@ -261,7 +257,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
261
257
|
|
262
258
|
runHandler(baseContext.browserName, imagesContext.images, error);
|
263
259
|
})().catch((error: unknown) => {
|
264
|
-
|
260
|
+
logger().error('Unexpected error:', error);
|
265
261
|
emitWorkerMessage({
|
266
262
|
type: 'error',
|
267
263
|
payload: { subtype: 'test', error: serializeError(error) },
|
@@ -269,7 +265,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
269
265
|
});
|
270
266
|
});
|
271
267
|
|
272
|
-
|
268
|
+
logger().info('Browser is ready');
|
273
269
|
|
274
270
|
emitWorkerMessage({ type: 'ready' });
|
275
271
|
}
|
package/src/types.ts
CHANGED
@@ -4,7 +4,6 @@ import type Pixelmatch from 'pixelmatch';
|
|
4
4
|
import type { ODiffOptions } from 'odiff-bin';
|
5
5
|
import type { expect } from 'chai';
|
6
6
|
import type EventEmitter from 'events';
|
7
|
-
import type Logger from 'loglevel';
|
8
7
|
import { LaunchOptions } from 'playwright-core';
|
9
8
|
// import type { Browser } from 'playwright-core';
|
10
9
|
|
@@ -164,7 +163,7 @@ export interface CreeveyWebdriver {
|
|
164
163
|
openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
|
165
164
|
closeBrowser(): Promise<void>;
|
166
165
|
loadStoriesFromBrowser(): Promise<StoriesRaw>;
|
167
|
-
switchStory(story: StoryInput, context: BaseCreeveyTestContext
|
166
|
+
switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext>;
|
168
167
|
afterTest(test: ServerTest): Promise<void>;
|
169
168
|
}
|
170
169
|
|
@@ -307,6 +306,11 @@ export interface Config {
|
|
307
306
|
* The `--ui` CLI option ignores this option
|
308
307
|
*/
|
309
308
|
failFast: boolean;
|
309
|
+
/**
|
310
|
+
* Start workers in sequential queue
|
311
|
+
* @default false
|
312
|
+
*/
|
313
|
+
useWorkerQueue: boolean;
|
310
314
|
/**
|
311
315
|
* Specify platform for docker images
|
312
316
|
*/
|
package/.yarnrc.yml
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
nodeLinker: node-modules
|