creevey 0.10.0-beta.1 → 0.10.0-beta.11

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/dist/client/addon/components/Panel.js +2 -2
  2. package/dist/client/addon/components/Panel.js.map +1 -1
  3. package/dist/client/addon/controller.js +4 -5
  4. package/dist/client/addon/controller.js.map +1 -1
  5. package/dist/client/addon/withCreevey.js +18 -34
  6. package/dist/client/addon/withCreevey.js.map +1 -1
  7. package/dist/client/shared/components/ImagesView/SwapView.js +12 -0
  8. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  9. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  10. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  11. package/dist/client/shared/components/ResultsPage.js +23 -5
  12. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  13. package/dist/client/web/CreeveyApp.js +22 -6
  14. package/dist/client/web/CreeveyApp.js.map +1 -1
  15. package/dist/client/web/CreeveyContext.d.ts +5 -0
  16. package/dist/client/web/CreeveyContext.js +3 -0
  17. package/dist/client/web/CreeveyContext.js.map +1 -1
  18. package/dist/client/web/CreeveyView/SideBar/Search.js +2 -2
  19. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  20. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -0
  21. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +49 -6
  23. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +1 -3
  25. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  26. package/dist/client/web/CreeveyView/SideBar/TestLink.js +1 -3
  27. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  28. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  29. package/dist/client/web/KeyboardEventsContext.js +62 -57
  30. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  31. package/dist/client/web/assets/{index-DkmZfG9C.js → index-BE9CL5_G.js} +94 -94
  32. package/dist/client/web/index.html +1 -1
  33. package/dist/creevey.js +13 -5
  34. package/dist/creevey.js.map +1 -1
  35. package/dist/server/config.js +4 -3
  36. package/dist/server/config.js.map +1 -1
  37. package/dist/server/docker.js +2 -2
  38. package/dist/server/docker.js.map +1 -1
  39. package/dist/server/index.js +29 -3
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/logger.d.ts +2 -1
  42. package/dist/server/logger.js +7 -3
  43. package/dist/server/logger.js.map +1 -1
  44. package/dist/server/master/api.js +1 -1
  45. package/dist/server/master/api.js.map +1 -1
  46. package/dist/server/master/pool.d.ts +3 -3
  47. package/dist/server/master/pool.js +10 -63
  48. package/dist/server/master/pool.js.map +1 -1
  49. package/dist/server/master/queue.d.ts +13 -0
  50. package/dist/server/master/queue.js +64 -0
  51. package/dist/server/master/queue.js.map +1 -0
  52. package/dist/server/master/runner.d.ts +1 -0
  53. package/dist/server/master/runner.js +4 -1
  54. package/dist/server/master/runner.js.map +1 -1
  55. package/dist/server/master/server.js +1 -1
  56. package/dist/server/master/server.js.map +1 -1
  57. package/dist/server/master/start.js +4 -4
  58. package/dist/server/master/start.js.map +1 -1
  59. package/dist/server/playwright/docker-file.js +12 -2
  60. package/dist/server/playwright/docker-file.js.map +1 -1
  61. package/dist/server/playwright/internal.d.ts +2 -2
  62. package/dist/server/playwright/internal.js +56 -44
  63. package/dist/server/playwright/internal.js.map +1 -1
  64. package/dist/server/playwright/webdriver.js +1 -1
  65. package/dist/server/playwright/webdriver.js.map +1 -1
  66. package/dist/server/providers/browser.js +2 -1
  67. package/dist/server/providers/browser.js.map +1 -1
  68. package/dist/server/providers/hybrid.js +1 -1
  69. package/dist/server/providers/hybrid.js.map +1 -1
  70. package/dist/server/reporter.js +8 -4
  71. package/dist/server/reporter.js.map +1 -1
  72. package/dist/server/selenium/internal.d.ts +2 -3
  73. package/dist/server/selenium/internal.js +116 -90
  74. package/dist/server/selenium/internal.js.map +1 -1
  75. package/dist/server/selenium/selenoid.js +2 -2
  76. package/dist/server/selenium/selenoid.js.map +1 -1
  77. package/dist/server/selenium/webdriver.js +1 -1
  78. package/dist/server/selenium/webdriver.js.map +1 -1
  79. package/dist/server/telemetry.js +7 -3
  80. package/dist/server/telemetry.js.map +1 -1
  81. package/dist/server/utils.d.ts +2 -1
  82. package/dist/server/utils.js +13 -3
  83. package/dist/server/utils.js.map +1 -1
  84. package/dist/server/webdriver.d.ts +2 -3
  85. package/dist/server/webdriver.js +10 -9
  86. package/dist/server/webdriver.js.map +1 -1
  87. package/dist/server/worker/chai-image.d.ts +1 -2
  88. package/dist/server/worker/chai-image.js +4 -3
  89. package/dist/server/worker/chai-image.js.map +1 -1
  90. package/dist/server/worker/start.js +24 -14
  91. package/dist/server/worker/start.js.map +1 -1
  92. package/dist/types.d.ts +30 -11
  93. package/dist/types.js +13 -1
  94. package/dist/types.js.map +1 -1
  95. package/package.json +36 -42
  96. package/src/client/addon/components/Panel.tsx +2 -2
  97. package/src/client/addon/controller.ts +13 -6
  98. package/src/client/addon/withCreevey.ts +25 -13
  99. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  100. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  101. package/src/client/shared/components/ResultsPage.tsx +28 -7
  102. package/src/client/web/CreeveyApp.tsx +25 -7
  103. package/src/client/web/CreeveyContext.tsx +9 -0
  104. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  105. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  106. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  107. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  108. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  109. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  110. package/src/creevey.ts +13 -6
  111. package/src/server/config.ts +4 -3
  112. package/src/server/docker.ts +2 -2
  113. package/src/server/index.ts +27 -4
  114. package/src/server/logger.ts +6 -2
  115. package/src/server/master/api.ts +1 -1
  116. package/src/server/master/pool.ts +18 -56
  117. package/src/server/master/queue.ts +64 -0
  118. package/src/server/master/runner.ts +4 -1
  119. package/src/server/master/server.ts +1 -1
  120. package/src/server/master/start.ts +7 -4
  121. package/src/server/playwright/docker-file.ts +14 -2
  122. package/src/server/playwright/internal.ts +76 -49
  123. package/src/server/playwright/webdriver.ts +1 -1
  124. package/src/server/providers/browser.ts +2 -1
  125. package/src/server/providers/hybrid.ts +1 -1
  126. package/src/server/reporter.ts +9 -3
  127. package/src/server/selenium/internal.ts +119 -92
  128. package/src/server/selenium/selenoid.ts +2 -2
  129. package/src/server/selenium/webdriver.ts +1 -1
  130. package/src/server/telemetry.ts +7 -3
  131. package/src/server/utils.ts +14 -4
  132. package/src/server/webdriver.ts +10 -15
  133. package/src/server/worker/chai-image.ts +4 -4
  134. package/src/server/worker/start.ts +25 -16
  135. package/src/types.ts +32 -13
  136. package/.yarnrc.yml +0 -1
  137. package/chromatic.config.json +0 -5
