From 9f00685f8c07653833e58eaca2554bdbedad0fb7 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Tue, 15 May 2018 10:40:35 -0400 Subject: [PATCH] Node acceptance tests, round 2 --- .eslintrc.js | 5 +- node-tests/acceptance/build-test.js | 86 ++++++++++++++ .../fixtures/skeleton-app/app/index.html | 0 .../fixtures/skeleton-app/app/styles/.gitkeep | 0 .../skeleton-app/config/environment.js | 9 ++ .../fixtures/skeleton-app/ember-cli-build.js | 7 ++ node-tests/fixtures/skeleton-app/package.json | 14 +++ .../fixtures/skeleton-app/tsconfig.json | 15 +++ node-tests/helpers/skeleton-app.js | 105 ++++++++++++++++++ package.json | 3 + yarn.lock | 2 +- 11 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 node-tests/acceptance/build-test.js create mode 100644 node-tests/fixtures/skeleton-app/app/index.html create mode 100644 node-tests/fixtures/skeleton-app/app/styles/.gitkeep create mode 100644 node-tests/fixtures/skeleton-app/config/environment.js create mode 100644 node-tests/fixtures/skeleton-app/ember-cli-build.js create mode 100644 node-tests/fixtures/skeleton-app/package.json create mode 100644 node-tests/fixtures/skeleton-app/tsconfig.json create mode 100644 node-tests/helpers/skeleton-app.js diff --git a/.eslintrc.js b/.eslintrc.js index a455bc7f6..12da1b8b2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,7 @@ module.exports = { plugins: ['node'], rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { // add your custom rules and overrides for node files here - 'ember/avoid-leaking-state-in-ember-objects': 'off', + 'ember/avoid-leaking-state-in-ember-objects': 'off' }), }, @@ -53,6 +53,9 @@ module.exports = { env: { mocha: true, }, + rules: { + 'node/no-unpublished-require': 'off' + } }, ], }; diff --git a/node-tests/acceptance/build-test.js b/node-tests/acceptance/build-test.js new file mode 100644 index 000000000..f9bd57652 --- /dev/null +++ b/node-tests/acceptance/build-test.js @@ -0,0 +1,86 @@ +'use strict'; + +const co = require('co'); +const SkeletonApp = require('../helpers/skeleton-app'); +const chai = require('ember-cli-blueprint-test-helpers/chai'); +const esprima = require('esprima'); +const expect = chai.expect; + +describe('Acceptance: build', function() { + this.timeout(30 * 1000); + + beforeEach(function() { + this.app = new SkeletonApp(); + }); + + afterEach(function() { + this.app.teardown(); + }); + + it('builds and rebuilds files', co.wrap(function*() { + this.app.writeFile('app/app.ts', ` + export function add(a: number, b: number) { + return a + b; + } + `); + + let server = this.app.serve(); + + yield server.waitForBuild(); + + expectModuleBody(this.app, 'skeleton-app/app', ` + exports.add = add; + function add(a, b) { + return a + b; + } + `); + + this.app.writeFile('app/app.ts', ` + export const foo: string = 'hello'; + `); + + yield server.waitForBuild(); + + expectModuleBody(this.app, 'skeleton-app/app', ` + var foo = exports.foo = 'hello'; + `); + })); + + it('fails the build when noEmitOnError is set and an error is emitted', co.wrap(function*() { + this.app.writeFile('app/app.ts', `import { foo } from 'nonexistent';`); + + yield expect(this.app.build()).to.be.rejectedWith(`Cannot find module 'nonexistent'`); + })); +}); + +function extractModuleBody(script, moduleName) { + let parsed = esprima.parseScript(script); + let definition = parsed.body + .filter(stmt => stmt.type === 'ExpressionStatement') + .map(stmt => stmt.expression) + .find(expr => + expr.type === 'CallExpression' && + expr.callee.type === 'Identifier' && + expr.callee.name === 'define' && + expr.arguments && + expr.arguments[0] && + expr.arguments[0].type === 'Literal' && + expr.arguments[0].value === moduleName); + + let moduleDef = definition.arguments[2].body; + + // Strip `'use strict'` + moduleDef.body.shift(); + + // Strip `__esModule` definition + moduleDef.body.shift(); + + return moduleDef; +} + +function expectModuleBody(app, name, body) { + let src = app.readFile('dist/assets/skeleton-app.js'); + let actual = extractModuleBody(src, name); + let expected = esprima.parseScript(body); + expect(actual.body).to.deep.equal(expected.body); +} diff --git a/node-tests/fixtures/skeleton-app/app/index.html b/node-tests/fixtures/skeleton-app/app/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/node-tests/fixtures/skeleton-app/app/styles/.gitkeep b/node-tests/fixtures/skeleton-app/app/styles/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/node-tests/fixtures/skeleton-app/config/environment.js b/node-tests/fixtures/skeleton-app/config/environment.js new file mode 100644 index 000000000..4884d703f --- /dev/null +++ b/node-tests/fixtures/skeleton-app/config/environment.js @@ -0,0 +1,9 @@ +/* eslint-env node */ +'use strict'; + +module.exports = function(environment) { + return { + environment, + modulePrefix: 'skeleton-app' + }; +}; diff --git a/node-tests/fixtures/skeleton-app/ember-cli-build.js b/node-tests/fixtures/skeleton-app/ember-cli-build.js new file mode 100644 index 000000000..dd9fdc549 --- /dev/null +++ b/node-tests/fixtures/skeleton-app/ember-cli-build.js @@ -0,0 +1,7 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function(defaults) { + return new EmberApp(defaults, {}).toTree(); +}; diff --git a/node-tests/fixtures/skeleton-app/package.json b/node-tests/fixtures/skeleton-app/package.json new file mode 100644 index 000000000..dbd9d543b --- /dev/null +++ b/node-tests/fixtures/skeleton-app/package.json @@ -0,0 +1,14 @@ +{ + "name": "skeleton-app", + "devDependencies": { + "ember-cli": "*", + "ember-cli-htmlbars": "*", + "ember-cli-babel": "*", + "ember-source": "*", + "loader.js": "*", + "typescript": "*" + }, + "ember-addon": { + "paths": [".."] + } +} diff --git a/node-tests/fixtures/skeleton-app/tsconfig.json b/node-tests/fixtures/skeleton-app/tsconfig.json new file mode 100644 index 000000000..4df47d822 --- /dev/null +++ b/node-tests/fixtures/skeleton-app/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES6", + "allowJs": false, + "moduleResolution": "node", + "noEmitOnError": true, + "baseUrl": ".", + "paths": { + "skeleton-app/*": ["app/*"] + } + }, + "include": [ + "app" + ] +} diff --git a/node-tests/helpers/skeleton-app.js b/node-tests/helpers/skeleton-app.js new file mode 100644 index 000000000..421bfa0dc --- /dev/null +++ b/node-tests/helpers/skeleton-app.js @@ -0,0 +1,105 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const mktemp = require('mktemp'); +const execa = require('execa'); +const EventEmitter = require('events').EventEmitter; + +module.exports = class SkeletonApp { + constructor() { + this._watched = null; + this.root = mktemp.createDirSync('test-skeleton-app-XXXXXX'); + fs.copySync(`${__dirname}/../fixtures/skeleton-app`, this.root); + } + + build() { + return this._ember(['build']); + } + + serve() { + if (this._watched) { + throw new Error('Already serving'); + } + + return this._watched = new WatchedBuild(this._ember(['serve'])); + } + + updatePackageJSON(callback) { + let pkgPath = `${this.root}/package.json`; + let pkg = fs.readJSONSync(pkgPath); + fs.writeJSONSync(pkgPath, callback(pkg) || pkg, { spaces: 2 }); + } + + writeFile(filePath, contents) { + let fullPath = `${this.root}/${filePath}`; + fs.ensureDirSync(path.dirname(fullPath)); + fs.writeFileSync(fullPath, contents, 'utf-8'); + } + + readFile(path) { + return fs.readFileSync(`${this.root}/${path}`, 'utf-8'); + } + + removeFile(path) { + return fs.unlinkSync(`${this.root}/${path}`); + } + + teardown() { + if (this._watched) { + this._watched.kill(); + } + + this._cleanupRootDir({ retries: 1 }); + } + + _ember(args) { + let ember = require.resolve('ember-cli/bin/ember'); + return execa('node', [ember].concat(args), { cwd: this.root }); + } + + _cleanupRootDir(options) { + let retries = options && options.retries || 0; + + try { + fs.removeSync(this.root); + } catch (error) { + if (retries > 0) { + // Windows doesn't necessarily kill the process immediately, so + // leave a little time before trying to remove the directory. + setTimeout(() => this._cleanupRootDir({ retries: retries - 1 }), 250); + } else { + // eslint-disable-next-line no-console + console.warn(`Warning: unable to remove skeleton-app tmpdir ${this.root} (${error.code})`); + } + } + } +} + +class WatchedBuild extends EventEmitter { + constructor(ember) { + super(); + this._ember = ember; + this._ember.stdout.on('data', (data) => { + let output = data.toString(); + if (output.includes('Build successful')) { + this.emit('did-rebuild'); + } + }); + + this._ember.catch((error) => { + this.emit('did-error', error); + }); + } + + waitForBuild() { + return new Promise((resolve, reject) => { + this.once('did-rebuild', resolve); + this.once('did-error', reject); + }); + } + + kill() { + this._ember.kill(); + } +} diff --git a/package.json b/package.json index d03f3943b..18cbdd31b 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/node": "^9.6.5", "@types/qunit": "^2.0.31", "broccoli-asset-rev": "^2.6.0", + "co": "^4.6.0", "ember-cli": "~2.18.2", "ember-cli-app-version": "^3.1.3", "ember-cli-babel": "^6.6.0", @@ -89,7 +90,9 @@ "eslint": "^4.17.0", "eslint-plugin-ember": "^5.0.3", "eslint-plugin-node": "^6.0.0", + "esprima": "^4.0.0", "loader.js": "^4.2.3", + "mktemp": "^0.4.0", "mocha": "^5.0.0", "testdouble": "^3.5.0", "typescript": "^2.7.2" diff --git a/yarn.lock b/yarn.lock index 6ead81bcc..32b9e0c1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5237,7 +5237,7 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mktemp@~0.4.0: +mktemp@^0.4.0, mktemp@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"