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.
- package/.yarn/install-state.gz +0 -0
- package/lib/cjs/creevey.js +6 -3
- package/lib/cjs/server/master/pool.js +1 -0
- package/lib/cjs/server/selenium/browser.js +58 -2
- package/lib/cjs/server/selenium/selenoid.js +2 -1
- package/lib/cjs/server/telemetry.js +75 -39
- package/lib/cjs/server/worker/reporter.js +1 -0
- package/lib/cjs/server/worker/worker.js +18 -4
- package/lib/esm/creevey.js +6 -3
- package/lib/esm/server/master/pool.js +1 -0
- package/lib/esm/server/selenium/browser.js +59 -4
- package/lib/esm/server/selenium/selenoid.js +2 -1
- package/lib/esm/server/telemetry.js +75 -39
- package/lib/esm/server/worker/reporter.js +1 -0
- package/lib/esm/server/worker/worker.js +18 -4
- package/lib/types/types.d.ts +1 -0
- package/package.json +3 -1
package/.yarn/install-state.gz
CHANGED
Binary file
|
package/lib/cjs/creevey.js
CHANGED
@@ -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.
|
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
|
-
|
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:
|
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}:${
|
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 = '
|
16
|
-
const action = '
|
17
|
-
|
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
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
}
|
@@ -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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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;
|
package/lib/esm/creevey.js
CHANGED
@@ -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.
|
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
|
-
|
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:
|
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}:${
|
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 = '
|
9
|
-
const action = '
|
10
|
-
|
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
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
}
|
@@ -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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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;
|
package/lib/types/types.d.ts
CHANGED
package/package.json
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
"addon",
|
14
14
|
"test"
|
15
15
|
],
|
16
|
-
"version": "0.9.0-beta.
|
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",
|