Skip to content
Permalink
Browse files

refactor: replace Mocha + superagent with Jest + Got, format with pre…

…ttier
  • Loading branch information...
msokk committed Mar 3, 2019
1 parent dad6d4f commit cd7dce96af70a9cb6d5737e52fc9f05aed7a783e
@@ -1,7 +1,7 @@
{
"env": {
"node": true,
"mocha": true
"jest": true
},
"extends": "airbnb-base"
"extends": ["airbnb-base", "plugin:jest/recommended", "plugin:prettier/recommended"]
}
@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}
@@ -0,0 +1,97 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const got = require('got');

const fixturePath = path.join(__dirname, 'fixtures');

const client = got.extend({
baseUrl: 'http://localhost:3000',
query: { accessKey: process.env.RENDERER_ACCESS_KEY }
});

function recordFailure(name, body) {
fs.writeFileSync(`./${name}`, body);
execSync(`curl --upload-file ./${name} https://transfer.sh/${name}`, {
stdio: 'inherit'
});
}

beforeEach(() => {
jest.setTimeout(10000);
});

describe('integration', () => {
describe('GET /stats', () => {
it('should disallow access without key', async () => {
expect.assertions(1);
try {
await got.get('http://localhost:3000/stats');
} catch (error) {
expect(error.response.statusCode).toEqual(403);
}
});

it('should print empty stats', async () => {
const { body } = await client.get('/stats', { json: true });

expect(body).toEqual({
concurrency: 1,
queue_length: 0,
workersList: []
});
});
});

describe('GET /png', () => {
it('should render valid png from fixtures/example.html', async () => {
const { body } = await client.get('/png', {
encoding: null,
query: { url: 'https://example.com/' }
});

const examplePngPath = path.join(fixturePath, 'example.png');
const fixture = fs.readFileSync(examplePngPath);
if (body.compare(fixture) === 0) return;

recordFailure('example_failed.png', body);
throw new Error(`${examplePngPath} does not match rendered screenshot`);
});
});

describe('GET /pdf', () => {
it('should render valid pdf from fixtures/example.html', async () => {
const { body } = await client.get('/pdf', {
encoding: null,
query: { url: 'https://example.com/' }
});

const examplePdfPath = path.join(fixturePath, 'example.pdf');
const fixture = fs.readFileSync(examplePdfPath);
if (body.slice(150).compare(fixture.slice(150)) === 0) return; // Slice out ModDate

recordFailure('example_failed.pdf', body);
throw new Error(`${examplePdfPath} does not match rendered pdf`);
});
});

describe('POST /pdf', () => {
it('should render valid pdf from POSTED html in fixtures/example.html', async () => {
const exampleHtmlPath = path.join(fixturePath, 'example.html');
const exampleHtml = fs.readFileSync(exampleHtmlPath, 'utf-8');

const { body } = await client.post('/pdf', {
body: exampleHtml,
encoding: null,
query: { url: 'https://example.com/' }
});

const examplePdfPath = path.join(fixturePath, 'example.pdf');
const fixture = fs.readFileSync(examplePdfPath);

if (body.slice(150).compare(fixture.slice(150)) === 0) return; // Slice out ModDate
recordFailure('example_failed_post.pdf', body);
throw new Error(`${examplePdfPath} does not match rendered pdf`);
});
});
});
@@ -0,0 +1,49 @@
const fs = require('fs');
const path = require('path');
const async = require('async');
const { execSync } = require('child_process');
const got = require('got');

const bigTableHtml = fs.readFileSync(path.resolve(__dirname, 'fixtures/bigTable.html'), 'utf-8');

beforeEach(() => {
jest.setTimeout(60000);
});

describe('stability', () => {
describe('POST /pdf', () => {
it("should render proper PDF's when posting large amounts of big HTML chunks", done => {
const fixturePath = path.join(__dirname, 'fixtures');
const examplePdfPath = path.join(fixturePath, 'bigTable.pdf');
const fixture = fs.readFileSync(examplePdfPath);

const q = async.queue(async task => {
const { body } = await got.post('http://localhost:3000/pdf', {
body: bigTableHtml,
encoding: null,
query: {
accessKey: process.env.RENDERER_ACCESS_KEY,
url: 'https://example.com/'
}
});

if (body.slice(150).compare(fixture.slice(150)) === 0) return; // Slice out ModDate

fs.writeFileSync('./bigTable_failed.pdf', body);
execSync(
'curl --upload-file ./bigTable_failed.pdf https://transfer.sh/bigTable_failed.pdf',
{ stdio: 'inherit' }
);
throw new Error(`${examplePdfPath} (${task}) does not match rendered screenshot`);
}, 8);

for (let i = 0; i < 80; i += 1) {
q.push(i, err => {
if (err) done(err);
});
}

q.drain = done;
});
});
});
@@ -0,0 +1,9 @@
const { printUsage } = require('../src/util');

