• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microlinkhq / browserless / 14312183135

07 Apr 2025 02:42PM UTC coverage: 85.025%. Remained the same
14312183135

push

github

web-flow
chore: update dependencies (#610)

* chore: update dependencies

* test: update snapshot

219 of 254 branches covered (86.22%)

Branch coverage included in aggregate %.

1138 of 1342 relevant lines covered (84.8%)

2756.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.6
/packages/goto/src/index.js
1
'use strict'
5✔
2

5✔
3
const { PuppeteerBlocker } = require('@ghostery/adblocker-puppeteer')
5✔
4
const { shallowEqualObjects } = require('shallow-equal')
5✔
5
const { setTimeout } = require('node:timers/promises')
5✔
6
const createDevices = require('@browserless/devices')
5✔
7
const toughCookie = require('tough-cookie')
5✔
8
const pReflect = require('p-reflect')
5✔
9
const pTimeout = require('p-timeout')
5✔
10
const isUrl = require('is-url-http')
5✔
11
const path = require('path')
5✔
12
const fs = require('fs')
5✔
13

5✔
14
const timeSpan = require('@kikobeats/time-span')({ format: require('pretty-ms') })
5✔
15

5✔
16
const { DEFAULT_INTERCEPT_RESOLUTION_PRIORITY } = require('puppeteer')
5✔
17

5✔
18
const debug = require('debug-logfmt')('browserless:goto')
5✔
19
debug.continue = require('debug-logfmt')('browserless:goto:continue')
5✔
20
debug.abort = require('debug-logfmt')('browserless:goto:abort')
5✔
21
debug.adblock = require('debug-logfmt')('browserless:goto:adblock')
5✔
22

5✔
23
const truncate = (str, n = 80) => (str.length > n ? str.substr(0, n - 1) + '…' : str)
5✔
24

5✔
25
const engine = PuppeteerBlocker.deserialize(
5✔
26
  new Uint8Array(fs.readFileSync(path.resolve(__dirname, './engine.bin')))
5✔
27
)
5✔
28

5✔
29
engine.on('request-blocked', ({ url }) => debug.adblock('block', url))
5✔
30
engine.on('request-redirected', ({ url }) => debug.adblock('redirect', url))
5✔
31

5✔
32
const isEmpty = val => val == null || !(Object.keys(val) || val).length
5!
33

5✔
34
const castArray = value => [].concat(value).filter(Boolean)
5✔
35

5✔
36
const run = async ({ fn, timeout, debug: props }) => {
5✔
37
  const debugProps = { duration: timeSpan() }
123✔
38
  const result = await pReflect(timeout ? pTimeout(fn, timeout) : fn)
123✔
39
  debugProps.duration = debugProps.duration()
123✔
40
  if (result.isRejected) debugProps.error = result.reason.message || result.reason
123!
41
  debug(props, debugProps)
123✔
42
  return result
123✔
43
}
123✔
44

5✔
45
const parseCookies = (url, str) =>
5✔
46
  str.split(';').reduce((acc, cookieStr) => {
4✔
47
    const jar = new toughCookie.CookieJar(undefined, { rejectPublicSuffixes: false })
8✔
48
    jar.setCookieSync(cookieStr.trim(), url)
8✔
49
    const parsedCookie = jar.serializeSync().cookies[0]
8✔
50

8✔
51
    // Use this instead of the above when the following issue is fixed:
8✔
52
    // https://github.com/salesforce/tough-cookie/issues/149
8✔
53
    // const ret = toughCookie.parse(cookie).serializeSync();
8✔
54

8✔
55
    parsedCookie.name = parsedCookie.key
8✔
56
    delete parsedCookie.key
8✔
57

8✔
58
    if (parsedCookie.expires) {
8!
59
      parsedCookie.expires = Math.floor(new Date(parsedCookie.expires) / 1000)
×
60
    }
×
61

8✔
62
    acc.push(parsedCookie)
8✔
63
    return acc
8✔
64
  }, [])
4✔
65

5✔
66
const getMediaFeatures = ({ animations, colorScheme }) => {
5✔
67
  const prefers = []
14✔
68
  if (animations === false) prefers.push({ name: 'prefers-reduced-motion', value: 'reduce' })
14✔
69
  if (colorScheme) prefers.push({ name: 'prefers-color-scheme', value: colorScheme })
14!
70
  return prefers
14✔
71
}
14✔
72

5✔
73
const injectScript = (page, value, attributes) =>
5✔
74
  page.addScriptTag({
2✔
75
    [getInjectKey('js', value)]: value,
2✔
76
    ...attributes
2✔
77
  })
2✔
78

5✔
79
const injectStyle = (page, style) =>
5✔
80
  page.addStyleTag({
14✔
81
    [getInjectKey('css', style)]: style
14✔
82
  })
14✔
83

5✔
84
const getInjectKey = (ext, value) =>
5✔
85
  isUrl(value) ? 'url' : value.endsWith(`.${ext}`) ? 'path' : 'content'
16!
86

5✔
87
const disableAnimations = `
5✔
88
  *,
5✔
89
  ::before,
5✔
90
  ::after {
5✔
91
    animation-delay: 0s !important;
5✔
92
    transition-delay: 0s !important;
5✔
93
    animation-duration: 0s !important;
5✔
94
    transition-duration: 0s !important;
5✔
95
    transition-property: none !important;
5✔
96
  }
5✔
97
`.trim()
5✔
98

5✔
99
const inject = async (page, { timeout, mediaType, animations, modules, scripts, styles }) => {
5✔
100
  const postPromises = []
14✔
101

14✔
102
  if (mediaType) {
14!
103
    postPromises.push(
×
104
      run({
×
105
        fn: page.emulateMediaType(mediaType),
×
106
        timeout,
×
107
        debug: { mediaType }
×
108
      })
×
109
    )
×
110
  }
×
111

14✔
112
  if (animations === false) {
14✔
113
    postPromises.push(
13✔
114
      run({
13✔
115
        fn: injectStyle(page, disableAnimations),
13✔
116
        timeout,
13✔
117
        debug: 'disableAnimations'
13✔
118
      })
13✔
119
    )
13✔
120
  }
13✔
121

14✔
122
  if (modules) {
14✔
123
    postPromises.push(
1✔
124
      run({
1✔
125
        fn: Promise.all(
1✔
126
          castArray(modules).map(value => injectScript(page, value, { type: 'module' }))
1✔
127
        ),
1✔
128
        timeout,
1✔
129
        debug: 'modules'
1✔
130
      })
1✔
131
    )
1✔
132
  }
1✔
133

14✔
134
  if (scripts) {
14✔
135
    postPromises.push(
1✔
136
      run({
1✔
137
        fn: Promise.all(castArray(scripts).map(value => injectScript(page, value))),
1✔
138
        timeout,
1✔
139
        debug: 'scripts'
1✔
140
      })
1✔
141
    )
1✔
142
  }
1✔
143

14✔
144
  if (styles) {
14✔
145
    postPromises.push(
1✔
146
      run({
1✔
147
        fn: Promise.all(castArray(styles).map(style => injectStyle(page, style))),
1✔
148
        timeout,
1✔
149
        debug: 'styles'
1✔
150
      })
1✔
151
    )
1✔
152
  }
1✔
153

14✔
154
  return Promise.all(postPromises)
14✔
155
}
14✔
156

5✔
157
module.exports = ({ defaultDevice = 'Macbook Pro 13', timeout: globalTimeout, ...deviceOpts }) => {
5✔
158
  const getDevice = createDevices(deviceOpts)
4✔
159
  const { viewport: defaultViewport } = getDevice.findDevice(defaultDevice)
4✔
160

4✔
161
  const timeouts = {
4✔
162
    base: (milliseconds = globalTimeout) => milliseconds * (2 / 3),
4✔
163
    action: (milliseconds = globalTimeout) => milliseconds * (1 / 11),
4✔
164
    goto: (milliseconds = globalTimeout) => milliseconds * (7 / 8)
4✔
165
  }
4✔
166

4✔
167
  // related https://github.com/puppeteer/puppeteer/issues/1353
4✔
168
  const _waitUntilAuto = (page, { timeout }) => {
4✔
169
    return Promise.all(
14✔
170
      [
14✔
171
        {
14✔
172
          fn: () => page.waitForNavigation({ waitUntil: 'networkidle2' }),
14✔
173
          debug: 'waitUntilAuto:waitForNavigation'
14✔
174
        },
14✔
175
        {
14✔
176
          fn: () => page.evaluate(() => window.history.pushState(null, null, null)),
14✔
177
          debug: 'waitUntilAuto:pushState'
14✔
178
        }
14✔
179
      ].map(({ fn, debug }) => run({ fn: fn(), debug, timeout }))
14✔
180
    )
14✔
181
  }
14✔
182

4✔
183
  const goto = async (
4✔
184
    page,
14✔
185
    {
14✔
186
      abortTypes = [],
14✔
187
      adblock = true,
14✔
188
      animations = false,
14✔
189
      authenticate,
14✔
190
      click,
14✔
191
      colorScheme,
14✔
192
      headers = {},
14✔
193
      html,
14✔
194
      javascript = true,
14✔
195
      mediaType,
14✔
196
      modules,
14✔
197
      scripts,
14✔
198
      scroll,
14✔
199
      styles,
14✔
200
      timeout,
14✔
201
      timezone,
14✔
202
      url,
14✔
203
      waitForFunction,
14✔
204
      waitForSelector,
14✔
205
      waitForTimeout,
14✔
206
      waitUntil = 'auto',
14✔
207
      waitUntilAuto = _waitUntilAuto,
14✔
208
      onPageRequest,
14✔
209
      ...args
14✔
210
    }
14✔
211
  ) => {
14✔
212
    const baseTimeout = timeouts.base(timeout || globalTimeout)
14✔
213
    const actionTimeout = timeouts.action(baseTimeout)
14✔
214
    const gotoTimeout = timeouts.goto(baseTimeout)
14✔
215

14✔
216
    const isWaitUntilAuto = waitUntil === 'auto'
14✔
217
    if (isWaitUntilAuto) waitUntil = 'load'
14✔
218

14✔
219
    const prePromises = []
14✔
220

14✔
221
    if (authenticate) {
14!
222
      prePromises.push(
×
223
        run({
×
224
          fn: page.authenticate(authenticate),
×
225
          timeout: actionTimeout,
×
226
          debug: 'authenticate'
×
227
        })
×
228
      )
×
229
    }
×
230

14✔
231
    if (modules || scripts || styles) {
14✔
232
      prePromises.push(
3✔
233
        run({
3✔
234
          fn: page.setBypassCSP(true),
3✔
235
          timeout: actionTimeout,
3✔
236
          debug: 'bypassCSP'
3✔
237
        })
3✔
238
      )
3✔
239
    }
3✔
240

14✔
241
    const enableInterception =
14✔
242
      (onPageRequest || abortTypes.length > 0) &&
14✔
243
      run({ fn: page.setRequestInterception(true), debug: 'enableInterception' })
1✔
244

14✔
245
    if (onPageRequest) {
14✔
246
      Promise.resolve(enableInterception).then(() => page.on('request', onPageRequest))
1✔
247
    }
1✔
248

14✔
249
    if (abortTypes.length > 0) {
14✔
250
      Promise.resolve(enableInterception).then(() => {
1✔
251
        page.on('request', req => {
1✔
252
          if (req.isInterceptResolutionHandled()) return
×
253
          const resourceType = req.resourceType()
×
254
          const url = truncate(req.url())
×
255

×
256
          if (!abortTypes.includes(resourceType)) {
×
257
            debug.continue({ url, resourceType })
×
258
            return req.continue(
×
259
              req.continueRequestOverrides(),
×
260
              DEFAULT_INTERCEPT_RESOLUTION_PRIORITY
×
261
            )
×
262
          }
×
263
          debug.abort({ url, resourceType })
×
264
          return req.abort('blockedbyclient', DEFAULT_INTERCEPT_RESOLUTION_PRIORITY)
×
265
        })
1✔
266
      })
1✔
267
    }
1✔
268

14✔
269
    if (adblock) {
14✔
270
      prePromises.push(
14✔
271
        run({
14✔
272
          fn: engine.enableBlockingInPage(page),
14✔
273
          timeout: actionTimeout,
14✔
274
          debug: 'adblock'
14✔
275
        })
14✔
276
      )
14✔
277
    }
14✔
278

14✔
279
    if (javascript === false) {
14!
280
      prePromises.push(
×
281
        run({
×
282
          fn: page.setJavaScriptEnabled(false),
×
283
          timeout: actionTimeout,
×
284
          debug: { javascript }
×
285
        })
×
286
      )
×
287
    }
×
288

14✔
289
    const device = getDevice({ headers, ...args, device: args.device ?? defaultDevice })
14✔
290

14✔
291
    if (device.userAgent && !headers['user-agent']) {
14✔
292
      headers['user-agent'] = device.userAgent
13✔
293
    }
13✔
294

14✔
295
    if (!isEmpty(device.viewport) && !shallowEqualObjects(defaultViewport, device.viewport)) {
14!
296
      prePromises.push(
×
297
        run({
×
298
          fn: page.setViewport(device.viewport),
×
299
          timeout: actionTimeout,
×
300
          debug: 'viewport'
×
301
        })
×
302
      )
×
303
    }
×
304

14✔
305
    const headersKeys = Object.keys(headers)
14✔
306

14✔
307
    if (headersKeys.length > 0) {
14✔
308
      if (headers.cookie) {
14✔
309
        const cookies = parseCookies(url, headers.cookie)
1✔
310
        prePromises.push(
1✔
311
          run({
1✔
312
            fn: page.setCookie(...cookies),
1✔
313
            timeout: actionTimeout,
1✔
314
            debug: ['cookies', ...cookies.map(({ name }) => name)]
1✔
315
          })
1✔
316
        )
1✔
317
      }
1✔
318

14✔
319
      if (headers['user-agent']) {
14✔
320
        prePromises.push(
14✔
321
          run({
14✔
322
            fn: page.setUserAgent(headers['user-agent']),
14✔
323
            timeout: actionTimeout,
14✔
324
            debug: { 'user-agent': headers['user-agent'] }
14✔
325
          })
14✔
326
        )
14✔
327
      }
14✔
328

14✔
329
      prePromises.push(
14✔
330
        run({
14✔
331
          fn: page.setExtraHTTPHeaders(headers),
14✔
332
          timeout: actionTimeout,
14✔
333
          debug: { headers: headersKeys }
14✔
334
        })
14✔
335
      )
14✔
336
    }
14✔
337

14✔
338
    if (timezone) {
14✔
339
      prePromises.push(
4✔
340
        run({
4✔
341
          fn: page.emulateTimezone(timezone),
4✔
342
          timeout: actionTimeout,
4✔
343
          debug: { timezone }
4✔
344
        })
4✔
345
      )
4✔
346
    }
4✔
347

14✔
348
    const mediaFeatures = getMediaFeatures({ animations, colorScheme })
14✔
349

14✔
350
    if (mediaFeatures.length > 0) {
14✔
351
      prePromises.push(
13✔
352
        run({
13✔
353
          fn: page.emulateMediaFeatures(mediaFeatures),
13✔
354
          timeout: actionTimeout,
13✔
355
          debug: { mediaFeatures: mediaFeatures.map(({ name }) => name) }
13✔
356
        })
13✔
357
      )
13✔
358
    }
13✔
359

14✔
360
    await Promise.all(prePromises)
14✔
361

14✔
362
    const { value: response, reason: error } = await run({
14✔
363
      fn: html
14✔
364
        ? page.setContent(html, { waitUntil, ...args })
14!
365
        : Promise.race([
14✔
366
          page.goto(url, { waitUntil, ...args }),
14✔
367
          setTimeout(gotoTimeout).then(() => page._client().send('Page.stopLoading'))
14✔
368
        ]),
14✔
369
      timeout: gotoTimeout,
14✔
370
      debug: { fn: html ? 'html' : 'url', waitUntil }
14!
371
    })
14✔
372

14✔
373
    for (const [key, value] of Object.entries({
14✔
374
      waitForSelector,
14✔
375
      waitForFunction
14✔
376
    })) {
14✔
377
      if (value) {
28✔
378
        await run({ fn: page[key](value), timeout: gotoTimeout, debug: { [key]: value } })
1✔
379
      }
1✔
380
    }
28✔
381

14✔
382
    if (waitForTimeout) {
14!
383
      await setTimeout(waitForTimeout)
×
384
    }
×
385

14✔
386
    await inject(page, {
14✔
387
      timeout: actionTimeout,
14✔
388
      mediaType,
14✔
389
      animations,
14✔
390
      modules,
14✔
391
      scripts,
14✔
392
      styles
14✔
393
    })
14✔
394

14✔
395
    if (click) {
14!
396
      for (const selector of castArray(click)) {
×
397
        await run({
×
398
          fn: page.click(selector),
×
399
          timeout: actionTimeout,
×
400
          debug: { click: selector }
×
401
        })
×
402
      }
×
403
    }
×
404

14✔
405
    if (scroll) {
14!
406
      await run({
×
407
        fn: page.$eval(scroll, el => el.scrollIntoView()),
×
408
        timeout: actionTimeout,
×
409
        debug: { scroll }
×
410
      })
×
411
    }
×
412

14✔
413
    if (isWaitUntilAuto) {
14✔
414
      await waitUntilAuto(page, { response, timeout: actionTimeout * 2 })
14✔
415
    }
14✔
416

14✔
417
    return { response, device, error }
14✔
418
  }
14✔
419

4✔
420
  goto.getDevice = getDevice
4✔
421
  goto.devices = getDevice.devices
4✔
422
  goto.findDevice = getDevice.findDevice
4✔
423
  goto.deviceDescriptors = getDevice.deviceDescriptors
4✔
424
  goto.defaultViewport = defaultViewport
4✔
425
  goto.waitUntilAuto = _waitUntilAuto
4✔
426
  goto.timeouts = timeouts
4✔
427
  goto.run = run
4✔
428

4✔
429
  return goto
4✔
430
}
4✔
431

5✔
432
module.exports.parseCookies = parseCookies
5✔
433
module.exports.inject = inject
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc