creevey 0.10.0-beta.43 → 0.10.0-beta.44
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/CHANGELOG.md +282 -0
- package/dist/client/addon/controller.js +1 -1
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/withCreevey.js +1 -18
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +10 -0
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/web/CreeveyApp.d.ts +1 -0
- package/dist/client/web/CreeveyApp.js +1 -0
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +1 -0
- package/dist/client/web/CreeveyContext.js +1 -0
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
- package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +8 -3
- package/dist/client/web/index.js.map +1 -1
- package/dist/creevey.d.ts +1 -1
- package/dist/creevey.js +1 -22
- package/dist/creevey.js.map +1 -1
- package/dist/playwright-reporter.d.ts +2 -0
- package/dist/playwright-reporter.js +5 -0
- package/dist/playwright-reporter.js.map +1 -0
- package/dist/playwright.d.ts +1 -1
- package/dist/server/config.js +8 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.js +10 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +15 -5
- package/dist/server/master/api.js +89 -27
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +2 -0
- package/dist/server/master/handlers/capture-handler.js +35 -0
- package/dist/server/master/handlers/capture-handler.js.map +1 -0
- package/dist/server/master/handlers/index.d.ts +4 -0
- package/dist/server/master/handlers/index.js +21 -0
- package/dist/server/master/handlers/index.js.map +1 -0
- package/dist/server/master/handlers/ping-handler.d.ts +2 -0
- package/dist/server/master/handlers/ping-handler.js +7 -0
- package/dist/server/master/handlers/ping-handler.js.map +1 -0
- package/dist/server/master/handlers/static-handler.d.ts +2 -0
- package/dist/server/master/handlers/static-handler.js +32 -0
- package/dist/server/master/handlers/static-handler.js.map +1 -0
- package/dist/server/master/handlers/stories-handler.d.ts +2 -0
- package/dist/server/master/handlers/stories-handler.js +38 -0
- package/dist/server/master/handlers/stories-handler.js.map +1 -0
- package/dist/server/master/master.js +7 -24
- package/dist/server/master/master.js.map +1 -1
- package/dist/server/master/runner.d.ts +4 -6
- package/dist/server/master/runner.js +30 -127
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +77 -87
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.d.ts +1 -2
- package/dist/server/master/start.js +11 -29
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/master/testsManager.d.ts +81 -0
- package/dist/server/master/testsManager.js +281 -0
- package/dist/server/master/testsManager.js.map +1 -0
- package/dist/server/playwright/reporter.d.ts +87 -0
- package/dist/server/playwright/reporter.js +351 -0
- package/dist/server/playwright/reporter.js.map +1 -0
- package/dist/server/selenium/internal.js +20 -2
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +4 -0
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/shutdown.d.ts +1 -0
- package/dist/server/shutdown.js +23 -0
- package/dist/server/shutdown.js.map +1 -0
- package/dist/server/stories.d.ts +0 -1
- package/dist/server/stories.js +0 -12
- package/dist/server/stories.js.map +1 -1
- package/dist/server/ui-update.d.ts +10 -0
- package/dist/server/ui-update.js +39 -0
- package/dist/server/ui-update.js.map +1 -0
- package/dist/server/utils.d.ts +6 -0
- package/dist/server/utils.js +39 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/worker/start.js +1 -1
- package/dist/server/worker/start.js.map +1 -1
- package/dist/types.d.ts +14 -8
- package/dist/types.js.map +1 -1
- package/docs/examples/playwright-reporter-example.ts +202 -0
- package/docs/migration-0.9-to-0.10.md +144 -0
- package/docs/playwright-reporter.md +357 -0
- package/package.json +9 -13
- package/src/client/addon/controller.ts +1 -1
- package/src/client/addon/withCreevey.ts +2 -16
- package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
- package/src/client/shared/creeveyClientApi.ts +10 -0
- package/src/client/web/CreeveyApp.tsx +2 -0
- package/src/client/web/CreeveyContext.tsx +2 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
- package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
- package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
- package/src/client/web/index.tsx +8 -3
- package/src/creevey.ts +1 -24
- package/src/playwright-reporter.ts +3 -0
- package/src/server/config.ts +9 -1
- package/src/server/index.ts +11 -4
- package/src/server/master/api.ts +95 -26
- package/src/server/master/handlers/capture-handler.ts +39 -0
- package/src/server/master/handlers/index.ts +4 -0
- package/src/server/master/handlers/ping-handler.ts +5 -0
- package/src/server/master/handlers/static-handler.ts +29 -0
- package/src/server/master/handlers/stories-handler.ts +48 -0
- package/src/server/master/master.ts +10 -27
- package/src/server/master/runner.ts +38 -132
- package/src/server/master/server.ts +93 -97
- package/src/server/master/start.ts +17 -41
- package/src/server/master/testsManager.ts +315 -0
- package/src/server/playwright/reporter.ts +386 -0
- package/src/server/selenium/internal.ts +23 -3
- package/src/server/selenium/selenoid.ts +5 -0
- package/src/server/shutdown.ts +19 -0
- package/src/server/stories.ts +1 -12
- package/src/server/ui-update.ts +46 -0
- package/src/server/utils.ts +40 -9
- package/src/server/worker/start.ts +1 -1
- package/src/types.ts +14 -8
@@ -1,44 +1,27 @@
|
|
1
|
-
import
|
2
|
-
import {
|
3
|
-
import { loadTestsFromStories, saveTestsJson } from '../stories.js';
|
1
|
+
import { Config } from '../../types.js';
|
2
|
+
import { loadTestsFromStories } from '../stories.js';
|
4
3
|
import Runner from './runner.js';
|
5
|
-
import {
|
6
|
-
|
7
|
-
function mergeTests(
|
8
|
-
testsWithReports: Partial<Record<string, TestData>>,
|
9
|
-
testsFromStories: Partial<Record<string, ServerTest>>,
|
10
|
-
): Partial<Record<string, ServerTest>> {
|
11
|
-
Object.values(testsFromStories)
|
12
|
-
.filter(isDefined)
|
13
|
-
.forEach((test) => {
|
14
|
-
const testWithReport = testsWithReports[test.id];
|
15
|
-
if (!testWithReport) return;
|
16
|
-
test.retries = testWithReport.retries;
|
17
|
-
if (testWithReport.status == 'success' || testWithReport.status == 'failed') test.status = testWithReport.status;
|
18
|
-
test.results = testWithReport.results;
|
19
|
-
test.approved = testWithReport.approved;
|
20
|
-
});
|
21
|
-
return testsFromStories;
|
22
|
-
}
|
4
|
+
import { TestsManager } from './testsManager.js';
|
23
5
|
|
24
6
|
export default async function master(config: Config, gridUrl?: string): Promise<Runner> {
|
25
|
-
|
26
|
-
const
|
27
|
-
|
7
|
+
// Create TestsManager instance
|
8
|
+
const testsManager = new TestsManager(config.screenDir, config.reportDir);
|
9
|
+
|
10
|
+
// Create Runner with TestsManager
|
11
|
+
const runner = new Runner(config, testsManager, gridUrl);
|
28
12
|
|
29
13
|
await runner.init();
|
30
14
|
|
15
|
+
// Load tests from stories and update TestsManager
|
31
16
|
const tests = await loadTestsFromStories(
|
32
17
|
Object.keys(config.browsers),
|
33
18
|
(listener) => config.storiesProvider(config, listener),
|
34
19
|
(testsDiff) => {
|
35
20
|
runner.updateTests(testsDiff);
|
36
|
-
saveTestsJson(runner.tests, config.reportDir);
|
37
21
|
},
|
38
22
|
);
|
39
23
|
|
40
|
-
|
41
|
-
saveTestsJson(runner.tests, config.reportDir);
|
24
|
+
testsManager.updateTests(testsManager.loadAndMergeTests(tests));
|
42
25
|
|
43
26
|
return runner;
|
44
27
|
}
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import path from 'path';
|
2
|
-
import { copyFile, mkdir } from 'fs/promises';
|
3
1
|
import { EventEmitter } from 'events';
|
4
2
|
import {
|
5
3
|
Config,
|
@@ -10,7 +8,6 @@ import {
|
|
10
8
|
CreeveyUpdate,
|
11
9
|
TestStatus,
|
12
10
|
ServerTest,
|
13
|
-
TestMeta,
|
14
11
|
TEST_EVENTS,
|
15
12
|
FakeSuite,
|
16
13
|
FakeTest,
|
@@ -19,6 +16,7 @@ import Pool from './pool.js';
|
|
19
16
|
import { WorkerQueue } from './queue.js';
|
20
17
|
import { getTestPath } from '../utils.js';
|
21
18
|
import { getReporter } from '../reporters/index.js';
|
19
|
+
import { TestsManager } from './testsManager.js';
|
22
20
|
|
23
21
|
// NOTE: This is workaround to fix parallel tests running with mocha-junit-reporter
|
24
22
|
let isJUnit = false;
|
@@ -33,24 +31,23 @@ class FakeRunner extends EventEmitter {
|
|
33
31
|
|
34
32
|
export default class Runner extends EventEmitter {
|
35
33
|
private failFast: boolean;
|
36
|
-
private screenDir: string;
|
37
|
-
private reportDir: string;
|
38
34
|
private browsers: string[];
|
39
35
|
private scheduler: WorkerQueue;
|
40
36
|
private pools: Record<string, Pool> = {};
|
41
37
|
private fakeRunner: FakeRunner;
|
42
38
|
private config: Config;
|
43
|
-
|
39
|
+
public testsManager: TestsManager;
|
40
|
+
|
44
41
|
public get isRunning(): boolean {
|
45
42
|
return Object.values(this.pools).some((pool) => pool.isRunning);
|
46
43
|
}
|
47
|
-
|
44
|
+
|
45
|
+
constructor(config: Config, testsManager: TestsManager, gridUrl?: string) {
|
48
46
|
super();
|
49
47
|
|
50
48
|
this.config = config;
|
51
49
|
this.failFast = config.failFast;
|
52
|
-
this.
|
53
|
-
this.reportDir = config.reportDir;
|
50
|
+
this.testsManager = testsManager;
|
54
51
|
this.scheduler = new WorkerQueue(config.useWorkerQueue);
|
55
52
|
this.browsers = Object.keys(config.browsers);
|
56
53
|
|
@@ -61,7 +58,7 @@ export default class Runner extends EventEmitter {
|
|
61
58
|
isJUnit = true;
|
62
59
|
}
|
63
60
|
|
64
|
-
new Reporter(runner, { reportDir:
|
61
|
+
new Reporter(runner, { reportDir: config.reportDir, reporterOptions: config.reporterOptions });
|
65
62
|
this.fakeRunner = runner;
|
66
63
|
|
67
64
|
this.browsers
|
@@ -71,10 +68,10 @@ export default class Runner extends EventEmitter {
|
|
71
68
|
|
72
69
|
private handlePoolMessage = (message: { id: string; status: TestStatus; result?: TestResult }): void => {
|
73
70
|
const { id, status, result } = message;
|
74
|
-
const test = this.
|
71
|
+
const test = this.testsManager.getTest(id);
|
75
72
|
|
76
73
|
if (!test) return;
|
77
|
-
const { browser, testName
|
74
|
+
const { browser, testName } = test;
|
78
75
|
|
79
76
|
const fakeSuite: FakeSuite = {
|
80
77
|
title: test.storyPath.slice(0, -1).join('/'),
|
@@ -103,20 +100,14 @@ export default class Runner extends EventEmitter {
|
|
103
100
|
|
104
101
|
fakeSuite.tests.push(fakeTest);
|
105
102
|
|
106
|
-
|
107
|
-
|
103
|
+
const update = this.testsManager.updateTestStatus(id, status, result);
|
104
|
+
if (!update) return;
|
105
|
+
|
108
106
|
if (!result) {
|
109
|
-
// NOTE: Running status
|
110
107
|
this.fakeRunner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
|
111
|
-
this.sendUpdate(
|
108
|
+
this.sendUpdate(update);
|
112
109
|
return;
|
113
110
|
}
|
114
|
-
test.results ??= [];
|
115
|
-
test.results.push(result);
|
116
|
-
|
117
|
-
if (status == 'failed') {
|
118
|
-
test.approved = null;
|
119
|
-
}
|
120
111
|
|
121
112
|
const { duration, attachments } = result;
|
122
113
|
|
@@ -146,20 +137,7 @@ export default class Runner extends EventEmitter {
|
|
146
137
|
|
147
138
|
this.fakeRunner.emit(TEST_EVENTS.TEST_END, fakeTest);
|
148
139
|
|
149
|
-
this.sendUpdate(
|
150
|
-
tests: {
|
151
|
-
[id]: {
|
152
|
-
id,
|
153
|
-
browser,
|
154
|
-
testName,
|
155
|
-
storyPath,
|
156
|
-
status: test.status,
|
157
|
-
approved: test.approved,
|
158
|
-
results: [result],
|
159
|
-
storyId,
|
160
|
-
},
|
161
|
-
},
|
162
|
-
});
|
140
|
+
this.sendUpdate(update);
|
163
141
|
|
164
142
|
if (this.failFast && status == 'failed') this.stop();
|
165
143
|
};
|
@@ -177,31 +155,8 @@ export default class Runner extends EventEmitter {
|
|
177
155
|
}
|
178
156
|
|
179
157
|
public updateTests(testsDiff: Partial<Record<string, ServerTest>>): void {
|
180
|
-
const
|
181
|
-
|
182
|
-
Object.entries(testsDiff).forEach(([id, newTest]) => {
|
183
|
-
const oldTest = this.tests[id];
|
184
|
-
if (newTest) {
|
185
|
-
if (oldTest) {
|
186
|
-
this.tests[id] = {
|
187
|
-
...newTest,
|
188
|
-
retries: oldTest.retries,
|
189
|
-
results: oldTest.results,
|
190
|
-
approved: oldTest.approved,
|
191
|
-
};
|
192
|
-
} else this.tests[id] = newTest;
|
193
|
-
|
194
|
-
const { story: _, fn: __, ...restTest } = newTest;
|
195
|
-
tests[id] = { ...restTest, status: 'unknown' };
|
196
|
-
} else if (oldTest) {
|
197
|
-
const { id, browser, testName, storyPath, storyId } = oldTest;
|
198
|
-
removedTests.push({ id, browser, testName, storyPath, storyId });
|
199
|
-
// TODO Use Map instead
|
200
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
201
|
-
delete this.tests[id];
|
202
|
-
}
|
203
|
-
});
|
204
|
-
this.sendUpdate({ tests, removedTests });
|
158
|
+
const update = this.testsManager.updateTests(testsDiff);
|
159
|
+
if (update) this.sendUpdate(update);
|
205
160
|
}
|
206
161
|
|
207
162
|
public start(ids: string[]): void {
|
@@ -209,27 +164,32 @@ export default class Runner extends EventEmitter {
|
|
209
164
|
if (this.isRunning) return;
|
210
165
|
|
211
166
|
const testsToStart = ids
|
212
|
-
.map((id) => this.
|
167
|
+
.map((id) => this.testsManager.getTest(id))
|
213
168
|
.filter(isDefined)
|
214
169
|
.filter((test) => !test.skip);
|
215
170
|
|
216
171
|
if (testsToStart.length == 0) return;
|
217
172
|
|
173
|
+
const pendingTests: CreeveyUpdate['tests'] = testsToStart.reduce(
|
174
|
+
(update: CreeveyUpdate['tests'], { id, storyId, browser, testName, storyPath }) => ({
|
175
|
+
...update,
|
176
|
+
[id]: { id, browser, testName, storyPath, status: 'pending', storyId },
|
177
|
+
}),
|
178
|
+
{},
|
179
|
+
);
|
180
|
+
|
218
181
|
this.sendUpdate({
|
219
182
|
isRunning: true,
|
220
|
-
tests:
|
221
|
-
(update: CreeveyUpdate['tests'], { id, storyId, browser, testName, storyPath }) => ({
|
222
|
-
...update,
|
223
|
-
[id]: { id, browser, testName, storyPath, status: 'pending', storyId },
|
224
|
-
}),
|
225
|
-
{},
|
226
|
-
),
|
183
|
+
tests: pendingTests,
|
227
184
|
});
|
228
185
|
|
229
186
|
const testsByBrowser: Partial<TestsByBrowser> = testsToStart.reduce((tests: Partial<TestsByBrowser>, test) => {
|
230
187
|
const { id, browser, testName, storyPath } = test;
|
231
188
|
const restPath = [...storyPath, testName].filter(isDefined);
|
232
|
-
|
189
|
+
|
190
|
+
// Update status to pending in TestsManager
|
191
|
+
this.testsManager.updateTestStatus(id, 'pending');
|
192
|
+
|
233
193
|
return {
|
234
194
|
...tests,
|
235
195
|
[browser]: [...(tests[browser] ?? []), { id, path: restPath }],
|
@@ -255,78 +215,24 @@ export default class Runner extends EventEmitter {
|
|
255
215
|
}
|
256
216
|
|
257
217
|
public get status(): CreeveyStatus {
|
258
|
-
const tests: CreeveyStatus['tests'] = {};
|
259
|
-
Object.values(this.tests)
|
260
|
-
.filter(isDefined)
|
261
|
-
|
262
|
-
.forEach(({ story: _, fn: __, ...test }) => (tests[test.id] = test));
|
263
218
|
return {
|
264
219
|
isRunning: this.isRunning,
|
265
|
-
tests,
|
220
|
+
tests: this.testsManager.getTestsData(),
|
266
221
|
browsers: this.browsers,
|
222
|
+
isUpdateMode: false,
|
267
223
|
};
|
268
224
|
}
|
269
225
|
|
270
|
-
private async copyImage(test: ServerTest, image: string, actual: string): Promise<void> {
|
271
|
-
const { browser, testName, storyPath } = test;
|
272
|
-
const restPath = [...storyPath, testName].filter(isDefined);
|
273
|
-
const testPath = path.join(...restPath, image == browser ? '' : browser);
|
274
|
-
const srcImagePath = path.join(this.reportDir, testPath, actual);
|
275
|
-
const dstImagePath = path.join(this.screenDir, testPath, `${image}.png`);
|
276
|
-
await mkdir(path.join(this.screenDir, testPath), { recursive: true });
|
277
|
-
await copyFile(srcImagePath, dstImagePath);
|
278
|
-
}
|
279
|
-
|
280
226
|
public async approveAll(): Promise<void> {
|
281
|
-
const
|
282
|
-
|
283
|
-
if (!test?.results) continue;
|
284
|
-
const retry = test.results.length - 1;
|
285
|
-
const { images, status } = test.results.at(retry) ?? {};
|
286
|
-
if (!images || status != 'failed') continue;
|
287
|
-
for (const [name, image] of Object.entries(images)) {
|
288
|
-
if (!image) continue;
|
289
|
-
await this.copyImage(test, name, image.actual);
|
290
|
-
|
291
|
-
test.approved ??= {};
|
292
|
-
test.approved[name] = retry;
|
293
|
-
test.status = 'approved';
|
294
|
-
|
295
|
-
updatedTests[test.id] = {
|
296
|
-
id: test.id,
|
297
|
-
browser: test.browser,
|
298
|
-
storyPath: test.storyPath,
|
299
|
-
storyId: test.storyId,
|
300
|
-
status: test.status,
|
301
|
-
approved: { [name]: retry },
|
302
|
-
};
|
303
|
-
}
|
304
|
-
}
|
305
|
-
this.sendUpdate({ tests: updatedTests });
|
227
|
+
const update = await this.testsManager.approveAll();
|
228
|
+
this.sendUpdate(update);
|
306
229
|
}
|
307
230
|
|
308
|
-
public async approve(
|
309
|
-
const
|
310
|
-
if (
|
311
|
-
const result = test.results[retry];
|
312
|
-
if (!result.images) return;
|
313
|
-
const images = result.images[image];
|
314
|
-
if (!images) return;
|
315
|
-
test.approved ??= {};
|
316
|
-
const { browser, testName, storyPath, storyId } = test;
|
317
|
-
|
318
|
-
await this.copyImage(test, image, images.actual);
|
319
|
-
|
320
|
-
test.approved[image] = retry;
|
321
|
-
|
322
|
-
if (Object.keys(result.images).every((name) => typeof test.approved?.[name] == 'number')) {
|
323
|
-
test.status = 'approved';
|
324
|
-
}
|
325
|
-
|
326
|
-
this.sendUpdate({
|
327
|
-
tests: { [id]: { id, browser, testName, storyPath, status: test.status, approved: { [image]: retry }, storyId } },
|
328
|
-
});
|
231
|
+
public async approve(payload: ApprovePayload): Promise<void> {
|
232
|
+
const update = await this.testsManager.approve(payload);
|
233
|
+
if (update) this.sendUpdate(update);
|
329
234
|
}
|
235
|
+
|
330
236
|
private sendUpdate(data: CreeveyUpdate): void {
|
331
237
|
this.emit('update', data);
|
332
238
|
}
|
@@ -1,125 +1,121 @@
|
|
1
1
|
import path from 'path';
|
2
|
-
import
|
3
|
-
import cluster from 'cluster';
|
4
|
-
import Koa from 'koa';
|
5
|
-
import cors from '@koa/cors';
|
6
|
-
import serve from 'koa-static';
|
7
|
-
import mount from 'koa-mount';
|
8
|
-
import body from 'koa-bodyparser';
|
9
|
-
import WebSocket from 'ws';
|
2
|
+
import HyperExpress from 'hyper-express';
|
10
3
|
import { fileURLToPath, pathToFileURL } from 'url';
|
11
4
|
import { CreeveyApi } from './api.js';
|
12
|
-
import {
|
13
|
-
import {
|
5
|
+
import { subscribeOn } from '../messages.js';
|
6
|
+
import { noop } from '../../types.js';
|
14
7
|
import { logger } from '../logger.js';
|
15
|
-
import {
|
8
|
+
import { pingHandler, createStoriesHandler, captureHandler, createStaticFileHandler } from './handlers/index.js';
|
16
9
|
|
17
10
|
const importMetaUrl = pathToFileURL(__filename).href;
|
18
11
|
|
19
12
|
export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
|
20
13
|
let resolveApi: (api: CreeveyApi) => void = noop;
|
21
|
-
let setStoriesCounter = 0;
|
22
14
|
const creeveyApi = new Promise<CreeveyApi>((resolve) => (resolveApi = resolve));
|
23
|
-
const app = new Koa();
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
25
|
-
const server = http.createServer(app.callback());
|
26
|
-
const wss = new WebSocket.Server({ server });
|
27
|
-
|
28
|
-
app.use(cors());
|
29
|
-
app.use(body());
|
30
|
-
|
31
|
-
app.use(async (ctx, next) => {
|
32
|
-
if (ctx.method == 'GET' && ctx.path == '/ping') {
|
33
|
-
ctx.body = 'pong';
|
34
|
-
return;
|
35
|
-
}
|
36
|
-
await next();
|
37
|
-
});
|
38
15
|
|
39
|
-
|
40
|
-
|
41
|
-
await creeveyApi;
|
42
|
-
await next();
|
43
|
-
});
|
44
|
-
}
|
16
|
+
// Create HyperExpress server instance
|
17
|
+
const server = new HyperExpress.Server();
|
45
18
|
|
46
|
-
|
47
|
-
|
48
|
-
const { setStoriesCounter: counter, stories } = ctx.request.body as {
|
49
|
-
setStoriesCounter: number;
|
50
|
-
stories: [string, StoryInput[]][];
|
51
|
-
};
|
52
|
-
if (setStoriesCounter >= counter) return;
|
53
|
-
|
54
|
-
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
55
|
-
file,
|
56
|
-
stories.map(deserializeStory),
|
57
|
-
]);
|
58
|
-
|
59
|
-
setStoriesCounter = counter;
|
60
|
-
emitStoriesMessage({ type: 'update', payload: deserializedStories });
|
61
|
-
Object.values(cluster.workers ?? {})
|
62
|
-
.filter(isDefined)
|
63
|
-
.filter((worker) => worker.isConnected())
|
64
|
-
.forEach((worker) => {
|
65
|
-
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
66
|
-
});
|
67
|
-
return;
|
68
|
-
}
|
69
|
-
await next();
|
70
|
-
});
|
19
|
+
// Store active WebSocket connections
|
20
|
+
const activeConnections = new Set<HyperExpress.Websocket>();
|
71
21
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
await new Promise<void>((resolve) => {
|
81
|
-
const unsubscribe = subscribeOnWorker(worker, 'stories', (message) => {
|
82
|
-
if (message.type != 'capture') return;
|
83
|
-
unsubscribe();
|
84
|
-
resolve();
|
85
|
-
});
|
86
|
-
sendStoriesMessage(worker, { type: 'capture', payload: options });
|
87
|
-
});
|
88
|
-
// TODO Pass screenshot result to show it in inspector
|
89
|
-
ctx.body = 'Ok';
|
90
|
-
return;
|
22
|
+
// Enable CORS for all routes
|
23
|
+
server.use((request, response, next) => {
|
24
|
+
response.header('Access-Control-Allow-Origin', '*');
|
25
|
+
response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
26
|
+
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
27
|
+
|
28
|
+
if (request.method === 'OPTIONS') {
|
29
|
+
return response.status(200).send();
|
91
30
|
}
|
92
|
-
|
31
|
+
|
32
|
+
next();
|
93
33
|
});
|
94
34
|
|
95
|
-
|
96
|
-
|
35
|
+
// Health check endpoint
|
36
|
+
server.get('/ping', pingHandler);
|
97
37
|
|
98
|
-
|
99
|
-
|
100
|
-
});
|
38
|
+
// Stories endpoint
|
39
|
+
server.post('/stories', createStoriesHandler());
|
101
40
|
|
102
|
-
|
41
|
+
// Capture endpoint
|
42
|
+
server.post('/capture', captureHandler);
|
103
43
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
44
|
+
// Serve report files
|
45
|
+
server.get('/report/*', createStaticFileHandler(reportDir, '/report/'));
|
46
|
+
|
47
|
+
// Serve static files
|
48
|
+
const webDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
49
|
+
server.get('/*', createStaticFileHandler(webDir));
|
50
|
+
|
51
|
+
// If UI mode, wait for CreeveyApi to be resolved
|
52
|
+
if (ui) {
|
53
|
+
// Create a custom broadcast function that works with our connections
|
54
|
+
const broadcast = (message: string) => {
|
55
|
+
for (const connection of activeConnections) {
|
56
|
+
connection.send(message);
|
57
|
+
}
|
58
|
+
};
|
59
|
+
|
60
|
+
// Create a custom WebSocket server that simulates the standard behavior
|
61
|
+
const customWsServer = {
|
62
|
+
clients: activeConnections,
|
63
|
+
publish: broadcast,
|
64
|
+
};
|
65
|
+
|
66
|
+
let api: CreeveyApi | null = null;
|
67
|
+
|
68
|
+
server.use(async (request, _response, next) => {
|
69
|
+
if (!api && request.path === '/') {
|
70
|
+
api = await creeveyApi;
|
71
|
+
api.subscribe(customWsServer);
|
72
|
+
}
|
73
|
+
next();
|
109
74
|
});
|
110
|
-
});
|
111
75
|
|
112
|
-
|
113
|
-
|
76
|
+
// Create WebSocket listener
|
77
|
+
server.ws('/', (ws) => {
|
78
|
+
// Add connection to the set of active connections
|
79
|
+
activeConnections.add(ws);
|
114
80
|
|
115
|
-
|
116
|
-
ws.on('message', (message:
|
117
|
-
|
118
|
-
|
119
|
-
|
81
|
+
// Handle message events
|
82
|
+
ws.on('message', (message: string | Buffer) => {
|
83
|
+
api?.handleMessage(ws, message);
|
84
|
+
});
|
85
|
+
|
86
|
+
// Handle close events to clean up connections
|
87
|
+
ws.on('close', () => {
|
88
|
+
activeConnections.delete(ws);
|
120
89
|
});
|
121
90
|
});
|
91
|
+
}
|
92
|
+
|
93
|
+
// Shutdown handling
|
94
|
+
subscribeOn('shutdown', () => {
|
95
|
+
// Close all WebSocket connections
|
96
|
+
for (const connection of activeConnections) {
|
97
|
+
try {
|
98
|
+
connection.close();
|
99
|
+
} catch (error) {
|
100
|
+
logger().error('Error closing WebSocket connection', error);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// Close the server
|
105
|
+
server.close();
|
122
106
|
});
|
123
107
|
|
108
|
+
// Start server
|
109
|
+
server
|
110
|
+
.listen(port)
|
111
|
+
.then(() => {
|
112
|
+
logger().info(`Server starting on port ${port}`);
|
113
|
+
})
|
114
|
+
.catch((error: unknown) => {
|
115
|
+
logger().error('Failed to start server', error);
|
116
|
+
process.exit(1);
|
117
|
+
});
|
118
|
+
|
119
|
+
// Return the function to resolve the API
|
124
120
|
return resolveApi;
|
125
121
|
}
|
@@ -1,41 +1,14 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { existsSync } from 'fs';
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
4
|
-
import { copyFile, readdir, mkdir, writeFile } from 'fs/promises';
|
5
3
|
import master from './master.js';
|
6
|
-
import
|
7
|
-
import { Config, Options,
|
8
|
-
import { shutdownWorkers, testsToImages, readDirRecursive } from '../utils.js';
|
4
|
+
import { CreeveyApi } from './api.js';
|
5
|
+
import { Config, Options, isDefined } from '../../types.js';
|
6
|
+
import { shutdownWorkers, testsToImages, readDirRecursive, copyStatics } from '../utils.js';
|
9
7
|
import { subscribeOn } from '../messages.js';
|
10
8
|
import Runner from './runner.js';
|
11
9
|
import { logger } from '../logger.js';
|
12
10
|
import { sendScreenshotsCount } from '../telemetry.js';
|
13
|
-
|
14
|
-
const importMetaUrl = pathToFileURL(__filename).href;
|
15
|
-
|
16
|
-
async function copyStatics(reportDir: string): Promise<void> {
|
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
|
-
.map((dirent) => dirent.name);
|
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));
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
28
|
-
function reportDataModule(data: Partial<Record<string, TestData>>): string {
|
29
|
-
return `
|
30
|
-
(function (root, factory) {
|
31
|
-
if (typeof module === 'object' && module.exports) {
|
32
|
-
module.exports = factory();
|
33
|
-
} else {
|
34
|
-
root.__CREEVEY_DATA__ = factory();
|
35
|
-
}
|
36
|
-
}(this, function () { return ${JSON.stringify(data)} }));
|
37
|
-
`;
|
38
|
-
}
|
11
|
+
import { start as startServer } from './server.js';
|
39
12
|
|
40
13
|
function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
41
14
|
if (!existsSync(imagesDir)) return;
|
@@ -50,12 +23,9 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
|
50
23
|
}
|
51
24
|
}
|
52
25
|
|
53
|
-
export async function start(
|
54
|
-
|
55
|
-
|
56
|
-
options: Options,
|
57
|
-
resolveApi: (api: CreeveyApi) => void,
|
58
|
-
): Promise<void> {
|
26
|
+
export async function start(gridUrl: string | undefined, config: Config, options: Options): Promise<void> {
|
27
|
+
const resolveApi = startServer(config.reportDir, options.port, options.ui);
|
28
|
+
|
59
29
|
let runner: Runner | null = null;
|
60
30
|
if (config.hooks.before) {
|
61
31
|
await config.hooks.before();
|
@@ -78,13 +48,19 @@ export async function start(
|
|
78
48
|
runner = await master(config, gridUrl);
|
79
49
|
|
80
50
|
runner.on('stop', () => {
|
81
|
-
void copyStatics(config.reportDir).then(() =>
|
82
|
-
writeFile(path.join(config.reportDir, 'data.js'), reportDataModule(runner.status.tests)),
|
83
|
-
);
|
51
|
+
void copyStatics(config.reportDir).then(() => runner.testsManager.saveTestData());
|
84
52
|
});
|
85
53
|
|
86
54
|
if (options.ui) {
|
87
|
-
|
55
|
+
// Initialize TestsManager
|
56
|
+
const testsManager = runner.testsManager;
|
57
|
+
|
58
|
+
// Create the CreeveyApi instance using the existing runner
|
59
|
+
const api = new CreeveyApi(testsManager, runner);
|
60
|
+
|
61
|
+
// Resolve the API for the server
|
62
|
+
resolveApi(api);
|
63
|
+
|
88
64
|
logger().info(`Started on http://localhost:${options.port}`);
|
89
65
|
} else {
|
90
66
|
if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
|