describe('util#printUsage()', () => {
it('should print default usage string', () => {
expect(printUsage()).toBe(
'Usage: GET /[pdf|png|jpeg]?accessKey=[token]&url=http%3A%2F%2Fgoogle.com'
);
});
});
@@ -8,7 +8,7 @@
"scripts": {
"start": "electron src/server.js",
"lint": "eslint src",
"test": "electron node_modules/.bin/_mocha --exit"
"test": "jest"
},
"keywords": [
"electron",
@@ -36,10 +36,14 @@
"retry": "^0.12.0"
},
"devDependencies": {
"eslint": "^5.14.1",
"eslint": "^5.15.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.16.0",
"mocha": "^6.0.2",
"supertest": "^3.4.2"
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-prettier": "^3.0.1",
"got": "^9.6.0",
"jest": "^24.1.0",
"prettier": "^1.16.4"
}
}
@@ -8,7 +8,9 @@ if (process.env[KEY_PREFIX]) validKeys.global = process.env[KEY_PREFIX];
Object.keys(process.env)
.filter(k => k.startsWith(`${KEY_PREFIX}_`))
.filter(k => process.env[k].length > 0)
.forEach((k) => { validKeys[k.replace(`${KEY_PREFIX}_`, '').toLowerCase()] = process.env[k]; });
.forEach(k => {
validKeys[k.replace(`${KEY_PREFIX}_`, '').toLowerCase()] = process.env[k];
});

