diff --git a/.gitignore b/.gitignore index 3c3629e6..93cab344 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +yarn-error.log diff --git a/README.md b/README.md index bbe175ad..8b2e17a8 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ key is `urls`. Other optional options are: * `puppeteerArgs` - Args sent to puppeteer when launching. [List of strings for headless Chrome](https://peter.sh/experiments/chromium-command-line-switches/). * `cssoOptions` - CSSO compress function [options](https://github.com/css/csso#compressast-options) +* `timeout` - Maximum navigation time in milliseconds, defaults to 30 seconds, pass 0 to disable timeout. ## Warnings diff --git a/src/run.js b/src/run.js index af8d143b..d2f3a40c 100644 --- a/src/run.js +++ b/src/run.js @@ -7,6 +7,7 @@ const csso = require('csso'); const csstree = require('css-tree'); const cheerio = require('cheerio'); const utils = require('./utils'); +const { createTracker } = require('./tracker'); const url = require('url'); const isOk = response => response.ok() || response.status() === 304; @@ -124,8 +125,20 @@ const processPage = ({ // a second time. let fulfilledPromise = false; + const tracker = createTracker(page); const safeReject = error => { if (!fulfilledPromise) { + if (error.message.startsWith('Navigation Timeout Exceeded')) { + const urls = tracker.urls(); + if (urls.length > 1) { + error.message += `\nFor one of the following urls: ${urls.join( + ',' + )}`; + } else if (urls.length > 0) { + error.message += `\nFor ${urls[0]}`; + } + } + tracker.dispose(); reject(error); } }; @@ -146,6 +159,10 @@ const processPage = ({ await page.setViewport(options.viewport); } + if (options.timeout !== undefined) { + page.setDefaultNavigationTimeout(options.timeout); + } + // A must or else you can't do console.log from within page.evaluate() page.on('console', msg => { if (debug) { @@ -311,7 +328,10 @@ const processPage = ({ allHrefs.push(href); }); - if (!fulfilledPromise) resolve(); + if (!fulfilledPromise) { + tracker.dispose(); + resolve(); + } } catch (e) { return safeReject(e); } diff --git a/src/tracker.js b/src/tracker.js new file mode 100644 index 00000000..e8dd04d1 --- /dev/null +++ b/src/tracker.js @@ -0,0 +1,18 @@ +const createTracker = page => { + const requests = new Set(); + const onStarted = request => requests.add(request); + const onFinished = request => requests.delete(request); + page.on('request', onStarted); + page.on('requestfinished', onFinished); + page.on('requestfailed', onFinished); + return { + urls: () => Array.from(requests).map(r => r.url()), + dispose: () => { + page.removeListener('request', onStarted); + page.removeListener('requestfinished', onFinished); + page.removeListener('requestfailed', onFinished); + } + }; +}; + +module.exports = { createTracker }; diff --git a/tests/main.test.js b/tests/main.test.js index 4d6bd512..d9e3ee81 100644 --- a/tests/main.test.js +++ b/tests/main.test.js @@ -16,6 +16,10 @@ fastify.get('/307.html', (req, reply) => { reply.redirect(307, '/redirected.html'); }); +fastify.get('/timeout.html', (req, reply) => { + setTimeout(() => reply.send('timeout'), 3000); +}); + let browser; const runMinimalcss = (path, options = {}) => { @@ -216,3 +220,13 @@ test('accept CSSO options', async () => { ({ finalCss } = await runMinimalcss('comments', { cssoOptions })); expect(finalCss).not.toMatch('test css comment'); }); + +test('timeout', async () => { + expect.assertions(2); + try { + await runMinimalcss('timeout', { timeout: 2000 }); + } catch (e) { + expect(e.message).toMatch('Navigation Timeout Exceeded: 2000ms exceeded'); + expect(e.message).toMatch('For http://localhost:3000/timeout.html'); + } +});