diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..00f195a4f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# dont include the following files in the zip +.gitattributes export-ignore +README.md export-ignore +CHANGELOG.md export-ignore +CONTRIBUTING.md export-ignore +create-release.sh export-ignore +travis.yml export-ignore +.travis export-ignore +app.json export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index f2b7a43bdb..022498ba1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # ignore tracking usage data config - we want a different one for each user usage-data-config.json +# ignore extension SCSS because it's dynamically generated +lib/extensions/_extensions.scss .env .sass-cache .DS_Store diff --git a/.travis.yml b/.travis.yml index 2c421e9528..4c56fd1bb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,37 @@ language: node_js + node_js: -- "lts/*" -before_deploy: -- test $TRAVIS_TEST_RESULT = 0 -before_install: -- npm install -g mocha -deploy: -- provider: script # (If version.txt is updated) - create a new tag and push to Github, update the latest-release branch - script: ./create-release.sh - on: master - # Automatic deploys are enabled in Heroku for this app - # Every push to master will deploy a new version of this app. Deploys happen automatically. - # Heroku will wait for CI to pass before deploying. + - "lts/*" + - "8" + +os: + - linux + - osx + - windows + +jobs: + include: + # If version.txt is updated on the master branch: + # - create a new tag and push it to Github + # - update the latest-release branch + - stage: deploy + name: "Create release on GitHub 🚀" + os: linux + script: echo "Checking whether we should release..." + deploy: + - provider: script + script: ./create-release.sh + on: + branch: master + env: + # DEPLOY_KEY + - secure: qpuH/3v+wsMLy9C3bGjallB6KxFQAtDlyZf3jmX8UQ0703tIvBnZoC1h9EJXOqxZbirLPHon7g902nOuhqUfrsPQgV7XEmXMGZsp31PuK/1lkyTCG3fAdeg8wcghwZn10Gw8jYZpOfWIf2GPDL+6utIU6Aog+odcls7tHWLVT80= + +# Automatic deploys are also enabled in Heroku for this app +# Every push to master will automatically deploy a new version of this app. +# Heroku will wait for CI to pass before deploying. + notifications: email: false + sudo: false -env: - global: - - secure: esj3ut+TwKX25QzU1koJyTVIX+x1V55graI65X8LiYXKcOP040VPJ9lIC0HeRs5UViBM0Ur1F93rGdgW2zYVzu5MLZoH7KjpzMcG30x+FDqLChT0JfNcLLtLm/wIhx/OAAmsBxXlvjPp092BeEKd3Jug8d329TxwwK5G5XxAatg= diff --git a/.travis/README.md b/.travis/README.md index f81b54c6d1..c585c90e48 100644 --- a/.travis/README.md +++ b/.travis/README.md @@ -1,18 +1,48 @@ # Travis encrypted files -This directory contains a public/private keypair generated just for this repository. +This directory contains an encrypted private deploy key with write access to the +Prototype Kit repository. -The public key is a deploy key which has been added to the GitHub repo for push access. +It has been encrypted using a key stored in the DEPLOY_KEY environment +variable, which is itself encrypted using `travis encrypt`. -The private key is encrypted using `travis encrypt-file` and then committed to this repo. +The deploy key is decrypted in create-release.sh. -The decrypt commands are in `.travis.yml`. +To update the key: -To regenerate a key: +1. Generate a new keypair using ssh-keygen + + ``` + ssh-keygen -b 4096 -f .travis/prototype-kit-deploy-key + ``` -``` -ssh-keygen -b 4096 -f .travis/govuk_prototype_kit # Make a new keypair -travis encrypt-file .travis/govuk_prototype_kit # Encrypt the private key -mv govuk_prototype_kit.enc .travis/ # Move the private key to the right place -rm .travis/govuk_prototype_kit # Remove the unencrypted private key -``` +2. Add the *public* key as a new [deploy key], with write access to the + repository + + ``` + cat .travis/prototype-kit-deploy-key.pub + ``` + +2. Generate a new random string which we can use as an encryption key + +3. Encrypt the private key using ssh-keygen + + ``` + openssl aes-256-cbc -k [encryption key here] \ + -in prototype-kit-deploy-key \ + -out prototype-kit-deploy-key.enc + ``` + +4. Remove the unencrypted private key and the public key + +5. Encrypt the private key using the encryption key + + ``` + travis encrypt DEPLOY_KEY=[encryption key] + ``` + +6. Add the encrypted variable to the environment variables for the deploy job + in .travis.yml + + +[deploy key]: https://github.com/alphagov/govuk-prototype-kit/settings/keys diff --git a/.travis/govuk_prototype_kit.enc b/.travis/govuk_prototype_kit.enc deleted file mode 100644 index 328d986903..0000000000 Binary files a/.travis/govuk_prototype_kit.enc and /dev/null differ diff --git a/.travis/govuk_prototype_kit.pub b/.travis/govuk_prototype_kit.pub deleted file mode 100644 index 0c934e9428..0000000000 --- a/.travis/govuk_prototype_kit.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDqbULxuN/WRr4EH3opb5e1K+VSR3ZlJagYS/D1JP7wrRA0drjs3ioR6FcxmHGrv3gAIeRPhwQJpqHC8jHpdNCsXc8IjzRGpB78CdV1LRI8XrBJO4FBHjp+w5uzrKM38fnRyR50R4iuXIUFQUh7JFCGd0URzoCljc/ts3ZWF6XDnSUK0zu/JhPjLuSGDsZxay4eZxR3pkPKEM9zznkn33m1/KzN8g/8JncnPinAucVCGHlZ5d/gggnrgVESob50MDMDy3AZI4fDwltlLVlZG5XLfi5uIou73mG8pDrGISiEz4xvA4aYgaKvng/3SrV5AzEV2O2N43c+1bc2T7E4NTl7H0QDn6lqxN9K2dtOK999J7s0xtmFe8yQZht1EWLWJQ462pCLRz8aGK6THbX4Mb/fOVjKj5OerOr+BJFwRXGr7ielXSLiPscjoLr3Ey7WYb5a7Bp3eiAtK/6BkXTGUcq0J2OMRA4rvdvvlCcL1q0oXORHhLqD87Mf+v0n9pGS4d0z7nlVFHC7i+eIiO+D2AV3/kU3awAcnVE487SYn+tUsPRYjXxQ033mZFAWqmYdzX1bT5fT0iRRcFuO2n0b0olUrlEKY3T+eMszIkE1NQPdZFuhJolk+mgaw70k1W5MPaVFXbXX19uUwqdoUZUvzvfeSQi0pGKNtr7DmAkg/r3PLw== gemmaleigh@gemma-mbp.local diff --git a/.travis/prototype-kit-deploy-key.enc b/.travis/prototype-kit-deploy-key.enc new file mode 100644 index 0000000000..c20c6f718f Binary files /dev/null and b/.travis/prototype-kit-deploy-key.enc differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0336547782..567f4eb471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,81 @@ +# 8.7.0 + +Features: +- [#613 Update to GOV.UK Frontend version 2.7.0 and adds experimental extensions feature](https://github.com/alphagov/govuk-prototype-kit/pull/613) (See GOV.UK Frontend 2.7.0 [release notes](https://github.com/alphagov/govuk-frontend/releases/tag/v2.7.0)). Big thanks @matcarey (https://github.com/matcarey) + As this is an **experimental** feature it should be used at your own risk, and is likely to change. Please contact us if you're interested in trying it out. + +- [#687 Update docs and package.json to Node 10 LTS](https://github.com/alphagov/govuk-prototype-kit/pull/687) + +- [#683 add guidance for CSS, JavaScript and images](https://github.com/alphagov/govuk-prototype-kit/pull/683) + +# 8.6.0 + +Features: +- [#680 Update to GOV.UK Frontend version 2.6.0](https://github.com/alphagov/govuk-prototype-kit/pull/680) (See GOV.UK Frontend 2.6.0 [release notes](https://github.com/alphagov/govuk-frontend/releases/tag/v2.6.0)) + +# 8.5.0 + +Features: +- [#672 Replace ‘check answers’ pattern with updated code](https://github.com/alphagov/govuk-prototype-kit/pull/672) +- [#671 Update to GOV.UK Frontend version 2.5.0](https://github.com/alphagov/govuk-prototype-kit/pull/671) + Allows use of new components Accordion and Summary List + +Fixes: + +- [#667 Add acorn dependency to fix npm warning](https://github.com/alphagov/govuk-prototype-kit/pull/667) +- [#647 Fix link context in step-by-step templates](https://github.com/alphagov/govuk-prototype-kit/pull/647) + +Internal: + +- [#663 update Standard to 12.0.1](https://github.com/alphagov/govuk-prototype-kit/pull/663) +- [#640 Replace Mocha with Jest](https://github.com/alphagov/govuk-prototype-kit/pull/640) +- [#659 Upgrade kit to use Gulp 4](https://github.com/alphagov/govuk-prototype-kit/pull/659) +- [#664 Remove deprecated gulp-util](https://github.com/alphagov/govuk-prototype-kit/pull/664) + +# 8.4.0 + +New features: + +- [#642 Update GOV.UK Frontend to v2.4.0](https://github.com/alphagov/govuk-prototype-kit/pull/642) + +Bug fixes: + +- [#634 Avoid double-nested buttons in step-by-step navigation](https://github.com/alphagov/govuk-prototype-kit/pull/634) + +- [#638 Make unbranded template available for use in app/views](https://github.com/alphagov/govuk-prototype-kit/pull/638) + +# 8.3.0 + +New features: + +- [#628 Update GOV.UK Frontend to v2.3.0](https://github.com/alphagov/govuk-prototype-kit/pull/628) + +- [#574 Add Notify integration guidance](https://github.com/alphagov/govuk-prototype-kit/pull/574) + +- [Add npm install reminder when prototype crashes](https://github.com/alphagov/govuk-prototype-kit/pull/598) + +- [#539 Add step by step navigation](https://github.com/alphagov/govuk-prototype-kit/pull/539) + +# 8.2.0 + +New Features: + +- [#609 Update GOV.UK Frontend to v2.2.0](https://github.com/alphagov/govuk-prototype-kit/pull/609) + +Also includes a new character-count component + +Bug fixes: + +- [#605 Set stylesheet media to "all" to allow print styles](https://github.com/alphagov/govuk-prototype-kit/pull/605) + +- [#608 Clearing session data now uses a POST request rather than a destructive GET request](https://github.com/alphagov/govuk-prototype-kit/pull/608) + +# 8.1.0 + +New features: + +- [#600 Update GOV.UK Frontend to v2.1.0](https://github.com/alphagov/govuk-prototype-kit/pull/600) + # 8.0.0 Breaking changes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab8b1ecdf2..1b6b1358c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,12 +3,13 @@ We really like contributions and bug reports, in fact the project wouldn't have got to this stage without them. We do have a few guidelines to bear in mind. -## Community +## Contributing -We have two Slack channels for the Prototype Kit. You'll need a government email address to join them. +If you’ve got an idea or suggestion you can: -* [Slack channel for users of the Prototype Kit](https://ukgovernmentdigital.slack.com/messages/prototype-kit/) -* [Slack channel for developers of the Prototype Kit](https://ukgovernmentdigital.slack.com/messages/prototype-kit-dev/) +* email [govuk-design-system-support@digital.cabinet-office.gov.uk](mailto:govuk-design-system-support@digital.cabinet-office.gov.uk) +* [get in touch on developer Slack channel](https://ukgovernmentdigital.slack.com/messages/prototype-kit-dev)([open in app](slack://channel?team=T04V6EBTR&id=C0E1063DW)) +* [create a GitHub issue](https://github.com/alphagov/govuk-prototype-kit/issues) ## Raising bugs @@ -51,16 +52,43 @@ which describes how we prefer git history and commit messages to read. ## To release a new version -Checkout a new branch for the release. +1. Checkout master and pull latest changes. -Update [CHANGELOG.md](https://github.com/alphagov/govuk-prototype-kit/blob/master/CHANGELOG.md) to summarise the changes made since the last release. +2. Decide on a new version number. Do this by looking at the [current "Unreleased" CHANGELOG](../../CHANGELOG.md) changes and updating the previous release number depending on the kind of entries: -To see the commits to be summarised in the changelog since the last release, [compare the latest-release branch with master](https://github.com/alphagov/govuk-prototype-kit/compare/latest-release...master). +When you fix a bug and your code stays backwards compatible, increase the patch component: -Propose a new version number in [VERSION.txt](https://github.com/alphagov/govuk-prototype-kit/blob/master/VERSION.txt) and update line 4 in [package.json](https://github.com/alphagov/govuk-prototype-kit/blob/master/package.json#L4) with the new version number. +``` +v0.0.3 // Before bugfix +v0.0.4 // After bugfix +``` -Open a new pull request with a single commit including the above changes. +When you add functionality and your code stays backwards compatible, increase the minor component and reset the patch component to zero: -[Here is an example for v6.1.0](https://github.com/alphagov/govuk-prototype-kit/commit/53e36d79a994ce3649b53f4008370cd75068c27c). +``` +v0.2.4 // Before addition of new functionality +v0.3.0 // After addition of new functionality +``` -Once merged into master a new version will be built. +When you implement changes and your code becomes backwards incompatible, increase the major component and reset the minor and patch components to zero: + +``` +v7.3.5 // Before implementing backwards incompatible changes +v8.0.0 // After implementing backwards incompatible changes +``` + +(From [jvandemo.com](https://www.jvandemo.com/a-simple-guide-to-semantic-versioning/)) + +3. Checkout a new branch called release-[new version number]. + +4. Update the version number in [VERSION.txt](https://github.com/alphagov/govuk-prototype-kit/blob/master/VERSION.txt) and update "version" in [package.json](https://github.com/alphagov/govuk-prototype-kit/blob/master/package.json#L4). + +5. Commit your changes and open a new pull request on GitHub - copy the relevant Changelog section into the description. + +6. Once someone has merged the pull request, [draft a new release on GitHub](https://github.com/alphagov/govuk-prototype-kit/releases) + +7. In Tag version and Release title, put v[version number], for example `v7.0.0`. + +8. In description, paste the relevant section from the Changelog. + +9. Click 'Publish release'. diff --git a/Procfile b/Procfile index 1b9f7acc84..08938d90e6 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node ./node_modules/gulp/bin/gulp generate-assets && node server.js +web: node ./node_modules/gulp/bin/gulp generate-assets && node listen-on-port.js diff --git a/README.md b/README.md index 27fee0d964..8c2b0b906b 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,17 @@ You must protect user privacy at all times, even when using prototypes. Prototyp - [Installation guide for new users (non technical)](https://govuk-prototype-kit.herokuapp.com/docs/install/introduction) - [Installation guide for developers (technical)](https://govuk-prototype-kit.herokuapp.com/docs/install/developer-install-instructions) -## Community +## Support -We have two Slack channels for the Prototype Kit. You'll need a government email address to join them. +The GOV.UK Prototype Kit is maintained by the Government Digital Service. If you’ve got a question or need support you can: -* [Slack channel for users of the Prototype Kit](https://ukgovernmentdigital.slack.com/messages/prototype-kit/) -* [Slack channel for developers of the Prototype Kit](https://ukgovernmentdigital.slack.com/messages/prototype-kit-dev/) +* email [govuk-design-system-support@digital.cabinet-office.gov.uk](mailto:govuk-design-system-support@digital.cabinet-office.gov.uk) +* [get in touch on Slack](https://ukgovernmentdigital.slack.com/messages/prototype-kit)([open in app](slack://channel?team=T04V6EBTR&id=C0647LW4R)) +* [view known issues on GitHub](https://github.com/alphagov/govuk-prototype-kit/issues) + +## Contributing + +If you’ve got an idea or suggestion you can: + +* [get in touch on the developer Slack channel](https://ukgovernmentdigital.slack.com/messages/prototype-kit-dev)([open in app](slack://channel?team=T04V6EBTR&id=C0E1063DW)) +* [create a GitHub issue](https://github.com/alphagov/govuk-prototype-kit/issues) diff --git a/VERSION.txt b/VERSION.txt index ae9a76b924..df5119ec64 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -8.0.0 +8.7.0 diff --git a/__tests__/spec/force-https-redirect.js b/__tests__/spec/force-https-redirect.js new file mode 100644 index 0000000000..7a2e2233df --- /dev/null +++ b/__tests__/spec/force-https-redirect.js @@ -0,0 +1,27 @@ +/* eslint-env jest */ +const request = require('supertest') +let app + +/* Setup Environment Variables before setting App */ +process.env.NODE_ENV = 'production' +process.env.USE_HTTPS = 'true' +app = require('../../server.js') + +describe('The Prototype Kit - force HTTPS redirect functionality', () => { + describe('should in a production environment', () => { + it('have HTTP header "location" field that begins with https', async () => { + const response = await request(app).get('/docs') + expect(response.header['location'].startsWith('https://')).toBeTruthy() + }) + + it('redirect to the same HTTPS url', async () => { + const response = await request(app).get('/docs') + expect(response.header['location'].endsWith('/docs')).toBeTruthy() + }) + + it('have HTTP header "status" field that indicates a redirect', async () => { + const response = await request(app).get('/docs') + expect(response.statusCode).toBe(302) + }) + }) +}) diff --git a/__tests__/spec/sanity-checks.js b/__tests__/spec/sanity-checks.js new file mode 100644 index 0000000000..7536039a97 --- /dev/null +++ b/__tests__/spec/sanity-checks.js @@ -0,0 +1,108 @@ +/* eslint-env jest */ +var request = require('supertest') +var app = require('../../server.js') +var path = require('path') +var fs = require('fs') +var assert = require('assert') + +function readFile (pathFromRoot) { + return fs.readFileSync(path.join(__dirname, '../../' + pathFromRoot), 'utf8') +} +/** + * Basic sanity checks on the dev server + */ +describe('The Prototype Kit', () => { + it('should generate assets into the /public folder', () => { + assert.doesNotThrow(function () { + fs.accessSync(path.resolve(__dirname, '../../public/javascripts/application.js')) + fs.accessSync(path.resolve(__dirname, '../../public/images/unbranded.ico')) + fs.accessSync(path.resolve(__dirname, '../../public/stylesheets/application.css')) + }) + }) + + describe('index page', () => { + it('should send a well formed response', async () => { + const response = await request(app).get('/') + expect(response.statusCode).toBe(200) + }) + + it('should return html file', async () => { + const response = await request(app).get('/') + expect(response.type).toBe('text/html') + }) + }) + + describe('docs index page', () => { + it('should send a well formed response', async () => { + const response = await request(app).get('/docs') + expect(response.statusCode).toBe(200) + }) + + it('should return html file', async () => { + const response = await request(app).get('/docs') + expect(response.type).toBe('text/html') + }) + }) + + describe('extensions', () => { + it('should allow known assets to be loaded from node_modules', (done) => { + request(app) + .get('/extension-assets/govuk-frontend/all.js') + .expect('Content-Type', /application\/javascript; charset=UTF-8/) + .expect(200) + .end(function (err, res) { + if (err) { + done(err) + } else { + assert.strictEqual('' + res.text, readFile('node_modules/govuk-frontend/all.js')) + done() + } + }) + }) + + it('should allow known assets to be loaded from node_modules', (done) => { + request(app) + .get('/assets/images/favicon.ico') + .expect('Content-Type', /image\/x-icon/) + .expect(200) + .end(function (err, res) { + if (err) { + done(err) + } else { + assert.strictEqual('' + res.body, readFile('node_modules/govuk-frontend/assets/images/favicon.ico')) + done() + } + }) + }) + + it('should not expose everything', function (done) { + request(app) + .get('/assets/common.js') + .expect(404) + .end(function (err, res) { + if (err) { + done(err) + } else { + done() + } + }) + }) + + describe('misconfigured prototype kit - while upgrading kit developer did not copy over changes in /app folder', () => { + it('should still allow known assets to be loaded from node_modules', (done) => { + request(app) + .get('/node_modules/govuk-frontend/all.js') + .expect('Content-Type', /application\/javascript; charset=UTF-8/) + .expect(200) + .end(function (err, res) { + if (err) { + done(err) + } else { + assert.strictEqual('' + res.text, readFile('node_modules/govuk-frontend/all.js')) + done() + } + }) + }) + }) + }) +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000000..a85e811f33 --- /dev/null +++ b/app.json @@ -0,0 +1,21 @@ +{ + "name": "govuk-prototype-kit", + "scripts": {}, + "env": { + "USE_AUTH": { + "required": true + } + }, + "formation": { + "web": { + "quantity": 1 + } + }, + "addons": [], + "buildpacks": [ + { + "url": "heroku/nodejs" + } + ] + } + \ No newline at end of file diff --git a/app/assets/javascripts/step-by-step-navigation.js b/app/assets/javascripts/step-by-step-navigation.js new file mode 100644 index 0000000000..7408a04249 --- /dev/null +++ b/app/assets/javascripts/step-by-step-navigation.js @@ -0,0 +1,357 @@ +/* global $, GOVUK, history, sessionStorage */ + +// based on https://github.com/alphagov/govuk_publishing_components/blob/v9.3.6/app/assets/javascripts/govuk_publishing_components/components/step-by-step-nav.js + +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {} +window.GOVUK.support = window.GOVUK.support || {} + +window.GOVUK.support.history = function () { + return window.history && window.history.pushState && window.history.replaceState +} + +window.GOVUK.getCurrentLocation = function () { + return window.location +}; + +(function (Modules) { + 'use strict' + + Modules.StepByStepNavigation = function () { + var actions = {} // stores text for JS appended elements 'show' and 'hide' on steps, and 'show/hide all' button + var rememberShownStep = false + var sessionStoreLink = 'govuk-step-nav-active-link' + var activeLinkClass = 'app-step-nav__list-item--active' + var activeLinkHref = '#content' + + this.start = function ($element) { + $(window).unload(storeScrollPosition) + + // Indicate that js has worked + $element.addClass('app-step-nav--active') + + // Prevent FOUC, remove class hiding content + $element.removeClass('js-hidden') + + rememberShownStep = !!$element.filter('[data-remember]').length + var $steps = $element.find('.js-step') + var $stepHeaders = $element.find('.js-toggle-panel') + var totalSteps = $element.find('.js-panel').length + + var $showOrHideAllButton + + getTextForInsertedElements() + addButtonstoSteps() + addShowHideAllButton() + addShowHideToggle() + addAriaControlsAttrForShowHideAllButton() + + hideAllSteps() + showLinkedStep() + ensureOnlyOneActiveLink() + + bindToggleForSteps() + bindToggleShowHideAllButton() + bindComponentLinkClicks() + + function getTextForInsertedElements () { + actions.showText = $element.attr('data-show-text') + actions.hideText = $element.attr('data-hide-text') + actions.showAllText = $element.attr('data-show-all-text') + actions.hideAllText = $element.attr('data-hide-all-text') + } + + // When navigating back in browser history to the step nav, the browser will try to be "clever" and return + // the user to their previous scroll position. However, since we collapse all but the currently-anchored + // step, the content length changes and the user is returned to the wrong position (often the footer). + // In order to correct this behaviour, as the user leaves the page, we anticipate the correct height we wish the + // user to return to by forcibly scrolling them to that height, which becomes the height the browser will return + // them to. + // If we can't find an element to return them to, then reset the scroll to the top of the page. This handles + // the case where the user has expanded all steps, so they are not returned to a particular step, but + // still could have scrolled a long way down the page. + function storeScrollPosition () { + hideAllSteps() + var $step = getStepForAnchor() + + document.body.scrollTop = $step && $step.length + ? $step.offset().top + : 0 + } + + function addShowHideAllButton () { + $element.prepend('
') + } + + function addShowHideToggle () { + $stepHeaders.each(function () { + var linkText = actions.showText + + if (headerIsOpen($(this))) { + linkText = actions.hideText + } + if (!$(this).find('.js-toggle-link').length) { + $(this).find('.js-step-title-button').append('') + } + }) + } + + function headerIsOpen ($stepHeader) { + return (typeof $stepHeader.closest('.js-step').data('show') !== 'undefined') + } + + function addAriaControlsAttrForShowHideAllButton () { + var ariaControlsValue = $element.find('.js-panel').first().attr('id') + + $showOrHideAllButton = $element.find('.js-step-controls-button') + $showOrHideAllButton.attr('aria-controls', ariaControlsValue) + } + + function hideAllSteps () { + setAllStepsShownState(false) + } + + function setAllStepsShownState (isShown) { + $.each($steps, function () { + var stepView = new StepView($(this)) + stepView.preventHashUpdate() + stepView.setIsShown(isShown) + }) + } + + function showLinkedStep () { + var $step + if (rememberShownStep) { + $step = getStepForAnchor() + } else { + $step = $steps.filter('[data-show]') + } + + if ($step && $step.length) { + var stepView = new StepView($step) + stepView.show() + } + } + + function getStepForAnchor () { + var anchor = getActiveAnchor() + + return anchor.length + ? $element.find('#' + escapeSelector(anchor.substr(1))) + : null + } + + function getActiveAnchor () { + return GOVUK.getCurrentLocation().hash + } + + function addButtonstoSteps () { + $.each($steps, function () { + var $step = $(this) + var $title = $step.find('.js-step-title') + var contentId = $step.find('.js-panel').first().attr('id') + + $title.wrapInner( + '' + ) + + $title.wrapInner( + '' + ) + }) + } + + function bindToggleForSteps () { + $element.find('.js-toggle-panel').click(function (event) { + var $step = $(this).closest('.js-step') + + var stepView = new StepView($step) + stepView.toggle() + + setShowHideAllText() + }) + } + + // tracking click events on links in step content + function bindComponentLinkClicks () { + $element.find('.js-link').click(function (event) { + var thisLinkHref = $(this).attr('href') + + if ($(this).attr('rel') !== 'external') { + saveToSessionStorage(sessionStoreLink, $(this).data('position')) + } + + if (thisLinkHref === activeLinkHref) { + setOnlyThisLinkActive($(this)) + } + }) + } + + function saveToSessionStorage (key, value) { + sessionStorage.setItem(key, value) + } + + function loadFromSessionStorage (key) { + return sessionStorage.getItem(key) + } + + function removeFromSessionStorage (key) { + sessionStorage.removeItem(key) + } + + function setOnlyThisLinkActive (clicked) { + $element.find('.' + activeLinkClass).removeClass(activeLinkClass) + clicked.parent().addClass(activeLinkClass) + } + + function ensureOnlyOneActiveLink () { + var $activeLinks = $element.find('.js-list-item.' + activeLinkClass) + + if ($activeLinks.length <= 1) { + return + } + + var lastClicked = loadFromSessionStorage(sessionStoreLink) + + if (lastClicked) { + removeActiveStateFromAllButCurrent($activeLinks, lastClicked) + removeFromSessionStorage(sessionStoreLink) + } else { + var activeLinkInActiveStep = $element.find('.app-step-nav__step--active').find('.' + activeLinkClass).first() + + if (activeLinkInActiveStep.length) { + $activeLinks.removeClass(activeLinkClass) + activeLinkInActiveStep.addClass(activeLinkClass) + } else { + $activeLinks.slice(1).removeClass(activeLinkClass) + } + } + } + + function removeActiveStateFromAllButCurrent ($links, current) { + $links.each(function () { + if ($(this).find('.js-link').data('position').toString() !== current.toString()) { + $(this).removeClass(activeLinkClass) + } + }) + } + + function bindToggleShowHideAllButton () { + $showOrHideAllButton = $element.find('.js-step-controls-button') + $showOrHideAllButton.on('click', function () { + var shouldshowAll + + if ($showOrHideAllButton.text() === actions.showAllText) { + $showOrHideAllButton.text(actions.hideAllText) + $element.find('.js-toggle-link').text(actions.hideText) + shouldshowAll = true + } else { + $showOrHideAllButton.text(actions.showAllText) + $element.find('.js-toggle-link').text(actions.showText) + shouldshowAll = false + } + + setAllStepsShownState(shouldshowAll) + $showOrHideAllButton.attr('aria-expanded', shouldshowAll) + setShowHideAllText() + setHash(null) + + return false + }) + } + + function setShowHideAllText () { + var shownSteps = $element.find('.step-is-shown').length + // Find out if the number of is-opens == total number of steps + if (shownSteps === totalSteps) { + $showOrHideAllButton.text(actions.hideAllText) + } else { + $showOrHideAllButton.text(actions.showAllText) + } + } + + // Ideally we'd use jQuery.escapeSelector, but this is only available from v3 + // See https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/selector-native.js#L46 + function escapeSelector (s) { + var cssMatcher = /([\x00-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g // eslint-disable-line no-control-regex + return s.replace(cssMatcher, '\\$&') + } + } + + function StepView ($stepElement) { + var $titleLink = $stepElement.find('.js-step-title-button') + var $stepContent = $stepElement.find('.js-panel') + var shouldUpdateHash = rememberShownStep + + this.title = $stepElement.find('.js-step-title-text').text().trim() + this.element = $stepElement + + this.show = show + this.hide = hide + this.toggle = toggle + this.setIsShown = setIsShown + this.isShown = isShown + this.isHidden = isHidden + this.preventHashUpdate = preventHashUpdate + this.numberOfContentItems = numberOfContentItems + + function show () { + setIsShown(true) + } + + function hide () { + setIsShown(false) + } + + function toggle () { + setIsShown(isHidden()) + } + + function setIsShown (isShown) { + $stepElement.toggleClass('step-is-shown', isShown) + $stepContent.toggleClass('js-hidden', !isShown) + $titleLink.attr('aria-expanded', isShown) + $stepElement.find('.js-toggle-link').text(isShown ? actions.hideText : actions.showText) + + if (shouldUpdateHash) { + updateHash($stepElement) + } + } + + function isShown () { + return $stepElement.hasClass('step-is-shown') + } + + function isHidden () { + return !isShown() + } + + function preventHashUpdate () { + shouldUpdateHash = false + } + + function numberOfContentItems () { + return $stepContent.find('.js-link').length + } + } + + function updateHash ($stepElement) { + var stepView = new StepView($stepElement) + var hash = stepView.isShown() && '#' + $stepElement.attr('id') + setHash(hash) + } + + // Sets the hash for the page. If a falsy value is provided, the hash is cleared. + function setHash (hash) { + if (!GOVUK.support.history()) { + return + } + + var newLocation = hash || GOVUK.getCurrentLocation().pathname + history.replaceState({}, '', newLocation) + } + } +})(window.GOVUK.Modules) diff --git a/app/assets/sass/application.scss b/app/assets/sass/application.scss index d54f828b6d..242336389c 100644 --- a/app/assets/sass/application.scss +++ b/app/assets/sass/application.scss @@ -1,11 +1,11 @@ // global styles for and

