diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5859404f..05ecafa1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ New features: Internal: - Update check script for new components and tweak docs (PR [#589](https://github.com/alphagov/govuk-frontend/pull/589)) +- Listen for development server on different port for tests + (PR [#622](https://github.com/alphagov/govuk-frontend/pull/622)) - Fix date-input default example (PR [#623](https://github.com/alphagov/govuk-frontend/pull/623)) diff --git a/app/__tests__/app.test.js b/app/__tests__/app.test.js index 3eea084ed0..f3de259534 100644 --- a/app/__tests__/app.test.js +++ b/app/__tests__/app.test.js @@ -5,57 +5,60 @@ const cheerio = require('cheerio') const lib = require('../../lib/file-helper') +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test + const requestParamsHomepage = { - url: 'http://localhost:3000/', + url: `http://localhost:${PORT}/`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleAllComponents = { - url: 'http://localhost:3000/examples/all-components', + url: `http://localhost:${PORT}/examples/all-components`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleFormAlignment = { - url: 'http://localhost:3000/examples/form-alignment', + url: `http://localhost:${PORT}/examples/form-alignment`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleFormElements = { - url: 'http://localhost:3000/examples/form-elements', + url: `http://localhost:${PORT}/examples/form-elements`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleGrid = { - url: 'http://localhost:3000/examples/grid', + url: `http://localhost:${PORT}/examples/grid`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleLinks = { - url: 'http://localhost:3000/examples/links', + url: `http://localhost:${PORT}/examples/links`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleProseScope = { - url: 'http://localhost:3000/examples/prose-scope', + url: `http://localhost:${PORT}/examples/prose-scope`, headers: { 'Content-Type': 'text/plain' } } const requestParamsExampleTypography = { - url: 'http://localhost:3000/examples/typography', + url: `http://localhost:${PORT}/examples/typography`, headers: { 'Content-Type': 'text/plain' } diff --git a/app/app.js b/app/app.js index 4a22fdc5ab..84dc08faa6 100644 --- a/app/app.js +++ b/app/app.js @@ -3,7 +3,6 @@ const app = express() const nunjucks = require('nunjucks') const fs = require('fs') const path = require('path') -const port = (process.env.PORT || 3000) const yaml = require('js-yaml') const helperFunctions = require('../lib/helper-functions') @@ -18,136 +17,137 @@ const appViews = [ configPaths.src ] -// Configure nunjucks -let env = nunjucks.configure(appViews, { - autoescape: true, // output with dangerous characters are escaped automatically - express: app, // the express app that nunjucks should install to - noCache: true, // never use a cache and recompile templates each time - trimBlocks: true, // automatically remove trailing newlines from a block/tag - lstripBlocks: true, // automatically remove leading whitespace from a block/tag - watch: true // reload templates when they are changed. needs chokidar dependency to be installed -}) - -// make the function available as a filter for all templates -env.addFilter('componentNameToMacroName', helperFunctions.componentNameToMacroName) - -// Set view engine -app.set('view engine', 'njk') - -// Set up middleware to serve static assets -app.use('/public', express.static(configPaths.public)) - -// serve html5-shiv from node modules -app.use('/vendor/html5-shiv/', express.static('node_modules/html5shiv/dist/')) -app.use('/icons', express.static(path.join(configPaths.src, 'icons'))) - -const server = app.listen(port, () => { - console.log('Listening on port ' + port + ' url: http://localhost:' + port) -}) - -// Define routes - -// Index page - render the component list template -app.get('/', function (req, res) { - Promise.all([ - directoryToObject(path.resolve(configPaths.src)), - directoryToObject(path.resolve(configPaths.examples)) - ]).then(result => { - const [components, examples] = result - - // filter out globals, all and icons package - const {globals, all, icons, ...filteredComponents} = components - - res.render('index', { - componentsDirectory: filteredComponents, - examplesDirectory: examples +module.exports = (options) => { + const nunjucksOptions = options ? options.nunjucks : {} + + // Configure nunjucks + let env = nunjucks.configure(appViews, { + autoescape: true, // output with dangerous characters are escaped automatically + express: app, // the express app that nunjucks should install to + noCache: true, // never use a cache and recompile templates each time + trimBlocks: true, // automatically remove trailing newlines from a block/tag + lstripBlocks: true, // automatically remove leading whitespace from a block/tag + watch: true, // reload templates when they are changed. needs chokidar dependency to be installed + ...nunjucksOptions // merge any additional options and overwrite defaults above. + }) + + // make the function available as a filter for all templates + env.addFilter('componentNameToMacroName', helperFunctions.componentNameToMacroName) + + // Set view engine + app.set('view engine', 'njk') + + // Set up middleware to serve static assets + app.use('/public', express.static(configPaths.public)) + + // serve html5-shiv from node modules + app.use('/vendor/html5-shiv/', express.static('node_modules/html5shiv/dist/')) + app.use('/icons', express.static(path.join(configPaths.src, 'icons'))) + + // Define routes + + // Index page - render the component list template + app.get('/', function (req, res) { + Promise.all([ + directoryToObject(path.resolve(configPaths.src)), + directoryToObject(path.resolve(configPaths.examples)) + ]).then(result => { + const [components, examples] = result + + // filter out globals, all and icons package + const {globals, all, icons, ...filteredComponents} = components + + res.render('index', { + componentsDirectory: filteredComponents, + examplesDirectory: examples + }) + }) + }) + + // Whenever the route includes a :component parameter, read the component data + // from its YAML file + app.param('component', function (req, res, next, componentName) { + let yamlPath = configPaths.src + `${componentName}/${componentName}.yaml` + + try { + res.locals.componentData = yaml.safeLoad( + fs.readFileSync(yamlPath, 'utf8'), { json: true } + ) + next() + } catch (e) { + next(new Error('failed to load component YAML file')) + } + }) + + // Component 'README' page + app.get('/components/:component', function (req, res, next) { + // make variables available to nunjucks template + res.locals.componentPath = req.params.component + + res.render(`${req.params.component}/index`, function (error, html) { + if (error) { + next(error) + } else { + res.send(html) + } }) }) -}) -// Whenever the route includes a :component parameter, read the component data -// from its YAML file -app.param('component', function (req, res, next, componentName) { - let yamlPath = configPaths.src + `${componentName}/${componentName}.yaml` + // Component example preview + app.get('/components/:component/:example*?/preview', function (req, res, next) { + // Find the data for the specified example (or the default example) + let componentName = req.params.component + let requestedExampleName = req.params.example || 'default' - try { - res.locals.componentData = yaml.safeLoad( - fs.readFileSync(yamlPath, 'utf8'), { json: true } + let exampleConfig = res.locals.componentData.examples.find( + example => example.name === requestedExampleName ) - next() - } catch (e) { - next(new Error('failed to load component YAML file')) - } -}) - -// Component 'README' page -app.get('/components/:component', function (req, res, next) { - // make variables available to nunjucks template - res.locals.componentPath = req.params.component - - res.render(`${req.params.component}/index`, function (error, html) { - if (error) { - next(error) - } else { - res.send(html) + + if (!exampleConfig) { + next() } - }) -}) -// Component example preview -app.get('/components/:component/:example*?/preview', function (req, res, next) { - // Find the data for the specified example (or the default example) - let componentName = req.params.component - let requestedExampleName = req.params.example || 'default' + // Construct and evaluate the component with the data for this example + let macroName = helperFunctions.componentNameToMacroName(componentName) + let macroParameters = JSON.stringify(exampleConfig.data, null, '\t') - let exampleConfig = res.locals.componentData.examples.find( - example => example.name === requestedExampleName - ) + res.locals.componentView = env.renderString( + `{% from '${componentName}/macro.njk' import ${macroName} %} + {{ ${macroName}(${macroParameters}) }}` + ) - if (!exampleConfig) { - next() - } - - // Construct and evaluate the component with the data for this example - let macroName = helperFunctions.componentNameToMacroName(componentName) - let macroParameters = JSON.stringify(exampleConfig.data, null, '\t') - - res.locals.componentView = env.renderString( - `{% from '${componentName}/macro.njk' import ${macroName} %} - {{ ${macroName}(${macroParameters}) }}` - ) - - let bodyClasses = '' - if (req.query.iframe) { - bodyClasses = 'app-iframe-in-component-preview' - } - - res.render('component-preview', { bodyClasses }) -}) - -// Example view -app.get('/examples/:example', function (req, res, next) { - res.render(`${req.params.example}/index`, function (error, html) { - if (error) { - next(error) - } else { - res.send(html) + let bodyClasses = '' + if (req.query.iframe) { + bodyClasses = 'app-iframe-in-component-preview' } + + res.render('component-preview', { bodyClasses }) }) -}) - -// Disallow search index indexing -app.use(function (req, res, next) { - // none - Equivalent to noindex, nofollow - // noindex - Do not show this page in search results and do not show a "Cached" link in search results. - // nofollow - Do not follow the links on this page - res.setHeader('X-Robots-Tag', 'none') - next() -}) - -app.get('/robots.txt', function (req, res) { - res.type('text/plain') - res.send('User-agent: *\nDisallow: /') -}) - -module.exports = server + + // Example view + app.get('/examples/:example', function (req, res, next) { + res.render(`${req.params.example}/index`, function (error, html) { + if (error) { + next(error) + } else { + res.send(html) + } + }) + }) + + // Disallow search index indexing + app.use(function (req, res, next) { + // none - Equivalent to noindex, nofollow + // noindex - Do not show this page in search results and do not show a "Cached" link in search results. + // nofollow - Do not follow the links on this page + res.setHeader('X-Robots-Tag', 'none') + next() + }) + + app.get('/robots.txt', function (req, res) { + res.type('text/plain') + res.send('User-agent: *\nDisallow: /') + }) + + return app +} diff --git a/app/start.js b/app/start.js new file mode 100644 index 0000000000..f622271e74 --- /dev/null +++ b/app/start.js @@ -0,0 +1,8 @@ +const configPaths = require('../config/paths.json') +const PORT = process.env.PORT || configPaths.ports.app + +const app = require('./app.js')() + +app.listen(PORT, () => { + console.log('Server started at http://localhost:' + PORT) +}) diff --git a/config/paths.json b/config/paths.json index 8328267247..16fdafe9e5 100644 --- a/config/paths.json +++ b/config/paths.json @@ -8,5 +8,9 @@ "dist": "dist/", "packages": "packages/", "public": "public/", - "src": "src/" + "src": "src/", + "ports": { + "app": 3000, + "test": 8888 + } } diff --git a/docs/development-and-publish-tasks.md b/docs/development-and-publish-tasks.md index f9a1de0d87..5bc317eead 100644 --- a/docs/development-and-publish-tasks.md +++ b/docs/development-and-publish-tasks.md @@ -4,7 +4,7 @@ This application used a number of a number of NPM scripts that run the applicati ## Express app only -To simply run the Express app without gulp tasks being triggered, simply run `node app.js`. +To simply run the Express app without gulp tasks being triggered, simply run `node app/start.js`. ## NPM script aliases diff --git a/gulpfile.js b/gulpfile.js index daa9161b0a..96efa31b38 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -78,7 +78,7 @@ gulp.task('dev', cb => { gulp.task('serve', ['watch'], () => { return nodemon({ - script: 'app/app.js' + script: 'app/start.js' }) }) diff --git a/lib/puppeteer/setup.js b/lib/puppeteer/setup.js index 836cd15c69..c66112aa18 100644 --- a/lib/puppeteer/setup.js +++ b/lib/puppeteer/setup.js @@ -4,14 +4,29 @@ const fs = require('fs') const mkdirp = require('mkdirp') const os = require('os') const path = require('path') -const app = require('../../app/app.js') +const app = require('../../app/app.js')({ + nunjucks: { watch: false } +}) + +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test const DIR = path.join(os.tmpdir(), 'jest-puppeteer-global-setup') -module.exports = async function () { - console.log(chalk.green('\nStart server')) - global.__SERVER__ = app.listen(3000) - console.log(chalk.green('Setup Puppeteer'))// +// Jest Setup.js expects promises, using callbacks results in a race condition. +const appListen = (port) => { + return new Promise((resolve) => { + const server = app.listen(port, () => { + resolve(server) + }) + }) +} + +module.exports = async () => { + console.log(chalk.green('\nStart Server')) + global.__SERVER__ = await appListen(PORT) + console.log('Server started at http://localhost:' + PORT) + console.log(chalk.green('Setup Puppeteer')) // we use --no-sandbox --disable-setuid-sandbox as a workaround for the // 'No usable sandbox! Update your kernel' error // see more https://github.com/Googlechrome/puppeteer/issues/290 diff --git a/package.json b/package.json index 51dee71b16..01996f7fa9 100644 --- a/package.json +++ b/package.json @@ -15,15 +15,15 @@ "homepage": "https://github.com/alphagov/govuk-frontend#readme", "scripts": { "start": "gulp dev", - "heroku": "gulp copy-assets && node app/app.js", + "heroku": "gulp copy-assets && node app/start.js", "pre-release": "node bin/check-nvmrc.js && ./bin/pre-release.sh", "release": "node bin/check-nvmrc.js && ./bin/release.sh", "build:packages": "node bin/check-nvmrc.js && gulp build:packages --destination 'packages' && ./bin/check-and-create-package-json.sh && npm run test:build:packages", "build:dist": "node bin/check-nvmrc.js && gulp build:dist --destination 'dist' && npm run test:build:dist", "test": "standard && gulp test && npm run test:app && npm run test:components && npm run test:generate:readme", - "test:app": "jest app/__tests__/app.test.js --forceExit # express server fails to end process", - "test:components": "gulp copy-assets && jest src/ --forceExit && jest tasks/gulp/__tests__/check-individual-components-compile.test.js --forceExit", - "test:generate:readme": "jest tasks/gulp/__tests__/check-generate-readme.test.js --forceExit", + "test:app": "jest app/__tests__/app.test.js", + "test:components": "gulp copy-assets && jest src/ && jest tasks/gulp/__tests__/check-individual-components-compile.test.js", + "test:generate:readme": "jest tasks/gulp/__tests__/check-generate-readme.test.js", "test:build:packages": "jest tasks/gulp/__tests__/after-build-packages.test.js", "test:build:dist": "jest tasks/gulp/__tests__/after-build-dist.test.js" }, diff --git a/src/button/button.test.js b/src/button/button.test.js index b0d46b087d..c903901680 100644 --- a/src/button/button.test.js +++ b/src/button/button.test.js @@ -3,9 +3,12 @@ */ /* eslint-env jest */ +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test + let browser let page -let baseUrl = 'http://localhost:3000' +let baseUrl = 'http://localhost:' + PORT beforeAll(async (done) => { browser = global.__BROWSER__ diff --git a/src/details/details.test.js b/src/details/details.test.js index 485a883d66..0bf7038d1e 100644 --- a/src/details/details.test.js +++ b/src/details/details.test.js @@ -3,9 +3,12 @@ */ /* eslint-env jest */ +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test + let browser let page -let baseUrl = 'http://localhost:3000' +let baseUrl = 'http://localhost:' + PORT beforeAll(async (done) => { browser = global.__BROWSER__