creevey 0.9.0-beta.13 → 0.9.0-beta.14
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/.yarnrc.yml +1 -0
- package/lib/cjs/cli.js +1 -0
- package/lib/cjs/client/addon/Manager.js +151 -223
- package/lib/cjs/client/addon/components/Addon.js +2 -9
- package/lib/cjs/client/addon/components/Icons.js +1 -7
- package/lib/cjs/client/addon/components/Panel.js +5 -18
- package/lib/cjs/client/addon/components/TestSelect.js +12 -25
- package/lib/cjs/client/addon/components/Tools.js +17 -28
- package/lib/cjs/client/addon/decorator.js +1 -4
- package/lib/cjs/client/addon/index.js +0 -4
- package/lib/cjs/client/addon/preset.js +3 -12
- package/lib/cjs/client/addon/preview.js +1 -4
- package/lib/cjs/client/addon/readyForCapture.js +1 -4
- package/lib/cjs/client/addon/register.js +11 -26
- package/lib/cjs/client/addon/utils.js +1 -9
- package/lib/cjs/client/addon/withCreevey.js +55 -134
- package/lib/cjs/client/shared/components/ImagesView/BlendView.js +5 -17
- package/lib/cjs/client/shared/components/ImagesView/ImagesView.js +8 -24
- package/lib/cjs/client/shared/components/ImagesView/SideBySideView.js +7 -23
- package/lib/cjs/client/shared/components/ImagesView/SlideView.js +7 -22
- package/lib/cjs/client/shared/components/ImagesView/SwapView.js +5 -17
- package/lib/cjs/client/shared/components/ImagesView/index.js +0 -5
- package/lib/cjs/client/shared/components/PageFooter/PageFooter.js +1 -8
- package/lib/cjs/client/shared/components/PageFooter/Paging.js +2 -19
- package/lib/cjs/client/shared/components/PageHeader/ImagePreview.js +21 -17
- package/lib/cjs/client/shared/components/PageHeader/PageHeader.js +5 -24
- package/lib/cjs/client/shared/components/ResultsPage.js +9 -37
- package/lib/cjs/client/shared/creeveyClientApi.js +3 -13
- package/lib/cjs/client/shared/helpers.js +21 -75
- package/lib/cjs/client/shared/viewMode.js +2 -6
- package/lib/cjs/client/web/192.js +1 -0
- package/lib/cjs/client/web/632.js +43 -0
- package/lib/cjs/client/web/794.js +1 -0
- package/lib/cjs/client/web/main.js +78 -1
- package/lib/cjs/client/web/main.js.LICENSE.txt +0 -15
- package/lib/cjs/creevey.js +5 -21
- package/lib/cjs/index.js +0 -15
- package/lib/cjs/server/config.js +13 -33
- package/lib/cjs/server/docker.js +5 -27
- package/lib/cjs/server/index.js +8 -33
- package/lib/cjs/server/logger.js +5 -19
- package/lib/cjs/server/master/api.js +1 -14
- package/lib/cjs/server/master/index.js +15 -46
- package/lib/cjs/server/master/master.js +6 -21
- package/lib/cjs/server/master/pool.js +2 -37
- package/lib/cjs/server/master/runner.js +15 -42
- package/lib/cjs/server/master/server.js +5 -27
- package/lib/cjs/server/messages.js +7 -53
- package/lib/cjs/server/selenium/browser.js +51 -136
- package/lib/cjs/server/selenium/index.js +0 -4
- package/lib/cjs/server/selenium/selenoid.js +7 -33
- package/lib/cjs/server/stories.js +25 -30
- package/lib/cjs/server/storybook/providers/browser.js +5 -18
- package/lib/cjs/server/storybook/providers/hybrid.js +9 -29
- package/lib/cjs/server/testsFiles/parser.js +3 -19
- package/lib/cjs/server/testsFiles/register.js +7 -9
- package/lib/cjs/server/update.js +3 -20
- package/lib/cjs/server/utils.js +9 -41
- package/lib/cjs/server/worker/chai-image.js +1 -27
- package/lib/cjs/server/worker/helpers.js +2 -12
- package/lib/cjs/server/worker/index.js +1 -3
- package/lib/cjs/server/worker/reporter.js +8 -24
- package/lib/cjs/server/worker/worker.js +5 -49
- package/lib/cjs/shared/index.js +22 -36
- package/lib/cjs/shared/serializeRegExp.js +0 -8
- package/lib/cjs/types.js +4 -14
- package/lib/esm/cli.js +1 -1
- package/lib/esm/client/addon/Manager.js +151 -214
- package/lib/esm/client/addon/components/Panel.js +4 -6
- package/lib/esm/client/addon/components/TestSelect.js +11 -17
- package/lib/esm/client/addon/components/Tools.js +15 -14
- package/lib/esm/client/addon/preset.js +2 -8
- package/lib/esm/client/addon/readyForCapture.js +1 -3
- package/lib/esm/client/addon/register.js +6 -8
- package/lib/esm/client/addon/utils.js +0 -5
- package/lib/esm/client/addon/withCreevey.js +54 -116
- package/lib/esm/client/shared/components/ImagesView/BlendView.js +1 -1
- package/lib/esm/client/shared/components/ImagesView/ImagesView.js +6 -8
- package/lib/esm/client/shared/components/ImagesView/SideBySideView.js +3 -4
- package/lib/esm/client/shared/components/ImagesView/SlideView.js +3 -3
- package/lib/esm/client/shared/components/ImagesView/SwapView.js +1 -1
- package/lib/esm/client/shared/components/PageFooter/Paging.js +1 -11
- package/lib/esm/client/shared/components/PageHeader/ImagePreview.js +18 -7
- package/lib/esm/client/shared/components/PageHeader/PageHeader.js +3 -8
- package/lib/esm/client/shared/components/ResultsPage.js +6 -15
- package/lib/esm/client/shared/creeveyClientApi.js +3 -10
- package/lib/esm/client/shared/helpers.js +21 -47
- package/lib/esm/client/web/192.js +1 -0
- package/lib/esm/client/web/632.js +43 -0
- package/lib/esm/client/web/794.js +1 -0
- package/lib/esm/client/web/index.html +19 -0
- package/lib/esm/client/web/main.js +79 -0
- package/lib/esm/client/web/main.js.LICENSE.txt +34 -0
- package/lib/esm/creevey.js +4 -8
- package/lib/esm/index.js +0 -1
- package/lib/esm/server/config.js +7 -14
- package/lib/esm/server/docker.js +4 -12
- package/lib/esm/server/index.js +7 -21
- package/lib/esm/server/logger.js +0 -1
- package/lib/esm/server/master/api.js +0 -9
- package/lib/esm/server/master/index.js +15 -32
- package/lib/esm/server/master/master.js +2 -7
- package/lib/esm/server/master/pool.js +0 -23
- package/lib/esm/server/master/runner.js +14 -27
- package/lib/esm/server/master/server.js +4 -9
- package/lib/esm/server/messages.js +6 -38
- package/lib/esm/server/selenium/browser.js +50 -114
- package/lib/esm/server/selenium/selenoid.js +6 -17
- package/lib/esm/server/stories.js +24 -20
- package/lib/esm/server/storybook/providers/browser.js +4 -8
- package/lib/esm/server/storybook/providers/hybrid.js +6 -14
- package/lib/esm/server/testsFiles/parser.js +0 -6
- package/lib/esm/server/testsFiles/register.js +5 -2
- package/lib/esm/server/update.js +0 -8
- package/lib/esm/server/utils.js +3 -11
- package/lib/esm/server/worker/chai-image.js +0 -21
- package/lib/esm/server/worker/helpers.js +2 -9
- package/lib/esm/server/worker/reporter.js +7 -10
- package/lib/esm/server/worker/worker.js +4 -25
- package/lib/esm/shared/index.js +24 -25
- package/lib/esm/types.js +4 -1
- package/lib/types/client/addon/Manager.d.ts +1 -1
- package/lib/types/client/addon/components/Addon.d.ts +1 -0
- package/lib/types/client/addon/components/Icons.d.ts +1 -0
- package/lib/types/client/addon/components/Panel.d.ts +1 -0
- package/lib/types/client/addon/components/TestSelect.d.ts +1 -0
- package/lib/types/client/addon/components/Tools.d.ts +1 -0
- package/lib/types/client/addon/decorator.d.ts +1 -1
- package/lib/types/client/addon/preset.d.ts +2 -2
- package/lib/types/client/addon/preview.d.ts +1 -1
- package/lib/types/client/addon/withCreevey.d.ts +3 -2
- package/lib/types/client/shared/components/ImagesView/BlendView.d.ts +3 -1
- package/lib/types/client/shared/components/ImagesView/ImagesView.d.ts +1 -0
- package/lib/types/client/shared/components/ImagesView/SideBySideView.d.ts +3 -1
- package/lib/types/client/shared/components/ImagesView/SlideView.d.ts +3 -1
- package/lib/types/client/shared/components/ImagesView/SwapView.d.ts +3 -1
- package/lib/types/client/shared/components/PageFooter/PageFooter.d.ts +1 -0
- package/lib/types/client/shared/components/PageFooter/Paging.d.ts +1 -0
- package/lib/types/client/shared/components/PageHeader/ImagePreview.d.ts +3 -1
- package/lib/types/client/shared/components/PageHeader/PageHeader.d.ts +1 -0
- package/lib/types/client/shared/components/ResultsPage.d.ts +3 -1
- package/lib/types/client/web/CreeveyApp.d.ts +1 -0
- package/lib/types/client/web/CreeveyLoader.d.ts +2 -1
- package/lib/types/client/web/CreeveyView/SideBar/Checkbox.d.ts +4 -1
- package/lib/types/client/web/CreeveyView/SideBar/Search.d.ts +1 -0
- package/lib/types/client/web/CreeveyView/SideBar/SideBarHeader.d.ts +1 -0
- package/lib/types/client/web/CreeveyView/SideBar/SuiteLink.d.ts +19 -14
- package/lib/types/client/web/CreeveyView/SideBar/TestLink.d.ts +1 -0
- package/lib/types/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +3 -1
- package/lib/types/client/web/CreeveyView/SideBar/TestsStatus.d.ts +3 -1
- package/lib/types/client/web/CreeveyView/SideBar/Toggle.d.ts +1 -0
- package/lib/types/shared/index.d.ts +1 -1
- package/lib/types/types.d.ts +7 -28
- package/package.json +60 -78
- package/lib/cjs/client/addon/preset.ie11.js +0 -74
- package/lib/cjs/client/addon/preset.sb7.js +0 -19
- package/lib/cjs/client/web/142.js +0 -2
- package/lib/cjs/client/web/142.js.LICENSE.txt +0 -12
- package/lib/cjs/client/web/32.js +0 -1
- package/lib/cjs/client/web/551.js +0 -1
- package/lib/cjs/client/web/566.js +0 -2
- package/lib/cjs/client/web/566.js.LICENSE.txt +0 -31
- package/lib/cjs/client/web/691.js +0 -2
- package/lib/cjs/client/web/691.js.LICENSE.txt +0 -8
- package/lib/cjs/client/web/725.js +0 -1
- package/lib/cjs/server/extract.js +0 -46
- package/lib/cjs/server/loaders/babel/creevey-plugin.js +0 -86
- package/lib/cjs/server/loaders/babel/helpers.js +0 -469
- package/lib/cjs/server/loaders/babel/register.js +0 -124
- package/lib/cjs/server/loaders/hooks/mdx.js +0 -30
- package/lib/cjs/server/loaders/hooks/svelte.js +0 -65
- package/lib/cjs/server/loaders/webpack/compile.js +0 -269
- package/lib/cjs/server/loaders/webpack/creevey-loader.js +0 -172
- package/lib/cjs/server/loaders/webpack/dummy-hmr.js +0 -39
- package/lib/cjs/server/loaders/webpack/mdx-loader.js +0 -72
- package/lib/cjs/server/loaders/webpack/start.js +0 -41
- package/lib/cjs/server/storybook/entry.js +0 -53
- package/lib/cjs/server/storybook/helpers.js +0 -158
- package/lib/cjs/server/storybook/providers/nodejs.js +0 -239
- package/lib/esm/client/addon/preset.ie11.js +0 -59
- package/lib/esm/client/addon/preset.sb7.js +0 -8
- package/lib/esm/server/extract.js +0 -32
- package/lib/esm/server/loaders/babel/creevey-plugin.js +0 -72
- package/lib/esm/server/loaders/babel/helpers.js +0 -452
- package/lib/esm/server/loaders/babel/register.js +0 -103
- package/lib/esm/server/loaders/hooks/mdx.js +0 -15
- package/lib/esm/server/loaders/hooks/svelte.js +0 -49
- package/lib/esm/server/loaders/webpack/compile.js +0 -246
- package/lib/esm/server/loaders/webpack/creevey-loader.js +0 -152
- package/lib/esm/server/loaders/webpack/dummy-hmr.js +0 -32
- package/lib/esm/server/loaders/webpack/mdx-loader.js +0 -58
- package/lib/esm/server/loaders/webpack/start.js +0 -27
- package/lib/esm/server/storybook/entry.js +0 -27
- package/lib/esm/server/storybook/helpers.js +0 -97
- package/lib/esm/server/storybook/providers/nodejs.js +0 -216
- package/lib/types/client/addon/preset.ie11.d.ts +0 -10
- package/lib/types/client/addon/preset.sb7.d.ts +0 -2
- package/lib/types/server/extract.d.ts +0 -2
- package/lib/types/server/loaders/babel/creevey-plugin.d.ts +0 -1
- package/lib/types/server/loaders/babel/helpers.d.ts +0 -19
- package/lib/types/server/loaders/babel/register.d.ts +0 -5
- package/lib/types/server/loaders/hooks/mdx.d.ts +0 -1
- package/lib/types/server/loaders/hooks/svelte.d.ts +0 -1
- package/lib/types/server/loaders/webpack/compile.d.ts +0 -2
- package/lib/types/server/loaders/webpack/creevey-loader.d.ts +0 -4
- package/lib/types/server/loaders/webpack/dummy-hmr.d.ts +0 -10
- package/lib/types/server/loaders/webpack/mdx-loader.d.ts +0 -6
- package/lib/types/server/loaders/webpack/start.d.ts +0 -1
- package/lib/types/server/storybook/entry.d.ts +0 -17
- package/lib/types/server/storybook/helpers.d.ts +0 -24
- package/lib/types/server/storybook/providers/nodejs.d.ts +0 -9
- package/preset/ie11.js +0 -5
- package/preset/index.js +0 -9
- package/preset/sb7.js +0 -5
- package/types/mdx.d.ts +0 -7
package/lib/cjs/server/utils.js
CHANGED
@@ -15,59 +15,40 @@ exports.shutdown = shutdown;
|
|
15
15
|
exports.shutdownWorkers = shutdownWorkers;
|
16
16
|
exports.skipOptionKeys = void 0;
|
17
17
|
exports.testsToImages = testsToImages;
|
18
|
-
|
19
18
|
var _fs = require("fs");
|
20
|
-
|
21
19
|
var _cluster = _interopRequireDefault(require("cluster"));
|
22
|
-
|
23
20
|
var _types = require("../types");
|
24
|
-
|
25
21
|
var _messages = require("./messages");
|
26
|
-
|
27
22
|
var _findCacheDir = _interopRequireDefault(require("find-cache-dir"));
|
28
|
-
|
29
23
|
var _https = require("https");
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
const isShuttingDown = {
|
24
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
25
|
+
const isShuttingDown = exports.isShuttingDown = {
|
34
26
|
current: false
|
35
27
|
};
|
36
|
-
exports.
|
37
|
-
const
|
38
|
-
exports.
|
39
|
-
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
|
40
|
-
exports.extensions = extensions;
|
41
|
-
const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
42
|
-
exports.skipOptionKeys = skipOptionKeys;
|
43
|
-
|
28
|
+
const LOCALHOST_REGEXP = exports.LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
|
29
|
+
const extensions = exports.extensions = ['.js', '.jsx', '.ts', '.tsx'];
|
30
|
+
const skipOptionKeys = exports.skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
44
31
|
function matchBy(pattern, value) {
|
45
32
|
return typeof pattern == 'string' && pattern == value || Array.isArray(pattern) && pattern.includes(value) || pattern instanceof RegExp && pattern.test(value) || !(0, _types.isDefined)(pattern);
|
46
33
|
}
|
47
|
-
|
48
34
|
function shouldSkip(browser, meta, skipOptions, test) {
|
49
35
|
if (typeof skipOptions != 'object') {
|
50
36
|
return skipOptions;
|
51
37
|
}
|
52
|
-
|
53
38
|
for (const skipKey in skipOptions) {
|
54
39
|
const reason = shouldSkipByOption(browser, meta, skipOptions[skipKey], skipKey, test);
|
55
40
|
if (reason) return reason;
|
56
41
|
}
|
57
|
-
|
58
42
|
return false;
|
59
43
|
}
|
60
|
-
|
61
44
|
function shouldSkipByOption(browser, meta, skipOption, reason, test) {
|
62
45
|
if (Array.isArray(skipOption)) {
|
63
46
|
for (const skip of skipOption) {
|
64
47
|
const result = shouldSkipByOption(browser, meta, skip, reason, test);
|
65
48
|
if (result) return result;
|
66
49
|
}
|
67
|
-
|
68
50
|
return false;
|
69
51
|
}
|
70
|
-
|
71
52
|
const {
|
72
53
|
in: browsers,
|
73
54
|
kinds,
|
@@ -84,7 +65,6 @@ function shouldSkipByOption(browser, meta, skipOption, reason, test) {
|
|
84
65
|
const skipByTest = !(0, _types.isDefined)(test) || matchBy(tests, test);
|
85
66
|
return skipByBrowser && skipByKind && skipByStory && skipByTest && reason;
|
86
67
|
}
|
87
|
-
|
88
68
|
async function shutdownWorkers() {
|
89
69
|
isShuttingDown.current = true;
|
90
70
|
await Promise.all(Object.values(_cluster.default.workers ?? {}).filter(_types.isDefined).filter(worker => worker.isConnected()).map(worker => new Promise(resolve => {
|
@@ -97,25 +77,21 @@ async function shutdownWorkers() {
|
|
97
77
|
})));
|
98
78
|
(0, _messages.emitShutdownMessage)();
|
99
79
|
}
|
100
|
-
|
101
80
|
function shutdown() {
|
102
81
|
// eslint-disable-next-line no-process-exit
|
103
82
|
process.exit();
|
104
83
|
}
|
105
|
-
|
106
84
|
function getCreeveyCache() {
|
107
85
|
return (0, _findCacheDir.default)({
|
108
86
|
name: 'creevey',
|
109
87
|
cwd: __dirname
|
110
88
|
});
|
111
89
|
}
|
112
|
-
|
113
90
|
async function runSequence(seq, predicate) {
|
114
91
|
for (const fn of seq) {
|
115
92
|
if (predicate()) await fn();
|
116
93
|
}
|
117
94
|
}
|
118
|
-
|
119
95
|
function testsToImages(tests) {
|
120
96
|
return new Set([].concat(...tests.filter(_types.isDefined).map(({
|
121
97
|
browser,
|
@@ -124,15 +100,12 @@ function testsToImages(tests) {
|
|
124
100
|
results
|
125
101
|
}) => {
|
126
102
|
var _results$slice$;
|
127
|
-
|
128
|
-
return Object.keys((results === null || results === void 0 ? void 0 : (_results$slice$ = results.slice(-1)[0]) === null || _results$slice$ === void 0 ? void 0 : _results$slice$.images) ?? {}).map(image => `${[...storyPath, testName, browser, browser == image ? undefined : image].filter(_types.isDefined).join('/')}.png`);
|
103
|
+
return Object.keys((results === null || results === void 0 || (_results$slice$ = results.slice(-1)[0]) === null || _results$slice$ === void 0 ? void 0 : _results$slice$.images) ?? {}).map(image => `${[...storyPath, testName, browser, browser == image ? undefined : image].filter(_types.isDefined).join('/')}.png`);
|
129
104
|
})));
|
130
|
-
}
|
131
|
-
|
132
|
-
|
133
|
-
const isInsideDocker = (0, _fs.existsSync)('/proc/1/cgroup') && /docker/.test((0, _fs.readFileSync)('/proc/1/cgroup', 'utf8'));
|
134
|
-
exports.isInsideDocker = isInsideDocker;
|
105
|
+
}
|
135
106
|
|
107
|
+
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
108
|
+
const isInsideDocker = exports.isInsideDocker = (0, _fs.existsSync)('/proc/1/cgroup') && /docker/.test((0, _fs.readFileSync)('/proc/1/cgroup', 'utf8'));
|
136
109
|
const downloadBinary = (downloadUrl, destination) => new Promise((resolve, reject) => (0, _https.get)(downloadUrl, response => {
|
137
110
|
if (response.statusCode == 302) {
|
138
111
|
const {
|
@@ -141,7 +114,6 @@ const downloadBinary = (downloadUrl, destination) => new Promise((resolve, rejec
|
|
141
114
|
if (!location) return reject(new Error(`Couldn't download selenoid. Status code: ${response.statusCode ?? 'UNKNOWN'}`));
|
142
115
|
return resolve(downloadBinary(location, destination));
|
143
116
|
}
|
144
|
-
|
145
117
|
if (response.statusCode != 200) return reject(new Error(`Couldn't download selenoid. Status code: ${response.statusCode ?? 'UNKNOWN'}`));
|
146
118
|
const fileStream = (0, _fs.createWriteStream)(destination);
|
147
119
|
response.pipe(fileStream);
|
@@ -154,12 +126,9 @@ const downloadBinary = (downloadUrl, destination) => new Promise((resolve, rejec
|
|
154
126
|
reject(error);
|
155
127
|
});
|
156
128
|
}));
|
157
|
-
|
158
129
|
exports.downloadBinary = downloadBinary;
|
159
|
-
|
160
130
|
function removeProps(obj, propPath) {
|
161
131
|
const [prop, ...restPath] = propPath;
|
162
|
-
|
163
132
|
if (restPath.length > 0) {
|
164
133
|
if (typeof prop == 'string') obj[prop] && removeProps(obj[prop], restPath);
|
165
134
|
if ((0, _types.isFunction)(prop)) Object.keys(obj).filter(prop).forEach(key => obj[key] && removeProps(obj[key], restPath));
|
@@ -168,7 +137,6 @@ function removeProps(obj, propPath) {
|
|
168
137
|
if ((0, _types.isFunction)(prop)) Object.keys(obj).filter(prop).forEach(key => delete obj[key]);
|
169
138
|
}
|
170
139
|
}
|
171
|
-
|
172
140
|
function readDirRecursive(dirPath) {
|
173
141
|
return [].concat(...(0, _fs.readdirSync)(dirPath, {
|
174
142
|
withFileTypes: true
|
@@ -4,20 +4,14 @@ Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
5
5
|
});
|
6
6
|
exports.default = _default;
|
7
|
-
|
8
7
|
var _pngjs = require("pngjs");
|
9
|
-
|
10
8
|
var _pixelmatch = _interopRequireDefault(require("pixelmatch"));
|
11
|
-
|
12
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
13
|
-
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
14
10
|
function normalizeImageSize(image, width, height) {
|
15
11
|
const normalizedImage = Buffer.alloc(4 * width * height);
|
16
|
-
|
17
12
|
for (let y = 0; y < height; y++) {
|
18
13
|
for (let x = 0; x < width; x++) {
|
19
14
|
const i = (y * width + x) * 4;
|
20
|
-
|
21
15
|
if (x < image.width && y < image.height) {
|
22
16
|
const j = (y * image.width + x) * 4;
|
23
17
|
normalizedImage[i + 0] = image.data[j + 0];
|
@@ -32,23 +26,17 @@ function normalizeImageSize(image, width, height) {
|
|
32
26
|
}
|
33
27
|
}
|
34
28
|
}
|
35
|
-
|
36
29
|
return normalizedImage;
|
37
30
|
}
|
38
|
-
|
39
31
|
function hasDiffPixels(diff) {
|
40
32
|
for (let i = 0; i < diff.length; i += 4) {
|
41
33
|
if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255) return true;
|
42
34
|
}
|
43
|
-
|
44
35
|
return false;
|
45
36
|
}
|
46
|
-
|
47
37
|
function compareImages(expect, actual, diffOptions) {
|
48
38
|
const expectImage = _pngjs.PNG.sync.read(expect);
|
49
|
-
|
50
39
|
const actualImage = _pngjs.PNG.sync.read(actual);
|
51
|
-
|
52
40
|
const width = Math.max(actualImage.width, expectImage.width);
|
53
41
|
const height = Math.max(actualImage.height, expectImage.height);
|
54
42
|
const diffImage = new _pngjs.PNG({
|
@@ -56,42 +44,34 @@ function compareImages(expect, actual, diffOptions) {
|
|
56
44
|
height
|
57
45
|
});
|
58
46
|
let actualImageData = actualImage.data;
|
59
|
-
|
60
47
|
if (actualImage.width < width || actualImage.height < height) {
|
61
48
|
actualImageData = normalizeImageSize(actualImage, width, height);
|
62
49
|
}
|
63
|
-
|
64
50
|
let expectImageData = expectImage.data;
|
65
|
-
|
66
51
|
if (expectImage.width < width || expectImage.height < height) {
|
67
52
|
expectImageData = normalizeImageSize(expectImage, width, height);
|
68
53
|
}
|
69
|
-
|
70
54
|
(0, _pixelmatch.default)(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
|
71
55
|
return {
|
72
56
|
isEqual: !hasDiffPixels(diffImage.data),
|
73
57
|
diff: _pngjs.PNG.sync.write(diffImage)
|
74
58
|
};
|
75
59
|
}
|
76
|
-
|
77
60
|
function _default(getExpected, diffOptions) {
|
78
61
|
return function chaiImage({
|
79
62
|
Assertion
|
80
63
|
}, utils) {
|
81
64
|
async function assertImage(actual, imageName) {
|
82
65
|
let onCompare = () => Promise.resolve();
|
83
|
-
|
84
66
|
let expected = await getExpected(imageName);
|
85
67
|
if (!(expected instanceof Buffer) && expected != null) ({
|
86
68
|
expected,
|
87
69
|
onCompare
|
88
70
|
} = expected);
|
89
|
-
|
90
71
|
if (expected == null) {
|
91
72
|
await onCompare(actual);
|
92
73
|
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
|
93
74
|
}
|
94
|
-
|
95
75
|
if (actual.equals(expected)) return await onCompare(actual);
|
96
76
|
const {
|
97
77
|
isEqual,
|
@@ -101,11 +81,9 @@ function _default(getExpected, diffOptions) {
|
|
101
81
|
await onCompare(actual, expected, diff);
|
102
82
|
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
|
103
83
|
}
|
104
|
-
|
105
84
|
utils.addMethod(Assertion.prototype, 'matchImage', async function matchImage(imageName) {
|
106
85
|
const actual = utils.flag(this, 'object');
|
107
86
|
const errorMessage = await assertImage(typeof actual == 'string' ? Buffer.from(actual, 'base64') : actual, imageName);
|
108
|
-
|
109
87
|
if (errorMessage) {
|
110
88
|
throw createImageError(imageName ? {
|
111
89
|
[imageName]: errorMessage
|
@@ -116,25 +94,21 @@ function _default(getExpected, diffOptions) {
|
|
116
94
|
const errors = {};
|
117
95
|
await Promise.all(Object.entries(utils.flag(this, 'object')).map(async ([imageName, imageOrBase64]) => {
|
118
96
|
let errorMessage;
|
119
|
-
|
120
97
|
try {
|
121
98
|
errorMessage = await assertImage(typeof imageOrBase64 == 'string' ? Buffer.from(imageOrBase64, 'base64') : imageOrBase64, imageName);
|
122
99
|
} catch (error) {
|
123
100
|
errorMessage = error.stack;
|
124
101
|
}
|
125
|
-
|
126
102
|
if (errorMessage) {
|
127
103
|
errors[imageName] = errorMessage;
|
128
104
|
}
|
129
105
|
}));
|
130
|
-
|
131
106
|
if (Object.keys(errors).length > 0) {
|
132
107
|
throw createImageError(errors);
|
133
108
|
}
|
134
109
|
});
|
135
110
|
};
|
136
111
|
}
|
137
|
-
|
138
112
|
function createImageError(imageErrors) {
|
139
113
|
const error = new Error('Expected image to match');
|
140
114
|
error.images = imageErrors;
|
@@ -4,34 +4,26 @@ Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
5
5
|
});
|
6
6
|
exports.addTestsFromStories = addTestsFromStories;
|
7
|
-
|
8
7
|
var _mocha = require("mocha");
|
9
|
-
|
10
8
|
var _types = require("../../types");
|
11
|
-
|
12
9
|
var _stories = require("../stories");
|
13
|
-
|
14
10
|
function findOrCreateSuite(name, parent) {
|
15
11
|
const suite = parent.suites.find(({
|
16
12
|
title
|
17
13
|
}) => title == name) || new _mocha.Suite(name, parent.ctx);
|
18
|
-
|
19
14
|
if (!suite.parent) {
|
20
15
|
suite.parent = parent;
|
21
16
|
parent.addSuite(suite);
|
22
17
|
}
|
23
|
-
|
24
18
|
return suite;
|
25
19
|
}
|
26
|
-
|
27
20
|
function createTest(name, fn, skip = false) {
|
28
21
|
const test = new _mocha.Test(name, skip ? undefined : fn);
|
29
|
-
test.pending = Boolean(skip);
|
30
|
-
|
22
|
+
test.pending = Boolean(skip);
|
23
|
+
// NOTE Can't define skip reason in mocha https://github.com/mochajs/mocha/issues/2026
|
31
24
|
test.skipReason = skip;
|
32
25
|
return test;
|
33
26
|
}
|
34
|
-
|
35
27
|
function addTest(rootSuite, test) {
|
36
28
|
const [testName, ...suitePath] = [...test.storyPath, test.testName].reverse().filter(_types.isDefined);
|
37
29
|
const suite = suitePath.reduceRight((subSuite, suiteName) => findOrCreateSuite(suiteName, subSuite), rootSuite);
|
@@ -43,7 +35,6 @@ function addTest(rootSuite, test) {
|
|
43
35
|
}, suite.ctx);
|
44
36
|
return mochaTest;
|
45
37
|
}
|
46
|
-
|
47
38
|
function removeTestOrSuite(testOrSuite) {
|
48
39
|
const {
|
49
40
|
parent
|
@@ -53,7 +44,6 @@ function removeTestOrSuite(testOrSuite) {
|
|
53
44
|
if (testOrSuite instanceof _mocha.Suite) parent.suites = parent.suites.filter(suite => suite != testOrSuite);
|
54
45
|
if (parent.tests.length == 0 && parent.suites.length == 0) removeTestOrSuite(parent);
|
55
46
|
}
|
56
|
-
|
57
47
|
async function addTestsFromStories(rootSuite, config, {
|
58
48
|
browser,
|
59
49
|
...options
|
@@ -9,7 +9,5 @@ Object.defineProperty(exports, "default", {
|
|
9
9
|
return _worker.default;
|
10
10
|
}
|
11
11
|
});
|
12
|
-
|
13
12
|
var _worker = _interopRequireDefault(require("./worker"));
|
14
|
-
|
15
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
13
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
@@ -4,25 +4,17 @@ Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
5
5
|
});
|
6
6
|
exports.TeamcityReporter = exports.CreeveyReporter = void 0;
|
7
|
-
|
8
7
|
var _chalk = _interopRequireDefault(require("chalk"));
|
9
|
-
|
10
8
|
var _mocha = require("mocha");
|
11
|
-
|
12
9
|
var _loglevelPluginPrefix = _interopRequireDefault(require("loglevel-plugin-prefix"));
|
13
|
-
|
14
10
|
var _types = require("../../types");
|
15
|
-
|
16
11
|
var _logger = require("../logger");
|
17
|
-
|
18
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
19
|
-
|
12
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
20
13
|
const testLevels = {
|
21
14
|
INFO: _chalk.default.green('PASS'),
|
22
15
|
WARN: _chalk.default.yellow('START'),
|
23
16
|
ERROR: _chalk.default.red('FAIL')
|
24
17
|
};
|
25
|
-
|
26
18
|
class CreeveyReporter extends _mocha.reporters.Base {
|
27
19
|
constructor(runner, options) {
|
28
20
|
super(runner);
|
@@ -31,23 +23,17 @@ class CreeveyReporter extends _mocha.reporters.Base {
|
|
31
23
|
topLevelSuite
|
32
24
|
} = options.reporterOptions;
|
33
25
|
const testLogger = (0, _logger.getLogger)(topLevelSuite);
|
34
|
-
|
35
26
|
_loglevelPluginPrefix.default.apply(testLogger, {
|
36
27
|
format(level) {
|
37
28
|
return `${testLevels[level]} => (${topLevelSuite}:${_chalk.default.gray(sessionId)})`;
|
38
29
|
}
|
39
|
-
|
40
30
|
});
|
41
|
-
|
42
31
|
runner.on('test', test => testLogger.warn(_chalk.default.cyan(test.titlePath().join('/'))));
|
43
32
|
runner.on('pass', test => testLogger.info(_chalk.default.cyan(test.titlePath().join('/'))));
|
44
33
|
runner.on('fail', (test, error) => testLogger.error(_chalk.default.cyan(test.titlePath().join('/')), '\n ', getErrors(error, (error, imageName) => `${_chalk.default.bold(imageName ?? topLevelSuite)}:${error}`, error => `${error.stack ?? error.message}`).join('\n ')));
|
45
34
|
}
|
46
|
-
|
47
35
|
}
|
48
|
-
|
49
36
|
exports.CreeveyReporter = CreeveyReporter;
|
50
|
-
|
51
37
|
class TeamcityReporter extends _mocha.reporters.Base {
|
52
38
|
constructor(runner, options) {
|
53
39
|
super(runner);
|
@@ -58,8 +44,8 @@ class TeamcityReporter extends _mocha.reporters.Base {
|
|
58
44
|
runner.on('fail', (test, error) => {
|
59
45
|
Object.entries(reporterOptions.images).forEach(([name, image]) => {
|
60
46
|
if (!image) return;
|
61
|
-
const filePath = test.titlePath().concat(name == topLevelSuite ? [] : [topLevelSuite]).map(this.escape).join('/');
|
62
|
-
|
47
|
+
const filePath = test.titlePath().concat(name == topLevelSuite ? [] : [topLevelSuite]).map(this.escape).join('/');
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
63
49
|
const {
|
64
50
|
error,
|
65
51
|
...rest
|
@@ -68,7 +54,9 @@ class TeamcityReporter extends _mocha.reporters.Base {
|
|
68
54
|
console.log(`##teamcity[publishArtifacts '${reporterOptions.reportDir}/${filePath}/${fileName} => report/${filePath}']`);
|
69
55
|
console.log(`##teamcity[testMetadata testName='${this.escape(test.title)}' type='image' value='report/${filePath}/${fileName}' flowId='${process.pid}']`);
|
70
56
|
});
|
71
|
-
});
|
57
|
+
});
|
58
|
+
|
59
|
+
// Output failed test as passed due TC don't support retry mechanic
|
72
60
|
// https://teamcity-support.jetbrains.com/hc/en-us/community/posts/207216829-Count-test-as-successful-if-at-least-one-try-is-successful?page=1#community_comment_207394125
|
73
61
|
|
74
62
|
reporterOptions.willRetry ? console.log(`##teamcity[testFinished name='${this.escape(test.title)}' flowId='${process.pid}']`) : console.log(`##teamcity[testFailed name='${this.escape(test.title)}' message='${this.escape(error.message)}' details='${this.escape(error.stack ?? '')}' flowId='${process.pid}']`);
|
@@ -78,19 +66,16 @@ class TeamcityReporter extends _mocha.reporters.Base {
|
|
78
66
|
runner.on('suite end', suite => suite.root || console.log(`##teamcity[testSuiteFinished name='${this.escape(suite.title)}' flowId='${process.pid}']`));
|
79
67
|
runner.on('end', () => console.log(`##teamcity[testSuiteFinished name='${topLevelSuite}' flowId='${process.pid}']`));
|
80
68
|
}
|
81
|
-
|
82
69
|
escape = str => {
|
83
70
|
if (!str) return '';
|
84
|
-
return str.toString()
|
71
|
+
return str.toString()
|
72
|
+
// eslint-disable-next-line no-control-regex
|
85
73
|
.replace(/\x1B.*?m/g, '').replace(/\|/g, '||').replace(/\n/g, '|n').replace(/\r/g, '|r').replace(/\[/g, '|[').replace(/\]/g, '|]').replace(/\u0085/g, '|x').replace(/\u2028/g, '|l').replace(/\u2029/g, '|p').replace(/'/g, "|'");
|
86
74
|
};
|
87
75
|
}
|
88
|
-
|
89
76
|
exports.TeamcityReporter = TeamcityReporter;
|
90
|
-
|
91
77
|
function getErrors(error, imageErrorToString, errorToString) {
|
92
78
|
const errors = [];
|
93
|
-
|
94
79
|
if (!(error instanceof Error)) {
|
95
80
|
errors.push(error);
|
96
81
|
} else if (!(0, _types.isImageError)(error)) {
|
@@ -103,6 +88,5 @@ function getErrors(error, imageErrorToString, errorToString) {
|
|
103
88
|
errors.push(imageErrorToString(imageErrors[imageName] ?? '', imageName));
|
104
89
|
});
|
105
90
|
}
|
106
|
-
|
107
91
|
return errors;
|
108
92
|
}
|
@@ -4,43 +4,26 @@ Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
5
5
|
});
|
6
6
|
exports.default = worker;
|
7
|
-
|
8
7
|
var _util = require("util");
|
9
|
-
|
10
8
|
var _fs = _interopRequireDefault(require("fs"));
|
11
|
-
|
12
9
|
var _path = _interopRequireDefault(require("path"));
|
13
|
-
|
14
10
|
var _chai = _interopRequireDefault(require("chai"));
|
15
|
-
|
16
11
|
var _chalk = _interopRequireDefault(require("chalk"));
|
17
|
-
|
18
12
|
var _mocha = _interopRequireDefault(require("mocha"));
|
19
|
-
|
20
13
|
var _seleniumWebdriver = require("selenium-webdriver");
|
21
|
-
|
22
14
|
var _types = require("../../types");
|
23
|
-
|
24
15
|
var _messages = require("../messages");
|
25
|
-
|
26
16
|
var _chaiImage = _interopRequireDefault(require("./chai-image"));
|
27
|
-
|
28
17
|
var _selenium = require("../selenium");
|
29
|
-
|
30
18
|
var _reporter = require("./reporter");
|
31
|
-
|
32
19
|
var _helpers = require("./helpers");
|
33
|
-
|
34
20
|
var _logger = require("../logger");
|
35
|
-
|
36
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
37
|
-
|
21
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
38
22
|
const statAsync = (0, _util.promisify)(_fs.default.stat);
|
39
23
|
const readdirAsync = (0, _util.promisify)(_fs.default.readdir);
|
40
24
|
const readFileAsync = (0, _util.promisify)(_fs.default.readFile);
|
41
25
|
const writeFileAsync = (0, _util.promisify)(_fs.default.writeFile);
|
42
26
|
const mkdirAsync = (0, _util.promisify)(_fs.default.mkdir);
|
43
|
-
|
44
27
|
async function getStat(filePath) {
|
45
28
|
try {
|
46
29
|
return await statAsync(filePath);
|
@@ -48,31 +31,26 @@ async function getStat(filePath) {
|
|
48
31
|
if (typeof error == 'object' && error && error.code === 'ENOENT') {
|
49
32
|
return null;
|
50
33
|
}
|
51
|
-
|
52
34
|
throw error;
|
53
35
|
}
|
54
36
|
}
|
55
|
-
|
56
37
|
async function getLastImageNumber(imageDir, imageName) {
|
57
38
|
const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`);
|
58
|
-
|
59
39
|
try {
|
60
40
|
return (await readdirAsync(imageDir)).map(filename => filename.replace(actualImagesRegexp, '$1')).map(Number).filter(x => !isNaN(x)).sort((a, b) => b - a)[0] ?? 0;
|
61
41
|
} catch (_error) {
|
62
42
|
return 0;
|
63
43
|
}
|
64
|
-
}
|
65
|
-
|
44
|
+
}
|
66
45
|
|
46
|
+
// FIXME browser options hotfix
|
67
47
|
async function worker(config, options) {
|
68
48
|
var _await$browser$getSes;
|
69
|
-
|
70
49
|
let retries = 0;
|
71
50
|
let images = {};
|
72
51
|
let error = undefined;
|
73
52
|
const screenshots = [];
|
74
53
|
const testScope = [];
|
75
|
-
|
76
54
|
function runHandler(failures) {
|
77
55
|
if (failures > 0 && (error || Object.values(images).some(image => (image === null || image === void 0 ? void 0 : image.error) != null))) {
|
78
56
|
const isTimeout = hasTimeout(error) || Object.values(images).some(image => hasTimeout(image === null || image === void 0 ? void 0 : image.error));
|
@@ -100,12 +78,10 @@ async function worker(config, options) {
|
|
100
78
|
});
|
101
79
|
}
|
102
80
|
}
|
103
|
-
|
104
81
|
async function saveImages(imageDir, images) {
|
105
82
|
await mkdirAsync(imageDir, {
|
106
83
|
recursive: true
|
107
84
|
});
|
108
|
-
|
109
85
|
for (const {
|
110
86
|
name,
|
111
87
|
data
|
@@ -113,7 +89,6 @@ async function worker(config, options) {
|
|
113
89
|
await writeFileAsync(_path.default.join(imageDir, name), data);
|
114
90
|
}
|
115
91
|
}
|
116
|
-
|
117
92
|
async function getExpected(assertImageName) {
|
118
93
|
// context => [kind, story, test, browser]
|
119
94
|
// rootSuite -> kindSuite -> storyTest -> [browsers.png]
|
@@ -121,21 +96,17 @@ async function worker(config, options) {
|
|
121
96
|
const testPath = [...testScope];
|
122
97
|
const imageName = assertImageName ?? testPath.pop();
|
123
98
|
const imagesMeta = [];
|
124
|
-
|
125
99
|
const reportImageDir = _path.default.join(config.reportDir, ...testPath);
|
126
|
-
|
127
100
|
const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1;
|
128
101
|
const actualImageName = `${imageName}-actual-${imageNumber}.png`;
|
129
102
|
const image = images[imageName] = images[imageName] ?? {
|
130
103
|
actual: actualImageName
|
131
104
|
};
|
132
|
-
|
133
105
|
const onCompare = async (actual, expect, diff) => {
|
134
106
|
imagesMeta.push({
|
135
107
|
name: image.actual,
|
136
108
|
data: actual
|
137
109
|
});
|
138
|
-
|
139
110
|
if (diff && expect) {
|
140
111
|
image.expect = `${imageName}-expect-${imageNumber}.png`;
|
141
112
|
image.diff = `${imageName}-diff-${imageNumber}.png`;
|
@@ -148,14 +119,11 @@ async function worker(config, options) {
|
|
148
119
|
data: diff
|
149
120
|
});
|
150
121
|
}
|
151
|
-
|
152
122
|
if (options.saveReport) {
|
153
123
|
await saveImages(reportImageDir, imagesMeta);
|
154
124
|
}
|
155
125
|
};
|
156
|
-
|
157
126
|
const expectImageDir = _path.default.join(config.screenDir, ...testPath);
|
158
|
-
|
159
127
|
const expectImageStat = await getStat(_path.default.join(expectImageDir, `${imageName}.png`));
|
160
128
|
if (!expectImageStat) return {
|
161
129
|
expected: null,
|
@@ -167,33 +135,26 @@ async function worker(config, options) {
|
|
167
135
|
onCompare
|
168
136
|
};
|
169
137
|
}
|
170
|
-
|
171
138
|
const mochaOptions = {
|
172
139
|
timeout: 30000,
|
173
140
|
reporter: process.env.TEAMCITY_VERSION ? _reporter.TeamcityReporter : options.reporter || _reporter.CreeveyReporter,
|
174
141
|
reporterOptions: {
|
175
142
|
reportDir: config.reportDir,
|
176
143
|
topLevelSuite: options.browser,
|
177
|
-
|
178
144
|
get willRetry() {
|
179
145
|
return retries < config.maxRetries;
|
180
146
|
},
|
181
|
-
|
182
147
|
get images() {
|
183
148
|
return images;
|
184
149
|
},
|
185
|
-
|
186
150
|
get sessionId() {
|
187
151
|
return sessionId;
|
188
152
|
}
|
189
|
-
|
190
153
|
}
|
191
154
|
};
|
192
155
|
const mocha = new _mocha.default(mochaOptions);
|
193
156
|
mocha.cleanReferencesAfterRun(false);
|
194
|
-
|
195
157
|
_chai.default.use((0, _chaiImage.default)(getExpected, config.diffOptions));
|
196
|
-
|
197
158
|
if ((await (0, _selenium.getBrowser)(config, options)) == null) return;
|
198
159
|
await (0, _helpers.addTestsFromStories)(mocha.suite, config, {
|
199
160
|
browser: options.browser,
|
@@ -201,15 +162,12 @@ async function worker(config, options) {
|
|
201
162
|
debug: options.debug,
|
202
163
|
port: options.port
|
203
164
|
});
|
204
|
-
|
205
165
|
try {
|
206
166
|
var _await$getBrowser;
|
207
|
-
|
208
167
|
await ((_await$getBrowser = await (0, _selenium.getBrowser)(config, options)) === null || _await$getBrowser === void 0 ? void 0 : _await$getBrowser.getCurrentUrl());
|
209
168
|
} catch (_) {
|
210
169
|
await (0, _selenium.closeBrowser)();
|
211
170
|
}
|
212
|
-
|
213
171
|
const browser = await (0, _selenium.getBrowser)(config, options);
|
214
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();
|
215
173
|
if (browser == null) return;
|
@@ -236,8 +194,9 @@ async function worker(config, options) {
|
|
236
194
|
error = undefined;
|
237
195
|
retries = test.retries;
|
238
196
|
mocha.grep(new RegExp(`^${testPath}$`));
|
239
|
-
const runner = mocha.run(runHandler);
|
197
|
+
const runner = mocha.run(runHandler);
|
240
198
|
|
199
|
+
// TODO How handle browser corruption?
|
241
200
|
runner.on('fail', (_test, reason) => {
|
242
201
|
if (!(reason instanceof Error)) {
|
243
202
|
error = reason;
|
@@ -255,14 +214,11 @@ async function worker(config, options) {
|
|
255
214
|
}
|
256
215
|
});
|
257
216
|
});
|
258
|
-
|
259
217
|
_logger.logger.info(`${options.browser}:${_chalk.default.gray(sessionId)} is ready`);
|
260
|
-
|
261
218
|
(0, _messages.emitWorkerMessage)({
|
262
219
|
type: 'ready'
|
263
220
|
});
|
264
221
|
}
|
265
|
-
|
266
222
|
function hasTimeout(str) {
|
267
223
|
return str != null && str.toLowerCase().includes('timeout');
|
268
224
|
}
|