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.
- package/README.md +19 -41
- package/dist/client/addon/withCreevey.js +1 -0
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +1 -1
- package/dist/client/web/CreeveyApp.js +1 -0
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/server/docker.d.ts +1 -1
- package/dist/server/docker.js +21 -14
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +9 -10
- package/dist/server/index.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -2
- package/dist/server/playwright/docker-file.js +10 -4
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/docker.d.ts +2 -1
- package/dist/server/playwright/docker.js +10 -2
- package/dist/server/playwright/docker.js.map +1 -1
- package/dist/server/playwright/internal.d.ts +3 -4
- package/dist/server/playwright/internal.js +48 -37
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.js +4 -7
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/selenium/internal.js +5 -12
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/webdriver.js +4 -8
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +2 -2
- package/dist/server/utils.d.ts +1 -2
- package/dist/server/utils.js +11 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +2 -0
- package/dist/server/webdriver.js +13 -1
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/context.d.ts +3 -0
- package/dist/server/worker/context.js +15 -0
- package/dist/server/worker/context.js.map +1 -0
- package/dist/types.d.ts +0 -2
- package/dist/types.js.map +1 -1
- package/docs/cli.md +12 -0
- package/docs/config.md +178 -167
- package/docs/storybook.md +60 -0
- package/docs/tests.md +50 -45
- package/package.json +1 -1
- package/src/client/addon/withCreevey.ts +1 -0
- package/src/client/web/CreeveyApp.tsx +1 -0
- package/src/server/docker.ts +24 -13
- package/src/server/index.ts +11 -14
- package/src/server/playwright/docker-file.ts +12 -5
- package/src/server/playwright/docker.ts +16 -3
- package/src/server/playwright/index-source.mjs +16 -0
- package/src/server/playwright/internal.ts +78 -52
- package/src/server/playwright/webdriver.ts +4 -7
- package/src/server/selenium/internal.ts +5 -12
- package/src/server/selenium/webdriver.ts +4 -8
- package/src/server/telemetry.ts +2 -2
- package/src/server/utils.ts +33 -25
- package/src/server/webdriver.ts +13 -0
- package/src/server/worker/context.ts +14 -0
- package/src/types.ts +0 -2
@@ -1,8 +1,10 @@
|
|
1
|
-
import
|
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 {
|
17
|
-
|
18
|
-
|
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
|
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
|
-
#
|
68
|
-
constructor(
|
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(
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
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(() =>
|
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);
|
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
|
-
// :
|
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
|
-
:
|
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
|
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
|
-
|
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(() =>
|
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
|
|
package/src/server/telemetry.ts
CHANGED
@@ -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
|
185
|
-
const chunksCount = Math.ceil(fullPathname.length /
|
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);
|
package/src/server/utils.ts
CHANGED
@@ -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
|
-
},
|
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(
|
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
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
}
|
package/src/server/webdriver.ts
CHANGED
@@ -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;
|