@browserless/screenshot 10.8.0 → 10.9.0

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/LICENSE.md CHANGED
File without changes
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/screenshot",
3
3
  "description": "Take a clean screenshot of any website",
4
4
  "homepage": "https://browserless.js.org/#/?id=screenshoturl-options",
5
- "version": "10.8.0",
5
+ "version": "10.9.0",
6
6
  "main": "src/index.js",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -28,7 +28,7 @@
28
28
  "screenshot"
29
29
  ],
30
30
  "dependencies": {
31
- "@browserless/goto": "^10.8.0",
31
+ "@browserless/goto": "^10.9.0",
32
32
  "@kikobeats/time-span": "~1.0.8",
33
33
  "debug-logfmt": "~1.4.0",
34
34
  "got": "~11.8.6",
@@ -45,7 +45,7 @@
45
45
  "svg-gradient": "~1.0.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@browserless/test": "^10.7.13",
48
+ "@browserless/test": "^10.9.0",
49
49
  "ava": "5",
50
50
  "cheerio": "latest"
51
51
  },
@@ -56,14 +56,15 @@
56
56
  "scripts",
57
57
  "src"
58
58
  ],
59
+ "scripts": {
60
+ "coverage": "exit 0",
61
+ "test": "ava"
62
+ },
59
63
  "license": "MIT",
60
64
  "ava": {
61
65
  "serial": true,
62
66
  "timeout": "2m",
63
67
  "workerThreads": false
64
68
  },
65
- "scripts": {
66
- "coverage": "exit 0",
67
- "test": "ava"
68
- }
69
- }
69
+ "gitHead": "d3843b489f96c2b52af786a8740ab2a47d7e7125"
70
+ }
package/src/index.js CHANGED
@@ -32,16 +32,80 @@ const waitForImagesOnViewport = page =>
32
32
  )
33
33
  )
34
34
 
35
+ const waitForDomStability = ({ idle, timeout } = {}) =>
36
+ new Promise(resolve => {
37
+ if (!document.body) return resolve({ status: 'no-body' })
38
+
39
+ let lastChange = performance.now()
40
+ const observer = new window.MutationObserver(() => {
41
+ lastChange = performance.now()
42
+ })
43
+ observer.observe(document.body, {
44
+ childList: true,
45
+ subtree: true,
46
+ attributes: false,
47
+ characterData: false
48
+ })
49
+
50
+ const deadline = performance.now() + timeout
51
+
52
+ ;(function check () {
53
+ const now = performance.now()
54
+ if (now - lastChange >= idle) {
55
+ observer.disconnect()
56
+ return resolve({ status: 'idle' })
57
+ }
58
+ if (now >= deadline) {
59
+ observer.disconnect()
60
+ return resolve({ status: 'timeout' })
61
+ }
62
+ window.requestAnimationFrame(check)
63
+ })()
64
+ })
65
+
66
+ const scrollFullPageToLoadContent = async (page, timeout) => {
67
+ const debug = require('debug-logfmt')('browserless:goto')
68
+
69
+ const duration = debug.duration()
70
+ const result = await page.evaluate(waitForDomStability, {
71
+ idle: timeout / 2 / 2,
72
+ timeout: timeout / 2
73
+ })
74
+
75
+ duration('waitForDomStability', result)
76
+
77
+ await page.evaluate(
78
+ timeout =>
79
+ new Promise(resolve => {
80
+ let currentScrollPosition = 0
81
+ const scrollStep = Math.floor(window.innerHeight)
82
+ const pageHeight = document.body.scrollHeight
83
+ const totalSteps = Math.ceil(pageHeight / scrollStep)
84
+ const stepDelay = timeout / 2 / totalSteps
85
+ const scrollNext = async () => {
86
+ if (currentScrollPosition >= pageHeight) {
87
+ resolve()
88
+ return
89
+ }
90
+ window.scrollBy(0, scrollStep)
91
+ currentScrollPosition += scrollStep
92
+ setTimeout(scrollNext, stepDelay)
93
+ }
94
+ scrollNext()
95
+ }),
96
+ timeout
97
+ )
98
+ await page.evaluate(() => window.scrollTo(0, 0))
99
+ }
100
+
35
101
  const waitForElement = async (page, element) => {
36
102
  const screenshotOpts = {}
37
-
38
103
  if (element) {
39
104
  await page.waitForSelector(element, { visible: true })
40
105
  screenshotOpts.clip = await page.$eval(element, getBoundingClientRect)
41
106
  screenshotOpts.fullPage = false
42
107
  return screenshotOpts
43
108
  }
44
-
45
109
  return screenshotOpts
46
110
  }
47
111
 
