feat: recording/playing back network requests with nock (#18)

* feat: recording/playing back network requests with nock

* lint fix
pull/20/head
Adam Pash 8 years ago committed by GitHub
parent 6e29848e9c
commit 629eada1f7

@ -1 +1,3 @@
**/fixtures/*
dist/*
coverage/*

@ -13,6 +13,8 @@
"fit",
"jasmine",
"beforeEach",
"beforeAll",
"afterAll",
},
"rules": {
"no-param-reassign": 0,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -5,7 +5,7 @@
"repository": "github:postlight/mercury-parser",
"main": "./dist/mercury.js",
"scripts": {
"lint": "eslint src/**/*.js --fix",
"lint": "eslint . --fix",
"lint-fix-quiet": "eslint --fix --quiet",
"build": "yarn lint && rollup -c",
"build-generator": "rollup -c scripts/rollup.config.js",
@ -45,6 +45,7 @@
"jest": "^16.0.2",
"jest-cli": "^16.0.2",
"mocha": "^3.0.2",
"nock": "^9.0.2",
"ora": "^0.3.0",
"rollup": "^0.36.3",
"rollup-plugin-babel": "^2.6.1",

@ -1,9 +1,10 @@
import babel from 'rollup-plugin-babel'
import babelrc from 'babelrc-rollup'
import commonjs from 'rollup-plugin-commonjs'
/* eslint-disable import/no-extraneous-dependencies */
import babel from 'rollup-plugin-babel';
import babelrc from 'babelrc-rollup'; // eslint-disable-line import/extensions
import commonjs from 'rollup-plugin-commonjs';
let babelOpts = babelrc()
babelOpts.runtimeHelpers = true
const babelOpts = babelrc();
babelOpts.runtimeHelpers = true;
export default {
entry: 'src/mercury.js',
@ -14,4 +15,4 @@ export default {
format: 'cjs',
dest: 'dist/mercury.js', // equivalent to --output
sourceMap: true,
}
};

@ -1,16 +1,19 @@
import fs from 'fs'
import URL from 'url'
import inquirer from 'inquirer'
import ora from 'ora'
import { exec } from 'child_process'
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-use-before-define */
/* eslint-disable no-console */
import fs from 'fs';
import URL from 'url';
import inquirer from 'inquirer';
import ora from 'ora';
import { exec } from 'child_process';
import Mercury from '../dist/mercury'
import {
stripJunkTags,
makeLinksAbsolute,
} from 'utils/dom'
import extractorTemplate from './templates/custom-extractor'
import extractorTestTemplate from './templates/custom-extractor-test'
} from 'utils/dom';
import Mercury from '../dist/mercury';
import extractorTemplate from './templates/custom-extractor';
import extractorTestTemplate from './templates/custom-extractor-test';
const questions = [
{
@ -25,16 +28,12 @@ const questions = [
},
},
];
inquirer.prompt(questions).then((answers) => {
scaffoldCustomParser(answers.website);
});
let spinner;
function confirm(fn, args, msg, newParser) {
spinner = ora({ text: msg });
spinner.start();
const result = fn.apply(null, args);
const result = fn(...args);
if (result && result.then) {
result.then(r => savePage(r, args, newParser));
@ -45,6 +44,52 @@ function confirm(fn, args, msg, newParser) {
return result;
}
function confirmCreateDir(dir, msg) {
if (!fs.existsSync(dir)) {
confirm(fs.mkdirSync, [dir], msg);
}
}
function getDir(url) {
const { hostname } = URL.parse(url);
return `./src/extractors/custom/${hostname}`;
}
function scaffoldCustomParser(url) {
const dir = getDir(url);
const { hostname } = URL.parse(url);
let newParser = false;
if (!fs.existsSync(dir)) {
newParser = true;
confirmCreateDir(dir, `Creating ${hostname} directory`);
confirmCreateDir(`./fixtures/${hostname}`, 'Creating fixtures directory');
}
confirm(Mercury.fetchResource, [url], 'Fetching fixture', newParser);
}
inquirer.prompt(questions).then((answers) => {
scaffoldCustomParser(answers.website);
});
function generateScaffold(url, file, result) {
const { hostname } = URL.parse(url);
const extractor = extractorTemplate(hostname, extractorName(hostname));
const extractorTest =
extractorTestTemplate(
file, url, getDir(url), result, extractorName(hostname)
);
fs.writeFileSync(`${getDir(url)}/index.js`, extractor);
fs.writeFileSync(`${getDir(url)}/index.test.js`, extractorTest);
fs.appendFileSync(
'./src/extractors/custom/index.js',
exportString(url),
);
exec(`npm run lint-fix-quiet -- ${getDir(url)}/*.js`);
}
function savePage($, [url], newParser) {
const { hostname } = URL.parse(url);
@ -53,83 +98,44 @@ function savePage($, [url], newParser) {
const filename = new Date().getTime();
const file = `./fixtures/${hostname}/${filename}.html`;
// fix http(s) relative links:
makeLinksAbsolute($('*').first(), $, url)
makeLinksAbsolute($('*').first(), $, url);
$('[src], [href]').each((index, node) => {
const $node = $(node)
const link = $node.attr('src')
const $node = $(node);
const link = $node.attr('src');
if (link && link.slice(0, 2) === '//') {
$node.attr('src', `http:${link}`)
$node.attr('src', `http:${link}`);
}
})
});
const html = stripJunkTags($('*').first(), $, ['script']).html();
fs.writeFileSync(file, html);
const result = Mercury.parse(url, html).then((result) => {
Mercury.parse(url, html).then((result) => {
if (newParser) {
confirm(generateScaffold, [url, file, result], 'Generating parser and tests');
console.log(`Your custom site extractor has been set up. To get started building it, run
yarn watch:test -- ${hostname}
-- OR --
npm run watch:test -- ${hostname}`)
npm run watch:test -- ${hostname}`);
} else {
console.log(`
It looks like you already have a custom parser for this url.
The page you linked to has been added to ${file}. Copy and paste
the following code to use that page in your tests:
const html = fs.readFileSync('${file}');`)
const html = fs.readFileSync('${file}');`);
}
})
});
}
function generateScaffold(url, file, result) {
function exportString(url) {
const { hostname } = URL.parse(url);
const extractor = extractorTemplate(hostname, extractorName(hostname))
const extractorTest = extractorTestTemplate(file, url, getDir(url), result, extractorName(hostname))
fs.writeFileSync(`${getDir(url)}/index.js`, extractor)
fs.writeFileSync(`${getDir(url)}/index.test.js`, extractorTest)
fs.appendFileSync(
'./src/extractors/custom/index.js',
exportString(url),
)
exec(`npm run lint-fix-quiet -- ${getDir(url)}/*.js`)
return `export * from './${hostname}';`;
}
function extractorName(hostname) {
const name = hostname
.split('.')
.map(w => `${w.charAt(0).toUpperCase()}${w.slice(1)}`)
.join('')
return `${name}Extractor`
}
function exportString(url) {
const { hostname } = URL.parse(url);
return `export * from './${hostname}';`;
}
function confirmCreateDir(dir, msg) {
if (!fs.existsSync(dir)) {
confirm(fs.mkdirSync, [dir], msg);
}
}
function scaffoldCustomParser(url) {
const dir = getDir(url);
const { hostname } = URL.parse(url);
let newParser = false
if (!fs.existsSync(dir)) {
newParser = true
confirmCreateDir(dir, `Creating ${hostname} directory`);
confirmCreateDir(`./fixtures/${hostname}`, 'Creating fixtures directory');
}
confirm(Mercury.fetchResource, [url], 'Fetching fixture', newParser);
}
function getDir(url) {
const { hostname } = URL.parse(url);
return `./src/extractors/custom/${hostname}`;
.join('');
return `${name}Extractor`;
}

@ -1,9 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies */
import babel from 'rollup-plugin-babel';
import babelrc from 'babelrc-rollup';
import babelrc from 'babelrc-rollup'; // eslint-disable-line import/extensions
import commonjs from 'rollup-plugin-commonjs';
let babelOpts = babelrc()
babelOpts.runtimeHelpers = true
const babelOpts = babelrc();
babelOpts.runtimeHelpers = true;
export default {
entry: './scripts/generate-custom-parser.js',

@ -10,10 +10,10 @@ const IGNORE = [
'direction',
'total_pages',
'rendered_pages',
]
];
function testFor(key, value, dir, file, url) {
if (IGNORE.find(k => k === key)) return ''
if (IGNORE.find(k => k === key)) return '';
return template`
it('returns the ${key}', async () => {
@ -29,7 +29,7 @@ function testFor(key, value, dir, file, url) {
// Update these values with the expected values from
// the article.
assert.equal(${key}, ${value ? "`" + value + "`" : "''"})
assert.equal(${key}, ${value ? `\`${value}\`` : "''"})
});
`;
}

@ -1,4 +1,4 @@
import insertValues from './insert-values'
import insertValues from './insert-values';
const bodyPattern = /^\n([\s\S]+)\s{2}$/gm;
const trailingWhitespace = /\s+$/;

@ -1,9 +1,14 @@
import assert from 'assert';
import { Errors } from 'utils';
import { record } from 'test-helpers';
import Mercury from './mercury';
describe('Mercury', () => {
const recorder = record('mercury-test');
beforeAll(recorder.before);
afterAll(recorder.after);
describe('parse(url)', () => {
it('returns an error if a malformed url is passed', async () => {
const error = await Mercury.parse('foo.com');

@ -1,9 +1,14 @@
import assert from 'assert';
import { Errors } from 'utils';
import { record } from 'test-helpers';
import Resource from './index';
describe('Resource', () => {
const recorder = record('resource-test');
beforeAll(recorder.before);
afterAll(recorder.after);
describe('create(url)', () => {
it('fetches the page and returns a cheerio object', async () => {
const url = 'http://theconcourse.deadspin.com/1786177057';

@ -28,7 +28,15 @@ function get(options) {
export function validateResponse(response, parseNon2xx = false) {
// Check if we got a valid status code
if (response.statusMessage !== 'OK') {
// This isn't great, but I'm requiring a statusMessage to be set
// before short circuiting b/c nock doesn't set it in tests
// statusMessage only not set in nock response, in which case
// I check statusCode, which is currently only 200 for OK responses
// in tests
if (
(response.statusMessage && response.statusMessage !== 'OK') ||
response.statusCode !== 200
) {
if (!response.statusCode) {
throw new Error(
`Unable to fetch content. Original exception was ${response.error}`

@ -1,6 +1,7 @@
import assert from 'assert';
import URL from 'url';
import { record } from 'test-helpers';
import {
default as fetchResource,
baseDomain,
@ -9,6 +10,10 @@ import {
import { MAX_CONTENT_LENGTH } from './constants';
describe('fetchResource(url)', () => {
const recorder = record('fetch-resource-test');
beforeAll(recorder.before);
afterAll(recorder.after);
it('returns appropriate json for bad url', async () => {
const url = 'http://www.nytimes.com/500';
const { error } = await fetchResource(url);

@ -1,4 +1,7 @@
import assert from 'assert';
import nock from 'nock'; // eslint-disable-line import/no-extraneous-dependencies
import fs from 'fs';
import path from 'path';
export function clean(string) {
return string.trim().replace(/\r?\n|\r/g, '').replace(/\s+/g, ' ');
@ -7,3 +10,45 @@ export function clean(string) {
export function assertClean(a, b) {
assert.equal(clean(a), clean(b));
}
// using this from https://www.ctl.io/developers/blog/post/http-apis-test-code
export function record(name, options = {}) {
const test_folder = options.test_folder || '.';
const fixtures_folder = options.fixtures_folder || 'fixtures/nock';
const fp = path.join(test_folder, fixtures_folder, `${name}.js`);
// `has_fixtures` indicates whether the test has fixtures we should read,
// or doesn't, so we should record and save them.
// the environment variable `NOCK_RECORD` can be used to force a new recording.
let has_fixtures = !!process.env.NOCK_RECORD;
return {
// starts recording, or ensure the fixtures exist
before: () => {
if (!has_fixtures) {
try {
require(`../${fp}`); // eslint-disable-line global-require, import/no-dynamic-require, max-len
has_fixtures = true;
} catch (e) {
nock.recorder.rec({
dont_print: true,
});
}
} else {
has_fixtures = false;
nock.recorder.rec({
dont_print: true,
});
}
},
// saves our recording if fixtures didn't already exist
after: (done) => {
if (!has_fixtures) {
has_fixtures = nock.recorder.play();
const text = `const nock = require('nock');\n${has_fixtures.join('\n')}`;
fs.writeFile(fp, text, done);
} else {
done();
}
},
};
}

@ -143,6 +143,10 @@ assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
assertion-error@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
async-array-reduce@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/async-array-reduce/-/async-array-reduce-0.2.0.tgz#743d91238cf71e79e6d59e86ad080f6c437a5bd6"
@ -801,6 +805,14 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
"chai@>=1.9.2 <4.0.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
dependencies:
assertion-error "^1.0.1"
deep-eql "^0.1.3"
type-detect "^1.0.0"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -1000,6 +1012,16 @@ decamelize@^1.0.0, decamelize@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
deep-eql@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
dependencies:
type-detect "0.1.1"
deep-equal@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@ -2260,7 +2282,7 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
json-stringify-safe@~5.0.1:
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@ -2495,6 +2517,10 @@ lodash@~2.4.x:
version "2.4.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
lodash@~4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.9.0.tgz#4c20d742f03ce85dc700e0dd7ab9bcab85e6fc14"
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@ -2664,6 +2690,19 @@ natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
nock:
version "9.0.2"
resolved "https://registry.yarnpkg.com/nock/-/nock-9.0.2.tgz#f6a5f4a8d560d61f48b5ad428ccff8dc9b62701e"
dependencies:
chai ">=1.9.2 <4.0.0"
debug "^2.2.0"
deep-equal "^1.0.0"
json-stringify-safe "^5.0.1"
lodash "~4.9.0"
mkdirp "^0.5.0"
propagate "0.4.0"
qs "^6.0.2"
node-emoji@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.4.1.tgz#c9fa0cf91094335bcb967a6f42b2305c15af2ebc"
@ -2903,6 +2942,10 @@ progress@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
propagate@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481"
prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@ -2911,7 +2954,7 @@ punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.3.0:
qs@^6.0.2, qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
@ -3439,6 +3482,14 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
type-detect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
type-detect@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822"
typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"

Loading…
Cancel
Save