creevey 0.10.0-beta.37 → 0.10.0-beta.38

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,13 @@ 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';
17
21
 
18
22
  export default class Runner extends EventEmitter {
19
23
  private failFast: boolean;
@@ -22,6 +26,8 @@ export default class Runner extends EventEmitter {
22
26
  private browsers: string[];
23
27
  private scheduler: WorkerQueue;
24
28
  private pools: Record<string, Pool> = {};
29
+ private fakeRunner: EventEmitter;
30
+ private config: Config;
25
31
  tests: Partial<Record<string, ServerTest>> = {};
26
32
  public get isRunning(): boolean {
27
33
  return Object.values(this.pools).some((pool) => pool.isRunning);
@@ -29,11 +35,19 @@ export default class Runner extends EventEmitter {
29
35
  constructor(config: Config, gridUrl?: string) {
30
36
  super();
31
37
 
38
+ this.config = config;
32
39
  this.failFast = config.failFast;
33
40
  this.screenDir = config.screenDir;
34
41
  this.reportDir = config.reportDir;
35
42
  this.scheduler = new WorkerQueue(config.useWorkerQueue);
36
43
  this.browsers = Object.keys(config.browsers);
44
+
45
+ class FakeRunner extends EventEmitter {}
46
+ const runner = new FakeRunner();
47
+ const Reporter = config.reporter;
48
+ new Reporter(runner, { reporterOptions: config.reporterOptions });
49
+ this.fakeRunner = runner;
50
+
37
51
  this.browsers
38
52
  .map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
39
53
  .map((pool) => pool.on('test', this.handlePoolMessage));
@@ -45,10 +59,38 @@ export default class Runner extends EventEmitter {
45
59
 
46
60
  if (!test) return;
47
61
  const { browser, testName, storyPath, storyId } = test;
62
+
63
+ const fakeSuite: FakeSuite = {
64
+ title: test.storyPath.slice(0, -1).join('/'),
65
+ fullTitle: () => fakeSuite.title,
66
+ titlePath: () => [fakeSuite.title],
67
+ tests: [],
68
+ };
69
+
70
+ const fakeTest: FakeTest = {
71
+ parent: fakeSuite,
72
+ title: [test.story.name, testName, browser].filter(isDefined).join('/'),
73
+ fullTitle: () => getTestPath(test).join('/'),
74
+ titlePath: () => getTestPath(test),
75
+ currentRetry: () => result?.retries,
76
+ retires: () => this.config.maxRetries,
77
+ slow: () => 1000,
78
+ creevey: {
79
+ reportDir: this.reportDir,
80
+ sessionId: id, // TODO SessionId
81
+ browserName: browser,
82
+ willRetry: (result?.retries ?? 0) < this.config.maxRetries,
83
+ images: result?.images ?? {},
84
+ },
85
+ };
86
+
87
+ fakeSuite.tests.push(fakeTest);
88
+
48
89
  // TODO Handle 'retrying' status
49
90
  test.status = status == 'retrying' ? 'failed' : status;
50
91
  if (!result) {
51
92
  // NOTE: Running status
93
+ this.fakeRunner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
52
94
  this.sendUpdate({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
53
95
  return;
54
96
  }
@@ -59,6 +101,24 @@ export default class Runner extends EventEmitter {
59
101
  test.approved = null;
60
102
  }
61
103
 
104
+ const { duration, attachments } = result;
105
+
106
+ fakeTest.duration = duration;
107
+ fakeTest.attachments = attachments;
108
+ fakeTest.state = result.status === 'failed' ? 'failed' : 'passed';
109
+ if (duration !== undefined) {
110
+ fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
111
+ }
112
+
113
+ if (result.status === 'failed') {
114
+ fakeTest.err = result.error;
115
+ this.fakeRunner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, result.error);
116
+ } else {
117
+ this.fakeRunner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
118
+ }
119
+
120
+ this.fakeRunner.emit(TEST_EVENTS.TEST_END, fakeTest);
121
+
62
122
  this.sendUpdate({
63
123
  tests: {
64
124
  [id]: {
@@ -79,6 +139,7 @@ export default class Runner extends EventEmitter {
79
139
 
80
140
  private handlePoolStop = (): void => {
81
141
  if (!this.isRunning) {
142
+ this.fakeRunner.emit(TEST_EVENTS.RUN_END);
82
143
  this.sendUpdate({ isRunning: false });
83
144
  this.emit('stop');
84
145
  }
@@ -148,6 +209,7 @@ export default class Runner extends EventEmitter {
148
209
  };
149
210
  }, {});
150
211
 
212
+ this.fakeRunner.emit(TEST_EVENTS.RUN_BEGIN);
151
213
  this.browsers.forEach((browser) => {
152
214
  const pool = this.pools[browser];
153
215
  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 {
@@ -494,7 +498,7 @@ export interface FakeTest {
494
498
  title: string;
495
499
  fullTitle: () => string;
496
500
  titlePath: () => string[];
497
- currentRetry: () => number;
501
+ currentRetry: () => number | undefined;
498
502
  retires: () => number;
499
503
  slow: () => number;
500
504
  duration?: number;
@@ -504,6 +508,15 @@ export interface FakeTest {
504
508
  err?: unknown;
505
509
  // NOTE: image files
506
510
  attachments?: string[];
511
+
512
+ // NOTE: Creevey specific fields
513
+ creevey: {
514
+ reportDir: string;
515
+ sessionId: string;
516
+ browserName: string;
517
+ willRetry: boolean;
518
+ images: Partial<Record<string, Partial<Images>>>;
519
+ };
507
520
  }
508
521
 
509
522
  export interface CreeveyStatus {