diff --git a/app/__tests__/app.test.js b/app/__tests__/app.test.js index 2b42f44dc8..3eea084ed0 100644 --- a/app/__tests__/app.test.js +++ b/app/__tests__/app.test.js @@ -3,7 +3,6 @@ const request = require('request') const cheerio = require('cheerio') -const app = require('../app.js') const lib = require('../../lib/file-helper') const requestParamsHomepage = { @@ -63,16 +62,6 @@ const requestParamsExampleTypography = { } describe('frontend app', () => { - let server - - beforeAll(done => { - server = app.listen(3000, done) - }) - - afterAll(done => { - server.close(done) - }) - describe('homepage', () => { it('should resolve with a http status code of 200', done => { request.get(requestParamsHomepage, (err, res) => { diff --git a/lib/puppeteer/environment.js b/lib/puppeteer/environment.js new file mode 100644 index 0000000000..4ce8734e31 --- /dev/null +++ b/lib/puppeteer/environment.js @@ -0,0 +1,33 @@ +const chalk = require('chalk') +const NodeEnvironment = require('jest-environment-node') +const puppeteer = require('puppeteer') +const fs = require('fs') +const os = require('os') +const path = require('path') + +const DIR = path.join(os.tmpdir(), 'jest-puppeteer-global-setup') + +class PuppeteerEnvironment extends NodeEnvironment { + async setup () { + console.log(chalk.yellow('Setup Test Environment.')) + await super.setup() + const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8') + if (!wsEndpoint) { + throw new Error('wsEndpoint not found') + } + this.global.__BROWSER__ = await puppeteer.connect({ + browserWSEndpoint: wsEndpoint + }) + } + + async teardown () { + console.log(chalk.yellow('Teardown Test Environment.')) + await super.teardown() + } + + runScript (script) { + return super.runScript(script) + } +} + +module.exports = PuppeteerEnvironment diff --git a/lib/puppeteer/setup.js b/lib/puppeteer/setup.js new file mode 100644 index 0000000000..836cd15c69 --- /dev/null +++ b/lib/puppeteer/setup.js @@ -0,0 +1,22 @@ +const chalk = require('chalk') +const puppeteer = require('puppeteer') +const fs = require('fs') +const mkdirp = require('mkdirp') +const os = require('os') +const path = require('path') +const app = require('../../app/app.js') + +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'))// + // 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 + const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}) + global.__BROWSER__ = browser + mkdirp.sync(DIR) + fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint()) +} diff --git a/lib/puppeteer/teardown.js b/lib/puppeteer/teardown.js new file mode 100644 index 0000000000..659a92c448 --- /dev/null +++ b/lib/puppeteer/teardown.js @@ -0,0 +1,14 @@ +const chalk = require('chalk') +const rimraf = require('rimraf') +const os = require('os') +const path = require('path') + +const DIR = path.join(os.tmpdir(), 'jest-puppeteer-global-setup') + +module.exports = async function () { + console.log(chalk.green('Teardown Puppeteer')) + await global.__BROWSER__.close() + console.log(chalk.green('Close server')) + await global.__SERVER__.close() + rimraf.sync(DIR) +} diff --git a/package-lock.json b/package-lock.json index 0592e5b2a4..a035676cb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,15 @@ "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", "dev": true }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "5.0.0" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -2779,6 +2788,21 @@ "event-emitter": "0.3.5" } }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "4.2.4" + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -3449,6 +3473,29 @@ "to-regex": "3.0.1" } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -3493,6 +3540,15 @@ "bser": "2.0.0" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, "figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", @@ -6181,6 +6237,27 @@ "sshpk": "1.13.1" } }, + "https-proxy-agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.0.tgz", + "integrity": "sha512-uUWcfXHvy/dwfM9bqa6AozvAjS32dZSTUYd/4SEpYKRg6LEcPLshksnQYRudM9AyNvUARMfAg5TLjUDyX/K4vA==", + "dev": true, + "requires": { + "agent-base": "4.2.0", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -10491,6 +10568,12 @@ "through": "2.3.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -13355,6 +13438,12 @@ "ipaddr.js": "1.5.2" } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, "ps-tree": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", @@ -13385,6 +13474,41 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, + "puppeteer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.1.1.tgz", + "integrity": "sha1-rb8l5J9e8DRDwQq44JqVTKDHv+4=", + "dev": true, + "requires": { + "debug": "2.6.9", + "extract-zip": "1.6.6", + "https-proxy-agent": "2.2.0", + "mime": "1.4.1", + "progress": "2.0.0", + "proxy-from-env": "1.0.0", + "rimraf": "2.6.2", + "ws": "3.3.3" + }, + "dependencies": { + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + } + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -16571,6 +16695,15 @@ "dev": true } } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } } } } diff --git a/package.json b/package.json index 3c6f4f82a4..51dee71b16 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "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": "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: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:build:packages": "jest tasks/gulp/__tests__/after-build-packages.test.js", "test:build:dist": "jest tasks/gulp/__tests__/after-build-dist.test.js" }, @@ -62,6 +62,7 @@ "oldie": "^1.3.0", "postcss-normalize": "^3.0.0", "postcss-pseudo-classes": "^0.2.0", + "puppeteer": "^1.1.1", "request": "^2.83.0", "run-sequence": "^2.2.0", "standard": "^10.0.2", @@ -86,6 +87,8 @@ "setupTestFrameworkScriptFile": "./config/jest-setup.js", "snapshotSerializers": [ "jest-serializer-html" - ] + ], + "globalSetup": "./lib/puppeteer/setup.js", + "globalTeardown": "./lib/puppeteer/teardown.js" } } diff --git a/src/button/button.js b/src/button/button.js new file mode 100644 index 0000000000..101ace8eef --- /dev/null +++ b/src/button/button.js @@ -0,0 +1,95 @@ +/** + * JavaScript 'shim' to trigger the click event of element(s) when the space key is pressed. + * + * Created since some Assistive Technologies (for example some Screenreaders) + * will tell a user to press space on a 'button', so this functionality needs to be shimmed + * See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270 + * + * Usage instructions: + * the 'shim' will be automatically initialised + */ +;(function (global) { + 'use strict' + + var GOVUK_FRONTEND = global.GOVUK_FRONTEND || {} + var KEY_SPACE = 32 + + GOVUK_FRONTEND.buttons = { + + /** + * Add event construct for modern browsers or IE8 + * which fires the callback with a pre-converted target reference + * @param {object} node element + * @param {string} type event type (e.g. click, load, or error) + * @param {function} callback function + */ + addEvent: function (node, type, callback) { + // Support: IE9+ and other browsers + if (node.addEventListener) { + node.addEventListener(type, function (e) { + callback(e, e.target) + }, false) + // Support: IE8 + } else if (node.attachEvent) { + node.attachEvent('on' + type, function (e) { + callback(e, e.srcElement) + }) + } + }, + + /** + * Cross-browser character code / key pressed + * @param {object} e event + * @returns {number} character code + */ + charCode: function (e) { + return (typeof e.which === 'number') ? e.which : e.keyCode + }, + + /** + * Cross-browser preventing default action + * @param {object} e event + */ + preventDefault: function (e) { + // Support: IE9+ and other browsers + if (e.preventDefault) { + e.preventDefault() + // Support: IE8 + } else { + e.returnValue = false + } + }, + + /** + * Add event handler + * if the event target element has a role='button' and the event is key space pressed + * then it prevents the default event and triggers a click event + * @param {object} e event + */ + eventHandler: function (e) { + // get the target element + var target = e.target || e.srcElement + // if the element has a role='button' and the pressed key is a space, we'll simulate a click + if (target.getAttribute('role') === 'button' && GOVUK_FRONTEND.buttons.charCode(e) === KEY_SPACE) { + GOVUK_FRONTEND.buttons.preventDefault(e) + // trigger the target's click event + target.click() + } + }, + + /** + * Initialise an event listener for keydown at document level + * this will help listening for later inserted elements with a role="button" + */ + init: function () { + GOVUK_FRONTEND.buttons.addEvent(document, 'keydown', GOVUK_FRONTEND.buttons.eventHandler) + } + + } + + // hand back to global + global.GOVUK_FRONTEND = GOVUK_FRONTEND + + // auto-initialise + GOVUK_FRONTEND.buttons.init() +})(window) diff --git a/src/button/button.test.js b/src/button/button.test.js new file mode 100644 index 0000000000..b0d46b087d --- /dev/null +++ b/src/button/button.test.js @@ -0,0 +1,42 @@ +/** + * @jest-environment ./lib/puppeteer/environment.js + */ +/* eslint-env jest */ + +let browser +let page +let baseUrl = 'http://localhost:3000' + +beforeAll(async (done) => { + browser = global.__BROWSER__ + page = await browser.newPage() + done() +}) + +afterAll(async (done) => { + await page.close() + done() +}) + +describe('/components/button', () => { + describe('/components/button/link', () => { + it('triggers the click event when the space key is pressed', async () => { + await page.goto(baseUrl + '/components/button/link/preview', { waitUntil: 'load' }) + + const href = await page.evaluate(() => document.body.getElementsByTagName('a')[0].getAttribute('href')) + + await page.focus('a[role="button"]') + + // we need to start the waitForNavigation() before the keyboard action + // so we return a promise that is fulfilled when the navigation and the keyboard action are respectively fulfilled + // this is somewhat counter intuitive, as we humans expect to act and then wait for something + await Promise.all([ + page.waitForNavigation(), + page.keyboard.press('Space') + ]) + + const url = await page.url() + expect(url).toBe(baseUrl + href) + }) + }) +})