-
Notifications
You must be signed in to change notification settings - Fork 0
/
playwright.js
253 lines (218 loc) · 6.88 KB
/
playwright.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
/* eslint no-await-in-loop: 0, no-sync: 0, no-use-before-define: 0 */
/**
* Common playwright fixtures and utilities.
*
* - Setup API recording and mocking
* - Collect Coverage
*/
require('dotenv').config({
path: ['.env']
});
const base = require('@playwright/test');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const URL_REGX = new RegExp(
'(' +
[
process.env.NEXT_PUBLIC_API_URL,
// also record all api routes, except test preview mode.
process.env.NEXT_PUBLIC_EMBED_ORIGIN + '/api/(?!test-preview-mode/)'
]
.filter((x) => !!x)
.join(')|(') +
')'
);
exports.URL_REGX = URL_REGX;
function generateUUID() {
return crypto.randomBytes(16).toString('hex');
}
const { COVERAGE, RECORD } = process.env;
const useCoverage = COVERAGE === 'true';
const istanbulTempDir = path.join(process.cwd(), 'coverage-e2e/tmp');
const baseRecordingDir = path.join(__dirname, './api-recordings');
async function getIsAPIRecording(info, browser) {
const recordingDir = path.join(baseRecordingDir, info.titlePath[0]);
const baseRecordingName =
info.titlePath
.slice(1)
.join('_')
.replace(/[\s/]/g, '_')
.replace(/(\.spec)?\.js/, '')
.toLowerCase() +
'.har';
// Recordings are prefixed with the line number for convenience when finding them.
let recordingName = info.line + '_' + baseRecordingName;
let recordingPath = path.join(recordingDir, recordingName);
let fileExists = await (async () => {
try {
await fs.promises.access(recordingPath);
return true;
} catch {
return false;
}
})();
// Since line numbers change often, merge with any existing recordings with the same name.
try {
let existingRecordings = await fs.promises.readdir(recordingDir);
existingRecordings = existingRecordings.filter(
(x) => x.indexOf(baseRecordingName) > -1
);
for (const rec of existingRecordings) {
if (rec === recordingName) continue; // eslint-disable-line no-continue
if (RECORD === 'true') {
// If recording update the file name
if (!fileExists) {
await fs.promises.rename(path.join(recordingDir, rec), recordingPath);
fileExists = true;
} else {
// remove duplicate recordings
await fs.promises.unlink(path.join(recordingDir, rec));
}
} else {
// If not recording use the old file name (assume a change was made that does not require updating recording)
recordingName = rec;
recordingPath = path.join(recordingDir, rec);
fileExists = true;
break;
}
}
} catch (err) {
// ignore
}
if (RECORD !== 'true') {
return { isRecording: false, recordingPath, fileExists };
}
return { isRecording: !fileExists, recordingPath, fileExists };
}
exports.test = base.test.extend({
context: async ({ browser }, use) => {
// Setup recording. If the recording already exists, use it.
const contextOptions = {};
const info = base.test.info();
const { isRecording, recordingPath, fileExists } = await getIsAPIRecording(
info,
browser
);
if (isRecording) {
contextOptions.recordHar = {
path: recordingPath,
mode: 'minimal',
urlFilter: URL_REGX
};
}
const context = await browser.newContext(contextOptions);
if (!isRecording && fileExists) {
await context.routeFromHAR(recordingPath, {
url: URL_REGX
});
} else if (!isRecording) {
// just abort any other requests if there is no recording
await context.route(URL_REGX, (route) => {
route.abort();
});
}
// S3 response mock (HAR recording is buggy with this)
await context.route(/s3\..*/, (route, req) => {
if (req.method() === 'GET') {
return route.fulfill({
status: 200,
body: 'abcde'
});
}
return route.fulfill({
status: 204
});
});
// setup coverage
if (useCoverage) {
await context.addInitScript(() =>
window.addEventListener('beforeunload', () =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
)
);
await fs.promises.mkdir(istanbulTempDir, { recursive: true });
await context.exposeFunction(
'collectIstanbulCoverage',
(coverageJSON) => {
if (coverageJSON)
fs.promises.writeFile(
path.join(
istanbulTempDir,
`playwright_coverage_${generateUUID()}.json`
),
coverageJSON
);
}
);
}
await use(context);
// clean up coverage
if (useCoverage) {
for (const page of context.pages()) {
// eslint-disable-next-line no-await-in-loop
await page.evaluate(() =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
);
}
}
await context.close();
// post process HAR recordings
if (isRecording) {
const infoAgain = base.test.info();
// Clean if failed
if (infoAgain.status === 'failed') {
try {
await fs.promises.unlink(recordingPath);
} catch (err) {
logger.error('Error while cleaning up playwright recordings.');
logger.error(err);
}
} else {
removeAborted(recordingPath); // eslint-disable-line no-use-before-define
}
}
},
page: async ({ page }, use, testInfo) => {
// Setup recording for server side rendering (getStaticProps)
const testName = testInfo.titlePath
.slice(1)
.join('_')
.replace(/[\s/]/g, '_')
.replace(/(\.spec)?\.js/, '')
.toLowerCase();
const baseDir = testInfo.titlePath[0];
// Set the current test name as a cookie
await page.context().addCookies([
{
name: 'testInfo',
value: JSON.stringify({ testName, baseDir }),
domain: 'localhost',
path: '/',
httpOnly: false
}
]);
// Enable preview mode to allow mocking in getStaticProps
await page.goto(`/api/test-preview-mode`);
await use(page);
},
});
function removeAborted(harFilePath) {
// Read the existing HAR file
const har = JSON.parse(fs.readFileSync(harFilePath, 'utf8'));
// Filter out entries with errors like `net::ERR_ABORTED`
const filteredEntries = har.log.entries.filter((entry) => {
// Check if the request failed due to "net::ERR_ABORTED"
return !(
entry.response._failureText &&
entry.response._failureText === 'net::ERR_ABORTED'
);
});
// eslint-disable-next-line no-console
console.warn(`Ignoring ${filteredEntries.length} aborted network requests`);
// Replace the original entries with the filtered ones
har.log.entries = filteredEntries;
// Write the cleaned HAR back to the original file
fs.writeFileSync(harFilePath, JSON.stringify(har, null, 2));
}
exports.expect = base.expect;