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.
Files changed (137) 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 +12 -5
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/master/api.d.ts +11 -6
  42. package/dist/server/master/api.js +88 -25
  43. package/dist/server/master/api.js.map +1 -1
  44. package/dist/server/master/handlers/capture-handler.d.ts +5 -0
  45. package/dist/server/master/handlers/capture-handler.js +25 -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 +8 -0
  52. package/dist/server/master/handlers/ping-handler.js.map +1 -0
  53. package/dist/server/master/handlers/static-handler.d.ts +1 -0
  54. package/dist/server/master/handlers/static-handler.js +22 -0
  55. package/dist/server/master/handlers/static-handler.js.map +1 -0
  56. package/dist/server/master/handlers/stories-handler.d.ts +4 -0
  57. package/dist/server/master/handlers/stories-handler.js +24 -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 +191 -89
  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/docker-file.js +2 -2
  73. package/dist/server/playwright/docker-file.js.map +1 -1
  74. package/dist/server/playwright/reporter.d.ts +87 -0
  75. package/dist/server/playwright/reporter.js +351 -0
  76. package/dist/server/playwright/reporter.js.map +1 -0
  77. package/dist/server/selenium/internal.js +20 -2
  78. package/dist/server/selenium/internal.js.map +1 -1
  79. package/dist/server/selenium/selenoid.js +4 -0
  80. package/dist/server/selenium/selenoid.js.map +1 -1
  81. package/dist/server/shutdown.d.ts +1 -0
  82. package/dist/server/shutdown.js +23 -0
  83. package/dist/server/shutdown.js.map +1 -0
  84. package/dist/server/stories.d.ts +0 -1
  85. package/dist/server/stories.js +0 -12
  86. package/dist/server/stories.js.map +1 -1
  87. package/dist/server/ui-update.d.ts +10 -0
  88. package/dist/server/ui-update.js +39 -0
  89. package/dist/server/ui-update.js.map +1 -0
  90. package/dist/server/utils.d.ts +6 -0
  91. package/dist/server/utils.js +39 -8
  92. package/dist/server/utils.js.map +1 -1
  93. package/dist/server/worker/start.js +1 -1
  94. package/dist/server/worker/start.js.map +1 -1
  95. package/dist/types.d.ts +14 -8
  96. package/dist/types.js.map +1 -1
  97. package/docs/examples/playwright-reporter-example.ts +202 -0
  98. package/docs/migration-0.9-to-0.10.md +144 -0
  99. package/docs/playwright-reporter.md +357 -0
  100. package/package.json +10 -14
  101. package/src/client/addon/controller.ts +1 -1
  102. package/src/client/addon/withCreevey.ts +2 -16
  103. package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
  104. package/src/client/shared/creeveyClientApi.ts +10 -0
  105. package/src/client/web/CreeveyApp.tsx +2 -0
  106. package/src/client/web/CreeveyContext.tsx +2 -0
  107. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
  108. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
  109. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
  110. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  111. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
  112. package/src/client/web/index.tsx +8 -3
  113. package/src/creevey.ts +1 -24
  114. package/src/playwright-reporter.ts +3 -0
  115. package/src/server/config.ts +9 -1
  116. package/src/server/index.ts +13 -6
  117. package/src/server/master/api.ts +94 -28
  118. package/src/server/master/handlers/capture-handler.ts +20 -0
  119. package/src/server/master/handlers/index.ts +4 -0
  120. package/src/server/master/handlers/ping-handler.ts +6 -0
  121. package/src/server/master/handlers/static-handler.ts +18 -0
  122. package/src/server/master/handlers/stories-handler.ts +20 -0
  123. package/src/server/master/master.ts +10 -27
  124. package/src/server/master/runner.ts +38 -132
  125. package/src/server/master/server.ts +210 -98
  126. package/src/server/master/start.ts +17 -41
  127. package/src/server/master/testsManager.ts +315 -0
  128. package/src/server/playwright/docker-file.ts +2 -2
  129. package/src/server/playwright/reporter.ts +386 -0
  130. package/src/server/selenium/internal.ts +23 -3
  131. package/src/server/selenium/selenoid.ts +5 -0
  132. package/src/server/shutdown.ts +19 -0
  133. package/src/server/stories.ts +1 -12
  134. package/src/server/ui-update.ts +46 -0
  135. package/src/server/utils.ts +40 -9
  136. package/src/server/worker/start.ts +1 -1
  137. package/src/types.ts +14 -8