tags $govuk-global-styles: true; -// Import GOV.UK Frontend -@import "node_modules/govuk-frontend/all"; +// Import GOV.UK Frontend and any extension styles if extensions have been configured +@import "lib/extensions/extensions"; // Patterns that aren't in Frontend -@import "patterns/check-your-answers"; +@import "patterns/step-by-step-navigation"; @import "patterns/task-list"; @import "patterns/related-items"; diff --git a/app/assets/sass/patterns/_check-your-answers.scss b/app/assets/sass/patterns/_check-your-answers.scss deleted file mode 100644 index e3aef13fc9..0000000000 --- a/app/assets/sass/patterns/_check-your-answers.scss +++ /dev/null @@ -1,83 +0,0 @@ -// Recommended - Use these styles for the check your answers pattern -.app-check-your-answers { - @include govuk-font(19); - margin-top: 0; - @include govuk-responsive-margin(9, "bottom"); - @include govuk-media-query($from: desktop) { - display: table; - } -} - -.app-check-your-answers--short { - @include govuk-media-query($from: desktop) { - // to make group of q&a line up horizontally (unless there is just one group) - width: 100%; - // recommended for mostly short questions - .app-check-your-answers__question { - width: 30%; - } - } -} - -.app-check-your-answers--long { - @include govuk-media-query($from: desktop) { - // to make group of q&a line up horizontally (unless there is just one group) - width: 100%; - // recommended for mostly long questions - .app-check-your-answers__question { - width: 50%; - } - } -} - -.app-check-your-answers__contents { - position: relative; - border-bottom: 1px solid $govuk-border-colour; - - @include govuk-media-query($from: desktop) { - display: table-row; - border-bottom-width: 0; - } -} - -.app-check-your-answers__contents:first-child .app-check-your-answers__question, -.app-check-your-answers__contents:first-child .app-check-your-answers__answer, -.app-check-your-answers__contents:first-child .app-check-your-answers__change { - @include govuk-media-query($from: desktop) { - padding-top: 0; - } -} - -.app-check-your-answers__question, -.app-check-your-answers__answer, -.app-check-your-answers__change { - display: block; - margin: 0; - @include govuk-media-query($from: desktop) { - display: table-cell; - border-bottom: 1px solid $govuk-border-colour; - padding: govuk-em(12, 19) govuk-em(20, 19) govuk-em(9, 19) 0; - } -} - -.app-check-your-answers__question { - font-weight: bold; - margin: govuk-em(12, 19) 4em govuk-em(4,19) 0; - // using margin instead of padding because of easier absolutely positioning of .app-check-your-answers__change -} - -.app-check-your-answers__answer { - padding-bottom: govuk-em(9, 19); -} - -.app-check-your-answers__change { - text-align: right; - position: absolute; - top: 0; - right: 0; - - @include govuk-media-query($from: desktop) { - position: static; - padding-right: 0; - } -} diff --git a/app/assets/sass/patterns/_step-by-step-navigation.scss b/app/assets/sass/patterns/_step-by-step-navigation.scss new file mode 100644 index 0000000000..e167ee82d3 --- /dev/null +++ b/app/assets/sass/patterns/_step-by-step-navigation.scss @@ -0,0 +1,568 @@ +// Based on https://github.com/alphagov/govuk_publishing_components/blob/v9.3.6/app/assets/stylesheets/govuk_publishing_components/components/_step-by-step-nav.scss +// Note - this code for prototype purposes only. It is not production code, +// nor an example of the best way to use GOV.UK Frontend + +.app-step-nav-header { + position: relative; + padding: 10px; + background: govuk-colour("grey-4"); + border-top: solid 1px $govuk-border-colour; + border-bottom: solid 1px $govuk-border-colour; + @include govuk-media-query($from: tablet) { + padding: 15px; + } +} + +.app-step-nav-header__part-of { + @include govuk-font(16, $weight: bold) + display: block; + padding-bottom: 0.2em; +} + +.app-step-nav-header__title { + @include govuk-font(24, $weight: bold) +} + +.app-step-nav-related { + border-top: 2px solid $govuk-link-colour; + margin-bottom: 30px; +} + +.app-step-nav-related__heading { + @include govuk-font(19, $weight: bold) + margin-top: 15px; + margin-bottom: 10px; +} + +.app-step-nav-related__links { + @include govuk-font(16, $weight: bold) + margin: 0; + padding: 0; +} + +.app-step-nav-related--singular { + margin-bottom: 13px; + + .app-step-nav-related__heading { + @include govuk-font(19, $weight: bold) + margin-top: 20px; + } + + .app-step-nav-related__pretitle { + margin-bottom: 5px; + } +} + +.app-step-nav-related__pretitle { + display: block; + margin-bottom: 15px; + @include govuk-media-query($from: tablet) { + margin-bottom: 5px; + } +} + +.app-step-nav-related__links { + list-style: none; +} + +.app-step-nav-related__link-item { + margin-top: 15px; + @include govuk-media-query($from: tablet) { + margin-top: 5px; + } +} + +.app-step-nav { + @include govuk-font(19); + margin-bottom: 30px; + &.app-step-nav--large { + @include govuk-media-query($from: tablet) { + margin-bottom: 60px; + } + } +} + +.app-step-nav__controls { + padding: 3px 3px 0 0; + text-align: right; +} + +.app-step-nav__button { + color: $govuk-link-colour; + cursor: pointer; + background: none; + border: 0; + margin: 0; +} + +// removes extra dotted outline from buttons in Firefox +// on focus (standard yellow outline unaffected) +.app-step-nav__button::-moz-focus-inner { + border: 0; +} + +.app-step-nav__button--title { + @include govuk-font(19, $weight: bold) + display: inline-block; + padding: 0; + text-align: left; + color: govuk-colour("black"); + + .app-step-nav--large & { + @include govuk-font(24, $weight: bold) + } + +} + +.app-step-nav__button--controls { + @include govuk-font(14) + position: relative; + z-index: 1; // this and relative position stops focus outline underlap with border of accordion + padding: 0.5em 0; + text-decoration: underline; + + &:hover { + color: $govuk-link-hover-colour; + } + + .app-step-nav--large & { + @include govuk-font(16) + } +} + +.app-step-nav__steps { + padding: 0; + margin: 0; +} + +.app-step-nav__step { + position: relative; + padding-left: 45px; + list-style: none; +} + +.app-step-nav__step:after { + content: ""; + position: absolute; + z-index: 2; + width: 0; + height: 100%; + border-left: solid 2px $govuk-border-colour; + background: #fff; + left: 0; + margin-left: 12px; + top: 15px; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__step { + padding-left: 60px; + } + + .app-step-nav--large .app-step-nav__step:after { + left: 0; + margin-left: 16px; + border-width: 3px; + top: 30px; + } +} + +.app-step-nav__step:last-child:before { + content: ""; + position: absolute; + z-index: 6; + bottom: 0; + left: 0; + margin-left: 6.5px; + width: 13px; + height: 0; + border-bottom: solid 2px $govuk-border-colour; +} + +.app-step-nav__step:last-child:after { + height: -webkit-calc(100% - 15px); + height: calc(100% - 15px); +} + +.app-step-nav__step:last-child .app-step-nav__help:after { + height: 100%; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__step:last-child:before { + margin-left: 8.75px; + width: 17.5px; + border-width: 3px; + } + + .app-step-nav--large .app-step-nav__step:last-child:after { + height: calc(100% - 30px); + } +} + +.app-step-nav__step--active .app-step-nav__circle--number, +.app-step-nav__step--active .app-step-nav__help:after, +.app-step-nav__step--active .app-step-nav__substep:after, +.app-step-nav__step--active:after, +.app-step-nav__step--active:last-child:before { + border-color: govuk-colour("black"); +} + +.app-step-nav__circle { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + z-index: 5; + top: 15px; + left: 0; + width: 26px; + height: 26px; + color: govuk-colour("black"); + background: #fff; + border-radius: 100px; + text-align: center; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__circle { + top: 30px; + width: 35px; + height: 35px; + } +} + +.app-step-nav__circle--number { + @include govuk-font(16, $weight: bold) + line-height: 23px; + border: solid 2px $govuk-border-colour; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav__circle--number { + font-size: 16px; + line-height: 23px; + } +} + +.app-step-nav--large .app-step-nav__circle--number { + @include govuk-font(16, $weight: bold) + line-height: 23px; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__circle--number { + font-size: 19px; + line-height: 30px; + } +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__circle--number { + border-width: 3px; + } +} + +.app-step-nav__circle--logic { + @include govuk-font(16, $weight: bold) + line-height: 28px; +} +@include govuk-media-query($from: tablet) { + .app-step-nav__circle--logic { + font-size: 16px; + line-height: 28px; + } +} + +.app-step-nav--large .app-step-nav__circle--logic { + @include govuk-font(16, $weight: bold) + line-height: 28px; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__circle--logic { + font-size: 19px; + line-height: 34px; + } +} + +.app-step-nav__circle-inner { + float: right; + min-width: 100%; +} + +.app-step-nav__circle-background { + text-shadow: 0 -0.1em 0 #fff, 0.1em 0 0 #fff, 0 0.1em 0 #fff, -0.1em 0 0 #fff; +} + +.app-step-nav__header { + padding: 15px 0; + border-top: solid 2px govuk-colour("grey-3"); +} + +.app-step-nav--active .app-step-nav__header { + cursor: pointer; +} + +.app-step-nav__header:hover .app-step-nav__button, +.app-step-nav__header:hover .app-step-nav__circle { + color: $govuk-link-colour; +} + +.app-step-nav__header:hover .app-step-nav__toggle-link { + text-decoration: underline; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__header { + padding: 30px 0; + } +} + +.app-step-nav__title { + @include govuk-font(19, $weight: bold) + line-height: 1.4; + margin: 0; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav__title { + font-size: 19px; + line-height: 1.4; + } +} + +.app-step-nav--large .app-step-nav__title { + @include govuk-font(19, $weight: bold) + line-height: 1.4; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__title { + font-size: 24px; + line-height: 1.4; + } +} + +.app-step-nav__toggle-link { + @include govuk-font(16) + line-height: 1.2; + display: block; + color: $govuk-link-colour; +} + +.app-step-nav--large .app-step-nav__toggle-link { + @include govuk-font(16) + line-height: 1.2; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__toggle-link { + font-size: 16px; + line-height: 1.2; + } +} + +.app-step-nav__panel { + @include govuk-font(16) + line-height: 1.25; + .js-enabled &.js-hidden { + display: none; + } +} + +@include govuk-media-query($from: tablet) { + .app-step-nav__panel { + font-size: 16px; + } +} + +.app-step-nav--large .app-step-nav__panel { + @include govuk-font(19) +} + +.app-step-nav__heading, +.app-step-nav__paragraph { + padding-bottom: 15px; + margin: 0; + font-size: inherit; +} + +.app-step-nav__heading + .app-step-nav__list, +.app-step-nav__paragraph + .app-step-nav__list { + margin-top: -5px; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__heading + .app-step-nav__list, + .app-step-nav--large .app-step-nav__paragraph + .app-step-nav__list { + margin-top: -15px; + } +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__heading, + .app-step-nav--large .app-step-nav__paragraph { + padding-bottom: 30px; + } +} + +.app-step-nav__heading { + font-weight: bold; +} + +.app-step-nav__list { + padding: 0 0 10px; +} +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__list { + padding-bottom: 20px; + } +} + +.app-step-nav__list--choice { + margin-left: 20px; + list-style: disc; +} + +.app-step-nav__list--choice .app-step-nav__list-item--active:before { + left: -45px; +} +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__list--choice .app-step-nav__list-item--active:before { + left: -80px; + } +} + +.app-step-nav__list-item { + list-style: none; + margin-bottom: 10px; +} + +.app-step-nav__list-item--active { + position: relative; +} + +.app-step-nav__list-item--active:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + content: ""; + position: absolute; + z-index: 5; + top: 0.6em; + left: -45px; + margin-left: 5px; + width: 16px; + height: 16px; + margin-top: -8px; + background: govuk-colour("black"); + border: solid 2px #fff; + border-radius: 100px; +} +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__list-item--active:before { + left: -60px; + margin-left: 9.5px; + } +} + +.app-step-nav__list-item--active .app-step-nav__link { + color: govuk-colour("black"); + text-decoration: none; +} + +.app-step-nav__list-item--active .app-step-nav__link:active, +.app-step-nav__list-item--active .app-step-nav__link:link, +.app-step-nav__list-item--active .app-step-nav__link:visited { + color: govuk-colour("black"); +} + +.app-step-nav__context { + display: inline-block; + font-weight: normal; + color: $govuk-secondary-text-colour; +} + +.app-step-nav__context:before { + content: " \2013\00a0"; +} + +.app-step-nav__help { + position: relative; + padding: 15px 0; + border-top: solid 2px govuk-colour("grey-3"); +} + +.app-step-nav__help:after { + content: ""; + position: absolute; + z-index: 2; + width: 0; + height: 100%; + border-left: dotted 2px $govuk-border-colour; + background: #fff; + left: 0; + margin-left: 12px; + z-index: 3; + top: 0; + left: -45px; + height: calc(100% + 30px); +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__help:after { + left: 0; + margin-left: 16px; + border-width: 3px; + left: -60px; + height: calc(100% + 30px + 15px); + } +} + +.app-step-nav__help-link { + text-decoration: none; + font-weight: bold; +} + +.app-step-nav__help-link:hover { + text-decoration: underline; +} + +.app-step-nav__substep { + position: relative; + padding-top: 15px; + border-top: solid 2px govuk-colour("grey-3"); +} + +.app-step-nav__substep:after { + content: ""; + position: absolute; + z-index: 2; + width: 0; + height: 100%; + border-left: solid 2px $govuk-border-colour; + background: #fff; + left: 0; + margin-left: 12px; + z-index: 3; + top: 0; + left: -45px; +} + +@include govuk-media-query($from: tablet) { + .app-step-nav--large .app-step-nav__substep { + padding-top: 30px; + } + + .app-step-nav--large .app-step-nav__substep:after { + left: 0; + margin-left: 16px; + border-width: 3px; + left: -60px; + } +} + +.app-step-nav__substep--optional:after { + border-left-style: dotted; +} diff --git a/app/assets/sass/unbranded.scss b/app/assets/sass/unbranded.scss index 2f46b65d9f..a64e40d5ee 100644 --- a/app/assets/sass/unbranded.scss +++ b/app/assets/sass/unbranded.scss @@ -1,4 +1,5 @@ @import "node_modules/govuk-frontend/settings/all"; +$govuk-global-styles: true; // Override the govuk-frontend font stack $govuk-font-family: "HelveticaNeue", "Helvetica Neue", "Arial", "Helvetica", sans-serif; diff --git a/app/views/includes/head.html b/app/views/includes/head.html index 88e56ebeee..05fcc71f56 100644 --- a/app/views/includes/head.html +++ b/app/views/includes/head.html @@ -1,2 +1,6 @@ - + + +{% for stylesheetUrl in extensionConfig.stylesheets %} + +{% endfor %} diff --git a/app/views/includes/scripts.html b/app/views/includes/scripts.html index aa86cc5ab9..46aff854ff 100644 --- a/app/views/includes/scripts.html +++ b/app/views/includes/scripts.html @@ -1,6 +1,10 @@ - + +{% for scriptUrl in extensionConfig.scripts %} + +{% endfor %} + {% if useAutoStoreData %} diff --git a/app/views/layout.html b/app/views/layout.html index a4884221cd..e93b0df763 100644 --- a/app/views/layout.html +++ b/app/views/layout.html @@ -1,27 +1,30 @@ {% extends "template.njk" %} -{% from "back-link/macro.njk" import govukBackLink %} -{% from "breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% from "button/macro.njk" import govukButton %} -{% from "checkboxes/macro.njk" import govukCheckboxes %} -{% from "date-input/macro.njk" import govukDateInput %} -{% from "details/macro.njk" import govukDetails %} -{% from "error-message/macro.njk" import govukErrorMessage %} -{% from "error-summary/macro.njk" import govukErrorSummary %} -{% from "fieldset/macro.njk" import govukFieldset %} -{% from "file-upload/macro.njk" import govukFileUpload %} -{% from "input/macro.njk" import govukInput %} -{% from "inset-text/macro.njk" import govukInsetText %} -{% from "panel/macro.njk" import govukPanel %} -{% from "phase-banner/macro.njk" import govukPhaseBanner %} -{% from "radios/macro.njk" import govukRadios %} -{% from "select/macro.njk" import govukSelect %} -{% from "skip-link/macro.njk" import govukSkipLink %} -{% from "table/macro.njk" import govukTable %} -{% from "tabs/macro.njk" import govukTabs %} -{% from "tag/macro.njk" import govukTag %} -{% from "textarea/macro.njk" import govukTextarea %} -{% from "warning-text/macro.njk" import govukWarningText %} +{% from "accordion/macro.njk" import govukAccordion %} +{% from "back-link/macro.njk" import govukBackLink %} +{% from "breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "button/macro.njk" import govukButton %} +{% from "character-count/macro.njk" import govukCharacterCount %} +{% from "checkboxes/macro.njk" import govukCheckboxes %} +{% from "date-input/macro.njk" import govukDateInput %} +{% from "details/macro.njk" import govukDetails %} +{% from "error-message/macro.njk" import govukErrorMessage %} +{% from "error-summary/macro.njk" import govukErrorSummary %} +{% from "fieldset/macro.njk" import govukFieldset %} +{% from "file-upload/macro.njk" import govukFileUpload %} +{% from "input/macro.njk" import govukInput %} +{% from "inset-text/macro.njk" import govukInsetText %} +{% from "panel/macro.njk" import govukPanel %} +{% from "phase-banner/macro.njk" import govukPhaseBanner %} +{% from "radios/macro.njk" import govukRadios %} +{% from "select/macro.njk" import govukSelect %} +{% from "skip-link/macro.njk" import govukSkipLink %} +{% from "summary-list/macro.njk" import govukSummaryList %} +{% from "table/macro.njk" import govukTable %} +{% from "tabs/macro.njk" import govukTabs %} +{% from "tag/macro.njk" import govukTag %} +{% from "textarea/macro.njk" import govukTextarea %} +{% from "warning-text/macro.njk" import govukWarningText %} {% block head %} {% include "includes/head.html" %} @@ -52,7 +55,7 @@ text: "GOV.UK Prototype Kit " + releaseVersion }, { - href: "/prototype-admin/clears-data", + href: "/prototype-admin/clear-data", text: "Clear data" } ] diff --git a/app/views/layout_unbranded.html b/app/views/layout_unbranded.html new file mode 100644 index 0000000000..77387dc65f --- /dev/null +++ b/app/views/layout_unbranded.html @@ -0,0 +1,21 @@ +{% extends "template.njk" %} + +{% block headIcons %} + + + + + + +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block header %}{% endblock %} +{% block footer %}{% endblock %} + +{% block bodyEnd %} + {% include "includes/scripts.html" %} +{% endblock %} diff --git a/create-release.sh b/create-release.sh index 65ade28839..4d65f0db88 100755 --- a/create-release.sh +++ b/create-release.sh @@ -1,41 +1,42 @@ #!/bin/bash set -e -REPO_PATH='alphagov/govuk-prototype-kit' - -echo "Add config for alphagov/$REPO_PATH" +# Check for the TRAVIS environment variable +if [[ -z "${TRAVIS}" ]]; then + echo "⛔️ Refusing to run outside of Travis..." + exit 1 +fi +# Configure git... git config --global user.name "Travis CI" git config --global user.email "travis@travis-ci.org" -git remote add origin_ssh git@github.com:$REPO_PATH.git +git remote add origin_ssh git@github.com:alphagov/govuk-prototype-kit.git -# This openssl command was generated automatically by `travis encrypt-file`, see `.travis/README.md` for more details -openssl aes-256-cbc -K $encrypted_a0ab9bc5246b_key -iv $encrypted_a0ab9bc5246b_iv -in .travis/govuk_prototype_kit.enc -out ~/.ssh/id_rsa -d -chmod 600 ~/.ssh/id_rsa +# Decrypt deploy key. +# +# See `.travis/README.md` for more details +openssl aes-256-cbc -d -k $DEPLOY_KEY \ + -in .travis/prototype-kit-deploy-key.enc \ + -out ~/.ssh/id_rsa -echo "Check to see if the version file has been updated" +chmod 600 ~/.ssh/id_rsa # Get the version from the version file VERSION_TAG="v`cat VERSION.txt`" -# Create a new tag - if the version file has been updated and a tag for that -# version doesn't already exist - -# Check to make sure the tag doesn't already exist +# Check that there's not a tag for the current version already if ! git rev-parse $VERSION_TAG >/dev/null 2>&1; then - echo "Creating new tag: $VERSION_TAG" - - # Create a new tag and push to Github + # Create a new tag and push to GitHub. + # + # GitHub will automatically create a release for the tag, ignoring any files + # specified in the .gitattributes file + echo "🏷 Creating new tag: $VERSION_TAG" git tag $VERSION_TAG git push origin_ssh $VERSION_TAG - # This tag will trigger the builds for the deploy providers marked "# For tagged commits" in .travis.yml - - # Alias branch for the most recently released tag, for easier diffing - # Force push local `master` branch to the `latest-release` branch on Github + # Force push the latest-release branch to this commit + echo "💨 Pushing latest-release branch to GitHub" git push --force origin_ssh master:latest-release - echo "Pushed latest-release branch to GitHub" - else - echo "Not creating a new tag, or updating the latest-release branch as the tag already exists..." + echo "😴 Current version already exists as a tag on GitHub. Nothing to do." fi diff --git a/docs/documentation/adding-css-javascript-and-images.md b/docs/documentation/adding-css-javascript-and-images.md new file mode 100644 index 0000000000..394b40c418 --- /dev/null +++ b/docs/documentation/adding-css-javascript-and-images.md @@ -0,0 +1,84 @@ +# Adding CSS, JavaScript and Images + +The Prototype Kit comes with standard GOV.UK Frontend styles and components for you to use in your prototypes. However if you need to add your own CSS (Cascading Style Sheets), JavaScript or images, use the `/app/assets` folder. + +The Prototype Kit processes all the files in the `/app/assets` folder, and puts the processed files in `/public`. + +Do not change files in the `/public` folder because it’s deleted and rebuilt every time you make a change to your prototype. + +## CSS + +CSS lets you change how web pages look, for example text sizes, colours or spacing. + +To add styles use: + +``` +/app/assets/sass/application.scss +``` + +Do not edit the file `/public/styles/application.css` because it’s deleted and rebuilt every time you make a change to your prototype. + +The Prototype Kit uses [Sass](https://sass-lang.com/guide), which adds extra features to CSS. + +### Using import + +If you have a very long application.scss file, you can split it up into multiple files and import those into `application.scss`. Use an underscore (_) at the start of the import file filenames, for example: + +``` +/app/assets/sass/_admin.scss +``` + +Import this file into your `application.scss` file without the underscore: + +``` +@import "admin"; +``` + +## JavaScript + +You can use JavaScript to make changes to a web page without loading a new one. For example a user could enter some numbers, then JavaScript displays the results of a calculation without loading a new page. + +To add JavaScript use: + +``` +/app/assets/javascripts/application.js +``` + +Do not edit the file `/public/javascript/application.js` because it’s deleted and rebuilt every time you make a change to your prototype. + +## Images + +If you add images to `/app/assets/images` the Prototype Kit will copy them to `/public`. + +For example if you add an image: + +``` +/app/assets/images/user.png +``` + +Use it in your page like this: + +``` +User icon +``` + +Use ‘alt’ text to describe the image for screen readers. + +Do not put files directly in `/public` because it’s deleted and rebuilt every time you make a change to your prototype. + +## Other files + +If you need to use other files in your prototype, you can add them to `/app/assets` and the Prototype Kit will copy them to `/public`. You can use sub-folders in the assets folder. + +For example if you add a PDF: + +``` +/app/assets/downloads/report.pdf +``` + +Link to it like this: + +``` +Download the report +``` +Do not put files directly in `/public` because it’s deleted and rebuilt every time you make a change to your prototype. diff --git a/docs/documentation/extension-system.md b/docs/documentation/extension-system.md new file mode 100644 index 0000000000..c979d88217 --- /dev/null +++ b/docs/documentation/extension-system.md @@ -0,0 +1,3 @@ +# Extension system + +The extension system information should go here before it's adopted into GOVUK. diff --git a/docs/documentation/install/developer-install-instructions.md b/docs/documentation/install/developer-install-instructions.md index 1dabb88225..816cca7d31 100644 --- a/docs/documentation/install/developer-install-instructions.md +++ b/docs/documentation/install/developer-install-instructions.md @@ -4,7 +4,7 @@ It's built on the [Express](http://expressjs.com/) framework, and uses [GOV.UK F ## Requirements -node.js - version 8.x.x +node.js - version 10.x.x ## Install dependencies diff --git a/docs/documentation/install/requirements.md b/docs/documentation/install/requirements.md index 3e3c539ccc..0a57250411 100644 --- a/docs/documentation/install/requirements.md +++ b/docs/documentation/install/requirements.md @@ -14,7 +14,7 @@ GDS staff can install the software themselves with Managed Software Centre (MSC) You'll need: -* Node.js 8.x.x +* Node.js 10.x.x * Atom (text editor) * Command line tools (Mac) * Git bash (Windows) @@ -48,9 +48,9 @@ command to be typed in to terminal Once you’ve typed the command, press enter to send it. -## Node.js version 8 LTS +## Node.js version 10 LTS -The kit is designed to work with Node.js version 8 LTS. The kit works with any 8.x.x version. +The kit is designed to work with Node.js version 10 LTS. The kit works with any 10.x.x version. ### Check if you have Node.js @@ -60,22 +60,22 @@ node --version ``` If it says `command not found` or `Error 0x2 starting node.exe --version` you don’t have node and will need to download and install it. -If the version number starts with 8 you have the correct version installed. +If the version number starts with 10 you have the correct version installed. -If it says another number such as `0.12` or `5.x.x`, you need to download and install version 8. +If it says another number such as `0.12` or `5.x.x`, you need to download and install version 10. ### Download and install Node.js #### Mac / Windows users -Download version 8 from [nodejs.org](https://nodejs.org/en/). +Download version 10 from [nodejs.org](https://nodejs.org/en/). Run the installer with all default options. #### Linux users Follow the instructions on the [Node.js](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions -) site. Make sure you get version 8, not 9. +) site. Make sure you get version 10. ### Once Node is installed @@ -86,7 +86,7 @@ To check it is installed correctly you can again run: node --version ``` -If it’s installed correctly it should show a number starting with 8. +If it’s installed correctly it should show a number starting with 10. ## Atom (text editor) diff --git a/docs/documentation/using-notify.md b/docs/documentation/using-notify.md new file mode 100644 index 0000000000..07021e0489 --- /dev/null +++ b/docs/documentation/using-notify.md @@ -0,0 +1,142 @@ +# Using GOV.UK Notify to prototype emails and text messages + +You can use GOV.UK Notify to send text messages or emails when users +interact with your prototype. For example you could send users a +confirmation email at the end of a journey. + +## Sign up for a GOV.UK Notify account + +If you have a government email address you can sign up for an account at +https://www.gov.uk/notify + +You need an account before you can use GOV.UK Notify to send text +messages or emails. + +## Getting an API key + +An API key is like a password that lets your prototype talk to Notify. +Your prototype sends this key every time it asks Notify to do something. + +To get a key: +- sign into GOV.UK Notify +- go to the ‘API integration’ page +- click ‘API keys’ +- click the ‘Create an API’ button +- choose the ‘Team and whitelist’ option +- copy the key to your clipboard + +### Saving your key on your computer + +This will let your prototype talk to Notify while it’s running on your +computer. + +To save the key on your computer, add this line to the end of the `.env` +file in your prototype (where `xxxxxxx` is the key you’ve copied from +Notify): +```shell +NOTIFYAPIKEY=xxxxxxx +``` +Your prototype will load the key from your `.env` file. If you don’t +have a `.env` file then run your prototype (with the `npm start` +command) and it will create one for you. + +### Saving the key on Heroku + +This will let your prototype talk to Notify while it’s running on +Heroku. + +To save the key on Heroku, go to the settings page of your app, click +‘Reveal config vars’ and fill in the two textboxes like this (where +xxxxxxx is the key you’ve copied from Notify): +``` +KEY | VALUE +-------------|---------- +NOTIFYAPIKEY | xxxxxxx +``` + +### Keeping your key safe + +It’s really important that you keep your key secret. If you put it in +the `.env` file it’s safe – that file isn’t published on GitHub. If you +do accidentally publish your key somewhere you must sign into Notify and +revoke it. + +## Add the Notify code to your prototype + +Add this code to the top of routes.js: + +```javascript +var NotifyClient = require('notifications-node-client').NotifyClient, + notify = new NotifyClient(process.env.NOTIFYAPIKEY); +``` + +## Sending your first email + +Make a page with a form to collect the user’s email address. For +example: +``` +{% extends "layout.html" %} + +{% block content %} + +

