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.
Files changed (134) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/dist/client/addon/controller.js +1 -1
  3. package/dist/client/addon/controller.js.map +1 -1
  4. package/dist/client/addon/withCreevey.js +1 -18
  5. package/dist/client/addon/withCreevey.js.map +1 -1
  6. package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
  7. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  8. package/dist/client/shared/creeveyClientApi.js +10 -0
  9. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  10. package/dist/client/web/CreeveyApp.d.ts +1 -0
  11. package/dist/client/web/CreeveyApp.js +1 -0
  12. package/dist/client/web/CreeveyApp.js.map +1 -1
  13. package/dist/client/web/CreeveyContext.d.ts +1 -0
  14. package/dist/client/web/CreeveyContext.js +1 -0
  15. package/dist/client/web/CreeveyContext.js.map +1 -1
  16. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
  17. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  18. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
  19. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  20. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
  21. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
  23. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
  25. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  26. package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
  27. package/dist/client/web/index.html +1 -1
  28. package/dist/client/web/index.js +8 -3
  29. package/dist/client/web/index.js.map +1 -1
  30. package/dist/creevey.d.ts +1 -1
  31. package/dist/creevey.js +1 -22
  32. package/dist/creevey.js.map +1 -1
  33. package/dist/playwright-reporter.d.ts +2 -0
  34. package/dist/playwright-reporter.js +5 -0
  35. package/dist/playwright-reporter.js.map +1 -0
  36. package/dist/playwright.d.ts +1 -1
  37. package/dist/server/config.js +8 -1
  38. package/dist/server/config.js.map +1 -1
  39. package/dist/server/index.js +10 -3
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/master/api.d.ts +15 -5
  42. package/dist/server/master/api.js +89 -27
  43. package/dist/server/master/api.js.map +1 -1
  44. package/dist/server/master/handlers/capture-handler.d.ts +2 -0
  45. package/dist/server/master/handlers/capture-handler.js +35 -0
  46. package/dist/server/master/handlers/capture-handler.js.map +1 -0
  47. package/dist/server/master/handlers/index.d.ts +4 -0
  48. package/dist/server/master/handlers/index.js +21 -0
  49. package/dist/server/master/handlers/index.js.map +1 -0
  50. package/dist/server/master/handlers/ping-handler.d.ts +2 -0
  51. package/dist/server/master/handlers/ping-handler.js +7 -0
  52. package/dist/server/master/handlers/ping-handler.js.map +1 -0
  53. package/dist/server/master/handlers/static-handler.d.ts +2 -0
  54. package/dist/server/master/handlers/static-handler.js +32 -0
  55. package/dist/server/master/handlers/static-handler.js.map +1 -0
  56. package/dist/server/master/handlers/stories-handler.d.ts +2 -0
  57. package/dist/server/master/handlers/stories-handler.js +38 -0
  58. package/dist/server/master/handlers/stories-handler.js.map +1 -0
  59. package/dist/server/master/master.js +7 -24
  60. package/dist/server/master/master.js.map +1 -1
  61. package/dist/server/master/runner.d.ts +4 -6
  62. package/dist/server/master/runner.js +30 -127
  63. package/dist/server/master/runner.js.map +1 -1
  64. package/dist/server/master/server.js +77 -87
  65. package/dist/server/master/server.js.map +1 -1
  66. package/dist/server/master/start.d.ts +1 -2
  67. package/dist/server/master/start.js +11 -29
  68. package/dist/server/master/start.js.map +1 -1
  69. package/dist/server/master/testsManager.d.ts +81 -0
  70. package/dist/server/master/testsManager.js +281 -0
  71. package/dist/server/master/testsManager.js.map +1 -0
  72. package/dist/server/playwright/reporter.d.ts +87 -0
  73. package/dist/server/playwright/reporter.js +351 -0
  74. package/dist/server/playwright/reporter.js.map +1 -0
  75. package/dist/server/selenium/internal.js +20 -2
  76. package/dist/server/selenium/internal.js.map +1 -1
  77. package/dist/server/selenium/selenoid.js +4 -0
  78. package/dist/server/selenium/selenoid.js.map +1 -1
  79. package/dist/server/shutdown.d.ts +1 -0
  80. package/dist/server/shutdown.js +23 -0
  81. package/dist/server/shutdown.js.map +1 -0
  82. package/dist/server/stories.d.ts +0 -1
  83. package/dist/server/stories.js +0 -12
  84. package/dist/server/stories.js.map +1 -1
  85. package/dist/server/ui-update.d.ts +10 -0
  86. package/dist/server/ui-update.js +39 -0
  87. package/dist/server/ui-update.js.map +1 -0
  88. package/dist/server/utils.d.ts +6 -0
  89. package/dist/server/utils.js +39 -8
  90. package/dist/server/utils.js.map +1 -1
  91. package/dist/server/worker/start.js +1 -1
  92. package/dist/server/worker/start.js.map +1 -1
  93. package/dist/types.d.ts +14 -8
  94. package/dist/types.js.map +1 -1
  95. package/docs/examples/playwright-reporter-example.ts +202 -0
  96. package/docs/migration-0.9-to-0.10.md +144 -0
  97. package/docs/playwright-reporter.md +357 -0
  98. package/package.json +9 -13
  99. package/src/client/addon/controller.ts +1 -1
  100. package/src/client/addon/withCreevey.ts +2 -16
  101. package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
  102. package/src/client/shared/creeveyClientApi.ts +10 -0
  103. package/src/client/web/CreeveyApp.tsx +2 -0
  104. package/src/client/web/CreeveyContext.tsx +2 -0
  105. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
  106. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
  107. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
  108. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  109. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
  110. package/src/client/web/index.tsx +8 -3
  111. package/src/creevey.ts +1 -24
  112. package/src/playwright-reporter.ts +3 -0
  113. package/src/server/config.ts +9 -1
  114. package/src/server/index.ts +11 -4
  115. package/src/server/master/api.ts +95 -26
  116. package/src/server/master/handlers/capture-handler.ts +39 -0
  117. package/src/server/master/handlers/index.ts +4 -0
  118. package/src/server/master/handlers/ping-handler.ts +5 -0
  119. package/src/server/master/handlers/static-handler.ts +29 -0
  120. package/src/server/master/handlers/stories-handler.ts +48 -0
  121. package/src/server/master/master.ts +10 -27
  122. package/src/server/master/runner.ts +38 -132
  123. package/src/server/master/server.ts +93 -97
  124. package/src/server/master/start.ts +17 -41
  125. package/src/server/master/testsManager.ts +315 -0
  126. package/src/server/playwright/reporter.ts +386 -0
  127. package/src/server/selenium/internal.ts +23 -3
  128. package/src/server/selenium/selenoid.ts +5 -0
  129. package/src/server/shutdown.ts +19 -0
  130. package/src/server/stories.ts +1 -12
  131. package/src/server/ui-update.ts +46 -0
  132. package/src/server/utils.ts +40 -9
  133. package/src/server/worker/start.ts +1 -1
  134. package/src/types.ts +14 -8
