@telefonica/acceptance-testing 4.1.0 → 5.0.0-beta2
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/README.md +21 -2
- package/dist/config.d.ts +22 -0
- package/dist/config.js +57 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +4 -0
- package/dist/coverage.d.ts +15 -15
- package/dist/coverage.js +119 -0
- package/dist/index.d.ts +86 -91
- package/dist/index.js +424 -8
- package/dist/jest-puppeteer.config.d.ts +1 -0
- package/{jest-puppeteer-config.js → dist/jest-puppeteer.config.js} +19 -60
- package/dist/utils.d.ts +12 -8
- package/dist/utils.js +43 -0
- package/dist/wsl.d.ts +6 -0
- package/dist/wsl.js +49 -0
- package/jest-puppeteer.config.js +1 -0
- package/package.json +15 -13
- package/dist/acceptance-testing.cjs.development.js +0 -1472
- package/dist/acceptance-testing.cjs.development.js.map +0 -1
- package/dist/acceptance-testing.cjs.production.min.js +0 -2
- package/dist/acceptance-testing.cjs.production.min.js.map +0 -1
- package/dist/acceptance-testing.esm.js +0 -1453
- package/dist/acceptance-testing.esm.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,424 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.waitForElementToBeRemoved = exports.wait = exports.prepareFile = exports.within = exports.screen = exports.getScreen = exports.openPage = exports.createApiEndpointMock = exports.interceptRequest = exports.getPageApi = exports.serverPort = exports.serverHostName = exports.getGlobalPage = exports.getGlobalBrowser = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const pptr_testing_library_1 = require("pptr-testing-library");
|
|
10
|
+
const jest_image_snapshot_1 = require("jest-image-snapshot");
|
|
11
|
+
const glob_to_regexp_1 = __importDefault(require("glob-to-regexp"));
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
const coverage_1 = require("./coverage");
|
|
14
|
+
const utils_1 = require("./utils");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const wsl_1 = require("./wsl");
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
const { isCi, server, coveragePath, coverageUrls, collectCoverage } = (0, config_1.getConfig)();
|
|
19
|
+
(0, utils_1.debug)('Config:', (0, config_1.getConfig)());
|
|
20
|
+
const getGlobalBrowser = () => global.browser;
|
|
21
|
+
exports.getGlobalBrowser = getGlobalBrowser;
|
|
22
|
+
const getGlobalPage = () => global.page;
|
|
23
|
+
exports.getGlobalPage = getGlobalPage;
|
|
24
|
+
const isUsingDockerizedChromium = isCi || new URL((0, exports.getGlobalBrowser)().wsEndpoint()).port === '9223';
|
|
25
|
+
exports.serverHostName = (() => {
|
|
26
|
+
if (isCi) {
|
|
27
|
+
return 'localhost';
|
|
28
|
+
}
|
|
29
|
+
if (isUsingDockerizedChromium) {
|
|
30
|
+
if ((0, wsl_1.isWsl)()) {
|
|
31
|
+
if ((0, wsl_1.isWsl2)()) {
|
|
32
|
+
return (0, wsl_1.getWslHostIp)();
|
|
33
|
+
}
|
|
34
|
+
throw new Error('WSL 1 is not supported. Please, use WSL 2.');
|
|
35
|
+
}
|
|
36
|
+
if (process.platform === 'win32') {
|
|
37
|
+
throw new Error('Windows is not supported. Please, use WSL 2.');
|
|
38
|
+
}
|
|
39
|
+
return process.platform === 'linux' ? constants_1.LINUX_DOCKER_HOST_IP : 'host.docker.internal';
|
|
40
|
+
}
|
|
41
|
+
return 'localhost';
|
|
42
|
+
})();
|
|
43
|
+
exports.serverPort = server.port;
|
|
44
|
+
const toMatchImageSnapshot = (0, jest_image_snapshot_1.configureToMatchImageSnapshot)({
|
|
45
|
+
failureThreshold: 0,
|
|
46
|
+
failureThresholdType: 'percent',
|
|
47
|
+
customSnapshotIdentifier: ({ defaultIdentifier }) => defaultIdentifier,
|
|
48
|
+
});
|
|
49
|
+
let calledToMatchImageSnapshotOutsideDocker = false;
|
|
50
|
+
const localToMatchImageSnapshot = () => {
|
|
51
|
+
calledToMatchImageSnapshotOutsideDocker = true;
|
|
52
|
+
// let the expectation pass, then fail in afterEach. This way we allow developers to debug screenshot tests in local
|
|
53
|
+
// but don't allow them to save screenshots taken outside the dockerized chromium
|
|
54
|
+
return {
|
|
55
|
+
message: () => '',
|
|
56
|
+
pass: true,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
expect.extend({
|
|
60
|
+
toMatchImageSnapshot: isUsingDockerizedChromium ? toMatchImageSnapshot : localToMatchImageSnapshot,
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (calledToMatchImageSnapshotOutsideDocker) {
|
|
64
|
+
const error = new Error(`Calling .toMatchImageSnapshot() is not allowed outside dockerized browser. Please, run your screenshot test in headless mode.`);
|
|
65
|
+
error.stack = (error.stack || '').split('\n')[0];
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const waitForPaintEnd = async (element, { fullPage = true, captureBeyondViewport } = {}) => {
|
|
70
|
+
const MAX_WAIT = 15000;
|
|
71
|
+
const STEP_TIME = 250;
|
|
72
|
+
const t0 = Date.now();
|
|
73
|
+
let buf1 = (await element.screenshot(normalizeSreenshotOptions({ fullPage, captureBeyondViewport })));
|
|
74
|
+
await new Promise((r) => setTimeout(r, STEP_TIME));
|
|
75
|
+
let buf2 = (await element.screenshot(normalizeSreenshotOptions({ fullPage, captureBeyondViewport })));
|
|
76
|
+
// buffers are different if compare != 0
|
|
77
|
+
while (buf1.compare(buf2)) {
|
|
78
|
+
if (Date.now() - t0 > MAX_WAIT) {
|
|
79
|
+
throw Error('Paint end timeout');
|
|
80
|
+
}
|
|
81
|
+
buf1 = buf2;
|
|
82
|
+
await new Promise((r) => setTimeout(r, STEP_TIME));
|
|
83
|
+
buf2 = (await element.screenshot(normalizeSreenshotOptions({ fullPage, captureBeyondViewport })));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const normalizeSreenshotOptions = ({ captureBeyondViewport = false, ...options } = {}) => {
|
|
87
|
+
// Puppeter default for captureBeyondViewport is true, but we think false is a better default.
|
|
88
|
+
// When this is true, the fixed elements (like fixed footers) are relative to the original page
|
|
89
|
+
// viewport, not to the full page, so those elements look weird in fullPage screenshots.
|
|
90
|
+
return { ...options, captureBeyondViewport };
|
|
91
|
+
};
|
|
92
|
+
// Puppeteer already calls scrollIntoViewIfNeeded before clicking an element. But it doesn't work in all situations
|
|
93
|
+
// For example, when there is a fixed footer in the page and the element to click is under it, the browser won't scroll
|
|
94
|
+
// because the element is already in the viewport (the ifNeeded part is important here). By forcing the scroll to the
|
|
95
|
+
// center, we manage to fix these edge cases.
|
|
96
|
+
const scrollIntoView = (el) => el.evaluate((e) => e.scrollIntoView({ block: 'center' }));
|
|
97
|
+
const getPageApi = (page) => {
|
|
98
|
+
const api = Object.create(page);
|
|
99
|
+
api.type = async (elementHandle, text, options) => {
|
|
100
|
+
await scrollIntoView(elementHandle);
|
|
101
|
+
return elementHandle.type(text, options);
|
|
102
|
+
};
|
|
103
|
+
api.click = async (elementHandle, options) => {
|
|
104
|
+
await scrollIntoView(elementHandle);
|
|
105
|
+
return elementHandle.click(options);
|
|
106
|
+
};
|
|
107
|
+
api.select = async (elementHandle, ...values) => {
|
|
108
|
+
await scrollIntoView(elementHandle);
|
|
109
|
+
return elementHandle.select(...values);
|
|
110
|
+
};
|
|
111
|
+
api.screenshot = async (options) => {
|
|
112
|
+
if (!options?.skipNetworkWait) {
|
|
113
|
+
await page.waitForNetworkIdle();
|
|
114
|
+
}
|
|
115
|
+
await waitForPaintEnd(page, options);
|
|
116
|
+
return page.screenshot(normalizeSreenshotOptions(options));
|
|
117
|
+
};
|
|
118
|
+
api.clear = async (elementHandle) => {
|
|
119
|
+
await elementHandle.click({ clickCount: 3 });
|
|
120
|
+
await elementHandle.press('Delete');
|
|
121
|
+
};
|
|
122
|
+
// For some reason, puppeteer browserContext.overridePermissions doesn't work with newer chrome versions.
|
|
123
|
+
// This workaround polyfills the browser geolocation api to return the mocked position
|
|
124
|
+
api.setGeolocation = (position) => page.evaluate((position) => {
|
|
125
|
+
window.navigator.geolocation.getCurrentPosition = (callback) => {
|
|
126
|
+
// @ts-expect-error - puppeteer's setGeoLocation does not expect a timestamp to be passed
|
|
127
|
+
callback({
|
|
128
|
+
coords: position,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
}, position);
|
|
132
|
+
return api;
|
|
133
|
+
};
|
|
134
|
+
exports.getPageApi = getPageApi;
|
|
135
|
+
let needsRequestInterception = false;
|
|
136
|
+
let requestHandlers = [];
|
|
137
|
+
const requestInterceptor = async (req) => {
|
|
138
|
+
const { handler } = requestHandlers.find(({ matcher }) => matcher(req)) ?? { handler: null };
|
|
139
|
+
if (!handler) {
|
|
140
|
+
req.continue();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const response = await handler(req);
|
|
144
|
+
req.respond(response);
|
|
145
|
+
};
|
|
146
|
+
const interceptRequest = (matcher) => {
|
|
147
|
+
needsRequestInterception = true;
|
|
148
|
+
const spy = jest.fn();
|
|
149
|
+
requestHandlers.push({ matcher, handler: spy });
|
|
150
|
+
return spy;
|
|
151
|
+
};
|
|
152
|
+
exports.interceptRequest = interceptRequest;
|
|
153
|
+
const createApiEndpointMock = ({ origin = '*' } = {}) => {
|
|
154
|
+
const originRegExp = (0, glob_to_regexp_1.default)(origin);
|
|
155
|
+
(0, exports.interceptRequest)((req) => {
|
|
156
|
+
const { origin } = new URL(req.url());
|
|
157
|
+
return req.method() === 'OPTIONS' && !!origin.match(originRegExp);
|
|
158
|
+
}).mockImplementation(() => ({
|
|
159
|
+
status: 204,
|
|
160
|
+
headers: {
|
|
161
|
+
'Access-Control-Allow-Origin': '*',
|
|
162
|
+
'Access-Control-Allow-Methods': 'POST,PATCH,PUT,GET,OPTIONS,DELETE',
|
|
163
|
+
'Access-Control-Allow-Headers': '*',
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
return {
|
|
167
|
+
spyOn(spiedPath, method = 'GET') {
|
|
168
|
+
const matcher = (req) => {
|
|
169
|
+
const { origin, pathname, search } = new URL(req.url());
|
|
170
|
+
const pathWithParams = pathname + search;
|
|
171
|
+
return (req.method() === method &&
|
|
172
|
+
!!origin.match(originRegExp) &&
|
|
173
|
+
(0, utils_1.matchPath)(spiedPath, pathWithParams));
|
|
174
|
+
};
|
|
175
|
+
const spy = jest.fn();
|
|
176
|
+
(0, exports.interceptRequest)(matcher).mockImplementation(async (req) => {
|
|
177
|
+
const spyResult = await spy(req);
|
|
178
|
+
const status = spyResult.status ?? 200;
|
|
179
|
+
const resBody = spyResult.body || spyResult;
|
|
180
|
+
return {
|
|
181
|
+
status,
|
|
182
|
+
headers: {
|
|
183
|
+
'Access-Control-Allow-Origin': '*',
|
|
184
|
+
},
|
|
185
|
+
contentType: 'application/json',
|
|
186
|
+
body: JSON.stringify(resBody),
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
return spy;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
exports.createApiEndpointMock = createApiEndpointMock;
|
|
194
|
+
const openPage = async ({ userAgent, isDarkMode, viewport, cookies, ...urlConfig }) => {
|
|
195
|
+
const url = (() => {
|
|
196
|
+
if (urlConfig.url !== undefined) {
|
|
197
|
+
return urlConfig.url;
|
|
198
|
+
}
|
|
199
|
+
const { path = '/', port = exports.serverPort, protocol = 'http', hostname = exports.serverHostName } = urlConfig;
|
|
200
|
+
if (!port) {
|
|
201
|
+
// Error.captureStackTrace(error, openPage);
|
|
202
|
+
throw new Error('You must specify a port. You can specify it when calling openPage() or by configuring a dev and ci server in the acceptanceTests config in your package.json');
|
|
203
|
+
}
|
|
204
|
+
return `${protocol}://${hostname}:${port}${path}`;
|
|
205
|
+
})();
|
|
206
|
+
(0, utils_1.debug)('Opening page:', url);
|
|
207
|
+
const currentUserAgent = userAgent || (await (0, exports.getGlobalBrowser)().userAgent());
|
|
208
|
+
const page = (0, exports.getGlobalPage)();
|
|
209
|
+
await page.bringToFront();
|
|
210
|
+
if (viewport) {
|
|
211
|
+
await page.setViewport(viewport);
|
|
212
|
+
}
|
|
213
|
+
if (cookies) {
|
|
214
|
+
await page.setCookie(...cookies);
|
|
215
|
+
}
|
|
216
|
+
await page.setUserAgent(`${currentUserAgent} acceptance-test`);
|
|
217
|
+
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: isDarkMode ? 'dark' : 'light' }]);
|
|
218
|
+
// A set of styles to make screenshot tests more reliable.
|
|
219
|
+
await page.evaluateOnNewDocument((viewport) => {
|
|
220
|
+
const overriddenSafeAreaInsets = !viewport
|
|
221
|
+
? []
|
|
222
|
+
: Object.keys(viewport?.safeAreaInset ?? {}).map((key) => {
|
|
223
|
+
const position = key;
|
|
224
|
+
return `--acceptance-test-override-safe-area-inset-${key}: ${viewport?.safeAreaInset?.[position]};`;
|
|
225
|
+
});
|
|
226
|
+
const style = document.createElement('style');
|
|
227
|
+
style.innerHTML = `
|
|
228
|
+
*, *:after, *:before {
|
|
229
|
+
transition-delay: 0s !important;
|
|
230
|
+
transition-duration: 0s !important;
|
|
231
|
+
animation-delay: -0.0001s !important;
|
|
232
|
+
animation-duration: 0s !important;
|
|
233
|
+
animation-play-state: paused !important;
|
|
234
|
+
caret-color: transparent !important;
|
|
235
|
+
font-variant-ligatures: none !important;
|
|
236
|
+
}
|
|
237
|
+
*::-webkit-scrollbar {
|
|
238
|
+
display: none !important;
|
|
239
|
+
width: 0 !important;
|
|
240
|
+
height: 0 !important;
|
|
241
|
+
}
|
|
242
|
+
:root {
|
|
243
|
+
${overriddenSafeAreaInsets.join('\n')}
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
247
|
+
document.head.appendChild(style);
|
|
248
|
+
});
|
|
249
|
+
}, viewport);
|
|
250
|
+
if (needsRequestInterception) {
|
|
251
|
+
await page.setRequestInterception(true);
|
|
252
|
+
page.on('request', requestInterceptor);
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await page.goto(url);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
if (e.message.includes('net::ERR_CONNECTION_REFUSED')) {
|
|
259
|
+
const connectionError = new Error(`Could not connect to ${url}. Is the server running?`);
|
|
260
|
+
Error.captureStackTrace(connectionError, exports.openPage);
|
|
261
|
+
throw connectionError;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
await page.waitForFunction('document.fonts.status === "loaded"');
|
|
268
|
+
return (0, exports.getPageApi)(page);
|
|
269
|
+
};
|
|
270
|
+
exports.openPage = openPage;
|
|
271
|
+
const buildQueryMethods = ({ page, element } = {}) => {
|
|
272
|
+
const boundQueries = {};
|
|
273
|
+
for (const [queryName, queryFn] of Object.entries(pptr_testing_library_1.queries)) {
|
|
274
|
+
boundQueries[queryName] = async (...args) => {
|
|
275
|
+
const doc = await (0, pptr_testing_library_1.getDocument)(page ?? (0, exports.getGlobalPage)());
|
|
276
|
+
const body = await doc.$('body');
|
|
277
|
+
const queryArgs = [...args];
|
|
278
|
+
if (queryName.startsWith('findBy')) {
|
|
279
|
+
if (queryArgs.length === 1) {
|
|
280
|
+
queryArgs.push(undefined);
|
|
281
|
+
}
|
|
282
|
+
queryArgs.push({ timeout: 10000 });
|
|
283
|
+
}
|
|
284
|
+
const elementHandle = await queryFn(element ?? body, ...queryArgs);
|
|
285
|
+
const newElementHandle = Object.create(elementHandle);
|
|
286
|
+
newElementHandle.screenshot = async (options) => {
|
|
287
|
+
if (!options?.skipNetworkWait) {
|
|
288
|
+
await (page ?? (0, exports.getGlobalPage)()).waitForNetworkIdle();
|
|
289
|
+
}
|
|
290
|
+
await waitForPaintEnd(elementHandle, { ...options, fullPage: false });
|
|
291
|
+
return elementHandle.screenshot(normalizeSreenshotOptions(options));
|
|
292
|
+
};
|
|
293
|
+
newElementHandle.click = async (options) => {
|
|
294
|
+
await scrollIntoView(elementHandle);
|
|
295
|
+
return elementHandle.click(options);
|
|
296
|
+
};
|
|
297
|
+
newElementHandle.type = async (text, options) => {
|
|
298
|
+
await scrollIntoView(elementHandle);
|
|
299
|
+
return elementHandle.type(text, options);
|
|
300
|
+
};
|
|
301
|
+
newElementHandle.select = async (...values) => {
|
|
302
|
+
await scrollIntoView(elementHandle);
|
|
303
|
+
return elementHandle.select(...values);
|
|
304
|
+
};
|
|
305
|
+
return newElementHandle;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return boundQueries;
|
|
309
|
+
};
|
|
310
|
+
const getScreen = (page) => buildQueryMethods({ page });
|
|
311
|
+
exports.getScreen = getScreen;
|
|
312
|
+
exports.screen = buildQueryMethods();
|
|
313
|
+
const within = (element) => buildQueryMethods({ element });
|
|
314
|
+
exports.within = within;
|
|
315
|
+
beforeEach(async () => {
|
|
316
|
+
await (0, exports.getGlobalPage)().setRequestInterception(false);
|
|
317
|
+
// by resetting the page we clean up all the evaluateOnNewDocument calls, which are persistent between documents
|
|
318
|
+
await global.jestPuppeteer.resetPage();
|
|
319
|
+
});
|
|
320
|
+
afterEach(async () => {
|
|
321
|
+
if (collectCoverage) {
|
|
322
|
+
await (0, coverage_1.collectFrontendCoverage)({ coveragePath });
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const page = (0, exports.getGlobalPage)();
|
|
326
|
+
requestHandlers = [];
|
|
327
|
+
needsRequestInterception = false;
|
|
328
|
+
page.off('request', requestInterceptor);
|
|
329
|
+
// clear tab, this way we clear the DOM and stop js execution or pending requests
|
|
330
|
+
await page.goto('about:blank');
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
// ignore, at this point page might be destroyed
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
afterAll(async () => {
|
|
337
|
+
if (collectCoverage) {
|
|
338
|
+
await (0, coverage_1.collectBackendCoverage)({ coveragePath, coverageUrls });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* Returns a new path to the file that can be used by chromium in acceptance tests
|
|
343
|
+
*
|
|
344
|
+
* To be able to use `element.uploadFile()` in a dockerized chromium, the file must exist in the
|
|
345
|
+
* host and the docker, and both sides must use the same path.
|
|
346
|
+
*
|
|
347
|
+
* To workaround this bug or limitation, this function prepares the file by copying it to /tmp in
|
|
348
|
+
* the host and the container.
|
|
349
|
+
*/
|
|
350
|
+
const prepareFile = (filepath) => {
|
|
351
|
+
const isLocal = !isCi;
|
|
352
|
+
const isHeadless = !!process.env.HEADLESS;
|
|
353
|
+
const usesDocker = isLocal && isHeadless;
|
|
354
|
+
const dockerComposeFile = path_1.default.join(__dirname, '..', 'docker-compose.yaml');
|
|
355
|
+
if (usesDocker) {
|
|
356
|
+
const containerId = (0, child_process_1.execSync)(`docker compose -f ${dockerComposeFile} ps -q`).toString().trim();
|
|
357
|
+
if (!containerId) {
|
|
358
|
+
throw Error('acceptance-testing container not found');
|
|
359
|
+
}
|
|
360
|
+
(0, child_process_1.execSync)(`docker cp ${filepath} ${containerId}:/tmp`);
|
|
361
|
+
const newPath = path_1.default.join('/tmp', path_1.default.basename(filepath));
|
|
362
|
+
fs_1.default.copyFileSync(filepath, newPath);
|
|
363
|
+
return newPath;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
return filepath;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
exports.prepareFile = prepareFile;
|
|
370
|
+
/**
|
|
371
|
+
* A convenience method to defer an expectation
|
|
372
|
+
*/
|
|
373
|
+
const wait = (expectation, timeout = 10000, interval = 50) => {
|
|
374
|
+
const startTime = Date.now();
|
|
375
|
+
const startStack = new Error().stack;
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const rejectOrRerun = (error) => {
|
|
378
|
+
if (Date.now() - startTime >= timeout) {
|
|
379
|
+
if (error instanceof Error) {
|
|
380
|
+
if (error.message === 'Element not removed') {
|
|
381
|
+
error.stack = startStack;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
reject(error);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
setTimeout(runExpectation, interval);
|
|
388
|
+
};
|
|
389
|
+
const runExpectation = () => {
|
|
390
|
+
try {
|
|
391
|
+
Promise.resolve(expectation())
|
|
392
|
+
.then((r) => resolve(r))
|
|
393
|
+
.catch(rejectOrRerun);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
rejectOrRerun(error);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
setTimeout(runExpectation, 0);
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
exports.wait = wait;
|
|
403
|
+
const waitForElementToBeRemoved = (element, timeout = 10000, interval = 100) => {
|
|
404
|
+
const startStack = new Error().stack;
|
|
405
|
+
const wait = async () => {
|
|
406
|
+
const t0 = Date.now();
|
|
407
|
+
while (Date.now() - t0 < timeout) {
|
|
408
|
+
// boundingBox returns null when the element is not in the DOM
|
|
409
|
+
const box = await element.boundingBox();
|
|
410
|
+
if (!box) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
414
|
+
}
|
|
415
|
+
throw new Error('Element not removed');
|
|
416
|
+
};
|
|
417
|
+
return wait().catch((error) => {
|
|
418
|
+
if (error.message === 'Element not removed') {
|
|
419
|
+
error.stack = startStack;
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
exports.waitForElementToBeRemoved = waitForElementToBeRemoved;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,89 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const fetch = require('node-fetch');
|
|
5
|
-
const execSync = require('child_process').execSync;
|
|
6
|
-
|
|
7
|
-
const poll = async (url) => {
|
|
8
|
-
let tries = 10;
|
|
9
|
-
while (tries--) {
|
|
10
|
-
try {
|
|
11
|
-
return await fetch(url);
|
|
12
|
-
} catch (e) {
|
|
13
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
throw Error(`Error fetching ${url}`);
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
4
|
};
|
|
18
|
-
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const config_1 = require("./config");
|
|
19
10
|
/**
|
|
20
11
|
* CI => everything runs inside a docker
|
|
21
12
|
* Local, headless => connects to a dockerized chromium at port 9223 (see docker-compose.yaml)
|
|
22
13
|
* Local, with UI => launches a local chromium installed by puppetteer
|
|
23
14
|
*/
|
|
24
15
|
const getPuppeteerConfig = async () => {
|
|
25
|
-
const isCi =
|
|
26
|
-
const isLocal = !isCi;
|
|
27
|
-
const isHeadless = !!process.env.HEADLESS;
|
|
28
|
-
|
|
29
|
-
const rootDir = findRoot(process.cwd());
|
|
30
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
31
|
-
const projectConfig = pkg.acceptanceTests || {};
|
|
32
|
-
|
|
33
|
-
const server = (isCi ? projectConfig.ciServer : projectConfig.devServer) || projectConfig.server || {};
|
|
34
|
-
|
|
16
|
+
const { isCi, isLocal, isHeadless, server } = (0, config_1.getConfig)();
|
|
35
17
|
const baseConfig = {
|
|
36
18
|
ignoreHTTPSErrors: true,
|
|
37
19
|
headless: isHeadless,
|
|
38
20
|
slowMo: isHeadless ? 0 : 50,
|
|
39
21
|
};
|
|
40
|
-
|
|
41
22
|
let connect;
|
|
42
|
-
|
|
43
23
|
if (isLocal && isHeadless) {
|
|
44
24
|
const dockerChromiumUrl = 'http://localhost:9223';
|
|
45
|
-
|
|
46
25
|
try {
|
|
47
26
|
await fetch(dockerChromiumUrl);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const dockerComposeFile = path_1.default.join(__dirname, '/../docker-compose.yaml');
|
|
51
30
|
process.on('SIGINT', () => {
|
|
52
31
|
process.exit(1); // this triggers the 'exit' event handler below when the process is killed with Ctrl+C
|
|
53
32
|
});
|
|
54
|
-
|
|
55
33
|
process.on('exit', () => {
|
|
56
|
-
execSync(`docker compose -f ${dockerComposeFile} stop -t 0`, {stdio: 'ignore'});
|
|
34
|
+
(0, child_process_1.execSync)(`docker compose -f ${dockerComposeFile} stop -t 0`, { stdio: 'ignore' });
|
|
57
35
|
});
|
|
58
|
-
|
|
59
|
-
execSync(`docker compose -f ${dockerComposeFile} up -d`, {stdio: 'inherit'});
|
|
36
|
+
(0, child_process_1.execSync)(`docker compose -f ${dockerComposeFile} up -d`, { stdio: 'inherit' });
|
|
60
37
|
console.log();
|
|
61
|
-
|
|
62
|
-
await poll(dockerChromiumUrl);
|
|
38
|
+
await (0, utils_1.poll)(dockerChromiumUrl);
|
|
63
39
|
}
|
|
64
|
-
|
|
65
|
-
const {webSocketDebuggerUrl} = await fetch(`${dockerChromiumUrl}/json/version`).then((r) => r.json());
|
|
66
|
-
|
|
40
|
+
const { webSocketDebuggerUrl } = await fetch(`${dockerChromiumUrl}/json/version`).then((r) => r.json());
|
|
67
41
|
connect = {
|
|
68
42
|
...baseConfig,
|
|
69
43
|
browserWSEndpoint: webSocketDebuggerUrl,
|
|
70
44
|
};
|
|
71
45
|
}
|
|
72
|
-
|
|
73
|
-
const defaultServerConfig = {
|
|
74
|
-
command: 'tail -f /dev/null',
|
|
75
|
-
host: 'localhost',
|
|
76
|
-
protocol: server.path ? 'http' : 'tcp',
|
|
77
|
-
debug: isCi,
|
|
78
|
-
usedPortAction: isCi ? 'error' : 'ignore', // In dev, if port is already taken, assume server is already running.
|
|
79
|
-
launchTimeout: 60000,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
46
|
return {
|
|
83
47
|
launch: {
|
|
84
48
|
...baseConfig,
|
|
85
49
|
// Launch chromium installed in docker in CI
|
|
86
|
-
...(isCi ? {executablePath: '/usr/bin/chromium'} : {}),
|
|
50
|
+
...(isCi ? { executablePath: '/usr/bin/chromium' } : {}),
|
|
87
51
|
env: {
|
|
88
52
|
...process.env,
|
|
89
53
|
TZ: 'UTC',
|
|
@@ -112,13 +76,8 @@ const getPuppeteerConfig = async () => {
|
|
|
112
76
|
},
|
|
113
77
|
connect,
|
|
114
78
|
browserContext: 'incognito',
|
|
115
|
-
server
|
|
116
|
-
|
|
117
|
-
...defaultServerConfig,
|
|
118
|
-
...server,
|
|
119
|
-
}
|
|
120
|
-
: undefined,
|
|
79
|
+
server,
|
|
80
|
+
exitOnPageError: !process.env.ACCEPTANCE_TESTING_IGNORE_PAGE_ERRORS,
|
|
121
81
|
};
|
|
122
82
|
};
|
|
123
|
-
|
|
124
83
|
module.exports = getPuppeteerConfig();
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug function that logs to the console if the
|
|
3
|
-
*/
|
|
4
|
-
export declare const debug: (...args: Array<unknown>) => void;
|
|
5
|
-
/**
|
|
6
|
-
* Returns true if the current path matches the spied path, including parameters
|
|
7
|
-
*/
|
|
8
|
-
export declare const matchPath: (spiedPath: string, currentPath: string) => boolean;
|
|
1
|
+
/**
|
|
2
|
+
* Debug function that logs to the console if the ACCEPTANCE_TESTING_DEBUG environment variable is set
|
|
3
|
+
*/
|
|
4
|
+
export declare const debug: (...args: Array<unknown>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if the current path matches the spied path, including parameters
|
|
7
|
+
*/
|
|
8
|
+
export declare const matchPath: (spiedPath: string, currentPath: string) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Polls a URL until it returns a successful response
|
|
11
|
+
*/
|
|
12
|
+
export declare const poll: (url: string) => Promise<Response>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.poll = exports.matchPath = exports.debug = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const glob_to_regexp_1 = __importDefault(require("glob-to-regexp"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
/**
|
|
11
|
+
* Debug function that logs to the console if the ACCEPTANCE_TESTING_DEBUG environment variable is set
|
|
12
|
+
*/
|
|
13
|
+
const debug = (...args) => {
|
|
14
|
+
if ((0, config_1.getConfig)().debug) {
|
|
15
|
+
console.debug('[acceptance-testing]', ...args);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
exports.debug = debug;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if the current path matches the spied path, including parameters
|
|
21
|
+
*/
|
|
22
|
+
const matchPath = (spiedPath, currentPath) => {
|
|
23
|
+
const normalizedCurrentPath = path_1.default.normalize(currentPath);
|
|
24
|
+
const pattern = (0, glob_to_regexp_1.default)(spiedPath);
|
|
25
|
+
return pattern.test(normalizedCurrentPath);
|
|
26
|
+
};
|
|
27
|
+
exports.matchPath = matchPath;
|
|
28
|
+
/**
|
|
29
|
+
* Polls a URL until it returns a successful response
|
|
30
|
+
*/
|
|
31
|
+
const poll = async (url) => {
|
|
32
|
+
let tries = 10;
|
|
33
|
+
while (tries--) {
|
|
34
|
+
try {
|
|
35
|
+
return await fetch(url);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw Error(`Error fetching ${url}`);
|
|
42
|
+
};
|
|
43
|
+
exports.poll = poll;
|
package/dist/wsl.d.ts
ADDED
package/dist/wsl.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getWslHostIp = exports.isWsl2 = exports.isWsl = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
/** Check if running inside WSL */
|
|
7
|
+
const isWsl = () => {
|
|
8
|
+
if (process.platform !== 'linux') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return (0, child_process_1.execSync)('which wsl.exe').toString().startsWith('/mnt/');
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
// catched because it throws an error if wsl.exe is not found
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
exports.isWsl = isWsl;
|
|
20
|
+
/** Assumes running inside WSL */
|
|
21
|
+
const isWsl2 = () => {
|
|
22
|
+
// `wsl.exe -l -v` returns something like:
|
|
23
|
+
//
|
|
24
|
+
// NAME STATE VERSION
|
|
25
|
+
// * Ubuntu-22.04 Running 2
|
|
26
|
+
// docker-desktop-data Running 2
|
|
27
|
+
// docker-desktop Running 2
|
|
28
|
+
// Ubuntu-20.04 Stopped 2
|
|
29
|
+
return ((0, child_process_1.execSync)('wsl.exe -l -v')
|
|
30
|
+
.toString()
|
|
31
|
+
// null-bytes are inserted between chars in this command output (UTF-16 output?)
|
|
32
|
+
.replace(/\0/g, '')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
// matches a line like: "* Ubuntu-22.04 Running 2"
|
|
36
|
+
.some((line) => !!line.match(/\*\s+\S+\s+Running\s+2/)));
|
|
37
|
+
};
|
|
38
|
+
exports.isWsl2 = isWsl2;
|
|
39
|
+
/** Assumes running inside WSL */
|
|
40
|
+
const getWslHostIp = () => {
|
|
41
|
+
const ips = (0, child_process_1.execSync)('wsl.exe hostname -I').toString().trim().split(/\s+/);
|
|
42
|
+
if (ips.includes(constants_1.LINUX_DOCKER_HOST_IP)) {
|
|
43
|
+
// looks like a docker engine installed inside linux
|
|
44
|
+
return constants_1.LINUX_DOCKER_HOST_IP;
|
|
45
|
+
}
|
|
46
|
+
// assuming docker-desktop installed in windows
|
|
47
|
+
return ips[0];
|
|
48
|
+
};
|
|
49
|
+
exports.getWslHostIp = getWslHostIp;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/jest.puppeteer-config.js');
|