+
+
+ +
+ + +
+ + + +
+
+
+{% endblock %} +``` + +Save this page as `email-address-page.html`. + +Then add this code to `routes.js`, above the line that says +`module.exports = router`: + +```javascript +// The URL here needs to match the URL of the page that the user is on +// when they type in their email address +router.post('/email-address-page', function (req, res) { + + notify.sendEmail( + // this long string is the template ID, copy it from the template + // page in GOV.UK Notify. It’s not a secret so it’s fine to put it + // in your code. + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + // `emailAddress` here needs to match the name of the form field in + // your HTML page + req.body.emailAddress + ); + + // This is the URL the users will be redirected to once the email + // has been sent + res.redirect('/confirmation-page'); + +}); +``` + +### Testing it out + +Now when you type your email address into the page and press the green +button you should get an email. You should also be able to see the email +you’ve sent on the GOV.UK Notify dashboard. + +Because your account is in trial mode you’ll only be able to send emails +to yourself. If you’re doing user research you can add the participants +email addresses to the ‘whitelist’ in GOV.UK Notify. This will let you +send them emails too. You’ll need to collect their email addresses and +get consent to use them before doing your research. + +## More things you can do with GOV.UK Notify + +The complete documentation for using the GOV.UK Notify API is here: https://docs.notifications.service.gov.uk/node.html diff --git a/docs/documentation/writing-css.md b/docs/documentation/writing-css.md deleted file mode 100644 index e165b22a88..0000000000 --- a/docs/documentation/writing-css.md +++ /dev/null @@ -1,25 +0,0 @@ -# Writing CSS - -CSS used in the Prototype Kit is written in the SCSS syntax of [Sass](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#syntax). - -## Sass - -Sass is an extension of CSS that gives a load of extra features useful in producing CSS for the mass of different devices and browsers. - -SCSS was chosen because you can paste CSS into it without breaking it which is useful for prototyping. Using SCSS means you can also use the [GOV.UK Frontend](https://github.com/alphagov/govuk-frontend) Sass libraries which include styles for colours, typography, cross-browser fixes and other things which are used across GOV.UK. - -## Writing code - -You write your Sass in [app/assets/sass](../app/assets/sass) and the Prototype Kit will compile it into the CSS used in your page (found in /public/stylesheets). The app watches your files so this will happen automatically. - -There is already a CSS file included to use called [application.scss](../app/assets/sass/application.scss) which compiles into [application.css](../public/stylesheets/application.css). Note that Sass files are identified by the `.scss` extension. - -Every time a change happens in [application.scss](../app/assets/sass/application.scss) it will produce a new version of [application.css](../public/stylesheets/application.css). Make sure to write your css in [application.scss](../app/assets/sass/application.scss) as anything you put in [application.css](../public/stylesheets/application.css) will get overridden. - -Try starting the app and adding some styles to `application.scss`. If you open `application.css` you should now see the compiled version of those styles. - -## Imports - -Imports are done from either [GOV.UK Frontend](https://github.com/alphagov/govuk-frontend/tree/master/src/) folder or the [app/assets/sass](../app/assets/sass) folder application.scss sits in. The latter means you can create your own partial files to import. - -Note that the convention is to start the name of any partial with an underscore, like those in GOV.UK Frontend. diff --git a/docs/documentation_routes.js b/docs/documentation_routes.js index 9b7439a4d5..4163499008 100644 --- a/docs/documentation_routes.js +++ b/docs/documentation_routes.js @@ -31,7 +31,7 @@ router.get('/install/:page', function (req, res) { redirectMarkdown(req.params.page, res) var doc = fs.readFileSync(path.join(__dirname, '/documentation/install/', req.params.page + '.md'), 'utf8') var html = marked(doc) - res.render('install_template', {'document': html}) + res.render('install_template', { 'document': html }) }) // Redirect to the zip of the latest release of the Prototype Kit on GitHub diff --git a/docs/views/about.html b/docs/views/about.html index 245a421746..8724bf2256 100644 --- a/docs/views/about.html +++ b/docs/views/about.html @@ -24,15 +24,45 @@

