creevey 0.10.0-beta.3 → 0.10.0-beta.30
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/Addon.js +17 -7
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/Tools.js +17 -7
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +1 -0
- package/dist/client/addon/withCreevey.js +10 -1
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
- 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/PageHeader/PageHeader.js +20 -8
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.js +43 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +8 -1
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +1 -3
- package/dist/client/shared/helpers.js +4 -19
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +41 -14
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +20 -7
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyLoader.js +2 -2
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
- 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 +79 -64
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-C5QCFtF-.js +595 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +17 -7
- package/dist/client/web/index.js.map +1 -1
- package/dist/client/web/themes.d.ts +2 -0
- package/dist/client/web/themes.js +22 -0
- package/dist/client/web/themes.js.map +1 -0
- package/dist/creevey.js +16 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +29 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/connection.d.ts +3 -0
- package/dist/server/connection.js +28 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/docker.js +38 -21
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +63 -11
- 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 +4 -3
- package/dist/server/master/pool.js +12 -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 +71 -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 +13 -11
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +2 -1
- package/dist/server/playwright/docker-file.js +7 -5
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/internal.d.ts +5 -4
- package/dist/server/playwright/internal.js +91 -71
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +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 +6 -4
- 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 +13 -9
- package/dist/server/reporter.js.map +1 -1
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -99
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +9 -6
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +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/testsFiles/parser.js +44 -2
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +20 -1
- package/dist/server/utils.js +82 -7
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +3 -4
- 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/match-image.d.ts +4 -4
- package/dist/server/worker/match-image.js +7 -4
- package/dist/server/worker/match-image.js.map +1 -1
- package/dist/server/worker/start.js +24 -14
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +38 -13
- package/dist/types.js.map +1 -1
- package/docs/config.md +3 -0
- package/package.json +66 -64
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +8 -1
- 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/PageHeader/PageHeader.tsx +4 -2
- package/src/client/shared/components/ResultsPage.tsx +31 -8
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +4 -24
- package/src/client/web/CreeveyApp.tsx +26 -8
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyLoader.tsx +1 -1
- 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/client/web/themes.ts +24 -0
- package/src/creevey.ts +16 -10
- package/src/server/config.ts +30 -8
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +42 -24
- package/src/server/index.ts +73 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +22 -56
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +4 -1
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +8 -5
- package/src/server/playwright/internal.ts +91 -78
- package/src/server/playwright/webdriver.ts +2 -2
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporter.ts +15 -9
- package/src/server/selenium/internal.ts +131 -107
- package/src/server/selenium/selenoid.ts +9 -7
- package/src/server/selenium/webdriver.ts +2 -2
- package/src/server/telemetry.ts +7 -3
- package/src/server/testsFiles/parser.ts +51 -1
- package/src/server/utils.ts +87 -8
- package/src/server/webdriver.ts +11 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +25 -16
- package/src/shared/index.ts +1 -1
- package/src/types.ts +40 -15
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
- package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -0,0 +1,77 @@
|
|
1
|
+
import cluster from 'cluster';
|
2
|
+
import { isWorkerMessage, Worker, WorkerMessage } from '../../types.js';
|
3
|
+
import { gracefullyKill, isShuttingDown } from '../utils.js';
|
4
|
+
|
5
|
+
const FORK_RETRIES = 5;
|
6
|
+
|
7
|
+
type MaybeWorker = Worker | { error: string };
|
8
|
+
|
9
|
+
export class WorkerQueue {
|
10
|
+
private isProcessing = false;
|
11
|
+
private queue: {
|
12
|
+
browser: string;
|
13
|
+
storybookUrl: string;
|
14
|
+
gridUrl?: string;
|
15
|
+
retry: number;
|
16
|
+
resolve: (mw: MaybeWorker) => void;
|
17
|
+
}[] = [];
|
18
|
+
|
19
|
+
// TODO Add concurrency
|
20
|
+
constructor(private useQueue: boolean) {}
|
21
|
+
|
22
|
+
async forkWorker(browser: string, storybookUrl: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
|
23
|
+
return new Promise<MaybeWorker>((resolve) => {
|
24
|
+
this.queue.push({ browser, storybookUrl, gridUrl, retry, resolve });
|
25
|
+
|
26
|
+
void this.process();
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
private async process() {
|
31
|
+
if (this.useQueue && this.isProcessing) return;
|
32
|
+
|
33
|
+
const { browser, storybookUrl, gridUrl, retry, resolve } = this.queue.pop() ?? {};
|
34
|
+
|
35
|
+
if (browser == undefined || storybookUrl == undefined || retry == undefined || resolve == undefined) return;
|
36
|
+
|
37
|
+
if (isShuttingDown.current) {
|
38
|
+
resolve({ error: 'Master process is shutting down' });
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
|
42
|
+
this.isProcessing = true;
|
43
|
+
|
44
|
+
cluster.setupPrimary({
|
45
|
+
args: [
|
46
|
+
'--browser',
|
47
|
+
browser,
|
48
|
+
...(gridUrl ? ['--gridUrl', gridUrl] : []),
|
49
|
+
...process.argv.slice(2),
|
50
|
+
'--storybookUrl',
|
51
|
+
storybookUrl,
|
52
|
+
],
|
53
|
+
});
|
54
|
+
const worker = cluster.fork();
|
55
|
+
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
56
|
+
const readyHandler = (message: unknown): void => {
|
57
|
+
if (!isWorkerMessage(message) || message.type == 'port') return;
|
58
|
+
worker.off('message', readyHandler);
|
59
|
+
resolve(message);
|
60
|
+
};
|
61
|
+
worker.on('message', readyHandler);
|
62
|
+
});
|
63
|
+
|
64
|
+
if (message.type == 'error') {
|
65
|
+
gracefullyKill(worker);
|
66
|
+
|
67
|
+
if (retry == FORK_RETRIES) resolve(message.payload);
|
68
|
+
else this.queue.push({ browser, storybookUrl, gridUrl, retry: retry + 1, resolve });
|
69
|
+
} else {
|
70
|
+
resolve(worker);
|
71
|
+
}
|
72
|
+
|
73
|
+
this.isProcessing = false;
|
74
|
+
|
75
|
+
setImmediate(() => void this.process());
|
76
|
+
}
|
77
|
+
}
|
@@ -13,12 +13,14 @@ import {
|
|
13
13
|
TestMeta,
|
14
14
|
} from '../../types.js';
|
15
15
|
import Pool from './pool.js';
|
16
|
+
import { WorkerQueue } from './queue.js';
|
16
17
|
|
17
18
|
export default class Runner extends EventEmitter {
|
18
19
|
private failFast: boolean;
|
19
20
|
private screenDir: string;
|
20
21
|
private reportDir: string;
|
21
22
|
private browsers: string[];
|
23
|
+
private scheduler: WorkerQueue;
|
22
24
|
private pools: Record<string, Pool> = {};
|
23
25
|
tests: Partial<Record<string, ServerTest>> = {};
|
24
26
|
public get isRunning(): boolean {
|
@@ -30,9 +32,10 @@ export default class Runner extends EventEmitter {
|
|
30
32
|
this.failFast = config.failFast;
|
31
33
|
this.screenDir = config.screenDir;
|
32
34
|
this.reportDir = config.reportDir;
|
35
|
+
this.scheduler = new WorkerQueue(config.useWorkerQueue);
|
33
36
|
this.browsers = Object.keys(config.browsers);
|
34
37
|
this.browsers
|
35
|
-
.map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
|
38
|
+
.map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
|
36
39
|
.map((pool) => pool.on('test', this.handlePoolMessage));
|
37
40
|
}
|
38
41
|
|
@@ -14,13 +14,14 @@ import { sendScreenshotsCount } from '../telemetry.js';
|
|
14
14
|
const importMetaUrl = pathToFileURL(__filename).href;
|
15
15
|
|
16
16
|
async function copyStatics(reportDir: string): Promise<void> {
|
17
|
-
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '
|
18
|
-
const
|
19
|
-
.filter((dirent) => dirent.isFile()
|
17
|
+
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../../dist/client/web');
|
18
|
+
const assets = (await readdir(path.join(clientDir, 'assets'), { withFileTypes: true }))
|
19
|
+
.filter((dirent) => dirent.isFile())
|
20
20
|
.map((dirent) => dirent.name);
|
21
|
-
await mkdir(reportDir, { recursive: true });
|
22
|
-
|
23
|
-
|
21
|
+
await mkdir(path.join(reportDir, 'assets'), { recursive: true });
|
22
|
+
await copyFile(path.join(clientDir, 'index.html'), path.join(reportDir, 'index.html'));
|
23
|
+
for (const asset of assets) {
|
24
|
+
await copyFile(path.join(clientDir, 'assets', asset), path.join(reportDir, 'assets', asset));
|
24
25
|
}
|
25
26
|
}
|
26
27
|
|
@@ -42,7 +43,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
|
42
43
|
.map((imagePath) => path.posix.relative(imagesDir, imagePath))
|
43
44
|
.filter((imagePath) => !images.has(imagePath));
|
44
45
|
if (unnecessaryImages.length > 0) {
|
45
|
-
logger.warn(
|
46
|
+
logger().warn(
|
47
|
+
'We found unnecessary screenshot images, those can be safely removed:\n',
|
48
|
+
unnecessaryImages.join('\n'),
|
49
|
+
);
|
46
50
|
}
|
47
51
|
}
|
48
52
|
|
@@ -81,10 +85,10 @@ export async function start(
|
|
81
85
|
|
82
86
|
if (options.ui) {
|
83
87
|
resolveApi(creeveyApi(runner));
|
84
|
-
logger.info(`Started on http://localhost:${options.port}`);
|
88
|
+
logger().info(`Started on http://localhost:${options.port}`);
|
85
89
|
} else {
|
86
90
|
if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
|
87
|
-
logger.warn("Don't have any tests to run");
|
91
|
+
logger().warn("Don't have any tests to run");
|
88
92
|
|
89
93
|
void shutdownWorkers().then(() => process.exit());
|
90
94
|
return;
|
@@ -101,10 +105,11 @@ export async function start(
|
|
101
105
|
void sendScreenshotsCount(config, options, runner.status)
|
102
106
|
.catch((reason: unknown) => {
|
103
107
|
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
104
|
-
logger.warn(`Can't send telemetry: ${error}`);
|
108
|
+
logger().warn(`Can't send telemetry: ${error}`);
|
105
109
|
})
|
106
110
|
.finally(() => {
|
107
|
-
|
111
|
+
// NOTE: Take some time to kill processes
|
112
|
+
void shutdownWorkers().then(() => setTimeout(() => process.exit(), 500));
|
108
113
|
});
|
109
114
|
});
|
110
115
|
// TODO grep
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import semver from 'semver';
|
2
2
|
import { exec } from 'shelljs';
|
3
|
+
import { LaunchOptions } from 'playwright-core';
|
4
|
+
import { resolvePlaywrightBrowserType } from '../utils';
|
3
5
|
|
4
6
|
// TODO Support custom docker images
|
5
|
-
export function playwrightDockerFile(browser: string, version: string): string {
|
7
|
+
export function playwrightDockerFile(browser: string, version: string, serverOptions?: LaunchOptions): string {
|
6
8
|
const sv = semver.coerce(version);
|
7
9
|
|
8
10
|
let npmRegistry;
|
@@ -13,19 +15,20 @@ export function playwrightDockerFile(browser: string, version: string): string {
|
|
13
15
|
}
|
14
16
|
|
15
17
|
return `
|
16
|
-
FROM
|
18
|
+
FROM node:lts
|
17
19
|
|
18
20
|
WORKDIR /creevey
|
19
21
|
|
20
22
|
RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
|
21
|
-
echo "import { ${browser} as browser } from 'playwright-core';" >> index.js && \\
|
22
|
-
echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\${
|
23
|
+
echo "import { ${resolvePlaywrightBrowserType(browser)} as browser } from 'playwright-core';" >> index.js && \\
|
24
|
+
echo "const ws = await browser.launchServer({ ...${JSON.stringify(serverOptions)}, port: 4444, wsPath: 'creevey' })" >> index.js && \\${
|
23
25
|
npmRegistry
|
24
26
|
? `
|
25
27
|
echo "registry=${npmRegistry}" > .npmrc && \\`
|
26
28
|
: ''
|
27
29
|
}
|
28
|
-
npm i playwright-core${sv ? `@${sv.format()}` : ''}
|
30
|
+
npm i playwright-core${sv ? `@${sv.format()}` : ''} && \\
|
31
|
+
npx -y playwright${sv ? `@${sv.format()}` : ''} install --with-deps ${browser}
|
29
32
|
|
30
33
|
EXPOSE 4444
|
31
34
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
|
2
|
-
import Logger from 'loglevel';
|
3
2
|
import chalk from 'chalk';
|
4
3
|
import { v4 } from 'uuid';
|
4
|
+
import Logger from 'loglevel';
|
5
5
|
import prefix from 'loglevel-plugin-prefix';
|
6
6
|
import {
|
7
7
|
BrowserConfigObject,
|
@@ -15,9 +15,15 @@ import {
|
|
15
15
|
} from '../../types';
|
16
16
|
import { subscribeOn } from '../messages';
|
17
17
|
import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
|
18
|
-
import { isShuttingDown, runSequence } from '../utils';
|
18
|
+
import { isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
|
19
19
|
import { colors, logger } from '../logger';
|
20
|
-
import { Args } from '@storybook/csf';
|
20
|
+
import type { Args } from '@storybook/csf';
|
21
|
+
|
22
|
+
const browsers = {
|
23
|
+
chromium,
|
24
|
+
firefox,
|
25
|
+
webkit,
|
26
|
+
};
|
21
27
|
|
22
28
|
async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser | null> {
|
23
29
|
let timeout: NodeJS.Timeout | null = null;
|
@@ -28,7 +34,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
|
|
28
34
|
(resolve) =>
|
29
35
|
(timeout = setTimeout(() => {
|
30
36
|
isTimeout = true;
|
31
|
-
logger.error(`Can't connect to ${type.name()} playwright browser`, error);
|
37
|
+
logger().error(`Can't connect to ${type.name()} playwright browser`, error);
|
32
38
|
resolve(null);
|
33
39
|
}, 10000)),
|
34
40
|
),
|
@@ -57,13 +63,13 @@ export class InternalBrowser {
|
|
57
63
|
#sessionId: string = v4();
|
58
64
|
#serverHost: string | null = null;
|
59
65
|
#serverPort: number;
|
60
|
-
#
|
66
|
+
#storybookGlobals?: StorybookGlobals;
|
61
67
|
#unsubscribe: () => void = noop;
|
62
|
-
constructor(browser: Browser, page: Page, port: number) {
|
68
|
+
constructor(browser: Browser, page: Page, port: number, storybookGlobals?: StorybookGlobals) {
|
63
69
|
this.#browser = browser;
|
64
70
|
this.#page = page;
|
65
71
|
this.#serverPort = port;
|
66
|
-
this.#
|
72
|
+
this.#storybookGlobals = storybookGlobals;
|
67
73
|
this.#unsubscribe = subscribeOn('shutdown', () => {
|
68
74
|
void this.closeBrowser();
|
69
75
|
});
|
@@ -99,7 +105,12 @@ export class InternalBrowser {
|
|
99
105
|
if (captureElement) {
|
100
106
|
const element = await this.#page.$(captureElement);
|
101
107
|
if (!element) throw new Error(`Element with selector ${captureElement} not found`);
|
102
|
-
|
108
|
+
|
109
|
+
return element.screenshot({
|
110
|
+
animations: 'disabled',
|
111
|
+
mask,
|
112
|
+
style: ':root { overflow: hidden !important; }',
|
113
|
+
});
|
103
114
|
}
|
104
115
|
return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
|
105
116
|
}
|
@@ -110,10 +121,11 @@ export class InternalBrowser {
|
|
110
121
|
|
111
122
|
async selectStory(id: string, waitForReady = false): Promise<boolean> {
|
112
123
|
// NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
|
124
|
+
await this.updateStorybookGlobals();
|
113
125
|
await this.updateBrowserGlobalVariables();
|
114
126
|
await this.resetMousePosition();
|
115
127
|
|
116
|
-
|
128
|
+
logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
|
117
129
|
|
118
130
|
const result = await this.#page.evaluate<
|
119
131
|
[error?: string | null, isCaptureCalled?: boolean] | null,
|
@@ -152,20 +164,12 @@ export class InternalBrowser {
|
|
152
164
|
);
|
153
165
|
}
|
154
166
|
|
155
|
-
async loadStoriesFromBrowser(
|
156
|
-
|
157
|
-
const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
|
167
|
+
async loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
168
|
+
const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
|
158
169
|
|
159
|
-
|
170
|
+
if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
|
160
171
|
|
161
|
-
|
162
|
-
} catch (error) {
|
163
|
-
// TODO Check how other solutions with playwright get stories from storybook
|
164
|
-
if (retry) throw error;
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
166
|
-
// NOTE: Try one more time because of dynamic nature of vite and storybook
|
167
|
-
return this.loadStoriesFromBrowser(true);
|
168
|
-
}
|
172
|
+
return stories;
|
169
173
|
}
|
170
174
|
|
171
175
|
static async getBrowser(
|
@@ -179,49 +183,50 @@ export class InternalBrowser {
|
|
179
183
|
storybookUrl: address = config.storybookUrl,
|
180
184
|
viewport,
|
181
185
|
_storybookGlobals,
|
182
|
-
|
186
|
+
seleniumCapabilities,
|
187
|
+
playwrightOptions,
|
183
188
|
} = browserConfig;
|
184
189
|
|
185
190
|
let browser: Browser | null = null;
|
186
191
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
case 'firefox':
|
193
|
-
browser = await tryConnect(firefox, gridUrl);
|
194
|
-
break;
|
195
|
-
case 'webkit':
|
196
|
-
browser = await tryConnect(webkit, gridUrl);
|
197
|
-
break;
|
198
|
-
|
199
|
-
default:
|
200
|
-
logger.error(
|
201
|
-
`Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
|
202
|
-
);
|
203
|
-
}
|
192
|
+
const parsedUrl = new URL(gridUrl);
|
193
|
+
if (parsedUrl.protocol === 'ws:') {
|
194
|
+
browser = await tryConnect(browsers[resolvePlaywrightBrowserType(browserConfig.browserName)], gridUrl);
|
195
|
+
} else if (parsedUrl.protocol === 'creevey:') {
|
196
|
+
browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch(playwrightOptions);
|
204
197
|
} else {
|
205
|
-
if (browserConfig.browserName
|
206
|
-
logger.error("Playwright's Selenium Grid feature supports only chrome browser");
|
198
|
+
if (browserConfig.browserName !== 'chrome') {
|
199
|
+
logger().error("Playwright's Selenium Grid feature supports only chrome browser");
|
207
200
|
return null;
|
208
201
|
}
|
209
202
|
|
210
203
|
process.env.SELENIUM_REMOTE_URL = gridUrl;
|
211
|
-
process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(
|
204
|
+
process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
|
212
205
|
|
213
|
-
browser = await chromium.launch();
|
206
|
+
browser = await chromium.launch(playwrightOptions);
|
214
207
|
}
|
215
208
|
|
216
209
|
if (!browser) {
|
217
210
|
return null;
|
218
211
|
}
|
219
212
|
|
213
|
+
// TODO Record video
|
220
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
|
+
// }
|
220
|
+
|
221
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
222
|
+
page.on('console', (msg) => {
|
223
|
+
logger().debug(`Console message: ${msg.text()}`);
|
224
|
+
});
|
225
|
+
}
|
221
226
|
|
222
227
|
// TODO Add debug output
|
223
228
|
|
224
|
-
const internalBrowser = new InternalBrowser(browser, page, options.port);
|
229
|
+
const internalBrowser = new InternalBrowser(browser, page, options.port, _storybookGlobals);
|
225
230
|
|
226
231
|
try {
|
227
232
|
if (isShuttingDown.current) return null;
|
@@ -229,8 +234,6 @@ export class InternalBrowser {
|
|
229
234
|
browserName,
|
230
235
|
viewport,
|
231
236
|
storybookUrl: address,
|
232
|
-
storybookGlobals: _storybookGlobals,
|
233
|
-
resolveStorybookUrl: config.resolveStorybookUrl,
|
234
237
|
});
|
235
238
|
|
236
239
|
return done ? internalBrowser : null;
|
@@ -241,7 +244,7 @@ export class InternalBrowser {
|
|
241
244
|
const error = new Error(`Can't load storybook root page: ${message}`);
|
242
245
|
if (originalError instanceof Error) error.stack = originalError.stack;
|
243
246
|
|
244
|
-
logger.error(error);
|
247
|
+
logger().error(error);
|
245
248
|
|
246
249
|
return null;
|
247
250
|
}
|
@@ -251,32 +254,28 @@ export class InternalBrowser {
|
|
251
254
|
browserName,
|
252
255
|
viewport,
|
253
256
|
storybookUrl,
|
254
|
-
storybookGlobals,
|
255
|
-
resolveStorybookUrl,
|
256
257
|
}: {
|
257
258
|
browserName: string;
|
258
259
|
viewport?: { width: number; height: number };
|
259
260
|
storybookUrl: string;
|
260
|
-
storybookGlobals?: StorybookGlobals;
|
261
|
-
resolveStorybookUrl?: () => Promise<string>;
|
262
261
|
}) {
|
263
262
|
const sessionId = this.#sessionId;
|
264
263
|
|
265
|
-
prefix.apply(
|
264
|
+
prefix.apply(logger(), {
|
266
265
|
format(level) {
|
267
266
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
268
|
-
return `[${browserName}:${chalk.gray(
|
267
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
|
269
268
|
},
|
270
269
|
});
|
271
270
|
|
272
|
-
this.#page.setDefaultNavigationTimeout(10000);
|
273
271
|
this.#page.setDefaultTimeout(60000);
|
274
272
|
|
275
273
|
return await runSequence(
|
276
274
|
[
|
277
|
-
() => this.openStorybookPage(storybookUrl
|
275
|
+
() => this.openStorybookPage(storybookUrl),
|
278
276
|
() => this.waitForStorybook(),
|
279
|
-
() => this.
|
277
|
+
() => this.triggerViteReload(),
|
278
|
+
() => this.updateStorybookGlobals(),
|
280
279
|
() => this.resolveCreeveyHost(),
|
281
280
|
() => this.updateBrowserGlobalVariables(),
|
282
281
|
() => this.resizeViewport(viewport),
|
@@ -285,46 +284,41 @@ export class InternalBrowser {
|
|
285
284
|
);
|
286
285
|
}
|
287
286
|
|
288
|
-
private async openStorybookPage(storybookUrl: string
|
287
|
+
private async openStorybookPage(storybookUrl: string): Promise<void> {
|
289
288
|
if (!LOCALHOST_REGEXP.test(storybookUrl)) {
|
290
289
|
await this.#page.goto(appendIframePath(storybookUrl));
|
291
290
|
return;
|
292
291
|
}
|
293
292
|
|
294
293
|
try {
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
const resolvedUrl = await resolver();
|
299
|
-
|
300
|
-
this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
|
301
|
-
|
302
|
-
await this.#page.goto(appendIframePath(resolvedUrl));
|
303
|
-
} else {
|
304
|
-
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
|
305
|
-
}
|
294
|
+
// TODO this.#page.setDefaultNavigationTimeout(10000);
|
295
|
+
const resolvedUrl = await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
|
296
|
+
await this.#page.goto(resolvedUrl);
|
306
297
|
} catch (error) {
|
307
|
-
|
298
|
+
logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
|
308
299
|
throw error;
|
309
300
|
}
|
310
301
|
}
|
311
302
|
|
312
303
|
private async checkUrl(url: string): Promise<boolean> {
|
304
|
+
const page = await this.#browser.newPage();
|
313
305
|
try {
|
314
|
-
|
315
|
-
const response = await
|
306
|
+
logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
|
307
|
+
const response = await page.goto(url, { waitUntil: 'commit' });
|
316
308
|
const source = await response?.text();
|
317
309
|
|
318
|
-
|
310
|
+
logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
|
319
311
|
return source?.includes(`id="${storybookRootID}"`) ?? false;
|
320
312
|
} catch {
|
321
313
|
return false;
|
314
|
+
} finally {
|
315
|
+
await page.close();
|
322
316
|
}
|
323
317
|
}
|
324
318
|
|
325
319
|
private async waitForStorybook(): Promise<void> {
|
326
320
|
// TODO Duplicated code with selenium
|
327
|
-
|
321
|
+
logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
|
328
322
|
|
329
323
|
const isTimeout = await Promise.race([
|
330
324
|
new Promise<boolean>((resolve) => {
|
@@ -337,14 +331,17 @@ export class InternalBrowser {
|
|
337
331
|
do {
|
338
332
|
try {
|
339
333
|
// TODO Research a different way to ensure storybook is initiated
|
334
|
+
// TODO Maybe use `__STORYBOOK_PREVIEW__.extract()`
|
340
335
|
wait = await this.#page.evaluate((SET_GLOBALS: string) => {
|
341
336
|
if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
|
342
337
|
if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
|
343
338
|
return false;
|
344
339
|
}, StorybookEvents.SET_GLOBALS);
|
345
340
|
} catch (e: unknown) {
|
346
|
-
|
341
|
+
logger().debug('An error has been caught during the script:', e);
|
342
|
+
if (this.#page.isClosed()) throw e;
|
347
343
|
}
|
344
|
+
if (wait) await new Promise((resolve) => setTimeout(resolve, 1000));
|
348
345
|
} while (wait);
|
349
346
|
return false;
|
350
347
|
})(),
|
@@ -354,13 +351,25 @@ export class InternalBrowser {
|
|
354
351
|
if (isTimeout) throw new Error('Failed to wait `setStories` event');
|
355
352
|
}
|
356
353
|
|
357
|
-
private async
|
358
|
-
|
354
|
+
private async triggerViteReload(): Promise<void> {
|
355
|
+
// NOTE: On the first load, Vite might try to optimize some dependencies and reload the page
|
356
|
+
// We need to trigger reload earlier to avoid unnecessary reloads further
|
357
|
+
try {
|
358
|
+
await this.#page.evaluate(async () => {
|
359
|
+
await window.__STORYBOOK_PREVIEW__.extract();
|
360
|
+
});
|
361
|
+
} catch {
|
362
|
+
await this.waitForStorybook();
|
363
|
+
}
|
364
|
+
}
|
365
|
+
|
366
|
+
private async updateStorybookGlobals(): Promise<void> {
|
367
|
+
if (!this.#storybookGlobals) return;
|
359
368
|
|
360
|
-
|
369
|
+
logger().debug('Applying storybook globals');
|
361
370
|
await this.#page.evaluate((globals: StorybookGlobals) => {
|
362
371
|
window.__CREEVEY_UPDATE_GLOBALS__(globals);
|
363
|
-
},
|
372
|
+
}, this.#storybookGlobals);
|
364
373
|
}
|
365
374
|
|
366
375
|
private async resolveCreeveyHost(): Promise<void> {
|
@@ -389,8 +398,10 @@ export class InternalBrowser {
|
|
389
398
|
}
|
390
399
|
|
391
400
|
private async updateBrowserGlobalVariables() {
|
401
|
+
logger().debug('Updating browser global variables');
|
392
402
|
await this.#page.evaluate(
|
393
403
|
([workerId, creeveyHost, creeveyPort]) => {
|
404
|
+
window.__CREEVEY_ENV__ = true;
|
394
405
|
window.__CREEVEY_WORKER_ID__ = workerId;
|
395
406
|
window.__CREEVEY_SERVER_HOST__ = creeveyHost ?? 'localhost';
|
396
407
|
window.__CREEVEY_SERVER_PORT__ = creeveyPort;
|
@@ -402,10 +413,12 @@ export class InternalBrowser {
|
|
402
413
|
private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
|
403
414
|
if (!viewport) return;
|
404
415
|
|
416
|
+
logger().debug('Resizing viewport to', viewport);
|
405
417
|
await this.#page.setViewportSize(viewport);
|
406
418
|
}
|
407
419
|
|
408
420
|
private async resetMousePosition(): Promise<void> {
|
421
|
+
logger().debug('Resetting mouse position to (0, 0)');
|
409
422
|
await this.#page.mouse.move(0, 0);
|
410
423
|
}
|
411
424
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/// <reference types="../../../types/playwright-context" />
|
2
|
-
import { Args } from '@storybook/csf';
|
2
|
+
import type { Args } from '@storybook/csf';
|
3
3
|
import { Config, Options, ServerTest, StoriesRaw, StoryInput } from '../../types';
|
4
4
|
import { logger } from '../logger';
|
5
5
|
import { subscribeOn } from '../messages';
|
@@ -53,7 +53,7 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
53
53
|
try {
|
54
54
|
return await import('./internal.js');
|
55
55
|
} catch (error) {
|
56
|
-
logger.error(error);
|
56
|
+
logger().error(error);
|
57
57
|
return null;
|
58
58
|
}
|
59
59
|
})();
|
@@ -5,6 +5,7 @@ import { isDefined } from '../../types.js';
|
|
5
5
|
import { logger } from '../logger.js';
|
6
6
|
import { deserializeRawStories } from '../../shared/index.js';
|
7
7
|
|
8
|
+
// TODO Don't have updates from stories
|
8
9
|
export const loadStories: StoriesProvider = async (_config, storiesListener, webdriver) => {
|
9
10
|
if (cluster.isPrimary) {
|
10
11
|
return new Promise<StoriesRaw>((resolve) => {
|
@@ -17,13 +18,13 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
|
|
17
18
|
if (message.type == 'set') {
|
18
19
|
const { stories, oldTests } = message.payload;
|
19
20
|
if (oldTests.length > 0)
|
20
|
-
logger.warn(
|
21
|
+
logger().warn(
|
21
22
|
`If you use browser stories provider of CSFv3 Storybook feature\n` +
|
22
23
|
`Creevey will not load tests defined in story parameters from following stories:\n` +
|
23
24
|
oldTests.join('\n'),
|
24
25
|
);
|
25
26
|
unsubscribe();
|
26
|
-
resolve(stories);
|
27
|
+
resolve(deserializeRawStories(stories));
|
27
28
|
}
|
28
29
|
});
|
29
30
|
sendStoriesMessage(worker, { type: 'get' });
|
@@ -36,10 +37,11 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
|
|
36
37
|
} else {
|
37
38
|
subscribeOn('stories', (message) => {
|
38
39
|
if (message.type == 'get')
|
39
|
-
emitStoriesMessage({ type: 'set', payload: { stories, oldTests: storiesWithOldTests } });
|
40
|
+
emitStoriesMessage({ type: 'set', payload: { stories: rawStories, oldTests: storiesWithOldTests } });
|
40
41
|
if (message.type == 'update') storiesListener(new Map(message.payload));
|
41
42
|
});
|
42
|
-
const
|
43
|
+
const rawStories = (await webdriver?.loadStoriesFromBrowser()) ?? {};
|
44
|
+
const stories = deserializeRawStories(rawStories);
|
43
45
|
|
44
46
|
const storiesWithOldTests: string[] = [];
|
45
47
|
|
@@ -54,7 +54,7 @@ async function parseParams(
|
|
54
54
|
|
55
55
|
if (listener) {
|
56
56
|
chokidar.watch(testFiles).on('change', (filePath) => {
|
57
|
-
logger.debug(`changed: ${filePath}`);
|
57
|
+
logger().debug(`changed: ${filePath}`);
|
58
58
|
|
59
59
|
// doesn't work, always returns {} due modules caching
|
60
60
|
// see https://github.com/nodejs/modules/issues/307
|