creevey 0.10.0-beta.1 → 0.10.0-beta.11
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/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/controller.js +4 -5
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/withCreevey.js +18 -34
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js +12 -0
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.js +23 -5
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +22 -6
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +3 -0
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +2 -2
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- 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/CreeveyView/SideBar/SideBarFooter.js +49 -6
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +1 -3
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +1 -3
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
- package/dist/client/web/KeyboardEventsContext.js +62 -57
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/{index-DkmZfG9C.js → index-BE9CL5_G.js} +94 -94
- package/dist/client/web/index.html +1 -1
- package/dist/creevey.js +13 -5
- package/dist/creevey.js.map +1 -1
- package/dist/server/config.js +4 -3
- 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 +29 -3
- 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 -63
- 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/docker-file.js +12 -2
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/internal.d.ts +2 -2
- package/dist/server/playwright/internal.js +56 -44
- 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 +8 -4
- package/dist/server/reporter.js.map +1 -1
- package/dist/server/selenium/internal.d.ts +2 -3
- package/dist/server/selenium/internal.js +116 -90
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +2 -2
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.js +1 -1
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +7 -3
- package/dist/server/telemetry.js.map +1 -1
- package/dist/server/utils.d.ts +2 -1
- package/dist/server/utils.js +13 -3
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +2 -3
- package/dist/server/webdriver.js +10 -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 +24 -14
- package/dist/server/worker/start.js.map +1 -1
- package/dist/types.d.ts +30 -11
- package/dist/types.js +13 -1
- package/dist/types.js.map +1 -1
- package/package.json +36 -42
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/controller.ts +13 -6
- package/src/client/addon/withCreevey.ts +25 -13
- package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
- package/src/client/shared/components/ResultsPage.tsx +28 -7
- package/src/client/web/CreeveyApp.tsx +25 -7
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
- package/src/client/web/KeyboardEventsContext.tsx +61 -73
- package/src/creevey.ts +13 -6
- package/src/server/config.ts +4 -3
- package/src/server/docker.ts +2 -2
- package/src/server/index.ts +27 -4
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +18 -56
- 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/docker-file.ts +14 -2
- package/src/server/playwright/internal.ts +76 -49
- 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 +9 -3
- package/src/server/selenium/internal.ts +119 -92
- package/src/server/selenium/selenoid.ts +2 -2
- package/src/server/selenium/webdriver.ts +1 -1
- package/src/server/telemetry.ts +7 -3
- package/src/server/utils.ts +14 -4
- package/src/server/webdriver.ts +10 -15
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/start.ts +25 -16
- package/src/types.ts +32 -13
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
@@ -1,5 +1,4 @@
|
|
1
1
|
import { Args } from '@storybook/csf';
|
2
|
-
import { SET_GLOBALS, UPDATE_STORY_ARGS, STORY_RENDERED } from '@storybook/core-events';
|
3
2
|
import chalk from 'chalk';
|
4
3
|
import http from 'http';
|
5
4
|
import https from 'https';
|
@@ -22,6 +21,7 @@ import {
|
|
22
21
|
StoriesRaw,
|
23
22
|
Options,
|
24
23
|
ServerTest,
|
24
|
+
StorybookEvents,
|
25
25
|
} from '../../types.js';
|
26
26
|
import { colors, logger } from '../logger.js';
|
27
27
|
import { subscribeOn } from '../messages.js';
|
@@ -94,21 +94,25 @@ async function openUrlAndWaitForPageSource(
|
|
94
94
|
}
|
95
95
|
|
96
96
|
async function buildWebdriver(
|
97
|
-
|
97
|
+
browser: string,
|
98
98
|
gridUrl: string,
|
99
99
|
config: Config,
|
100
100
|
options: Options,
|
101
101
|
): Promise<WebDriver | null> {
|
102
|
-
const browserConfig = config.browsers[
|
103
|
-
const {
|
102
|
+
const browserConfig = config.browsers[browser] as BrowserConfigObject;
|
103
|
+
const { /*customizeBuilder,*/ seleniumCapabilities, browserName } = browserConfig;
|
104
104
|
|
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
|
-
const capabilities = new Capabilities({
|
111
|
+
const capabilities = new Capabilities({
|
112
|
+
browserName,
|
113
|
+
...seleniumCapabilities,
|
114
|
+
pageLoadStrategy: PageLoadStrategy.EAGER,
|
115
|
+
});
|
112
116
|
const prefs = new logging.Preferences();
|
113
117
|
|
114
118
|
if (options.trace) {
|
@@ -121,37 +125,65 @@ async function buildWebdriver(
|
|
121
125
|
// TODO Validate browsers, versions, and platform
|
122
126
|
// TODO Use `customizeBuilder`
|
123
127
|
|
124
|
-
let
|
128
|
+
let webdriver: WebDriver | null;
|
125
129
|
|
126
130
|
try {
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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');
|
149
181
|
} catch (error) {
|
150
|
-
logger.error(`
|
182
|
+
logger().error(`Failed to start browser:`, error);
|
151
183
|
return null;
|
152
184
|
}
|
153
185
|
|
154
|
-
return
|
186
|
+
return webdriver;
|
155
187
|
}
|
156
188
|
|
157
189
|
export class InternalBrowser {
|
@@ -159,13 +191,13 @@ export class InternalBrowser {
|
|
159
191
|
#browser: WebDriver;
|
160
192
|
#serverHost: string | null = null;
|
161
193
|
#serverPort: number;
|
162
|
-
#
|
194
|
+
#storybookGlobals?: StorybookGlobals;
|
163
195
|
#unsubscribe: () => void = noop;
|
164
196
|
#keepAliveInterval: NodeJS.Timeout | null = null;
|
165
|
-
constructor(browser: WebDriver, port: number,
|
197
|
+
constructor(browser: WebDriver, port: number, storybookGlobals?: StorybookGlobals) {
|
166
198
|
this.#browser = browser;
|
167
199
|
this.#serverPort = port;
|
168
|
-
this.#
|
200
|
+
this.#storybookGlobals = storybookGlobals;
|
169
201
|
this.#unsubscribe = subscribeOn('shutdown', () => {
|
170
202
|
void this.closeBrowser();
|
171
203
|
});
|
@@ -194,7 +226,7 @@ export class InternalBrowser {
|
|
194
226
|
|
195
227
|
const ignoreStyles = await this.insertIgnoreStyles(ignoreElements);
|
196
228
|
|
197
|
-
if (
|
229
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
198
230
|
const { innerWidth, innerHeight } = await this.#browser.executeScript<{
|
199
231
|
innerWidth: number;
|
200
232
|
innerHeight: number;
|
@@ -204,16 +236,16 @@ export class InternalBrowser {
|
|
204
236
|
innerHeight: window.innerHeight,
|
205
237
|
};
|
206
238
|
});
|
207
|
-
|
239
|
+
logger().debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
|
208
240
|
}
|
209
241
|
|
210
242
|
try {
|
211
243
|
if (!captureElement) {
|
212
|
-
|
244
|
+
logger().debug('Capturing viewport screenshot');
|
213
245
|
screenshot = await this.#browser.takeScreenshot();
|
214
|
-
|
246
|
+
logger().debug('Viewport screenshot is captured');
|
215
247
|
} else {
|
216
|
-
|
248
|
+
logger().debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
|
217
249
|
const rects = await this.#browser.executeScript<
|
218
250
|
{ elementRect: ElementRect; windowRect: ElementRect } | undefined
|
219
251
|
>(function (selector: string): { elementRect: ElementRect; windowRect: ElementRect } | undefined {
|
@@ -252,11 +284,11 @@ export class InternalBrowser {
|
|
252
284
|
elementRect.height + elementRect.top <= windowRect.height;
|
253
285
|
|
254
286
|
if (isFitIntoViewport) {
|
255
|
-
|
287
|
+
logger().debug(
|
256
288
|
`Capturing ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
|
257
289
|
);
|
258
290
|
} else
|
259
|
-
|
291
|
+
logger().debug(
|
260
292
|
`Capturing composite screenshot image of ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
|
261
293
|
);
|
262
294
|
|
@@ -272,7 +304,7 @@ export class InternalBrowser {
|
|
272
304
|
: // TODO pointer-events: none, need to research
|
273
305
|
await this.takeCompositeScreenshot(windowRect, elementRect);
|
274
306
|
|
275
|
-
|
307
|
+
logger().debug(`${chalk.cyan(captureElement)} is captured`);
|
276
308
|
}
|
277
309
|
} finally {
|
278
310
|
await this.removeIgnoreStyles(ignoreStyles);
|
@@ -291,10 +323,11 @@ export class InternalBrowser {
|
|
291
323
|
|
292
324
|
async selectStory(id: string, waitForReady = false): Promise<boolean> {
|
293
325
|
// NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
|
326
|
+
await this.updateStorybookGlobals();
|
294
327
|
await this.updateBrowserGlobalVariables();
|
295
328
|
await this.resetMousePosition();
|
296
329
|
|
297
|
-
|
330
|
+
logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
|
298
331
|
|
299
332
|
const result = await this.#browser.executeAsyncScript<[error?: string | null, isCaptureCalled?: boolean] | null>(
|
300
333
|
function (
|
@@ -338,8 +371,8 @@ export class InternalBrowser {
|
|
338
371
|
},
|
339
372
|
story.id,
|
340
373
|
updatedArgs,
|
341
|
-
UPDATE_STORY_ARGS,
|
342
|
-
STORY_RENDERED,
|
374
|
+
StorybookEvents.UPDATE_STORY_ARGS,
|
375
|
+
StorybookEvents.STORY_RENDERED,
|
343
376
|
);
|
344
377
|
}
|
345
378
|
|
@@ -356,14 +389,14 @@ export class InternalBrowser {
|
|
356
389
|
}
|
357
390
|
|
358
391
|
async afterTest(test: ServerTest): Promise<void> {
|
359
|
-
if (
|
392
|
+
if (logger().getLevel() === Logger.levels.TRACE) {
|
360
393
|
const output: string[] = [];
|
361
394
|
const types = await this.#browser.manage().logs().getAvailableLogTypes();
|
362
395
|
for (const type of types) {
|
363
396
|
const logs = await this.#browser.manage().logs().get(type);
|
364
397
|
output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n'));
|
365
398
|
}
|
366
|
-
|
399
|
+
logger().debug(
|
367
400
|
'----------',
|
368
401
|
getTestPath(test).join('/'),
|
369
402
|
'----------\n',
|
@@ -387,7 +420,7 @@ export class InternalBrowser {
|
|
387
420
|
|
388
421
|
if (!browser) return null;
|
389
422
|
|
390
|
-
const internalBrowser = new InternalBrowser(browser, options.port,
|
423
|
+
const internalBrowser = new InternalBrowser(browser, options.port, _storybookGlobals);
|
391
424
|
|
392
425
|
try {
|
393
426
|
if (isShuttingDown.current) return null;
|
@@ -397,7 +430,6 @@ export class InternalBrowser {
|
|
397
430
|
gridUrl,
|
398
431
|
viewport,
|
399
432
|
storybookUrl: address,
|
400
|
-
storybookGlobals: _storybookGlobals,
|
401
433
|
resolveStorybookUrl: config.resolveStorybookUrl,
|
402
434
|
});
|
403
435
|
|
@@ -405,11 +437,12 @@ export class InternalBrowser {
|
|
405
437
|
} catch (originalError) {
|
406
438
|
void internalBrowser.closeBrowser();
|
407
439
|
|
408
|
-
const message =
|
409
|
-
|
440
|
+
const message =
|
441
|
+
originalError instanceof Error ? originalError.message : ((originalError ?? 'Unknown error') as string);
|
442
|
+
const error = new Error(`Can't load storybook root page: ${message}`);
|
410
443
|
if (originalError instanceof Error) error.stack = originalError.stack;
|
411
444
|
|
412
|
-
logger.error(error);
|
445
|
+
logger().error(error);
|
413
446
|
|
414
447
|
return null;
|
415
448
|
}
|
@@ -420,14 +453,12 @@ export class InternalBrowser {
|
|
420
453
|
gridUrl,
|
421
454
|
viewport,
|
422
455
|
storybookUrl,
|
423
|
-
storybookGlobals,
|
424
456
|
resolveStorybookUrl,
|
425
457
|
}: {
|
426
458
|
browserName: string;
|
427
459
|
gridUrl: string;
|
428
460
|
viewport?: { width: number; height: number };
|
429
461
|
storybookUrl: string;
|
430
|
-
storybookGlobals?: StorybookGlobals;
|
431
462
|
resolveStorybookUrl?: () => Promise<string>;
|
432
463
|
}): Promise<boolean> {
|
433
464
|
const sessionId = (await this.#browser.getSession()).getId();
|
@@ -440,23 +471,21 @@ export class InternalBrowser {
|
|
440
471
|
/* noop */
|
441
472
|
}
|
442
473
|
|
443
|
-
|
444
|
-
|
445
|
-
prefix.apply(this.#logger, {
|
474
|
+
prefix.apply(logger(), {
|
446
475
|
format(level) {
|
447
476
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
448
|
-
return `[${browserName}:${chalk.gray(
|
477
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
|
449
478
|
},
|
450
479
|
});
|
451
480
|
|
452
|
-
|
481
|
+
logger().debug(`Connected successful with ${chalk.green(browserHost)}`);
|
453
482
|
|
454
483
|
return await runSequence(
|
455
484
|
[
|
456
|
-
() => this.#browser.manage().setTimeouts({ pageLoad:
|
485
|
+
() => this.#browser.manage().setTimeouts({ pageLoad: 60000, script: 60000 }),
|
457
486
|
() => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
|
458
487
|
() => this.waitForStorybook(),
|
459
|
-
() => this.updateStorybookGlobals(
|
488
|
+
() => this.updateStorybookGlobals(),
|
460
489
|
() => this.resolveCreeveyHost(),
|
461
490
|
() => this.updateBrowserGlobalVariables(),
|
462
491
|
// NOTE: Selenium draws automation toolbar with some delay after webdriver initialization
|
@@ -478,19 +507,20 @@ export class InternalBrowser {
|
|
478
507
|
|
479
508
|
try {
|
480
509
|
if (resolver) {
|
481
|
-
|
510
|
+
logger().debug('Resolving storybook url with custom resolver');
|
482
511
|
|
483
512
|
const resolvedUrl = await resolver();
|
484
513
|
|
485
|
-
|
514
|
+
logger().debug(`Resolver storybook url ${resolvedUrl}`);
|
486
515
|
|
487
516
|
await this.#browser.get(appendIframePath(resolvedUrl));
|
488
517
|
} else {
|
518
|
+
// TODO Pageload timeout 10s
|
489
519
|
// NOTE: getUrlChecker already calls `browser.get` so we don't need another one
|
490
|
-
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url)
|
520
|
+
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
|
491
521
|
}
|
492
522
|
} catch (error) {
|
493
|
-
|
523
|
+
logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
|
494
524
|
throw error;
|
495
525
|
}
|
496
526
|
}
|
@@ -498,13 +528,13 @@ export class InternalBrowser {
|
|
498
528
|
private async checkUrl(url: string): Promise<boolean> {
|
499
529
|
try {
|
500
530
|
// NOTE: Before trying a new url, reset the current one
|
501
|
-
|
531
|
+
logger().debug(`Opening ${chalk.magenta('about:blank')} page`);
|
502
532
|
await openUrlAndWaitForPageSource(
|
503
533
|
this.#browser,
|
504
534
|
'about:blank',
|
505
535
|
(source: string) => !source.includes('<body></body>'),
|
506
536
|
);
|
507
|
-
|
537
|
+
logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
|
508
538
|
const source = await openUrlAndWaitForPageSource(
|
509
539
|
this.#browser,
|
510
540
|
url,
|
@@ -516,7 +546,7 @@ export class InternalBrowser {
|
|
516
546
|
// because other add significant delay and some of them don't work in earlier chrome versions
|
517
547
|
// Browsers always load page successful even it's failed
|
518
548
|
// So we just check `root` element
|
519
|
-
|
549
|
+
logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
|
520
550
|
return source.includes(`id="${storybookRootID}"`);
|
521
551
|
} catch {
|
522
552
|
return false;
|
@@ -524,7 +554,7 @@ export class InternalBrowser {
|
|
524
554
|
}
|
525
555
|
|
526
556
|
private async waitForStorybook(): Promise<void> {
|
527
|
-
|
557
|
+
logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
|
528
558
|
|
529
559
|
const isTimeout = await Promise.race([
|
530
560
|
new Promise<boolean>((resolve) => {
|
@@ -535,19 +565,15 @@ export class InternalBrowser {
|
|
535
565
|
(async () => {
|
536
566
|
let wait = true;
|
537
567
|
do {
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
}, SET_GLOBALS);
|
548
|
-
} catch (e: unknown) {
|
549
|
-
this.#logger.debug('An error has been caught during the script:', e);
|
550
|
-
}
|
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);
|
551
577
|
} while (wait);
|
552
578
|
return false;
|
553
579
|
})(),
|
@@ -557,13 +583,13 @@ export class InternalBrowser {
|
|
557
583
|
if (isTimeout) throw new Error('Failed to wait `setStories` event');
|
558
584
|
}
|
559
585
|
|
560
|
-
private async updateStorybookGlobals(
|
561
|
-
if (!
|
586
|
+
private async updateStorybookGlobals(): Promise<void> {
|
587
|
+
if (!this.#storybookGlobals) return;
|
562
588
|
|
563
|
-
|
589
|
+
logger().debug('Applying storybook globals');
|
564
590
|
await this.#browser.executeScript(function (globals: StorybookGlobals) {
|
565
591
|
window.__CREEVEY_UPDATE_GLOBALS__(globals);
|
566
|
-
},
|
592
|
+
}, this.#storybookGlobals);
|
567
593
|
}
|
568
594
|
|
569
595
|
private async resolveCreeveyHost(): Promise<void> {
|
@@ -630,7 +656,7 @@ export class InternalBrowser {
|
|
630
656
|
},
|
631
657
|
);
|
632
658
|
|
633
|
-
|
659
|
+
logger().debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
|
634
660
|
|
635
661
|
const dWidth = windowRect.width - innerWidth;
|
636
662
|
const dHeight = windowRect.height - innerHeight;
|
@@ -644,7 +670,7 @@ export class InternalBrowser {
|
|
644
670
|
}
|
645
671
|
|
646
672
|
private async resetMousePosition(): Promise<void> {
|
647
|
-
|
673
|
+
logger().debug('Resetting mouse position to the top-left corner');
|
648
674
|
const browserName = (await this.#browser.getCapabilities()).getBrowserName();
|
649
675
|
const [browserVersion] =
|
650
676
|
(await this.#browser.getCapabilities()).getBrowserVersion()?.split('.') ??
|
@@ -672,8 +698,9 @@ export class InternalBrowser {
|
|
672
698
|
y: Math.ceil((-1 * height) / 2) - top,
|
673
699
|
})
|
674
700
|
.perform();
|
675
|
-
} else if (browserName == 'firefox'
|
701
|
+
} else if (browserName == 'firefox') {
|
676
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
|
677
704
|
await this.#browser.actions().move({ origin: Origin.VIEWPORT, x: 0, y: 1 }).perform();
|
678
705
|
} else {
|
679
706
|
// NOTE IE don't emit move events until force window focus or connect by RDP on virtual machine
|
@@ -685,7 +712,7 @@ export class InternalBrowser {
|
|
685
712
|
const ignoreSelectors = Array.prototype.concat(ignoreElements).filter(Boolean);
|
686
713
|
if (!ignoreSelectors.length) return null;
|
687
714
|
|
688
|
-
|
715
|
+
logger().debug('Hiding ignored elements before capturing');
|
689
716
|
|
690
717
|
return await this.#browser.executeScript(function (ignoreSelectors: string[]) {
|
691
718
|
return window.__CREEVEY_INSERT_IGNORE_STYLES__(ignoreSelectors);
|
@@ -766,7 +793,7 @@ export class InternalBrowser {
|
|
766
793
|
|
767
794
|
private async removeIgnoreStyles(ignoreStyles: WebElement | null): Promise<void> {
|
768
795
|
if (ignoreStyles) {
|
769
|
-
|
796
|
+
logger().debug('Revert hiding ignored elements');
|
770
797
|
await this.#browser.executeScript(function (ignoreStyles: HTMLStyleElement) {
|
771
798
|
window.__CREEVEY_REMOVE_IGNORE_STYLES__(ignoreStyles);
|
772
799
|
}, ignoreStyles);
|
@@ -805,7 +832,7 @@ export class InternalBrowser {
|
|
805
832
|
this.#keepAliveInterval = setInterval(() => {
|
806
833
|
// NOTE Simple way to keep session alive
|
807
834
|
void this.#browser.getCurrentUrl().then((url) => {
|
808
|
-
logger.debug('current url', chalk.magenta(url));
|
835
|
+
logger().debug('current url', chalk.magenta(url));
|
809
836
|
});
|
810
837
|
}, 10 * 1000);
|
811
838
|
}
|
@@ -30,7 +30,7 @@ async function createSelenoidConfig(
|
|
30
30
|
browsers.forEach(
|
31
31
|
({
|
32
32
|
browserName,
|
33
|
-
browserVersion = 'latest',
|
33
|
+
seleniumCapabilities: { browserVersion = 'latest' } = {},
|
34
34
|
dockerImage = `selenoid/${browserName}:${browserVersion}`,
|
35
35
|
webdriverCommand = [],
|
36
36
|
}) => {
|
@@ -117,7 +117,7 @@ export async function startSelenoidContainer(config: Config, debug: boolean): Pr
|
|
117
117
|
browsers.forEach(
|
118
118
|
({
|
119
119
|
browserName,
|
120
|
-
browserVersion = 'latest',
|
120
|
+
seleniumCapabilities: { browserVersion = 'latest' } = {},
|
121
121
|
limit: browserLimit = 1,
|
122
122
|
dockerImage = `selenoid/${browserName}:${browserVersion}`,
|
123
123
|
}) => {
|
package/src/server/telemetry.ts
CHANGED
@@ -154,12 +154,16 @@ export async function sendScreenshotsCount(
|
|
154
154
|
name: name,
|
155
155
|
gridUrl: browser.gridUrl ? sanitizeGridUrl(browser.gridUrl) : undefined,
|
156
156
|
browserName: browser.browserName,
|
157
|
-
|
158
|
-
|
157
|
+
// @ts-expect-error Support old config version
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
159
|
+
browserVersion: browser.seleniumCapabilities?.browserVersion ?? browser.browserVersion,
|
160
|
+
// @ts-expect-error Support old config version
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
162
|
+
platformName: browser.seleniumCapabilities?.platformName ?? browser.platformName,
|
159
163
|
viewport: browser.viewport,
|
160
164
|
limit: browser.limit,
|
161
165
|
dockerImage: browser.dockerImage,
|
162
|
-
'se:teamname': browser['se:teamname'],
|
166
|
+
'se:teamname': browser.seleniumCapabilities?.['se:teamname'],
|
163
167
|
}
|
164
168
|
: browser,
|
165
169
|
]),
|
package/src/server/utils.ts
CHANGED
@@ -3,10 +3,9 @@ import { get } from 'https';
|
|
3
3
|
import cluster from 'cluster';
|
4
4
|
import { dirname } from 'path';
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'url';
|
6
|
-
import { createRequire } from 'module';
|
7
6
|
import { register as esmRegister } from 'tsx/esm/api';
|
8
7
|
import { register as cjsRegister } from 'tsx/cjs/api';
|
9
|
-
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
|
8
|
+
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
|
10
9
|
import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
|
11
10
|
|
12
11
|
const importMetaUrl = pathToFileURL(__filename).href;
|
@@ -96,6 +95,17 @@ export async function shutdownWorkers(): Promise<void> {
|
|
96
95
|
emitShutdownMessage();
|
97
96
|
}
|
98
97
|
|
98
|
+
export function gracefullyKill(worker: Worker): void {
|
99
|
+
worker.isShuttingDown = true;
|
100
|
+
const timeout = setTimeout(() => {
|
101
|
+
worker.kill();
|
102
|
+
}, 10000);
|
103
|
+
worker.on('exit', () => {
|
104
|
+
clearTimeout(timeout);
|
105
|
+
});
|
106
|
+
sendShutdownMessage(worker);
|
107
|
+
}
|
108
|
+
|
99
109
|
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
110
|
const { default: findCacheDir } = await import('find-cache-dir');
|
101
111
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
@@ -175,10 +185,10 @@ export function readDirRecursive(dirPath: string): string[] {
|
|
175
185
|
);
|
176
186
|
}
|
177
187
|
|
178
|
-
const _require = createRequire(importMetaUrl);
|
179
188
|
export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
|
180
189
|
try {
|
181
|
-
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
191
|
+
return require(filename) as Partial<Record<string, ServerTest>>;
|
182
192
|
} catch {
|
183
193
|
/* noop */
|
184
194
|
}
|
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,15 +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();
|
27
|
+
// TODO Use Promise.race?
|
29
28
|
for (const ip of addresses) {
|
30
29
|
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
31
|
-
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
30
|
+
logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
32
31
|
if (await checkUrl(resolvedUrl)) {
|
33
|
-
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
32
|
+
logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
34
33
|
return resolvedUrl;
|
35
34
|
}
|
36
35
|
}
|
@@ -74,11 +73,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
74
73
|
|
75
74
|
abstract afterTest(test: ServerTest): Promise<void>;
|
76
75
|
|
77
|
-
async switchStory(
|
78
|
-
story: StoryInput,
|
79
|
-
context: BaseCreeveyTestContext,
|
80
|
-
logger: Logger.Logger,
|
81
|
-
): Promise<CreeveyTestContext> {
|
76
|
+
async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
|
82
77
|
const { id, title, name, parameters } = story;
|
83
78
|
const {
|
84
79
|
captureElement = `#${storybookRootID}`,
|
@@ -86,7 +81,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
86
81
|
ignoreElements,
|
87
82
|
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
88
83
|
|
89
|
-
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)}`);
|
90
85
|
|
91
86
|
let storyPlayResolver: (isCompleted: boolean) => void;
|
92
87
|
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
@@ -107,7 +102,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
107
102
|
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
108
103
|
|
109
104
|
if (isCaptureCalled) {
|
110
|
-
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`);
|
111
106
|
while (!(await waitForComplete)) {
|
112
107
|
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
113
108
|
}
|
@@ -115,8 +110,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
115
110
|
|
116
111
|
unsubscribe();
|
117
112
|
|
118
|
-
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
119
|
-
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`);
|
120
115
|
|
121
116
|
return Object.assign(
|
122
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;
|