About

The GOV.UK Prototype Kit provides a simple way to make interactive prototypes that look like pages on GOV.UK. These prototypes can be used to show ideas to people you work with, and to do user research

- -

Community

- -

We have two Slack channels for the GOV.UK Prototype Kit. You'll need a government email address to join.

+ +

Support

+ +

The GOV.UK Prototype Kit is maintained by the Government Digital Service. If you’ve got a question or need support you can:

+

Contributing

+ +

If you’ve got an idea or suggestion you can:

+

Principles

diff --git a/docs/views/includes/head.html b/docs/views/includes/head.html index f78e30e92f..33dbf59c86 100644 --- a/docs/views/includes/head.html +++ b/docs/views/includes/head.html @@ -1,6 +1,6 @@ - + - + diff --git a/docs/views/includes/scripts.html b/docs/views/includes/scripts.html index f2487ab98d..65f5c79369 100644 --- a/docs/views/includes/scripts.html +++ b/docs/views/includes/scripts.html @@ -1,6 +1,6 @@ - + {% if useAutoStoreData %} diff --git a/docs/views/layout.html b/docs/views/layout.html index 8d43b0630a..bd59d0656b 100644 --- a/docs/views/layout.html +++ b/docs/views/layout.html @@ -40,49 +40,33 @@ {% block header %} {% include "includes/cookie-banner.html" %} {# Set serviceName in config.js. #} -{# TODO: Use the macro when data attributes can be passed to individual items: #} -{# https://github.com/alphagov/govuk-frontend/issues/904 #} - + {{ govukHeader({ + serviceName: serviceName, + serviceUrl: "/", + navigation: [ + { + href: "/docs/install", + text: "Install", + attributes: { + "data-install": "Install" + } + }, + { + href: "/docs/tutorials-and-examples", + text: "Tutorials and examples", + attributes: { + "data-tutorials": "Tutorials and examples" + } + }, + { + href: "/docs/about", + text: "About", + attributes: { + "data-about": "About" + } + } + ] + }) }} {% endblock %} {% block footer %} @@ -107,6 +91,8 @@ {% endblock %} {% block bodyEnd %} - {% include "includes/scripts.html" %} - + {% block scripts %} + {% include "includes/scripts.html" %} + {% block pageScripts %}{% endblock %} + {% endblock %} {% endblock %} diff --git a/docs/views/layout_unbranded.html b/docs/views/layout_unbranded.html index 9dbff9e7f8..77387dc65f 100644 --- a/docs/views/layout_unbranded.html +++ b/docs/views/layout_unbranded.html @@ -10,7 +10,7 @@ {% endblock %} {% block head %} - + {% endblock %} {% block header %}{% endblock %} diff --git a/docs/views/templates/check-your-answers.html b/docs/views/templates/check-your-answers.html index bc28e9f615..08899ef5ec 100644 --- a/docs/views/templates/check-your-answers.html +++ b/docs/views/templates/check-your-answers.html @@ -9,159 +9,151 @@ {% endblock %} {% block content %} -
-
+
+
-

- Check your answers before sending your application -

+

Check your answers before sending your application

- -

- Personal details -

+

Personal details

- - - - -
-
-
+
+
+
Name
-
+
Sarah Philips
-
+
- Change name + Change + name
- -
-
+
+
Date of birth
-
+
5 January 1978
-
+
- Change date of birth + Change + date of birth
- -
-
- Home address +
+
+ Contact information
-
- 72 Guild Street
- London
- SE23 6FH +
+ 72 Guild Street +
London +
SE23 6FH
-
+
- Change home address + Change + contact information
- -
-
+
+
Contact details
-
- 07700 900457
- sarah.phillips@example.com +
+

07700 900457

+

sarah.phillips@example.com

-
+
- Change contact details + Change + contact details
+

Application details

-

- Application details -

- -
-
-
+
+ - -
-
+
+
Licence type
-
+
For personal use
-
+
- Change licence type + Change + licence type
- -
-
+
+
Home address
-
- 72 Guild Street
- London
- SE23 6FH +
+ 72 Guild Street +
London +
SE23 6FH
-
+
- Change home address + Change + home address
- -
-
+
+
Licence period
-
- This is a longer paragraph of text provided by the user to provide additional information.

- This is a second paragraph of text provided by the user. +
+ Valid for 6 months
-
+
- Change licence period + Change + licence period
-

Now send your application

-

- By submitting this notification you are confirming that, to the best of your knowledge, the details you are providing are correct. -

+

By submitting this notification you are confirming that, to the best of your knowledge, the details you are providing are correct.

+ +
+ + + + - - Accept and send application - +
diff --git a/docs/views/templates/question.html b/docs/views/templates/question.html index 73f87235a8..430d848b08 100644 --- a/docs/views/templates/question.html +++ b/docs/views/templates/question.html @@ -13,8 +13,7 @@
-

Heading or question goes here -

+

Heading or question goes here

diff --git a/docs/views/templates/start-with-step-by-step.html b/docs/views/templates/start-with-step-by-step.html new file mode 100644 index 0000000000..d19dce6ab8 --- /dev/null +++ b/docs/views/templates/start-with-step-by-step.html @@ -0,0 +1,315 @@ +{% extends "layout.html" %} {% block pageTitle %} Start page example {% endblock %} {% block header %} + +{{ govukHeader() }} {% endblock %} {% block pageScripts %} + + +{% endblock %} {% block beforeContent %} + +{% endblock %} {% block content %} +
+
+ +

+ Check what age you can drive +

+ +

Use this service to:

+ +
    +
  • do something
  • +
  • update your name, address or other details
  • +
  • do something else
  • +
+ + Start now + +
+ +
+ + + +
+
    +
  1. +
    +

    + + + + Step 1 + + + + + + + Check you're allowed to drive + +

    +
    + +
    +

    Most people can start learning to drive when they’re 17.

    + +
      +
    1. + + You are currently viewing: Check what age you can drive +
    2. +
    3. + Requirements for driving legally +
    4. +
    5. + Driving eyesight rules +
    6. +
    + +
    + +
  2. +
  3. +
    +

    + + + + Step 2 + + + + + + + Get a provisional licence + +

    +
    + + + +
  4. +
  5. +
    +

    + + + + Step 3 + + + + + + + Driving lessons and practice + +

    +
    + +
    +

    You need a provisional driving licence to take lessons or practice.

    + +
      +
    1. + The Highway Code +
    2. +
    3. + Taking driving lessons +
    4. +
    5. + Find driving schools, lessons and instructors +
    6. +
    7. + Practise vehicle safety questions +
    8. +
    + +
    + +
  6. +
  7. +
    +

    + + + + and + + + + + + Prepare for your theory test + +

    +
    + + + +
  8. +
  9. +
    +

    + + + + Step 4 + + + + + + + Book and manage your theory test + +

    +
    + +
    +

    You need a provisional driving licence to book your theory test.

    + +
      +
    1. + Book your theory test ÂŁ23 +
    2. +
    3. + What to take to your test +
    4. +
    5. + Change your theory test appointment +
    6. +
    7. + Check your theory test appointment details +
    8. +
    9. + Cancel your theory test +
    10. +
    + +
    + +
  10. +
  11. +
    +

    + + + + Step 5 + + + + + + + Book and manage your driving test + +

    +
    + +
    +

    You must pass your theory test before you can book your driving test.

    + +
      +
    1. + Book your driving test ÂŁ62 to ÂŁ75 +
    2. +
    3. + What to take to your test +
    4. +
    5. + Change your driving test appointment +
    6. +
    7. + Check your driving test appointment details +
    8. +
    9. + Cancel your driving test +
    10. +
    + +
    + +
  12. +
  13. +
    +

    + + + + Step 6 + + + + + + + When you pass + +

    +
    + +
    +

    You can start driving as soon as you pass your driving test.

    + +

    You must have an insurance policy that allows you to drive without supervision.

    + +
      +
    1. + Find out about Pass Plus training courses +
    2. +
    + +
    + +
  14. +
+
+ + +
+ +
+ + +
+
+{% endblock %} diff --git a/docs/views/templates/step-by-step-navigation.html b/docs/views/templates/step-by-step-navigation.html new file mode 100644 index 0000000000..fabbc841dd --- /dev/null +++ b/docs/views/templates/step-by-step-navigation.html @@ -0,0 +1,299 @@ +{% extends "layout.html" %} + +{% block pageTitle %} + Step by step navigation +{% endblock %} + +{% block pageScripts %} + + +{% endblock %} + +{% block beforeContent %} +
+
    +
  1. + Home +
  2. +
  3. + Section +
  4. +
  5. + Subsection +
  6. +
+
+{% endblock %} + +{% block content %} + +
+
+ +

+ Learn to drive a car: step by step +

+ +

+ Check what you need to do to learn to drive. +

+ +
+
    +
  1. +
    +

    + + + + Step 1 + + + + + + Check you're allowed to drive + +

    +
    + +
    +

    Most people can start learning to drive when they’re 17.

    + +
      +
    1. + Check what age you can drive +
    2. +
    3. + Requirements for driving legally +
    4. +
    5. + Driving eyesight rules +
    6. +
    +
    + +
  2. + +
  3. +
    +

    + + + + Step 2 + + + + + + Get a provisional licence + +

    +
    + + + +
  4. + +
  5. +
    +

    + + + + Step 3 + + + + + + Driving lessons and practice + +

    +
    + +
    +

    You need a provisional driving licence to take lessons or practice.

    + +
      +
    1. + The Highway Code +
    2. +
    3. + Taking driving lessons +
    4. +
    5. + Find driving schools, lessons and instructors +
    6. +
    7. + Practise vehicle safety questions +
    8. +
    +
    + +
  6. + +
  7. +
    +

    + + + + and + + + + + + Prepare for your theory test + +

    +
    + + + +
  8. + +
  9. +
    +

    + + + + Step 4 + + + + + + Book and manage your theory test + +

    +
    + +
    +

    You need a provisional driving licence to book your theory test.

    + +
      +
    1. + Book your theory test ÂŁ23 +
    2. +
    3. + What to take to your test +
    4. +
    5. + Change your theory test appointment +
    6. +
    7. + Check your theory test appointment details +
    8. +
    9. + Cancel your theory test +
    10. +
    +
    + +
  10. + +
  11. +
    +

    + + + + Step 5 + + + + + + Book and manage your driving test + +

    +
    + +
    +

    You must pass your theory test before you can book your driving test.

    + +
      +
    1. + Book your driving test ÂŁ62 to ÂŁ75 + +
    2. +
    3. + What to take to your test +
    4. +
    5. + Change your driving test appointment +
    6. +
    7. + Check your driving test appointment details +
    8. +
    9. + Cancel your driving test +
    10. +
    +
    + +
  12. + +
  13. +
    +

    + + + + Step 6 + + + + + + When you pass + +

    +
    + +
    +

    You can start driving as soon as you pass your driving test.

    + +

    You must have an insurance policy that allows you to drive without supervision.

    + +
      +
    1. + Find out about Pass Plus training courses +
    2. +
    +
    + +
  14. +
+
+ +
+
+ +{% endblock %} diff --git a/docs/views/tutorials-and-examples.html b/docs/views/tutorials-and-examples.html index 9ba5c5bf73..666e5d332e 100644 --- a/docs/views/tutorials-and-examples.html +++ b/docs/views/tutorials-and-examples.html @@ -64,7 +64,7 @@

Basic usage

Making pages
  • - Writing CSS + Adding CSS, JavaScript and Images
  • Setting up Git @@ -105,6 +105,9 @@

    Advanced usage

  • Using GOV.UK Verify
  • +
  • + Using GOV.UK Notify +
  • @@ -119,10 +122,10 @@

    Advanced usage

    +
  • + +
    +

    GOV.UK Design System

    diff --git a/gulp/clean.js b/gulp/clean.js index 6587689497..7983c2cb09 100644 --- a/gulp/clean.js +++ b/gulp/clean.js @@ -5,13 +5,12 @@ - public */ +const del = require('del') const gulp = require('gulp') -const clean = require('gulp-clean') const config = require('./config.json') -gulp.task('clean', function () { - return gulp.src([config.paths.public + '/*', - '.port.tmp'], {read: false}) - .pipe(clean()) +gulp.task('clean', function (done) { + return del([config.paths.public + '/*', + '.port.tmp']) }) diff --git a/gulp/copy-assets.js b/gulp/copy-assets.js index fbe57c6988..3c6503710e 100644 --- a/gulp/copy-assets.js +++ b/gulp/copy-assets.js @@ -5,6 +5,7 @@ */ const gulp = require('gulp') + const config = require('./config.json') gulp.task('copy-assets', function () { diff --git a/gulp/nodemon.js b/gulp/nodemon.js index 2f08392437..dd6fd778c2 100644 --- a/gulp/nodemon.js +++ b/gulp/nodemon.js @@ -8,23 +8,36 @@ const fs = require('fs') const path = require('path') const gulp = require('gulp') +const colour = require('ansi-colors') +const log = require('fancy-log') const nodemon = require('gulp-nodemon') const config = require('./config.json') +// Warn about npm install on crash +const onCrash = () => { + log(colour.cyan('[nodemon] For missing modules try running `npm install`')) +} + +// Remove .port.tmp if it exists +const onQuit = () => { + try { + fs.unlinkSync(path.join(__dirname, '/../.port.tmp')) + } catch (e) {} + + process.exit(0) +} + gulp.task('server', function () { nodemon({ watch: ['.env', '**/*.js', '**/*.json'], - script: 'server.js', - ignore: [config.paths.public + '*', + script: 'listen-on-port.js', + ignore: [ + config.paths.public + '*', config.paths.assets + '*', - config.paths.nodeModules + '*'] - }).on('quit', function () { - // remove .port.tmp if it exists - try { - fs.unlinkSync(path.join(__dirname, '/../.port.tmp')) - } catch (e) {} - - process.exit(0) + config.paths.nodeModules + '*' + ] }) + .on('crash', onCrash) + .on('quit', onQuit) }) diff --git a/gulp/sass.js b/gulp/sass.js index 022e9e59fb..1b33eed74c 100644 --- a/gulp/sass.js +++ b/gulp/sass.js @@ -8,13 +8,23 @@ const gulp = require('gulp') const sass = require('gulp-sass') const sourcemaps = require('gulp-sourcemaps') +const path = require('path') +const fs = require('fs') +const extensions = require('../lib/extensions/extensions') const config = require('./config.json') +gulp.task('sass-extensions', function (done) { + const fileContents = '$govuk-extensions-url-context: "/extension-assets"; ' + extensions.getFileSystemPaths('sass') + .map(filePath => `@import "${filePath.split(path.sep).join('/')}";`) + .join('\n') + fs.writeFile(path.join(config.paths.lib + 'extensions', '_extensions.scss'), fileContents, done) +}) + gulp.task('sass', function () { return gulp.src(config.paths.assets + '/sass/*.scss') .pipe(sourcemaps.init()) - .pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError)) + .pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError)) .pipe(sourcemaps.write()) .pipe(gulp.dest(config.paths.public + '/stylesheets/')) }) @@ -22,7 +32,7 @@ gulp.task('sass', function () { gulp.task('sass-documentation', function () { return gulp.src(config.paths.docsAssets + '/sass/*.scss') .pipe(sourcemaps.init()) - .pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError)) + .pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError)) .pipe(sourcemaps.write()) .pipe(gulp.dest(config.paths.public + '/stylesheets/')) }) diff --git a/gulp/tasks.js b/gulp/tasks.js deleted file mode 100644 index 24980f2694..0000000000 --- a/gulp/tasks.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - tasks.js - =========== - defaults wraps generate-assets, watch and server -*/ - -const gulp = require('gulp') -const mocha = require('gulp-mocha') -const runSequence = require('run-sequence') - -gulp.task('default', function (done) { - runSequence('generate-assets', - 'watch', - 'server', done) -}) - -gulp.task('generate-assets', function (done) { - runSequence('clean', - 'sass', - 'copy-assets', - 'sass-documentation', - 'copy-assets-documentation', - 'sass-v6', - 'copy-assets-v6', done) -}) - -gulp.task('watch', function (done) { - runSequence('watch-sass', - 'watch-assets', - 'watch-sass-v6', - 'watch-assets-v6', done) -}) - -gulp.task('test', function () { - runSequence('generate-assets', - 'mocha') -}) - -gulp.task('mocha', function () { - return gulp.src(['test/**/*.js'], { read: false }) - .pipe(mocha({ reporter: 'spec', exit: true })) - .on('error', console.error) -}) diff --git a/gulp/watch.js b/gulp/watch.js index 2c3e15fedb..47fa6faf0e 100644 --- a/gulp/watch.js +++ b/gulp/watch.js @@ -5,24 +5,25 @@ */ const gulp = require('gulp') + const config = require('./config.json') gulp.task('watch-sass', function () { - return gulp.watch(config.paths.assets + 'sass/**', {cwd: './'}, ['sass']) + return gulp.watch(config.paths.assets + 'sass/**', { cwd: './' }, gulp.task('sass')) }) gulp.task('watch-assets', function () { return gulp.watch([config.paths.assets + 'images/**', - config.paths.assets + 'javascripts/**'], {cwd: './'}, ['copy-assets']) + config.paths.assets + 'javascripts/**'], { cwd: './' }, gulp.task('copy-assets')) }) // Backward compatibility with Elements gulp.task('watch-sass-v6', function () { - return gulp.watch(config.paths.v6Assets + 'sass/**', {cwd: './'}, ['sass-v6']) + return gulp.watch(config.paths.v6Assets + 'sass/**', { cwd: './' }, gulp.task('sass-v6')) }) gulp.task('watch-assets-v6', function () { return gulp.watch([config.paths.v6Assets + 'images/**', - config.paths.v6Assets + 'javascripts/**'], {cwd: './'}, ['copy-assets-v6']) + config.paths.v6Assets + 'javascripts/**'], { cwd: './' }, gulp.task('copy-assets-v6')) }) diff --git a/gulpfile.js b/gulpfile.js index fb9ba12649..3b4702b22a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,11 +6,38 @@ its own file in `/gulp`. Any files in that directory get automatically required below. To add a new task, simply add a new task file that directory. - `/gulp/tasks.js` specifies the default set of - tasks to run when you run `gulp`. */ +const gulp = require('gulp') const requireDir = require('require-dir') // Require all tasks in gulp/tasks, including subfolders -requireDir('./gulp', {recurse: true}) +requireDir('./gulp', { recurse: true }) + +// gulp 4 requires dependency tasks to be defined before they are called. +// We'll keep our top-level tasks in this file so that they are defined at the end of the chain, after their dependencies. +gulp.task('generate-assets', gulp.series( + 'clean', + 'sass-extensions', + gulp.parallel( + 'sass', + 'copy-assets', + 'sass-documentation', + 'copy-assets-documentation', + 'sass-v6', + 'copy-assets-v6' + ) +)) +gulp.task('watch', gulp.parallel( + 'watch-sass', + 'watch-assets', + 'watch-sass-v6', + 'watch-assets-v6' +)) +gulp.task('default', gulp.series( + 'generate-assets', + gulp.parallel( + 'watch', + 'server' + ) +)) diff --git a/lib/extensions/extensions.js b/lib/extensions/extensions.js new file mode 100644 index 0000000000..0d18746945 --- /dev/null +++ b/lib/extensions/extensions.js @@ -0,0 +1,174 @@ +/** + * Extensions.js (Use with caution) + * + * Experimental feature which is likely to change. + * This file returns helper methods to enable services to include + * their own departmental frontend(Styles, Scripts, nunjucks etc) + * + * Module.exports + * getPublicUrls: + * Params: (type | string ) eg. 'scripts', 'stylesheets' + * Description: + * returns array of urls for a type (script, stylesheet, nunjucks etc). + * getFileSystemPaths: + * Params: (type | string ) eg. 'scripts', 'stylesheets' + * Description: + * returns array paths to the file in the filesystem for a type (script, stylesheet, nunjucks etc) + * getPublicUrlAndFileSystemPaths: + * Params: (type | string ) eg. 'scripts', 'stylesheets' + * Description: + * returns Array of objects, each object is an extension and each obj has the filesystem & public url for the given type + * getAppConfig: + * Params: (type | string ) eg. 'scripts', 'stylesheets' + * Description: + * Returns an object containing two keys(scripts & stylesheets), each item contains an array of full paths to specific files. + * This is used in the views to output links and scripts each file. + * getAppViews: + * Params: (additionalViews | Array ) eg.extensions.getAppViews([path.join(__dirname, '/app/views/'),path.join(__dirname, '/lib/')]) + * Description: + * Returns an array of paths to nunjucks templates which is used to configure nunjucks in server.js + * setExtensionsByType + * Params: N/A + * Description: only used for test purposes to reset mocked extensions items to ensure they are up-to-date when the tests run + * + * * + */ + +// Core dependencies +const fs = require('fs') +const path = require('path') + +// Local dependencies +const appConfig = require('../../app/config') + +// Generic utilities +const removeDuplicates = arr => [...new Set(arr)] +const filterOutParentAndEmpty = part => part && part !== '..' +const objectMap = (object, mapFn) => Object.keys(object).reduce((result, key) => { + result[key] = mapFn(object[key], key) + return result +}, {}) + +// File utilities +const getPathFromProjectRoot = (...all) => { + return path.join.apply(null, [__dirname, '..', '..'].concat(all)) +} +const pathToPackageConfigFile = packageName => getPathFromProjectRoot('node_modules', packageName, 'govuk-prototype-kit.config.json') + +const readJsonFile = (filePath) => { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) +} +const getPackageConfig = packageName => { + if (fs.existsSync(pathToPackageConfigFile(packageName))) { + return readJsonFile(pathToPackageConfigFile(packageName)) + } else { + return {} + } +} + +// Handle errors to do with extension paths +// Example of `subject`: { packageName: 'govuk-frontend', item: '/all.js' } +const throwIfBadFilepath = subject => { + if (('' + subject.item).indexOf('\\') > -1) { + throw new Error(`Can't use backslashes in extension paths - "${subject.packageName}" used "${subject.item}".`) + } + if (!('' + subject.item).startsWith('/')) { + throw new Error(`All extension paths must start with a forward slash - "${subject.packageName}" used "${subject.item}".`) + } +} + +// Check for `baseExtensions` in config.js. If it's not there, default to `govuk-frontend` +const getBaseExtensions = () => appConfig.baseExtensions || ['govuk-frontend'] + +// Get all npm dependencies +// Get baseExtensions in the order defined in `baseExtensions` in config.js +// Then place baseExtensions before npm dependencies (and remove duplicates) +const getPackageNamesInOrder = () => { + const dependencies = readJsonFile(getPathFromProjectRoot('package.json')).dependencies || {} + const allNpmDependenciesInAlphabeticalOrder = Object.keys(dependencies).sort() + const installedBaseExtensions = getBaseExtensions() + .filter(packageName => allNpmDependenciesInAlphabeticalOrder.includes(packageName)) + + return removeDuplicates(installedBaseExtensions.concat(allNpmDependenciesInAlphabeticalOrder)) +} + +// Extensions provide items such as sass scripts, asset paths etc. +// This function groups them by type in a format which can used by getList +// Example of return +// { +// nunjucksPaths: [ +// { packageName: 'govuk-frontend', item: '/' }, +// { packageName: 'govuk-frontend', item: '/components'} +// ], +// scripts: [ +// { packageName: 'govuk-frontend', item: '/all.js' } +// ] +// assets: [ +// { packageName: 'govuk-frontend', item: '/assets' } +// ], +// sass: [ +// { packageName: 'govuk-frontend', item: '/all.scss' } +// ]} +const getExtensionsByType = () => { + return getPackageNamesInOrder() + .reduce((accum, packageName) => Object.assign({}, accum, objectMap( + getPackageConfig(packageName), + (listOfItemsForType, type) => (accum[type] || []) + .concat([].concat(listOfItemsForType).map(item => ({ + packageName, + item + }))) + )), {}) +} + +let extensionsByType + +const setExtensionsByType = () => { + extensionsByType = getExtensionsByType() +} + +setExtensionsByType() + +// The hard-coded reference to govuk-frontend allows us to soft launch without a breaking change. After a hard launch +// govuk-frontend assets will be served on /extension-assets/govuk-frontend +const getPublicUrl = config => { + if (config.item === '/assets' && config.packageName === 'govuk-frontend') { + return '/assets' + } else { + return ['', 'extension-assets', config.packageName] + .concat(config.item.split('/').filter(filterOutParentAndEmpty)) + .map(encodeURIComponent) + .join('/') + } +} + +const getFileSystemPath = config => { + throwIfBadFilepath(config) + return getPathFromProjectRoot('node_modules', + config.packageName, + config.item.split('/').filter(filterOutParentAndEmpty).join(path.sep)) +} + +const getPublicUrlAndFileSystemPath = config => ({ + fileSystemPath: getFileSystemPath(config), + publicUrl: getPublicUrl(config) +}) + +const getList = type => extensionsByType[type] || [] + +// Exports +const self = module.exports = { + getPublicUrls: type => getList(type).map(getPublicUrl), + getFileSystemPaths: type => getList(type).map(getFileSystemPath), + getPublicUrlAndFileSystemPaths: type => getList(type).map(getPublicUrlAndFileSystemPath), + getAppConfig: _ => ({ + scripts: self.getPublicUrls('scripts'), + stylesheets: self.getPublicUrls('stylesheets') + }), + getAppViews: additionalViews => self + .getFileSystemPaths('nunjucksPaths') + .reverse() + .concat(additionalViews || []), + + setExtensionsByType // exposed only for testing purposes +} diff --git a/lib/extensions/extensions.test.js b/lib/extensions/extensions.test.js new file mode 100644 index 0000000000..7a443dc233 --- /dev/null +++ b/lib/extensions/extensions.test.js @@ -0,0 +1,474 @@ +/* eslint-env jest */ +/* global spyOn */ +// NPM dependencies +const path = require('path') +const fs = require('fs') +const appConfig = require('../../app/config') + +// Local dependencies +const extensions = require('./extensions.js') + +// Local variables +const rootPath = path.join(__dirname, '..', '..') +let testScope + +// helpers +const joinPaths = arr => arr.map(x => path.join.apply(null, [rootPath].concat(x))) + +describe('extensions', () => { + beforeEach(() => { + testScope = { + originalValues: { + appConfigBaseExtensions: appConfig.baseExtensions + }, + fileSystem: {} + } + addFileToMockFileSystem(['package.json'], fs.readFileSync('package.json', 'utf8')) + addFileToMockFileSystem(['node_modules', 'govuk-frontend', 'govuk-prototype-kit.config.json'], '{"nunjucksPaths": ["/","/components"],"scripts": ["/all.js"],"assets": ["/assets"],"sass": ["/all.scss"]}') + setupFakeFilesystem() + extensions.setExtensionsByType() + }) + afterEach(() => { + jest.clearAllMocks() + appConfig.baseExtensions = testScope.originalValues.appConfigBaseExtensions + }) + + describe('Lookup file system paths', () => { + it('should lookup asset paths as file system paths', () => { + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'govuk-frontend', 'assets'] + ])) + }) + it('should not allow traversing the file system', () => { + mockExtensionConfig('govuk-frontend', { assets: ['/abc/../../../../../def'] }) + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'govuk-frontend', 'abc', 'def'] + ])) + }) + it('should show installed extensions asset paths as file system paths', () => { + delete appConfig.baseExtensions + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'govuk-frontend', 'assets'], + ['node_modules', 'another-frontend', 'abc'], + ['node_modules', 'another-frontend', 'def'], + ['node_modules', 'hmrc-frontend', 'ghi'], + ['node_modules', 'hmrc-frontend', 'jkl'] + ])) + }) + it('should follow strict alphabetical order when no base extensions used', () => { + appConfig.baseExtensions = [] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'another-frontend', 'abc'], + ['node_modules', 'another-frontend', 'def'], + ['node_modules', 'govuk-frontend', 'assets'], + ['node_modules', 'hmrc-frontend', 'ghi'], + ['node_modules', 'hmrc-frontend', 'jkl'] + ])) + }) + it('should put specified baseExtensions at the top', () => { + appConfig.baseExtensions = ['hmrc-frontend', 'govuk-frontend'] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'hmrc-frontend', 'ghi'], + ['node_modules', 'hmrc-frontend', 'jkl'], + ['node_modules', 'govuk-frontend', 'assets'], + ['node_modules', 'another-frontend', 'abc'], + ['node_modules', 'another-frontend', 'def'] + ])) + }) + it('should show installed extensions asset paths as file system paths', () => { + mockExtensionConfig('hmrc-frontend', { + assets: ['/abc', '/def'] + }) + expect(extensions.getFileSystemPaths('assets')).toEqual(joinPaths([ + ['node_modules', 'govuk-frontend', 'assets'], + ['node_modules', 'hmrc-frontend', 'abc'], + ['node_modules', 'hmrc-frontend', 'def'] + ])) + }) + it('should lookup scripts paths as file system paths', () => { + expect(extensions.getFileSystemPaths('scripts')).toEqual(joinPaths([ + 'node_modules/govuk-frontend/all.js' + ])) + }) + it('should not break when asking for an extension key which isn\'t used', function () { + expect(extensions.getFileSystemPaths('thisListDoesNotExist')).toEqual([]) + }) + }) + + describe('Lookup public URLs', () => { + it('should show installed extensions asset paths as file system paths', () => { + delete appConfig.baseExtensions + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrls('assets')).toEqual([ + '/assets', + '/extension-assets/another-frontend/abc', + '/extension-assets/another-frontend/def', + '/extension-assets/hmrc-frontend/ghi', + '/extension-assets/hmrc-frontend/jkl' + ]) + }) + it('should follow strict alphabetical order when no base extensions used', () => { + appConfig.baseExtensions = [] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrls('assets')).toEqual([ + '/extension-assets/another-frontend/abc', + '/extension-assets/another-frontend/def', + '/assets', + '/extension-assets/hmrc-frontend/ghi', + '/extension-assets/hmrc-frontend/jkl' + ]) + }) + it('should put specified baseExtensions at the top', () => { + appConfig.baseExtensions = ['hmrc-frontend', 'govuk-frontend'] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrls('assets')).toEqual([ + '/extension-assets/hmrc-frontend/ghi', + '/extension-assets/hmrc-frontend/jkl', + '/assets', + '/extension-assets/another-frontend/abc', + '/extension-assets/another-frontend/def' + ]) + }) + it('should url encode each part', () => { + mockExtensionConfig('mine', { assets: ['/abc:def'] }) + mockUninstallExtension('govuk-frontend') + + expect(extensions.getPublicUrls('assets')).toEqual(['/extension-assets/mine/abc%3Adef']) + }) + }) + + describe('Lookup public URLs with file system paths', () => { + it('should show installed extensions asset paths as file system paths', () => { + delete appConfig.baseExtensions + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrlAndFileSystemPaths('assets')).toEqual([ + { + publicUrl: '/assets', + fileSystemPath: path.join(rootPath, 'node_modules', 'govuk-frontend', 'assets') + }, + { + publicUrl: '/extension-assets/another-frontend/abc', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'abc') + }, + { + publicUrl: '/extension-assets/another-frontend/def', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'def') + }, + { + publicUrl: '/extension-assets/hmrc-frontend/ghi', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'ghi') + }, + { + publicUrl: '/extension-assets/hmrc-frontend/jkl', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'jkl') + } + ]) + }) + it('should follow strict alphabetical order when no base extensions used', () => { + appConfig.baseExtensions = [] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrlAndFileSystemPaths('assets')).toEqual([ + { + publicUrl: '/extension-assets/another-frontend/abc', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'abc') + }, + { + publicUrl: '/extension-assets/another-frontend/def', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'def') + }, + { + publicUrl: '/assets', + fileSystemPath: path.join(rootPath, 'node_modules', 'govuk-frontend', 'assets') + }, + { + publicUrl: '/extension-assets/hmrc-frontend/ghi', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'ghi') + }, + { + publicUrl: '/extension-assets/hmrc-frontend/jkl', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'jkl') + } + ]) + }) + it('should put specified baseExtensions at the top', () => { + appConfig.baseExtensions = ['hmrc-frontend', 'govuk-frontend'] + mockExtensionConfig('another-frontend', { + assets: ['/abc', '/def'] + }) + mockExtensionConfig('hmrc-frontend', { + assets: ['/ghi', '/jkl'] + }) + expect(extensions.getPublicUrlAndFileSystemPaths('assets')).toEqual([ + { + publicUrl: '/extension-assets/hmrc-frontend/ghi', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'ghi') + }, + { + publicUrl: '/extension-assets/hmrc-frontend/jkl', + fileSystemPath: path.join(rootPath, 'node_modules', 'hmrc-frontend', 'jkl') + }, + { + publicUrl: '/assets', + fileSystemPath: path.join(rootPath, 'node_modules', 'govuk-frontend', 'assets') + }, + { + publicUrl: '/extension-assets/another-frontend/abc', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'abc') + }, + { + publicUrl: '/extension-assets/another-frontend/def', + fileSystemPath: path.join(rootPath, 'node_modules', 'another-frontend', 'def') + } + ]) + }) + it('should url encode each part', () => { + mockExtensionConfig('mine', { assets: ['/abc:def'] }) + mockUninstallExtension('govuk-frontend') + + expect(extensions.getPublicUrls('assets')).toEqual(['/extension-assets/mine/abc%3Adef']) + }) + it('should not break when asking for an extension key which isn\'t used', function () { + expect(extensions.getPublicUrls('anotherListThatDoesntExist')).toEqual([]) + }) + }) + + describe('getAppViews', () => { + it('should be a function', () => { + expect(extensions.getAppViews).toBeInstanceOf(Function) + }) + + it('should output govuk-frontend nunjucks paths as an array', () => { + expect(extensions.getAppViews()).toEqual(joinPaths([ + 'node_modules/govuk-frontend/components', + 'node_modules/govuk-frontend' + ])) + }) + + it('should also output hmcts-frontend nunjucks paths after it is installed', () => { + mockExtensionConfig('hmcts-frontend', { + nunjucksPaths: [ + '/my-components', + '/my-layouts' + ] + }) + + expect(extensions.getAppViews()).toEqual(joinPaths([ + 'node_modules/hmcts-frontend/my-layouts', + 'node_modules/hmcts-frontend/my-components', + 'node_modules/govuk-frontend/components', + 'node_modules/govuk-frontend' + ])) + }) + + it('should not output any nunjucks paths when frontends are uninstalled', () => { + mockUninstallExtension('govuk-frontend') + + expect(extensions.getAppViews()).toEqual([]) + }) + + it('should also output provided paths in the array', () => { + expect(extensions.getAppViews(joinPaths([ + '/app/views', + '/lib' + ]))).toEqual(joinPaths([ + 'node_modules/govuk-frontend/components', + 'node_modules/govuk-frontend', + '/app/views', + '/lib' + ])) + }) + + it('should output any provided paths in the array', () => { + expect(extensions.getAppViews([ + '/my-new-views-directory' + ])).toEqual([ + path.join(rootPath, 'node_modules/govuk-frontend/components'), + path.join(rootPath, 'node_modules/govuk-frontend'), + '/my-new-views-directory' + ]) + }) + }) + + describe('getAppConfig', () => { + it('returns an object', () => { + expect(extensions.getAppConfig()).toBeInstanceOf(Object) + }) + + it('should have script and stylesheet keys', () => { + expect(Object.keys(extensions.getAppConfig())).toEqual(['scripts', 'stylesheets']) + }) + + it('should return a list of public urls for the scripts', () => { + expect(extensions.getAppConfig().scripts).toEqual([ + '/extension-assets/govuk-frontend/all.js' + ]) + }) + + it('should return a list of public urls for the stylesheets', () => { + expect(extensions.getAppConfig().stylesheets).toEqual([]) + }) + + it('should include installed extensions', () => { + mockExtensionConfig('my-extension', { scripts: ['/abc/def/ghi.js'] }) + expect(extensions.getAppConfig().scripts).toEqual([ + '/extension-assets/govuk-frontend/all.js', + '/extension-assets/my-extension/abc/def/ghi.js' + ]) + }) + + it('should return a list of public urls for the stylesheets', () => { + expect(extensions.getAppConfig().stylesheets).toEqual([]) + }) + + it('should include installed extensions', () => { + mockExtensionConfig('my-extension', { stylesheets: ['/abc/def/ghi.css'] }) + expect(extensions.getAppConfig().stylesheets).toEqual([ + '/extension-assets/my-extension/abc/def/ghi.css' + ]) + }) + }) + + describe('error handling', () => { + it('should cope with keys which aren\'t arrays', () => { + mockExtensionConfig('my-fixable-extension', { stylesheets: '/abc.css' }) + mockExtensionConfig('another-fixable-extension', { stylesheets: '/abc.css' }) + + expect(extensions.getAppConfig().stylesheets).toEqual([ + '/extension-assets/another-fixable-extension/abc.css', + '/extension-assets/my-fixable-extension/abc.css' + ]) + }) + it('should throw if paths use backslashes', () => { + mockExtensionConfig('my-unfixable-extension', { stylesheets: '\\abc\\def.css' }) + mockExtensionConfig('another-fixable-extension', { stylesheets: ['/abc.css'] }) + + const expectedError = new Error('Can\'t use backslashes in extension paths - "my-unfixable-extension" used "\\abc\\def.css".') + + expect(() => { + extensions.getFileSystemPaths('stylesheets') + }).toThrow(expectedError) + + expect(() => { + extensions.getPublicUrlAndFileSystemPaths('stylesheets') + }).toThrow(expectedError) + }) + it('should throw if paths use backslashes further into the path', () => { + mockExtensionConfig('my-other-unfixable-extension', { stylesheets: ['/abc\\def.css'] }) + const expectedError2 = new Error('Can\'t use backslashes in extension paths - "my-other-unfixable-extension" used "/abc\\def.css".') + + expect(() => { + extensions.getFileSystemPaths('stylesheets') + }).toThrow(expectedError2) + + expect(() => { + extensions.getPublicUrlAndFileSystemPaths('stylesheets') + }).toThrow(expectedError2) + }) + it('should throw if it doesn\'t start with a forward slash', () => { + mockExtensionConfig('yet-another-unfixable-extension', { stylesheets: ['abc.css'] }) + + const noLeadingForwardSlashError = new Error('All extension paths must start with a forward slash - "yet-another-unfixable-extension" used "abc.css".') + + expect(() => { + extensions.getFileSystemPaths('stylesheets') + }).toThrow(noLeadingForwardSlashError) + + expect(() => { + extensions.getPublicUrlAndFileSystemPaths('stylesheets') + }).toThrow(noLeadingForwardSlashError) + }) + }) + + const setupFakeFilesystem = () => { + const prepFilePath = filePath => { + return (filePath).replace(rootPath + path.sep, '').split(path.sec) + } + + spyOn(fs, 'readFileSync').and.callFake(function (filePath) { + const trimmedPath = prepFilePath(filePath) + if (doesFileExitInMockFileSystem(trimmedPath)) { + return readFileFromMockFileSystem(trimmedPath) + } else { + const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) + err.code = 'ENOENT' + throw err + } + }) + spyOn(fs, 'existsSync').and.callFake(filePath => doesFileExitInMockFileSystem(prepFilePath(filePath))) + } + + const addFileToMockFileSystem = (pathParts, content) => { + testScope.fileSystem[path.join(...pathParts)] = content + } + + const readFileFromMockFileSystem = (pathParts) => testScope.fileSystem[path.join(...pathParts)] + + const doesFileExitInMockFileSystem = (pathParts) => { + return testScope.fileSystem.hasOwnProperty(path.join(...pathParts)) + } + + const mockInstallExtension = (packageName, version = '^0.0.1') => { + const existingPackageJson = JSON.parse(readFileFromMockFileSystem(['package.json'])) + existingPackageJson.dependencies[packageName] = version + addFileToMockFileSystem(['package.json'], JSON.stringify(existingPackageJson)) + extensions.setExtensionsByType() + } + + const mockUninstallExtension = (packageName) => { + const existingPackageJson = JSON.parse(readFileFromMockFileSystem(['package.json'])) + if (!existingPackageJson.dependencies.hasOwnProperty(packageName)) { + throw new Error(`Could not uninstall '${packageName}' as it is not installed`) + } + delete existingPackageJson.dependencies[packageName] + addFileToMockFileSystem(['package.json'], JSON.stringify(existingPackageJson)) + extensions.setExtensionsByType() + } + + const mockExtensionConfig = (packageName, config = {}, version) => { + addFileToMockFileSystem(['node_modules', packageName, 'govuk-prototype-kit.config.json'], JSON.stringify(config, null, 2)) + mockInstallExtension(packageName, version) + } +}) diff --git a/lib/middleware/authentication/authentication.js b/lib/middleware/authentication/authentication.js new file mode 100644 index 0000000000..f7f04ebaa9 --- /dev/null +++ b/lib/middleware/authentication/authentication.js @@ -0,0 +1,42 @@ +/** + * Simple basic auth middleware for use with Express 4.x. + * + * Based on template found at: http://www.danielstjules.com/2014/08/03/basic-auth-with-express-4/ + * + * @example + * const authentication = required('authentication.js') + * app.use(authentication) + * + * @param {string} req Express Request object + * @param {string} res Express Response object + * @returns {function} Express 4 middleware requiring the given credentials + */ + +module.exports = function (req, res, next) { + // NPM Dependencies + const basicAuth = require('basic-auth') + + // Local dependencies + const config = require('../../../app/config.js') + + // Local Variables + const env = (process.env.NODE_ENV || 'development').toLowerCase() + const useAuth = (process.env.USE_AUTH || config.useAuth).toLowerCase() + const username = process.env.USERNAME + const password = process.env.PASSWORD + + if (env === 'production' && useAuth === 'true') { + if (!username || !password) { + console.error('Username or password is not set.') + return res.send('

    Error:

    Username or password not set. See guidance for setting these.

    ') + } + + const user = basicAuth(req) + + if (!user || user.name !== username || user.pass !== password) { + res.set('WWW-Authenticate', 'Basic realm=Authorization Required') + return res.sendStatus(401) + } + } + next() +} diff --git a/lib/middleware/authentication/authentication.test.js b/lib/middleware/authentication/authentication.test.js new file mode 100644 index 0000000000..cd3997f643 --- /dev/null +++ b/lib/middleware/authentication/authentication.test.js @@ -0,0 +1,162 @@ +/* eslint-env jest */ +// NPM dependencies +jest.mock('basic-auth') +jest.mock('express/lib/response') +const basicAuth = require('basic-auth') + +// Local dependencies +let authentication = require('./authentication.js') + +// Local variables +const userDetails = { + name: 'secret-username', + pass: 'secure-password' +} +const next = jest.fn() + +const res = require('express/lib/response') + +// Mock console.log so we can check any output +console.error = jest.fn() + +describe('authentication', () => { + it('should be a function', () => { + expect(authentication).toBeInstanceOf(Function) + }) + + describe('when it runs in production', () => { + beforeAll(() => { + process.env.NODE_ENV = 'production' + process.env.USE_AUTH = 'true' + }) + + describe('server with no username/password set', () => { + beforeAll(() => { + delete process.env.USERNAME + delete process.env.PASSWORD + }) + + beforeEach(() => { + // Jest mocks stores each call to the mocked function + // so we want to clear them before running the authentication again. + console.error.mockClear() + authentication({}, res, next) + }) + + it('should return a console error', () => { + let consoleErrorMessage = console.error.mock.calls[0][0] + expect(consoleErrorMessage).toBe('Username or password is not set.') + }) + + it('should send marked up error message', () => { + let errorDisplayedToUser = res.send.mock.calls[0][0] + expect(errorDisplayedToUser).toBe('

    Error:

    Username or password not set. See guidance for setting these.

    ') + }) + }) + + describe('server with username/password set', () => { + beforeAll(() => { + process.env.USERNAME = 'secret-username' + process.env.PASSWORD = 'secure-password' + }) + + describe('when a user supplies correct username/password', () => { + beforeEach(() => { + jest.clearAllMocks() + basicAuth.mockReturnValue(userDetails) + authentication({}, res, next) + }) + + it('should not return a console error', () => { + expect(console.error).not.toBeCalled() + }) + + it('should call basic-auth and return username/password', () => { + expect(basicAuth).toReturnWith(userDetails) + }) + + it('should not set authentication header', () => { + expect(res.set).not.toBeCalled() + }) + + it('should not return a status', () => { + expect(res.sendStatus).not.toBeCalled() + }) + + it('should progress to the next middleware', () => { + expect(next).toBeCalled() + }) + }) + + describe('when a user supplies incorrect username/password', () => { + beforeEach(() => { + jest.clearAllMocks() + basicAuth.mockReturnValue(undefined) + authentication({}, res, next) + }) + + it('should not return a console error', () => { + expect(console.error).not.toBeCalled() + }) + + it('should not send error message to the browser', () => { + expect(res.send).not.toBeCalled() + }) + + it('should call basic-auth and return username/password', () => { + expect(basicAuth).toReturnWith(undefined) + }) + + it('should set authentication header', () => { + expect(res.set).toHaveBeenCalledWith('WWW-Authenticate', 'Basic realm=Authorization Required') + }) + + it('should return 401/UnAuthorized', () => { + expect(res.sendStatus).toHaveBeenCalledWith(401) + }) + + it('should not progress to the next middleware', () => { + expect(next).not.toBeCalled() + }) + }) + }) + }) + + describe('when it runs in non-production enviroment (dev by default)', () => { + beforeAll(() => { + // Jest automatically sets NODE_ENV to 'test' + // but we want to test when there is no NODE_ENV and it defaults + // to development + delete process.env.NODE_ENV + }) + + beforeEach(() => { + jest.clearAllMocks() + authentication({}, res, next) + }) + + it('should not call console error', () => { + expect(console.error).not.toBeCalled() + }) + + it('should not send marked up error message', () => { + expect(res.send).not.toBeCalled() + }) + + it('should not call basic-auth', () => { + expect(basicAuth).not.toBeCalled() + }) + + it('should not set authentication header', () => { + expect(res.set).not.toBeCalled() + }) + + it('should not return 401/UnAuthorized', () => { + expect(res.sendStatus).not.toBeCalled() + }) + + it('should progress to the next middleware', () => { + expect(next).toBeCalled() + }) + }) +}) diff --git a/lib/middleware/extensions/extensions.js b/lib/middleware/extensions/extensions.js new file mode 100644 index 0000000000..9475ae9cd5 --- /dev/null +++ b/lib/middleware/extensions/extensions.js @@ -0,0 +1,17 @@ +const express = require('express') +const extensions = require('../../extensions/extensions') +const router = express.Router() + +// Serve assets from extensions +function setupPathsFor (item) { + extensions.getPublicUrlAndFileSystemPaths(item) + .forEach(paths => { + router.use(paths.publicUrl, express.static(paths.fileSystemPath)) + }) +} + +setupPathsFor('scripts') +setupPathsFor('stylesheets') +setupPathsFor('assets') + +module.exports = router diff --git a/lib/prototype-admin/clear-data-success.html b/lib/prototype-admin/clear-data-success.html new file mode 100644 index 0000000000..cc80f50364 --- /dev/null +++ b/lib/prototype-admin/clear-data-success.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block pageTitle %} + Data cleared - GOV.UK Prototype Kit +{% endblock %} + +{% block content %} + +

    + Data cleared +

    + +

    + The session data has been cleared. +

    + +

    + + Prototype home page + +

    + +{% endblock %} diff --git a/lib/prototype-admin/clear-data.html b/lib/prototype-admin/clear-data.html index 14750f8484..005fa65f3c 100644 --- a/lib/prototype-admin/clear-data.html +++ b/lib/prototype-admin/clear-data.html @@ -1,23 +1,36 @@ + {% extends "layout.html" %} {% block pageTitle %} - Clear data + Clear data? - GOV.UK Prototype Kit {% endblock %} -{% block content %} +{% block beforeContent %} + {{ govukBackLink({ + "text": "Back", + "href": "javascript: window.history.go(-1)" + }) + }} +{% endblock %} -

    - Data cleared -

    +{% block content %} -

    - The session data has been cleared. -

    + +
    +
    +

    + Clear data? +

    + {{ govukWarningText({ + text: "This will clear all of the data entered in this session", + iconFallbackText: "Warning" + }) }} -

    - - Prototype home page - -

    + {{ govukButton({ + text: "Clear the data" + }) }} +
    +
    + {% endblock %} diff --git a/lib/prototype-admin/clears-data.html b/lib/prototype-admin/clears-data.html deleted file mode 100644 index 0e6a77906b..0000000000 --- a/lib/prototype-admin/clears-data.html +++ /dev/null @@ -1,36 +0,0 @@ - -{% extends "layout.html" %} - -{% block pageTitle %} - Clear data? | GOV.UK Prototype Kit -{% endblock %} - -{% block beforeContent %} - {{ govukBackLink({ - "text": "Back", - "href": "javascript: window.history.go(-1)" - }) - }} -{% endblock %} - -{% block content %} - -
    -
    -

    - Clear data? -

    - {{ govukWarningText({ - text: "This will clear all of the data entered in this session", - iconFallbackText: "Warning" - }) }} - - - {{ govukButton({ - text: "Clear the data", - href: "/prototype-admin/clear-data" - }) }} -
    -
    - -{% endblock %} diff --git a/lib/utils.js b/lib/utils.js index ba0722d16e..b3adf58100 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,7 +2,6 @@ const fs = require('fs') // NPM dependencies -const basicAuth = require('basic-auth') const getKeypath = require('keypather/get') const marked = require('marked') const path = require('path') @@ -64,37 +63,6 @@ exports.addCheckedFunction = function (env) { }) } -/** - * Simple basic auth middleware for use with Express 4.x. - * - * Based on template found at: http://www.danielstjules.com/2014/08/03/basic-auth-with-express-4/ - * - * @example - * app.use('/api-requiring-auth', utils.basicAuth('username', 'password')) - * - * @param {string} username Expected username - * @param {string} password Expected password - * @returns {function} Express 4 middleware requiring the given credentials - */ - -exports.basicAuth = function (username, password) { - return function (req, res, next) { - if (!username || !password) { - console.log('Username or password is not set.') - return res.send('

    Error:

    Username or password not set. See guidance for setting these.

    ') - } - - var user = basicAuth(req) - - if (!user || user.name !== username || user.pass !== password) { - res.set('WWW-Authenticate', 'Basic realm=Authorization Required') - return res.sendStatus(401) - } - - next() - } -} - // Find an available port to run the server on exports.findAvailablePort = function (app, callback) { var port = null @@ -180,7 +148,7 @@ exports.getLatestRelease = function () { 'GET', 'https://api.github.com/repos/alphagov/govuk-prototype-kit/releases/latest', { - headers: {'user-agent': 'node.js'} + headers: { 'user-agent': 'node.js' } } ) var data = JSON.parse(res.getBody('utf8')) @@ -245,7 +213,7 @@ exports.matchMdRoutes = function (req, res) { if (fs.existsSync(path.join(__dirname, docsPath, req.params[0] + '.md'), 'utf8')) { var doc = fs.readFileSync(path.join(__dirname, docsPath, req.params[0] + '.md'), 'utf8') var html = marked(doc) - res.render('documentation_template', {'document': html}) + res.render('documentation_template', { 'document': html }) return true } return false diff --git a/listen-on-port.js b/listen-on-port.js new file mode 100644 index 0000000000..dbcb5434c7 --- /dev/null +++ b/listen-on-port.js @@ -0,0 +1,32 @@ + +// NPM dependencies +const browserSync = require('browser-sync') + +// Local dependencies +const server = require('./server.js') +const config = require('./app/config.js') +const utils = require('./lib/utils.js') + +// Set up configuration variables +var useBrowserSync = config.useBrowserSync.toLowerCase() +var env = (process.env.NODE_ENV || 'development').toLowerCase() + +utils.findAvailablePort(server, function (port) { + console.log('Listening on port ' + port + ' url: http://localhost:' + port) + if (env === 'production' || useBrowserSync === 'false') { + server.listen(port) + } else { + server.listen(port - 50, function () { + browserSync({ + proxy: 'localhost:' + (port - 50), + port: port, + ui: false, + files: ['public/**/*.*', 'app/views/**/*.*'], + ghostmode: false, + open: false, + notify: false, + logLevel: 'error' + }) + }) + } +}) diff --git a/package.json b/package.json index de8c7b45d4..e9ce229a5d 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,19 @@ { "name": "express-prototype", "description": "GOVUK prototyping app in Express", - "version": "8.0.0", + "version": "8.7.0", "private": true, "engines": { - "node": "^8.9.1" + "node": "^10.0.0" }, "scripts": { "start": "node start.js", "lint": "standard", - "test": "gulp test && npm run lint" + "test": "npm run lint && gulp generate-assets && jest" }, "dependencies": { + "acorn": "^6.0.5", + "ansi-colors": "^3.2.3", "basic-auth": "^2.0.0", "basic-auth-connect": "^1.0.0", "body-parser": "^1.14.1", @@ -19,32 +21,27 @@ "client-sessions": "^0.8.0", "cookie-parser": "^1.4.3", "cross-spawn": "^6.0.5", + "del": "^3.0.0", "dotenv": "^6.0.0", "express": "4.16.3", "express-session": "^1.13.0", "express-writer": "0.0.4", + "fancy-log": "^1.3.3", "govuk-elements-sass": "^3.1.3", - "govuk-frontend": "^2.0.0", + "govuk-frontend": "^2.7.0", "govuk_frontend_toolkit": "^7.5.0", "govuk_template_jinja": "^0.24.1", - "gulp": "^3.9.1", - "gulp-clean": "^0.4.0", - "gulp-mocha": "^6.0.0", + "gulp": "^4.0.0", "gulp-nodemon": "^2.1.0", "gulp-sass": "^4.0.1", "gulp-sourcemaps": "^2.6.0", - "gulp-util": "^3.0.7", "keypather": "^3.0.0", "marked": "^0.4.0", - "minimist": "1.2.0", "notifications-node-client": "^4.1.0", "nunjucks": "^3.1.3", "portscanner": "^2.1.1", "prompt": "^1.0.0", "require-dir": "^1.0.0", - "run-sequence": "^2.2.1", - "standard": "^11.0.1", - "supertest": "^3.0.0", "sync-request": "^6.0.0", "universal-analytics": "^0.4.16", "uuid": "^3.2.1" @@ -53,5 +50,10 @@ "ignore": [ "nunjucks" ] + }, + "devDependencies": { + "jest": "^23.6.0", + "standard": "^12.0.1", + "supertest": "^3.0.0" } } diff --git a/server.js b/server.js index 1eb52695dc..3d58729cbe 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,6 @@ const path = require('path') // NPM dependencies const bodyParser = require('body-parser') -const browserSync = require('browser-sync') const dotenv = require('dotenv') const express = require('express') const nunjucks = require('nunjucks') @@ -15,11 +14,16 @@ const cookieParser = require('cookie-parser') dotenv.config() // Local dependencies +const middleware = [ + require('./lib/middleware/authentication/authentication.js'), + require('./lib/middleware/extensions/extensions.js') +] const config = require('./app/config.js') const documentationRoutes = require('./docs/documentation_routes.js') const packageJson = require('./package.json') const routes = require('./app/routes.js') const utils = require('./lib/utils.js') +const extensions = require('./lib/extensions/extensions.js') // Variables for v6 backwards compatibility // Set false by default, then turn on if we find /app/v6/routes.js @@ -51,20 +55,13 @@ documentationApp.use(handleCookies) // Set up configuration variables var releaseVersion = packageJson.version -var username = process.env.USERNAME -var password = process.env.PASSWORD -var env = process.env.NODE_ENV || 'development' -var useAuth = process.env.USE_AUTH || config.useAuth +var env = (process.env.NODE_ENV || 'development').toLowerCase() var useAutoStoreData = process.env.USE_AUTO_STORE_DATA || config.useAutoStoreData var useCookieSessionStore = process.env.USE_COOKIE_SESSION_STORE || config.useCookieSessionStore var useHttps = process.env.USE_HTTPS || config.useHttps -var useBrowserSync = config.useBrowserSync var gtmId = process.env.GOOGLE_TAG_MANAGER_TRACKING_ID -env = env.toLowerCase() -useAuth = useAuth.toLowerCase() useHttps = useHttps.toLowerCase() -useBrowserSync = useBrowserSync.toLowerCase() var useDocumentation = (config.useDocumentation === 'true') @@ -83,25 +80,27 @@ if (isSecure) { app.set('trust proxy', 1) // needed for secure cookies on heroku } -// Ask for username and password on production -if (env === 'production' && useAuth === 'true') { - app.use(utils.basicAuth(username, password)) -} +middleware.forEach(func => app.use(func)) // Set up App -var appViews = [ - path.join(__dirname, '/node_modules/govuk-frontend/'), - path.join(__dirname, '/node_modules/govuk-frontend/components'), +var appViews = extensions.getAppViews([ path.join(__dirname, '/app/views/'), path.join(__dirname, '/lib/') -] +]) -var nunjucksAppEnv = nunjucks.configure(appViews, { +var nunjucksConfig = { autoescape: true, - express: app, noCache: true, - watch: true -}) + watch: false // We are now setting this to `false` (it's by default false anyway) as having it set to `true` for production was making the tests hang +} + +if (env === 'development') { + nunjucksConfig.watch = true +} + +nunjucksConfig.express = app + +var nunjucksAppEnv = nunjucks.configure(appViews, nunjucksConfig) // Add Nunjucks filters utils.addNunjucksFilters(nunjucksAppEnv) @@ -111,9 +110,8 @@ app.set('view engine', 'html') // Middleware to serve static assets app.use('/public', express.static(path.join(__dirname, '/public'))) -app.use('/assets', express.static(path.join(__dirname, 'node_modules', 'govuk-frontend', 'assets'))) -// Serve govuk-frontend in /public +// Serve govuk-frontend in from node_modules (so not to break pre-extenstions prototype kits) app.use('/node_modules/govuk-frontend', express.static(path.join(__dirname, '/node_modules/govuk-frontend'))) // Set up documentation app @@ -125,12 +123,8 @@ if (useDocumentation) { path.join(__dirname, '/lib/') ] - var nunjucksDocumentationEnv = nunjucks.configure(documentationViews, { - autoescape: true, - express: documentationApp, - noCache: true, - watch: true - }) + nunjucksConfig.express = documentationApp + var nunjucksDocumentationEnv = nunjucks.configure(documentationViews, nunjucksConfig) // Nunjucks filters utils.addNunjucksFilters(nunjucksDocumentationEnv) @@ -151,13 +145,9 @@ if (useV6) { path.join(__dirname, '/app/v6/views/'), path.join(__dirname, '/lib/v6') // for old unbranded template ] + nunjucksConfig.express = v6App + var nunjucksV6Env = nunjucks.configure(v6Views, nunjucksConfig) - var nunjucksV6Env = nunjucks.configure(v6Views, { - autoescape: true, - express: v6App, - noCache: true, - watch: true - }) // Nunjucks filters utils.addNunjucksFilters(nunjucksV6Env) @@ -188,6 +178,8 @@ app.locals.cookieText = config.cookieText app.locals.promoMode = promoMode app.locals.releaseVersion = 'v' + releaseVersion app.locals.serviceName = config.serviceName +// extensionConfig sets up variables used to add the scripts and stylesheets to each page. +app.locals.extensionConfig = extensions.getAppConfig() // Session uses service name to avoid clashes with other prototypes const sessionName = 'govuk-prototype-kit-' + (Buffer.from(config.serviceName, 'utf8')).toString('hex') @@ -227,9 +219,9 @@ if (useAutoStoreData === 'true') { } // Clear all data in session if you open /prototype-admin/clear-data -app.get('/prototype-admin/clear-data', function (req, res) { +app.post('/prototype-admin/clear-data', function (req, res) { req.session.data = {} - res.render('prototype-admin/clear-data') + res.render('prototype-admin/clear-data-success') }) // Redirect root to /docs when in promo mode. @@ -350,25 +342,4 @@ app.use(function (err, req, res, next) { console.log('\nGOV.UK Prototype Kit v' + releaseVersion) console.log('\nNOTICE: the kit is for building prototypes, do not use it for production services.') -// Find a free port and start the server -utils.findAvailablePort(app, function (port) { - console.log('Listening on port ' + port + ' url: http://localhost:' + port) - if (env === 'production' || useBrowserSync === 'false') { - app.listen(port) - } else { - app.listen(port - 50, function () { - browserSync({ - proxy: 'localhost:' + (port - 50), - port: port, - ui: false, - files: ['public/**/*.*', 'app/views/**/*.*'], - ghostmode: false, - open: false, - notify: false, - logLevel: 'error' - }) - }) - } -}) - module.exports = app diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 4a52320178..0000000000 --- a/test/mocha.opts +++ /dev/null @@ -1 +0,0 @@ ---recursive diff --git a/test/spec/sanity-checks.js b/test/spec/sanity-checks.js deleted file mode 100644 index 6773ac7dea..0000000000 --- a/test/spec/sanity-checks.js +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-env mocha */ -var request = require('supertest') -var app = require('../../server.js') -var path = require('path') -var fs = require('fs') -var assert = require('assert') - -/** - * Basic sanity checks on the dev server - */ -describe('The Prototype Kit', function () { - it('should generate assets into the /public folder', function () { - assert.doesNotThrow(function () { - fs.accessSync(path.resolve(__dirname, '../../public/javascripts/application.js')) - fs.accessSync(path.resolve(__dirname, '../../public/images/unbranded.ico')) - fs.accessSync(path.resolve(__dirname, '../../public/stylesheets/application.css')) - }) - }) - - it('should send with a well formed response for the index page', function (done) { - request(app) - .get('/') - .expect('Content-Type', /text\/html/) - .expect(200) - .end(function (err, res) { - if (err) { - done(err) - } else { - done() - } - }) - }) - - it('should send with a well formed response for the docs page', function (done) { - request(app) - .get('/docs') - .expect('Content-Type', /text\/html/) - .expect(200) - .end(function (err, res) { - if (err) { - done(err) - } else { - done() - } - }) - }) -})