@@ -1,44 +1,27 @@
1
- import path from 'path';
2
- import { Config, TestData, isDefined, ServerTest } from '../../types.js';
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 { tryToLoadTestsData } from '../utils.js';
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
- const runner = new Runner(config, gridUrl);
26
- const reportDataPath = path.join(config.reportDir, 'data.js');
27
- const testsFromReport = tryToLoadTestsData(reportDataPath) ?? {};
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
- runner.tests = mergeTests(testsFromReport, tests);
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
- tests: Partial<Record<string, ServerTest>> = {};
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
- constructor(config: Config, gridUrl?: string) {
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.screenDir = config.screenDir;
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: this.reportDir, reporterOptions: config.reporterOptions });
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.tests[id];
71
+ const test = this.testsManager.getTest(id);
75
72
 
76
73
  if (!test) return;
77
- const { browser, testName, storyPath, storyId } = test;
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
- // TODO Handle 'retrying' status
107
- test.status = status == 'retrying' ? 'failed' : status;
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({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
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 tests: CreeveyStatus['tests'] = {};
181
- const removedTests: TestMeta[] = [];
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.tests[id])
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: testsToStart.reduce(
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
- test.status = 'pending';
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 updatedTests: NonNullable<CreeveyUpdate['tests']> = {};
282
- for (const test of Object.values(this.tests)) {
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({ id, retry, image }: ApprovePayload): Promise<void> {
309
- const test = this.tests[id];
310
- if (!test?.results) return;
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 http from 'http';
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 { emitStoriesMessage, sendStoriesMessage, subscribeOn, subscribeOnWorker } from '../messages.js';
13
- import { CaptureOptions, isDefined, noop, StoryInput } from '../../types.js';
5
+ import { subscribeOn } from '../messages.js';
6
+ import { noop } from '../../types.js';
14
7
  import { logger } from '../logger.js';
15
- import { deserializeStory } from '../../shared/index.js';
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
- if (ui) {
40
- app.use(async (_, next) => {
41
- await creeveyApi;
42
- await next();
43
- });
44
- }
16
+ // Create HyperExpress server instance
17
+ const server = new HyperExpress.Server();
45
18
 
46
- app.use(async (ctx, next) => {
47
- if (ctx.method == 'POST' && ctx.path == '/stories') {
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
- app.use(async (ctx, next) => {
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;
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
- await next();
31
+
32
+ next();
93
33
  });
94
34
 
95
- app.use(serve(path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web')));
96
- app.use(mount('/report', serve(reportDir)));
35
+ // Health check endpoint
36
+ server.get('/ping', pingHandler);
97
37
 
98
- wss.on('error', (error) => {
99
- logger().error(error);
100
- });
38
+ // Stories endpoint
39
+ server.post('/stories', createStoriesHandler());
101
40
 
102
- server.listen(port);
41
+ // Capture endpoint
42
+ server.post('/capture', captureHandler);
103
43
 
104
- subscribeOn('shutdown', () => {
105
- server.close();
106
- wss.close();
107
- wss.clients.forEach((ws) => {
108
- ws.close();
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
- void creeveyApi.then((api) => {
113
- api.subscribe(wss);
76
+ // Create WebSocket listener
77
+ server.ws('/', (ws) => {
78
+ // Add connection to the set of active connections
79
+ activeConnections.add(ws);
114
80
 
115
- wss.on('connection', (ws) => {
116
- ws.on('message', (message: WebSocket.RawData, isBinary: boolean) => {
117
- // NOTE Text messages are passed as Buffer https://github.com/websockets/ws/releases/tag/8.0.0
118
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
119
- api.handleMessage(ws, isBinary ? message : message.toString('utf-8'));
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 creeveyApi, { CreeveyApi } from './api.js';
7
- import { Config, Options, TestData, isDefined } from '../../types.js';
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
- gridUrl: string | undefined,
55
- config: Config,
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
- resolveApi(creeveyApi(runner));
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) {