creevey 0.10.0-beta.37 → 0.10.0-beta.39

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.
@@ -11,9 +11,16 @@ import {
11
11
  TestStatus,
12
12
  ServerTest,
13
13
  TestMeta,
14
+ TEST_EVENTS,
15
+ FakeSuite,
16
+ FakeTest,
14
17
  } from '../../types.js';
15
18
  import Pool from './pool.js';
16
19
  import { WorkerQueue } from './queue.js';
20
+ import { getTestPath } from '../utils.js';
21
+
22
+ // NOTE: This is workaround to fix parallel tests running with mocha-junit-reporter
23
+ let isJUnit = false;
17
24
 
18
25
  export default class Runner extends EventEmitter {
19
26
  private failFast: boolean;
@@ -22,6 +29,8 @@ export default class Runner extends EventEmitter {
22
29
  private browsers: string[];
23
30
  private scheduler: WorkerQueue;
24
31
  private pools: Record<string, Pool> = {};
32
+ private fakeRunner: EventEmitter;
33
+ private config: Config;
25
34
  tests: Partial<Record<string, ServerTest>> = {};
26
35
  public get isRunning(): boolean {
27
36
  return Object.values(this.pools).some((pool) => pool.isRunning);
@@ -29,11 +38,24 @@ export default class Runner extends EventEmitter {
29
38
  constructor(config: Config, gridUrl?: string) {
30
39
  super();
31
40
 
41
+ this.config = config;
32
42
  this.failFast = config.failFast;
33
43
  this.screenDir = config.screenDir;
34
44
  this.reportDir = config.reportDir;
35
45
  this.scheduler = new WorkerQueue(config.useWorkerQueue);
36
46
  this.browsers = Object.keys(config.browsers);
47
+
48
+ class FakeRunner extends EventEmitter {}
49
+ const runner = new FakeRunner();
50
+ const Reporter = config.reporter;
51
+
52
+ if (Reporter.name == 'MochaJUnitReporter') {
53
+ isJUnit = true;
54
+ }
55
+
56
+ new Reporter(runner, { reporterOptions: config.reporterOptions });
57
+ this.fakeRunner = runner;
58
+
37
59
  this.browsers
38
60
  .map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
39
61
  .map((pool) => pool.on('test', this.handlePoolMessage));
@@ -45,10 +67,38 @@ export default class Runner extends EventEmitter {
45
67
 
46
68
  if (!test) return;
47
69
  const { browser, testName, storyPath, storyId } = test;
70
+
71
+ const fakeSuite: FakeSuite = {
72
+ title: test.storyPath.slice(0, -1).join('/'),
73
+ fullTitle: () => fakeSuite.title,
74
+ titlePath: () => [fakeSuite.title],
75
+ tests: [],
76
+ };
77
+
78
+ const fakeTest: FakeTest = {
79
+ parent: fakeSuite,
80
+ title: [test.story.name, testName, browser].filter(isDefined).join('/'),
81
+ fullTitle: () => getTestPath(test).join('/'),
82
+ titlePath: () => getTestPath(test),
83
+ currentRetry: () => result?.retries,
84
+ retires: () => this.config.maxRetries,
85
+ slow: () => 1000,
86
+ creevey: {
87
+ reportDir: this.reportDir,
88
+ sessionId: id, // TODO SessionId
89
+ browserName: browser,
90
+ willRetry: (result?.retries ?? 0) < this.config.maxRetries,
91
+ images: result?.images ?? {},
92
+ },
93
+ };
94
+
95
+ fakeSuite.tests.push(fakeTest);
96
+
48
97
  // TODO Handle 'retrying' status
49
98
  test.status = status == 'retrying' ? 'failed' : status;
50
99
  if (!result) {
51
100
  // NOTE: Running status
101
+ this.fakeRunner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
52
102
  this.sendUpdate({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
53
103
  return;
54
104
  }
@@ -59,6 +109,32 @@ export default class Runner extends EventEmitter {
59
109
  test.approved = null;
60
110
  }
61
111
 
112
+ const { duration, attachments } = result;
113
+
114
+ fakeTest.duration = duration;
115
+ fakeTest.attachments = attachments;
116
+ fakeTest.state = result.status === 'failed' ? 'failed' : 'passed';
117
+ if (duration !== undefined) {
118
+ fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
119
+ }
120
+
121
+ if (isJUnit) {
122
+ this.fakeRunner.emit(TEST_EVENTS.SUITE_BEGIN, fakeSuite);
123
+ }
124
+
125
+ if (result.status === 'failed') {
126
+ fakeTest.err = result.error;
127
+ this.fakeRunner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, result.error);
128
+ } else {
129
+ this.fakeRunner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
130
+ }
131
+
132
+ if (isJUnit) {
133
+ this.fakeRunner.emit(TEST_EVENTS.SUITE_END, fakeSuite);
134
+ }
135
+
136
+ this.fakeRunner.emit(TEST_EVENTS.TEST_END, fakeTest);
137
+
62
138
  this.sendUpdate({
63
139
  tests: {
64
140
  [id]: {
@@ -79,6 +155,7 @@ export default class Runner extends EventEmitter {
79
155
 
80
156
  private handlePoolStop = (): void => {
81
157
  if (!this.isRunning) {
158
+ this.fakeRunner.emit(TEST_EVENTS.RUN_END);
82
159
  this.sendUpdate({ isRunning: false });
83
160
  this.emit('stop');
84
161
  }
@@ -148,6 +225,7 @@ export default class Runner extends EventEmitter {
148
225
  };
149
226
  }, {});
150
227
 
228
+ this.fakeRunner.emit(TEST_EVENTS.RUN_BEGIN);
151
229
  this.browsers.forEach((browser) => {
152
230
  const pool = this.pools[browser];
153
231
  const tests = testsByBrowser[browser];
@@ -4,14 +4,6 @@ import prefix from 'loglevel-plugin-prefix';
4
4
  import { FakeTest, Images, isDefined, isImageError, TEST_EVENTS } from '../types.js';
5
5
  import EventEmitter from 'events';
6
6
 
7
- interface ReporterOptions {
8
- reportDir: string;
9
- sessionId: string;
10
- browserName: string;
11
- willRetry: boolean;
12
- images: Partial<Record<string, Partial<Images>>>;
13
- }
14
-
15
7
  const testLevels: Record<string, string> = {
16
8
  INFO: chalk.green('PASS'),
17
9
  WARN: chalk.yellow('START'),
@@ -19,36 +11,43 @@ const testLevels: Record<string, string> = {
19
11
  };
20
12
 
21
13
  export class CreeveyReporter {
14
+ private logger: Logger.Logger | null = null;
22
15
  // TODO Output in better way, like vitest, maybe
23
- constructor(runner: EventEmitter, options: { reporterOptions: { creevey: ReporterOptions } }) {
24
- const { sessionId, browserName } = options.reporterOptions.creevey;
25
- const testLogger = Logger.getLogger(sessionId);
26
-
27
- prefix.apply(testLogger, {
28
- format(level) {
29
- return `[${browserName}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
30
- },
31
- });
32
-
16
+ constructor(runner: EventEmitter) {
33
17
  runner.on(TEST_EVENTS.TEST_BEGIN, (test: FakeTest) => {
34
- testLogger.warn(chalk.cyan(test.fullTitle()));
18
+ this.getLogger(test.creevey).warn(chalk.cyan(test.fullTitle()));
35
19
  });
36
20
  runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
37
- testLogger.info(chalk.cyan(test.fullTitle()), chalk.gray(`(${test.duration} ms)`));
21
+ this.getLogger(test.creevey).info(chalk.cyan(test.fullTitle()), chalk.gray(`(${test.duration} ms)`));
38
22
  });
39
23
  runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error) => {
40
- testLogger.error(
24
+ this.getLogger(test.creevey).error(
41
25
  chalk.cyan(test.fullTitle()),
42
26
  chalk.gray(`(${test.duration} ms)`),
43
27
  '\n ',
44
28
  this.getErrors(
45
29
  error,
46
- (error, imageName) => `${chalk.bold(imageName ?? browserName)}:${error}`,
30
+ (error, imageName) => `${chalk.bold(imageName ?? test.creevey.browserName)}:${error}`,
47
31
  (error) => error.stack ?? error.message,
48
32
  ).join('\n '),
49
33
  );
50
34
  });
51
35
  }
36
+
37
+ private getLogger(options: { sessionId: string; browserName: string }) {
38
+ if (this.logger) return this.logger;
39
+ const { sessionId, browserName } = options;
40
+ const testLogger = Logger.getLogger(sessionId);
41
+
42
+ this.logger = prefix.apply(testLogger, {
43
+ format(level) {
44
+ return `[${browserName}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
45
+ },
46
+ });
47
+
48
+ return this.logger;
49
+ }
50
+
52
51
  private getErrors(
53
52
  error: unknown,
54
53
  imageErrorToString: (error: string, imageName?: string) => string,
@@ -72,10 +71,7 @@ export class CreeveyReporter {
72
71
  }
73
72
 
74
73
  export class TeamcityReporter {
75
- constructor(runner: EventEmitter, options: { reporterOptions: { creevey: ReporterOptions } }) {
76
- const browserName = this.escape(options.reporterOptions.creevey.browserName);
77
- const reporterOptions = options.reporterOptions.creevey;
78
-
74
+ constructor(runner: EventEmitter) {
79
75
  runner.on(TEST_EVENTS.TEST_BEGIN, (test: FakeTest) => {
80
76
  console.log(`##teamcity[testStarted name='${this.escape(test.fullTitle())}' flowId='${process.pid}']`);
81
77
  });
@@ -85,7 +81,8 @@ export class TeamcityReporter {
85
81
  });
86
82
 
87
83
  runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error: Error) => {
88
- Object.entries(reporterOptions.images).forEach(([name, image]) => {
84
+ const browserName = this.escape(test.creevey.browserName);
85
+ Object.entries(test.creevey.images).forEach(([name, image]) => {
89
86
  if (!image) return;
90
87
  const filePath = test
91
88
  .titlePath()
@@ -99,7 +96,7 @@ export class TeamcityReporter {
99
96
  .filter(isDefined)
100
97
  .forEach((fileName) => {
101
98
  console.log(
102
- `##teamcity[publishArtifacts '${reporterOptions.reportDir}/${filePath}/${fileName} => report/${filePath}']`,
99
+ `##teamcity[publishArtifacts '${test.creevey.reportDir}/${filePath}/${fileName} => report/${filePath}']`,
103
100
  );
104
101
  console.log(
105
102
  `##teamcity[testMetadata testName='${this.escape(
@@ -112,7 +109,7 @@ export class TeamcityReporter {
112
109
  // Output failed test as passed due TC don't support retry mechanic
113
110
  // https://teamcity-support.jetbrains.com/hc/en-us/community/posts/207216829-Count-test-as-successful-if-at-least-one-try-is-successful?page=1#community_comment_207394125
114
111
 
115
- if (reporterOptions.willRetry)
112
+ if (test.creevey.willRetry)
116
113
  console.log(`##teamcity[testFinished name='${this.escape(test.fullTitle())}' flowId='${process.pid}']`);
117
114
  else
118
115
  console.log(
@@ -1,16 +1,12 @@
1
1
  import chai from 'chai';
2
- import EventEmitter from 'events';
3
2
  import {
4
3
  BaseCreeveyTestContext,
5
4
  Config,
6
5
  CreeveyWebdriver,
7
- FakeSuite,
8
- FakeTest,
9
- Images,
10
6
  Options,
11
7
  ServerTest,
12
- TEST_EVENTS,
13
8
  TestMessage,
9
+ TestResult,
14
10
  isDefined,
15
11
  isImageError,
16
12
  } from '../../types.js';
@@ -45,9 +41,10 @@ async function getTestsFromStories(
45
41
  return testsById;
46
42
  }
47
43
 
48
- function runHandler(browserName: string, images: Partial<Record<string, Images>>, error?: unknown): void {
44
+ function runHandler(browserName: string, result: Omit<TestResult, 'status'>, error?: unknown): void {
49
45
  // TODO How handle browser corruption?
50
- if (isImageError(error)) {
46
+ const { images } = result;
47
+ if (images != null && isImageError(error)) {
51
48
  if (typeof error.images == 'string') {
52
49
  const image = images[browserName];
53
50
  if (image) image.error = error.images;
@@ -60,25 +57,31 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
60
57
  }
61
58
  }
62
59
 
63
- if (error || Object.values(images).some((image) => image?.error != null)) {
64
- const errorMessage = serializeError(error);
60
+ if (error || (images != null && Object.values(images).some((image) => image?.error != null))) {
61
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62
+ const errorMessage = result.error!;
65
63
 
66
64
  const isUnexpectedError =
67
65
  hasTimeout(errorMessage) ||
68
66
  hasDisconnected(errorMessage) ||
69
- Object.values(images).some((image) => hasTimeout(image?.error));
67
+ (images != null && Object.values(images).some((image) => hasTimeout(image?.error)));
70
68
  if (isUnexpectedError) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error: errorMessage } });
71
69
  else
72
70
  emitTestMessage({
73
71
  type: 'end',
74
72
  payload: {
75
73
  status: 'failed',
76
- images,
77
- error: errorMessage,
74
+ ...result,
78
75
  },
79
76
  });
80
77
  } else {
81
- emitTestMessage({ type: 'end', payload: { status: 'success', images } });
78
+ emitTestMessage({
79
+ type: 'end',
80
+ payload: {
81
+ status: 'success',
82
+ ...result,
83
+ },
84
+ });
82
85
  }
83
86
  }
84
87
 
@@ -112,7 +115,6 @@ function hasTimeout(str: string | null | undefined): boolean {
112
115
  }
113
116
 
114
117
  export async function start(browser: string, gridUrl: string, config: Config, options: Options): Promise<void> {
115
- let retries = 0;
116
118
  const imagesContext: ImageContext = {
117
119
  attachments: [],
118
120
  testFullPath: [],
@@ -123,26 +125,6 @@ export async function start(browser: string, gridUrl: string, config: Config, op
123
125
 
124
126
  if (!webdriver || !sessionId) return;
125
127
 
126
- const reporterOptions = {
127
- ...config.reporterOptions,
128
- creevey: {
129
- sessionId,
130
- reportDir: config.reportDir,
131
- browserName: browser,
132
- get willRetry() {
133
- return retries < config.maxRetries;
134
- },
135
- get images() {
136
- return imagesContext.images;
137
- },
138
- },
139
- };
140
-
141
- class FakeRunner extends EventEmitter {}
142
- const runner = new FakeRunner();
143
- const Reporter = config.reporter;
144
- new Reporter(runner, { reporterOptions });
145
-
146
128
  const { matchImage, matchImages } = options.odiff
147
129
  ? getOdiffMatchers(imagesContext, config)
148
130
  : await getMatchers(imagesContext, config);
@@ -196,32 +178,9 @@ export async function start(browser: string, gridUrl: string, config: Config, op
196
178
  imagesContext.testFullPath = getTestPath(test);
197
179
  imagesContext.images = {};
198
180
 
199
- retries = message.payload.retries;
200
181
  let error = undefined;
201
182
 
202
- const fakeSuite: FakeSuite = {
203
- title: test.storyPath.slice(0, -1).join('/'),
204
- fullTitle: () => fakeSuite.title,
205
- titlePath: () => [fakeSuite.title],
206
- tests: [],
207
- };
208
-
209
- const fakeTest: FakeTest = {
210
- parent: fakeSuite,
211
- title: [test.story.name, test.testName, test.browser].filter(isDefined).join('/'),
212
- fullTitle: () => getTestPath(test).join('/'),
213
- titlePath: () => getTestPath(test),
214
- currentRetry: () => retries,
215
- retires: () => config.maxRetries,
216
- slow: () => 1000,
217
- };
218
-
219
- fakeSuite.tests.push(fakeTest);
220
-
221
183
  void (async () => {
222
- runner.emit(TEST_EVENTS.RUN_BEGIN);
223
- runner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
224
-
225
184
  let timeout;
226
185
  let isRejected = false;
227
186
  const start = Date.now();
@@ -241,22 +200,9 @@ export async function start(browser: string, gridUrl: string, config: Config, op
241
200
  ]);
242
201
  } catch (testError) {
243
202
  error = testError;
244
- fakeTest.err = error;
245
203
  }
246
204
  const duration = Date.now() - start;
247
205
  clearTimeout(timeout);
248
- fakeTest.attachments = imagesContext.attachments;
249
- fakeTest.state = error ? 'failed' : 'passed';
250
- fakeTest.duration = duration;
251
- fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
252
-
253
- if (error) {
254
- runner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, error);
255
- } else {
256
- runner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
257
- }
258
- runner.emit(TEST_EVENTS.TEST_END, fakeTest);
259
- runner.emit(TEST_EVENTS.RUN_END);
260
206
 
261
207
  await webdriver.afterTest(test);
262
208
 
@@ -267,7 +213,14 @@ export async function start(browser: string, gridUrl: string, config: Config, op
267
213
  payload: { subtype: 'unknown', error: serializeError(error) },
268
214
  });
269
215
  } else {
270
- runHandler(baseContext.browserName, imagesContext.images, error);
216
+ const result = {
217
+ images: imagesContext.images,
218
+ error: serializeError(error),
219
+ duration,
220
+ attachments: imagesContext.attachments,
221
+ retries: message.payload.retries,
222
+ };
223
+ runHandler(baseContext.browserName, result, error);
271
224
  }
272
225
  })().catch((error: unknown) => {
273
226
  logger().error('Unexpected error:', error);
package/src/types.ts CHANGED
@@ -419,10 +419,14 @@ export type TestStatus = 'unknown' | 'pending' | 'running' | 'failed' | 'approve
419
419
 
420
420
  export interface TestResult {
421
421
  status: 'failed' | 'success';
422
+ retries: number;
422
423
  // TODO Remove checks `name == browser` in TestResultsView
423
424
  // images?: Partial<{ [name: string]: Images }> | Images;
424
425
  images?: Partial<Record<string, Images>>;
425
426
  error?: string;
427
+ // Test metadata for reporting
428
+ duration?: number;
429
+ attachments?: string[];
426
430
  }
427
431
 
428
432
  export class ImagesError extends Error {
@@ -470,6 +474,8 @@ export interface CreeveyTestContext extends BaseCreeveyTestContext {
470
474
  export enum TEST_EVENTS {
471
475
  RUN_BEGIN = 'start',
472
476
  RUN_END = 'end',
477
+ SUITE_BEGIN = 'suite',
478
+ SUITE_END = 'suite end',
473
479
  TEST_BEGIN = 'test',
474
480
  TEST_END = 'test end',
475
481
  TEST_FAIL = 'fail',
@@ -494,7 +500,7 @@ export interface FakeTest {
494
500
  title: string;
495
501
  fullTitle: () => string;
496
502
  titlePath: () => string[];
497
- currentRetry: () => number;
503
+ currentRetry: () => number | undefined;
498
504
  retires: () => number;
499
505
  slow: () => number;
500
506
  duration?: number;
@@ -504,6 +510,15 @@ export interface FakeTest {
504
510
  err?: unknown;
505
511
  // NOTE: image files
506
512
  attachments?: string[];
513
+
514
+ // NOTE: Creevey specific fields
515
+ creevey: {
516
+ reportDir: string;
517
+ sessionId: string;
518
+ browserName: string;
519
+ willRetry: boolean;
520
+ images: Partial<Record<string, Partial<Images>>>;
521
+ };
507
522
  }
508
523
 
509
524
  export interface CreeveyStatus {