@@ -1,18 +1,9 @@
1
- import cluster, { Worker as ClusterWorker } from 'cluster';
1
+ import { Worker as ClusterWorker } from 'cluster';
2
2
  import { EventEmitter } from 'events';
3
- import {
4
- Worker,
5
- Config,
6
- TestResult,
7
- BrowserConfigObject,
8
- WorkerMessage,
9
- TestStatus,
10
- isWorkerMessage,
11
- } from '../../types.js';
12
- import { sendTestMessage, sendShutdownMessage, subscribeOnWorker } from '../messages.js';
13
- import { isShuttingDown } from '../utils.js';
14
-
15
- const FORK_RETRIES = 5;
3
+ import { Worker, Config, TestResult, BrowserConfigObject, TestStatus } from '../../types.js';
4
+ import { sendTestMessage, subscribeOnWorker } from '../messages.js';
5
+ import { gracefullyKill, isShuttingDown } from '../utils.js';
6
+ import { WorkerQueue } from './queue.js';
16
7
 
17
8
  interface WorkerTest {
18
9
  id: string;
@@ -32,6 +23,7 @@ export default class Pool extends EventEmitter {
32
23
  return this.workers.length !== this.freeWorkers.length;
33
24
  }
34
25
  constructor(
26
+ public scheduler: WorkerQueue,
35
27
  config: Config,
36
28
  private browser: string,
37
29
  gridUrl?: string,
@@ -46,10 +38,11 @@ export default class Pool extends EventEmitter {
46
38
 
47
39
  async init(): Promise<void> {
48
40
  const poolSize = Math.max(1, this.config.limit ?? 1);
49
- // TODO Init queue for workers to smooth browser starting load
50
- this.workers = (await Promise.all(Array.from({ length: poolSize }).map(() => this.forkWorker()))).filter(
51
- (workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker,
52
- );
41
+ this.workers = (
42
+ await Promise.all(
43
+ Array.from({ length: poolSize }).map(() => this.scheduler.forkWorker(this.browser, this.gridUrl)),
44
+ )
45
+ ).filter((workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker);
53
46
  if (this.workers.length != poolSize)
54
47
  throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
55
48
  this.workers.forEach((worker) => {
@@ -66,7 +59,7 @@ export default class Pool extends EventEmitter {
66
59
  return true;
67
60
  }
68
61
 
69
- stop(): void {
62
+ stop() {
70
63
  if (!this.isRunning) {
71
64
  this.emit('stop');
72
65
  return;
@@ -76,7 +69,7 @@ export default class Pool extends EventEmitter {
76
69
  this.queue = [];
77
70
  }
78
71
 
79
- process(): void {
72
+ process() {
80
73
  const worker = this.getFreeWorker();
81
74
  const test = this.queue.at(0);
82
75
 
@@ -99,7 +92,9 @@ export default class Pool extends EventEmitter {
99
92
 
100
93
  sendTestMessage(worker, { type: 'start', payload: test });
101
94
 
102
- this.process();
95
+ setImmediate(() => {
96
+ this.process();
97
+ });
103
98
  }
104
99
 
105
100
  private sendStatus(message: { id: string; status: TestStatus; result?: TestResult }): void {
@@ -120,34 +115,12 @@ export default class Pool extends EventEmitter {
120
115
  return this.aliveWorkers.filter((worker) => !worker.isRunning);
121
116
  }
122
117
 
123
- private async forkWorker(retry = 0): Promise<Worker | { error: string }> {
124
- cluster.setupPrimary({
125
- args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
126
- });
127
- const worker = cluster.fork();
128
- const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
129
- const readyHandler = (message: unknown): void => {
130
- if (!isWorkerMessage(message) || message.type == 'port') return;
131
- worker.off('message', readyHandler);
132
- resolve(message);
133
- };
134
- worker.on('message', readyHandler);
135
- });
136
-
137
- if (message.type != 'error') return worker;
138
-
139
- this.gracefullyKill(worker);
140
-
141
- if (retry == FORK_RETRIES) return message.payload;
142
- return this.forkWorker(retry + 1);
143
- }
144
-
145
118
  private exitHandler(worker: Worker): void {
146
119
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
147
120
  worker.once('exit', async () => {
148
121
  if (isShuttingDown.current) return;
149
122
 
150
- const workerOrError = await this.forkWorker();
123
+ const workerOrError = await this.scheduler.forkWorker(this.browser, this.gridUrl);
151
124
 
152
125
  if (!(workerOrError instanceof ClusterWorker))
153
126
  throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
@@ -158,17 +131,6 @@ export default class Pool extends EventEmitter {
158
131
  });
159
132
  }
160
133
 
161
- private gracefullyKill(worker: Worker): void {
162
- worker.isShuttingDown = true;
163
- const timeout = setTimeout(() => {
164
- worker.kill();
165
- }, 10000);
166
- worker.on('exit', () => {
167
- clearTimeout(timeout);
168
- });
169
- sendShutdownMessage(worker);
170
- }
171
-
172
134
  private shouldRetry(test: WorkerTest): boolean {
173
135
  return test.retries < this.maxRetries && !this.forcedStop;
174
136
  }
@@ -200,7 +162,7 @@ export default class Pool extends EventEmitter {
200
162
  });
201
163
 
202
164
  if (message.payload.subtype == 'unknown') {
203
- this.gracefullyKill(worker);
165
+ gracefullyKill(worker);
204
166
  }
205
167
 
206
168
  this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
@@ -0,0 +1,64 @@
1
+ import cluster from 'cluster';
2
+ import { isWorkerMessage, Worker, WorkerMessage } from '../../types.js';
3
+ import { gracefullyKill, isShuttingDown } from '../utils.js';
4
+
5
+ const FORK_RETRIES = 5;
6
+
7
+ type MaybeWorker = Worker | { error: string };
8
+
9
+ export class WorkerQueue {
10
+ private isProcessing = false;
11
+ private queue: { browser: string; gridUrl?: string; retry: number; resolve: (mw: MaybeWorker) => void }[] = [];
12
+
13
+ // TODO Add concurrency
14
+ constructor(private useQueue: boolean) {}
15
+
16
+ async forkWorker(browser: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
17
+ return new Promise<MaybeWorker>((resolve) => {
18
+ this.queue.push({ browser, gridUrl, retry, resolve });
19
+
20
+ void this.process();
21
+ });
22
+ }
23
+
24
+ private async process() {
25
+ if (this.useQueue && this.isProcessing) return;
26
+
27
+ const { browser, gridUrl, retry, resolve } = this.queue.pop() ?? {};
28
+
29
+ if (browser == undefined || retry == undefined || resolve == undefined) return;
30
+
31
+ if (isShuttingDown.current) {
32
+ resolve({ error: 'Master process is shutting down' });
33
+ return;
34
+ }
35
+
36
+ this.isProcessing = true;
37
+
38
+ cluster.setupPrimary({
39
+ args: ['--browser', browser, ...(gridUrl ? ['--gridUrl', gridUrl] : []), ...process.argv.slice(2)],
40
+ });
41
+ const worker = cluster.fork();
42
+ const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
43
+ const readyHandler = (message: unknown): void => {
44
+ if (!isWorkerMessage(message) || message.type == 'port') return;
45
+ worker.off('message', readyHandler);
46
+ resolve(message);
47
+ };
48
+ worker.on('message', readyHandler);
49
+ });
50
+
51
+ if (message.type == 'error') {
52
+ gracefullyKill(worker);
53
+
54
+ if (retry == FORK_RETRIES) resolve(message.payload);
55
+ else this.queue.push({ browser, gridUrl, retry: retry + 1, resolve });
56
+ } else {
57
+ resolve(worker);
58
+ }
59
+
60
+ this.isProcessing = false;
61
+
62
+ setImmediate(() => void this.process());
63
+ }
64
+ }
@@ -13,12 +13,14 @@ import {
13
13
  TestMeta,
14
14
  } from '../../types.js';
15
15
  import Pool from './pool.js';
16
+ import { WorkerQueue } from './queue.js';
16
17
 
17
18
  export default class Runner extends EventEmitter {
18
19
  private failFast: boolean;
19
20
  private screenDir: string;
20
21
  private reportDir: string;
21
22
  private browsers: string[];
23
+ private scheduler: WorkerQueue;
22
24
  private pools: Record<string, Pool> = {};
23
25
  tests: Partial<Record<string, ServerTest>> = {};
24
26
  public get isRunning(): boolean {
@@ -30,9 +32,10 @@ export default class Runner extends EventEmitter {
30
32
  this.failFast = config.failFast;
31
33
  this.screenDir = config.screenDir;
32
34
  this.reportDir = config.reportDir;
35
+ this.scheduler = new WorkerQueue(config.useWorkerQueue);
33
36
  this.browsers = Object.keys(config.browsers);
34
37
  this.browsers
35
- .map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
38
+ .map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
36
39
  .map((pool) => pool.on('test', this.handlePoolMessage));
37
40
  }
38
41
 
@@ -96,7 +96,7 @@ export function start(reportDir: string, port: number, ui: boolean): (api: Creev
96
96
  app.use(mount('/report', serve(reportDir)));
97
97
 
98
98
  wss.on('error', (error) => {
99
- logger.error(error);
99
+ logger().error(error);
100
100
  });
101
101
 
102
102
  server.listen(port);
@@ -42,7 +42,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
42
42
  .map((imagePath) => path.posix.relative(imagesDir, imagePath))
43
43
  .filter((imagePath) => !images.has(imagePath));
44
44
  if (unnecessaryImages.length > 0) {
45
- logger.warn('We found unnecessary screenshot images, those can be safely removed:\n', unnecessaryImages.join('\n'));
45
+ logger().warn(
46
+ 'We found unnecessary screenshot images, those can be safely removed:\n',
47
+ unnecessaryImages.join('\n'),
48
+ );
46
49
  }
47
50
  }
48
51
 
@@ -81,10 +84,10 @@ export async function start(
81
84
 
82
85
  if (options.ui) {
83
86
  resolveApi(creeveyApi(runner));
84
- logger.info(`Started on http://localhost:${options.port}`);
87
+ logger().info(`Started on http://localhost:${options.port}`);
85
88
  } else {
86
89
  if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
87
- logger.warn("Don't have any tests to run");
90
+ logger().warn("Don't have any tests to run");
88
91
 
89
92
  void shutdownWorkers().then(() => process.exit());
90
93
  return;
@@ -101,7 +104,7 @@ export async function start(
101
104
  void sendScreenshotsCount(config, options, runner.status)
102
105
  .catch((reason: unknown) => {
103
106
  const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
104
- logger.warn(`Can't send telemetry: ${error}`);
107
+ logger().warn(`Can't send telemetry: ${error}`);
105
108
  })
106
109
  .finally(() => {
107
110
  void shutdownWorkers().then(() => process.exit());
@@ -1,10 +1,17 @@
1
1
  import semver from 'semver';
2
+ import { exec } from 'shelljs';
2
3
 
3
4
  // TODO Support custom docker images
4
- // TODO Support nexus
5
5
  export function playwrightDockerFile(browser: string, version: string): string {
6
6
  const sv = semver.coerce(version);
7
7
 
8
+ let npmRegistry;
9
+ try {
10
+ npmRegistry = exec('npm config get registry', { silent: true }).stdout.trim();
11
+ } catch {
12
+ /* noop */
13
+ }
14
+
8
15
  return `
9
16
  FROM mcr.microsoft.com/playwright:v${sv?.format() ?? version}
10
17
 
@@ -12,7 +19,12 @@ WORKDIR /creevey
12
19
 
13
20
  RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
14
21
  echo "import { ${browser} as browser } from 'playwright-core';" >> index.js && \\
15
- echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\
22
+ echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\${
23
+ npmRegistry
24
+ ? `
25
+ echo "registry=${npmRegistry}" > .npmrc && \\`
26
+ : ''
27
+ }
16
28
  npm i playwright-core${sv ? `@${sv.format()}` : ''}
17
29
 
18
30
  EXPOSE 4444
@@ -1,10 +1,17 @@
1
1
  import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
2
- import Logger from 'loglevel';
3
2
  import chalk from 'chalk';
4
3
  import { v4 } from 'uuid';
5
4
  import prefix from 'loglevel-plugin-prefix';
6
- import { SET_GLOBALS, STORY_RENDERED, UPDATE_STORY_ARGS } from '@storybook/core-events';
7
- import { BrowserConfigObject, Config, Options, StoriesRaw, StoryInput, StorybookGlobals, noop } from '../../types';
5
+ import {
6
+ BrowserConfigObject,
7
+ Config,
8
+ Options,
9
+ StoriesRaw,
10
+ StoryInput,
11
+ StorybookEvents,
12
+ StorybookGlobals,
13
+ noop,
14
+ } from '../../types';
8
15
  import { subscribeOn } from '../messages';
9
16
  import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
10
17
  import { isShuttingDown, runSequence } from '../utils';
@@ -20,7 +27,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
20
27
  (resolve) =>
21
28
  (timeout = setTimeout(() => {
22
29
  isTimeout = true;
23
- logger.error(`Can't connect to ${type.name()} playwright browser`, error);
30
+ logger().error(`Can't connect to ${type.name()} playwright browser`, error);
24
31
  resolve(null);
25
32
  }, 10000)),
26
33
  ),
@@ -49,13 +56,13 @@ export class InternalBrowser {
49
56
  #sessionId: string = v4();
50
57
  #serverHost: string | null = null;
51
58
  #serverPort: number;
52
- #logger: Logger.Logger;
59
+ #storybookGlobals?: StorybookGlobals;
53
60
  #unsubscribe: () => void = noop;
54
- constructor(browser: Browser, page: Page, port: number) {
61
+ constructor(browser: Browser, page: Page, port: number, storybookGlobals?: StorybookGlobals) {
55
62
  this.#browser = browser;
56
63
  this.#page = page;
57
64
  this.#serverPort = port;
58
- this.#logger = Logger.getLogger(this.#sessionId);
65
+ this.#storybookGlobals = storybookGlobals;
59
66
  this.#unsubscribe = subscribeOn('shutdown', () => {
60
67
  void this.closeBrowser();
61
68
  });
@@ -91,7 +98,12 @@ export class InternalBrowser {
91
98
  if (captureElement) {
92
99
  const element = await this.#page.$(captureElement);
93
100
  if (!element) throw new Error(`Element with selector ${captureElement} not found`);
94
- return element.screenshot({ animations: 'disabled', mask });
101
+
102
+ return element.screenshot({
103
+ animations: 'disabled',
104
+ mask,
105
+ style: ':root { overflow: hidden !important; }',
106
+ });
95
107
  }
96
108
  return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
97
109
  }
@@ -102,10 +114,11 @@ export class InternalBrowser {
102
114
 
103
115
  async selectStory(id: string, waitForReady = false): Promise<boolean> {
104
116
  // NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
117
+ await this.updateStorybookGlobals();
105
118
  await this.updateBrowserGlobalVariables();
106
119
  await this.resetMousePosition();
107
120
 
108
- this.#logger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
121
+ logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
109
122
 
110
123
  const result = await this.#page.evaluate<
111
124
  [error?: string | null, isCaptureCalled?: boolean] | null,
@@ -140,7 +153,7 @@ export class InternalBrowser {
140
153
  });
141
154
  });
142
155
  },
143
- [story.id, updatedArgs, UPDATE_STORY_ARGS, STORY_RENDERED] as const,
156
+ [story.id, updatedArgs, StorybookEvents.UPDATE_STORY_ARGS, StorybookEvents.STORY_RENDERED] as const,
144
157
  );
145
158
  }
146
159
 
@@ -167,26 +180,43 @@ export class InternalBrowser {
167
180
  options: Options,
168
181
  ): Promise<InternalBrowser | null> {
169
182
  const browserConfig = config.browsers[browserName] as BrowserConfigObject;
170
- const { storybookUrl: address = config.storybookUrl, viewport, _storybookGlobals } = browserConfig;
183
+ const {
184
+ storybookUrl: address = config.storybookUrl,
185
+ viewport,
186
+ _storybookGlobals,
187
+ seleniumCapabilities,
188
+ playwrightOptions,
189
+ } = browserConfig;
171
190
 
172
191
  let browser: Browser | null = null;
173
192
 
174
- // TODO Support Selenium Grid 4
175
- switch (browserConfig.browserName) {
176
- case 'chromium':
177
- browser = await tryConnect(chromium, gridUrl);
178
- break;
179
- case 'firefox':
180
- browser = await tryConnect(firefox, gridUrl);
181
- break;
182
- case 'webkit':
183
- browser = await tryConnect(webkit, gridUrl);
184
- break;
185
-
186
- default:
187
- throw new Error(
188
- `Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
189
- );
193
+ if (new URL(gridUrl).protocol === 'ws:') {
194
+ switch (browserConfig.browserName) {
195
+ case 'chromium':
196
+ browser = await tryConnect(chromium, gridUrl);
197
+ break;
198
+ case 'firefox':
199
+ browser = await tryConnect(firefox, gridUrl);
200
+ break;
201
+ case 'webkit':
202
+ browser = await tryConnect(webkit, gridUrl);
203
+ break;
204
+
205
+ default:
206
+ logger().error(
207
+ `Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
208
+ );
209
+ }
210
+ } else {
211
+ if (browserConfig.browserName != 'chrome') {
212
+ logger().error("Playwright's Selenium Grid feature supports only chrome browser");
213
+ return null;
214
+ }
215
+
216
+ process.env.SELENIUM_REMOTE_URL = gridUrl;
217
+ process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
218
+
219
+ browser = await chromium.launch(playwrightOptions);
190
220
  }
191
221
 
192
222
  if (!browser) {
@@ -197,7 +227,7 @@ export class InternalBrowser {
197
227
 
198
228
  // TODO Add debug output
199
229
 
200
- const internalBrowser = new InternalBrowser(browser, page, options.port);
230
+ const internalBrowser = new InternalBrowser(browser, page, options.port, _storybookGlobals);
201
231
 
202
232
  try {
203
233
  if (isShuttingDown.current) return null;
@@ -205,7 +235,6 @@ export class InternalBrowser {
205
235
  browserName,
206
236
  viewport,
207
237
  storybookUrl: address,
208
- storybookGlobals: _storybookGlobals,
209
238
  resolveStorybookUrl: config.resolveStorybookUrl,
210
239
  });
211
240
 
@@ -217,7 +246,7 @@ export class InternalBrowser {
217
246
  const error = new Error(`Can't load storybook root page: ${message}`);
218
247
  if (originalError instanceof Error) error.stack = originalError.stack;
219
248
 
220
- logger.error(error);
249
+ logger().error(error);
221
250
 
222
251
  return null;
223
252
  }
@@ -227,32 +256,29 @@ export class InternalBrowser {
227
256
  browserName,
228
257
  viewport,
229
258
  storybookUrl,
230
- storybookGlobals,
231
259
  resolveStorybookUrl,
232
260
  }: {
233
261
  browserName: string;
234
262
  viewport?: { width: number; height: number };
235
263
  storybookUrl: string;
236
- storybookGlobals?: StorybookGlobals;
237
264
  resolveStorybookUrl?: () => Promise<string>;
238
265
  }) {
239
266
  const sessionId = this.#sessionId;
240
267
 
241
- prefix.apply(this.#logger, {
268
+ prefix.apply(logger(), {
242
269
  format(level) {
243
270
  const levelColor = colors[level.toUpperCase() as keyof typeof colors];
244
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
271
+ return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
245
272
  },
246
273
  });
247
274
 
248
- this.#page.setDefaultNavigationTimeout(10000);
249
275
  this.#page.setDefaultTimeout(60000);
250
276
 
251
277
  return await runSequence(
252
278
  [
253
279
  () => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
254
280
  () => this.waitForStorybook(),
255
- () => this.updateStorybookGlobals(storybookGlobals),
281
+ () => this.updateStorybookGlobals(),
256
282
  () => this.resolveCreeveyHost(),
257
283
  () => this.updateBrowserGlobalVariables(),
258
284
  () => this.resizeViewport(viewport),
@@ -269,29 +295,30 @@ export class InternalBrowser {
269
295
 
270
296
  try {
271
297
  if (resolver) {
272
- this.#logger.debug('Resolving storybook url with custom resolver');
298
+ logger().debug('Resolving storybook url with custom resolver');
273
299
 
274
300
  const resolvedUrl = await resolver();
275
301
 
276
- this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
302
+ logger().debug(`Resolver storybook url ${resolvedUrl}`);
277
303
 
278
304
  await this.#page.goto(appendIframePath(resolvedUrl));
279
305
  } else {
280
- await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
306
+ // TODO this.#page.setDefaultNavigationTimeout(10000);
307
+ await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
281
308
  }
282
309
  } catch (error) {
283
- this.#logger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
310
+ logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
284
311
  throw error;
285
312
  }
286
313
  }
287
314
 
288
315
  private async checkUrl(url: string): Promise<boolean> {
289
316
  try {
290
- this.#logger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
317
+ logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
291
318
  const response = await this.#page.goto(url, { waitUntil: 'commit' });
292
319
  const source = await response?.text();
293
320
 
294
- this.#logger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
321
+ logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
295
322
  return source?.includes(`id="${storybookRootID}"`) ?? false;
296
323
  } catch {
297
324
  return false;
@@ -300,7 +327,7 @@ export class InternalBrowser {
300
327
 
301
328
  private async waitForStorybook(): Promise<void> {
302
329
  // TODO Duplicated code with selenium
303
- this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
330
+ logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
304
331
 
305
332
  const isTimeout = await Promise.race([
306
333
  new Promise<boolean>((resolve) => {
@@ -317,9 +344,9 @@ export class InternalBrowser {
317
344
  if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
318
345
  if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
319
346
  return false;
320
- }, SET_GLOBALS);
347
+ }, StorybookEvents.SET_GLOBALS);
321
348
  } catch (e: unknown) {
322
- this.#logger.debug('An error has been caught during the script:', e);
349
+ logger().debug('An error has been caught during the script:', e);
323
350
  }
324
351
  } while (wait);
325
352
  return false;
@@ -330,13 +357,13 @@ export class InternalBrowser {
330
357
  if (isTimeout) throw new Error('Failed to wait `setStories` event');
331
358
  }
332
359
 
333
- private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
334
- if (!globals) return;
360
+ private async updateStorybookGlobals(): Promise<void> {
361
+ if (!this.#storybookGlobals) return;
335
362
 
336
- this.#logger.debug('Applying storybook globals');
363
+ logger().debug('Applying storybook globals');
337
364
  await this.#page.evaluate((globals: StorybookGlobals) => {
338
365
  window.__CREEVEY_UPDATE_GLOBALS__(globals);
339
- }, globals);
366
+ }, this.#storybookGlobals);
340
367
  }
341
368
 
342
369
  private async resolveCreeveyHost(): Promise<void> {
@@ -53,7 +53,7 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
53
53
  try {
54
54
  return await import('./internal.js');
55
55
  } catch (error) {
56
- logger.error(error);
56
+ logger().error(error);
57
57
  return null;
58
58
  }
59
59
  })();
@@ -5,6 +5,7 @@ import { isDefined } from '../../types.js';
5
5
  import { logger } from '../logger.js';
6
6
  import { deserializeRawStories } from '../../shared/index.js';
7
7
 
8
+ // TODO Don't have updates from stories
8
9
  export const loadStories: StoriesProvider = async (_config, storiesListener, webdriver) => {
9
10
  if (cluster.isPrimary) {
10
11
  return new Promise<StoriesRaw>((resolve) => {
@@ -17,7 +18,7 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
17
18
  if (message.type == 'set') {
18
19
  const { stories, oldTests } = message.payload;
19
20
  if (oldTests.length > 0)
20
- logger.warn(
21
+ logger().warn(
21
22
  `If you use browser stories provider of CSFv3 Storybook feature\n` +
22
23
  `Creevey will not load tests defined in story parameters from following stories:\n` +
23
24
  oldTests.join('\n'),
@@ -54,7 +54,7 @@ async function parseParams(
54
54
 
55
55
  if (listener) {
56
56
  chokidar.watch(testFiles).on('change', (filePath) => {
57
- logger.debug(`changed: ${filePath}`);
57
+ logger().debug(`changed: ${filePath}`);
58
58
 
59
59
  // doesn't work, always returns {} due modules caching
60
60
  // see https://github.com/nodejs/modules/issues/307
@@ -22,11 +22,11 @@ export class CreeveyReporter {
22
22
  // TODO Output in better way, like vitest, maybe
23
23
  constructor(runner: EventEmitter, options: { reporterOptions: { creevey: ReporterOptions } }) {
24
24
  const { sessionId, browserName } = options.reporterOptions.creevey;
25
- const testLogger = Logger.getLogger(browserName);
25
+ const testLogger = Logger.getLogger(sessionId);
26
26
 
27
27
  prefix.apply(testLogger, {
28
28
  format(level) {
29
- return `${testLevels[level]} => (${browserName}:${chalk.gray(sessionId)})`;
29
+ return `[${browserName}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
30
30
  },
31
31
  });
32
32
 
@@ -34,11 +34,12 @@ export class CreeveyReporter {
34
34
  testLogger.warn(chalk.cyan(test.titlePath().join('/')));
35
35
  });
36
36
  runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
37
- testLogger.info(chalk.cyan(test.titlePath().join('/')));
37
+ testLogger.info(chalk.cyan(test.titlePath().join('/')), chalk.gray(`(${test.duration} ms)`));
38
38
  });
39
39
  runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error) => {
40
40
  testLogger.error(
41
41
  chalk.cyan(test.titlePath().join('/')),
42
+ chalk.gray(`(${test.duration} ms)`),
42
43
  '\n ',
43
44
  this.getErrors(
44
45
  error,
@@ -79,11 +80,16 @@ export class TeamcityReporter {
79
80
  console.log(`##teamcity[testStarted name='${this.escape(test.title)}' flowId='${process.pid}']`);
80
81
  });
81
82
 
83
+ runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
84
+ console.log(`##teamcity[testFinished name='${this.escape(test.title)}' flowId='${process.pid}']`);
85
+ });
86
+
82
87
  runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error: Error) => {
83
88
  Object.entries(reporterOptions.images).forEach(([name, image]) => {
84
89
  if (!image) return;
85
90
  const filePath = test
86
91
  .titlePath()
92
+ .slice(0, -1)
87
93
  .concat(name == browserName ? [] : [browserName])
88
94
  .map(this.escape)
89
95
  .join('/');