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,18 +1,9 @@
|
|
1
|
-
import
|
1
|
+
import { Worker as ClusterWorker } from 'cluster';
|
2
2
|
import { EventEmitter } from 'events';
|
3
|
-
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
BrowserConfigObject,
|
8
|
-
WorkerMessage,
|
9
|
-
TestStatus,
|
10
|
-
isWorkerMessage,
|
11
|
-
} from '../../types.js';
|
12
|
-
import { sendTestMessage, sendShutdownMessage, subscribeOnWorker } from '../messages.js';
|
13
|
-
import { isShuttingDown } from '../utils.js';
|
14
|
-
|
15
|
-
const FORK_RETRIES = 5;
|
3
|
+
import { Worker, Config, TestResult, BrowserConfigObject, TestStatus } from '../../types.js';
|
4
|
+
import { sendTestMessage, subscribeOnWorker } from '../messages.js';
|
5
|
+
import { gracefullyKill, isShuttingDown } from '../utils.js';
|
6
|
+
import { WorkerQueue } from './queue.js';
|
16
7
|
|
17
8
|
interface WorkerTest {
|
18
9
|
id: string;
|
@@ -32,6 +23,7 @@ export default class Pool extends EventEmitter {
|
|
32
23
|
return this.workers.length !== this.freeWorkers.length;
|
33
24
|
}
|
34
25
|
constructor(
|
26
|
+
public scheduler: WorkerQueue,
|
35
27
|
config: Config,
|
36
28
|
private browser: string,
|
37
29
|
gridUrl?: string,
|
@@ -46,10 +38,11 @@ export default class Pool extends EventEmitter {
|
|
46
38
|
|
47
39
|
async init(): Promise<void> {
|
48
40
|
const poolSize = Math.max(1, this.config.limit ?? 1);
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
41
|
+
this.workers = (
|
42
|
+
await Promise.all(
|
43
|
+
Array.from({ length: poolSize }).map(() => this.scheduler.forkWorker(this.browser, this.gridUrl)),
|
44
|
+
)
|
45
|
+
).filter((workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker);
|
53
46
|
if (this.workers.length != poolSize)
|
54
47
|
throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
|
55
48
|
this.workers.forEach((worker) => {
|
@@ -66,7 +59,7 @@ export default class Pool extends EventEmitter {
|
|
66
59
|
return true;
|
67
60
|
}
|
68
61
|
|
69
|
-
stop()
|
62
|
+
stop() {
|
70
63
|
if (!this.isRunning) {
|
71
64
|
this.emit('stop');
|
72
65
|
return;
|
@@ -76,7 +69,7 @@ export default class Pool extends EventEmitter {
|
|
76
69
|
this.queue = [];
|
77
70
|
}
|
78
71
|
|
79
|
-
process()
|
72
|
+
process() {
|
80
73
|
const worker = this.getFreeWorker();
|
81
74
|
const test = this.queue.at(0);
|
82
75
|
|
@@ -99,7 +92,9 @@ export default class Pool extends EventEmitter {
|
|
99
92
|
|
100
93
|
sendTestMessage(worker, { type: 'start', payload: test });
|
101
94
|
|
102
|
-
|
95
|
+
setImmediate(() => {
|
96
|
+
this.process();
|
97
|
+
});
|
103
98
|
}
|
104
99
|
|
105
100
|
private sendStatus(message: { id: string; status: TestStatus; result?: TestResult }): void {
|
@@ -120,34 +115,12 @@ export default class Pool extends EventEmitter {
|
|
120
115
|
return this.aliveWorkers.filter((worker) => !worker.isRunning);
|
121
116
|
}
|
122
117
|
|
123
|
-
private async forkWorker(retry = 0): Promise<Worker | { error: string }> {
|
124
|
-
cluster.setupPrimary({
|
125
|
-
args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
|
126
|
-
});
|
127
|
-
const worker = cluster.fork();
|
128
|
-
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
129
|
-
const readyHandler = (message: unknown): void => {
|
130
|
-
if (!isWorkerMessage(message) || message.type == 'port') return;
|
131
|
-
worker.off('message', readyHandler);
|
132
|
-
resolve(message);
|
133
|
-
};
|
134
|
-
worker.on('message', readyHandler);
|
135
|
-
});
|
136
|
-
|
137
|
-
if (message.type != 'error') return worker;
|
138
|
-
|
139
|
-
this.gracefullyKill(worker);
|
140
|
-
|
141
|
-
if (retry == FORK_RETRIES) return message.payload;
|
142
|
-
return this.forkWorker(retry + 1);
|
143
|
-
}
|
144
|
-
|
145
118
|
private exitHandler(worker: Worker): void {
|
146
119
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
147
120
|
worker.once('exit', async () => {
|
148
121
|
if (isShuttingDown.current) return;
|
149
122
|
|
150
|
-
const workerOrError = await this.forkWorker();
|
123
|
+
const workerOrError = await this.scheduler.forkWorker(this.browser, this.gridUrl);
|
151
124
|
|
152
125
|
if (!(workerOrError instanceof ClusterWorker))
|
153
126
|
throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
|
@@ -158,17 +131,6 @@ export default class Pool extends EventEmitter {
|
|
158
131
|
});
|
159
132
|
}
|
160
133
|
|
161
|
-
private gracefullyKill(worker: Worker): void {
|
162
|
-
worker.isShuttingDown = true;
|
163
|
-
const timeout = setTimeout(() => {
|
164
|
-
worker.kill();
|
165
|
-
}, 10000);
|
166
|
-
worker.on('exit', () => {
|
167
|
-
clearTimeout(timeout);
|
168
|
-
});
|
169
|
-
sendShutdownMessage(worker);
|
170
|
-
}
|
171
|
-
|
172
134
|
private shouldRetry(test: WorkerTest): boolean {
|
173
135
|
return test.retries < this.maxRetries && !this.forcedStop;
|
174
136
|
}
|
@@ -200,7 +162,7 @@ export default class Pool extends EventEmitter {
|
|
200
162
|
});
|
201
163
|
|
202
164
|
if (message.payload.subtype == 'unknown') {
|
203
|
-
|
165
|
+
gracefullyKill(worker);
|
204
166
|
}
|
205
167
|
|
206
168
|
this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
|
@@ -0,0 +1,64 @@
|
|
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: { browser: string; gridUrl?: string; retry: number; resolve: (mw: MaybeWorker) => void }[] = [];
|
12
|
+
|
13
|
+
// TODO Add concurrency
|
14
|
+
constructor(private useQueue: boolean) {}
|
15
|
+
|
16
|
+
async forkWorker(browser: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
|
17
|
+
return new Promise<MaybeWorker>((resolve) => {
|
18
|
+
this.queue.push({ browser, gridUrl, retry, resolve });
|
19
|
+
|
20
|
+
void this.process();
|
21
|
+
});
|
22
|
+
}
|
23
|
+
|
24
|
+
private async process() {
|
25
|
+
if (this.useQueue && this.isProcessing) return;
|
26
|
+
|
27
|
+
const { browser, gridUrl, retry, resolve } = this.queue.pop() ?? {};
|
28
|
+
|
29
|
+
if (browser == undefined || retry == undefined || resolve == undefined) return;
|
30
|
+
|
31
|
+
if (isShuttingDown.current) {
|
32
|
+
resolve({ error: 'Master process is shutting down' });
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
this.isProcessing = true;
|
37
|
+
|
38
|
+
cluster.setupPrimary({
|
39
|
+
args: ['--browser', browser, ...(gridUrl ? ['--gridUrl', gridUrl] : []), ...process.argv.slice(2)],
|
40
|
+
});
|
41
|
+
const worker = cluster.fork();
|
42
|
+
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
43
|
+
const readyHandler = (message: unknown): void => {
|
44
|
+
if (!isWorkerMessage(message) || message.type == 'port') return;
|
45
|
+
worker.off('message', readyHandler);
|
46
|
+
resolve(message);
|
47
|
+
};
|
48
|
+
worker.on('message', readyHandler);
|
49
|
+
});
|
50
|
+
|
51
|
+
if (message.type == 'error') {
|
52
|
+
gracefullyKill(worker);
|
53
|
+
|
54
|
+
if (retry == FORK_RETRIES) resolve(message.payload);
|
55
|
+
else this.queue.push({ browser, gridUrl, retry: retry + 1, resolve });
|
56
|
+
} else {
|
57
|
+
resolve(worker);
|
58
|
+
}
|
59
|
+
|
60
|
+
this.isProcessing = false;
|
61
|
+
|
62
|
+
setImmediate(() => void this.process());
|
63
|
+
}
|
64
|
+
}
|
@@ -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
|
|
@@ -42,7 +42,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
|
42
42
|
.map((imagePath) => path.posix.relative(imagesDir, imagePath))
|
43
43
|
.filter((imagePath) => !images.has(imagePath));
|
44
44
|
if (unnecessaryImages.length > 0) {
|
45
|
-
logger.warn(
|
45
|
+
logger().warn(
|
46
|
+
'We found unnecessary screenshot images, those can be safely removed:\n',
|
47
|
+
unnecessaryImages.join('\n'),
|
48
|
+
);
|
46
49
|
}
|
47
50
|
}
|
48
51
|
|
@@ -81,10 +84,10 @@ export async function start(
|
|
81
84
|
|
82
85
|
if (options.ui) {
|
83
86
|
resolveApi(creeveyApi(runner));
|
84
|
-
logger.info(`Started on http://localhost:${options.port}`);
|
87
|
+
logger().info(`Started on http://localhost:${options.port}`);
|
85
88
|
} else {
|
86
89
|
if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
|
87
|
-
logger.warn("Don't have any tests to run");
|
90
|
+
logger().warn("Don't have any tests to run");
|
88
91
|
|
89
92
|
void shutdownWorkers().then(() => process.exit());
|
90
93
|
return;
|
@@ -101,7 +104,7 @@ export async function start(
|
|
101
104
|
void sendScreenshotsCount(config, options, runner.status)
|
102
105
|
.catch((reason: unknown) => {
|
103
106
|
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
104
|
-
logger.warn(`Can't send telemetry: ${error}`);
|
107
|
+
logger().warn(`Can't send telemetry: ${error}`);
|
105
108
|
})
|
106
109
|
.finally(() => {
|
107
110
|
void shutdownWorkers().then(() => process.exit());
|
@@ -1,10 +1,17 @@
|
|
1
1
|
import semver from 'semver';
|
2
|
+
import { exec } from 'shelljs';
|
2
3
|
|
3
4
|
// TODO Support custom docker images
|
4
|
-
// TODO Support nexus
|
5
5
|
export function playwrightDockerFile(browser: string, version: string): string {
|
6
6
|
const sv = semver.coerce(version);
|
7
7
|
|
8
|
+
let npmRegistry;
|
9
|
+
try {
|
10
|
+
npmRegistry = exec('npm config get registry', { silent: true }).stdout.trim();
|
11
|
+
} catch {
|
12
|
+
/* noop */
|
13
|
+
}
|
14
|
+
|
8
15
|
return `
|
9
16
|
FROM mcr.microsoft.com/playwright:v${sv?.format() ?? version}
|
10
17
|
|
@@ -12,7 +19,12 @@ WORKDIR /creevey
|
|
12
19
|
|
13
20
|
RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
|
14
21
|
echo "import { ${browser} as browser } from 'playwright-core';" >> index.js && \\
|
15
|
-
echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js &&
|
22
|
+
echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\${
|
23
|
+
npmRegistry
|
24
|
+
? `
|
25
|
+
echo "registry=${npmRegistry}" > .npmrc && \\`
|
26
|
+
: ''
|
27
|
+
}
|
16
28
|
npm i playwright-core${sv ? `@${sv.format()}` : ''}
|
17
29
|
|
18
30
|
EXPOSE 4444
|
@@ -1,10 +1,17 @@
|
|
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';
|
5
4
|
import prefix from 'loglevel-plugin-prefix';
|
6
|
-
import {
|
7
|
-
|
5
|
+
import {
|
6
|
+
BrowserConfigObject,
|
7
|
+
Config,
|
8
|
+
Options,
|
9
|
+
StoriesRaw,
|
10
|
+
StoryInput,
|
11
|
+
StorybookEvents,
|
12
|
+
StorybookGlobals,
|
13
|
+
noop,
|
14
|
+
} from '../../types';
|
8
15
|
import { subscribeOn } from '../messages';
|
9
16
|
import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
|
10
17
|
import { isShuttingDown, runSequence } from '../utils';
|
@@ -20,7 +27,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
|
|
20
27
|
(resolve) =>
|
21
28
|
(timeout = setTimeout(() => {
|
22
29
|
isTimeout = true;
|
23
|
-
logger.error(`Can't connect to ${type.name()} playwright browser`, error);
|
30
|
+
logger().error(`Can't connect to ${type.name()} playwright browser`, error);
|
24
31
|
resolve(null);
|
25
32
|
}, 10000)),
|
26
33
|
),
|
@@ -49,13 +56,13 @@ export class InternalBrowser {
|
|
49
56
|
#sessionId: string = v4();
|
50
57
|
#serverHost: string | null = null;
|
51
58
|
#serverPort: number;
|
52
|
-
#
|
59
|
+
#storybookGlobals?: StorybookGlobals;
|
53
60
|
#unsubscribe: () => void = noop;
|
54
|
-
constructor(browser: Browser, page: Page, port: number) {
|
61
|
+
constructor(browser: Browser, page: Page, port: number, storybookGlobals?: StorybookGlobals) {
|
55
62
|
this.#browser = browser;
|
56
63
|
this.#page = page;
|
57
64
|
this.#serverPort = port;
|
58
|
-
this.#
|
65
|
+
this.#storybookGlobals = storybookGlobals;
|
59
66
|
this.#unsubscribe = subscribeOn('shutdown', () => {
|
60
67
|
void this.closeBrowser();
|
61
68
|
});
|
@@ -91,7 +98,12 @@ export class InternalBrowser {
|
|
91
98
|
if (captureElement) {
|
92
99
|
const element = await this.#page.$(captureElement);
|
93
100
|
if (!element) throw new Error(`Element with selector ${captureElement} not found`);
|
94
|
-
|
101
|
+
|
102
|
+
return element.screenshot({
|
103
|
+
animations: 'disabled',
|
104
|
+
mask,
|
105
|
+
style: ':root { overflow: hidden !important; }',
|
106
|
+
});
|
95
107
|
}
|
96
108
|
return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
|
97
109
|
}
|
@@ -102,10 +114,11 @@ export class InternalBrowser {
|
|
102
114
|
|
103
115
|
async selectStory(id: string, waitForReady = false): Promise<boolean> {
|
104
116
|
// NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
|
117
|
+
await this.updateStorybookGlobals();
|
105
118
|
await this.updateBrowserGlobalVariables();
|
106
119
|
await this.resetMousePosition();
|
107
120
|
|
108
|
-
|
121
|
+
logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
|
109
122
|
|
110
123
|
const result = await this.#page.evaluate<
|
111
124
|
[error?: string | null, isCaptureCalled?: boolean] | null,
|
@@ -140,7 +153,7 @@ export class InternalBrowser {
|
|
140
153
|
});
|
141
154
|
});
|
142
155
|
},
|
143
|
-
[story.id, updatedArgs, UPDATE_STORY_ARGS, STORY_RENDERED] as const,
|
156
|
+
[story.id, updatedArgs, StorybookEvents.UPDATE_STORY_ARGS, StorybookEvents.STORY_RENDERED] as const,
|
144
157
|
);
|
145
158
|
}
|
146
159
|
|
@@ -167,26 +180,43 @@ export class InternalBrowser {
|
|
167
180
|
options: Options,
|
168
181
|
): Promise<InternalBrowser | null> {
|
169
182
|
const browserConfig = config.browsers[browserName] as BrowserConfigObject;
|
170
|
-
const {
|
183
|
+
const {
|
184
|
+
storybookUrl: address = config.storybookUrl,
|
185
|
+
viewport,
|
186
|
+
_storybookGlobals,
|
187
|
+
seleniumCapabilities,
|
188
|
+
playwrightOptions,
|
189
|
+
} = browserConfig;
|
171
190
|
|
172
191
|
let browser: Browser | null = null;
|
173
192
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
193
|
+
if (new URL(gridUrl).protocol === 'ws:') {
|
194
|
+
switch (browserConfig.browserName) {
|
195
|
+
case 'chromium':
|
196
|
+
browser = await tryConnect(chromium, gridUrl);
|
197
|
+
break;
|
198
|
+
case 'firefox':
|
199
|
+
browser = await tryConnect(firefox, gridUrl);
|
200
|
+
break;
|
201
|
+
case 'webkit':
|
202
|
+
browser = await tryConnect(webkit, gridUrl);
|
203
|
+
break;
|
204
|
+
|
205
|
+
default:
|
206
|
+
logger().error(
|
207
|
+
`Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
|
208
|
+
);
|
209
|
+
}
|
210
|
+
} else {
|
211
|
+
if (browserConfig.browserName != 'chrome') {
|
212
|
+
logger().error("Playwright's Selenium Grid feature supports only chrome browser");
|
213
|
+
return null;
|
214
|
+
}
|
215
|
+
|
216
|
+
process.env.SELENIUM_REMOTE_URL = gridUrl;
|
217
|
+
process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
|
218
|
+
|
219
|
+
browser = await chromium.launch(playwrightOptions);
|
190
220
|
}
|
191
221
|
|
192
222
|
if (!browser) {
|
@@ -197,7 +227,7 @@ export class InternalBrowser {
|
|
197
227
|
|
198
228
|
// TODO Add debug output
|
199
229
|
|
200
|
-
const internalBrowser = new InternalBrowser(browser, page, options.port);
|
230
|
+
const internalBrowser = new InternalBrowser(browser, page, options.port, _storybookGlobals);
|
201
231
|
|
202
232
|
try {
|
203
233
|
if (isShuttingDown.current) return null;
|
@@ -205,7 +235,6 @@ export class InternalBrowser {
|
|
205
235
|
browserName,
|
206
236
|
viewport,
|
207
237
|
storybookUrl: address,
|
208
|
-
storybookGlobals: _storybookGlobals,
|
209
238
|
resolveStorybookUrl: config.resolveStorybookUrl,
|
210
239
|
});
|
211
240
|
|
@@ -217,7 +246,7 @@ export class InternalBrowser {
|
|
217
246
|
const error = new Error(`Can't load storybook root page: ${message}`);
|
218
247
|
if (originalError instanceof Error) error.stack = originalError.stack;
|
219
248
|
|
220
|
-
logger.error(error);
|
249
|
+
logger().error(error);
|
221
250
|
|
222
251
|
return null;
|
223
252
|
}
|
@@ -227,32 +256,29 @@ export class InternalBrowser {
|
|
227
256
|
browserName,
|
228
257
|
viewport,
|
229
258
|
storybookUrl,
|
230
|
-
storybookGlobals,
|
231
259
|
resolveStorybookUrl,
|
232
260
|
}: {
|
233
261
|
browserName: string;
|
234
262
|
viewport?: { width: number; height: number };
|
235
263
|
storybookUrl: string;
|
236
|
-
storybookGlobals?: StorybookGlobals;
|
237
264
|
resolveStorybookUrl?: () => Promise<string>;
|
238
265
|
}) {
|
239
266
|
const sessionId = this.#sessionId;
|
240
267
|
|
241
|
-
prefix.apply(
|
268
|
+
prefix.apply(logger(), {
|
242
269
|
format(level) {
|
243
270
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
244
|
-
return `[${browserName}:${chalk.gray(
|
271
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
|
245
272
|
},
|
246
273
|
});
|
247
274
|
|
248
|
-
this.#page.setDefaultNavigationTimeout(10000);
|
249
275
|
this.#page.setDefaultTimeout(60000);
|
250
276
|
|
251
277
|
return await runSequence(
|
252
278
|
[
|
253
279
|
() => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
|
254
280
|
() => this.waitForStorybook(),
|
255
|
-
() => this.updateStorybookGlobals(
|
281
|
+
() => this.updateStorybookGlobals(),
|
256
282
|
() => this.resolveCreeveyHost(),
|
257
283
|
() => this.updateBrowserGlobalVariables(),
|
258
284
|
() => this.resizeViewport(viewport),
|
@@ -269,29 +295,30 @@ export class InternalBrowser {
|
|
269
295
|
|
270
296
|
try {
|
271
297
|
if (resolver) {
|
272
|
-
|
298
|
+
logger().debug('Resolving storybook url with custom resolver');
|
273
299
|
|
274
300
|
const resolvedUrl = await resolver();
|
275
301
|
|
276
|
-
|
302
|
+
logger().debug(`Resolver storybook url ${resolvedUrl}`);
|
277
303
|
|
278
304
|
await this.#page.goto(appendIframePath(resolvedUrl));
|
279
305
|
} else {
|
280
|
-
|
306
|
+
// TODO this.#page.setDefaultNavigationTimeout(10000);
|
307
|
+
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
|
281
308
|
}
|
282
309
|
} catch (error) {
|
283
|
-
|
310
|
+
logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
|
284
311
|
throw error;
|
285
312
|
}
|
286
313
|
}
|
287
314
|
|
288
315
|
private async checkUrl(url: string): Promise<boolean> {
|
289
316
|
try {
|
290
|
-
|
317
|
+
logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
|
291
318
|
const response = await this.#page.goto(url, { waitUntil: 'commit' });
|
292
319
|
const source = await response?.text();
|
293
320
|
|
294
|
-
|
321
|
+
logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
|
295
322
|
return source?.includes(`id="${storybookRootID}"`) ?? false;
|
296
323
|
} catch {
|
297
324
|
return false;
|
@@ -300,7 +327,7 @@ export class InternalBrowser {
|
|
300
327
|
|
301
328
|
private async waitForStorybook(): Promise<void> {
|
302
329
|
// TODO Duplicated code with selenium
|
303
|
-
|
330
|
+
logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
|
304
331
|
|
305
332
|
const isTimeout = await Promise.race([
|
306
333
|
new Promise<boolean>((resolve) => {
|
@@ -317,9 +344,9 @@ export class InternalBrowser {
|
|
317
344
|
if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
|
318
345
|
if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
|
319
346
|
return false;
|
320
|
-
}, SET_GLOBALS);
|
347
|
+
}, StorybookEvents.SET_GLOBALS);
|
321
348
|
} catch (e: unknown) {
|
322
|
-
|
349
|
+
logger().debug('An error has been caught during the script:', e);
|
323
350
|
}
|
324
351
|
} while (wait);
|
325
352
|
return false;
|
@@ -330,13 +357,13 @@ export class InternalBrowser {
|
|
330
357
|
if (isTimeout) throw new Error('Failed to wait `setStories` event');
|
331
358
|
}
|
332
359
|
|
333
|
-
private async updateStorybookGlobals(
|
334
|
-
if (!
|
360
|
+
private async updateStorybookGlobals(): Promise<void> {
|
361
|
+
if (!this.#storybookGlobals) return;
|
335
362
|
|
336
|
-
|
363
|
+
logger().debug('Applying storybook globals');
|
337
364
|
await this.#page.evaluate((globals: StorybookGlobals) => {
|
338
365
|
window.__CREEVEY_UPDATE_GLOBALS__(globals);
|
339
|
-
},
|
366
|
+
}, this.#storybookGlobals);
|
340
367
|
}
|
341
368
|
|
342
369
|
private async resolveCreeveyHost(): Promise<void> {
|
@@ -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,7 +18,7 @@ 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'),
|
@@ -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
|
package/src/server/reporter.ts
CHANGED
@@ -22,11 +22,11 @@ export class CreeveyReporter {
|
|
22
22
|
// TODO Output in better way, like vitest, maybe
|
23
23
|
constructor(runner: EventEmitter, options: { reporterOptions: { creevey: ReporterOptions } }) {
|
24
24
|
const { sessionId, browserName } = options.reporterOptions.creevey;
|
25
|
-
const testLogger = Logger.getLogger(
|
25
|
+
const testLogger = Logger.getLogger(sessionId);
|
26
26
|
|
27
27
|
prefix.apply(testLogger, {
|
28
28
|
format(level) {
|
29
|
-
return
|
29
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
|
30
30
|
},
|
31
31
|
});
|
32
32
|
|
@@ -34,11 +34,12 @@ export class CreeveyReporter {
|
|
34
34
|
testLogger.warn(chalk.cyan(test.titlePath().join('/')));
|
35
35
|
});
|
36
36
|
runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
|
37
|
-
testLogger.info(chalk.cyan(test.titlePath().join('/')));
|
37
|
+
testLogger.info(chalk.cyan(test.titlePath().join('/')), chalk.gray(`(${test.duration} ms)`));
|
38
38
|
});
|
39
39
|
runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error) => {
|
40
40
|
testLogger.error(
|
41
41
|
chalk.cyan(test.titlePath().join('/')),
|
42
|
+
chalk.gray(`(${test.duration} ms)`),
|
42
43
|
'\n ',
|
43
44
|
this.getErrors(
|
44
45
|
error,
|
@@ -79,11 +80,16 @@ export class TeamcityReporter {
|
|
79
80
|
console.log(`##teamcity[testStarted name='${this.escape(test.title)}' flowId='${process.pid}']`);
|
80
81
|
});
|
81
82
|
|
83
|
+
runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
|
84
|
+
console.log(`##teamcity[testFinished name='${this.escape(test.title)}' flowId='${process.pid}']`);
|
85
|
+
});
|
86
|
+
|
82
87
|
runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error: Error) => {
|
83
88
|
Object.entries(reporterOptions.images).forEach(([name, image]) => {
|
84
89
|
if (!image) return;
|
85
90
|
const filePath = test
|
86
91
|
.titlePath()
|
92
|
+
.slice(0, -1)
|
87
93
|
.concat(name == browserName ? [] : [browserName])
|
88
94
|
.map(this.escape)
|
89
95
|
.join('/');
|