@@ -51,35 +115,58 @@ module.exports = ({ goto, ...gotoOpts }) => {
51
115
  return function screenshot (page) {
52
116
  return async (
53
117
  url,
54
- {
55
- element,
56
- codeScheme = 'atom-dark',
57
- overlay: overlayOpts = {},
58
- waitUntil = 'auto',
59
- ...opts
60
- } = {}
118
+ { codeScheme = 'atom-dark', overlay: overlayOpts = {}, waitUntil = 'auto', ...opts } = {}
61
119
  ) => {
62
120
  let screenshot
63
121
  let response
64
122
 
65
- const beforeScreenshot = response => {
66
- const timeout = goto.timeouts.action(goto.timeouts.base(opts.timeout))
67
- return Promise.all(
68
- [
69
- {
70
- fn: () => page.evaluate('document.fonts.ready'),
71
- debug: 'beforeScreenshot:fontsReady'
72
- },
73
- {
74
- fn: () => waitForPrism(page, response, { codeScheme, ...opts }),
75
- debug: 'beforeScreenshot:waitForPrism'
123
+ const beforeScreenshot = async (page, response, { element, fullPage = false } = {}) => {
124
+ const timeout = goto.timeouts.action(opts.timeout)
125
+
126
+ let screenshotOpts = {}
127
+ const tasks = [
128
+ {
129
+ fn: () => page.evaluate('document.fonts.ready'),
130
+ debug: 'beforeScreenshot:fontsReady'
131
+ },
132
+ {
133
+ fn: () => waitForImagesOnViewport(page),
134
+ debug: 'beforeScreenshot:waitForImagesOnViewport'
135
+ }
136
+ ]
137
+
138
+ if (codeScheme && response) {
139
+ tasks.push({
140
+ fn: () => waitForPrism(page, response, { codeScheme, ...opts }),
141
+ debug: 'beforeScreenshot:waitForPrism'
142
+ })
143
+ }
144
+
145
+ if (fullPage) {
146
+ tasks.push({
147
+ fn: () => scrollFullPageToLoadContent(page, timeout, goto),
148
+ debug: 'beforeScreenshot:scrollFullPageToLoadContent'
149
+ })
150
+ } else if (element) {
151
+ tasks.push({
152
+ fn: async () => {
153
+ screenshotOpts = await waitForElement(page, element)
76
154
  },
77
- {
78
- fn: () => waitForImagesOnViewport(page),
79
- debug: 'beforeScreenshot:waitForImagesOnViewport'
80
- }
81
- ].map(({ fn, ...opts }) => goto.run({ fn: fn(), ...opts, timeout }))
155
+ debug: 'beforeScreenshot:waitForElement'
156
+ })
157
+ }
158
+
159
+ await Promise.all(
160
+ tasks.map(({ fn, ...opts }) =>
161
+ goto.run({
162
+ fn: fn(),
163
+ ...opts,
164
+ timeout: fullPage ? timeout * 2 : timeout
165
+ })
166
+ )
82
167
  )
168
+
169
+ return screenshotOpts
83
170
  }
84
171
 
85
172
  const takeScreenshot = async opts => {
@@ -98,19 +185,13 @@ module.exports = ({ goto, ...gotoOpts }) => {
98
185
 
99
186
  if (waitUntil !== 'auto') {
100
187
  ;({ response } = await goto(page, { ...opts, url, waitUntil }))
101
- const [screenshotOpts] = await Promise.all([
102
- waitForElement(page, element),
103
- beforeScreenshot(response)
104
- ])
188
+ const screenshotOpts = await beforeScreenshot(page, response, opts)
105
189
  screenshot = await page.screenshot({ ...opts, ...screenshotOpts })
106
190
  debug('screenshot', { waitUntil, duration: timeScreenshot() })
107
191
  } else {
108
192
  ;({ response } = await goto(page, { ...opts, url, waitUntil, waitUntilAuto }))
109
193
  async function waitUntilAuto (page, { response }) {
110
- const [screenshotOpts] = await Promise.all([
111
- waitForElement(page, element),
112
- beforeScreenshot(response)
113
- ])
194
+ const screenshotOpts = await beforeScreenshot(page, response, opts)
114
195
  const { isWhite } = await takeScreenshot({ ...opts, ...screenshotOpts })
115
196
  debug('screenshot', { waitUntil, isWhite, duration: timeScreenshot() })
116
197
  }
@@ -32,8 +32,6 @@ const JSONParse = input => {
32
32
  }
33
33
 
34
34
  module.exports = async (page, response, { timeout, codeScheme, styles, scripts, modules }) => {
35
- if (!response || !codeScheme) return
36
-
37
35
  let [theme, content, prism] = await Promise.all([getTheme(codeScheme), response.text(), getPrism])
38
36
 
39
37
  if (isHtmlContent(content)) return