@@ -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) {
@@ -0,0 +1,315 @@
1
+ import path from 'path';
2
+ import { mkdirSync, writeFileSync } from 'fs';
3
+ import EventEmitter from 'events';
4
+ import {
5
+ ServerTest,
6
+ TestMeta,
7
+ TestResult,
8
+ TestStatus,
9
+ CreeveyUpdate,
10
+ ApprovePayload,
11
+ isDefined,
12
+ isFunction,
13
+ CreeveyStatus,
14
+ } from '../../types.js';
15
+ import { tryToLoadTestsData } from '../utils.js';
16
+ import { copyFile, mkdir, writeFile } from 'fs/promises';
17
+
18
+ /**
19
+ * TestsManager is responsible for all operations related to test data management
20
+ * including loading, saving, merging, and updating test data.
21
+ * It extends EventEmitter to emit update events that can be subscribed to.
22
+ */
23
+ export class TestsManager extends EventEmitter {
24
+ private tests: Partial<Record<string, ServerTest>> = {};
25
+ private screenDir: string;
26
+ private reportDir: string;
27
+
28
+ /**
29
+ * Creates a new TestsManager instance
30
+ * @param screenDir Directory for storing reference images
31
+ * @param reportDir Directory for storing reports and screenshots
32
+ */
33
+ constructor(screenDir: string, reportDir: string) {
34
+ super();
35
+ this.screenDir = screenDir;
36
+ this.reportDir = reportDir;
37
+ }
38
+
39
+ /**
40
+ * Get a copy of all tests
41
+ * @returns all tests
42
+ */
43
+ public getTests(): Partial<Record<string, ServerTest>> {
44
+ return this.tests;
45
+ }
46
+
47
+ /**
48
+ * Get a test by ID
49
+ * @param id Test ID
50
+ * @returns Test data
51
+ */
52
+ public getTest(id: string): ServerTest | undefined {
53
+ return this.tests[id];
54
+ }
55
+
56
+ /**
57
+ * Get test data in a format suitable for status reporting
58
+ * @returns Test data in the format needed for status
59
+ */
60
+ public getTestsData(): CreeveyStatus['tests'] {
61
+ const testsData: CreeveyStatus['tests'] = {};
62
+
63
+ Object.entries(this.tests).forEach(([id, test]) => {
64
+ if (!test) return;
65
+
66
+ const { story: _, fn: __, ...testData } = test;
67
+ testsData[id] = testData;
68
+ });
69
+
70
+ return testsData;
71
+ }
72
+
73
+ /**
74
+ * Load tests from a report file
75
+ */
76
+ public loadTestsFromReport(): Partial<Record<string, ServerTest>> {
77
+ const reportDataPath = path.join(this.reportDir, 'data.js');
78
+ const testsFromReport = tryToLoadTestsData(reportDataPath) ?? {};
79
+ return testsFromReport;
80
+ }
81
+
82
+ /**
83
+ * Merge tests from report with tests from stories
84
+ */
85
+ private mergeTests(
86
+ testsWithReports: CreeveyStatus['tests'],
87
+ testsFromStories: Partial<Record<string, ServerTest>>,
88
+ ): Partial<Record<string, ServerTest>> {
89
+ Object.values(testsFromStories)
90
+ .filter(isDefined)
91
+ .forEach((test) => {
92
+ const testWithReport = testsWithReports[test.id];
93
+ if (!testWithReport) return;
94
+ test.retries = testWithReport.retries;
95
+ if (testWithReport.status === 'success' || testWithReport.status === 'failed') {
96
+ test.status = testWithReport.status;
97
+ }
98
+ test.results = testWithReport.results;
99
+ test.approved = testWithReport.approved;
100
+ });
101
+
102
+ return testsFromStories;
103
+ }
104
+
105
+ public loadAndMergeTests(testsFromStories: Partial<Record<string, ServerTest>>): Partial<Record<string, ServerTest>> {
106
+ const testsFromReport = this.loadTestsFromReport();
107
+
108
+ return this.mergeTests(testsFromReport, testsFromStories);
109
+ }
110
+
111
+ /**
112
+ * Update tests with incremental changes
113
+ * @param testsDiff Tests to update or remove
114
+ */
115
+ public updateTests(testsDiff: Partial<Record<string, ServerTest>>): CreeveyUpdate | null {
116
+ const tests: CreeveyUpdate['tests'] = {};
117
+ const removedTests: TestMeta[] = [];
118
+
119
+ Object.entries(testsDiff).forEach(([id, newTest]) => {
120
+ if (newTest) {
121
+ if (this.tests[id]) {
122
+ this.tests[id] = {
123
+ ...newTest,
124
+ retries: this.tests[id].retries,
125
+ results: this.tests[id].results,
126
+ approved: this.tests[id].approved,
127
+ };
128
+ } else {
129
+ this.tests[id] = newTest;
130
+ }
131
+
132
+ const { story: _, fn: __, ...restTest } = newTest;
133
+ tests[id] = { ...restTest, status: 'unknown' };
134
+ } else if (this.tests[id]) {
135
+ const { id: testId, browser, testName, storyPath, storyId } = this.tests[id];
136
+ removedTests.push({ id: testId, browser, testName, storyPath, storyId });
137
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
138
+ delete this.tests[id];
139
+ }
140
+ });
141
+
142
+ this.saveTestsToJson();
143
+
144
+ const update = { tests, removedTests };
145
+ this.emit('update', update);
146
+ return update;
147
+ }
148
+
149
+ /**
150
+ * Update test result
151
+ * @param id Test ID
152
+ * @param status New test status
153
+ * @param result Optional test result
154
+ */
155
+ public updateTestStatus(id: string, status: TestStatus, result?: TestResult): CreeveyUpdate | null {
156
+ // TODO Handle 'retrying' status
157
+ const test = this.tests[id];
158
+ if (!test) return null;
159
+
160
+ const { browser, testName, storyPath, storyId } = test;
161
+ test.status = status === 'retrying' ? 'failed' : status;
162
+
163
+ if (!result) {
164
+ // NOTE: Running status
165
+ const update = { tests: { [id]: { id, browser, testName, storyPath, status, storyId } } };
166
+ this.emit('update', update);
167
+ return update;
168
+ }
169
+
170
+ test.results ??= [];
171
+ test.results.push(result);
172
+
173
+ if (status === 'failed') {
174
+ test.approved = null;
175
+ }
176
+
177
+ const update = {
178
+ tests: {
179
+ [id]: {
180
+ id,
181
+ browser,
182
+ testName,
183
+ storyPath,
184
+ status,
185
+ approved: test.approved,
186
+ results: [result],
187
+ storyId,
188
+ },
189
+ },
190
+ };
191
+
192
+ this.emit('update', update);
193
+ return update;
194
+ }
195
+
196
+ /**
197
+ * Save tests to JSON file
198
+ * @param reportDir Directory to save the JSON file
199
+ */
200
+ public saveTestsToJson(): void {
201
+ mkdirSync(this.reportDir, { recursive: true });
202
+ writeFileSync(
203
+ path.join(this.reportDir, 'tests.json'),
204
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
205
+ JSON.stringify(this.tests, (_, value) => (isFunction(value) ? value.toString() : value), 2),
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Save test data to a module
211
+ * @param data Test data to include in the module
212
+ */
213
+ public async saveTestData(data: CreeveyStatus['tests'] = this.getTestsData()): Promise<void> {
214
+ const dataModule = `
215
+ (function (root, factory) {
216
+ if (typeof module === 'object' && module.exports) {
217
+ module.exports = factory();
218
+ } else {
219
+ root.__CREEVEY_DATA__ = factory();
220
+ }
221
+ }(this, function () { return ${JSON.stringify(data)} }));
222
+ `;
223
+ await writeFile(path.join(this.reportDir, 'data.js'), dataModule);
224
+ }
225
+
226
+ /**
227
+ * Copy image for approval
228
+ * @param test Test data
229
+ * @param image Image name
230
+ * @param actual Actual image path
231
+ */
232
+ private async copyImage(test: ServerTest, image: string, actual: string): Promise<void> {
233
+ const { browser, testName, storyPath } = test;
234
+ const restPath = [...storyPath, testName].filter(isDefined);
235
+ const testPath = path.join(...restPath, image == browser ? '' : browser);
236
+ const srcImagePath = path.join(this.reportDir, testPath, actual);
237
+ const dstImagePath = path.join(this.screenDir, testPath, `${image}.png`);
238
+ await mkdir(path.join(this.screenDir, testPath), { recursive: true });
239
+ await copyFile(srcImagePath, dstImagePath);
240
+ }
241
+
242
+ /**
243
+ * Approve a specific test
244
+ * @param payload Approval payload with test ID, retry index, and image name
245
+ */
246
+ public async approve({ id, retry, image }: ApprovePayload): Promise<CreeveyUpdate | null> {
247
+ const test = this.tests[id];
248
+ if (!test?.results) return null;
249
+ const result = test.results[retry];
250
+ if (!result.images) return null;
251
+ const images = result.images[image];
252
+ if (!images) return null;
253
+ test.approved ??= {};
254
+ const { browser, testName, storyPath, storyId } = test;
255
+
256
+ await this.copyImage(test, image, images.actual);
257
+
258
+ test.approved[image] = retry;
259
+
260
+ if (Object.keys(result.images).every((name) => typeof test.approved?.[name] == 'number')) {
261
+ test.status = 'approved';
262
+ }
263
+
264
+ const update = {
265
+ tests: {
266
+ [id]: {
267
+ id,
268
+ browser,
269
+ testName,
270
+ storyPath,
271
+ status: test.status,
272
+ approved: test.approved,
273
+ storyId,
274
+ },
275
+ },
276
+ };
277
+
278
+ this.emit('update', update);
279
+ return update;
280
+ }
281
+
282
+ /**
283
+ * Approve all failed tests
284
+ */
285
+ public async approveAll(): Promise<CreeveyUpdate> {
286
+ const updatedTests: NonNullable<CreeveyUpdate['tests']> = {};
287
+ for (const test of Object.values(this.tests)) {
288
+ if (!test?.results) continue;
289
+ const retry = test.results.length - 1;
290
+ const { images, status } = test.results.at(retry) ?? {};
291
+ if (!images || status != 'failed') continue;
292
+ for (const [name, image] of Object.entries(images)) {
293
+ if (!image) continue;
294
+ await this.copyImage(test, name, image.actual);
295
+
296
+ test.approved ??= {};
297
+ test.approved[name] = retry;
298
+ test.status = 'approved';
299
+
300
+ updatedTests[test.id] = {
301
+ id: test.id,
302
+ browser: test.browser,
303
+ storyPath: test.storyPath,
304
+ storyId: test.storyId,
305
+ status: test.status,
306
+ approved: { [name]: retry },
307
+ };
308
+ }
309
+ }
310
+
311
+ const result = { tests: updatedTests };
312
+ this.emit('update', result);
313
+ return result;
314
+ }
315
+ }
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'fs/promises';
2
2
  import { pathToFileURL } from 'url';
3
3
  import semver from 'semver';
4
- import { exec } from 'shelljs';
4
+ import sh from 'shelljs';
5
5
 
6
6
  const importMetaUrl = pathToFileURL(__filename).href;
7
7
 
@@ -11,7 +11,7 @@ export async function playwrightDockerFile(browser: string, version: string): Pr
11
11
 
12
12
  let npmRegistry;
13
13
  try {
14
- npmRegistry = exec('npm config get registry', { silent: true }).stdout.trim();
14
+ npmRegistry = sh.exec('npm config get registry', { silent: true }).stdout.trim();
15
15
  } catch {
16
16
  /* noop */
17
17
  }