creevey 0.10.0-beta.43 → 0.10.0-beta.45
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 +12 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +11 -6
- package/dist/server/master/api.js +88 -25
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +5 -0
- package/dist/server/master/handlers/capture-handler.js +25 -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 +8 -0
- package/dist/server/master/handlers/ping-handler.js.map +1 -0
- package/dist/server/master/handlers/static-handler.d.ts +1 -0
- package/dist/server/master/handlers/static-handler.js +22 -0
- package/dist/server/master/handlers/static-handler.js.map +1 -0
- package/dist/server/master/handlers/stories-handler.d.ts +4 -0
- package/dist/server/master/handlers/stories-handler.js +24 -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 +191 -89
- 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/docker-file.js +2 -2
- package/dist/server/playwright/docker-file.js.map +1 -1
- 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 +10 -14
- 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 +13 -6
- package/src/server/master/api.ts +94 -28
- package/src/server/master/handlers/capture-handler.ts +20 -0
- package/src/server/master/handlers/index.ts +4 -0
- package/src/server/master/handlers/ping-handler.ts +6 -0
- package/src/server/master/handlers/static-handler.ts +18 -0
- package/src/server/master/handlers/stories-handler.ts +20 -0
- package/src/server/master/master.ts +10 -27
- package/src/server/master/runner.ts +38 -132
- package/src/server/master/server.ts +210 -98
- package/src/server/master/start.ts +17 -41
- package/src/server/master/testsManager.ts +315 -0
- package/src/server/playwright/docker-file.ts +2 -2
- 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,237 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import url from 'url';
|
1
3
|
import path from 'path';
|
2
|
-
import
|
3
|
-
import
|
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';
|
4
|
+
import { IncomingMessage, ServerResponse, createServer } from 'http';
|
5
|
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
10
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
11
|
-
import {
|
12
|
-
import {
|
13
|
-
import {
|
7
|
+
import { shutdownOnException } from '../utils.js';
|
8
|
+
import { subscribeOn } from '../messages.js';
|
9
|
+
import { noop } from '../../types.js';
|
14
10
|
import { logger } from '../logger.js';
|
15
|
-
import {
|
11
|
+
import { CreeveyApi } from './api.js';
|
12
|
+
import { pingHandler, captureHandler, storiesHandler, staticHandler } from './handlers/index.js';
|
13
|
+
|
14
|
+
function json<T = unknown>(
|
15
|
+
handler: (data: T) => void,
|
16
|
+
defaultValue: T,
|
17
|
+
): (request: IncomingMessage, response: ServerResponse) => void {
|
18
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
19
|
+
const chunks: Buffer[] = [];
|
20
|
+
|
21
|
+
request.on('data', (chunk: Buffer) => {
|
22
|
+
chunks.push(chunk);
|
23
|
+
});
|
24
|
+
|
25
|
+
request.on('end', () => {
|
26
|
+
try {
|
27
|
+
const body = Buffer.concat(chunks);
|
28
|
+
const value = body.length === 0 ? defaultValue : (JSON.parse(body.toString('utf-8')) as T);
|
29
|
+
|
30
|
+
handler(value);
|
31
|
+
response.end();
|
32
|
+
} catch (error) {
|
33
|
+
logger().error('Failed to parse JSON', error);
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
35
|
+
response.statusCode = 500;
|
36
|
+
response.setHeader('Content-Type', 'text/plain');
|
37
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
|
41
|
+
request.on('error', (error) => {
|
42
|
+
logger().error('Failed to parse JSON', error);
|
43
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
44
|
+
response.statusCode = 500;
|
45
|
+
response.setHeader('Content-Type', 'text/plain');
|
46
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
47
|
+
});
|
48
|
+
};
|
49
|
+
}
|
50
|
+
|
51
|
+
function file(handler: (requestedPath: string) => string | undefined) {
|
52
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
53
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
54
|
+
const requestedPath = parsedUrl.pathname ?? '/';
|
55
|
+
|
56
|
+
try {
|
57
|
+
const filePath = handler(requestedPath);
|
58
|
+
if (filePath) {
|
59
|
+
const stat = fs.statSync(filePath);
|
60
|
+
// Set appropriate MIME type
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
62
|
+
const mimeTypes: Record<string, string> = {
|
63
|
+
'.html': 'text/html',
|
64
|
+
'.js': 'application/javascript',
|
65
|
+
'.css': 'text/css',
|
66
|
+
'.json': 'application/json',
|
67
|
+
'.png': 'image/png',
|
68
|
+
'.jpg': 'image/jpeg',
|
69
|
+
'.jpeg': 'image/jpeg',
|
70
|
+
'.gif': 'image/gif',
|
71
|
+
'.svg': 'image/svg+xml',
|
72
|
+
'.ico': 'image/x-icon',
|
73
|
+
};
|
74
|
+
|
75
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
76
|
+
|
77
|
+
response.statusCode = 200;
|
78
|
+
response.setHeader('Content-Type', contentType);
|
79
|
+
response.setHeader('Content-Length', stat.size);
|
80
|
+
|
81
|
+
// Stream the file
|
82
|
+
const stream = fs.createReadStream(filePath);
|
83
|
+
stream.pipe(response);
|
84
|
+
|
85
|
+
stream.on('error', (error) => {
|
86
|
+
logger().error('Error streaming file', error);
|
87
|
+
if (!response.headersSent) {
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
89
|
+
response.statusCode = 500;
|
90
|
+
response.setHeader('Content-Type', 'text/plain');
|
91
|
+
response.end(`Internal server error: ${errorMessage}`);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
} else {
|
95
|
+
logger().error('File not found', requestedPath);
|
96
|
+
response.statusCode = 404;
|
97
|
+
response.setHeader('Content-Type', 'text/plain');
|
98
|
+
response.end('File not found');
|
99
|
+
}
|
100
|
+
} catch (error) {
|
101
|
+
logger().error('Failed to serve file', error);
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
103
|
+
response.statusCode = 500;
|
104
|
+
response.setHeader('Content-Type', 'text/plain');
|
105
|
+
response.end(`Failed to serve file: ${errorMessage}`);
|
106
|
+
}
|
107
|
+
};
|
108
|
+
}
|
16
109
|
|
17
110
|
const importMetaUrl = pathToFileURL(__filename).href;
|
18
111
|
|
19
112
|
export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
|
113
|
+
let wss: WebSocketServer | null = null;
|
114
|
+
let creeveyApi: CreeveyApi | null = null;
|
20
115
|
let resolveApi: (api: CreeveyApi) => void = noop;
|
21
|
-
|
22
|
-
const
|
23
|
-
const
|
24
|
-
|
25
|
-
const
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
116
|
+
|
117
|
+
const webDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
118
|
+
const server = createServer();
|
119
|
+
|
120
|
+
const routes = [
|
121
|
+
{
|
122
|
+
path: '/ping',
|
123
|
+
method: 'GET',
|
124
|
+
handler: pingHandler,
|
125
|
+
},
|
126
|
+
{
|
127
|
+
path: '/stories',
|
128
|
+
method: 'POST',
|
129
|
+
handler: json(storiesHandler, { stories: [] }),
|
130
|
+
},
|
131
|
+
{
|
132
|
+
path: '/capture',
|
133
|
+
method: 'POST',
|
134
|
+
handler: json(captureHandler, { workerId: 0, options: undefined }),
|
135
|
+
},
|
136
|
+
{
|
137
|
+
path: '/report/',
|
138
|
+
method: 'GET',
|
139
|
+
handler: file(staticHandler(reportDir, '/report/')),
|
140
|
+
},
|
141
|
+
{
|
142
|
+
path: '/',
|
143
|
+
method: 'GET',
|
144
|
+
handler: file(staticHandler(webDir)),
|
145
|
+
},
|
146
|
+
];
|
147
|
+
|
148
|
+
const router = (request: IncomingMessage, response: ServerResponse): void => {
|
149
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
150
|
+
const path = parsedUrl.pathname ?? '/';
|
151
|
+
const method = request.method ?? 'GET';
|
152
|
+
|
153
|
+
try {
|
154
|
+
const route = routes.find((route) => path.startsWith(route.path) && route.method === method);
|
155
|
+
if (route) {
|
156
|
+
route.handler(request, response);
|
157
|
+
} else {
|
158
|
+
response.statusCode = 404;
|
159
|
+
response.setHeader('Content-Type', 'text/plain');
|
160
|
+
response.end('Not Found');
|
161
|
+
}
|
162
|
+
} catch (error) {
|
163
|
+
logger().error('Request handling error', error);
|
164
|
+
response.statusCode = 500;
|
165
|
+
response.setHeader('Content-Type', 'text/plain');
|
166
|
+
response.end('Internal Server Error');
|
35
167
|
}
|
36
|
-
|
37
|
-
});
|
168
|
+
};
|
38
169
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
});
|
44
|
-
}
|
170
|
+
server.on('request', (request: IncomingMessage, response: ServerResponse): void => {
|
171
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
172
|
+
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
173
|
+
response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
45
174
|
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
});
|
175
|
+
if (request.method === 'OPTIONS') {
|
176
|
+
response.statusCode = 200;
|
177
|
+
response.end();
|
67
178
|
return;
|
68
179
|
}
|
69
|
-
await next();
|
70
|
-
});
|
71
180
|
|
72
|
-
|
73
|
-
if (ctx.method == 'POST' && ctx.path == '/capture') {
|
74
|
-
const { workerId, options } = ctx.request.body as { workerId: number; options?: CaptureOptions };
|
75
|
-
const worker = Object.values(cluster.workers ?? {})
|
76
|
-
.filter(isDefined)
|
77
|
-
.find((worker) => worker.process.pid == workerId);
|
78
|
-
// NOTE: Hypothetical case when someone send to us capture req and we don't have a worker with browser session for it
|
79
|
-
if (!worker) return;
|
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;
|
91
|
-
}
|
92
|
-
await next();
|
181
|
+
router(request, response);
|
93
182
|
});
|
94
183
|
|
95
|
-
|
96
|
-
|
184
|
+
if (ui) {
|
185
|
+
wss = new WebSocketServer({ server });
|
186
|
+
wss.on('connection', (ws: WebSocket) => {
|
187
|
+
ws.on('message', (message: RawData, isBinary: boolean) => {
|
188
|
+
if (creeveyApi) {
|
189
|
+
// NOTE Text messages are passed as Buffer https://github.com/websockets/ws/releases/tag/8.0.0
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
191
|
+
creeveyApi.handleMessage(ws, isBinary ? message : message.toString('utf-8'));
|
192
|
+
return;
|
193
|
+
}
|
194
|
+
});
|
97
195
|
|
98
|
-
|
99
|
-
|
100
|
-
|
196
|
+
ws.on('error', (error) => {
|
197
|
+
logger().error('WebSocket error', error);
|
198
|
+
});
|
199
|
+
});
|
101
200
|
|
102
|
-
|
201
|
+
wss.on('error', (error) => {
|
202
|
+
logger().error('WebSocket error', error);
|
203
|
+
});
|
204
|
+
}
|
103
205
|
|
104
206
|
subscribeOn('shutdown', () => {
|
207
|
+
if (wss) {
|
208
|
+
wss.clients.forEach((ws) => {
|
209
|
+
ws.close();
|
210
|
+
});
|
211
|
+
wss.close();
|
212
|
+
}
|
213
|
+
|
105
214
|
server.close();
|
106
|
-
wss.close();
|
107
|
-
wss.clients.forEach((ws) => {
|
108
|
-
ws.close();
|
109
|
-
});
|
110
215
|
});
|
111
216
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
api.handleMessage(ws, isBinary ? message : message.toString('utf-8'));
|
120
|
-
});
|
217
|
+
server
|
218
|
+
.listen(port, () => {
|
219
|
+
logger().info(`Server starting on port ${port}`);
|
220
|
+
})
|
221
|
+
.on('error', (error: unknown) => {
|
222
|
+
logger().error('Failed to start server', error);
|
223
|
+
process.exit(1);
|
121
224
|
});
|
122
|
-
});
|
123
225
|
|
226
|
+
void new Promise<CreeveyApi>((resolve) => (resolveApi = resolve))
|
227
|
+
.then((api) => {
|
228
|
+
creeveyApi = api;
|
229
|
+
if (wss) {
|
230
|
+
creeveyApi.subscribe(wss);
|
231
|
+
}
|
232
|
+
})
|
233
|
+
.catch(shutdownOnException);
|
234
|
+
|
235
|
+
// Return the function to resolve the API
|
124
236
|
return resolveApi;
|
125
237
|
}
|