From 7bc54be547b027ece4c3bcc33a2b7cc1da4d4b1a Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Thu, 13 Jul 2017 11:03:38 -0400 Subject: [PATCH 01/17] Allowing mechanism for serving built files --- cli/build.js | 17 +- cli/e2e.js | 50 +-- config/webpack/common.webpack.config.js | 4 +- config/webpack/serve.webpack.config.js | 83 +---- index.js | 12 + lib/sky-pages-module-generator.js | 4 +- loader/sky-pages-module/index.js | 4 +- test/cli-build.spec.js | 39 ++- test/cli-e2e.spec.js | 102 ++---- test/config-webpack-serve.spec.js | 330 ++---------------- ...pages-out-skyux2.spec.js => index.spec.js} | 18 +- test/utils-browser.spec.js | 176 ++++++++++ ...utils.spec.js => utils-host-utils.spec.js} | 2 +- test/utils-server.spec.js | 118 +++++++ utils/browser.js | 75 ++++ utils/runtime-test-utils.js | 7 + utils/server.js | 64 ++++ 17 files changed, 593 insertions(+), 512 deletions(-) rename test/{sky-pages-out-skyux2.spec.js => index.spec.js} (76%) create mode 100644 test/utils-browser.spec.js rename test/{host-utils.spec.js => utils-host-utils.spec.js} (98%) create mode 100644 test/utils-server.spec.js create mode 100644 utils/browser.js create mode 100644 utils/server.js diff --git a/cli/build.js b/cli/build.js index 594e14a1..fd75f4dd 100644 --- a/cli/build.js +++ b/cli/build.js @@ -4,10 +4,13 @@ const logger = require('winston'); const fs = require('fs-extra'); const merge = require('merge'); + const skyPagesConfigUtil = require('../config/sky-pages/sky-pages.config'); const generator = require('../lib/sky-pages-module-generator'); const assetsConfig = require('../lib/assets-configuration'); const pluginFileProcessor = require('../lib/plugin-file-processor'); +const server = require('../utils/server'); +const browser = require('../utils/browser'); function writeTSConfig() { var config = { @@ -157,7 +160,19 @@ function build(argv, skyPagesConfig, webpack) { cleanupAot(); } - resolve(stats); + // Serve build files + switch (argv.launch) { + case 'host': + case 'local': + server.start().then(port => { + browser(argv, skyPagesConfig, stats, port); + }); + break; + default: + resolve(stats); + break; + } + }); }); } diff --git a/cli/e2e.js b/cli/e2e.js index 549fb0da..8f48007f 100644 --- a/cli/e2e.js +++ b/cli/e2e.js @@ -4,15 +4,14 @@ const path = require('path'); const spawn = require('cross-spawn'); const logger = require('winston'); -const portfinder = require('portfinder'); -const HttpServer = require('http-server'); const selenium = require('selenium-standalone'); + const build = require('./build'); +const server = require('../utils/server'); // Disable this to quiet the output const spawnOptions = { stdio: 'inherit' }; -let httpServer; let seleniumServer; let start; @@ -44,18 +43,13 @@ function killServers(exitCode) { seleniumServer = null; } - if (httpServer) { - logger.info('Closing http server'); - httpServer.close(); - httpServer = null; - } - // Catch protractor's "Kitchen Sink" error. if (exitCode === 199) { logger.warn('Supressing protractor\'s "kitchen sink" error 199'); exitCode = 0; } + server.stop(); logger.info(`Execution Time: ${(new Date().getTime() - start) / 1000} seconds`); logger.info(`Exiting process with ${exitCode}`); process.exit(exitCode || 0); @@ -149,42 +143,6 @@ function spawnSelenium() { }); } -/** - * Spawns the httpServer - */ -function spawnServer() { - return new Promise((resolve, reject) => { - logger.info('Requesting open port...'); - - httpServer = HttpServer.createServer({ - root: 'dist/', - cors: true, - https: { - cert: path.resolve(__dirname, '../', 'ssl', 'server.crt'), - key: path.resolve(__dirname, '../', 'ssl', 'server.key') - }, - logFn: (req, res, err) => { - if (err) { - reject(err); - return; - } - } - }); - - portfinder - .getPortPromise() - .then(port => { - logger.info(`Open port found: ${port}`); - logger.info('Starting web server...'); - httpServer.listen(port, 'localhost', () => { - logger.info('Web server running.'); - resolve(port); - }); - }) - .catch(reject); - }); -} - /** * Spawns the build process. Captures the config used. */ @@ -212,7 +170,7 @@ function e2e(argv, skyPagesConfig, webpack) { Promise .all([ spawnBuild(argv, skyPagesConfig, webpack), - spawnServer(), + server.start(), spawnSelenium() ]) .then(values => { diff --git a/config/webpack/common.webpack.config.js b/config/webpack/common.webpack.config.js index 446100a0..3ac9d72b 100644 --- a/config/webpack/common.webpack.config.js +++ b/config/webpack/common.webpack.config.js @@ -41,10 +41,10 @@ function getWebpackConfig(skyPagesConfig) { switch (outConfigMode) { case 'advanced': appPath = spaPath('src', 'main.ts'); - break; + break; default: appPath = outPath('src', 'main-internal.ts'); - break; + break; } return { diff --git a/config/webpack/serve.webpack.config.js b/config/webpack/serve.webpack.config.js index d87a5b7e..ef072d2c 100644 --- a/config/webpack/serve.webpack.config.js +++ b/config/webpack/serve.webpack.config.js @@ -3,39 +3,13 @@ const fs = require('fs'); const path = require('path'); -const util = require('util'); -const open = require('open'); const logger = require('winston'); const webpackMerge = require('webpack-merge'); const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin'); const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); const skyPagesConfigUtil = require('../sky-pages/sky-pages.config'); -const hostUtils = require('../../utils/host-utils'); - -/** - * Returns the querystring base for parameters allowed to be passed through. - * PLEASE NOTE: The method is nearly duplicated in `runtime/params.ts`. - * @name getQueryStringFromArgv - * @param {Object} argv - * @param {SkyPagesConfig} skyPagesConfig - * @returns {string} - */ -function getQueryStringFromArgv(argv, skyPagesConfig) { - - let found = []; - skyPagesConfig.skyux.params.forEach(param => { - if (argv[param]) { - found.push(`${param}=${encodeURIComponent(argv[param])}`); - } - }); - - if (found.length) { - return `?${found.join('&')}`; - } - - return ''; -} +const browser = require('../../utils/browser'); /** * Returns the default webpackConfig. @@ -49,66 +23,17 @@ function getWebpackConfig(argv, skyPagesConfig) { * @name WebpackPluginDone */ function WebpackPluginDone() { - const shorthand = { - l: 'launch', - b: 'browser' - }; let launched = false; this.plugin('done', (stats) => { if (!launched) { - - const queryStringBase = getQueryStringFromArgv(argv, skyPagesConfig); - let localUrl = util.format( - 'https://localhost:%s%s', - this.options.devServer.port, - this.options.devServer.publicPath - ); - - let hostUrl = hostUtils.resolve( - queryStringBase, - localUrl, - stats.toJson().chunks, - skyPagesConfig - ); - logger.info('SKY UX builder is ready.'); launched = true; - // Process shorthand flags - Object.keys(shorthand).forEach(key => { - if (argv[key]) { - argv[shorthand[key]] = argv[key]; - } - }); - - // Edge uses a different technique (protocol vs executable) - if (argv.browser === 'edge') { - const edge = 'microsoft-edge:'; - argv.browser = undefined; - hostUrl = edge + hostUrl; - localUrl = edge + localUrl; - } - - switch (argv.launch) { - case 'none': - break; - case 'local': - - // Only adding queryStringBase to the message + local url opened, - // Meaning doesn't need those to communicate back to localhost - localUrl += queryStringBase; - - logger.info(`Launching Local URL: ${localUrl}`); - open(localUrl, argv.browser); - break; - default: - logger.info(`Launching Host URL: ${hostUrl}`); - open(hostUrl, argv.browser); - break; - } + // Host is default launch + argv.launch = argv.launch || 'host'; + browser(argv, skyPagesConfig, stats, this.options.devServer.port); } - }); } diff --git a/index.js b/index.js index 990f90f2..b822b521 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,18 @@ const config = require('./config/sky-pages/sky-pages.config'); module.exports = { runCommand: (command, argv) => { const skyPagesConfig = config.getSkyPagesConfig(command); + const shorthand = { + l: 'launch', + b: 'browser' + }; + + // Process shorthand flags + Object.keys(shorthand).forEach(key => { + if (argv[key]) { + argv[shorthand[key]] = argv[key]; + } + }); + switch (command) { case 'build': require('./cli/build')(argv, skyPagesConfig, webpack); diff --git a/lib/sky-pages-module-generator.js b/lib/sky-pages-module-generator.js index f7ccba27..9a2acc11 100644 --- a/lib/sky-pages-module-generator.js +++ b/lib/sky-pages-module-generator.js @@ -92,13 +92,13 @@ function getSource(skyAppConfig) { enableProdMode = `import { enableProdMode } from '@angular/core'; enableProdMode();`; - break; + break; case 'e2e': useMockAuth = `import { BBAuth } from '@blackbaud/auth-client'; BBAuth.mock = true; `; - break; + break; } let useHashRouting = skyAppConfig.skyux.useHashRouting === true; diff --git a/loader/sky-pages-module/index.js b/loader/sky-pages-module/index.js index a24d310c..2d4f3132 100644 --- a/loader/sky-pages-module/index.js +++ b/loader/sky-pages-module/index.js @@ -24,14 +24,14 @@ module.exports = function () { switch (filenameParsed.ext) { case '.html': writeTimeStamp(); - break; + break; case '.ts': if (filenameParsed.name !== 'sky-pages.module') { writeTimeStamp(); } - break; + break; } }); diff --git a/test/cli-build.spec.js b/test/cli-build.spec.js index 2ebbadc4..a905ef3b 100644 --- a/test/cli-build.spec.js +++ b/test/cli-build.spec.js @@ -15,8 +15,7 @@ describe('cli build', () => { }); afterEach(() => { - mock.stop('../config/webpack/build.webpack.config'); - mock.stop('../lib/source-files-walker'); + mock.stopAll(); }); it('should call getWebpackConfig', () => { @@ -287,4 +286,40 @@ describe('cli build', () => { ); }); + + function testBuildAndServe(launch, done) { + const argv = { + launch: launch + }; + + mock('../utils/server', { + start: () => Promise.resolve() + }); + mock('../utils/browser', (argv) => { + expect(argv.launch).toBe(launch); + done(); + }); + + mock.reRequire('../cli/build')(argv, runtimeUtils.getDefault(), () => ({ + run: (cb) => { + cb( + null, + { + toJson: () => ({ + errors: [], + warnings: [] + }) + } + ); + } + })); + } + + it('should serve and browse to the built files if launch flag is host', (done) => { + testBuildAndServe('host', done); + }); + + it('should serve and browse to the built files if launch flag is local', (done) => { + testBuildAndServe('local', done); + }); }); diff --git a/test/cli-e2e.spec.js b/test/cli-e2e.spec.js index 00c6c4b7..3e790a74 100644 --- a/test/cli-e2e.spec.js +++ b/test/cli-e2e.spec.js @@ -44,30 +44,24 @@ describe('cli e2e', () => { beforeEach(() => { EXIT_CODE = 0; - mock('../cli/build', () => { - return new Promise(resolve => { - resolve({ - toJson: () => ({ - chunks: CHUNKS - }) - }); + mock('../cli/build', () => new Promise(resolve => { + resolve({ + toJson: () => ({ + chunks: CHUNKS + }) }); - }); + })); mock('cross-spawn', { - spawn: () => { - return { - on: (evt, cb) => { - if (evt === 'exit') { - PROTRACTOR_CB = cb; - cb(EXIT_CODE); - } + spawn: () => ({ + on: (evt, cb) => { + if (evt === 'exit') { + PROTRACTOR_CB = cb; + cb(EXIT_CODE); } - }; - }, - sync: () => { - return { }; - } + } + }), + sync: () => ({ }) }); mock('portfinder', { @@ -76,7 +70,10 @@ describe('cli e2e', () => { mock('http-server', { createServer: () => ({ - close: () => {}, + close: () => { + console.log('INNER 2'); + }, + listen: (port, host, cb) => { cb(); } @@ -99,7 +96,7 @@ describe('cli e2e', () => { }); EXIT_CODE = 1; - require('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); + mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); }); it('should catch protractor kitchen sink error', (done) => { @@ -111,10 +108,10 @@ describe('cli e2e', () => { }); EXIT_CODE = 199; - require('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); + mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); }); - it('should install and start selenium only if a seleniumAddress is specified', (done) => { + it('should install, start, and kill selenium only if a seleniumAddress is specified', (done) => { let killCalled = false; mock(configPath, { @@ -140,25 +137,7 @@ describe('cli e2e', () => { done(); }); - require('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); - }); - - it('should only kill servers that exist', (done) => { - let called = false; - spyOn(logger, 'warn'); - spyOn(process, 'exit').and.callFake(() => { - // Stop the infinite loop - if (!called) { - called = true; - expect(infoCalledWith('Closing http server')).toEqual(true); - logger.info.calls.reset(); - PROTRACTOR_CB(); - expect(infoCalledWith('Closing http server')).toEqual(false); - done(); - } - }); - - require('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); + mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); }); it('should catch build failures', (done) => { @@ -172,41 +151,6 @@ describe('cli e2e', () => { mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); }); - it('should catch http-server failures', (done) => { - mock('http-server', { - createServer: (opts) => ({ - close: () => {}, - listen: () => { - // Log a message. - opts.logFn.call({}, null, null, null); - - // Log an error. - opts.logFn.call({}, null, null, new Error('Server failed.')); - } - }) - }); - - spyOn(process, 'exit').and.callFake(exitCode => { - expect(exitCode).toEqual(1); - done(); - }); - - mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); - }); - - it('should catch portfinder failures', (done) => { - mock('portfinder', { - getPortPromise: () => Promise.reject(new Error('Portfinder failed.')) - }); - - spyOn(process, 'exit').and.callFake(exitCode => { - expect(exitCode).toEqual(1); - done(); - }); - - mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); - }); - it('should catch selenium failures', (done) => { mock(configPath, { config: { @@ -228,7 +172,7 @@ describe('cli e2e', () => { done(); }); - require('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); + mock.reRequire('../cli/e2e')(ARGV, SKY_PAGES_CONFIG, WEBPACK); }); it('should catch protractor\'s selenium failures', (done) => { diff --git a/test/config-webpack-serve.spec.js b/test/config-webpack-serve.spec.js index 00498e40..cbd7b177 100644 --- a/test/config-webpack-serve.spec.js +++ b/test/config-webpack-serve.spec.js @@ -1,148 +1,46 @@ /*jshint jasmine: true, node: true */ 'use strict'; -const mock = require('mock-require'); const logger = require('winston'); -const urlLibrary = require('url'); +const mock = require('mock-require'); const runtimeUtils = require('../utils/runtime-test-utils'); describe('config webpack serve', () => { - const skyuxConfig = { - runtime: runtimeUtils.getDefaultRuntime(), - skyux: runtimeUtils.getDefaultSkyux({ - app: { - externals: { - test: true - } - }, - host: { - url: 'https://my-host-server.url' - } - }) - }; - - let lib; - let called; - let openParamUrl; - let openParamBrowser; - let config; - let argv = {}; - - function getPluginOptions() { - return { - appConfig: { - base: 'my-custom-base' - }, - devServer: { - port: 1234 - } - }; - } - - beforeEach(() => { - called = false; - openParamUrl = ''; - openParamBrowser = undefined; - - argv = {}; + let paramArgv; - spyOn(logger, 'info'); - mock('open', (url, browser) => { - called = true; - openParamUrl = url; - openParamBrowser = browser; + beforeAll(() => { + mock('../utils/browser', (argv) => { + paramArgv = argv; }); - - lib = require('../config/webpack/serve.webpack.config'); - config = lib.getWebpackConfig(argv, skyuxConfig); }); afterEach(() => { - mock.stop('open'); - lib = null; - config = null; + paramArgv = undefined; + mock.stop('../utils/browser'); }); - function bindToDone() { - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); - } - } - }); - } - }); - } - it('should expose a getWebpackConfig method', () => { + const lib = require('../config/webpack/serve.webpack.config'); expect(typeof lib.getWebpackConfig).toEqual('function'); }); - it('should only log the ready message once during multiple dones', () => { - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); - cb({ - toJson: () => ({ - chunks: [] - }) - }); - } - } - }); - } - }); - - // Once for ready and once for default launching host - expect(logger.info).toHaveBeenCalledTimes(2); - }); + it('should set default `launch` to `host', () => { + spyOn(logger, 'info'); + const lib = require('../config/webpack/serve.webpack.config'); + const config = lib.getWebpackConfig({}, runtimeUtils.getDefault()); - it('should log the host url and launch it when launch flag is not present', () => { config.plugins.forEach(plugin => { if (plugin.name === 'WebpackPluginDone') { plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); + options: { + appConfig: { + base: 'my-custom-base' + }, + devServer: { + port: 1234 } - } - }); - } - }); - - const url = 'https://my-host-server.url/@blackbaud/skyux-builder/?local=true&_cfg='; - expect(logger.info).toHaveBeenCalledTimes(2); - expect(openParamUrl.indexOf(url)).toBe(0); - }); - - it('should log the host url and launch it when --launch host', () => { - argv.launch = 'host'; - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), + }, plugin: (evt, cb) => { if (evt === 'done') { cb({ @@ -156,207 +54,47 @@ describe('config webpack serve', () => { } }); - const url = 'https://my-host-server.url'; - expect(logger.info).toHaveBeenCalledTimes(2); - expect(openParamUrl.indexOf(url)).toBe(0); + expect(paramArgv.launch).toEqual('host'); }); - it('should log the local url and launch it when --launch local', () => { - argv.launch = 'local'; - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); - } - } - }); - } - }); + it('should only log the ready message once during multiple dones', () => { - const url = 'https://localhost:1234'; - expect(logger.info).toHaveBeenCalledTimes(2); - expect(openParamUrl.indexOf(url)).toBe(0); - }); + spyOn(logger, 'info'); + const lib = require('../config/webpack/serve.webpack.config'); + const config = lib.getWebpackConfig({}, runtimeUtils.getDefault()); - it('should log a done message and not launch it when --launch none', () => { - argv.launch = 'none'; config.plugins.forEach(plugin => { if (plugin.name === 'WebpackPluginDone') { plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); + options: { + appConfig: { + base: 'my-custom-base' + }, + devServer: { + port: 1234 } - } - }); - } - }); - - expect(logger.info).toHaveBeenCalledTimes(1); - expect(called).toEqual(false); - }); - - it('should pass shorthand -l as --launch flag', () => { - argv.l = 'none'; - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), + }, plugin: (evt, cb) => { if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); - } - } - }); - } - }); - - expect(logger.info).toHaveBeenCalledTimes(1); - expect(called).toEqual(false); - }); - - - it('host querystring should not contain externals if they do not exist', () => { - const localConfig = lib.getWebpackConfig(argv, { - runtime: runtimeUtils.getDefaultRuntime(), - skyux: runtimeUtils.getDefaultSkyux() - }); - - localConfig.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'emit') { - const done = () => {}; - const compilation = {}; - - cb(compilation, done); - } - - if (evt === 'done') { + // Simulating a save by calling callback twice cb({ toJson: () => ({ chunks: [] }) }); - const urlParsed = urlLibrary.parse(openParamUrl, true); - const configString = new Buffer.from(urlParsed.query._cfg, 'base64').toString(); - const configObject = JSON.parse(configString); - - expect(urlParsed.query._cfg).toBeDefined(); - expect(configObject.externals).not.toBeDefined(); - } - } - }); - } - }); - }); - - it('host querystring should contain externals (if they exist), scripts, and localUrl', () => { - - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: getPluginOptions(), - plugin: (evt, cb) => { - if (evt === 'emit') { - const done = () => {}; - const compilation = {}; - - cb(compilation, done); - } - - if (evt === 'done') { cb({ toJson: () => ({ - chunks: [ - { files: ['a.js'] }, - { files: ['b.js'] } - ] + chunks: [] }) }); - const urlParsed = urlLibrary.parse(openParamUrl, true); - const configString = new Buffer.from(urlParsed.query._cfg, 'base64').toString(); - const configObject = JSON.parse(configString); - const url = 'https://localhost:1234'; - - expect(urlParsed.query._cfg).toBeDefined(); - expect(configObject.externals).toEqual(skyuxConfig.skyux.app.externals); - expect(configObject.localUrl.indexOf(url)).toBe(0); - expect(configObject.scripts).toEqual([ - { name: 'a.js' }, - { name: 'b.js' } - ]); } } }); } }); - }); - - it('should pass through envid from the command line', () => { - argv.envid = 'asdf'; - - bindToDone(); - expect(openParamUrl).toContain(`?envid=asdf`); - }); - - it('should pass through svcid from the command line', () => { - argv.svcid = 'asdf'; - - bindToDone(); - expect(openParamUrl).toContain(`?svcid=asdf`); - }); - - it('should run envid and svcid through encodeURIComponent', () => { - argv.envid = '&=$'; - argv.svcid = '^%'; - - bindToDone(); - expect(openParamUrl).toContain( - `?envid=${encodeURIComponent(argv.envid)}&svcid=${encodeURIComponent(argv.svcid)}` - ); - }); - - it('should pass through envid and svcid, but not other flags from the command line', () => { - argv.envid = 'asdf1'; - argv.svcid = 'asdf2'; - argv.myid = 'asdf3'; - - bindToDone(); - expect(openParamUrl).toContain(`?envid=asdf1&svcid=asdf2`); - expect(openParamUrl).not.toContain(`myid=asdf3`); - }); - - it('should pass --browser flag to open', () => { - argv.browser = 'custom-browser'; - bindToDone(); - expect(openParamBrowser).toEqual(argv.browser); - }); - it('should handle --browser edge different syntax', () => { - argv.browser = 'edge'; - bindToDone(); - expect(openParamBrowser).not.toBeDefined(); - expect(openParamUrl.indexOf('microsoft-edge')).toBe(0); + expect(logger.info).toHaveBeenCalledWith(`SKY UX builder is ready.`); }); }); diff --git a/test/sky-pages-out-skyux2.spec.js b/test/index.spec.js similarity index 76% rename from test/sky-pages-out-skyux2.spec.js rename to test/index.spec.js index f90e0a55..0a670861 100644 --- a/test/sky-pages-out-skyux2.spec.js +++ b/test/index.spec.js @@ -44,7 +44,7 @@ describe('@blackbaud/skyux-builder', () => { mock('../cli/' + cmds[key].lib, () => { cmds[key].called = true; }); - lib.runCommand(cmds[key].cmd); + lib.runCommand(cmds[key].cmd, {}); expect(cmds[key].called).toEqual(true); }); }); @@ -53,11 +53,25 @@ describe('@blackbaud/skyux-builder', () => { spyOn(logger, 'info'); const cmd = 'junk-command-that-does-not-exist'; const lib = require('../index'); - lib.runCommand(cmd); + lib.runCommand(cmd, {}); expect(logger.info).toHaveBeenCalledWith( '@blackbaud/skyux-builder: Unknown command %s', cmd ); }); + it('should process shorthand tags', (done) => { + const argv = { + l: 'showForLaunch', + b: 'showForBrowser' + }; + mock('../cli/test', (c, a) => { + expect(a.launch).toEqual(argv.l); + expect(a.browser).toEqual(argv.b); + done(); + }); + const lib = require('../index'); + lib.runCommand('test', argv); + }); + }); diff --git a/test/utils-browser.spec.js b/test/utils-browser.spec.js new file mode 100644 index 00000000..5e45062e --- /dev/null +++ b/test/utils-browser.spec.js @@ -0,0 +1,176 @@ +/*jshint jasmine: true, node: true */ +'use strict'; + +const logger = require('winston'); +const mock = require('mock-require'); +const merge = require('merge'); +const url = require('url'); + +const hostUtils = require('../utils/host-utils'); +const runtimeUtils = require('../utils/runtime-test-utils'); + +describe('browser utils', () => { + + let openCalled; + let openParamUrl; + let openParamBrowser; + + beforeEach(() => { + openCalled = false; + openParamUrl = ''; + openParamBrowser = undefined; + + mock('open', (url, browser) => { + openCalled = true; + openParamUrl = url; + openParamBrowser = browser; + }); + + spyOn(logger, 'info'); + }); + + afterEach(() => { + mock.stopAll(); + }); + + function bind(settings) { + const merged = merge.recursive({ + argv: {}, + skyPagesConfig: runtimeUtils.getDefault(), + stats: { + toJson: () => ({ + chunks: [] + }) + }, + port: '' + }, settings); + + mock.reRequire('../utils/browser')( + merged.argv, + merged.skyPagesConfig, + merged.stats, + merged.port + ); + + return merged; + } + + it('should run envid and svcid through encodeURIComponent', () => { + const s = bind({ + argv: { + launch: 'host', + envid: '&=$', + svcid: '^%' + } + }); + + expect(openParamUrl).toContain( + `?envid=${encodeURIComponent(s.argv.envid)}&svcid=${encodeURIComponent(s.argv.svcid)}` + ); + }); + + it('should pass through envid and svcid, but not other flags from the command line', () => { + const settings = bind({ + argv: { + launch: 'host', + envid: 'my-envid', + svcid: 'my-svcid', + noid: 'nope' + } + }); + + const parsed = url.parse(openParamUrl, true); + expect(parsed.query.envid).toBe(settings.argv.envid); + expect(parsed.query.svcid).toBe(settings.argv.svcid); + expect(parsed.query.noid).not.toBeDefined(); + }); + + it('should log the host url and launch it when --launch host', () => { + const port = 1234; + const appBase = 'app-base'; + + const settings = bind({ + argv: { + launch: 'host' + }, + port: port, + skyPagesConfig: { + runtime: runtimeUtils.getDefaultRuntime, + skyux: runtimeUtils.getDefaultSkyux({ + name: appBase + }) + } + }); + + const localUrl = `https://localhost:${port}/${appBase}/`; + const hostUrl = hostUtils.resolve( + '', + localUrl, + [], + settings.skyPagesConfig + ); + + expect(logger.info).toHaveBeenCalledWith(`Launching Host URL: ${hostUrl}`); + expect(openCalled).toBe(true); + expect(openParamUrl).toBe(hostUrl); + }); + + it('should log the local url and launch it when --launch local', () => { + + const port = 1234; + const appBase = 'app-base'; + const url = `https://localhost:${port}/${appBase}/`; + + bind({ + argv: { + launch: 'local' + }, + port: port, + skyPagesConfig: { + runtime: runtimeUtils.getDefaultRuntime, + skyux: runtimeUtils.getDefaultSkyux({ + name: appBase + }) + } + }); + + expect(logger.info).toHaveBeenCalledWith(`Launching Local URL: ${url}`); + expect(openCalled).toBe(true); + expect(openParamUrl).toBe(url); + }); + + it('should log a done message and not launch it when --launch none', () => { + bind({ + argv: { + launch: 'none' + } + }); + expect(logger.info).not.toHaveBeenCalled(); + expect(openCalled).toBe(false); + }); + + it('should pass --browser flag to open', () => { + const settings = { + argv: { + browser: 'custom-browser', + launch: 'host' + } + }; + + bind(settings); + expect(openCalled).toBe(true); + expect(openParamBrowser).toEqual(settings.argv.browser); + }); + + it('should handle --browser edge different syntax', () => { + bind({ + argv: { + browser: 'edge', + launch: 'host' + } + }); + expect(openParamBrowser).not.toBeDefined(); + expect(openParamUrl.indexOf('microsoft-edge')).toBe(0); + }); + +}); diff --git a/test/host-utils.spec.js b/test/utils-host-utils.spec.js similarity index 98% rename from test/host-utils.spec.js rename to test/utils-host-utils.spec.js index 296128de..99f6167f 100644 --- a/test/host-utils.spec.js +++ b/test/utils-host-utils.spec.js @@ -73,7 +73,7 @@ describe('host-utils', () => { }; } - return readFileSync(filename, encoding); + return readJsonSync(filename, encoding); }); const externals = { diff --git a/test/utils-server.spec.js b/test/utils-server.spec.js new file mode 100644 index 00000000..a02e7f3a --- /dev/null +++ b/test/utils-server.spec.js @@ -0,0 +1,118 @@ +/*jshint jasmine: true, node: true */ +'use strict'; + +const logger = require('winston'); +const mock = require('mock-require'); + +describe('server utils', () => { + + let closeCalled = false; + let customServerError; + let customPortError; + let customPortNumber; + + beforeEach(() => { + spyOn(logger, 'info'); + }); + + afterEach(() => { + closeCalled = false; + customServerError = undefined; + customPortError = undefined; + customPortNumber = undefined; + mock.stopAll(); + }); + + function bind() { + mock('http-server', { + createServer: (settings) => { + + if (customServerError) { + settings.logFn({}, {}, customServerError); + } + + if (customPortNumber) { + settings.logFn({}, {}); + } + + return { + close: () => closeCalled = true, + listen: (port, host, cb) => { + cb(); + } + }; + } + }); + + mock('portfinder', { + getPortPromise: () => { + if (customPortError) { + return Promise.reject(customPortError); + } else { + return Promise.resolve(customPortNumber); + } + } + }); + + return mock.reRequire('../utils/server'); + } + + it('should expose start and stop methods', () => { + const server = bind(); + expect(server.start).toBeDefined(); + expect(server.stop).toBeDefined(); + }); + + it('should close the http-server if it exists', (done) => { + const server = bind(); + + server.stop(); + expect(closeCalled).toBe(false); + + server.start().then(() => { + logger.info.calls.reset(); + server.stop(); + expect(closeCalled).toBe(true); + expect(logger.info).toHaveBeenCalledWith(`Stopping http server`); + done(); + }); + }); + + it('should catch http-server failures', (done) => { + + customServerError = 'custom-error'; + const server = bind(); + + server.start().catch(err => { + expect(err).toBe(customServerError); + done(); + }); + }); + + it('should resolve the portfinder port', (done) => { + customPortNumber = 1234; + const server = bind(); + + server.start().then(port => { + expect(port).toBe(customPortNumber); + done(); + }); + }); + + it('should catch portfinder failures', (done) => { + + customPortError = 'custom-portfinder-error'; + mock('portfinder', { + getPortPromise: () => { + Promise.reject(customPortError); + } + }); + + const server = bind(); + server.start().catch(err => { + mock.stop('portfinder'); + expect(err).toBe(customPortError); + done(); + }); + }); +}); diff --git a/utils/browser.js b/utils/browser.js new file mode 100644 index 00000000..320e02bf --- /dev/null +++ b/utils/browser.js @@ -0,0 +1,75 @@ +/*jslint node: true */ +'use strict'; + +const util = require('util'); +const open = require('open'); +const logger = require('winston'); +const hostUtils = require('./host-utils'); +const skyPagesConfigUtil = require('../config/sky-pages/sky-pages.config'); + +/** + * Returns the querystring base for parameters allowed to be passed through. + * PLEASE NOTE: The method is nearly duplicated in `runtime/params.ts`. + * @name getQueryStringFromArgv + * @param {Object} argv + * @param {SkyPagesConfig} skyPagesConfig + * @returns {string} + */ +function getQueryStringFromArgv(argv, skyPagesConfig) { + + let found = []; + skyPagesConfig.skyux.params.forEach(param => { + if (argv[param]) { + found.push(`${param}=${encodeURIComponent(argv[param])}`); + } + }); + + if (found.length) { + return `?${found.join('&')}`; + } + + return ''; +} + +function browser(argv, skyPagesConfig, stats, port) { + + const queryStringBase = getQueryStringFromArgv(argv, skyPagesConfig); + let localUrl = util.format( + 'https://localhost:%s%s', + port, + skyPagesConfigUtil.getAppBase(skyPagesConfig) + ); + + let hostUrl = hostUtils.resolve( + queryStringBase, + localUrl, + stats.toJson().chunks, + skyPagesConfig + ); + + // Edge uses a different technique (protocol vs executable) + if (argv.browser === 'edge') { + const edge = 'microsoft-edge:'; + argv.browser = undefined; + hostUrl = edge + hostUrl; + localUrl = edge + localUrl; + } + + switch (argv.launch) { + case 'local': + + // Only adding queryStringBase to the message + local url opened, + // Meaning doesn't need those to communicate back to localhost + localUrl += queryStringBase; + + logger.info(`Launching Local URL: ${localUrl}`); + open(localUrl, argv.browser); + break; + case 'host': + logger.info(`Launching Host URL: ${hostUrl}`); + open(hostUrl, argv.browser); + break; + } +} + +module.exports = browser; diff --git a/utils/runtime-test-utils.js b/utils/runtime-test-utils.js index 9c742bf2..bb4da309 100644 --- a/utils/runtime-test-utils.js +++ b/utils/runtime-test-utils.js @@ -4,6 +4,13 @@ const merge = require('merge'); module.exports = { + getDefault: function (runtime, skyux) { + return { + runtime: this.getDefaultRuntime(runtime), + skyux: this.getDefaultSkyux(skyux) + }; + }, + getDefaultRuntime: function (runtime) { return merge.recursive({ app: { diff --git a/utils/server.js b/utils/server.js new file mode 100644 index 00000000..51f05cc9 --- /dev/null +++ b/utils/server.js @@ -0,0 +1,64 @@ +/*jslint node: true */ +'use strict'; + +const path = require('path'); +const logger = require('winston'); +const portfinder = require('portfinder'); +const HttpServer = require('http-server'); + +let httpServer; + +/** + * Starts the httpServer + * @name start + */ +function start() { + return new Promise((resolve, reject) => { + logger.info('Requesting open port...'); + + httpServer = HttpServer.createServer({ + // root: 'dist/', + cors: true, + cache: -1, + https: { + cert: path.resolve(__dirname, '../ssl/server.crt'), + key: path.resolve(__dirname, '../ssl/server.key') + }, + logFn: (req, res, err) => { + if (err) { + reject(err); + return; + } + } + }); + + portfinder + .getPortPromise() + .then(port => { + logger.info(`Open port found: ${port}`); + logger.info('Starting web server...'); + httpServer.listen(port, 'localhost', () => { + logger.info('Web server running.'); + resolve(port); + }); + }) + .catch(reject); + }); +} + +/** + * Kills the server if it exists + * @name kill + */ +function stop() { + if (httpServer) { + logger.info('Stopping http server'); + httpServer.close(); + httpServer = null; + } +} + +module.exports = { + start: start, + stop: stop +}; From afae3566d0feca2e40c2ecaf1ffcffbe1dcd06b0 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Thu, 13 Jul 2017 12:36:22 -0400 Subject: [PATCH 02/17] Cleaned up bad merge --- cli/build.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/build.js b/cli/build.js index 7f8b2a81..9965925b 100644 --- a/cli/build.js +++ b/cli/build.js @@ -8,12 +8,10 @@ const skyPagesConfigUtil = require('../config/sky-pages/sky-pages.config'); const generator = require('../lib/sky-pages-module-generator'); const assetsConfig = require('../lib/assets-configuration'); const pluginFileProcessor = require('../lib/plugin-file-processor'); -<<<<<<< HEAD + const server = require('../utils/server'); const browser = require('../utils/browser'); -======= const runCompiler = require('./utils/run-compiler'); ->>>>>>> master function writeTSConfig() { var config = { From c60e9aef3011294107569625ff4bd9a074d33d43 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Thu, 13 Jul 2017 12:49:49 -0400 Subject: [PATCH 03/17] Fixed linting errors --- cli/utils/stage-library-ts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/utils/stage-library-ts.js b/cli/utils/stage-library-ts.js index 8aba720b..10672412 100644 --- a/cli/utils/stage-library-ts.js +++ b/cli/utils/stage-library-ts.js @@ -66,10 +66,10 @@ function getFileContents(filePath) { switch (path.extname(filePath)) { case '.scss': contents = compileSass(filePath); - break; + break; case '.html': contents = getHtmlContents(filePath); - break; + break; } contents = contents From 047b223c4e178e44daa7bfbf24a99f666e38cf97 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 14 Jul 2017 11:47:27 -0400 Subject: [PATCH 04/17] Returning from build. Removing Protractor warning --- cli/build.js | 3 +-- cli/e2e.js | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/build.js b/cli/build.js index afc16a69..6a00296a 100644 --- a/cli/build.js +++ b/cli/build.js @@ -156,8 +156,7 @@ function build(argv, skyPagesConfig, webpack) { }); break; default: - Promise.resolve(stats); - break; + return Promise.resolve(stats); } }); diff --git a/cli/e2e.js b/cli/e2e.js index 8f48007f..85a78b2f 100644 --- a/cli/e2e.js +++ b/cli/e2e.js @@ -84,6 +84,7 @@ function spawnProtractor(chunks, port, skyPagesConfig) { protractorPath, [ getProtractorConfigPath(), + `--disableChecks`, `--baseUrl ${skyPagesConfig.skyux.host.url}`, `--params.localUrl=https://localhost:${port}`, `--params.chunks=${JSON.stringify(chunks)}`, From d1a54ea587ab4b154eedcc9523035d68ab84bc7a Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 14 Jul 2017 13:24:13 -0400 Subject: [PATCH 05/17] Moved files. Fixed bug causing test failure --- cli/build.js | 4 ++-- cli/e2e.js | 2 +- {utils => cli/utils}/browser.js | 4 ++-- {utils => cli/utils}/server.js | 6 +++--- config/webpack/serve.webpack.config.js | 2 +- test/cli-build.spec.js | 4 ++-- test/cli-e2e.spec.js | 17 +++-------------- ...rowser.spec.js => cli-utils-browser.spec.js} | 2 +- ...-server.spec.js => cli-utils-server.spec.js} | 2 +- test/config-webpack-serve.spec.js | 4 ++-- 10 files changed, 18 insertions(+), 29 deletions(-) rename {utils => cli/utils}/browser.js (93%) rename {utils => cli/utils}/server.js (88%) rename test/{utils-browser.spec.js => cli-utils-browser.spec.js} (98%) rename test/{utils-server.spec.js => cli-utils-server.spec.js} (98%) diff --git a/cli/build.js b/cli/build.js index 6a00296a..3e62a434 100644 --- a/cli/build.js +++ b/cli/build.js @@ -9,8 +9,8 @@ const generator = require('../lib/sky-pages-module-generator'); const assetsProcessor = require('../lib/assets-processor'); const pluginFileProcessor = require('../lib/plugin-file-processor'); -const server = require('../utils/server'); -const browser = require('../utils/browser'); +const server = require('./utils/server'); +const browser = require('./utils/browser'); const runCompiler = require('./utils/run-compiler'); function writeTSConfig() { diff --git a/cli/e2e.js b/cli/e2e.js index 85a78b2f..ebf9513e 100644 --- a/cli/e2e.js +++ b/cli/e2e.js @@ -7,7 +7,7 @@ const logger = require('winston'); const selenium = require('selenium-standalone'); const build = require('./build'); -const server = require('../utils/server'); +const server = require('./utils/server'); // Disable this to quiet the output const spawnOptions = { stdio: 'inherit' }; diff --git a/utils/browser.js b/cli/utils/browser.js similarity index 93% rename from utils/browser.js rename to cli/utils/browser.js index 320e02bf..f0d7c013 100644 --- a/utils/browser.js +++ b/cli/utils/browser.js @@ -4,8 +4,8 @@ const util = require('util'); const open = require('open'); const logger = require('winston'); -const hostUtils = require('./host-utils'); -const skyPagesConfigUtil = require('../config/sky-pages/sky-pages.config'); +const hostUtils = require('../../utils/host-utils'); +const skyPagesConfigUtil = require('../../config/sky-pages/sky-pages.config'); /** * Returns the querystring base for parameters allowed to be passed through. diff --git a/utils/server.js b/cli/utils/server.js similarity index 88% rename from utils/server.js rename to cli/utils/server.js index 51f05cc9..4e429643 100644 --- a/utils/server.js +++ b/cli/utils/server.js @@ -17,12 +17,12 @@ function start() { logger.info('Requesting open port...'); httpServer = HttpServer.createServer({ - // root: 'dist/', + root: 'dist/', cors: true, cache: -1, https: { - cert: path.resolve(__dirname, '../ssl/server.crt'), - key: path.resolve(__dirname, '../ssl/server.key') + cert: path.resolve(__dirname, '../../ssl/server.crt'), + key: path.resolve(__dirname, '../../ssl/server.key') }, logFn: (req, res, err) => { if (err) { diff --git a/config/webpack/serve.webpack.config.js b/config/webpack/serve.webpack.config.js index ef072d2c..4fd17c3c 100644 --- a/config/webpack/serve.webpack.config.js +++ b/config/webpack/serve.webpack.config.js @@ -9,7 +9,7 @@ const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin'); const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); const skyPagesConfigUtil = require('../sky-pages/sky-pages.config'); -const browser = require('../../utils/browser'); +const browser = require('../../cli/utils/browser'); /** * Returns the default webpackConfig. diff --git a/test/cli-build.spec.js b/test/cli-build.spec.js index 2b9f72fc..26212d2e 100644 --- a/test/cli-build.spec.js +++ b/test/cli-build.spec.js @@ -292,10 +292,10 @@ describe('cli build', () => { launch: launch }; - mock('../utils/server', { + mock('../cli/utils/server', { start: () => Promise.resolve() }); - mock('../utils/browser', (argv) => { + mock('../cli/utils/browser', (argv) => { expect(argv.launch).toBe(launch); done(); }); diff --git a/test/cli-e2e.spec.js b/test/cli-e2e.spec.js index 3e790a74..9fa3a4ab 100644 --- a/test/cli-e2e.spec.js +++ b/test/cli-e2e.spec.js @@ -64,20 +64,9 @@ describe('cli e2e', () => { sync: () => ({ }) }); - mock('portfinder', { - getPortPromise: () => new Promise(resolve => resolve(PORT)) - }); - - mock('http-server', { - createServer: () => ({ - close: () => { - console.log('INNER 2'); - }, - - listen: (port, host, cb) => { - cb(); - } - }) + mock('../cli/utils/server', { + start: () => PORT, + stop: () => {} }); spyOn(logger, 'info'); diff --git a/test/utils-browser.spec.js b/test/cli-utils-browser.spec.js similarity index 98% rename from test/utils-browser.spec.js rename to test/cli-utils-browser.spec.js index 5e45062e..caccb474 100644 --- a/test/utils-browser.spec.js +++ b/test/cli-utils-browser.spec.js @@ -45,7 +45,7 @@ describe('browser utils', () => { port: '' }, settings); - mock.reRequire('../utils/browser')( + mock.reRequire('../cli/utils/browser')( merged.argv, merged.skyPagesConfig, merged.stats, diff --git a/test/utils-server.spec.js b/test/cli-utils-server.spec.js similarity index 98% rename from test/utils-server.spec.js rename to test/cli-utils-server.spec.js index a02e7f3a..a75fb633 100644 --- a/test/utils-server.spec.js +++ b/test/cli-utils-server.spec.js @@ -54,7 +54,7 @@ describe('server utils', () => { } }); - return mock.reRequire('../utils/server'); + return mock.reRequire('../cli/utils/server'); } it('should expose start and stop methods', () => { diff --git a/test/config-webpack-serve.spec.js b/test/config-webpack-serve.spec.js index cbd7b177..177d0333 100644 --- a/test/config-webpack-serve.spec.js +++ b/test/config-webpack-serve.spec.js @@ -10,14 +10,14 @@ describe('config webpack serve', () => { let paramArgv; beforeAll(() => { - mock('../utils/browser', (argv) => { + mock('../cli/utils/browser', (argv) => { paramArgv = argv; }); }); afterEach(() => { paramArgv = undefined; - mock.stop('../utils/browser'); + mock.stop('../cli/utils/browser'); }); it('should expose a getWebpackConfig method', () => { From 8a0808c164d1eec0beb0d11f4bc2b7074ecb5447 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 14 Jul 2017 16:17:06 -0400 Subject: [PATCH 06/17] Manually setting name if launching after build --- cli/build.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/build.js b/cli/build.js index 3e62a434..b705de89 100644 --- a/cli/build.js +++ b/cli/build.js @@ -137,8 +137,14 @@ function build(argv, skyPagesConfig, webpack) { buildConfig = require('../config/webpack/build.webpack.config'); } - const config = buildConfig.getWebpackConfig(skyPagesConfig); + // If we're going to launch the files, change the app name to dist + // This is easier than moving the dist/ folder around on disk. + if (argv.launch) { + skyPagesConfig.skyux.name = 'dist'; + skyPagesConfig.runtime.app.base = '/dist/'; + } + const config = buildConfig.getWebpackConfig(skyPagesConfig); assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl); return runCompiler(webpack, config) From 8947d70caf5db63cdf856de5a1fd13183d114ccf Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 14 Jul 2017 19:55:31 -0400 Subject: [PATCH 07/17] Using different server to allow for better redirects --- cli/build.js | 8 +++--- cli/utils/server.js | 46 +++++++++++++++++------------------ test/cli-utils-server.spec.js | 39 +++++++++++------------------ 3 files changed, 41 insertions(+), 52 deletions(-) diff --git a/cli/build.js b/cli/build.js index b705de89..e1395c02 100644 --- a/cli/build.js +++ b/cli/build.js @@ -139,10 +139,10 @@ function build(argv, skyPagesConfig, webpack) { // If we're going to launch the files, change the app name to dist // This is easier than moving the dist/ folder around on disk. - if (argv.launch) { - skyPagesConfig.skyux.name = 'dist'; - skyPagesConfig.runtime.app.base = '/dist/'; - } + // if (argv.launch) { + // skyPagesConfig.skyux.name = 'dist'; + // skyPagesConfig.runtime.app.base = '/dist/'; + // } const config = buildConfig.getWebpackConfig(skyPagesConfig); assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl); diff --git a/cli/utils/server.js b/cli/utils/server.js index 4e429643..b3733ef2 100644 --- a/cli/utils/server.js +++ b/cli/utils/server.js @@ -1,43 +1,43 @@ /*jslint node: true */ 'use strict'; +const fs = require('fs'); const path = require('path'); const logger = require('winston'); const portfinder = require('portfinder'); -const HttpServer = require('http-server'); +const express = require('express'); +const https = require('https'); +const cors = require('cors'); +const app = express(); -let httpServer; +let server; /** * Starts the httpServer * @name start */ -function start() { +function start(root) { return new Promise((resolve, reject) => { - logger.info('Requesting open port...'); - httpServer = HttpServer.createServer({ - root: 'dist/', - cors: true, - cache: -1, - https: { - cert: path.resolve(__dirname, '../../ssl/server.crt'), - key: path.resolve(__dirname, '../../ssl/server.key') - }, - logFn: (req, res, err) => { - if (err) { - reject(err); - return; - } - } - }); + const options = { + cert: fs.readFileSync(path.resolve(__dirname, '../../ssl/server.crt')), + key: fs.readFileSync(path.resolve(__dirname, '../../ssl/server.key')) + }; + + logger.info('Creating web server'); + app.use(cors()); + app.use(root || '/dist', express.static('dist')); + server = https.createServer(options, app); + server.on('error', reject); + + logger.info('Requesting open port...'); portfinder .getPortPromise() .then(port => { logger.info(`Open port found: ${port}`); logger.info('Starting web server...'); - httpServer.listen(port, 'localhost', () => { + server.listen(port, 'localhost', () => { logger.info('Web server running.'); resolve(port); }); @@ -51,10 +51,10 @@ function start() { * @name kill */ function stop() { - if (httpServer) { + if (server) { logger.info('Stopping http server'); - httpServer.close(); - httpServer = null; + server.close(); + server = null; } } diff --git a/test/cli-utils-server.spec.js b/test/cli-utils-server.spec.js index a75fb633..0b1c9835 100644 --- a/test/cli-utils-server.spec.js +++ b/test/cli-utils-server.spec.js @@ -7,6 +7,7 @@ const mock = require('mock-require'); describe('server utils', () => { let closeCalled = false; + let onErrorCB; let customServerError; let customPortError; let customPortNumber; @@ -17,6 +18,7 @@ describe('server utils', () => { afterEach(() => { closeCalled = false; + onErrorCB = undefined; customServerError = undefined; customPortError = undefined; customPortNumber = undefined; @@ -24,24 +26,17 @@ describe('server utils', () => { }); function bind() { - mock('http-server', { - createServer: (settings) => { - - if (customServerError) { - settings.logFn({}, {}, customServerError); - } - - if (customPortNumber) { - settings.logFn({}, {}); - } - - return { - close: () => closeCalled = true, - listen: (port, host, cb) => { - cb(); + mock('https', { + createServer: () => ({ + on: (err, cb) => onErrorCB = cb, + close: () => closeCalled = true, + listen: (port, host, cb) => { + if (customServerError) { + onErrorCB(customServerError); } - }; - } + cb(port); + } + }) }); mock('portfinder', { @@ -63,7 +58,7 @@ describe('server utils', () => { expect(server.stop).toBeDefined(); }); - it('should close the http-server if it exists', (done) => { + it('should close the http server if it exists', (done) => { const server = bind(); server.stop(); @@ -78,7 +73,7 @@ describe('server utils', () => { }); }); - it('should catch http-server failures', (done) => { + it('should catch http server failures', (done) => { customServerError = 'custom-error'; const server = bind(); @@ -102,15 +97,9 @@ describe('server utils', () => { it('should catch portfinder failures', (done) => { customPortError = 'custom-portfinder-error'; - mock('portfinder', { - getPortPromise: () => { - Promise.reject(customPortError); - } - }); const server = bind(); server.start().catch(err => { - mock.stop('portfinder'); expect(err).toBe(customPortError); done(); }); From 8c486634aee71ca93523224ebad71b64572487c9 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Wed, 16 Aug 2017 16:55:14 -0400 Subject: [PATCH 08/17] Cleanup --- cli/build.js | 10 +--- cli/utils/server.js | 13 ++++-- config/protractor/protractor-dev.conf.js | 59 ++++++++++++++---------- package.json | 4 +- test/cli-utils-server.spec.js | 8 ++++ 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/cli/build.js b/cli/build.js index e1395c02..60de979b 100644 --- a/cli/build.js +++ b/cli/build.js @@ -137,13 +137,6 @@ function build(argv, skyPagesConfig, webpack) { buildConfig = require('../config/webpack/build.webpack.config'); } - // If we're going to launch the files, change the app name to dist - // This is easier than moving the dist/ folder around on disk. - // if (argv.launch) { - // skyPagesConfig.skyux.name = 'dist'; - // skyPagesConfig.runtime.app.base = '/dist/'; - // } - const config = buildConfig.getWebpackConfig(skyPagesConfig); assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl); @@ -157,7 +150,8 @@ function build(argv, skyPagesConfig, webpack) { switch (argv.launch) { case 'host': case 'local': - server.start().then(port => { + const appBase = skyPagesConfigUtil.getAppBase(skyPagesConfig); + server.start(appBase).then(port => { browser(argv, skyPagesConfig, stats, port); }); break; diff --git a/cli/utils/server.js b/cli/utils/server.js index b3733ef2..ebfd4e3d 100644 --- a/cli/utils/server.js +++ b/cli/utils/server.js @@ -19,15 +19,20 @@ let server; function start(root) { return new Promise((resolve, reject) => { + logger.info('Creating web server'); + app.use(cors()); + + app.use(express.static('/dist')); + if (root) { + console.log(`Mapping server requests from ${root} to 'dist'`); + app.use(root, express.static('dist')); + } + const options = { cert: fs.readFileSync(path.resolve(__dirname, '../../ssl/server.crt')), key: fs.readFileSync(path.resolve(__dirname, '../../ssl/server.key')) }; - logger.info('Creating web server'); - app.use(cors()); - app.use(root || '/dist', express.static('dist')); - server = https.createServer(options, app); server.on('error', reject); diff --git a/config/protractor/protractor-dev.conf.js b/config/protractor/protractor-dev.conf.js index 43ff8b85..f79abaa2 100644 --- a/config/protractor/protractor-dev.conf.js +++ b/config/protractor/protractor-dev.conf.js @@ -1,6 +1,7 @@ /*jshint jasmine: true, node: true */ 'use strict'; +const fs = require('fs-extra'); const path = require('path'); const merge = require('merge'); const SpecReporter = require('jasmine-spec-reporter').SpecReporter; @@ -19,36 +20,44 @@ let config = { jasmine.getEnv().addReporter(new SpecReporter()); return new Promise((resolve, reject) => { - const url = 'https://github.com/blackbaud/skyux-template'; - const branch = 'master'; - common.rimrafPromise(common.tmp) - .then(() => common.exec(`git`, [ - `clone`, - `-b`, - branch, - `--single-branch`, - url, - common.tmp - ])) - .then(() => common.exec(`npm`, [`i`, '--only=prod'], common.cwdOpts)) - .then(() => common.exec(`npm`, [`i`, `../`], common.cwdOpts)) - .then(resolve) - .catch(reject); - }); - }, + if (fs.existsSync(common.tmp) && !process.argv.includes('--clean')) { - onComplete: () => { + console.log(''); + console.log('*********'); + console.log('Running fast e2e tests'); + console.log(`Delete ${common.tmp} to have the install steps run.`); + console.log('*********'); + console.log(''); - // Catch any rogue servers - common.afterAll(); + resolve(); - return new Promise((resolve, reject) => { - common.rimrafPromise(common.tmp) - .then(resolve) - .catch(reject); + } else { + + const url = 'https://github.com/blackbaud/skyux-template'; + const branch = 'master'; + + console.log('Running command using full install.'); + common.rimrafPromise(common.tmp) + .then(() => common.exec(`git`, [ + `clone`, + `-b`, + branch, + `--single-branch`, + url, + common.tmp + ])) + .then(() => common.exec(`npm`, [`i`, '--only=prod'], common.cwdOpts)) + .then(() => common.exec(`npm`, [`i`, `../`], common.cwdOpts)) + .then(resolve) + .catch(reject); + + } }); - } + }, + + // Catch any rogue servers + onComplete: () => common.afterAll }; // In CI, use firefox diff --git a/package.json b/package.json index da442bd5..41d23b7a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "awesome-typescript-loader": "3.1.3", "codelyzer": "3.0.1", "core-js": "2.4.1", + "cors": "2.8.4", + "express": "4.15.3", "fontfaceobserver": "2.0.9", "fs-extra": "3.0.1", "glob": "7.1.1", @@ -88,10 +90,10 @@ "ts-node": "3.0.4", "tslint": "5.2.0", "typescript": "2.3.2", + "web-animations-js": "2.2.5", "webpack": "2.5.1", "webpack-dev-server": "2.4.5", "webpack-merge": "4.1.0", - "web-animations-js": "2.2.5", "winston": "2.3.1", "zone.js": "0.8.10" }, diff --git a/test/cli-utils-server.spec.js b/test/cli-utils-server.spec.js index 0b1c9835..1de4f222 100644 --- a/test/cli-utils-server.spec.js +++ b/test/cli-utils-server.spec.js @@ -58,6 +58,14 @@ describe('server utils', () => { expect(server.stop).toBeDefined(); }); + it('should accept a root', () => { + const server = bind(); + const root = 'custom-root'; + server.start(root).then(() =>{ + + }); + }); + it('should close the http server if it exists', (done) => { const server = bind(); From 8fdf1a6d7d0107f51e010f692dca9e8b0e18d564 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Wed, 16 Aug 2017 16:58:35 -0400 Subject: [PATCH 09/17] Cleaned up linting --- cli/e2e.js | 2 -- config/webpack/serve.webpack.config.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/cli/e2e.js b/cli/e2e.js index 8c8887e2..7b5c2551 100644 --- a/cli/e2e.js +++ b/cli/e2e.js @@ -4,8 +4,6 @@ const glob = require('glob'); const path = require('path'); const spawn = require('cross-spawn'); -const portfinder = require('portfinder'); -const HttpServer = require('http-server'); const selenium = require('selenium-standalone'); const build = require('./build'); diff --git a/config/webpack/serve.webpack.config.js b/config/webpack/serve.webpack.config.js index d78c7fff..3ed26462 100644 --- a/config/webpack/serve.webpack.config.js +++ b/config/webpack/serve.webpack.config.js @@ -3,8 +3,6 @@ const fs = require('fs'); const path = require('path'); -const util = require('util'); -const open = require('open'); const webpackMerge = require('webpack-merge'); const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin'); const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); From a7a77816d614ef949852b7f808c927f5cd1e1afa Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Wed, 16 Aug 2017 17:01:37 -0400 Subject: [PATCH 10/17] Fixed mock --- test/cli-e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli-e2e.spec.js b/test/cli-e2e.spec.js index 2ac3923e..da90b981 100644 --- a/test/cli-e2e.spec.js +++ b/test/cli-e2e.spec.js @@ -66,7 +66,7 @@ describe('cli e2e', () => { }); mock('../cli/utils/server', { - start: () => PORT, + start: () => Promise.resolve(PORT), stop: () => {} }); From 2d3cb65acf8ba4eda24098e4b44b8cecda8d3a5f Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Thu, 17 Aug 2017 15:26:10 -0400 Subject: [PATCH 11/17] Cleaned up static directory --- cli/utils/server.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/utils/server.js b/cli/utils/server.js index ebfd4e3d..5770a4fa 100644 --- a/cli/utils/server.js +++ b/cli/utils/server.js @@ -8,6 +8,7 @@ const portfinder = require('portfinder'); const express = require('express'); const https = require('https'); const cors = require('cors'); + const app = express(); let server; @@ -19,13 +20,16 @@ let server; function start(root) { return new Promise((resolve, reject) => { + const dist = path.resolve(process.cwd(), 'dist'); + logger.info('Creating web server'); app.use(cors()); - app.use(express.static('/dist')); + logger.info('Exposing static directory: ${dist}'); + app.use(express.static(dist)); if (root) { - console.log(`Mapping server requests from ${root} to 'dist'`); - app.use(root, express.static('dist')); + logger.info(`Mapping server requests from ${root} to ${dist}`); + app.use('/' + root, express.static(dist)); } const options = { From 53dfb1c254ec7c9b03a3b8c68c70977fef2ad7d0 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 18 Aug 2017 08:13:33 -0400 Subject: [PATCH 12/17] Merged master --- config/axe/axe.config.js | 96 +++++++++++++++++++++++ lib/a11y-analyzer.js | 58 ++++++++++++++ package.json | 2 + runtime/config.ts | 5 ++ runtime/testing/e2e/a11y.d.ts | 3 + runtime/testing/e2e/a11y.js | 2 + runtime/testing/e2e/index.ts | 1 + test/config-axe.spec.js | 73 +++++++++++++++++ test/config-protractor.spec.js | 9 ++- test/lib-a11y-analyzer.spec.js | 138 +++++++++++++++++++++++++++++++++ 10 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 config/axe/axe.config.js create mode 100644 lib/a11y-analyzer.js create mode 100644 runtime/testing/e2e/a11y.d.ts create mode 100644 runtime/testing/e2e/a11y.js create mode 100644 test/config-axe.spec.js create mode 100644 test/lib-a11y-analyzer.spec.js diff --git a/config/axe/axe.config.js b/config/axe/axe.config.js new file mode 100644 index 00000000..3e167f63 --- /dev/null +++ b/config/axe/axe.config.js @@ -0,0 +1,96 @@ +/*jshint node: true*/ +'use strict'; + +// Defaults derived from: https://github.com/dequelabs/axe-core +const defaults = { + rules: { + 'area-alt': { 'enabled': true }, + 'audio-caption': { 'enabled': true }, + 'button-name': { 'enabled': true }, + 'document-title': { 'enabled': true }, + 'empty-heading': { 'enabled': true }, + 'frame-title': { 'enabled': true }, + 'frame-title-unique': { 'enabled': true }, + 'image-alt': { 'enabled': true }, + 'image-redundant-alt': { 'enabled': true }, + 'input-image-alt': { 'enabled': true }, + 'link-name': { 'enabled': true }, + 'object-alt': { 'enabled': true }, + 'server-side-image-map': { 'enabled': true }, + 'video-caption': { 'enabled': true }, + 'video-description': { 'enabled': true }, + + 'definition-list': { 'enabled': true }, + 'dlitem': { 'enabled': true }, + 'heading-order': { 'enabled': true }, + 'href-no-hash': { 'enabled': true }, + 'layout-table': { 'enabled': true }, + 'list': { 'enabled': true }, + 'listitem': { 'enabled': true }, + 'p-as-heading': { 'enabled': true }, + + 'scope-attr-valid': { 'enabled': true }, + 'table-duplicate-name': { 'enabled': true }, + 'table-fake-caption': { 'enabled': true }, + 'td-has-header': { 'enabled': true }, + 'td-headers-attr': { 'enabled': true }, + 'th-has-data-cells': { 'enabled': true }, + + 'duplicate-id': { 'enabled': true }, + 'html-has-lang': { 'enabled': true }, + 'html-lang-valid': { 'enabled': true }, + 'meta-refresh': { 'enabled': true }, + 'valid-lang': { 'enabled': true }, + + 'checkboxgroup': { 'enabled': true }, + 'label': { 'enabled': true }, + 'radiogroup': { 'enabled': true }, + + 'accesskeys': { 'enabled': true }, + 'bypass': { 'enabled': true }, + 'tabindex': { 'enabled': true }, + + 'aria-allowed-attr': { 'enabled': true }, + 'aria-required-attr': { 'enabled': true }, + 'aria-required-children': { 'enabled': true }, + 'aria-required-parent': { 'enabled': true }, + 'aria-roles': { 'enabled': true }, + 'aria-valid-attr': { 'enabled': true }, + 'aria-valid-attr-value': { 'enabled': true }, + + 'blink': { 'enabled': true }, + 'color-contrast': { 'enabled': true }, + 'link-in-text-block': { 'enabled': true }, + 'marquee': { 'enabled': true }, + 'meta-viewport': { 'enabled': true }, + 'meta-viewport-large': { 'enabled': true } + } +}; + +module.exports = { + getConfig: () => { + const skyPagesConfigUtil = require('../sky-pages/sky-pages.config'); + const skyPagesConfig = skyPagesConfigUtil.getSkyPagesConfig(); + + let config = {}; + + // Merge rules from skyux config. + if (skyPagesConfig.skyux.a11y && skyPagesConfig.skyux.a11y.rules) { + config.rules = Object.assign({}, defaults.rules, skyPagesConfig.skyux.a11y.rules); + } + + // The consuming SPA wishes to disable all rules. + if (skyPagesConfig.skyux.a11y === false) { + config.rules = Object.assign({}, defaults.rules); + Object.keys(config.rules).forEach((key) => { + config.rules[key].enabled = false; + }); + } + + if (!config.rules) { + return defaults; + } + + return config; + } +}; diff --git a/lib/a11y-analyzer.js b/lib/a11y-analyzer.js new file mode 100644 index 00000000..0a20fda1 --- /dev/null +++ b/lib/a11y-analyzer.js @@ -0,0 +1,58 @@ +/*jshint node: true */ +'use strict'; + +const axeBuilder = require('axe-webdriverjs'); +const logger = require('../utils/logger'); +const axeConfig = require('../config/axe/axe.config'); +const { browser } = require('protractor'); + +function SkyA11y() {} + +SkyA11y.run = function () { + return browser + .getCurrentUrl() + .then(url => new Promise((resolve) => { + const config = axeConfig.getConfig(); + + logger.info(`Starting accessibility checks for ${url}...`); + + axeBuilder(browser.driver) + .options(config) + .analyze((results) => { + const numViolations = results.violations.length; + const subject = (numViolations === 1) ? 'violation' : 'violations'; + + logger.info(`Accessibility checks finished with ${numViolations} ${subject}.\n`); + + if (numViolations > 0) { + logViolations(results); + } + + resolve(numViolations); + }); + })); +}; + +function logViolations(results) { + results.violations.forEach((violation) => { + const wcagTags = violation.tags + .filter(tag => tag.match(/wcag\d{3}|^best*/gi)) + .join(', '); + + const html = violation.nodes + .reduce( + (accumulator, node) => `${accumulator}\n${node.html}\n`, + ' Elements:\n' + ); + + const error = [ + `aXe - [Rule: \'${violation.id}\'] ${violation.help} - WCAG: ${wcagTags}`, + ` Get help at: ${violation.helpUrl}\n`, + `${html}\n\n` + ].join('\n'); + + logger.error(error); + }); +} + +module.exports = SkyA11y; diff --git a/package.json b/package.json index 896bdc27..2b836ef4 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/node": "7.0.18", "angular2-template-loader": "0.6.2", "awesome-typescript-loader": "3.1.3", + "axe-webdriverjs": "1.1.3", "codelyzer": "3.0.1", "core-js": "2.4.1", "cors": "2.8.4", @@ -84,6 +85,7 @@ "rxjs": "5.4.2", "sass-loader": "6.0.5", "selenium-standalone": "6.4.1", + "selenium-webdriver": "3.5.0", "source-map-inline-loader": "blackbaud-bobbyearl/source-map-inline-loader", "source-map-loader": "0.2.1", "style-loader": "0.17.0", diff --git a/runtime/config.ts b/runtime/config.ts index a493907e..01cabb1d 100644 --- a/runtime/config.ts +++ b/runtime/config.ts @@ -25,6 +25,10 @@ export interface RuntimeConfig { useTemplateUrl: boolean; } +export interface SkyuxConfigA11y { + rules?: any; +} + export interface SkyuxConfigApp { externals?: Object; port?: string; @@ -36,6 +40,7 @@ export interface SkyuxConfigHost { } export interface SkyuxConfig { + a11y?: SkyuxConfigA11y|boolean; app?: SkyuxConfigApp; appSettings?: any; auth?: boolean; diff --git a/runtime/testing/e2e/a11y.d.ts b/runtime/testing/e2e/a11y.d.ts new file mode 100644 index 00000000..b1bec7ab --- /dev/null +++ b/runtime/testing/e2e/a11y.d.ts @@ -0,0 +1,3 @@ +export declare class SkyA11y { + static run(): Promise; +} diff --git a/runtime/testing/e2e/a11y.js b/runtime/testing/e2e/a11y.js new file mode 100644 index 00000000..bfbcce5c --- /dev/null +++ b/runtime/testing/e2e/a11y.js @@ -0,0 +1,2 @@ +const analyzer = require('../../../lib/a11y-analyzer'); +module.exports = { SkyA11y: analyzer }; diff --git a/runtime/testing/e2e/index.ts b/runtime/testing/e2e/index.ts index 8d2d6c88..97d36f6d 100644 --- a/runtime/testing/e2e/index.ts +++ b/runtime/testing/e2e/index.ts @@ -1 +1,2 @@ export * from './host-browser'; +export * from './a11y'; diff --git a/test/config-axe.spec.js b/test/config-axe.spec.js new file mode 100644 index 00000000..1539d186 --- /dev/null +++ b/test/config-axe.spec.js @@ -0,0 +1,73 @@ +/*jshint jasmine: true, node: true */ +'use strict'; + +const mock = require('mock-require'); + +describe('config axe', () => { + afterEach(() => { + mock.stopAll(); + }); + + it('should return a config object', () => { + mock('../config/sky-pages/sky-pages.config', { + getSkyPagesConfig: () => { + return { + skyux: {} + }; + } + }); + const lib = mock.reRequire('../config/axe/axe.config'); + const config = lib.getConfig(); + expect(config).toBeDefined(); + expect(config.rules.label.enabled).toEqual(true); + }); + + it('should merge config from a consuming SPA', () => { + mock('../config/sky-pages/sky-pages.config', { + getSkyPagesConfig: () => { + return { + skyux: { + a11y: { + rules: { + label: { enabled: false } + } + } + } + }; + } + }); + const lib = mock.reRequire('../config/axe/axe.config'); + const config = lib.getConfig(); + expect(config.rules.label.enabled).toEqual(false); + }); + + it('should return defaults if rules are not defined', () => { + mock('../config/sky-pages/sky-pages.config', { + getSkyPagesConfig: () => { + return { + skyux: { + a11y: {} + } + }; + } + }); + const lib = mock.reRequire('../config/axe/axe.config'); + const config = lib.getConfig(); + expect(config.rules.label.enabled).toEqual(true); + }); + + it('should disabled all rules if accessibility is set to false', () => { + mock('../config/sky-pages/sky-pages.config', { + getSkyPagesConfig: () => { + return { + skyux: { + a11y: false + } + }; + } + }); + const lib = mock.reRequire('../config/axe/axe.config'); + const config = lib.getConfig(); + expect(config.rules.label.enabled).toEqual(false); + }); +}); diff --git a/test/config-protractor.spec.js b/test/config-protractor.spec.js index 2412d584..8e7107ed 100644 --- a/test/config-protractor.spec.js +++ b/test/config-protractor.spec.js @@ -9,10 +9,14 @@ describe('config protractor test', () => { let config; beforeEach(() => { - lib = require('../config/protractor/protractor.conf.js'); + lib = mock.reRequire('../config/protractor/protractor.conf.js'); config = lib.config; }); + afterEach(() => { + mock.stopAll(); + }); + it('should return a config object', () => { expect(lib.config).toBeDefined(); }); @@ -28,9 +32,6 @@ describe('config protractor test', () => { expect(config.beforeLaunch).toBeDefined(); config.beforeLaunch(); expect(called).toBe(true); - - mock.stop('ts-node'); - }); it('should provide a method for onPrepare', () => { diff --git a/test/lib-a11y-analyzer.spec.js b/test/lib-a11y-analyzer.spec.js new file mode 100644 index 00000000..6a8c7808 --- /dev/null +++ b/test/lib-a11y-analyzer.spec.js @@ -0,0 +1,138 @@ +/*jshint jasmine: true, node: true */ +'use strict'; + +const mock = require('mock-require'); +const logger = require('../utils/logger'); + +describe('SkyA11y', () => { + let browser; + let context; + + beforeEach(() => { + mock('protractor', { + browser: { + getCurrentUrl: () => Promise.resolve('') + } + }); + + mock('../config/axe/axe.config', { + getConfig: () => { + return {}; + } + }); + + context = { + config: { + axe: {} + } + }; + + browser = { + getCurrentUrl: () => { + return Promise.resolve('http://foo.bar'); + } + }; + }); + + afterEach(() => { + mock.stopAll(); + }); + + it('should return a promise', () => { + mock('axe-webdriverjs', function MockAxeBuilder() { + return { + options: () => { + return { + analyze: (callback) => Promise.resolve() + }; + } + }; + }); + + spyOn(logger, 'info').and.returnValue(); + spyOn(logger, 'error').and.returnValue(); + + const plugin = mock.reRequire('../lib/a11y-analyzer'); + const result = plugin.run(); + expect(typeof result.then).toEqual('function'); + }); + + it('should return a class', () => { + mock('axe-webdriverjs', function MockAxeBuilder() { + return { + options: () => { + return { + analyze: (callback) => Promise.resolve() + }; + } + }; + }); + + spyOn(logger, 'info').and.returnValue(); + spyOn(logger, 'error').and.returnValue(); + + const plugin = mock.reRequire('../lib/a11y-analyzer'); + const result = new plugin(); + expect(typeof result).toBeDefined(); + }); + + it('should print a message to the console', (done) => { + mock('axe-webdriverjs', function MockAxeBuilder() { + return { + options: () => { + return { + analyze: (callback) => { + callback({ + violations: [] + }); + expect(logger.info.calls.argsFor(1)[0]) + .toContain(`Accessibility checks finished with 0 violations.`); + done(); + return Promise.resolve(); + } + }; + } + }; + }); + + spyOn(logger, 'info').and.returnValue(); + spyOn(logger, 'error').and.returnValue(); + + const plugin = mock.reRequire('../lib/a11y-analyzer'); + const result = plugin.run(); + }); + + it('should log violations', (done) => { + mock('axe-webdriverjs', function MockAxeBuilder() { + return { + options: () => { + return { + analyze: (callback) => { + callback({ + violations: [{ + id: 'label', + help: 'Description here.', + helpUrl: 'https://foo.bar', + nodes: [{ html: '

' }], + tags: ['cat.forms', 'wcag2a', 'wcag332', 'wcag131'] + }] + }); + expect(logger.info.calls.argsFor(1)[0]) + .toContain(`Accessibility checks finished with 1 violation.`); + expect(logger.error.calls.argsFor(0)[0]) + .toContain('aXe - [Rule: \'label\'] Description here. - WCAG: wcag332, wcag131'); + done(); + return Promise.resolve(); + } + }; + } + }; + }); + + spyOn(logger, 'info').and.returnValue(); + spyOn(logger, 'error').and.returnValue(); + + const plugin = mock.reRequire('../lib/a11y-analyzer'); + const result = plugin.run(); + }); +}); From ced2832bf4333d8f9dada5903c98d93a8e511966 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 18 Aug 2017 14:11:48 -0400 Subject: [PATCH 13/17] Removed unnecessary forward slash. Fixed logging. --- cli/utils/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/utils/server.js b/cli/utils/server.js index 5770a4fa..39ca6b62 100644 --- a/cli/utils/server.js +++ b/cli/utils/server.js @@ -25,11 +25,11 @@ function start(root) { logger.info('Creating web server'); app.use(cors()); - logger.info('Exposing static directory: ${dist}'); + logger.info(`Exposing static directory: ${dist}`); app.use(express.static(dist)); if (root) { logger.info(`Mapping server requests from ${root} to ${dist}`); - app.use('/' + root, express.static(dist)); + app.use(root, express.static(dist)); } const options = { From dc2945381946aa740dbcd944bd82b6ac4ef9c3f8 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 18 Aug 2017 16:32:03 -0400 Subject: [PATCH 14/17] Separated launch from serve functionality. Defaulting launch to host --- cli/build.js | 41 ++++++++++-------- cli/utils/browser.js | 3 ++ config/webpack/serve.webpack.config.js | 3 -- index.js | 3 +- test/cli-build.spec.js | 60 +++++++++++--------------- test/cli-utils-browser.spec.js | 60 ++++++++++++++------------ test/config-webpack-serve.spec.js | 32 -------------- 7 files changed, 87 insertions(+), 115 deletions(-) diff --git a/cli/build.js b/cli/build.js index c9da9602..11ac34d7 100644 --- a/cli/build.js +++ b/cli/build.js @@ -118,6 +118,20 @@ function cleanupAot() { fs.removeSync(skyPagesConfigUtil.spaPathTemp()); } +function compile(webpack, config, compileModeIsAoT) { + return new Promise((resolve, reject) => { + runCompiler(webpack, config) + .then(stats => { + if (compileModeIsAoT) { + cleanupAot(); + } + + resolve(stats); + }) + .catch(reject); + }); +} + /** * Executes the build command. * @name build @@ -148,26 +162,17 @@ function build(argv, skyPagesConfig, webpack) { const config = buildConfig.getWebpackConfig(skyPagesConfig); assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl, assetsRel); - return runCompiler(webpack, config) - .then(stats => { - if (compileModeIsAoT) { - cleanupAot(); - } - - // Serve build files - switch (argv.launch) { - case 'host': - case 'local': - const appBase = skyPagesConfigUtil.getAppBase(skyPagesConfig); - server.start(appBase).then(port => { - browser(argv, skyPagesConfig, stats, port); - }); - break; - default: - return Promise.resolve(stats); - } + if (!argv.serve) { + return compile(webpack, config, compileModeIsAoT); + } + // We need port from server, to configure assets url, which must be set before building. + server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)).then(port => { + argv.assets = 'https://localhost:' + port; + compile(webpack, config, compileModeIsAoT).then(stats => { + browser(argv, skyPagesConfig, stats, port); }); + }); } module.exports = build; diff --git a/cli/utils/browser.js b/cli/utils/browser.js index f0d7c013..2d7c359c 100644 --- a/cli/utils/browser.js +++ b/cli/utils/browser.js @@ -55,6 +55,9 @@ function browser(argv, skyPagesConfig, stats, port) { localUrl = edge + localUrl; } + // Browser defaults to launching host + argv.launch = argv.launch || 'host'; + switch (argv.launch) { case 'local': diff --git a/config/webpack/serve.webpack.config.js b/config/webpack/serve.webpack.config.js index 3ed26462..8269af1d 100644 --- a/config/webpack/serve.webpack.config.js +++ b/config/webpack/serve.webpack.config.js @@ -29,9 +29,6 @@ function getWebpackConfig(argv, skyPagesConfig) { if (!launched) { logger.info('SKY UX builder is ready.'); launched = true; - - // Host is default launch - argv.launch = argv.launch || 'host'; browser(argv, skyPagesConfig, stats, this.options.devServer.port); } }); diff --git a/index.js b/index.js index 7d1b13fb..a97d8ef9 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,8 @@ module.exports = { const skyPagesConfig = config.getSkyPagesConfig(command); const shorthand = { l: 'launch', - b: 'browser' + b: 'browser', + s: 'serve' }; // Process shorthand flags diff --git a/test/cli-build.spec.js b/test/cli-build.spec.js index 17bdeb99..695342af 100644 --- a/test/cli-build.spec.js +++ b/test/cli-build.spec.js @@ -8,35 +8,6 @@ const runtimeUtils = require('../utils/runtime-test-utils'); describe('cli build', () => { - function testBuildAndServe(launch, done) { - const argv = { - launch: launch - }; - - mock('../cli/utils/server', { - start: () => Promise.resolve() - }); - - mock('../cli/utils/browser', (argv) => { - expect(argv.launch).toBe(launch); - done(); - }); - - mock.reRequire('../cli/build')(argv, runtimeUtils.getDefault(), () => ({ - run: (cb) => { - cb( - null, - { - toJson: () => ({ - errors: [], - warnings: [] - }) - } - ); - } - })); - } - beforeEach(() => { spyOn(process, 'exit').and.callFake(() => {}); mock('../cli/utils/ts-linter', { @@ -341,11 +312,32 @@ describe('cli build', () => { expect(process.exit).toHaveBeenCalledWith(1); }); - it('should serve and browse to the built files if launch flag is host', (done) => { - testBuildAndServe('host', done); - }); + it('should serve and browse to the built files if serve flag is present', (done) => { + const port = 1234; + + mock('../cli/utils/server', { + start: () => Promise.resolve(port) + }); + + mock('../cli/utils/browser', (argv, c, s, p) => { + expect(argv.serve).toBe(true); + expect(p).toBe(port); + done(); + }); - it('should serve and browse to the built files if launch flag is local', (done) => { - testBuildAndServe('local', done); + mock.reRequire('../cli/build')({ serve: true }, runtimeUtils.getDefault(), () => ({ + run: (cb) => { + cb( + null, + { + toJson: () => ({ + errors: [], + warnings: [] + }) + } + ); + } + })); }); + }); diff --git a/test/cli-utils-browser.spec.js b/test/cli-utils-browser.spec.js index caccb474..cfaa2989 100644 --- a/test/cli-utils-browser.spec.js +++ b/test/cli-utils-browser.spec.js @@ -55,6 +55,34 @@ describe('browser utils', () => { return merged; } + function testLaunchHost(argv) { + const port = 1234; + const appBase = 'app-base'; + + const settings = bind({ + argv: argv, + port: port, + skyPagesConfig: { + runtime: runtimeUtils.getDefaultRuntime, + skyux: runtimeUtils.getDefaultSkyux({ + name: appBase + }) + } + }); + + const localUrl = `https://localhost:${port}/${appBase}/`; + const hostUrl = hostUtils.resolve( + '', + localUrl, + [], + settings.skyPagesConfig + ); + + expect(logger.info).toHaveBeenCalledWith(`Launching Host URL: ${hostUrl}`); + expect(openCalled).toBe(true); + expect(openParamUrl).toBe(hostUrl); + } + it('should run envid and svcid through encodeURIComponent', () => { const s = bind({ argv: { @@ -85,34 +113,12 @@ describe('browser utils', () => { expect(parsed.query.noid).not.toBeDefined(); }); - it('should log the host url and launch it when --launch host', () => { - const port = 1234; - const appBase = 'app-base'; - - const settings = bind({ - argv: { - launch: 'host' - }, - port: port, - skyPagesConfig: { - runtime: runtimeUtils.getDefaultRuntime, - skyux: runtimeUtils.getDefaultSkyux({ - name: appBase - }) - } - }); - - const localUrl = `https://localhost:${port}/${appBase}/`; - const hostUrl = hostUtils.resolve( - '', - localUrl, - [], - settings.skyPagesConfig - ); + it('should default --launch to host', () => { + testLaunchHost({}); + }); - expect(logger.info).toHaveBeenCalledWith(`Launching Host URL: ${hostUrl}`); - expect(openCalled).toBe(true); - expect(openParamUrl).toBe(hostUrl); + it('should log the host url and launch it when --launch host', () => { + testLaunchHost({ launch: 'host' }); }); it('should log the local url and launch it when --launch local', () => { diff --git a/test/config-webpack-serve.spec.js b/test/config-webpack-serve.spec.js index b3d60a99..c27d96cf 100644 --- a/test/config-webpack-serve.spec.js +++ b/test/config-webpack-serve.spec.js @@ -27,38 +27,6 @@ describe('config webpack serve', () => { expect(typeof lib.getWebpackConfig).toEqual('function'); }); - it('should set default `launch` to `host', () => { - spyOn(logger, 'info'); - const lib = require('../config/webpack/serve.webpack.config'); - const config = lib.getWebpackConfig({}, runtimeUtils.getDefault()); - - config.plugins.forEach(plugin => { - if (plugin.name === 'WebpackPluginDone') { - plugin.apply({ - options: { - appConfig: { - base: 'my-custom-base' - }, - devServer: { - port: 1234 - } - }, - plugin: (evt, cb) => { - if (evt === 'done') { - cb({ - toJson: () => ({ - chunks: [] - }) - }); - } - } - }); - } - }); - - expect(paramArgv.launch).toEqual('host'); - }); - it('should only log the ready message once during multiple dones', () => { spyOn(logger, 'info'); From 2085ebb42e1866df09598ade754274c1dc3b9f5b Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Fri, 18 Aug 2017 18:16:39 -0400 Subject: [PATCH 15/17] Refactored to use --serve and correctly set assets --- cli/build.js | 68 +++++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/cli/build.js b/cli/build.js index 11ac34d7..fc8a284b 100644 --- a/cli/build.js +++ b/cli/build.js @@ -118,11 +118,35 @@ function cleanupAot() { fs.removeSync(skyPagesConfigUtil.spaPathTemp()); } -function compile(webpack, config, compileModeIsAoT) { +function buildServe(argv, skyPagesConfig, webpack, isAot) { + server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)) + .then(port => { + argv.assets = argv.assets || `https://localhost:${port}`; + buildPromise(argv, skyPagesConfig, webpack, isAot) + .then(stats => browser(argv, skyPagesConfig, stats, port)); + }); +} + +function buildPromise(argv, skyPagesConfig, webpack, isAot) { + const assetsBaseUrl = argv.assets || ''; + const assetsRel = argv.assetsrel; + + let buildConfig; + + if (isAot) { + stageAot(skyPagesConfig, assetsBaseUrl, assetsRel); + buildConfig = require('../config/webpack/build-aot.webpack.config'); + } else { + buildConfig = require('../config/webpack/build.webpack.config'); + } + return new Promise((resolve, reject) => { - runCompiler(webpack, config) + const config = buildConfig.getWebpackConfig(skyPagesConfig); + assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl, assetsRel); + + runCompiler(webpack, config, isAot) .then(stats => { - if (compileModeIsAoT) { + if (isAot) { cleanupAot(); } @@ -135,44 +159,24 @@ function compile(webpack, config, compileModeIsAoT) { /** * Executes the build command. * @name build + * @param {*} skyPagesConfig + * @param {*} webpack + * @param {*} isAot */ function build(argv, skyPagesConfig, webpack) { - const compileModeIsAoT = skyPagesConfig && + + const lintResult = tsLinter.lintSync(); + const isAot = skyPagesConfig && skyPagesConfig.skyux && skyPagesConfig.skyux.compileMode === 'aot'; - let buildConfig; - - const assetsBaseUrl = argv.assets || ''; - const assetsRel = argv.assetsrel; - - const lintResult = tsLinter.lintSync(); if (lintResult.exitCode > 0) { process.exit(lintResult.exitCode); - return; - } - - if (compileModeIsAoT) { - stageAot(skyPagesConfig, assetsBaseUrl, assetsRel); - buildConfig = require('../config/webpack/build-aot.webpack.config'); + } else if (argv.serve) { + buildServe(argv, skyPagesConfig, webpack, isAot); } else { - buildConfig = require('../config/webpack/build.webpack.config'); - } - - const config = buildConfig.getWebpackConfig(skyPagesConfig); - assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl, assetsRel); - - if (!argv.serve) { - return compile(webpack, config, compileModeIsAoT); + return buildPromise(argv, skyPagesConfig, webpack, isAot); } - - // We need port from server, to configure assets url, which must be set before building. - server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)).then(port => { - argv.assets = 'https://localhost:' + port; - compile(webpack, config, compileModeIsAoT).then(stats => { - browser(argv, skyPagesConfig, stats, port); - }); - }); } module.exports = build; From 7c6c2983185ba6b4ba2512f8362470d0f0990637 Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Mon, 21 Aug 2017 14:54:59 -0400 Subject: [PATCH 16/17] Always returning promise (if lint passes) from build. Fixed e2e branch of template --- cli/build.js | 26 ++++++++++++++++-------- config/protractor/protractor-dev.conf.js | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cli/build.js b/cli/build.js index fc8a284b..003909cd 100644 --- a/cli/build.js +++ b/cli/build.js @@ -119,15 +119,22 @@ function cleanupAot() { } function buildServe(argv, skyPagesConfig, webpack, isAot) { - server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)) + return new Promise((resolve, reject) => { + server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)) .then(port => { argv.assets = argv.assets || `https://localhost:${port}`; - buildPromise(argv, skyPagesConfig, webpack, isAot) - .then(stats => browser(argv, skyPagesConfig, stats, port)); - }); + buildCompiler(argv, skyPagesConfig, webpack, isAot) + .then(stats => { + browser(argv, skyPagesConfig, stats, port); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + }); } -function buildPromise(argv, skyPagesConfig, webpack, isAot) { +function buildCompiler(argv, skyPagesConfig, webpack, isAot) { const assetsBaseUrl = argv.assets || ''; const assetsRel = argv.assetsrel; @@ -172,10 +179,13 @@ function build(argv, skyPagesConfig, webpack) { if (lintResult.exitCode > 0) { process.exit(lintResult.exitCode); - } else if (argv.serve) { - buildServe(argv, skyPagesConfig, webpack, isAot); } else { - return buildPromise(argv, skyPagesConfig, webpack, isAot); + return new Promise((resolve, reject) => { + const name = argv.serve ? buildServe : buildCompiler; + name(argv, skyPagesConfig, webpack, isAot) + .then(resolve) + .catch(reject); + }); } } diff --git a/config/protractor/protractor-dev.conf.js b/config/protractor/protractor-dev.conf.js index f79abaa2..40505f6a 100644 --- a/config/protractor/protractor-dev.conf.js +++ b/config/protractor/protractor-dev.conf.js @@ -35,7 +35,7 @@ let config = { } else { const url = 'https://github.com/blackbaud/skyux-template'; - const branch = 'master'; + const branch = 'builder-dev'; console.log('Running command using full install.'); common.rimrafPromise(common.tmp) From bdc59f566464e896857432d6b6f36fdb80cb56ac Mon Sep 17 00:00:00 2001 From: Bobby Earl Date: Mon, 21 Aug 2017 16:04:51 -0400 Subject: [PATCH 17/17] Cleaned up promises --- cli/build.js | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/cli/build.js b/cli/build.js index 003909cd..7e85373e 100644 --- a/cli/build.js +++ b/cli/build.js @@ -119,19 +119,17 @@ function cleanupAot() { } function buildServe(argv, skyPagesConfig, webpack, isAot) { - return new Promise((resolve, reject) => { - server.start(skyPagesConfigUtil.getAppBase(skyPagesConfig)) + const base = skyPagesConfigUtil.getAppBase(skyPagesConfig); + return server + .start(base) .then(port => { argv.assets = argv.assets || `https://localhost:${port}`; - buildCompiler(argv, skyPagesConfig, webpack, isAot) + return buildCompiler(argv, skyPagesConfig, webpack, isAot) .then(stats => { browser(argv, skyPagesConfig, stats, port); - resolve(); - }) - .catch(reject); - }) - .catch(reject); - }); + return stats; + }); + }); } function buildCompiler(argv, skyPagesConfig, webpack, isAot) { @@ -147,20 +145,17 @@ function buildCompiler(argv, skyPagesConfig, webpack, isAot) { buildConfig = require('../config/webpack/build.webpack.config'); } - return new Promise((resolve, reject) => { - const config = buildConfig.getWebpackConfig(skyPagesConfig); - assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl, assetsRel); + const config = buildConfig.getWebpackConfig(skyPagesConfig); + assetsProcessor.setSkyAssetsLoaderUrl(config, skyPagesConfig, assetsBaseUrl, assetsRel); - runCompiler(webpack, config, isAot) - .then(stats => { - if (isAot) { - cleanupAot(); - } + return runCompiler(webpack, config, isAot) + .then(stats => { + if (isAot) { + cleanupAot(); + } - resolve(stats); - }) - .catch(reject); - }); + return stats; + }); } /** @@ -180,12 +175,8 @@ function build(argv, skyPagesConfig, webpack) { if (lintResult.exitCode > 0) { process.exit(lintResult.exitCode); } else { - return new Promise((resolve, reject) => { - const name = argv.serve ? buildServe : buildCompiler; - name(argv, skyPagesConfig, webpack, isAot) - .then(resolve) - .catch(reject); - }); + const name = argv.serve ? buildServe : buildCompiler; + return name(argv, skyPagesConfig, webpack, isAot); } }