@opentermsarchive/engine 12.0.2 → 13.0.1
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/package.json +2 -2
- package/scripts/declarations/validate/index.mocha.js +4 -2
- package/src/archivist/fetcher/fullDomFetcher.js +34 -13
- package/src/archivist/fetcher/fullDomFetcher.test.js +165 -5
- package/src/archivist/fetcher/index.js +1 -1
- package/src/archivist/fetcher/index.test.js +1 -1
- package/src/archivist/index.js +2 -1
- package/src/archivist/services/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentermsarchive/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.1",
|
|
4
4
|
"description": "Tracks and makes visible changes to the terms of online services",
|
|
5
5
|
"homepage": "https://opentermsarchive.org",
|
|
6
6
|
"bugs": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"morgan": "^1.10.1",
|
|
93
93
|
"node-fetch": "^3.3.2",
|
|
94
94
|
"octokit": "^5.0.3",
|
|
95
|
-
"puppeteer": "^
|
|
95
|
+
"puppeteer": "^25.1.0",
|
|
96
96
|
"puppeteer-extra": "^3.3.6",
|
|
97
97
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
98
98
|
"simple-git": "^3.28.0",
|
|
@@ -51,9 +51,11 @@ export default async options => {
|
|
|
51
51
|
const filePath = path.join(declarationsPath, `${serviceId}.json`);
|
|
52
52
|
const historyFilePath = path.join(declarationsPath, `${serviceId}.history.json`);
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
if (!schemaOnly) {
|
|
55
|
+
before(() => launchHeadlessBrowser(config.get('@opentermsarchive/engine.fetcher.language')));
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
after(stopHeadlessBrowser);
|
|
58
|
+
}
|
|
57
59
|
|
|
58
60
|
context(serviceId, () => {
|
|
59
61
|
before(function () {
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import puppeteer from 'puppeteer-extra';
|
|
2
2
|
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
3
|
+
import navigatorLanguages from 'puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js';
|
|
4
|
+
import userAgentOverride from 'puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js';
|
|
3
5
|
|
|
4
6
|
import { resolveProxyConfiguration, extractProxyCredentials } from './proxyUtils.js';
|
|
5
7
|
|
|
6
8
|
let browser;
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
+
export function parseLanguage(value) {
|
|
11
|
+
if (typeof value !== 'string') {
|
|
12
|
+
throw new Error(`Fetcher language must be a string; received ${JSON.stringify(value)}. Pass the value of "@opentermsarchive/engine.fetcher.language" explicitly.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (value.toLowerCase().includes(';q=')) {
|
|
16
|
+
throw new Error(`Quality factors are not supported in fetcher language configuration; received "${value}". Provide a comma-separated list of BCP 47 tags in priority order, for example "en-IE,en-GB,en".`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const languages = value.split(',').map(tag => tag.trim()).filter(Boolean);
|
|
20
|
+
|
|
21
|
+
if (!languages.length) {
|
|
22
|
+
throw new Error(`Fetcher language must contain at least one tag; received "${value}".`);
|
|
23
|
+
}
|
|
10
24
|
|
|
25
|
+
return { locale: languages.join(','), languages };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function fetch(url, cssSelectors, config) {
|
|
11
29
|
if (!browser) {
|
|
12
30
|
throw new Error('The headless browser should be controlled manually with "launchHeadlessBrowser" and "stopHeadlessBrowser".');
|
|
13
31
|
}
|
|
@@ -21,7 +39,7 @@ export default async function fetch(url, cssSelectors, config) {
|
|
|
21
39
|
page = await context.newPage();
|
|
22
40
|
client = await page.createCDPSession();
|
|
23
41
|
|
|
24
|
-
await configurePage(page,
|
|
42
|
+
await configurePage(page, config);
|
|
25
43
|
|
|
26
44
|
const selectors = [].concat(cssSelectors).filter(Boolean);
|
|
27
45
|
|
|
@@ -97,14 +115,24 @@ export default async function fetch(url, cssSelectors, config) {
|
|
|
97
115
|
/**
|
|
98
116
|
* Launches a headless browser instance using Puppeteer if one is not already running. Returns the existing browser instance if one is already running, otherwise creates and returns a new instance.
|
|
99
117
|
* @function launchHeadlessBrowser
|
|
100
|
-
* @
|
|
118
|
+
* @param {string} language Accept-Language header value applied to the browser context
|
|
119
|
+
* @returns {Promise<puppeteer.Browser>} The Puppeteer browser instance.
|
|
101
120
|
* @async
|
|
102
121
|
*/
|
|
103
|
-
export async function launchHeadlessBrowser() {
|
|
122
|
+
export async function launchHeadlessBrowser(language) {
|
|
104
123
|
if (browser) {
|
|
105
124
|
return browser;
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
const { locale, languages } = parseLanguage(language);
|
|
128
|
+
const stealth = stealthPlugin();
|
|
129
|
+
|
|
130
|
+
stealth.enabledEvasions.delete('user-agent-override');
|
|
131
|
+
stealth.enabledEvasions.delete('navigator.languages');
|
|
132
|
+
puppeteer.use(stealth);
|
|
133
|
+
puppeteer.use(userAgentOverride({ locale }));
|
|
134
|
+
puppeteer.use(navigatorLanguages({ languages }));
|
|
135
|
+
|
|
108
136
|
const options = {
|
|
109
137
|
args: [],
|
|
110
138
|
headless: !process.env.OTA_ENGINE_FETCHER_NO_HEADLESS,
|
|
@@ -156,16 +184,9 @@ function isValidHttpStatus(status) {
|
|
|
156
184
|
return (status >= 200 && status < 300) || status === 304;
|
|
157
185
|
}
|
|
158
186
|
|
|
159
|
-
async function configurePage(page,
|
|
187
|
+
async function configurePage(page, config) {
|
|
160
188
|
await page.setViewport({ width: 1920, height: 1080 }); // Realistic viewport to avoid detection based on default Puppeteer dimensions (800x600)
|
|
161
189
|
await page.setDefaultNavigationTimeout(config.navigationTimeout);
|
|
162
|
-
await page.setExtraHTTPHeaders({ 'Accept-Language': config.language });
|
|
163
|
-
|
|
164
|
-
// Use CDP to ensure browser language is set correctly (see https://zirkelc.dev/posts/puppeteer-language-experiment)
|
|
165
|
-
await client.send('Network.setUserAgentOverride', {
|
|
166
|
-
userAgent: await browser.userAgent(),
|
|
167
|
-
acceptLanguage: config.language,
|
|
168
|
-
});
|
|
169
190
|
|
|
170
191
|
if (browser.proxyCredentials?.username && browser.proxyCredentials?.password) {
|
|
171
192
|
await page.authenticate(browser.proxyCredentials);
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { expect, use } from 'chai';
|
|
7
7
|
import chaiAsPromised from 'chai-as-promised';
|
|
8
8
|
|
|
9
|
-
import fetch, { launchHeadlessBrowser, stopHeadlessBrowser } from './fullDomFetcher.js';
|
|
9
|
+
import fetch, { launchHeadlessBrowser, stopHeadlessBrowser, parseLanguage } from './fullDomFetcher.js';
|
|
10
10
|
|
|
11
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
|
|
@@ -16,6 +16,8 @@ use(chaiAsPromised);
|
|
|
16
16
|
|
|
17
17
|
const dynamicHTML = '<!DOCTYPE html><html><head><title>Dynamic Page</title><script>setTimeout(() => { document.body.innerHTML += "<div class=\'dynamic\'>Loaded</div>"; }, 100);</script></head><body></body></html>';
|
|
18
18
|
const delayedContentHTML = '<!DOCTYPE html><html><head><title>Delayed Content</title><script>setTimeout(() => { document.querySelector(".content").textContent = "Final content"; }, 100);</script></head><body><div class="content"></div></body></html>';
|
|
19
|
+
const langEchoHTML = '<!DOCTYPE html><html><body><script>document.body.setAttribute("data-language", navigator.language); document.body.setAttribute("data-languages", navigator.languages.join(","));</script></body></html>';
|
|
20
|
+
const stealthProbeHTML = '<!DOCTYPE html><html><body><script>document.body.setAttribute("data-webdriver", String(navigator.webdriver)); document.body.setAttribute("data-user-agent", navigator.userAgent); document.body.setAttribute("data-plugin-count", String(navigator.plugins.length)); document.body.setAttribute("data-viewport-width", String(window.innerWidth)); document.body.setAttribute("data-viewport-height", String(window.innerHeight)); (() => { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) { document.body.setAttribute("data-webgl-vendor", "none"); return; } const ext = gl.getExtension("WEBGL_debug_renderer_info"); document.body.setAttribute("data-webgl-vendor", ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : ""); document.body.setAttribute("data-webgl-renderer", ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : ""); })();</script></body></html>';
|
|
19
21
|
|
|
20
22
|
describe('Full DOM Fetcher', function () {
|
|
21
23
|
this.timeout(60000);
|
|
@@ -24,7 +26,7 @@ describe('Full DOM Fetcher', function () {
|
|
|
24
26
|
let expectedPDFContent;
|
|
25
27
|
|
|
26
28
|
before(async () => {
|
|
27
|
-
await launchHeadlessBrowser();
|
|
29
|
+
await launchHeadlessBrowser('en');
|
|
28
30
|
|
|
29
31
|
temporaryServer = http.createServer((request, response) => {
|
|
30
32
|
if (request.url === '/dynamic') {
|
|
@@ -33,6 +35,17 @@ describe('Full DOM Fetcher', function () {
|
|
|
33
35
|
if (request.url === '/delayed-content') {
|
|
34
36
|
response.writeHead(200, { 'Content-Type': 'text/html' }).write(delayedContentHTML);
|
|
35
37
|
}
|
|
38
|
+
if (request.url === '/lang-header') {
|
|
39
|
+
const acceptLanguage = request.headers['accept-language'] || '';
|
|
40
|
+
|
|
41
|
+
response.writeHead(200, { 'Content-Type': 'text/html' }).write(`<!DOCTYPE html><html><body data-accept-language="${acceptLanguage}"></body></html>`);
|
|
42
|
+
}
|
|
43
|
+
if (request.url === '/lang-echo') {
|
|
44
|
+
response.writeHead(200, { 'Content-Type': 'text/html' }).write(langEchoHTML);
|
|
45
|
+
}
|
|
46
|
+
if (request.url === '/stealth-probe') {
|
|
47
|
+
response.writeHead(200, { 'Content-Type': 'text/html' }).write(stealthProbeHTML);
|
|
48
|
+
}
|
|
36
49
|
if (request.url === '/terms.pdf') {
|
|
37
50
|
expectedPDFContent = fs.readFileSync(path.resolve(__dirname, '../../../test/fixtures/terms.pdf'));
|
|
38
51
|
response.writeHead(200, { 'Content-Type': 'application/pdf' }).write(expectedPDFContent);
|
|
@@ -54,12 +67,12 @@ describe('Full DOM Fetcher', function () {
|
|
|
54
67
|
await stopHeadlessBrowser();
|
|
55
68
|
await expect(fetch('http://example.com', [], { navigationTimeout: 5000, waitForElementsTimeout: 5000, language: 'en' }))
|
|
56
69
|
.to.be.rejectedWith('The headless browser should be controlled manually');
|
|
57
|
-
await launchHeadlessBrowser();
|
|
70
|
+
await launchHeadlessBrowser('en');
|
|
58
71
|
});
|
|
59
72
|
|
|
60
73
|
it('reuses existing browser instance', async () => {
|
|
61
|
-
const browser1 = await launchHeadlessBrowser();
|
|
62
|
-
const browser2 = await launchHeadlessBrowser();
|
|
74
|
+
const browser1 = await launchHeadlessBrowser('en');
|
|
75
|
+
const browser2 = await launchHeadlessBrowser('en');
|
|
63
76
|
|
|
64
77
|
expect(browser1).to.equal(browser2);
|
|
65
78
|
});
|
|
@@ -81,6 +94,12 @@ describe('Full DOM Fetcher', function () {
|
|
|
81
94
|
await expect(fetch(url, ['.non-existent'], { ...config, navigationTimeout: timeout })).to.be.rejectedWith(`Timed out after ${timeout / 1000} seconds when trying to fetch '${url}'`);
|
|
82
95
|
});
|
|
83
96
|
|
|
97
|
+
it('sends the configured language as Accept-Language header', async () => {
|
|
98
|
+
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/lang-header`, [], config);
|
|
99
|
+
|
|
100
|
+
expect(result.content).to.match(/data-accept-language="en"/);
|
|
101
|
+
});
|
|
102
|
+
|
|
84
103
|
context('when a DOM element exists but its content is loaded asynchronously', () => {
|
|
85
104
|
it('waits for the element content to be fully loaded', async () => {
|
|
86
105
|
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/delayed-content`, ['.content'], config);
|
|
@@ -118,4 +137,145 @@ describe('Full DOM Fetcher', function () {
|
|
|
118
137
|
});
|
|
119
138
|
});
|
|
120
139
|
});
|
|
140
|
+
|
|
141
|
+
describe('Language configuration', () => {
|
|
142
|
+
context('with a regional locale such as fr-FR,fr', () => {
|
|
143
|
+
const language = 'fr-FR,fr';
|
|
144
|
+
const config = { navigationTimeout: 5000, waitForElementsTimeout: 5000, language };
|
|
145
|
+
|
|
146
|
+
before(async () => {
|
|
147
|
+
await stopHeadlessBrowser();
|
|
148
|
+
await launchHeadlessBrowser(language);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('exposes the primary tag through navigator.language', async () => {
|
|
152
|
+
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/lang-echo`, [], config);
|
|
153
|
+
|
|
154
|
+
expect(result.content).to.match(/data-language="fr-FR"/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('exposes every tag through navigator.languages', async () => {
|
|
158
|
+
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/lang-echo`, [], config);
|
|
159
|
+
|
|
160
|
+
expect(result.content).to.match(/data-languages="fr-FR,fr"/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('sends every configured tag as Accept-Language header', async () => {
|
|
164
|
+
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/lang-header`, [], config);
|
|
165
|
+
|
|
166
|
+
expect(result.content).to.match(/data-accept-language="fr-FR,fr[^"]*"/);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
context('with multiple comma-separated tags such as en-IE,en-GB,en', () => {
|
|
171
|
+
const language = 'en-IE,en-GB,en';
|
|
172
|
+
const config = { navigationTimeout: 5000, waitForElementsTimeout: 5000, language };
|
|
173
|
+
|
|
174
|
+
before(async () => {
|
|
175
|
+
await stopHeadlessBrowser();
|
|
176
|
+
await launchHeadlessBrowser(language);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('derives Accept-Language quality factors from tag order', async () => {
|
|
180
|
+
const result = await fetch(`http://127.0.0.1:${SERVER_PORT}/lang-header`, [], config);
|
|
181
|
+
|
|
182
|
+
expect(result.content).to.match(/data-accept-language="en-IE,en-GB;q=0\.9,en;q=0\.8"/);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Stealth evasions', () => {
|
|
188
|
+
// These assertions guard against a class of regressions where the puppeteer-extra-plugin-stealth fails to be registered before puppeteer.launch(): if it is registered later, puppeteer-extra never binds its onPageCreated hooks and all evasions stay inactive, leaving navigator.webdriver === true and HeadlessChrome in the UA.
|
|
189
|
+
const config = { navigationTimeout: 5000, waitForElementsTimeout: 5000, language: 'en' };
|
|
190
|
+
let content;
|
|
191
|
+
|
|
192
|
+
before(async () => {
|
|
193
|
+
({ content } = await fetch(`http://127.0.0.1:${SERVER_PORT}/stealth-probe`, [], config));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('hides navigator.webdriver', () => {
|
|
197
|
+
expect(content).to.match(/data-webdriver="false"/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('removes HeadlessChrome from the user agent', () => {
|
|
201
|
+
expect(content).not.to.match(/HeadlessChrome/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('uses a realistic viewport instead of Puppeteer default', () => {
|
|
205
|
+
expect(content).to.match(/data-viewport-width="1920"/);
|
|
206
|
+
expect(content).to.match(/data-viewport-height="1080"/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('exposes a non-empty navigator.plugins list', () => {
|
|
210
|
+
const match = content.match(/data-plugin-count="(\d+)"/);
|
|
211
|
+
|
|
212
|
+
expect(match).to.not.be.null;
|
|
213
|
+
expect(Number(match[1])).to.be.greaterThan(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('hides headless WebGL vendor and renderer signature', () => {
|
|
217
|
+
expect(content).to.not.match(/data-webgl-vendor="[^"]*Google[^"]*"/);
|
|
218
|
+
expect(content).to.not.match(/data-webgl-renderer="[^"]*(?:SwiftShader|ANGLE)[^"]*"/);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('parseLanguage', () => {
|
|
224
|
+
context('with a single tag', () => {
|
|
225
|
+
it('returns the tag as both locale and a singleton languages array', () => {
|
|
226
|
+
expect(parseLanguage('en')).to.deep.equal({ locale: 'en', languages: ['en'] });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('preserves the region of a regional tag', () => {
|
|
230
|
+
expect(parseLanguage('fr-FR')).to.deep.equal({ locale: 'fr-FR', languages: ['fr-FR'] });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
context('with a comma-separated priority list', () => {
|
|
235
|
+
it('splits every tag into the languages array and rejoins them as locale', () => {
|
|
236
|
+
expect(parseLanguage('en-IE,en-GB,en')).to.deep.equal({ locale: 'en-IE,en-GB,en', languages: [ 'en-IE', 'en-GB', 'en' ] });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('trims whitespace surrounding each tag', () => {
|
|
240
|
+
expect(parseLanguage('fr-FR, fr')).to.deep.equal({ locale: 'fr-FR,fr', languages: [ 'fr-FR', 'fr' ] });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('drops empty entries produced by consecutive commas', () => {
|
|
244
|
+
expect(parseLanguage('en,,fr')).to.deep.equal({ locale: 'en,fr', languages: [ 'en', 'fr' ] });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('drops a trailing comma', () => {
|
|
248
|
+
expect(parseLanguage('en,fr,')).to.deep.equal({ locale: 'en,fr', languages: [ 'en', 'fr' ] });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
context('with quality factors', () => {
|
|
253
|
+
it('throws for a priority list carrying quality factors', () => {
|
|
254
|
+
expect(() => parseLanguage('en-IE,en-GB;q=0.9,en;q=0.8')).to.throw('Quality factors are not supported');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('throws for a single tag carrying a quality factor', () => {
|
|
258
|
+
expect(() => parseLanguage('en;q=1.0')).to.throw('Quality factors are not supported');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('throws regardless of quality-factor letter case', () => {
|
|
262
|
+
expect(() => parseLanguage('en-GB;Q=0.9,en')).to.throw('Quality factors are not supported');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
context('with invalid input', () => {
|
|
267
|
+
it('throws when the value is not a string', () => {
|
|
268
|
+
expect(() => parseLanguage(undefined)).to.throw('must be a string');
|
|
269
|
+
expect(() => parseLanguage(42)).to.throw('must be a string');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('throws when the value is empty or blank', () => {
|
|
273
|
+
expect(() => parseLanguage('')).to.throw('must contain at least one tag');
|
|
274
|
+
expect(() => parseLanguage(' ')).to.throw('must contain at least one tag');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('throws when the value contains only separators', () => {
|
|
278
|
+
expect(() => parseLanguage(',,')).to.throw('must contain at least one tag');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
121
281
|
});
|
|
@@ -21,7 +21,7 @@ export const FETCHER_TYPES = {
|
|
|
21
21
|
* @param {string|Array} [params.cssSelectors] List of CSS selectors to await when loading the resource in a headless browser. Can be a CSS selector or an array of CSS selectors. Only relevant when `executeClientScripts` is enabled
|
|
22
22
|
* @param {object} [params.config] Fetcher configuration
|
|
23
23
|
* @param {number} [params.config.navigationTimeout] Maximum time (in milliseconds) to wait before considering the fetch failed
|
|
24
|
-
* @param {string} [params.config.language] Language
|
|
24
|
+
* @param {string} [params.config.language] Accept-Language value applied to the browser context. Accepts a comma-separated list of [BCP 47](https://www.rfc-editor.org/rfc/rfc5646) language tags in priority order, without quality factors (for example `fr`, `en-US`, or `en-IE,en-GB,en`). The tag list drives `navigator.language` and `navigator.languages` in the headless browser, and the browser derives the `Accept-Language` HTTP header from the tag order
|
|
25
25
|
* @param {number} [params.config.waitForElementsTimeout] Maximum time (in milliseconds) to wait for selectors to exist on page before considering the fetch failed. Only relevant when `executeClientScripts` is enabled
|
|
26
26
|
* @returns {Promise<{ mimeType: string, content: string | Buffer, fetcher: string }>} Promise containing the fetched resource's MIME type, content, and fetcher type
|
|
27
27
|
* @throws {FetchDocumentError} When the fetch operation fails
|
|
@@ -21,7 +21,7 @@ const termsWithOtherCharsetHTML = '<!DOCTYPE html><html><head><meta http-equiv="
|
|
|
21
21
|
describe('Fetcher', function () {
|
|
22
22
|
this.timeout(60000);
|
|
23
23
|
|
|
24
|
-
before(launchHeadlessBrowser);
|
|
24
|
+
before(() => launchHeadlessBrowser('en'));
|
|
25
25
|
|
|
26
26
|
after(stopHeadlessBrowser);
|
|
27
27
|
|
package/src/archivist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ export default class Archivist extends events.EventEmitter {
|
|
|
45
45
|
|
|
46
46
|
constructor({ recorderConfig, fetcherConfig }) {
|
|
47
47
|
super();
|
|
48
|
+
this.fetcherConfig = fetcherConfig;
|
|
48
49
|
this.recorder = new Recorder(recorderConfig);
|
|
49
50
|
this.fetch = params => fetch({ ...params, config: fetcherConfig });
|
|
50
51
|
this.extract = extract;
|
|
@@ -151,7 +152,7 @@ export default class Archivist extends events.EventEmitter {
|
|
|
151
152
|
|
|
152
153
|
this.emit('trackingStarted', servicesIds.length, numberOfTerms, technicalUpgradeOnly);
|
|
153
154
|
|
|
154
|
-
await Promise.all([ launchHeadlessBrowser(), this.recorder.initialize() ]);
|
|
155
|
+
await Promise.all([ launchHeadlessBrowser(this.fetcherConfig.language), this.recorder.initialize() ]);
|
|
155
156
|
|
|
156
157
|
this.trackingQueue.concurrency = concurrency;
|
|
157
158
|
|
|
@@ -260,6 +260,9 @@ async function loadServiceHistoryFiles(serviceId) {
|
|
|
260
260
|
]);
|
|
261
261
|
|
|
262
262
|
Object.entries(serviceDeclaration.terms).forEach(([ termsType, declaration ]) => {
|
|
263
|
+
if (!Array.isArray(serviceHistory[termsType]) && serviceHistory[termsType] !== undefined) {
|
|
264
|
+
throw new Error(`History file for "${termsType}" is not properly formatted: expected an array but got an object. Please use [] instead of {}.`);
|
|
265
|
+
}
|
|
263
266
|
serviceHistory[termsType] = serviceHistory[termsType] || [];
|
|
264
267
|
serviceHistory[termsType].push(declaration);
|
|
265
268
|
});
|