creevey 0.10.0-beta.31 → 0.10.0-beta.33

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 (59) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/withCreevey.js +1 -0
  3. package/dist/client/addon/withCreevey.js.map +1 -1
  4. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  5. package/dist/client/web/CreeveyApp.js +1 -0
  6. package/dist/client/web/CreeveyApp.js.map +1 -1
  7. package/dist/server/docker.d.ts +1 -1
  8. package/dist/server/docker.js +21 -14
  9. package/dist/server/docker.js.map +1 -1
  10. package/dist/server/index.js +9 -10
  11. package/dist/server/index.js.map +1 -1
  12. package/dist/server/playwright/docker-file.d.ts +1 -2
  13. package/dist/server/playwright/docker-file.js +10 -4
  14. package/dist/server/playwright/docker-file.js.map +1 -1
  15. package/dist/server/playwright/docker.d.ts +2 -1
  16. package/dist/server/playwright/docker.js +10 -2
  17. package/dist/server/playwright/docker.js.map +1 -1
  18. package/dist/server/playwright/internal.d.ts +3 -4
  19. package/dist/server/playwright/internal.js +48 -37
  20. package/dist/server/playwright/internal.js.map +1 -1
  21. package/dist/server/playwright/webdriver.js +4 -7
  22. package/dist/server/playwright/webdriver.js.map +1 -1
  23. package/dist/server/selenium/internal.js +5 -12
  24. package/dist/server/selenium/internal.js.map +1 -1
  25. package/dist/server/selenium/webdriver.js +4 -8
  26. package/dist/server/selenium/webdriver.js.map +1 -1
  27. package/dist/server/telemetry.js +2 -2
  28. package/dist/server/utils.d.ts +1 -2
  29. package/dist/server/utils.js +11 -8
  30. package/dist/server/utils.js.map +1 -1
  31. package/dist/server/webdriver.d.ts +2 -0
  32. package/dist/server/webdriver.js +13 -1
  33. package/dist/server/webdriver.js.map +1 -1
  34. package/dist/server/worker/context.d.ts +3 -0
  35. package/dist/server/worker/context.js +15 -0
  36. package/dist/server/worker/context.js.map +1 -0
  37. package/dist/types.d.ts +0 -2
  38. package/dist/types.js.map +1 -1
  39. package/docs/cli.md +12 -0
  40. package/docs/config.md +178 -167
  41. package/docs/storybook.md +60 -0
  42. package/docs/tests.md +50 -45
  43. package/package.json +1 -1
  44. package/src/client/addon/withCreevey.ts +1 -0
  45. package/src/client/web/CreeveyApp.tsx +1 -0
  46. package/src/server/docker.ts +24 -13
  47. package/src/server/index.ts +11 -14
  48. package/src/server/playwright/docker-file.ts +12 -5
  49. package/src/server/playwright/docker.ts +16 -3
  50. package/src/server/playwright/index-source.mjs +16 -0
  51. package/src/server/playwright/internal.ts +78 -52
  52. package/src/server/playwright/webdriver.ts +4 -7
  53. package/src/server/selenium/internal.ts +5 -12
  54. package/src/server/selenium/webdriver.ts +4 -8
  55. package/src/server/telemetry.ts +2 -2
  56. package/src/server/utils.ts +33 -25
  57. package/src/server/webdriver.ts +13 -0
  58. package/src/server/worker/context.ts +14 -0
  59. package/src/types.ts +0 -2
