creevey 0.9.0-beta.19 → 0.9.0-beta.20

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.
Binary file
@@ -28,7 +28,7 @@ if (_cluster.default.isWorker) process.on('SIGINT', _types.noop);
28
28
  if (_cluster.default.isPrimary) process.on('SIGINT', _utils.shutdown);
29
29
  const argv = (0, _minimist.default)(process.argv.slice(2), {
30
30
  string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'storybookUrl'],
31
- boolean: ['debug', 'ui', 'saveReport', 'tests'],
31
+ boolean: ['debug', 'trace', 'ui', 'saveReport', 'tests'],
32
32
  default: {
33
33
  port: 3000,
34
34
  saveReport: true
@@ -42,8 +42,11 @@ const argv = (0, _minimist.default)(process.argv.slice(2), {
42
42
  });
43
43
 
44
44
  // @ts-expect-error: define log level for storybook
45
- global.LOGLEVEL = argv.debug ? 'debug' : 'warn';
46
- if (argv.debug) {
45
+ global.LOGLEVEL = argv.trace ? 'trace' : argv.debug ? 'debug' : 'warn';
46
+ if (argv.trace) {
47
+ _logger.logger.setDefaultLevel(_loglevel.levels.TRACE);
48
+ (0, _loglevel.setDefaultLevel)(_loglevel.levels.TRACE);
49
+ } else if (argv.debug) {
47
50
  _logger.logger.setDefaultLevel(_loglevel.levels.DEBUG);
48
51
  (0, _loglevel.setDefaultLevel)(_loglevel.levels.DEBUG);
49
52
  } else {
@@ -28,6 +28,7 @@ class Pool extends _events.EventEmitter {
28
28
  }
29
29
  async init() {
30
30
  const poolSize = this.config.limit || 1;
31
+ // TODO Init queue for workers to smooth browser starting load
31
32
  this.workers = (await Promise.all(Array.from({
32
33
  length: poolSize
33
34
  }).map(() => this.forkWorker()))).filter(workerOrError => workerOrError instanceof _cluster.Worker);
@@ -24,11 +24,20 @@ var _logger = require("../logger");
24
24
  var _messages = require("../messages");
25
25
  var _utils = require("../utils");
26
26
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
27
+ // import { Options as IeOptions } from 'selenium-webdriver/ie';
28
+ // import { Options as EdgeOptions } from 'selenium-webdriver/edge';
29
+ // import { Options as ChromeOptions } from 'selenium-webdriver/chrome';
30
+ // import { Options as SafariOptions } from 'selenium-webdriver/safari';
31
+ // import { Options as FirefoxOptions } from 'selenium-webdriver/firefox';
32
+
33
+ // type UnPromise<P> = P extends Promise<infer T> ? T : never;
34
+
27
35
  const storybookRootID = 'storybook-root';
28
36
  const DOCKER_INTERNAL = 'host.docker.internal';
29
37
  let browserLogger = _logger.logger;
30
38
  let browserName = '';
31
39
  let browser = null;
40
+ // let context: UnPromise<ReturnType<typeof BrowsingContext>> | null = null;
32
41
  let creeveyServerHost = null;
33
42
  function getSessionData(grid, sessionId = '') {
34
43
  const gridUrl = new URL(grid);
@@ -278,6 +287,18 @@ async function takeScreenshot(browser, captureElement, ignoreElements) {
278
287
  const ignoreStyles = await insertIgnoreStyles(browser, ignoreElements);
279
288
  try {
280
289
  if (!captureElement) {
290
+ if (browserLogger.getLevel() <= _loglevel.levels.DEBUG) {
291
+ const {
292
+ innerWidth,
293
+ innerHeight
294
+ } = await browser.executeScript(function () {
295
+ return {
296
+ innerWidth: window.innerWidth,
297
+ innerHeight: window.innerHeight
298
+ };
299
+ });
300
+ browserLogger.debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
301
+ }
281
302
  browserLogger.debug('Capturing viewport screenshot');
282
303
  screenshot = await browser.takeScreenshot();
283
304
  browserLogger.debug('Viewport screenshot is captured');
@@ -313,6 +334,14 @@ async function takeScreenshot(browser, captureElement, ignoreElements) {
313
334
  if (!elementRect || !windowRect) throw new Error(`Couldn't find element with selector: '${captureElement}'`);
314
335
  const isFitIntoViewport = elementRect.width + elementRect.left <= windowRect.width && elementRect.height + elementRect.top <= windowRect.height;
315
336
  if (isFitIntoViewport) browserLogger.debug(`Capturing ${_chalk.default.cyan(captureElement)}`);else browserLogger.debug(`Capturing composite screenshot image of ${_chalk.default.cyan(captureElement)}`);
337
+
338
+ // const element = await browser.findElement(By.css(captureElement));
339
+ // screenshot = isFitIntoViewport
340
+ // ? context
341
+ // ? await context.captureElementScreenshot(await element.getId())
342
+ // : await browser.findElement(By.css(captureElement)).takeScreenshot()
343
+ // : // TODO pointer-events: none, need to research
344
+ // await takeCompositeScreenshot(browser, windowRect, elementRect);
316
345
  screenshot = isFitIntoViewport ? await browser.findElement(_seleniumWebdriver.By.css(captureElement)).takeScreenshot() :
317
346
  // TODO pointer-events: none, need to research
318
347
  await takeCompositeScreenshot(browser, windowRect, elementRect);
@@ -432,7 +461,34 @@ async function getBrowser(config, options) {
432
461
  url.username = url.username ? '********' : '';
433
462
  url.password = url.password ? '********' : '';
434
463
  browserLogger.debug(`(${browserName}) Connecting to Selenium ${_chalk.default.magenta(url.toString())}`);
435
- browser = await new _seleniumWebdriver.Builder().usingServer(gridUrl).withCapabilities(capabilities).build();
464
+ const prefs = new _seleniumWebdriver.logging.Preferences();
465
+ if (options.trace) {
466
+ for (const type of Object.values(_seleniumWebdriver.logging.Type)) {
467
+ prefs.setLevel(type, _seleniumWebdriver.logging.Level.ALL);
468
+ }
469
+ }
470
+
471
+ // const ie = new IeOptions();
472
+ // const edge = new EdgeOptions();
473
+ // const chrome = new ChromeOptions();
474
+ // const safari = new SafariOptions();
475
+ // const firefox = new FirefoxOptions();
476
+ // edge.enableBidi();
477
+ // chrome.enableBidi();
478
+ // firefox.enableBidi();
479
+
480
+ browser = await new _seleniumWebdriver.Builder()
481
+ // .setIeOptions(ie)
482
+ // .setEdgeOptions(edge)
483
+ // .setChromeOptions(chrome)
484
+ // .setSafariOptions(safari)
485
+ // .setFirefoxOptions(firefox)
486
+ .usingServer(gridUrl).withCapabilities(capabilities).setLoggingPrefs(prefs) // NOTE: Should go last
487
+ .build();
488
+
489
+ // const id = await browser.getWindowHandle();
490
+ // context = await BrowsingContext(browser, { browsingContextId: id });
491
+
436
492
  const sessionId = (_await$browser$getSes = await browser.getSession()) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
437
493
  let browserHost = '';
438
494
  try {
@@ -454,7 +510,7 @@ async function getBrowser(config, options) {
454
510
  await (0, _utils.runSequence)([() => {
455
511
  var _browser2;
456
512
  return (_browser2 = browser) === null || _browser2 === void 0 ? void 0 : _browser2.manage().setTimeouts({
457
- pageLoad: 5000,
513
+ pageLoad: 10000,
458
514
  script: 60000
459
515
  });
460
516
  }, () => viewport && browser && resizeViewport(browser, viewport), () => browser && openStorybookPage(browser, realAddress, config.resolveStorybookUrl), () => browser && waitForStorybook(browser)], () => !_utils.isShuttingDown.current);
@@ -111,8 +111,9 @@ async function startSelenoidContainer(config, debug) {
111
111
  browsers.forEach(({
112
112
  browserName,
113
113
  version = 'latest',
114
+ browserVersion = version,
114
115
  limit: browserLimit = 1,
115
- dockerImage = `selenoid/${browserName}:${version}`
116
+ dockerImage = `selenoid/${browserName}:${browserVersion}`
116
117
  }) => {
117
118
  limit += browserLimit;
118
119
  images.push(dockerImage);
@@ -8,20 +8,22 @@ var _path = _interopRequireDefault(require("path"));
8
8
  var _https = _interopRequireDefault(require("https"));
9
9
  var _shelljs = require("shelljs");
10
10
  var _qs = require("qs");
11
+ var _lodash = require("lodash");
12
+ var _uuid = require("uuid");
13
+ var _types = require("../types");
11
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
15
  const konturGitHost = 'git.skbkontur.ru';
13
16
  const trackId = 232; // front_infra
14
17
  const origin = 'http://localhost/';
15
- const category = 'screenshots';
16
- const action = 'count';
17
- const label = 'full_run';
18
- function buildPathname(info) {
18
+ const category = 'tests_run';
19
+ const action = 'done';
20
+ function buildPathname(label, info) {
19
21
  return `/track-event?${(0, _qs.stringify)({
20
22
  id: trackId,
21
23
  c: category,
22
24
  a: action,
23
25
  l: label,
24
- cv: JSON.stringify(info),
26
+ cv: typeof info == 'string' ? info : JSON.stringify(info),
25
27
  ts: new Date().toISOString(),
26
28
  url: origin
27
29
  })}`;
@@ -63,15 +65,40 @@ function tryGetCreeveyVersion() {
63
65
  return [undefined, error];
64
66
  }
65
67
  }
68
+ function sendRequest(options) {
69
+ return new Promise((resolve, reject) => {
70
+ const req = _https.default.request(options, res => {
71
+ if (res.statusCode) {
72
+ if (res.statusCode >= 200 && res.statusCode < 300) {
73
+ resolve();
74
+ } else if (res.statusCode >= 300 && res.statusCode < 400) {
75
+ reject(new Error(`Redirection error: ${res.statusCode}`));
76
+ } else if (res.statusCode >= 400 && res.statusCode < 500) {
77
+ reject(new Error(`Client error: ${res.statusCode}`));
78
+ } else if (res.statusCode >= 500 && res.statusCode < 600) {
79
+ reject(new Error(`Server error: ${res.statusCode}`));
80
+ } else {
81
+ reject(new Error(`Unexpected status code: ${res.statusCode}`));
82
+ }
83
+ } else {
84
+ reject(new Error('No status code received'));
85
+ }
86
+ });
87
+ req.on('error', reject);
88
+ req.end();
89
+ });
90
+ }
66
91
  async function sendScreenshotsCount(config, options, status) {
67
92
  var _config$storiesProvid;
68
93
  const [repoUrl] = tryGetRepoUrl();
69
94
  const isKonturRepo = repoUrl === null || repoUrl === void 0 ? void 0 : repoUrl.includes(konturGitHost);
70
95
  if (!isKonturRepo || config.disableTelemetry) return;
96
+ const uuid = (0, _uuid.v4)();
71
97
  const [creeveyVersion, creeveyVersionError] = tryGetCreeveyVersion();
72
98
  const [storybookVersion, storybookVersionError] = tryGetStorybookVersion();
73
99
  const gridUrl = config.gridUrl ? sanitizeGridUrl(config.gridUrl) : undefined;
74
- const info = {
100
+ const configMeta = {
101
+ runId: uuid,
75
102
  repoUrl: repoUrl ?? 'unknown',
76
103
  creeveyVersion: creeveyVersion ?? 'unknown',
77
104
  storybookVersion: storybookVersion ?? 'unknown',
@@ -83,6 +110,10 @@ async function sendScreenshotsCount(config, options, status) {
83
110
  maxRetries: config.maxRetries,
84
111
  diffOptions: config.diffOptions,
85
112
  storiesProvider: ((_config$storiesProvid = config.storiesProvider) === null || _config$storiesProvid === void 0 ? void 0 : _config$storiesProvid.providerName) ?? 'unknown',
113
+ errors: [creeveyVersionError, storybookVersionError].some(Boolean) ? [creeveyVersionError ? `Error while getting creevey version: ${creeveyVersionError.message}` : undefined, storybookVersionError ? `Error while getting storybook version: ${storybookVersionError.message}` : undefined].filter(Boolean) : undefined
114
+ };
115
+ const browsersMeta = {
116
+ runId: uuid,
86
117
  browsers: Object.fromEntries(Object.entries(config.browsers ?? {}).map(([name, browser]) => [name, typeof browser === 'object' ? {
87
118
  name: browser.name,
88
119
  gridUrl: browser.gridUrl ? sanitizeGridUrl(browser.gridUrl) : undefined,
@@ -93,39 +124,44 @@ async function sendScreenshotsCount(config, options, status) {
93
124
  limit: browser.limit,
94
125
  dockerImage: browser.dockerImage,
95
126
  'se:teamname': browser['se:teamname']
96
- } : browser])),
97
- tests: Object.values((status === null || status === void 0 ? void 0 : status.tests) ?? {}).filter(x => Boolean(x)).map(test => ({
98
- id: test.id,
99
- browser: test.browser,
100
- testName: test.testName,
101
- storyPath: test.storyPath,
102
- status: test.status
103
- })),
104
- errors: [creeveyVersionError, storybookVersionError].some(Boolean) ? [creeveyVersionError ? `Error while getting creevey version: ${creeveyVersionError.message}` : undefined, storybookVersionError ? `Error while getting storybook version: ${storybookVersionError.message}` : undefined].filter(Boolean) : undefined
127
+ } : browser]))
105
128
  };
106
- return new Promise((resolve, reject) => {
107
- const req = _https.default.request({
108
- host: 'metrika.kontur.ru',
109
- path: buildPathname(info),
110
- protocol: 'https:'
111
- }, res => {
112
- if (res.statusCode) {
113
- if (res.statusCode >= 200 && res.statusCode < 300) {
114
- resolve();
115
- } else if (res.statusCode >= 300 && res.statusCode < 400) {
116
- reject(new Error(`Redirection error: ${res.statusCode}`));
117
- } else if (res.statusCode >= 400 && res.statusCode < 500) {
118
- reject(new Error(`Client error: ${res.statusCode}`));
119
- } else if (res.statusCode >= 500 && res.statusCode < 600) {
120
- reject(new Error(`Server error: ${res.statusCode}`));
121
- } else {
122
- reject(new Error(`Unexpected status code: ${res.statusCode}`));
123
- }
124
- } else {
125
- reject(new Error('No status code received'));
126
- }
127
- });
128
- req.on('error', reject);
129
- req.end();
129
+ const tests = {};
130
+ Object.values((status === null || status === void 0 ? void 0 : status.tests) ?? {}).filter(_types.isDefined).forEach(test => {
131
+ (0, _lodash.set)(tests, [...test.storyPath, test.testName, test.browser].filter(_types.isDefined), test.id);
130
132
  });
133
+ const testsMeta = {
134
+ runId: uuid,
135
+ tests
136
+ };
137
+ const fullPathname = buildPathname('tests', testsMeta);
138
+ // NOTE: Keep request path shorter than 32k symbols
139
+ const chunksCount = Math.ceil(fullPathname.length / 32_000);
140
+ let chunks = [];
141
+ if (chunksCount > 1) {
142
+ const testsString = JSON.stringify(tests);
143
+ const chunkSize = Math.ceil(testsString.length / chunksCount);
144
+ chunks = Array.from({
145
+ length: chunksCount
146
+ }).map((_, chunkIndex) => testsString.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize)).map((testsPart, seq) => buildPathname('tests', {
147
+ runId: uuid,
148
+ tests: testsPart,
149
+ seq
150
+ }));
151
+ } else {
152
+ chunks = [fullPathname];
153
+ }
154
+ await Promise.all([sendRequest({
155
+ host: 'metrika.kontur.ru',
156
+ path: buildPathname('config', configMeta),
157
+ protocol: 'https:'
158
+ }), sendRequest({
159
+ host: 'metrika.kontur.ru',
160
+ path: buildPathname('browsers', browsersMeta),
161
+ protocol: 'https:'
162
+ }), ...chunks.map(chunk => sendRequest({
163
+ host: 'metrika.kontur.ru',
164
+ path: chunk,
165
+ protocol: 'https:'
166
+ }))]);
131
167
  }
@@ -16,6 +16,7 @@ const testLevels = {
16
16
  ERROR: _chalk.default.red('FAIL')
17
17
  };
18
18
  class CreeveyReporter extends _mocha.reporters.Base {
19
+ // TODO Output in better way, like vitest, maybe
19
20
  constructor(runner, options) {
20
21
  super(runner);
21
22
  const {
@@ -171,10 +171,12 @@ async function worker(config, options) {
171
171
  const browser = await (0, _selenium.getBrowser)(config, options);
172
172
  const sessionId = (_await$browser$getSes = await (browser === null || browser === void 0 ? void 0 : browser.getSession())) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
173
173
  if (browser == null) return;
174
- const interval = setInterval(() => void browser.getCurrentUrl().then(url => {
175
- if (options.debug) _logger.logger.debug(`${options.browser}:${_chalk.default.gray(sessionId)}`, 'current url', _chalk.default.magenta(url));
176
- }), 10 * 1000);
177
- (0, _messages.subscribeOn)('shutdown', () => clearInterval(interval));
174
+ if (options.debug) {
175
+ const interval = setInterval(() => void browser.getCurrentUrl().then(url => {
176
+ _logger.logger.debug(`${options.browser}:${_chalk.default.gray(sessionId)}`, 'current url', _chalk.default.magenta(url));
177
+ }), 10 * 1000);
178
+ (0, _messages.subscribeOn)('shutdown', () => clearInterval(interval));
179
+ }
178
180
  mocha.suite.beforeAll(function () {
179
181
  this.config = config;
180
182
  this.browser = browser;
@@ -186,6 +188,18 @@ async function worker(config, options) {
186
188
  this.screenshots = screenshots;
187
189
  });
188
190
  mocha.suite.beforeEach(_selenium.switchStory);
191
+ if (options.trace) {
192
+ mocha.suite.afterEach(async function () {
193
+ const types = await (browser === null || browser === void 0 ? void 0 : browser.manage().logs().getAvailableLogTypes());
194
+ for (const type of types ?? []) {
195
+ const logs = await (browser === null || browser === void 0 ? void 0 : browser.manage().logs().get(type));
196
+ logs.forEach(log => {
197
+ var _this$currentTest;
198
+ return _logger.logger.trace(sessionId, (_this$currentTest = this.currentTest) === null || _this$currentTest === void 0 ? void 0 : _this$currentTest.titlePath().join('/'), log.toJSON());
199
+ });
200
+ }
201
+ });
202
+ }
189
203
  (0, _messages.subscribeOn)('test', message => {
190
204
  if (message.type != 'start') return;
191
205
  const test = message.payload;
@@ -25,7 +25,7 @@ if (cluster.isWorker) process.on('SIGINT', noop);
25
25
  if (cluster.isPrimary) process.on('SIGINT', shutdown);
26
26
  const argv = minimist(process.argv.slice(2), {
27
27
  string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'storybookUrl'],
28
- boolean: ['debug', 'ui', 'saveReport', 'tests'],
28
+ boolean: ['debug', 'trace', 'ui', 'saveReport', 'tests'],
29
29
  default: {
30
30
  port: 3000,
31
31
  saveReport: true
@@ -39,8 +39,11 @@ const argv = minimist(process.argv.slice(2), {
39
39
  });
40
40
 
41
41
  // @ts-expect-error: define log level for storybook
42
- global.LOGLEVEL = argv.debug ? 'debug' : 'warn';
43
- if (argv.debug) {
42
+ global.LOGLEVEL = argv.trace ? 'trace' : argv.debug ? 'debug' : 'warn';
43
+ if (argv.trace) {
44
+ logger.setDefaultLevel(levels.TRACE);
45
+ setDefaultLevel(levels.TRACE);
46
+ } else if (argv.debug) {
44
47
  logger.setDefaultLevel(levels.DEBUG);
45
48
  setDefaultLevel(levels.DEBUG);
46
49
  } else {
@@ -20,6 +20,7 @@ export default class Pool extends EventEmitter {
20
20
  }
21
21
  async init() {
22
22
  const poolSize = this.config.limit || 1;
23
+ // TODO Init queue for workers to smooth browser starting load
23
24
  this.workers = (await Promise.all(Array.from({
24
25
  length: poolSize
25
26
  }).map(() => this.forkWorker()))).filter(workerOrError => workerOrError instanceof ClusterWorker);
@@ -2,21 +2,29 @@ import { SET_GLOBALS, UPDATE_STORY_ARGS, STORY_RENDERED } from '@storybook/core-
2
2
  import chalk from 'chalk';
3
3
  import http from 'http';
4
4
  import https from 'https';
5
- import { getLogger } from 'loglevel';
5
+ import { getLogger, levels } from 'loglevel';
6
6
  import prefix from 'loglevel-plugin-prefix';
7
7
  import { networkInterfaces } from 'os';
8
8
  import { PNG } from 'pngjs';
9
- import { Builder, By, Capabilities, Origin } from 'selenium-webdriver';
9
+ import { Builder, By, Capabilities, Origin, logging } from 'selenium-webdriver';
10
+ // import { Options as IeOptions } from 'selenium-webdriver/ie';
11
+ // import { Options as EdgeOptions } from 'selenium-webdriver/edge';
12
+ // import { Options as ChromeOptions } from 'selenium-webdriver/chrome';
13
+ // import { Options as SafariOptions } from 'selenium-webdriver/safari';
14
+ // import { Options as FirefoxOptions } from 'selenium-webdriver/firefox';
10
15
  import { PageLoadStrategy } from 'selenium-webdriver/lib/capabilities';
11
16
  import { isDefined, noop } from '../../types';
12
17
  import { colors, logger } from '../logger';
13
18
  import { emitStoriesMessage, subscribeOn } from '../messages';
14
19
  import { isShuttingDown, LOCALHOST_REGEXP, runSequence } from '../utils';
20
+ // type UnPromise<P> = P extends Promise<infer T> ? T : never;
21
+
15
22
  const storybookRootID = 'storybook-root';
16
23
  const DOCKER_INTERNAL = 'host.docker.internal';
17
24
  let browserLogger = logger;
18
25
  let browserName = '';
19
26
  let browser = null;
27
+ // let context: UnPromise<ReturnType<typeof BrowsingContext>> | null = null;
20
28
  let creeveyServerHost = null;
21
29
  function getSessionData(grid, sessionId = '') {
22
30
  const gridUrl = new URL(grid);
@@ -266,6 +274,18 @@ export async function takeScreenshot(browser, captureElement, ignoreElements) {
266
274
  const ignoreStyles = await insertIgnoreStyles(browser, ignoreElements);
267
275
  try {
268
276
  if (!captureElement) {
277
+ if (browserLogger.getLevel() <= levels.DEBUG) {
278
+ const {
279
+ innerWidth,
280
+ innerHeight
281
+ } = await browser.executeScript(function () {
282
+ return {
283
+ innerWidth: window.innerWidth,
284
+ innerHeight: window.innerHeight
285
+ };
286
+ });
287
+ browserLogger.debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
288
+ }
269
289
  browserLogger.debug('Capturing viewport screenshot');
270
290
  screenshot = await browser.takeScreenshot();
271
291
  browserLogger.debug('Viewport screenshot is captured');
@@ -301,6 +321,14 @@ export async function takeScreenshot(browser, captureElement, ignoreElements) {
301
321
  if (!elementRect || !windowRect) throw new Error(`Couldn't find element with selector: '${captureElement}'`);
302
322
  const isFitIntoViewport = elementRect.width + elementRect.left <= windowRect.width && elementRect.height + elementRect.top <= windowRect.height;
303
323
  if (isFitIntoViewport) browserLogger.debug(`Capturing ${chalk.cyan(captureElement)}`);else browserLogger.debug(`Capturing composite screenshot image of ${chalk.cyan(captureElement)}`);
324
+
325
+ // const element = await browser.findElement(By.css(captureElement));
326
+ // screenshot = isFitIntoViewport
327
+ // ? context
328
+ // ? await context.captureElementScreenshot(await element.getId())
329
+ // : await browser.findElement(By.css(captureElement)).takeScreenshot()
330
+ // : // TODO pointer-events: none, need to research
331
+ // await takeCompositeScreenshot(browser, windowRect, elementRect);
304
332
  screenshot = isFitIntoViewport ? await browser.findElement(By.css(captureElement)).takeScreenshot() :
305
333
  // TODO pointer-events: none, need to research
306
334
  await takeCompositeScreenshot(browser, windowRect, elementRect);
@@ -420,7 +448,34 @@ export async function getBrowser(config, options) {
420
448
  url.username = url.username ? '********' : '';
421
449
  url.password = url.password ? '********' : '';
422
450
  browserLogger.debug(`(${browserName}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
423
- browser = await new Builder().usingServer(gridUrl).withCapabilities(capabilities).build();
451
+ const prefs = new logging.Preferences();
452
+ if (options.trace) {
453
+ for (const type of Object.values(logging.Type)) {
454
+ prefs.setLevel(type, logging.Level.ALL);
455
+ }
456
+ }
457
+
458
+ // const ie = new IeOptions();
459
+ // const edge = new EdgeOptions();
460
+ // const chrome = new ChromeOptions();
461
+ // const safari = new SafariOptions();
462
+ // const firefox = new FirefoxOptions();
463
+ // edge.enableBidi();
464
+ // chrome.enableBidi();
465
+ // firefox.enableBidi();
466
+
467
+ browser = await new Builder()
468
+ // .setIeOptions(ie)
469
+ // .setEdgeOptions(edge)
470
+ // .setChromeOptions(chrome)
471
+ // .setSafariOptions(safari)
472
+ // .setFirefoxOptions(firefox)
473
+ .usingServer(gridUrl).withCapabilities(capabilities).setLoggingPrefs(prefs) // NOTE: Should go last
474
+ .build();
475
+
476
+ // const id = await browser.getWindowHandle();
477
+ // context = await BrowsingContext(browser, { browsingContextId: id });
478
+
424
479
  const sessionId = (_await$browser$getSes = await browser.getSession()) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
425
480
  let browserHost = '';
426
481
  try {
@@ -442,7 +497,7 @@ export async function getBrowser(config, options) {
442
497
  await runSequence([() => {
443
498
  var _browser2;
444
499
  return (_browser2 = browser) === null || _browser2 === void 0 ? void 0 : _browser2.manage().setTimeouts({
445
- pageLoad: 5000,
500
+ pageLoad: 10000,
446
501
  script: 60000
447
502
  });
448
503
  }, () => viewport && browser && resizeViewport(browser, viewport), () => browser && openStorybookPage(browser, realAddress, config.resolveStorybookUrl), () => browser && waitForStorybook(browser)], () => !isShuttingDown.current);
@@ -103,8 +103,9 @@ export async function startSelenoidContainer(config, debug) {
103
103
  browsers.forEach(({
104
104
  browserName,
105
105
  version = 'latest',
106
+ browserVersion = version,
106
107
  limit: browserLimit = 1,
107
- dockerImage = `selenoid/${browserName}:${version}`
108
+ dockerImage = `selenoid/${browserName}:${browserVersion}`
108
109
  }) => {
109
110
  limit += browserLimit;
110
111
  images.push(dockerImage);
@@ -2,19 +2,21 @@ import path from 'path';
2
2
  import https from 'https';
3
3
  import { exec } from 'shelljs';
4
4
  import { stringify } from 'qs';
5
+ import { set } from 'lodash';
6
+ import { v4 } from 'uuid';
7
+ import { isDefined } from '../types';
5
8
  const konturGitHost = 'git.skbkontur.ru';
6
9
  const trackId = 232; // front_infra
7
10
  const origin = 'http://localhost/';
8
- const category = 'screenshots';
9
- const action = 'count';
10
- const label = 'full_run';
11
- function buildPathname(info) {
11
+ const category = 'tests_run';
12
+ const action = 'done';
13
+ function buildPathname(label, info) {
12
14
  return `/track-event?${stringify({
13
15
  id: trackId,
14
16
  c: category,
15
17
  a: action,
16
18
  l: label,
17
- cv: JSON.stringify(info),
19
+ cv: typeof info == 'string' ? info : JSON.stringify(info),
18
20
  ts: new Date().toISOString(),
19
21
  url: origin
20
22
  })}`;
@@ -56,15 +58,40 @@ function tryGetCreeveyVersion() {
56
58
  return [undefined, error];
57
59
  }
58
60
  }
61
+ function sendRequest(options) {
62
+ return new Promise((resolve, reject) => {
63
+ const req = https.request(options, res => {
64
+ if (res.statusCode) {
65
+ if (res.statusCode >= 200 && res.statusCode < 300) {
66
+ resolve();
67
+ } else if (res.statusCode >= 300 && res.statusCode < 400) {
68
+ reject(new Error(`Redirection error: ${res.statusCode}`));
69
+ } else if (res.statusCode >= 400 && res.statusCode < 500) {
70
+ reject(new Error(`Client error: ${res.statusCode}`));
71
+ } else if (res.statusCode >= 500 && res.statusCode < 600) {
72
+ reject(new Error(`Server error: ${res.statusCode}`));
73
+ } else {
74
+ reject(new Error(`Unexpected status code: ${res.statusCode}`));
75
+ }
76
+ } else {
77
+ reject(new Error('No status code received'));
78
+ }
79
+ });
80
+ req.on('error', reject);
81
+ req.end();
82
+ });
83
+ }
59
84
  export async function sendScreenshotsCount(config, options, status) {
60
85
  var _config$storiesProvid;
61
86
  const [repoUrl] = tryGetRepoUrl();
62
87
  const isKonturRepo = repoUrl === null || repoUrl === void 0 ? void 0 : repoUrl.includes(konturGitHost);
63
88
  if (!isKonturRepo || config.disableTelemetry) return;
89
+ const uuid = v4();
64
90
  const [creeveyVersion, creeveyVersionError] = tryGetCreeveyVersion();
65
91
  const [storybookVersion, storybookVersionError] = tryGetStorybookVersion();
66
92
  const gridUrl = config.gridUrl ? sanitizeGridUrl(config.gridUrl) : undefined;
67
- const info = {
93
+ const configMeta = {
94
+ runId: uuid,
68
95
  repoUrl: repoUrl ?? 'unknown',
69
96
  creeveyVersion: creeveyVersion ?? 'unknown',
70
97
  storybookVersion: storybookVersion ?? 'unknown',
@@ -76,6 +103,10 @@ export async function sendScreenshotsCount(config, options, status) {
76
103
  maxRetries: config.maxRetries,
77
104
  diffOptions: config.diffOptions,
78
105
  storiesProvider: ((_config$storiesProvid = config.storiesProvider) === null || _config$storiesProvid === void 0 ? void 0 : _config$storiesProvid.providerName) ?? 'unknown',
106
+ errors: [creeveyVersionError, storybookVersionError].some(Boolean) ? [creeveyVersionError ? `Error while getting creevey version: ${creeveyVersionError.message}` : undefined, storybookVersionError ? `Error while getting storybook version: ${storybookVersionError.message}` : undefined].filter(Boolean) : undefined
107
+ };
108
+ const browsersMeta = {
109
+ runId: uuid,
79
110
  browsers: Object.fromEntries(Object.entries(config.browsers ?? {}).map(([name, browser]) => [name, typeof browser === 'object' ? {
80
111
  name: browser.name,
81
112
  gridUrl: browser.gridUrl ? sanitizeGridUrl(browser.gridUrl) : undefined,
@@ -86,39 +117,44 @@ export async function sendScreenshotsCount(config, options, status) {
86
117
  limit: browser.limit,
87
118
  dockerImage: browser.dockerImage,
88
119
  'se:teamname': browser['se:teamname']
89
- } : browser])),
90
- tests: Object.values((status === null || status === void 0 ? void 0 : status.tests) ?? {}).filter(x => Boolean(x)).map(test => ({
91
- id: test.id,
92
- browser: test.browser,
93
- testName: test.testName,
94
- storyPath: test.storyPath,
95
- status: test.status
96
- })),
97
- errors: [creeveyVersionError, storybookVersionError].some(Boolean) ? [creeveyVersionError ? `Error while getting creevey version: ${creeveyVersionError.message}` : undefined, storybookVersionError ? `Error while getting storybook version: ${storybookVersionError.message}` : undefined].filter(Boolean) : undefined
120
+ } : browser]))
98
121
  };
99
- return new Promise((resolve, reject) => {
100
- const req = https.request({
101
- host: 'metrika.kontur.ru',
102
- path: buildPathname(info),
103
- protocol: 'https:'
104
- }, res => {
105
- if (res.statusCode) {
106
- if (res.statusCode >= 200 && res.statusCode < 300) {
107
- resolve();
108
- } else if (res.statusCode >= 300 && res.statusCode < 400) {
109
- reject(new Error(`Redirection error: ${res.statusCode}`));
110
- } else if (res.statusCode >= 400 && res.statusCode < 500) {
111
- reject(new Error(`Client error: ${res.statusCode}`));
112
- } else if (res.statusCode >= 500 && res.statusCode < 600) {
113
- reject(new Error(`Server error: ${res.statusCode}`));
114
- } else {
115
- reject(new Error(`Unexpected status code: ${res.statusCode}`));
116
- }
117
- } else {
118
- reject(new Error('No status code received'));
119
- }
120
- });
121
- req.on('error', reject);
122
- req.end();
122
+ const tests = {};
123
+ Object.values((status === null || status === void 0 ? void 0 : status.tests) ?? {}).filter(isDefined).forEach(test => {
124
+ set(tests, [...test.storyPath, test.testName, test.browser].filter(isDefined), test.id);
123
125
  });
126
+ const testsMeta = {
127
+ runId: uuid,
128
+ tests
129
+ };
130
+ const fullPathname = buildPathname('tests', testsMeta);
131
+ // NOTE: Keep request path shorter than 32k symbols
132
+ const chunksCount = Math.ceil(fullPathname.length / 32_000);
133
+ let chunks = [];
134
+ if (chunksCount > 1) {
135
+ const testsString = JSON.stringify(tests);
136
+ const chunkSize = Math.ceil(testsString.length / chunksCount);
137
+ chunks = Array.from({
138
+ length: chunksCount
139
+ }).map((_, chunkIndex) => testsString.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize)).map((testsPart, seq) => buildPathname('tests', {
140
+ runId: uuid,
141
+ tests: testsPart,
142
+ seq
143
+ }));
144
+ } else {
145
+ chunks = [fullPathname];
146
+ }
147
+ await Promise.all([sendRequest({
148
+ host: 'metrika.kontur.ru',
149
+ path: buildPathname('config', configMeta),
150
+ protocol: 'https:'
151
+ }), sendRequest({
152
+ host: 'metrika.kontur.ru',
153
+ path: buildPathname('browsers', browsersMeta),
154
+ protocol: 'https:'
155
+ }), ...chunks.map(chunk => sendRequest({
156
+ host: 'metrika.kontur.ru',
157
+ path: chunk,
158
+ protocol: 'https:'
159
+ }))]);
124
160
  }
@@ -9,6 +9,7 @@ const testLevels = {
9
9
  ERROR: chalk.red('FAIL')
10
10
  };
11
11
  export class CreeveyReporter extends reporters.Base {
12
+ // TODO Output in better way, like vitest, maybe
12
13
  constructor(runner, options) {
13
14
  super(runner);
14
15
  const {
@@ -164,10 +164,12 @@ export default async function worker(config, options) {
164
164
  const browser = await getBrowser(config, options);
165
165
  const sessionId = (_await$browser$getSes = await (browser === null || browser === void 0 ? void 0 : browser.getSession())) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
166
166
  if (browser == null) return;
167
- const interval = setInterval(() => void browser.getCurrentUrl().then(url => {
168
- if (options.debug) logger.debug(`${options.browser}:${chalk.gray(sessionId)}`, 'current url', chalk.magenta(url));
169
- }), 10 * 1000);
170
- subscribeOn('shutdown', () => clearInterval(interval));
167
+ if (options.debug) {
168
+ const interval = setInterval(() => void browser.getCurrentUrl().then(url => {
169
+ logger.debug(`${options.browser}:${chalk.gray(sessionId)}`, 'current url', chalk.magenta(url));
170
+ }), 10 * 1000);
171
+ subscribeOn('shutdown', () => clearInterval(interval));
172
+ }
171
173
  mocha.suite.beforeAll(function () {
172
174
  this.config = config;
173
175
  this.browser = browser;
@@ -179,6 +181,18 @@ export default async function worker(config, options) {
179
181
  this.screenshots = screenshots;
180
182
  });
181
183
  mocha.suite.beforeEach(switchStory);
184
+ if (options.trace) {
185
+ mocha.suite.afterEach(async function () {
186
+ const types = await (browser === null || browser === void 0 ? void 0 : browser.manage().logs().getAvailableLogTypes());
187
+ for (const type of types ?? []) {
188
+ const logs = await (browser === null || browser === void 0 ? void 0 : browser.manage().logs().get(type));
189
+ logs.forEach(log => {
190
+ var _this$currentTest;
191
+ return logger.trace(sessionId, (_this$currentTest = this.currentTest) === null || _this$currentTest === void 0 ? void 0 : _this$currentTest.titlePath().join('/'), log.toJSON());
192
+ });
193
+ }
194
+ });
195
+ }
182
196
  subscribeOn('test', message => {
183
197
  if (message.type != 'start') return;
184
198
  const test = message.payload;
@@ -251,6 +251,7 @@ export interface Options {
251
251
  ui: boolean;
252
252
  update: boolean | string;
253
253
  debug: boolean;
254
+ trace: boolean;
254
255
  tests: boolean;
255
256
  browser?: string;
256
257
  reporter?: string;
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "addon",
14
14
  "test"
15
15
  ],
16
- "version": "0.9.0-beta.19",
16
+ "version": "0.9.0-beta.20",
17
17
  "bin": "./lib/cjs/cli.js",
18
18
  "exports": {
19
19
  ".": {
@@ -125,6 +125,7 @@
125
125
  "selenium-webdriver": "^4.25.0",
126
126
  "shelljs": "^0.8.5",
127
127
  "tsconfig-paths": "^4.0.0",
128
+ "uuid": "^10.0.0",
128
129
  "ws": "^8.8.0"
129
130
  },
130
131
  "devDependencies": {
@@ -169,6 +170,7 @@
169
170
  "@types/resize-observer-browser": "^0.1.7",
170
171
  "@types/shelljs": "^0.8.11",
171
172
  "@types/tmp": "^0.2.3",
173
+ "@types/uuid": "^10",
172
174
  "@types/webpack": "^5.28.0",
173
175
  "@types/ws": "^8.5.3",
174
176
  "@typescript-eslint/eslint-plugin": "^5.30.5",