if (Object.keys(validKeys).length === 0) {
process.stderr.write(`No ${KEY_PREFIX} environment variable defined!\n`);
@@ -23,7 +25,7 @@ module.exports = function authMiddleware(req, res, next) {
const key = Object.keys(validKeys).filter(k => validKeys[k] === sentKey);
if (!sentKey || key.length === 0) {
return res.status(403).send({
error: { code: 'UNAUTHORIZED', message: 'Invalid or missing access key.' },
error: { code: 'UNAUTHORIZED', message: 'Invalid or missing access key.' }
});
}

@@ -11,24 +11,36 @@ class RendererError extends Error {
}
exports.RendererError = RendererError;


/**
* Handle loading failure errors
*/
function handleLoadingError(currentUrl, event, code, desc, url) {
switch (code) {
case -102:
return Promise.reject(new RendererError('CONNECTION_REFUSED', 'Connection attempt was refused.'));
return Promise.reject(
new RendererError('CONNECTION_REFUSED', 'Connection attempt was refused.')
);
case -105:
return Promise.reject(new RendererError('NAME_NOT_RESOLVED', 'The host name could not be resolved.'));
return Promise.reject(
new RendererError('NAME_NOT_RESOLVED', 'The host name could not be resolved.')
);
case -137:
return Promise.reject(new RendererError('NAME_RESOLUTION_FAILED', 'Hostname resolution failed (DNS).'));
return Promise.reject(
new RendererError('NAME_RESOLUTION_FAILED', 'Hostname resolution failed (DNS).')
);
case -300:
return Promise.reject(new RendererError('INVALID_URL', 'The URL is invalid.'));
case -501:
return Promise.reject(new RendererError('INSECURE_RESPONSE', 'The server\'s response was insecure (e.g. there was a cert error).'));
return Promise.reject(
new RendererError(
'INSECURE_RESPONSE',
"The server's response was insecure (e.g. there was a cert error)."
)
);
case -6:
return Promise.reject(new RendererError('FILE_NOT_FOUND', 'The file or directory cannot be found.'));
return Promise.reject(
new RendererError('FILE_NOT_FOUND', 'The file or directory cannot be found.')
);
case -3:
// Subresource fails to load, render page anyway
if (currentUrl !== url) {
@@ -42,24 +54,26 @@ function handleLoadingError(currentUrl, event, code, desc, url) {
}
}


/**
* Validate renderer result
*/
exports.validateResult = function validateResult(originalUrl, eventType, ...args) {
switch (eventType) {
// Loading failures
case 'did-fail-load': return handleLoadingError(originalUrl, ...args);
case 'did-fail-load':
return handleLoadingError(originalUrl, ...args);
// Renderer process has crashed
case 'crashed':
return Promise.reject(new RendererError('RENDERER_CRASH', 'Render process crashed.'));
// Page loading timed out
case 'timeout':
return Promise.reject(new RendererError('RENDERER_TIMEOUT', 'Renderer timed out.', 524));
// Page loaded successfully
case 'did-finish-load': return Promise.resolve();
case 'did-finish-load':
return Promise.resolve();

// Unhandled event
default: return Promise.reject(new RendererError('UNHANDLED_EVENT', eventType));
default:
return Promise.reject(new RendererError('UNHANDLED_EVENT', eventType));
}
};
@@ -29,9 +29,10 @@ function renderPDF(options, done) {
// Support setting page size in microns with NxN syntax
const customPage = options.pageSize.match(/([0-9]+)x([0-9]+)/);
if (customPage) {
options.pageSize = { // eslint-disable-line no-param-reassign
// eslint-disable-next-line no-param-reassign
options.pageSize = {
width: parseInt(customPage[1], 10),
height: parseInt(customPage[2], 10),
height: parseInt(customPage[2], 10)
};
}

@@ -43,7 +44,8 @@ function renderPDF(options, done) {
return;
}
this.webContents.printToPDF(options, (err, data) => {
if (data.slice(150).compare(pdfFailedFixture.slice(150)) === 0) { // Slice out ModDate
if (data.slice(150).compare(pdfFailedFixture.slice(150)) === 0) {
// Slice out ModDate
console.log('Pdf empty, creation failed! Retrying...');
setTimeout(attemptRender, 50);
return;
@@ -58,9 +60,7 @@ function renderPDF(options, done) {
/**
* Render image png/jpeg
*/
function renderImage({
type, quality, browserWidth, browserHeight, clippingRect,
}, done) {
function renderImage({ type, quality, browserWidth, browserHeight, clippingRect }, done) {
const handleCapture = image => done(null, type === 'png' ? image.toPNG() : image.toJPEG(quality));

if (clippingRect) {
@@ -87,7 +87,7 @@ exports.renderWorker = function renderWorker(window, task, done) {
retries: TIMEOUT,
factor: 1,
minTimeout: 750,
maxTimeout: 1000,
maxTimeout: 1000
});
}

@@ -104,12 +104,12 @@ exports.renderWorker = function renderWorker(window, task, done) {
if (type !== 'did-finish-load') {
renderIt();

// Delay rendering n seconds
// Delay rendering n seconds
} else if (task.delay > 0) {
console.log('delaying pdf generation by %sms', task.delay * 1000);
setTimeout(renderIt, task.delay * 1000);

// Look for specific string before rendering
// Look for specific string before rendering
} else if (task.waitForText) {
console.log('delaying pdf generation, waiting for text "%s" to appear', task.waitForText);
waitOperation.attempt(() => {
@@ -122,7 +122,9 @@ exports.renderWorker = function renderWorker(window, task, done) {
const isRetrying = waitOperation.retry(new Error('not ready to render'));

if (!isRetrying) {
done(new RendererError('TEXT_NOT_FOUND', `Failed to find text: ${task.waitForText}`, 404));
done(
new RendererError('TEXT_NOT_FOUND', `Failed to find text: ${task.waitForText}`, 404)
);
webContents.removeListener('found-in-page', foundInPage);
}
} else if (result.finalUpdate) {
@@ -154,16 +156,16 @@ exports.createWindow = function createWindow() {
blinkFeatures: 'OverlayScrollbars', // Slimmer scrollbars
allowDisplayingInsecureContent: true, // Show http content on https site
allowRunningInsecureContent: true, // Run JS, CSS from http urls
nodeIntegration: false, // Disable exposing of Node.js symbols to DOM
},
nodeIntegration: false // Disable exposing of Node.js symbols to DOM
}
});

// Set user agent
const { webContents } = window;
webContents.setUserAgent(`${webContents.getUserAgent()} ${pjson.name}/${pjson.version}`);

// Emit end events to an aggregate for worker to listen on once
['did-fail-load', 'crashed', 'did-finish-load', 'timeout'].forEach((e) => {
['did-fail-load', 'crashed', 'did-finish-load', 'timeout'].forEach(e => {
webContents.on(e, (...args) => webContents.emit('finished', e, ...args));
});

Oops, something went wrong.

0 comments on commit cd7dce9

Please sign in to comment.
You can’t perform that action at this time.