@@ -1,8 +1,10 @@
1
- import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
1
+ import path from 'path';
2
+ import { Browser, BrowserContext, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
2
3
  import chalk from 'chalk';
3
4
  import { v4 } from 'uuid';
4
5
  import Logger from 'loglevel';
5
6
  import prefix from 'loglevel-plugin-prefix';
7
+ import type { Args } from '@storybook/csf';
6
8
  import {
7
9
  BrowserConfigObject,
8
10
  Config,
@@ -11,13 +13,18 @@ import {
11
13
  StoryInput,
12
14
  StorybookEvents,
13
15
  StorybookGlobals,
14
- noop,
15
16
  } from '../../types';
16
- import { subscribeOn } from '../messages';
17
- import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
18
- import { isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
17
+ import {
18
+ appendIframePath,
19
+ getAddresses,
20
+ LOCALHOST_REGEXP,
21
+ openBrowser,
22
+ resolveStorybookUrl,
23
+ storybookRootID,
24
+ } from '../webdriver';
25
+ import { getCreeveyCache, isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
19
26
  import { colors, logger } from '../logger';
20
- import type { Args } from '@storybook/csf';
27
+ import assert from 'assert';
21
28
 
22
29
  const browsers = {
23
30
  chromium,
@@ -59,22 +66,34 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
59
66
  export class InternalBrowser {
60
67
  #isShuttingDown = false;
61
68
  #browser: Browser;
69
+ #context: BrowserContext;
62
70
  #page: Page;
71
+ #traceDir: string;
63
72
  #sessionId: string = v4();
64
73
  #serverHost: string | null = null;
65
74
  #serverPort: number;
75
+ #debug: boolean;
66
76
  #storybookGlobals?: StorybookGlobals;
67
- #unsubscribe: () => void = noop;
68
- constructor(browser: Browser, page: Page, port: number, storybookGlobals?: StorybookGlobals) {
77
+ #closeBrowser = openBrowser();
78
+ constructor(
79
+ browser: Browser,
80
+ context: BrowserContext,
81
+ page: Page,
82
+ traceDir: string,
83
+ port: number,
84
+ debug: boolean,
85
+ storybookGlobals?: StorybookGlobals,
86
+ ) {
69
87
  this.#browser = browser;
88
+ this.#context = context;
70
89
  this.#page = page;
90
+ this.#traceDir = traceDir;
71
91
  this.#serverPort = port;
92
+ this.#debug = debug;
72
93
  this.#storybookGlobals = storybookGlobals;
73
- this.#unsubscribe = subscribeOn('shutdown', () => {
74
- void this.closeBrowser();
75
- });
76
94
  }
77
95
 
96
+ // TODO Expose #browser and #context in tests
78
97
  get browser() {
79
98
  return this.#page;
80
99
  }
@@ -87,19 +106,21 @@ export class InternalBrowser {
87
106
  if (this.#isShuttingDown) return;
88
107
 
89
108
  this.#isShuttingDown = true;
90
- this.#unsubscribe();
91
109
 
92
110
  try {
111
+ if (this.#debug) await this.#context.tracing.stop({ path: path.join(this.#traceDir, 'trace.zip') });
93
112
  await this.#page.close();
113
+ if (this.#debug) await this.#page.video()?.saveAs(path.join(this.#traceDir, 'video.webm'));
114
+ await this.#context.close();
94
115
  await this.#browser.close();
95
- } catch (_) {
116
+ } catch {
96
117
  /* noop */
118
+ } finally {
119
+ this.#closeBrowser();
97
120
  }
98
121
  }
99
122
 
100
123
  async takeScreenshot(captureElement?: string | null, ignoreElements?: string | string[] | null): Promise<Buffer> {
101
- // TODO Implement features from selenium `takeScreenshot`
102
- // TODO Do we need scroll bar hack from selenium?
103
124
  const ignore = Array.isArray(ignoreElements) ? ignoreElements : ignoreElements ? [ignoreElements] : [];
104
125
  const mask = ignore.map((el) => this.#page.locator(el));
105
126
  if (captureElement) {
@@ -186,14 +207,24 @@ export class InternalBrowser {
186
207
  seleniumCapabilities,
187
208
  playwrightOptions,
188
209
  } = browserConfig;
210
+ const parsedUrl = new URL(gridUrl);
211
+ const tracesDir = path.join(
212
+ playwrightOptions?.tracesDir ?? path.join(config.reportDir, 'traces'),
213
+ process.pid.toString(),
214
+ );
215
+ const cacheDir = await getCreeveyCache();
216
+
217
+ assert(cacheDir, "Couldn't get cache directory");
189
218
 
190
219
  let browser: Browser | null = null;
191
220
 
192
- const parsedUrl = new URL(gridUrl);
193
221
  if (parsedUrl.protocol === 'ws:') {
194
222
  browser = await tryConnect(browsers[resolvePlaywrightBrowserType(browserConfig.browserName)], gridUrl);
195
223
  } else if (parsedUrl.protocol === 'creevey:') {
196
- browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch(playwrightOptions);
224
+ browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch({
225
+ ...playwrightOptions,
226
+ tracesDir: path.join(cacheDir, `${process.pid}`),
227
+ });
197
228
  } else {
198
229
  if (browserConfig.browserName !== 'chrome') {
199
230
  logger().error("Playwright's Selenium Grid feature supports only chrome browser");
@@ -203,20 +234,29 @@ export class InternalBrowser {
203
234
  process.env.SELENIUM_REMOTE_URL = gridUrl;
204
235
  process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
205
236
 
206
- browser = await chromium.launch(playwrightOptions);
237
+ browser = await chromium.launch({ ...playwrightOptions, tracesDir: path.join(cacheDir, `${process.pid}`) });
207
238
  }
208
239
 
209
240
  if (!browser) {
210
241
  return null;
211
242
  }
212
243
 
213
- // TODO Record video
214
- const page = await browser.newPage();
215
- // TODO Support tracing
216
- // if (playwrightOptions?.trace) {
217
- // const context = page.context();
218
- // await context.tracing.start(playwrightOptions.trace);
219
- // }
244
+ const context = await browser.newContext({
245
+ recordVideo: options.debug
246
+ ? {
247
+ dir: path.join(cacheDir, `${process.pid}`),
248
+ size: viewport,
249
+ }
250
+ : undefined,
251
+ screen: viewport,
252
+ viewport,
253
+ });
254
+ const page = await context.newPage();
255
+ if (options.debug) {
256
+ await context.tracing.start(
257
+ Object.assign({ screenshots: true, snapshots: true, sources: true }, playwrightOptions?.trace),
258
+ );
259
+ }
220
260
 
221
261
  if (logger().getLevel() <= Logger.levels.DEBUG) {
222
262
  page.on('console', (msg) => {
@@ -224,15 +264,20 @@ export class InternalBrowser {
224
264
  });
225
265
  }
226
266
 
227
- // TODO Add debug output
228
-
229
- const internalBrowser = new InternalBrowser(browser, page, options.port, _storybookGlobals);
267
+ const internalBrowser = new InternalBrowser(
268
+ browser,
269
+ context,
270
+ page,
271
+ tracesDir,
272
+ options.port,
273
+ options.debug,
274
+ _storybookGlobals,
275
+ );
230
276
 
231
277
  try {
232
278
  if (isShuttingDown.current) return null;
233
279
  const done = await internalBrowser.init({
234
280
  browserName,
235
- viewport,
236
281
  storybookUrl: address,
237
282
  });
238
283
 
@@ -250,15 +295,7 @@ export class InternalBrowser {
250
295
  }
251
296
  }
252
297
 
253
- private async init({
254
- browserName,
255
- viewport,
256
- storybookUrl,
257
- }: {
258
- browserName: string;
259
- viewport?: { width: number; height: number };
260
- storybookUrl: string;
261
- }) {
298
+ private async init({ browserName, storybookUrl }: { browserName: string; storybookUrl: string }) {
262
299
  const sessionId = this.#sessionId;
263
300
 
264
301
  prefix.apply(logger(), {
@@ -278,7 +315,6 @@ export class InternalBrowser {
278
315
  () => this.updateStorybookGlobals(),
279
316
  () => this.resolveCreeveyHost(),
280
317
  () => this.updateBrowserGlobalVariables(),
281
- () => this.resizeViewport(viewport),
282
318
  ],
283
319
  () => !this.#isShuttingDown,
284
320
  );
@@ -291,7 +327,6 @@ export class InternalBrowser {
291
327
  }
292
328
 
293
329
  try {
294
- // TODO this.#page.setDefaultNavigationTimeout(10000);
295
330
  const resolvedUrl = await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
296
331
  await this.#page.goto(resolvedUrl);
297
332
  } catch (error) {
@@ -317,8 +352,7 @@ export class InternalBrowser {
317
352
  }
318
353
 
319
354
  private async waitForStorybook(): Promise<void> {
320
- // TODO Duplicated code with selenium
321
- logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
355
+ logger().debug('Waiting for Storybook to initiate');
322
356
 
323
357
  const isTimeout = await Promise.race([
324
358
  new Promise<boolean>((resolve) => {
@@ -331,7 +365,6 @@ export class InternalBrowser {
331
365
  do {
332
366
  try {
333
367
  // TODO Research a different way to ensure storybook is initiated
334
- // TODO Maybe use `__STORYBOOK_PREVIEW__.extract()`
335
368
  wait = await this.#page.evaluate((SET_GLOBALS: string) => {
336
369
  if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
337
370
  if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
@@ -347,10 +380,10 @@ export class InternalBrowser {
347
380
  })(),
348
381
  ]);
349
382
 
350
- // TODO Change the message to describe a reason why it might happen
351
- if (isTimeout) throw new Error('Failed to wait `setStories` event');
383
+ if (isTimeout) throw new Error('Failed to wait Storybook init');
352
384
  }
353
385
 
386
+ // TODO Doesn't work for some reason, maybe because of race-condition
354
387
  private async triggerViteReload(): Promise<void> {
355
388
  // NOTE: On the first load, Vite might try to optimize some dependencies and reload the page
356
389
  // We need to trigger reload earlier to avoid unnecessary reloads further
@@ -410,13 +443,6 @@ export class InternalBrowser {
410
443
  );
411
444
  }
412
445
 
413
- private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
414
- if (!viewport) return;
415
-
416
- logger().debug('Resizing viewport to', viewport);
417
- await this.#page.setViewportSize(viewport);
418
- }
419
-
420
446
  private async resetMousePosition(): Promise<void> {
421
447
  logger().debug('Resetting mouse position to (0, 0)');
422
448
  await this.#page.mouse.move(0, 0);
@@ -5,6 +5,7 @@ import { logger } from '../logger';
5
5
  import { subscribeOn } from '../messages';
6
6
  import { CreeveyWebdriverBase } from '../webdriver';
7
7
  import type { InternalBrowser } from './internal';
8
+ import { removeWorkerContainer } from '../worker/context.js'; // Import container context
8
9
 
9
10
  export class PlaywrightWebdriver extends CreeveyWebdriverBase {
10
11
  #browser: InternalBrowser | null = null;
@@ -21,7 +22,9 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
21
22
  this.#options = options;
22
23
 
23
24
  subscribeOn('shutdown', () => {
24
- void this.#browser?.closeBrowser().finally(() => process.exit());
25
+ void this.#browser?.closeBrowser().finally(() => {
26
+ void removeWorkerContainer().finally(() => () => process.exit());
27
+ });
25
28
  this.#browser = null;
26
29
  });
27
30
  }
@@ -32,7 +35,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
32
35
 
33
36
  getSessionId(): Promise<string> {
34
37
  if (!this.#browser) {
35
- // TODO Describe the error
36
38
  throw new Error('Browser is not initialized');
37
39
  }
38
40
 
@@ -79,7 +81,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
79
81
 
80
82
  async loadStoriesFromBrowser(): Promise<StoriesRaw> {
81
83
  if (!this.#browser) {
82
- // TODO Describe the error
83
84
  throw new Error('Browser is not initialized');
84
85
  }
85
86
 
@@ -95,7 +96,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
95
96
  ignoreElements?: string | string[] | null,
96
97
  ): Promise<Buffer> {
97
98
  if (!this.#browser) {
98
- // TODO Describe the error
99
99
  throw new Error('Browser is not initialized');
100
100
  }
101
101
 
@@ -104,7 +104,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
104
104
 
105
105
  protected waitForComplete(callback: (isCompleted: boolean) => void): void {
106
106
  if (!this.#browser) {
107
- // TODO Describe the error
108
107
  throw new Error('Browser is not initialized');
109
108
  }
110
109
 
@@ -113,7 +112,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
113
112
 
114
113
  protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
115
114
  if (!this.#browser) {
116
- // TODO Describe the error
117
115
  throw new Error('Browser is not initialized');
118
116
  }
119
117
 
@@ -122,7 +120,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
122
120
 
123
121
  protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
124
122
  if (!this.#browser) {
125
- // TODO Describe the error
126
123
  throw new Error('Browser is not initialized');
127
124
  }
128
125
 
@@ -249,7 +249,7 @@ export class InternalBrowser {
249
249
  const rects = await this.#browser.executeScript<
250
250
  { elementRect: ElementRect; windowRect: ElementRect } | undefined
251
251
  >(function (selector: string): { elementRect: ElementRect; windowRect: ElementRect } | undefined {
252
- window.scrollTo(0, 0); // TODO Maybe we should remove same code from `resetMousePosition`
252
+ window.scrollTo(0, 0);
253
253
  // eslint-disable-next-line no-var
254
254
  var element = document.querySelector(selector);
255
255
  if (!element) return;
@@ -297,12 +297,10 @@ export class InternalBrowser {
297
297
  // ? context
298
298
  // ? await context.captureElementScreenshot(await element.getId())
299
299
  // : await browser.findElement(By.css(captureElement)).takeScreenshot()
300
- // : // TODO pointer-events: none, need to research
301
- // await takeCompositeScreenshot(browser, windowRect, elementRect);
300
+ // : await takeCompositeScreenshot(browser, windowRect, elementRect);
302
301
  screenshot = isFitIntoViewport
303
302
  ? await this.#browser.findElement(By.css(captureElement)).takeScreenshot()
304
- : // TODO pointer-events: none, need to research
305
- await this.takeCompositeScreenshot(windowRect, elementRect);
303
+ : await this.takeCompositeScreenshot(windowRect, elementRect);
306
304
 
307
305
  logger().debug(`${chalk.cyan(captureElement)} is captured`);
308
306
  }
@@ -503,7 +501,6 @@ export class InternalBrowser {
503
501
  }
504
502
 
505
503
  try {
506
- // TODO Pageload timeout 10s
507
504
  // NOTE: getUrlChecker already calls `browser.get` so we don't need another one
508
505
  await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
509
506
  } catch (error) {
@@ -541,7 +538,7 @@ export class InternalBrowser {
541
538
  }
542
539
 
543
540
  private async waitForStorybook(): Promise<void> {
544
- logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
541
+ logger().debug('Waiting for Storybook to initiate');
545
542
 
546
543
  const isTimeout = await Promise.race([
547
544
  new Promise<boolean>((resolve) => {
@@ -554,9 +551,6 @@ export class InternalBrowser {
554
551
  do {
555
552
  // TODO Research a different way to ensure storybook is initiated
556
553
  wait = await this.#browser.executeScript<boolean>(function (SET_GLOBALS: string): boolean {
557
- // TODO Maybe use
558
- // import { global } from '@storybook/global';
559
- // global.IS_STORYBOOK
560
554
  if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
561
555
  if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
562
556
  return false;
@@ -566,8 +560,7 @@ export class InternalBrowser {
566
560
  })(),
567
561
  ]);
568
562
 
569
- // TODO Change the message to describe a reason why it might happen
570
- if (isTimeout) throw new Error('Failed to wait `setStories` event');
563
+ if (isTimeout) throw new Error('Failed to wait Storybook init');
571
564
  }
572
565
 
573
566
  private async updateStorybookGlobals(): Promise<void> {
@@ -5,6 +5,7 @@ import { subscribeOn } from '../messages.js';
5
5
  import { CreeveyWebdriverBase } from '../webdriver.js';
6
6
  import type { InternalBrowser } from './internal.js';
7
7
  import { logger } from '../logger.js';
8
+ import { removeWorkerContainer } from '../worker/context.js';
8
9
 
9
10
  declare global {
10
11
  interface Window {
@@ -31,7 +32,9 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
31
32
  this.#options = options;
32
33
 
33
34
  subscribeOn('shutdown', () => {
34
- void this.#browser?.closeBrowser().finally(() => process.exit());
35
+ void this.#browser?.closeBrowser().finally(() => {
36
+ void removeWorkerContainer().finally(() => process.exit());
37
+ });
35
38
  this.#browser = null;
36
39
  });
37
40
  }
@@ -42,7 +45,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
42
45
 
43
46
  getSessionId(): Promise<string> {
44
47
  if (!this.#browser) {
45
- // TODO Describe the error
46
48
  throw new Error('Browser is not initialized');
47
49
  }
48
50
 
@@ -89,7 +91,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
89
91
 
90
92
  async loadStoriesFromBrowser(): Promise<StoriesRaw> {
91
93
  if (!this.#browser) {
92
- // TODO Describe the error
93
94
  throw new Error('Browser is not initialized');
94
95
  }
95
96
 
@@ -98,7 +99,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
98
99
 
99
100
  afterTest(test: ServerTest): Promise<void> {
100
101
  if (!this.#browser) {
101
- // TODO Describe the error
102
102
  throw new Error('Browser is not initialized');
103
103
  }
104
104
 
@@ -110,7 +110,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
110
110
  ignoreElements?: string | string[] | null,
111
111
  ): Promise<Buffer> {
112
112
  if (!this.#browser) {
113
- // TODO Describe the error
114
113
  throw new Error('Browser is not initialized');
115
114
  }
116
115
 
@@ -119,7 +118,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
119
118
 
120
119
  protected waitForComplete(callback: (isCompleted: boolean) => void): void {
121
120
  if (!this.#browser) {
122
- // TODO Describe the error
123
121
  throw new Error('Browser is not initialized');
124
122
  }
125
123
 
@@ -128,7 +126,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
128
126
 
129
127
  protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
130
128
  if (!this.#browser) {
131
- // TODO Describe the error
132
129
  throw new Error('Browser is not initialized');
133
130
  }
134
131
 
@@ -137,7 +134,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
137
134
 
138
135
  protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
139
136
  if (!this.#browser) {
140
- // TODO Describe the error
141
137
  throw new Error('Browser is not initialized');
142
138
  }
143
139
 
@@ -181,8 +181,8 @@ export async function sendScreenshotsCount(
181
181
  const testsMeta = { runId: uuid, tests };
182
182
 
183
183
  const fullPathname = buildPathname('tests', testsMeta);
184
- // NOTE: Keep request path shorter than 32k symbols
185
- const chunksCount = Math.ceil(fullPathname.length / 32_000);
184
+ // NOTE: Keep request path shorter than 24k symbols
185
+ const chunksCount = Math.ceil(fullPathname.length / 24_000);
186
186
  let chunks: string[] = [];
187
187
  if (chunksCount > 1) {
188
188
  const testsString = JSON.stringify(tests);
@@ -8,6 +8,7 @@ import { register as esmRegister } from 'tsx/esm/api';
8
8
  import { register as cjsRegister } from 'tsx/cjs/api';
9
9
  import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
10
10
  import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
11
+ import { LOCALHOST_REGEXP } from './webdriver.js';
11
12
  import assert from 'assert';
12
13
  import pidtree from 'pidtree';
13
14
 
@@ -99,7 +100,7 @@ export async function shutdownWorkers(): Promise<void> {
99
100
  new Promise<void>((resolve) => {
100
101
  const timeout = setTimeout(() => {
101
102
  if (worker.process.pid) void killTree(worker.process.pid);
102
- }, 10000);
103
+ }, 10_000);
103
104
  worker.on('exit', () => {
104
105
  clearTimeout(timeout);
105
106
  resolve();
@@ -134,10 +135,6 @@ export async function killTree(rootPid: number): Promise<void> {
134
135
  });
135
136
  }
136
137
 
137
- export function shutdown(): void {
138
- process.exit();
139
- }
140
-
141
138
  export function shutdownWithError(): void {
142
139
  process.exit(1);
143
140
  }
@@ -261,25 +258,36 @@ export async function loadThroughTSX<T>(
261
258
  return result;
262
259
  }
263
260
 
264
- export function waitOnUrl(url: string, timeout: number, delay: number) {
261
+ export function waitOnUrl(waitUrl: string, timeout: number, delay: number) {
262
+ const urls = [waitUrl];
263
+ if (!LOCALHOST_REGEXP.test(waitUrl)) {
264
+ const parsedUrl = new URL(waitUrl);
265
+ parsedUrl.host = 'localhost';
266
+ urls.push(parsedUrl.toString());
267
+ }
265
268
  const startTime = Date.now();
266
- return new Promise<void>((resolve, reject) => {
267
- const interval = setInterval(() => {
268
- http
269
- .get(url, (response) => {
270
- if (response.statusCode === 200) {
271
- clearInterval(interval);
272
- resolve();
273
- }
274
- })
275
- .on('error', () => {
276
- // Ignore HTTP errors
277
- });
278
-
279
- if (Date.now() - startTime > timeout) {
280
- clearInterval(interval);
281
- reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
282
- }
283
- }, delay);
284
- });
269
+ return Promise.race(
270
+ urls.map(
271
+ (url) =>
272
+ new Promise<void>((resolve, reject) => {
273
+ const interval = setInterval(() => {
274
+ http
275
+ .get(url, (response) => {
276
+ if (response.statusCode === 200) {
277
+ clearInterval(interval);
278
+ resolve();
279
+ }
280
+ })
281
+ .on('error', () => {
282
+ // Ignore HTTP errors
283
+ });
284
+
285
+ if (Date.now() - startTime > timeout) {
286
+ clearInterval(interval);
287
+ reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
288
+ }
289
+ }, delay);
290
+ }),
291
+ ),
292
+ );
285
293
  }
@@ -18,6 +18,19 @@ export const storybookRootID = 'storybook-root';
18
18
  export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
19
19
  const DOCKER_INTERNAL = 'host.docker.internal';
20
20
 
21
+ let browserClosePromise: Promise<void> | null = null;
22
+
23
+ export const openBrowser = () => {
24
+ let resolve: () => void;
25
+ browserClosePromise = new Promise((r) => (resolve = r));
26
+ return () => {
27
+ resolve();
28
+ browserClosePromise = null;
29
+ };
30
+ };
31
+
32
+ export const waitForBrowserClose = () => browserClosePromise;
33
+
21
34
  export async function resolveStorybookUrl(
22
35
  storybookUrl: string,
23
36
  checkUrl: (url: string) => Promise<boolean>,
@@ -0,0 +1,14 @@
1
+ import type { Container } from 'dockerode';
2
+
3
+ let workerContainer: Container | null = null;
4
+
5
+ export function setWorkerContainer(container: Container): void {
6
+ workerContainer = container;
7
+ }
8
+
9
+ export async function removeWorkerContainer(): Promise<void> {
10
+ if (workerContainer) {
11
+ await workerContainer.remove({ force: true });
12
+ workerContainer = null;
13
+ }
14
+ }
package/src/types.ts CHANGED
@@ -150,7 +150,6 @@ export interface BrowserConfigObject {
150
150
  screenshots?: boolean;
151
151
  snapshots?: boolean;
152
152
  sources?: boolean;
153
- path: string;
154
153
  };
155
154
  };
156
155
  }
@@ -369,7 +368,6 @@ export interface Options {
369
368
  storybookUrl?: string;
370
369
  storybookPort?: string;
371
370
  storybookAutorunCmd?: string;
372
- saveReport: boolean;
373
371
  failFast?: boolean;
374
372
  odiff?: boolean;
375
373
  noDocker?: boolean;