diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 80743efcaa..c37bd56057 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -23,9 +23,9 @@ assignees: '' ## Environment (where applicable) - + - Operating system: - Browser: - Browser version: -- GOV.UK Prototype Kit version: +- Prototype Kit version: diff --git a/.github/workflows/test-acceptance.yaml b/.github/workflows/test-acceptance.yaml index 7b879b4acf..12c6e606fa 100644 --- a/.github/workflows/test-acceptance.yaml +++ b/.github/workflows/test-acceptance.yaml @@ -14,8 +14,8 @@ jobs: fail-fast: false # continue other tests if one test in matrix fails matrix: node-version: [20.x] - os: [macos-latest, windows-latest, ubuntu-latest] - type: [smoke, plugins, styles, dev, prod, errors] + os: [macos-latest, ubuntu-latest] + type: [plugins, dev] name: Acceptance ${{ matrix.type }} test kit on Node v${{ matrix.node-version }} (${{ matrix.os }}) runs-on: ${{ matrix.os }} @@ -44,11 +44,6 @@ jobs: - run: npm ci - run: npm run test:acceptance:${{ matrix.type }} - env: - CYPRESS_REQUEST_TIMEOUT: 20000 - CYPRESS_DEFAULT_COMMAND_TIMEOUT: 40000 - CYPRESS_PAGE_LOAD_TIMEOUT: 120000 - CYPRESS_RETRIES: 3 - if: ${{ failure() }} uses: actions/upload-artifact@v3 diff --git a/.github/workflows/test-heroku.yaml b/.github/workflows/test-heroku.yaml deleted file mode 100644 index 1b15ea5544..0000000000 --- a/.github/workflows/test-heroku.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Tests (Heroku) - -on: - push: - branches: - - main - - support/* - pull_request: - -jobs: - test-heroku: - - name: Test kit on Heroku - runs-on: ubuntu-latest - - env: - CYPRESS_CACHE_FOLDER: ~/.cache/Cypress - - steps: - - uses: actions/checkout@v3 - - name: Use Node v18 - uses: actions/setup-node@v3 - with: - cache: 'npm' - node-version: '20' - - name: Cache Cypress binary - uses: actions/cache@v3 - with: - path: ~/.cache/Cypress - key: cypress-${{ runner.os }}-cypress-${{ hashFiles('**/package.json') }} - - run: npm ci - - run: npm run test:heroku diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 0dc1eb99da..fa2f3e8d4c 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -15,4 +15,6 @@ jobs: steps: - uses: actions/checkout@v3 - - run: npx govuk-prototype-kit@snapshot validate-plugin + - run: npm ci + + - run: ./bin/cli validate-plugin validate-plugin diff --git a/.nvmrc b/.nvmrc index 9de2256827..eb800ed459 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/iron +v18.19.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e6bda10bd0..52eec112ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1600 +1,9 @@ # Changelog -## Unreleased +## New Features -### Fixes + - Striped out GOV.UK References + - Put in Now Prototype It branding (unstyled) + - New Plugin Details page -- [#2327: Use GOV.UK Frontend v5 on management pages and tests](https://github.com/alphagov/govuk-prototype-kit/pull/2327) - -## 13.16.0 - -### New features - -- [#2384: Add format items filter to core filters](https://github.com/alphagov/govuk-prototype-kit/pull/2384) -- [#2382: Make any npm module a plugin via a proxy plugin config](https://github.com/alphagov/govuk-prototype-kit/pull/2382) - -## 13.15.3 - -### Fixes - -- [#2380: Only display packages that are plugins](https://github.com/alphagov/govuk-prototype-kit/pull/2380) - -## 13.15.2 - -### Fixes - -- [#2378: Add filters and functions when the environment is ready](https://github.com/alphagov/govuk-prototype-kit/pull/2378) - -## 13.15.1 - -### Fixes - -- [#2375: Plugin validator should support nunjucks functions](https://github.com/alphagov/govuk-prototype-kit/pull/2375) - -## 13.15.0 - -### New features - -- [#2369: Support multiple passwords](https://github.com/alphagov/govuk-prototype-kit/pull/2369) - -## 13.14.1 - -### Fixes - -- [#2370: Support no internet connection](https://github.com/alphagov/govuk-prototype-kit/pull/2370) - -## 13.14.0 - -### New features - -- [#2363: Support for the new Node LTS (version 20)](https://github.com/alphagov/govuk-prototype-kit/pull/2363) - -## 13.13.6 - -### Fixes - -- [#2364: Fix plugin update detection](https://github.com/alphagov/govuk-prototype-kit/pull/2364) - -## 13.13.5 - -### Fixes - -- [#2355: Prevent management pages using "plugin" GOV.UK Frontend views](https://github.com/alphagov/govuk-prototype-kit/pull/2355) -- [#2356: Only allow plugin update functionality when installed from npm](https://github.com/alphagov/govuk-prototype-kit/pull/2356) -- [#2357: Simplify and improve acceptance tests](https://github.com/alphagov/govuk-prototype-kit/pull/2357) -- [#2358: Suppress Sass warnings for `$legacy` deprecated colour palette](https://github.com/alphagov/govuk-prototype-kit/pull/2358) -- [#2359: Update application.js to ` -``` -- modify `app/assets/javascripts/application.js` file to initialise the JavaScript -``` -$(document).ready(function () { - window.GOVUKFrontend.initAll() -}) -``` - -### New features - -- [#501 Add default session data](https://github.com/alphagov/govuk_prototype_kit/pull/501) -- [#502 Add Cookies and Privacy policy text](https://github.com/alphagov/govuk_prototype_kit/pull/502) -- [#521 Do not track users who have enabled 'DoNotTrack'](https://github.com/alphagov/govuk_prototype_kit/pull/521) -- [#522 Add inline-code block styles](https://github.com/alphagov/govuk_prototype_kit/pull/522) -- [#523 Track app usage](https://github.com/alphagov/govuk_prototype_kit/pull/523) -- [#525 Add design system message to home page](https://github.com/alphagov/govuk_prototype_kit/pull/525) - -### Bug fixes - -- [#530 Update elements class to frontend on examples page](https://github.com/alphagov/govuk_prototype_kit/pull/530) -- [#491 Remove redundant Google Analytics](https://github.com/alphagov/govuk_prototype_kit/pull/491) -- [#524 Make "Prototype Kit" casing consistent](https://github.com/alphagov/govuk_prototype_kit/pull/524) -- [#527 Update docs/index page to include same information as private beta](https://github.com/alphagov/govuk_prototype_kit/pull/527) - -To see the previous private beta releases see the archived [private beta repository](https://github.com/alphagov/govuk-prototype-kit-private-beta/blob/master/CHANGELOG.md#700-beta9). - -## 6.3.0 - -### New features - -- [#430 Recommend Atom over Sublime text](https://github.com/alphagov/govuk_prototype_kit/pull/430) -- [#415 Update to govuk-elements-sass v3.1.1](https://github.com/alphagov/govuk_prototype_kit/pull/415) -- [#422 fix(package): update govuk_template_jinja to version 0.22.3](https://github.com/alphagov/govuk_prototype_kit/pull/422) -- [#401 Update govuk_template_jinja to 0.22.2](https://github.com/alphagov/govuk_prototype_kit/pull/401) -- [#409 Update govuk_frontend_toolkit to 7.0.0](https://github.com/alphagov/govuk_prototype_kit/pull/409) -- [#406 Add documentation for creating a release](https://github.com/alphagov/govuk_prototype_kit/pull/406) -- [#410 Copyright should be Crown Copyright](https://github.com/alphagov/govuk_prototype_kit/pull/410) -- [#407 Support deprecated check-your-answers table styles](https://github.com/alphagov/govuk_prototype_kit/pull/407) - -### Bug fixes - -- [#431 Remove the Heroku deploy provider from Travis](https://github.com/alphagov/govuk_prototype_kit/pull/431) -- [#405 Upgrade node.js version for Heroku](https://github.com/alphagov/govuk_prototype_kit/pull/405) - -## 6.2.0 - -### New features -- [#365 Improvements to Check your answers page](https://github.com/alphagov/govuk_prototype_kit/pull/365) -- [#398 Bump govuk_template_jinja to 0.22.1](https://github.com/alphagov/govuk_prototype_kit/pull/398) - -### Bug fixes -- [#405 Upgrade node.js version for Heroku](https://github.com/alphagov/govuk_prototype_kit/pull/405) -- [#399 Fix JS error in Safari’s Private Browsing mode](https://github.com/alphagov/govuk_prototype_kit/pull/399) - -## 6.1.0 - -### New features - -- [#386 Add GOV.UK Notify client library to kit](https://github.com/alphagov/govuk_prototype_kit/pull/386) -- [#383 Add .env file to support storing private data](https://github.com/alphagov/govuk_prototype_kit/pull/383) -- [#347 Add ie8 elements support](https://github.com/alphagov/govuk_prototype_kit/pull/347) -- [#349 Add IE 8 bind polyfill](https://github.com/alphagov/govuk_prototype_kit/pull/349) -- [#373 add page_scripts block](https://github.com/alphagov/govuk_prototype_kit/pull/373) -- [#371 Update README](https://github.com/alphagov/govuk_prototype_kit/pull/371) - -## 6.0.0 - -### New features - -- [#369 Add template pages for content and questions](https://github.com/alphagov/govuk_prototype_kit/pull/369) -- [#340 Auto data session 4](https://github.com/alphagov/govuk_prototype_kit/pull/340) -- [#367 Added config to turn off browser sync](https://github.com/alphagov/govuk_prototype_kit/pull/367) -- [#368 Update Travis deployment to be consistent with other govuk frontend repos](https://github.com/alphagov/govuk_prototype_kit/pull/368) -- [#361 Add an example of the task list pattern](https://github.com/alphagov/govuk_prototype_kit/pull/361) -- [#364 Use GOV.UK elements v3.0.1](https://github.com/alphagov/govuk_prototype_kit/pull/364) -- [#360 Bump govuk_frontend_toolkit to 5.1.1](https://github.com/alphagov/govuk_prototype_kit/pull/360) -- [#352 bump gulp-sass to increase node-sass dependency](https://github.com/alphagov/govuk_prototype_kit/pull/352) - -### Bug fixes - -- [#356 fix download link](https://github.com/alphagov/govuk_prototype_kit/pull/356) -- [#357 fix docs links](https://github.com/alphagov/govuk_prototype_kit/pull/357) -- [#354 Allow search indexing in promo mode](https://github.com/alphagov/govuk_prototype_kit/pull/354) - -## 5.1.0 - -### New features - -- [#335 Add ability to override service name on a page](https://github.com/alphagov/govuk_prototype_kit/pull/335) - -### Bug fixes - -- [#350 Prevent asking users to authenticate twice](https://github.com/alphagov/govuk_prototype_kit/pull/350) -- [#344 Removing links to route.js / updating example in branching.html](https://github.com/alphagov/govuk_prototype_kit/pull/344) -- [#343 Remove the title attribute from the cookie message](https://github.com/alphagov/govuk_prototype_kit/pull/343) -- [#341 fix css sourcemaps](https://github.com/alphagov/govuk_prototype_kit/pull/341) -- [#337 Add Git step to Heroku guide](https://github.com/alphagov/govuk_prototype_kit/pull/337) -- [#336 Use app.locals instead of app.use](https://github.com/alphagov/govuk_prototype_kit/pull/336) - -## 5.0.1 - -- [#330 Update GOV.UK toolkit and StandardJS to latest](https://github.com/alphagov/govuk_prototype_kit/pull/330) -- [#328 Update GOV.UK template to latest](https://github.com/alphagov/govuk_prototype_kit/pull/328) -- [#324 Fix the example question page’s back link](https://github.com/alphagov/govuk_prototype_kit/pull/324) - -## 5.0.0 - -### Breaking changes - -- [#284 Use Gulp instead of Grunt](https://github.com/alphagov/govuk_prototype_kit/pull/284) - -Use [Gulp.js](http://gulpjs.com/) rather than [Grunt](http://gruntjs.com/) as a build tool. -It is recommended to [install Gulp globally](https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md), do so using: - -`npm install --global gulp-cli` - -### All changes - -The short version: - -- [#311 Update govuk-elements-sass to 2.2.0](https://github.com/alphagov/govuk_prototype_kit/pull/311) -- [#308 Change node version from 4 to 6](https://github.com/alphagov/govuk_prototype_kit/pull/308) -- [#299 Basic sanity check test suite](https://github.com/alphagov/govuk_prototype_kit/pull/299) -- [#296 Keep the latest release branch up-to-date](https://github.com/alphagov/govuk_prototype_kit/pull/296) -- Fix broken links for the documentation app - -The extended version: - -This release includes custom radio buttons and checkbox styles from govuk-elements-sass v2.2.0. -The version of Node that the prototype kit uses has been updated, we recommend using LTS (version 6 or above). -Travis will now run tests against each pull request to ensure that the app runs (by checking the server and build tasks). -The latest-release branch can be used to update the prototype kit. Instructions for [updating your version of the prototype kit via the latest-release branch can be found here](https://govuk-prototype-kit.herokuapp.com/docs/updating-the-kit#updating-via-the-command-line-advanced-). - -## 4.0.0 - -### Breaking changes - -- [#244](https://github.com/alphagov/govuk_prototype_kit/pull/244) Migrate documentation into a separate application - -### All changes - -- Bump all GOV.UK assets to their latest versions -- Remove duplicate GOV.UK assets copied to the app -- [#241](https://github.com/alphagov/govuk_prototype_kit/pull/241) Warn against using the prototype kit to build production services -- [#268](https://github.com/alphagov/govuk_prototype_kit/pull/268) Automatically keep the latest release branch up to date. This can be used to update the kit -- [#270](https://github.com/alphagov/govuk_prototype_kit/pull/270) Add a new stylesheet for the unbranded layout to fix font issues -- [#257](https://github.com/alphagov/govuk_prototype_kit/pull/257) Make CSS output easier to debug (with sourcemaps) -- [#237](https://github.com/alphagov/govuk_prototype_kit/pull/237) Make links with role="button" behave like buttons -- [#224](https://github.com/alphagov/govuk_prototype_kit/pull/224) Lint the prototype kit’s codebase using [Standard](http://standardjs.com/). This only applies to the kit’s codebase - there’s no requirement for your app to meet this -- [#197](https://github.com/alphagov/govuk_prototype_kit/pull/197) Add the ability to store user data per session - - -## 3.0.0 - -BrowserSync support, so you don't need to refresh the browser to see your changes. Nunjucks filters file has been added, so you can [add your own filters](https://mozilla.github.io/nunjucks/api.html#custom-filters) to your project, check the examples page in the kit for more details. - -### Breaking changes - -- #188 Force SSL on production - -### All changes - -- #213 Remove references to "latest version" of Node -- #212 Remove the mustache version of govuk template -- #211 Remove govuk_template.html copied in build task from the repository -- #209 Use release 1.2.0 of the govuk-elements-sass package -- #208 Remove govuk elements sass from the app folder -- #207 Bump the govuk frontend toolkit to 4.12.0 -- #206 Bump the govuk template to 0.17.3 -- #200 Adds custom 'filters' to the nunjucks templating engine -- #194 Windows heroku login instructions -- #193 Adding browser-sync to the prototyping kit. -- #192 Security guidance -- #191 Edit sass docs for clarity -- #188 Force SSL on production -- #186 add guidance page for using verify prototype -- #181 Add link to styleguide on writing commit messages -- #180 Change smart quotes to straight quotes -- #177 Add a link to install Git -- #176 Bump govuk template to 0.17.0 -- #175 Bump govuk frontend toolkit to 4.10.0 -- #172 Fix closing element -- #169 Fix broken url and typo -- #166 Stop prototypes being indexed by search engines. -- #165 Redirect .html and .htm if in url path -- #164 Fix link to developer install instructions -- #162 Have kit self-identify as being the GOV.UK Prototype kit -- #161 always convert port to Number -- #160 Minor documentation update -- #159 Remove invalid ARIA role -- #156 fix port restart issue -- #155 Update the GOV.UK template and remove napa as a dependency -- #154 Use TRAVIS_BRANCH when running in travis-ci -- #152 Amend travis yml - - -## 2.1.0 - -New documentation to make it easier to install and run from scratch - tested with users and everything! The kit will now copy new files from assets to public (previously only updates to existing files were copied). It's easier to run multiple prototypes at once - the kit will automatically find a free port to run on. - -- Add default cookie message (#150) -- New documentation (#145) -- Add example pages for branching (#143) -- Use grunt-sync for assets (#141) -- Fix warning for npm engine (#140) -- Add tmuxp config files to gitignore (#132) -- Improve 'port in use' errors, find a new port (#130) - -## 2.0.0 - -This release switches templating language from [Mustache](http://mustache.github.io/) to [Nunjucks](https://mozilla.github.io/nunjucks/). - -### Breaking change - -To convert your old prototype pages for use with this version, [follow this guide](https://github.com/alphagov/govuk_prototype_kit/blob/master/docs/updating-the-kit.md). - -- Bump the govuk frontend toolkit to 4.6.0 (#127) -- Update govuk elements sass (#124) -- Update the prototype kit to use Nunjucks for templating (#123) -- Create config file that stores prototype configuration (#120) -- Add phase banner includes (#118) -- Use npm start as the standard way to run the app (#111) -- Add warning if folder missing or module missing (#100) -- Improve error handling around port in use, find new port (#95) -- Add body-parser for parsing POSTs (#86) -- Add question page (#72) -- Add js for toggled content (#70) -- Add Start Page (#45) -- Add Check Your Answers page (#36) -- Add confirmation page (#35) -- Upgraded to Express 4 (#32) -- Add jQuery to the kit, so it's available on all pages by default (#18) -- Add page without header and footer (#12) - -## 1.0.0 - -Initial release of prototype kit +To see historic changes before we forked the GOV.UK Prototype Kit check out their changelog from that time: https://github.com/alphagov/govuk-prototype-kit/blob/2fedd466c8628519be54aca3ac86695dfd241ae1/CHANGELOG.md diff --git a/CODEOWNERS b/CODEOWNERS index a291c8899a..c9461eaaf0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1 @@ -# Ensure any changes to GitHub Actions workflows are reviewed by developers -.github/workflows/ @alphagov/gov-uk-prototype-developers +@nataliecarey diff --git a/README.md b/README.md index 3bddf9add9..4eaa2ca30b 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,23 @@ -# GOV.UK Prototype Kit +# No Prototype It - GOV.UK Prototype Kit -Go to the [GOV.UK Prototype Kit site](https://prototype-kit.service.gov.uk/docs) to get the latest version and read the documentation. - -## About the Prototype Kit - -The 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. - -Read the [project principles](https://prototype-kit.service.gov.uk/docs/principles). - -## Make sure prototypes are password-protected - -If you publish your prototypes online, they **must** be [protected by a password](https://prototype-kit.service.gov.uk/docs/publishing). This is to prevent members of the public finding prototypes and thinking they are real services. - -You must protect user privacy at all times, even when using prototypes. Prototypes made with the kit look like GOV.UK, but do not have the same security provisions. Always make sure you are handling user data appropriately. - -## Installation instructions - -- [Installation guide for new users (non technical)](https://prototype-kit.service.gov.uk/docs/install/getting-started) -- [Installation guide for developers (technical)](https://prototype-kit.service.gov.uk/docs/install/getting-started-advanced) - -## Node version requirements - -We always recommend you use the [current long term support (LTS) version of Node.js](https://github.com/nodejs/release#release-schedule). - -The Prototype Kit always supports at least the current and previous LTS releases. +This prototype kit is designed to be compatible with [The GOV.UK Prototype Kit](https://prototype-kit.service.gov.uk/docs), ## Support -The GOV.UK Prototype Kit is maintained by the Government Digital Service. If you’ve got a question or need support you can: +At this time there is no regular support offering for this prototype kit, it's something +we want to add in the future. In the meantime you can: -* 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/app_redirect?channel=prototype-kit) -* [view known issues on GitHub](https://github.com/alphagov/govuk-prototype-kit/issues) + - [Raise an issue on GitHub](https://github.com/nowprototypeit/govuk/issues) + - [Email to request support](mailto:natalie@nataliecarey.uk) + - If you want to access [the support offered by GOV.UK](https://github.com/alphagov/govuk-prototype-kit?tab=readme-ov-file#support) you can move back to their prototype kit and then request support ## Contributing If you’ve got an idea or suggestion, you can: -* [get in touch on the developer Slack channel](https://ukgovernmentdigital.slack.com/app_redirect?channel=prototype-kit-dev) -* [create a GitHub issue](https://github.com/alphagov/govuk-prototype-kit/issues) - -The govuk-prototype-kit repository is public and we welcome contributions from anyone. - -Contributors to alphagov repositories are expected to follow the [Contributor Covenant Code of Conduct](https://github.com/alphagov/.github/blob/main/CODE_OF_CONDUCT.md#contributor-covenant-code-of-conduct). Contributors working within government are also expected to follow the [Civil Service code](https://www.gov.uk/government/publications/civil-service-code/the-civil-service-code). - -We're unable to monitor activity on this repository outside of our office hours (10am to 4pm, UK time). To get a faster response at other times, you can [report abuse or spam to GitHub](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam). +- [Raise an issue on GitHub](https://github.com/nowprototypeit/govuk/issues) +- [Raise a Pull Request on GitHub](https://github.com/nowprototypeit/govuk/pulls) ### Security -GDS is an advocate of responsible vulnerability disclosure. If you’ve found a vulnerability, we would like to know so we can fix it. - -For full details on how to tell us about vulnerabilities, [see our security policy](https://github.com/alphagov/govuk-prototype-kit/security/policy). +If you’ve found a vulnerability, we would like to know by [Raising an issue on GitHub](https://github.com/nowprototypeit/govuk/issues) so we can fix it. diff --git a/__tests__/fixtures/test-v11-prototype/app/assets/javascripts/application.js b/__tests__/fixtures/test-v11-prototype/app/assets/javascripts/application.js index e36ceb58d9..eff4ce73a4 100644 --- a/__tests__/fixtures/test-v11-prototype/app/assets/javascripts/application.js +++ b/__tests__/fixtures/test-v11-prototype/app/assets/javascripts/application.js @@ -3,7 +3,7 @@ // Warn about using the kit in production if (window.console && window.console.info) { - window.console.info('GOV.UK Prototype Kit - do not use for production') + window.console.info('Now Prototype It Kit - do not use for production') } $(document).ready(function () { diff --git a/__tests__/fixtures/test-v11-prototype/app/views/layout.html b/__tests__/fixtures/test-v11-prototype/app/views/layout.html index 4f1fe386e1..83eebab87b 100644 --- a/__tests__/fixtures/test-v11-prototype/app/views/layout.html +++ b/__tests__/fixtures/test-v11-prototype/app/views/layout.html @@ -76,5 +76,5 @@ {% include "includes/scripts.html" %} {% block pageScripts %}{% endblock %} {% endblock %} - + {% endblock %} diff --git a/__tests__/spec/errors.js b/__tests__/spec/errors.js index 8f8b777758..5681668115 100644 --- a/__tests__/spec/errors.js +++ b/__tests__/spec/errors.js @@ -30,11 +30,11 @@ const getFirstParagraph = html => { } const getErrorFile = html => { const $ = cheerio.load(html) - return $('#govuk-prototype-kit-error-file').text().trim() + return $('#nowprototypeit-error-file').text().trim() } const getErrorMessage = html => { const $ = cheerio.load(html) - return $('#govuk-prototype-kit-error-message').text().trim() + return $('#nowprototypeit-error-message').text().trim() } describe('error handling', () => { @@ -80,7 +80,7 @@ describe('error handling', () => { expect(console.error).toHaveBeenCalledWith('test error') expect(response.status).toBe(500) - expect(getPageTitle(response.text)).toEqual('Error – GOV.UK Prototype Kit – GOV.UK Prototype Kit') + expect(getPageTitle(response.text)).toEqual('Error – Now Prototype It') expect(getH1(response.text)).toEqual('There is an error') expect(getErrorFile(response.text)).toEqual(`${path.join('__tests__', 'spec', 'errors.js')} (line 73)`) expect(getErrorMessage(response.text)).toEqual('test error') @@ -100,7 +100,7 @@ describe('error handling', () => { expect(console.error).toHaveBeenCalledWith('template not found: test-page.html') expect(response.status).toBe(500) - expect(getPageTitle(response.text)).toEqual('Error – GOV.UK Prototype Kit – GOV.UK Prototype Kit') + expect(getPageTitle(response.text)).toEqual('Error – Now Prototype It') expect(getH1(response.text)).toEqual('There is an error') expect(getErrorFile(response.text)).toEqual('') expect(getErrorMessage(response.text)).toEqual('template not found: test-page.html') @@ -113,7 +113,7 @@ describe('error handling', () => { expect(console.error).not.toHaveBeenCalled() expect(response.status).toBe(404) - expect(getPageTitle(response.text)).toEqual('Page not found – GOV.UK Prototype Kit – GOV.UK Prototype Kit') + expect(getPageTitle(response.text)).toEqual('Page not found – Now Prototype It') expect(getH1(response.text)).toEqual('Page not found') expect(getFirstParagraph(response.text)).toMatch(/^There is no page at/) }) diff --git a/__tests__/spec/force-https-redirect.js b/__tests__/spec/force-https-redirect.js index e41b9600da..4f002ecd18 100644 --- a/__tests__/spec/force-https-redirect.js +++ b/__tests__/spec/force-https-redirect.js @@ -18,10 +18,6 @@ process.env.KIT_PROJECT_DIR = testDir process.env.NODE_ENV = 'production' process.env.USE_HTTPS = 'true' -jest.mock('../../lib/plugins/packages.js', () => { - return {} -}) - const app = require('../../server.js') describe('The Prototype Kit - force HTTPS redirect functionality', () => { diff --git a/__tests__/spec/migrate.js b/__tests__/spec/migrate.js deleted file mode 100644 index c3fb3b2f1f..0000000000 --- a/__tests__/spec/migrate.js +++ /dev/null @@ -1,205 +0,0 @@ -/* eslint-env jest */ - -// core dependencies -const fs = require('fs') -const os = require('os') -const path = require('path') - -// npm dependencies -const fse = require('fs-extra') - -// local dependencies -const { exec } = require('../../lib/exec') -const { mkdtempSync } = require('../utils') -const { normaliseLineEndings } = require('../../migrator/file-helpers') - -const testDirectory = mkdtempSync() -const projectDirectory = path.join(testDirectory, 'migrate-checks') -const appDirectory = path.join(projectDirectory, 'app') -const assetsDirectory = path.join(appDirectory, 'assets') -const fixtureProjectDirectory = path.join(__dirname, '..', 'fixtures', 'test-v11-prototype') -const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli') - -const pkg = { - name: 'test-prototype', - version: '11.0.0', - dependencies: { - 'govuk-frontend': '^4.3.1', - 'govuk-prototype-kit': 'file:../../..' - } -} - -function getNormalisedFileContent (path) { - return normaliseLineEndings(fs.readFileSync(path, 'utf8')) -} - -describe('migrate test prototype', () => { - beforeAll(async () => { - fse.copySync(fixtureProjectDirectory, projectDirectory, { clobber: true }) - fse.writeJsonSync(path.join(projectDirectory, 'package.json'), pkg, { clobber: true }) - await exec( - `"${process.execPath}" ${cliPath} migrate --version local ${projectDirectory}`, - { cwd: projectDirectory, env: process.env, stdio: 'inherit' } - ) - }, 240000) - - it('config.js should be replaced with config.json', () => { - expect(fs.existsSync(path.join(appDirectory, 'config.js'))).toBe(false) - - const config = fse.readJsonSync(path.join(appDirectory, 'config.json')) - - expect(config).toEqual({ - basePlugins: [ - 'govuk-prototype-kit' - ], - port: 3010, - serviceName: 'Migrate test prototype' - }) - }) - - it('package.json should be created correctly', () => { - const pkgJson = fse.readJsonSync(path.join(projectDirectory, 'package.json')) - - const { dependencies, name, scripts } = pkgJson - - expect(Object.keys(dependencies)).toEqual([ - '@govuk-prototype-kit/step-by-step', - 'govuk-frontend', - 'govuk-prototype-kit', - 'jquery', - 'notifications-node-client' - ]) - - expect(scripts).toEqual({ - dev: 'govuk-prototype-kit dev', - serve: 'govuk-prototype-kit serve', - start: 'govuk-prototype-kit start' - }) - - expect(name).toEqual('test-prototype') - }) - - it('routes.js should be updated correctly', () => { - const routesFileContents = getNormalisedFileContent(path.join(appDirectory, 'routes.js')) - - expect(routesFileContents).toEqual( - '//\n' + - '// For guidance on how to create routes see:\n' + - '// https://prototype-kit.service.gov.uk/docs/create-routes\n' + - '//\n' + - '\n' + - 'const govukPrototypeKit = require(\'govuk-prototype-kit\')\n' + - 'const router = govukPrototypeKit.requests.setupRouter()\n' + - '\n' + - '// Add your routes here\n' + - '\n\n' - ) - }) - - it('filters.js should be overwritten', () => { - const filtersFileContents = getNormalisedFileContent(path.join(appDirectory, 'filters.js')) - - expect(filtersFileContents).toEqual(`// -// For guidance on how to create filters see: -// https://prototype-kit.service.gov.uk/docs/filters -// - -const govukPrototypeKit = require('govuk-prototype-kit') -const addFilter = govukPrototypeKit.views.addFilter - -// Add your filters here - -` - ) - }) - - it('separator-2x.png should be deleted', () => { - expect(fs.existsSync(path.join(assetsDirectory, 'images', 'separator-2x.png'))).toBe(false) - }) - - it('application.js should be overwritten', () => { - const jsFileContents = getNormalisedFileContent(path.join(assetsDirectory, 'javascripts', 'application.js')) - - expect(jsFileContents).toEqual(`/* global GOVUK */ - -// -// For guidance on how to add JavaScript see: -// https://prototype-kit.service.gov.uk/docs/adding-css-javascript-and-images -// - - -window.GOVUKPrototypeKit.documentReady(function () { - // Use GOV.UK shim-links-with-button-role.js to trigger a link styled to look like a button, - // with role="button" when the space key is pressed. - GOVUK.shimLinksWithButtonRole.init() - - // Details/summary polyfill from frontend toolkit - GOVUK.details.init() - - // Show and hide toggled content - // Where .multiple-choice uses the data-target attribute - // to toggle hidden content - var showHideContent = new GOVUK.ShowHideContent() - showHideContent.init() -}) -`) - }) - - it('application.scss should be updated correctly', () => { - const sassFileContents = getNormalisedFileContent(path.join(assetsDirectory, 'sass', 'application.scss')) - - expect(sassFileContents).toEqual( - '//\n' + - '// For guidance on how to add CSS and SCSS see:\n' + - '// https://prototype-kit.service.gov.uk/docs/adding-css-javascript-and-images\n' + - '// \n' + - '\n' + - '// Add extra styles here' + - '\n\n\n' + - '.my-style {\n' + - ' font-size: large;\n' + - '}\n' + - '\n' - ) - }) - - it('layout.html should be overwritten', () => { - const layoutFileContents = getNormalisedFileContent(path.join(appDirectory, 'views', 'layout.html')) - - expect(layoutFileContents).toEqual( - '{#\n' + - 'For guidance on how to use layouts see:\n' + - 'https://prototype-kit.service.gov.uk/docs/how-to-use-layouts\n' + - '#}\n' + - '\n' + - '{% extends "govuk-prototype-kit/layouts/govuk-branded.njk" %}' + '\n' - ) - }) - - it('unbranded-test.html should replace unbranded extend', () => { - const unbrandedFileContents = getNormalisedFileContent(path.join(appDirectory, 'views', 'nested-test-folder', 'unbranded-test.html')) - - expect(unbrandedFileContents).toEqual( - '{% extends "govuk-prototype-kit/layouts/unbranded.html" %}\n' + - '{% block pageScripts %}\n' + - ' \n' + - '{% endblock %}' - ) - }) - - it('migrate.log does not contain user home directory', () => { - const migrateLog = getNormalisedFileContent(path.join(projectDirectory, 'migrate.log')) - - expect(migrateLog).not.toContain(os.homedir()) - - // Because we don't run the test in the home directory, we also have to do - // a slightly different check, we make sure all paths except for the - // metadata in the header lines are relative to the prototype directory. - // If this is true then this should mean we won't get the home dir path. - expect(migrateLog.split('\n').filter( - line => line.includes(testDirectory) && !line.startsWith('argv:') && !line.startsWith('cwd:') - )).toEqual([]) - }) -}) diff --git a/__tests__/spec/sanity-checks.js b/__tests__/spec/sanity-checks.js index d1c605d20c..359fc6ac92 100644 --- a/__tests__/spec/sanity-checks.js +++ b/__tests__/spec/sanity-checks.js @@ -26,7 +26,7 @@ const createKitTimeout = parseInt(process.env.CREATE_KIT_TIMEOUT || '90000', 10) describe('The Prototype Kit', () => { beforeAll(async () => { await mkPrototype(tmpDir, { allowTracking: false, overwrite: true }) - app = require(path.join(tmpDir, 'node_modules', 'govuk-prototype-kit', 'server.js')) + app = require(path.join(tmpDir, 'node_modules', '@nowprototypeit', 'govuk', 'server.js')) jest.spyOn(fse, 'writeFileSync').mockImplementation(() => {}) jest.spyOn(sass, 'compile').mockImplementation((css, options) => ({ css })) diff --git a/__tests__/utils/mock-file-system.js b/__tests__/utils/mock-file-system.js index f7f3cd522e..ae7ff9c72b 100644 --- a/__tests__/utils/mock-file-system.js +++ b/__tests__/utils/mock-file-system.js @@ -158,6 +158,7 @@ function mockFileSystem (rootPath) { jest.spyOn(fs, 'lstatSync').mockImplementation(lstatImplementation) jest.spyOn(fs, 'readdirSync').mockImplementation(readdirImplementation) jest.spyOn(fs, 'existsSync').mockImplementation(existsImplementation) + jest.spyOn(fse, 'existsSync').mockImplementation(existsImplementation) jest.spyOn(fse, 'exists').mockImplementation(promiseWrap(existsImplementation)) jest.spyOn(fs.promises, 'readFile').mockImplementation(promiseWrap(readFileImplementation)) jest.spyOn(fs.promises, 'writeFile').mockImplementation(promiseWrap(writeFileImplementation)) diff --git a/bin/cli b/bin/cli index bafa217d0f..872dee0ae9 100755 --- a/bin/cli +++ b/bin/cli @@ -10,7 +10,7 @@ if (process.argv.indexOf('--suppress-node-version-warning') === -1 || (process.a const additionalText = nodeVersionIsTooOldToUse ? '' : ' Some features may not work with your version.' printLn('\nYou\'re using Node', process.version) - printLn('The GOV.UK Prototype Kit only supports Node v16, v18 and v20.' + additionalText + '\n') + printLn('The Now Prototype It Kit only supports Node v16, v18 and v20.' + additionalText + '\n') printLn('You can', updateOrDownload, 'Node v20 at https://nodejs.org/en/download\n') if (nodeVersionIsTooOldToUse) { @@ -26,7 +26,7 @@ const { spawn, exec } = require('../lib/exec') const { parse } = require('./utils/argv-parser') const { prepareMigration, preflightChecks } = require('../migrator') const { validatePlugin } = require('../lib/plugins/plugin-validator') -const { npmInstall, packageJsonFormat, getPackageVersionFromPackageJson, splitSemverVersion } = require('./utils') +const { npmInstall, packageJsonFormat, splitSemverVersion } = require('./utils') const { recursiveDirectoryContentsSync } = require('../lib/utils') const { appViewsDir } = require('../lib/utils/paths') @@ -84,9 +84,9 @@ usage-data-config.json const packageJson = { scripts: { - dev: 'govuk-prototype-kit dev', - serve: 'govuk-prototype-kit serve', - start: 'govuk-prototype-kit start' + dev: 'now-prototype-it-govuk dev', + serve: 'now-prototype-it-govuk serve', + start: 'now-prototype-it-govuk start' } } @@ -131,13 +131,13 @@ async function initialiseGitRepo () { const commitMessage = 'Create prototype' await exec(`git commit -am "${commitMessage}"`) .catch(() => - exec(`git -c "user.email=gov.uk-prototype@digital.cabinet-office.gov.uk" -c "user.name=GOV.UK Prototype Kit" commit -am "${commitMessage}"`) + exec(`git -c "user.email=auto-commit@nowprototypeit.com" -c "user.name=Now Prototype It" commit -am "${commitMessage}"`) ) .catch(failSilently) } function usage () { - const prog = 'npx govuk-prototype-kit' + const prog = 'npx @nowprototypeit/govuk' console.log(` ${prog} @@ -167,7 +167,7 @@ function getInstallLocation () { } function getChosenKitDependency () { - const defaultValue = 'govuk-prototype-kit' + const defaultValue = '@nowprototypeit/govuk' const versionRequested = argv.options.version || argv.options.v if (!versionRequested) { @@ -216,18 +216,11 @@ function getArgumentsToPassThrough () { return additionalArgs } -async function initialiserRequiresOldInitSyntax () { - const version = await getPackageVersionFromPackageJson(path.join(getInstallLocation(), 'node_modules', 'govuk-prototype-kit', 'package.json')) - - const requiresOldInitSyntax = version.major === 13 && version.minor < 2 - return requiresOldInitSyntax -} - async function runCreate () { // Install as a two-stage bootstrap process. // // In stage one (`create`) we create an empty project folder and install - // govuk-prototype-kit and govuk-frontend, then bootstrap stage two from + // @nowprototypeit/govuk and govuk-frontend, then bootstrap stage two from // the newly installed package. // // In stage two (`init`) we do the actual setup of the starter files. @@ -265,15 +258,9 @@ async function runCreate () { await fse.ensureSymlink(kitDependency, dependencyInstallLocation) } - let runningWithinCreateScriptFlag = '--running-within-create-script' - - if (await initialiserRequiresOldInitSyntax()) { - runningWithinCreateScriptFlag = '--' - } - progressLogger('Setting up your prototype') - await spawn('npx', ['govuk-prototype-kit', 'init', runningWithinCreateScriptFlag, installDirectory, `--created-from-version=${kitVersion}`, ...(getArgumentsToPassThrough()), '--suppress-node-version-warning'], { + await spawn('npx', ['@nowprototypeit/govuk', 'init', '--running-within-create-script', installDirectory, `--created-from-version=${kitVersion}`, ...(getArgumentsToPassThrough()), '--suppress-node-version-warning'], { cwd: installDirectory, stdio: 'inherit' }) @@ -363,7 +350,7 @@ async function runMigrate () { await prepareMigration(kitDependency, projectDirectory) - await spawn('npx', ['govuk-prototype-kit', 'migrate', '--', projectDirectory], { + await spawn('npx', ['@nowprototypeit/govuk', 'migrate', '--', projectDirectory], { stdio: 'inherit' }) .catch(e => { @@ -393,7 +380,7 @@ async function runMigrate () { } async function runDev () { - console.log(`GOV.UK Prototype Kit ${kitVersion}`) + console.log(`Now Prototype It Kit ${kitVersion}`) console.log('') console.log('starting...') diff --git a/cypress/e2e/dev/1-watch-files/watch-config.cypress.js b/cypress/e2e/dev/1-watch-files/watch-config.cypress.js deleted file mode 100644 index 2ea07fd919..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-config.cypress.js +++ /dev/null @@ -1,27 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appConfigPath = path.join('app', 'config.json') -const appConfig = path.join(Cypress.env('projectFolder'), appConfigPath) - -const originalText = 'Service name goes here' -const newText = 'Cypress test' - -const serverNameQuery = 'h1.govuk-heading-xl' - -describe('watch config file', () => { - describe(`service name in config file ${appConfig} should be changed and restored`, () => { - afterEach(restoreStarterFiles) - - it('The service name should change to "cypress test"', () => { - waitForApplication('/') - cy.get(serverNameQuery).contains(originalText) - cy.task('replaceTextInFile', { filename: appConfig, originalText, newText }) - waitForApplication('/') - cy.get(serverNameQuery).contains(newText) - }) - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-filters.cypress.js b/cypress/e2e/dev/1-watch-files/watch-filters.cypress.js deleted file mode 100644 index 549ad7b2b6..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-filters.cypress.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path') -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appFiltersPath = path.join(Cypress.env('projectFolder'), 'app', 'filters.js') -const appFiltersViewPath = path.join(Cypress.env('projectFolder'), 'app', 'views', 'filters.html') - -const filtersViewMarkup = ` -{% extends "layouts/main.html" %} -{% block content %} -
{{ 'abc' | foo__strong }}
-{% endblock %} -` -const filtersAddition = ` -const govukPrototypeKit = require('govuk-prototype-kit') -const addFilter = govukPrototypeKit.views.addFilter -addFilter('foo__strong', (content) => '' + content + '', { renderAsHtml: true }) -` -describe('Filters Test', () => { - beforeEach(() => { - // Restore filters file from prototype starter - cy.task('createFile', { filename: appFiltersPath, data: filtersAddition }) - cy.task('createFile', { filename: appFiltersViewPath, data: filtersViewMarkup }) - }) - - afterEach(restoreStarterFiles) - - it('view the filters html file', () => { - waitForApplication('/filters') - cy.get('#test-foo-strong-filter') - .should('have.html', 'abc') - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-functions.cypress.js b/cypress/e2e/dev/1-watch-files/watch-functions.cypress.js deleted file mode 100644 index 4600651022..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-functions.cypress.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path') -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appFunctionsPath = path.join(Cypress.env('projectFolder'), 'app', 'functions.js') -const appFiltersViewPath = path.join(Cypress.env('projectFolder'), 'app', 'views', 'functions.html') - -const functionsViewMarkup = ` -{% extends "layouts/main.html" %} -{% block content %} -
{{ fooEmphasize('def') }}
-{% endblock %} -` -const functionsAddition = ` -const govukPrototypeKit = require('govuk-prototype-kit') -const addFunction = govukPrototypeKit.views.addFunction -addFunction('fooEmphasize', (content) => '' + content + '', { renderAsHtml: true }) -` -describe('Functions Test', () => { - beforeEach(() => { - // Restore functions file from prototype starter - cy.task('createFile', { filename: appFunctionsPath, data: functionsAddition }) - cy.task('createFile', { filename: appFiltersViewPath, data: functionsViewMarkup }) - }) - - afterEach(restoreStarterFiles) - - it('view the functions html file', () => { - waitForApplication('/functions') - cy.get('#test-foo-emphasize-function') - .should('have.html', 'def') - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-images.cypress.js b/cypress/e2e/dev/1-watch-files/watch-images.cypress.js deleted file mode 100644 index 1a5ea07ecf..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-images.cypress.js +++ /dev/null @@ -1,64 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const imageFile = 'larry-the-cat.jpg' -const cypressImages = path.join(Cypress.config('fixturesFolder'), 'images') -const appImages = path.join(Cypress.env('projectFolder'), 'app', 'assets', 'images') -const publicImages = 'public/images' - -const pageFixture = 'larry-the-cat' -const pageFixtureName = `${pageFixture}.html` -const pageFixturePath = path.join(Cypress.config('fixturesFolder'), 'views', pageFixtureName) -const pageAppPath = path.join(Cypress.env('projectFolder'), 'app', 'views', pageFixtureName) - -describe('watch image files', () => { - afterEach(restoreStarterFiles) - - it(`image created in ${appImages} should be copied to ${publicImages} and accessible from the browser`, () => { - const source = path.join(cypressImages, imageFile) - const target = path.join(appImages, imageFile) - const publicImage = `${publicImages}/${imageFile}` - - cy.task('log', 'Creating a page to view our image') - cy.task('copyFile', { source: pageFixturePath, target: pageAppPath }) - - cy.task('log', 'Our page should be missing its image') - - waitForApplication() - cy.visit(`/${pageFixture}`) - cy.get('img#larry') - .should('be.visible') - .and(($img) => { - // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth).to.equal(0) - }) - - cy.task('log', 'Copying the image to the app folder') - cy.task('copyFile', { source, target }) - - cy.task('log', 'Wait for the image to be loaded') - cy.waitForResource(imageFile) - - // FIXME: the expected behaviour is that the page should update itself, - // however there is a bug in our setup where on Windows the image doesn't - // show up without a page reload. When this is fixed, remove this step. - // See issue #1440 for other details. - cy.task('log', 'Reload the page') - cy.reload() - - cy.task('log', 'Our page should now have its image') - cy.get('img#larry') - .should('be.visible') - .and(($img) => { - // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth).not.to.equal(0) - }) - - cy.task('log', `Requesting ${publicImage}`) - cy.request(`/${publicImage}`, { retryOnStatusCodeFailure: true }) - .then(response => expect(response.status).to.eq(200)) - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-javascripts.cypress.js b/cypress/e2e/dev/1-watch-files/watch-javascripts.cypress.js deleted file mode 100644 index 716cd67efe..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-javascripts.cypress.js +++ /dev/null @@ -1,37 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appJsPath = path.join('app', 'assets', 'javascripts', 'application.js') -const appJs = path.join(Cypress.env('projectFolder'), appJsPath) - -const heading = 'Test Heading' - -const scriptToAdd = ` - document.querySelector("h1").innerHTML = "${heading}" -` - -describe('watch application.js', () => { - afterEach(restoreStarterFiles) - - it('changes to application.js should be reflected in browser', () => { - waitForApplication() - - cy.get('h1').should('not.contain.text', heading) - - const markerText = '// Add JavaScript here' - - cy.task('replaceTextInFile', { - filename: appJs, - originalText: markerText, - newText: markerText + scriptToAdd - }) - - cy.get('h1').contains(heading) - - // Restore application.js from prototype starter - cy.task('copyFromStarterFiles', { filename: appJsPath }) - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-routes.cypress.js b/cypress/e2e/dev/1-watch-files/watch-routes.cypress.js deleted file mode 100644 index 7edc68d18f..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-routes.cypress.js +++ /dev/null @@ -1,44 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appRoutesPath = path.join('app', 'routes.js') - -const routesFixture = path.join(Cypress.config('fixturesFolder'), 'routes.js') -const appRoutes = path.join(Cypress.env('projectFolder'), appRoutesPath) -const pagePath = '/cypress-test' - -describe('watch route file', () => { - afterEach(restoreStarterFiles) - - it(`add and remove ${pagePath} route`, () => { - waitForApplication() - - cy.task('log', 'The cypress test page should not be found') - cy.visit(pagePath, { failOnStatusCode: false }) - cy.get('body') - .contains(`There is no page at ${pagePath}`) - - cy.task('log', `Replace ${appRoutes} with Cypress routes`) - cy.task('copyFile', { source: routesFixture, target: appRoutes }) - - waitForApplication() - - cy.task('log', 'The cypress test page should be displayed') - cy.visit(pagePath) - cy.get('h1') - .contains('CYPRESS TEST PAGE') - - cy.task('log', `Restore ${appRoutesPath}`) - cy.task('copyFromStarterFiles', { filename: appRoutesPath }) - - waitForApplication() - - cy.task('log', 'The cypress test page should not be found') - cy.visit(pagePath, { failOnStatusCode: false }) - cy.get('body') - .contains(`There is no page at ${pagePath}`) - }) -}) diff --git a/cypress/e2e/dev/1-watch-files/watch-views.cypress.js b/cypress/e2e/dev/1-watch-files/watch-views.cypress.js deleted file mode 100644 index af18a3792d..0000000000 --- a/cypress/e2e/dev/1-watch-files/watch-views.cypress.js +++ /dev/null @@ -1,30 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const templatesView = path.join(Cypress.config('fixturesFolder'), 'views', 'start.html') -const appView = path.join(Cypress.env('projectFolder'), 'app', 'views', 'start.html') -const pagePath = '/start' - -describe('watching start page', () => { - afterEach(restoreStarterFiles) - - it('Add and remove the start page', () => { - waitForApplication() - - cy.task('log', 'The start page should not be found') - cy.visit(pagePath, { failOnStatusCode: false }) - cy.get('body') - .contains(`There is no page at ${pagePath}`) - - cy.task('log', `Copy ${templatesView} to ${appView}`) - cy.task('copyFile', { source: templatesView, target: appView }) - - cy.task('log', 'The start page should be displayed') - cy.visit(pagePath) - cy.get('.govuk-button--start') - .contains('Start now') - }) -}) diff --git a/cypress/e2e/dev/2-page-component-tests/checkboxes.cypress.js b/cypress/e2e/dev/2-page-component-tests/checkboxes.cypress.js deleted file mode 100644 index d6de02c248..0000000000 --- a/cypress/e2e/dev/2-page-component-tests/checkboxes.cypress.js +++ /dev/null @@ -1,54 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const templatesView = path.join(Cypress.config('fixturesFolder'), 'views', 'checkbox-test.html') -const appView = path.join(Cypress.env('projectFolder'), 'app', 'views', 'checkbox-test.html') - -const pagePath = '/checkbox-test' - -describe('checkbox tests', () => { - before(() => { - cy.task('log', `Copy ${templatesView} to ${appView}`) - cy.task('copyFile', { source: templatesView, target: appView }) - }) - - after(restoreStarterFiles) - - const loadTestView = async () => { - cy.task('log', 'The checkbox-test page should be displayed') - cy.visit(pagePath) - cy.get('h1') - .contains('Checkbox tests') - } - - const submitAndCheck = async (matchData) => { - cy.intercept('POST', pagePath).as('submitPage') - cy.get('button[data-module="govuk-button"]') - .contains('Continue') - .click() - cy.wait('@submitPage').its('request.body') - .then((body) => { - const data = decodeURIComponent(body).split('&') - cy.expect(data.length).equal(matchData.length) - matchData.forEach((match) => { - cy.expect(data).includes(match) - }) - }) - } - - it('request should include the _unchecked option only', () => { - waitForApplication() - loadTestView() - submitAndCheck(['vehicle1[vehicle-features]=_unchecked']) - }) - - it('when the GPS checkbox is selected, the request should include the GPS option', () => { - waitForApplication() - loadTestView() - cy.get('input[value="GPS"]').check() - submitAndCheck(['vehicle1[vehicle-features]=_unchecked', 'vehicle1[vehicle-features]=GPS']) - }) -}) diff --git a/cypress/e2e/dev/3-link-page-tests/branching-journey.cypress.js b/cypress/e2e/dev/3-link-page-tests/branching-journey.cypress.js deleted file mode 100644 index 98f5e8dd81..0000000000 --- a/cypress/e2e/dev/3-link-page-tests/branching-journey.cypress.js +++ /dev/null @@ -1,44 +0,0 @@ -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') -const { setUpPages, setUpBranchingPages } = require('./link-page-utils') - -const jugglingBallsPath = '/juggling-balls' - -const eligibleHowManyBalls = '3 or more' -const ineligibleHowManyBalls = '1 or 2' - -describe('Branching journey', async () => { - before(() => { - setUpPages() - setUpBranchingPages() - waitForApplication() - }) - - after(restoreStarterFiles) - - it('eligible journey', () => { - // Visit Juggling balls page, click continue - cy.visit(jugglingBallsPath) - cy.task('log', 'The juggling balls page should be displayed') - cy.get('h1').contains('How many balls can you juggle?') - cy.get(`input[value="${eligibleHowManyBalls}"]`).check() - cy.get('button.govuk-button').contains('Continue').click() - - // Juggling trick page should be displayed correctly - cy.task('log', 'The juggling trick page should be displayed') - cy.get('h1').contains('What is your most impressive juggling trick?') - }) - - it('Ineligible journey', () => { - // Visit Juggling balls page, click continue - cy.visit(jugglingBallsPath) - cy.task('log', 'The juggling balls page should be displayed') - cy.get('h1').contains('How many balls can you juggle?') - cy.get(`input[value="${ineligibleHowManyBalls}"]`).check() - cy.get('button.govuk-button').contains('Continue').click() - - // Juggling trick page should be displayed correctly - cy.task('log', 'The juggling trick page should be displayed') - cy.get('h1').contains('Sorry, you are ineligible for juggling tricks') - }) -}) diff --git a/cypress/e2e/dev/3-link-page-tests/change-answers.cypress.js b/cypress/e2e/dev/3-link-page-tests/change-answers.cypress.js deleted file mode 100644 index 4976c15d72..0000000000 --- a/cypress/e2e/dev/3-link-page-tests/change-answers.cypress.js +++ /dev/null @@ -1,73 +0,0 @@ -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') -const { setUpPages, setUpData } = require('./link-page-utils') - -const checkAnswersPath = '/check-answers' - -const howManyBalls = '3 or more' -const mostImpressiveTrick = 'Standing on my head' - -const defaultHowManyBalls = 'None - I cannot juggle' -const defaultMostImpressiveTrick = 'None - I cannot do tricks' - -describe('Change answers', async () => { - before(() => { - setUpPages() - setUpData() - }) - - after(restoreStarterFiles) - - it('Change juggling balls journey', () => { - // Visit Check answers page, click change juggling balls - waitForApplication() - cy.task('log', 'The check answers page should be displayed') - cy.visit(checkAnswersPath) - cy.get('h1').contains('Check your answers before sending your application') - cy.get('.govuk-summary-list__value:first').contains(defaultHowManyBalls) - cy.get('.govuk-summary-list__value:last').contains(defaultMostImpressiveTrick) - cy.get('a[href="/juggling-balls"]').contains('Change').click() - - // On Juggling balls page, click continue - cy.task('log', 'The juggling balls page should be displayed') - cy.get('h1').contains('How many balls can you juggle?') - cy.get(`input[value="${defaultHowManyBalls}"]`).should('be.checked') - cy.get(`input[value="${howManyBalls}"]`).check() - cy.get('button.govuk-button').contains('Continue').click() - - // On Juggling trick page, click continue - cy.task('log', 'The juggling trick page should be displayed') - cy.get('h1').contains('What is your most impressive juggling trick?') - cy.get('textarea#most-impressive-trick').contains(defaultMostImpressiveTrick) - cy.get('button.govuk-button').contains('Continue').click() - - // On Check answers page, click accept and send - cy.task('log', 'The check answers page should be displayed') - cy.get('h1').contains('Check your answers before sending your application') - cy.get('.govuk-summary-list__value:first').contains(howManyBalls) - cy.get('.govuk-summary-list__value:last').contains(defaultMostImpressiveTrick) - }) - - it('Change juggling trick journey', () => { - // Visit Check answers page, click change juggling trick - waitForApplication() - cy.task('log', 'The check answers page should be displayed') - cy.visit(checkAnswersPath) - cy.get('h1').contains('Check your answers before sending your application') - cy.get('.govuk-summary-list__value:first').contains(defaultHowManyBalls) - cy.get('.govuk-summary-list__value:last').contains(defaultMostImpressiveTrick) - cy.get('a[href="/juggling-trick"]').contains('Change').click() - - // On Juggling trick page, click continue - cy.task('log', 'The juggling trick page should be displayed') - cy.get('h1').contains('What is your most impressive juggling trick?') - cy.get('textarea#most-impressive-trick').contains(defaultMostImpressiveTrick).type(mostImpressiveTrick) - cy.get('button.govuk-button').contains('Continue').click() - - // On Check answers page, click accept and send - cy.task('log', 'The check answers page should be displayed') - cy.get('h1').contains('Check your answers before sending your application') - cy.get('.govuk-summary-list__value:first').contains(defaultHowManyBalls) - cy.get('.govuk-summary-list__value:last').contains(mostImpressiveTrick) - }) -}) diff --git a/cypress/e2e/dev/3-link-page-tests/link-index-to-start.cypress.js b/cypress/e2e/dev/3-link-page-tests/link-index-to-start.cypress.js deleted file mode 100644 index 1063cebd26..0000000000 --- a/cypress/e2e/dev/3-link-page-tests/link-index-to-start.cypress.js +++ /dev/null @@ -1,38 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, copyFile, restoreStarterFiles } = require('../../utils') - -const appViewsPath = path.join('app', 'views') -const indexViewPath = path.join(appViewsPath, 'index.html') - -const indexView = path.join(Cypress.env('projectFolder'), indexViewPath) -const startView = path.join(Cypress.env('projectFolder'), appViewsPath, 'start.html') -const templateStartView = path.join(Cypress.config('fixturesFolder'), 'views', 'start.html') - -const commentText = '{% include "govuk-prototype-kit/includes/homepage-bottom.njk" %}' - -const startText = 'Click here to start' - -const linkText = `${startText}` - -describe('Link index page to start page', async () => { - beforeEach(() => { - copyFile(templateStartView, startView) - cy.task('replaceTextInFile', { - filename: indexView, - originalText: commentText, - newText: linkText - }) - }) - - afterEach(restoreStarterFiles) - - it('click start link', () => { - waitForApplication() - cy.get('a[href="/start"]').contains(startText).click() - cy.get('a[role="button"]') - .contains('Start') - }) -}) diff --git a/cypress/e2e/dev/3-link-page-tests/link-page-utils.js b/cypress/e2e/dev/3-link-page-tests/link-page-utils.js deleted file mode 100644 index 54b203bd80..0000000000 --- a/cypress/e2e/dev/3-link-page-tests/link-page-utils.js +++ /dev/null @@ -1,103 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { copyFile } = require('../../utils') - -const templates = path.join(Cypress.config('fixturesFolder'), 'views') -const startTemplate = path.join(templates, 'start.html') -const questionTemplate = path.join(templates, 'question.html') -const confirmationTemplate = path.join(templates, 'confirmation.html') -const contentTemplate = path.join(templates, 'content.html') - -const fixtures = path.join(Cypress.config('fixturesFolder')) - -const fixtureViews = path.join(fixtures, 'views') -const jugglingCheckAnswersFixtureView = path.join(fixtureViews, 'juggling-check-answers.html') - -const components = path.join(fixtures, 'components') -const jugglingBallsComponent = path.join(components, 'juggling-balls-component.html') -const jugglingTrickComponent = path.join(components, 'juggling-trick-component.html') -const jugglingBallsAnswerComponent = path.join(components, 'juggling-balls-route-component.js') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const startView = path.join(appViews, 'start.html') -const jugglingBallsView = path.join(appViews, 'juggling-balls.html') -const jugglingTrickView = path.join(appViews, 'juggling-trick.html') -const checkAnswersView = path.join(appViews, 'check-answers.html') -const confirmationView = path.join(appViews, 'confirmation.html') -const ineligibleView = path.join(appViews, 'ineligible.html') - -const appRoutesPath = path.join('app', 'routes.js') -const appRoutes = path.join(Cypress.env('projectFolder'), appRoutesPath) - -const appDataFile = path.join(Cypress.env('projectFolder'), 'app', 'data', 'session-data-defaults.js') - -const jugglingBallsPath = '/juggling-balls' -const jugglingTrickPath = '/juggling-trick' -const checkAnswersPath = '/check-answers' - -const jugglingBallsAnswerRoute = '/juggling-balls-answer' - -const defaultHowManyBalls = 'None - I cannot juggle' -const defaultMostImpressiveTrick = 'None - I cannot do tricks' - -const createQuestionView = (view, content, nextPath) => { - copyFile(questionTemplate, view) - cy.task('replaceMultipleTextInFile', { - filename: view, - list: [ - { originalText: '

Heading or question goes here

', newText: '' }, - { originalText: '

[See the GOV.UK Design System for examples]

', newText: '' }, - { originalText: '

[Insert question content here]

', source: content }, - { originalText: '/url/of/next/page', newText: nextPath } - ] - }) -} - -const setUpPages = () => { - // Set up start view - copyFile(startTemplate, startView) - cy.task('replaceTextInFile', { filename: startView, originalText: ' { - cy.task('replaceTextInFile', { filename: appDataFile, originalText: '// Insert values here', newText: `"how-many-balls": "${defaultHowManyBalls}", "most-impressive-trick": "${defaultMostImpressiveTrick}"` }) -} - -const setUpBranchingPages = () => { - // Set up ineligible view - copyFile(contentTemplate, ineligibleView) - cy.task('replaceMultipleTextInFile', { - filename: ineligibleView, - list: [ - { originalText: 'Heading goes here', newText: 'Sorry, you are ineligible for juggling tricks' }, - { originalText: 'This is a paragraph of text. It explains in more detail what has happened and wraps across several lines.', newText: 'Keep practicing and when you can juggle 3 or more balls, you will be eligible for tricks.' }, - { originalText: '

Read more about this topic.

', newText: '' } - ] - }) - - // Update the juggling balls action - cy.task('replaceTextInFile', { filename: jugglingBallsView, originalText: jugglingTrickPath, newText: jugglingBallsAnswerRoute }) - - // Update routes with juggling balls answer component - cy.task('replaceTextInFile', { filename: appRoutes, originalText: '// Add your routes here', source: jugglingBallsAnswerComponent }) -} - -module.exports = { - setUpPages, - setUpBranchingPages, - setUpData -} diff --git a/cypress/e2e/dev/3-link-page-tests/simple-journey.cypress.js b/cypress/e2e/dev/3-link-page-tests/simple-journey.cypress.js deleted file mode 100644 index 36e8832af2..0000000000 --- a/cypress/e2e/dev/3-link-page-tests/simple-journey.cypress.js +++ /dev/null @@ -1,48 +0,0 @@ -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') -const { setUpPages } = require('./link-page-utils') - -const startPath = '/start' - -const howManyBalls = '3 or more' -const mostImpressiveTrick = 'Standing on my head' - -describe('Question journey', async () => { - beforeEach(() => { - setUpPages() - }) - - afterEach(restoreStarterFiles) - - it('Happy path journey', () => { - waitForApplication() - // Visit start page and click start - cy.task('log', 'The start page should be displayed') - cy.visit(startPath) - cy.get('body').contains('Start now') - cy.get('a.govuk-button--start').click() - - // On Juggling balls page, click continue - cy.task('log', 'The juggling balls page should be displayed') - cy.get('h1').contains('How many balls can you juggle?') - cy.get(`input[value="${howManyBalls}"]`).check() - cy.get('button.govuk-button').contains('Continue').click() - - // On Juggling trick page, click continue - cy.task('log', 'The juggling trick page should be displayed') - cy.get('h1').contains('What is your most impressive juggling trick?') - cy.get('textarea#most-impressive-trick').type(mostImpressiveTrick) - cy.get('button.govuk-button').contains('Continue').click() - - // On Check answers page, click accept and send - cy.task('log', 'The check answers page should be displayed') - cy.get('h1').contains('Check your answers before sending your application') - cy.get('.govuk-summary-list__value:first').contains(howManyBalls) - cy.get('.govuk-summary-list__value:last').contains(mostImpressiveTrick) - cy.get('button.govuk-button').contains('Accept and send').click() - - // Confirmation page should be displayed correctly - cy.task('log', 'The confirmation page should be displayed') - cy.get('h1.govuk-panel__title').contains('Application complete') - }) -}) diff --git a/cypress/e2e/dev/4-step-by-step-tests/step-by-step-journey.cypress.js b/cypress/e2e/dev/4-step-by-step-tests/step-by-step-journey.cypress.js deleted file mode 100644 index a301e852ad..0000000000 --- a/cypress/e2e/dev/4-step-by-step-tests/step-by-step-journey.cypress.js +++ /dev/null @@ -1,108 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { copyFile, waitForApplication, installPlugin, restoreStarterFiles } = require('../../utils') -const { - assertHidden, - assertVisible, - showHideAllLinkQuery, - titleQuery, - toggleButtonQuery -} = require('../../step-by-step-utils') - -const plugin = '@govuk-prototype-kit/step-by-step@1' - -const projectFolder = Cypress.env('projectFolder') - -const appViews = path.join(projectFolder, 'app', 'views') - -const stepByStepTestData = [{ - name: 'step-by-step-navigation', - heading: 'Learn to drive a car: step by step', - title1: 'Check you\'re allowed to drive', - title2: 'Get a provisional licence' -}, { - name: 'start-with-step-by-step', - heading: 'Check what age you can drive', - title1: 'Check you\'re allowed to drive', - title2: 'Get a provisional licence' -}] - -stepByStepTestData.forEach(({ name, heading, title1, title2 }) => { - const stepByStepTemplateView = path.join(Cypress.config('fixturesFolder'), 'views', `${name}.html`) - const stepByStepView = path.join(appViews, `${name}.html`) - const stepByStepPath = `/${name}` - - describe(`${name} journey`, async () => { - before(() => { - copyFile(stepByStepTemplateView, stepByStepView) - }) - - after(restoreStarterFiles) - - const loadPage = async () => { - cy.visit(stepByStepPath) - cy.get('h1').contains(heading) - } - - it('renders ok', () => { - waitForApplication() - - installPlugin(plugin) - - waitForApplication() - - loadPage() - cy.get(titleQuery(1)).should('contain.text', title1) - cy.get(titleQuery(2)).should('contain.text', title2) - assertHidden(1) - assertHidden(2) - }) - - it('toggle step 1', () => { - waitForApplication() - - loadPage() - // click toggle button and check that only step 1 details are visible - cy.get(toggleButtonQuery(1)).click() - assertVisible(1) - assertHidden(2) - - // click toggle button and check that only both steps are hidden - cy.get(toggleButtonQuery(1)).click() - assertHidden(1) - assertHidden(2) - }) - - it('toggle step 2', () => { - waitForApplication() - - loadPage() - // click toggle button and check that only step 1 details are visible - cy.get(toggleButtonQuery(2)).click() - assertHidden(1) - assertVisible(2) - - // click toggle button and check that only both steps are hidden - cy.get(toggleButtonQuery(2)).click() - assertHidden(1) - assertHidden(2) - }) - - it('toggle all steps', () => { - waitForApplication() - - loadPage() - // click toggle button and check that all steps details are visible - cy.get(showHideAllLinkQuery).contains('Show all').click() - assertVisible(1) - assertVisible(2) - - // click toggle button and check that all steps details are hidden - cy.get(showHideAllLinkQuery).contains('Hide all').click() - assertHidden(1) - assertHidden(2) - }) - }) -}) diff --git a/cypress/e2e/dev/5-management-tests/change-service-name.cypress.js b/cypress/e2e/dev/5-management-tests/change-service-name.cypress.js deleted file mode 100644 index 5d06efaa38..0000000000 --- a/cypress/e2e/dev/5-management-tests/change-service-name.cypress.js +++ /dev/null @@ -1,48 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appConfigPath = path.join('app', 'config.json') -const appConfig = path.join(Cypress.env('projectFolder'), appConfigPath) - -const originalText = 'Service name goes here' -const newText = 'Cypress test' - -const serverNameQuery = 'a.govuk-header__link.govuk-header__service-name, a.govuk-header__link--service-name' - -const managePagePath = '/manage-prototype' - -describe('change service name', () => { - afterEach(restoreStarterFiles) - - it('The service name should change to "cypress test" and the task should be set to "Done"', () => { - waitForApplication() - - cy.task('log', 'Visit the index page and navigate to the manage your prototype page') - cy.visit('/') - cy.get('.govuk-heading-xl').contains(originalText) - cy.get('p strong').contains(appConfigPath) - cy.get(`main a[href="${managePagePath}"]`).contains('Manage your prototype').click() - - cy.task('log', 'Visit the manage prototype page') - - cy.get(serverNameQuery).contains(originalText) - cy.get('.govuk-prototype-kit-manage-prototype-task-list__item') - .contains(appConfigPath) - .get('.govuk-prototype-kit-manage-prototype-task-list__tag').contains('To do') - - cy.task('replaceTextInFile', { filename: appConfig, originalText, newText }) - - waitForApplication(managePagePath) - - cy.get(serverNameQuery).contains(newText) - cy.get('.govuk-prototype-kit-manage-prototype-task-list__item') - .contains(appConfigPath) - .get('.govuk-prototype-kit-manage-prototype-task-list__tag').contains('Done') - - cy.visit('/index') - cy.get('.govuk-heading-xl').contains(newText) - }) -}) diff --git a/cypress/e2e/dev/5-management-tests/clear-data-page.cypress.js b/cypress/e2e/dev/5-management-tests/clear-data-page.cypress.js deleted file mode 100644 index 644ee88310..0000000000 --- a/cypress/e2e/dev/5-management-tests/clear-data-page.cypress.js +++ /dev/null @@ -1,84 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { - copyFile, - createFile, - replaceInFile, - waitForApplication, restoreStarterFiles -} = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const templates = path.join(Cypress.config('fixturesFolder'), 'views') -const components = path.join(Cypress.config('fixturesFolder'), 'components') - -const questionComponent = path.join(components, 'juggling-trick-component.html') -const questionTemplate = path.join(templates, 'question.html') - -const questionView = path.join(appViews, 'question.html') -const questionCheckView = path.join(appViews, 'question-check.html') - -const questionTestMarkUp = ` -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block content %} - -

question results

- -
-
-

Answer: {{ data['most-impressive-trick'] }}

-
-
- -{% endblock %} -` - -function clearData () { - cy.get('footer a[href*="/manage-prototype/clear-data"]').click() - cy.get('h1').should('contain.text', 'Clear session data') - cy.get('button').should('contain.text', 'Clear the data').click() - cy.get('main h1').should('contain.text', 'Data cleared') - cy.get('main a').should('contain.text', 'Prototype home page').click() -} - -const answer = 'Standing on my head' - -describe('clear data page', () => { - before(() => { - copyFile(questionTemplate, questionView) - replaceInFile(questionView, '

[Insert question content here]

', questionComponent) - replaceInFile(questionView, '/url/of/next/page', '', '/question-check') - createFile(questionCheckView, { data: questionTestMarkUp }) - cy.task('copyFromStarterFiles', { filename: 'app/data/session-data-defaults.js' }) - }) - - after(restoreStarterFiles) - - it('save and clear data', () => { - waitForApplication() - - cy.task('log', 'Check data is cleared initially') - cy.visit('/index') - clearData() - cy.visit('/question-check') - cy.get('#answer').should('have.text', '') - - cy.task('log', 'Add some data') - cy.visit('/question') - cy.get('form textarea').type(answer) - cy.get('form').submit() - - cy.task('log', 'Check data has been saved') - cy.get('#answer').should('contain.text', answer) - cy.visit('/question') - cy.get('form textarea').should('contain.value', answer) - - cy.task('log', 'Check data can be cleared') - cy.visit('/index') - clearData() - cy.visit('/question-check') - cy.get('#answer').should('have.text', '') - }) -}) diff --git a/cypress/e2e/dev/5-management-tests/edit-home-page.cypress.js b/cypress/e2e/dev/5-management-tests/edit-home-page.cypress.js deleted file mode 100644 index 513aaf1174..0000000000 --- a/cypress/e2e/dev/5-management-tests/edit-home-page.cypress.js +++ /dev/null @@ -1,41 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appHomePath = path.join('app', 'views', 'index.html') -const appHome = path.join(Cypress.env('projectFolder'), appHomePath) - -const originalText = 'Service name goes here' -const newText = 'Cypress test service' - -const managePagePath = '/manage-prototype' - -describe('edit home page', () => { - afterEach(restoreStarterFiles) - - it(`The home page heading should change to "${newText}" and the task should be set to "Done"`, () => { - waitForApplication(managePagePath) - - cy.task('log', 'Visit the manage prototype templates page') - - cy.get('.govuk-prototype-kit-manage-prototype-task-list__item') - .contains(appHomePath) - .get('.govuk-prototype-kit-manage-prototype-task-list__tag').contains('To do') - - cy.visit('/index') - cy.get('.govuk-heading-xl').contains(originalText) - - cy.task('replaceTextInFile', { filename: appHome, originalText: '{{ serviceName }}', newText }) - - waitForApplication(managePagePath) - - cy.get('.govuk-prototype-kit-manage-prototype-task-list__item') - .contains(appHomePath) - .get('.govuk-prototype-kit-manage-prototype-task-list__tag').contains('Done') - - cy.visit('/index') - cy.get('.govuk-heading-xl').contains(newText) - }) -}) diff --git a/cypress/e2e/dev/5-management-tests/management-available.cypress.js b/cypress/e2e/dev/5-management-tests/management-available.cypress.js deleted file mode 100644 index 6ef0a89e58..0000000000 --- a/cypress/e2e/dev/5-management-tests/management-available.cypress.js +++ /dev/null @@ -1,29 +0,0 @@ -// local dependencies -const { waitForApplication } = require('../../utils') - -describe('management available', () => { - it('when attempting to visit "/manage-prototype" page', () => { - waitForApplication() - cy.visit('/manage-prototype') - cy.get('h1').should('contain.text', 'Manage your prototype') - }) - - it('manage prototype link should exist on the home page', () => { - waitForApplication() - cy.visit('/') - cy.get('main a[href="/manage-prototype"]').should('contain.text', 'Manage your prototype') - }) - - it('manage prototype link should exist in the footer', () => { - waitForApplication() - cy.visit('/') - cy.get('footer a[href="/manage-prototype"]').should('contain.text', 'Manage your prototype') - }) - - it('clear data link should exist in the footer and work correctly', () => { - waitForApplication() - cy.visit('/') - cy.get('footer a[href="/manage-prototype/clear-data"]').should('contain.text', 'Clear data').click() - cy.get('h1').should('contain.text', 'Clear session data') - }) -}) diff --git a/cypress/e2e/dev/5-management-tests/no-autodatastore-on-management-pages.cypress.js b/cypress/e2e/dev/5-management-tests/no-autodatastore-on-management-pages.cypress.js deleted file mode 100644 index 8a0f1e75c8..0000000000 --- a/cypress/e2e/dev/5-management-tests/no-autodatastore-on-management-pages.cypress.js +++ /dev/null @@ -1,67 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { - copyFile, - createFile, - waitForApplication, restoreStarterFiles -} = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const templates = path.join(Cypress.config('fixturesFolder'), 'views') - -const questionTemplate = path.join(templates, 'question.html') - -const questionView = path.join(appViews, 'question.html') -const questionCheckView = path.join(appViews, 'question-check.html') - -const dataOutputNjk = ` -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block content %} -

Answer: {{ data['most-impressive-trick'] }}

-

Should be empty: {{ data['abc'] }}

-{% endblock %} -` - -const answer = 'Standing on my head' - -function clearData () { - cy.get('footer a[href*="/manage-prototype/clear-data"]').click() - cy.get('h1').should('contain.text', 'Clear session data') - cy.get('button').should('contain.text', 'Clear the data').click() - cy.get('main h1').should('contain.text', 'Data cleared') - cy.get('main a').should('contain.text', 'Prototype home page').click() -} -describe('clear data page', () => { - before(() => { - copyFile(questionTemplate, questionView) - createFile(questionCheckView, { data: dataOutputNjk }) - cy.task('copyFromStarterFiles', { filename: 'app/data/session-data-defaults.js' }) - }) - - after(restoreStarterFiles) - - it('add data from query string, but not in magnagement pages', () => { - waitForApplication() - - cy.task('log', 'Check data is cleared initially') - cy.visit('/index') - clearData() - cy.visit('/question-check') - cy.get('#answer').should('have.text', '') - cy.get('#empty').should('have.text', '') - - cy.task('log', 'Add some data') - cy.visit(`/question-check?most-impressive-trick=${encodeURIComponent(answer)}`) - cy.get('.govuk-header__logotype') - cy.visit('/manage-prototype/plugins?abc=def') - cy.get('.govuk-header__logotype') - - cy.task('log', 'Check data has been saved') - cy.visit('/question-check') - cy.get('#answer').should('have.text', answer) - cy.get('#empty').should('have.text', '') - }) -}) diff --git a/cypress/e2e/dev/6-layout-tests/default-layout.cypress.js b/cypress/e2e/dev/6-layout-tests/default-layout.cypress.js deleted file mode 100644 index db406feadc..0000000000 --- a/cypress/e2e/dev/6-layout-tests/default-layout.cypress.js +++ /dev/null @@ -1,37 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const defaultLayoutFilePath = path.join('app', 'views', 'layouts', 'main.html') -const backupLayoutComment = '' - -const comments = el => cy.wrap( - [...el.childNodes] - .filter(node => node.nodeName === '#comment') - .map(commentNode => '') -) - -describe('default-layout', () => { - afterEach(restoreStarterFiles) - - it('deleting default layout does not cause pages to fail to render', () => { - waitForApplication() - cy.visit('/') - - cy.document().then(doc => - comments(doc.head).should('not.contain', backupLayoutComment) - ) - - cy.task('deleteFile', { filename: path.join(Cypress.env('projectFolder'), defaultLayoutFilePath) }) - - cy.visit('/', { failOnStatusCode: false }) - cy.get('body').should('not.contains.text', 'Error: template not found') - - cy.document().then(doc => { - cy.log('head content', doc.head.innerHTML) - comments(doc.head).should('contain', backupLayoutComment) - }) - }) -}) diff --git a/cypress/e2e/dev/6-layout-tests/title-variable.cypress.js b/cypress/e2e/dev/6-layout-tests/title-variable.cypress.js deleted file mode 100644 index 1c227465c5..0000000000 --- a/cypress/e2e/dev/6-layout-tests/title-variable.cypress.js +++ /dev/null @@ -1,56 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const indexFile = path.join('app', 'views', 'index.html') - -describe('', () => { - afterEach(restoreStarterFiles) - - it('should allow title to be set in multiple ways', () => { - waitForApplication() - cy.visit('/') - - cy.task('log', 'Should display standard home page title') - - cy.title().should('eq', 'Home - Service name goes here - GOV.UK') - - cy.task('log', 'Update index.html using "set pageName" to display customised page title') - - cy.task('createFile', { - filename: path.join(Cypress.env('projectFolder'), indexFile), - data: ` - {% extends "layouts/main.html" %} - - {% set pageName="This is my custom title" %}`, - replace: true - }) - - cy.visit('/') - - cy.task('log', 'Should display customised page title') - - cy.title().should('eq', 'This is my custom title - Service name goes here - GOV.UK') - - cy.task('log', 'Update index.html using "block pageTitle" to display overridden page title') - - cy.task('createFile', { - filename: path.join(Cypress.env('projectFolder'), indexFile), - data: ` - {% extends "layouts/main.html" %} - - {% block pageTitle %} - This is my override title - {% endblock %}`, - replace: true - }) - - cy.visit('/') - - cy.task('log', 'Should display overridden page title') - - cy.title().should('eq', 'This is my override title') - }) -}) diff --git a/cypress/e2e/errors/1-error-page-tests/fatal-error.cypress.js b/cypress/e2e/errors/1-error-page-tests/fatal-error.cypress.js deleted file mode 100644 index 6bb31caf23..0000000000 --- a/cypress/e2e/errors/1-error-page-tests/fatal-error.cypress.js +++ /dev/null @@ -1,40 +0,0 @@ -const path = require('path') -const { restoreStarterFiles } = require('../../utils') -const completelyBrokenRoutesFixture = path.join(Cypress.config('fixturesFolder'), 'completely-broken-routes.js') -const appRoutesPath = path.join('app', 'routes.js') -const appRoutes = path.join(Cypress.env('projectFolder'), appRoutesPath) - -const pageName = 'There is an error' -const contactSupportText = 'Get support' -const expectedErrorFileAndLine = `${['app', 'routes.js'].join(Cypress.config('pathSeparator'))} (line 1)` -const expectedErrorMessage = 'lkewjflkjadsf is not defined' - -const homePageName = 'GOV.UK Prototype Kit' - -describe('Fatal Error Test', () => { - afterEach(restoreStarterFiles) - - it('fatal error should show an error page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/', { failOnStatusCode: false }) - - cy.get('.govuk-heading-l').contains(homePageName) - - cy.task('log', `Replace ${appRoutes} with Broken routes`) - cy.task('copyFile', { source: completelyBrokenRoutesFixture, target: appRoutes }) - - cy.wait(5000) - - cy.get('.govuk-heading-l').contains(pageName) - cy.get('.govuk-body .govuk-link').contains(contactSupportText) - cy.get('#govuk-prototype-kit-error-file').contains(expectedErrorFileAndLine) - cy.get('#govuk-prototype-kit-error-message').contains(expectedErrorMessage) - - cy.task('log', `Restore ${appRoutes} with original routes`) - cy.task('copyFromStarterFiles', { filename: appRoutesPath }) - - cy.wait(5000) - - cy.get('.govuk-heading-l').contains(homePageName) - }) -}) diff --git a/cypress/e2e/errors/1-error-page-tests/page-not-found-error.cypress.js b/cypress/e2e/errors/1-error-page-tests/page-not-found-error.cypress.js deleted file mode 100644 index b6827ee371..0000000000 --- a/cypress/e2e/errors/1-error-page-tests/page-not-found-error.cypress.js +++ /dev/null @@ -1,28 +0,0 @@ -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const pageName = 'Page not found' -const checkPageNotFoundText = 'There is no page at /p4ge-n0t-f0und' -const helpText = 'This may be because:' -const helpList = [ - 'you typed or pasted the web address incorrectly', - 'a link in your code is wrong', - 'a form in your code is wrong', - 'you have not created the page yet'] -const contactSupportText = 'You can try and fix this yourself or contact the GOV.UK Prototype Kit team if you need help.' - -describe('Internal error Test', () => { - afterEach(restoreStarterFiles) - - it('internal server error results in 500 page being displayed correctly', () => { - waitForApplication() - - cy.visit('/p4ge-n0t-f0und', { failOnStatusCode: false }) - cy.get('.govuk-heading-l').contains(pageName) - cy.get('.govuk-body').contains(checkPageNotFoundText) - cy.get('.govuk-body').contains(helpText) - for (const hint of helpList) { - cy.get('li').contains(hint) - } - cy.get('.govuk-body').contains(contactSupportText) - }) -}) diff --git a/cypress/e2e/errors/1-error-page-tests/server-error.cypress.js b/cypress/e2e/errors/1-error-page-tests/server-error.cypress.js deleted file mode 100644 index ca1ddf70d1..0000000000 --- a/cypress/e2e/errors/1-error-page-tests/server-error.cypress.js +++ /dev/null @@ -1,74 +0,0 @@ -const path = require('path') -const { waitForApplication, restoreStarterFiles } = require('../../utils') -const routesFixture = path.join(Cypress.config('fixturesFolder'), 'routes.js') -const appRoutesPath = path.join('app', 'routes.js') -const appRoutes = path.join(Cypress.env('projectFolder'), appRoutesPath) - -const homePageName = 'GOV.UK Prototype Kit' -const errorPageName = 'There is an error' -const contactSupportText = 'Get support' - -describe('Server Error Test', () => { - beforeEach(() => { - cy.task('log', `Replace ${appRoutes} with Cypress routes`) - cy.task('copyFile', { source: routesFixture, target: appRoutes }) - }) - - afterEach(restoreStarterFiles) - - it('internal server error results in 500 page being displayed correctly', () => { - const expectedErrorFileAndLine = `${['app', 'routes.js'].join(Cypress.config('pathSeparator'))} (line 18)` - const expectedErrorMessage = 'test error' - - waitForApplication() - - cy.visit('/error', { failOnStatusCode: false }) - - cy.get('.govuk-heading-l').contains(errorPageName) - cy.get('.govuk-body .govuk-link').contains(contactSupportText) - cy.get('#govuk-prototype-kit-error-file').contains(expectedErrorFileAndLine) - cy.get('#govuk-prototype-kit-error-message').contains(expectedErrorMessage) - }) - - it('shows an error if a template cannot be found', () => { - const templateNotFoundText = 'template not found: test-page.html' - - waitForApplication() - - cy.visit('/test-page', { failOnStatusCode: false }) - - cy.get('.govuk-heading-l').contains(errorPageName) - cy.get('.govuk-body .govuk-link').contains(contactSupportText) - cy.get('#govuk-prototype-kit-error-message').contains(templateNotFoundText) - }) - - it('shows an error if sass is broken', () => { - const brokenStylesFixture = path.join(Cypress.config('fixturesFolder'), 'sass', 'broken-styles.scss') - const appSassPath = path.join('app', 'assets', 'sass', 'application.scss') - const appSass = path.join(Cypress.env('projectFolder'), appSassPath) - const brokenSassText = 'color red' - - waitForApplication() - - // Page has no error - cy.get('.govuk-heading-l').contains(homePageName) - - // Break the application.scss - cy.task('copyFile', { source: brokenStylesFixture, target: appSass }) - - cy.wait(2000) - - // Page now shows an error - cy.get('.govuk-heading-l').contains(errorPageName) - cy.get('.govuk-body .govuk-link').contains(contactSupportText) - cy.get('#govuk-prototype-kit-error-line').contains(brokenSassText) - - // Restore the application.scss from prototype starter - cy.task('copyFromStarterFiles', { filename: appSassPath }) - - waitForApplication() - - // Page should recover with no error - cy.get('.govuk-heading-l').contains(homePageName) - }) -}) diff --git a/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-cli-test.cypress.js b/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-cli-test.cypress.js deleted file mode 100644 index cb6b97ed92..0000000000 --- a/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-cli-test.cypress.js +++ /dev/null @@ -1,60 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, installPlugin, createFile, restoreStarterFiles } = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const pluginBazView = path.join(appViews, 'plugin-baz.html') -const fixtures = path.join(Cypress.config('fixturesFolder')) -const pluginLocation = path.join(fixtures, 'plugins', 'plugin-baz') - -const CYAN = 'rgb(0, 255, 255)' -const MAGENTA = 'rgb(255, 0, 255)' - -const pluginBazViewMarkup = ` -{% extends "layouts/main.html" %} - -{% block content %} -{% include "baz.njk" %} -{% endblock %} - -{% block pageScripts %} - -{% endblock %} -` - -function installBaz () { - createFile(pluginBazView, { data: pluginBazViewMarkup }) - installPlugin(`file:${pluginLocation}`) - return waitForApplication('/plugin-baz') -} - -describe('Install Plugin via CLI Test', async () => { - afterEach(restoreStarterFiles) - - it('Loads plugin-baz view correctly', () => { - installBaz() - cy.get('.plugin-baz') - .contains('Plugin Baz') - }) - - it('Loads plugin-baz style correctly', () => { - installBaz() - cy.get('.plugin-baz') - .should('have.css', 'background-color', MAGENTA) - .should('have.css', 'border-color', CYAN) - }) - - it('Loads plugin-baz script correctly', () => { - installBaz() - cy.get('.plugin-baz').click() - cy.get('.plugin-baz') - .should('have.css', 'background-color', CYAN) - .should('have.css', 'border-color', MAGENTA) - }) -}) diff --git a/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-ui-test.cypress.js b/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-ui-test.cypress.js deleted file mode 100644 index 630978c6ba..0000000000 --- a/cypress/e2e/plugins/0-mock-plugin-tests/install-plugin-via-ui-test.cypress.js +++ /dev/null @@ -1,107 +0,0 @@ -const { restoreStarterFiles, log } = require('../../utils') -const path = require('path') -const { - loadTemplatesPage, - managePluginsPagePath, - performPluginAction, - initiatePluginAction, - provePluginInstalled, - provePluginTemplatesInstalled, - provePluginTemplatesUninstalled -} = require('../plugin-utils') - -const panelCompleteQuery = '[aria-live="polite"] #panel-complete' -const fixtures = path.join(Cypress.config('fixturesFolder')) -const dependentPlugin = 'plugin-fee' -const dependentPluginName = 'Plugin Fee' -const dependentPluginLocation = [fixtures, 'plugins', dependentPlugin].join(Cypress.config('pathSeparator')) -const dependencyPlugin = 'govuk-frontend' -const dependencyPluginName = 'GOV.UK Frontend' - -describe('Install and uninstall Local Plugin via UI Test', async () => { - afterEach(restoreStarterFiles) - - it(`The ${dependentPlugin} plugin will be installed`, () => { - log(`The ${dependentPlugin} plugin templates are not available`) - loadTemplatesPage() - provePluginTemplatesUninstalled(dependentPlugin) - - // ------------------------ - - log(`Install the ${dependentPlugin} plugin`) - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(dependentPlugin)}&version=${encodeURIComponent(dependentPluginLocation)}`) - cy.get('#plugin-action-button').click() - - cy.get(panelCompleteQuery, { timeout: 20000 }) - .should('be.visible') - cy.get('a').contains('Back to plugins').click() - - provePluginInstalled(dependentPlugin, dependentPluginName) - - // ------------------------ - - log(`The ${dependentPlugin} plugin templates are available`) - cy.get('a').contains('Templates').click() - provePluginTemplatesInstalled(dependentPlugin) - - // ------------------------ - - log('Uninstall the local plugin') - cy.get('a').contains('Plugins').click() - - initiatePluginAction('uninstall', dependentPlugin, dependentPluginName) - - // ------------------------ - - log(`The ${dependentPlugin} plugin templates are not available`) - cy.get('a').contains('Templates').click() - provePluginTemplatesUninstalled(dependentPlugin) - }) - - it(`The ${dependentPlugin} plugin and ${dependencyPlugin} will be installed`, () => { - log(`The ${dependentPlugin} plugin templates are not available`) - loadTemplatesPage() - provePluginTemplatesUninstalled(dependentPlugin) - - // ------------------------ - - log(`Uninstall the ${dependencyPlugin} to force the UI to ask for it later`) - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/uninstall?package=${encodeURIComponent(dependencyPlugin)}`) - cy.get('#plugin-action-button').click() - performPluginAction('uninstall', dependencyPlugin, dependencyPluginName) - - // ------------------------ - - log(`Install the ${dependentPlugin} plugin and the ${dependencyPlugin}`) - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(dependentPlugin)}&version=${encodeURIComponent(dependentPluginLocation)}`) - // Should list the dependency plugin - cy.get('li').contains(dependencyPluginName) - cy.get('#plugin-action-button').click() - performPluginAction('install', dependentPlugin, dependentPluginName) - - // ------------------------ - - log(`The ${dependentPlugin} plugin templates are available`) - cy.get('a').contains('Templates').click() - provePluginTemplatesInstalled(dependentPlugin) - - // ------------------------ - - log('Uninstall the dependency plugin') - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/uninstall?package=${encodeURIComponent(dependencyPlugin)}`) - // Should list the dependent plugin - cy.get('li').contains(dependentPluginName) - cy.get('#plugin-action-button').click() - performPluginAction('uninstall', dependencyPlugin, dependencyPluginName) - - // ------------------------ - - log(`The ${dependentPlugin} plugin templates are not available`) - cy.get('a').contains('Templates').click() - provePluginTemplatesUninstalled(dependentPlugin) - }) -}) diff --git a/cypress/e2e/plugins/0-mock-plugin-tests/multi-combined-plugin-test.cypress.js b/cypress/e2e/plugins/0-mock-plugin-tests/multi-combined-plugin-test.cypress.js deleted file mode 100644 index 1722582f8b..0000000000 --- a/cypress/e2e/plugins/0-mock-plugin-tests/multi-combined-plugin-test.cypress.js +++ /dev/null @@ -1,104 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const pluginFooBarView = path.join(appViews, 'plugin-foo-bar.html') - -const WHITE = 'rgb(255, 255, 255)' -const RED = 'rgb(255, 0, 0)' -const YELLOW = 'rgb(255, 255, 0)' -const BLUE = 'rgb(0, 0, 255)' - -const pluginFooBarCombinedViewMarkup = ` -{% extends "layouts/main.html" %} - -{% block content %} -{% set testVar="Hello" %} -

Plugin Foo Bar

-

{{testVar | foo__strong}}

-

{{testVar | bar__link('https://gov.uk/')}}

-{% endblock %} - -{% block pageScripts %} - -{% endblock %} -` - -describe('Multiple Plugin test', async () => { - before(() => { - cy.task('createFile', { filename: pluginFooBarView, data: pluginFooBarCombinedViewMarkup }) - }) - - after(restoreStarterFiles) - - describe('Plugin Bar', () => { - it('Loads plugin-bar view correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar') - .contains('Plugin Foo Bar') - }) - - it('Loads plugin-bar style correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar') - .should('have.css', 'background-color', YELLOW) - .should('have.css', 'border-color', WHITE) - }) - - it('Loads plugin-bar script correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar').click() - cy.get('.plugin-bar') - .should('have.css', 'background-color', BLUE) - .should('have.css', 'border-color', RED) - }) - it('Uses the foo filter correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('#filter-test-bar') - .should('contain.html', 'Hello') - }) - }) - - describe('Plugin Foo', () => { - it('Loads plugin-foo view correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo') - .contains('Plugin Foo Bar') - }) - - it('Loads plugin-foo style correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo') - .should('have.css', 'background-color', YELLOW) - .should('have.css', 'border-color', WHITE) - }) - - it('Loads plugin-foo script correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo').click() - cy.get('.plugin-foo').should('have.css', 'background-color', BLUE) - .should('have.css', 'border-color', RED) - }) - it('Uses the bar filter correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('#filter-test-foo') - .should('contain.html', 'Hello') - }) - }) -}) diff --git a/cypress/e2e/plugins/0-mock-plugin-tests/multi-plugin-test.cypress.js b/cypress/e2e/plugins/0-mock-plugin-tests/multi-plugin-test.cypress.js deleted file mode 100644 index 57c2127452..0000000000 --- a/cypress/e2e/plugins/0-mock-plugin-tests/multi-plugin-test.cypress.js +++ /dev/null @@ -1,91 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const pluginFooBarView = path.join(appViews, 'plugin-foo-bar.html') - -const WHITE = 'rgb(255, 255, 255)' -const RED = 'rgb(255, 0, 0)' -const GREEN = 'rgb(0, 255, 0)' -const YELLOW = 'rgb(255, 255, 0)' -const BLUE = 'rgb(0, 0, 255)' - -const pluginFooBarSeparatedViewMarkup = ` -{% extends "layouts/main.html" %} - -{% block content %} -{% include "bar.njk" %} -{% include "foo.njk" %} -{% endblock %} - -{% block pageScripts %} - -{% endblock %} -` - -describe('Plugins test', async () => { - before(() => { - cy.task('createFile', { filename: pluginFooBarView, data: pluginFooBarSeparatedViewMarkup }) - }) - - after(restoreStarterFiles) - - describe('Plugin Bar', () => { - it('Loads plugin-bar view correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar') - .contains('Plugin Bar') - }) - - it('Loads plugin-bar style correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar') - .should('have.css', 'background-color', RED) - .should('have.css', 'border-color', GREEN) - }) - - it('Loads plugin-bar script correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-bar').click() - cy.get('.plugin-bar').should('have.css', 'background-color', GREEN) - .should('have.css', 'border-color', RED) - }) - }) - - describe('Plugin Foo', () => { - it('Loads plugin-foo view correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo') - .contains('Plugin Foo') - }) - - it('Loads plugin-foo style correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo') - .should('have.css', 'background-color', YELLOW) - .should('have.css', 'border-color', WHITE) - }) - - it('Loads plugin-foo script correctly', () => { - waitForApplication() - cy.visit('/plugin-foo-bar') - cy.get('.plugin-foo').click() - cy.get('.plugin-foo') - .should('have.css', 'background-color', BLUE) - .should('have.css', 'border-color', WHITE) - }) - }) -}) diff --git a/cypress/e2e/plugins/0-mock-plugin-tests/single-plugin-test.cypress.js b/cypress/e2e/plugins/0-mock-plugin-tests/single-plugin-test.cypress.js deleted file mode 100644 index 985e9da41b..0000000000 --- a/cypress/e2e/plugins/0-mock-plugin-tests/single-plugin-test.cypress.js +++ /dev/null @@ -1,89 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const pluginFooView = path.join(appViews, 'plugin-foo.html') - -const WHITE = 'rgb(255, 255, 255)' -const YELLOW = 'rgb(255, 255, 0)' -const BLUE = 'rgb(0, 0, 255)' - -const pluginFooViewMarkup = ` -{% extends "layouts/main.html" %} - -{% block content %} -{% include "foo.njk" %} -
{{ 'abc' | foo__strong }}
-
{{ fooEmphasize('def') }}
-
{{ fooField('pass', value='ghi') }}
-{% endblock %} - -{% block pageScripts %} - -{% endblock %} -` - -describe('Single Plugin Test', async () => { - before(() => { - cy.task('createFile', { filename: pluginFooView, data: pluginFooViewMarkup }) - }) - - after(restoreStarterFiles) - - it('Loads plugin-foo view correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('.plugin-foo') - .contains('Plugin Foo') - }) - - it('Loads plugin-foo style correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('.plugin-foo') - .should('have.css', 'background-color', YELLOW) - .should('have.css', 'border-color', WHITE) - }) - - it('Loads plugin-foo filter correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('#test-foo-strong-filter') - .should('have.html', 'abc') - }) - - it('Loads plugin-foo function correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('#test-foo-emphasize-function') - .should('have.html', 'def') - }) - - it('Loads plugin-foo foo-field macro correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('#test-foo-field-macro input') - .should('have.value', 'ghi') - }) - - it('Loads plugin-foo script correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('.plugin-foo').click() - cy.get('.plugin-foo').should('have.css', 'background-color', BLUE) - cy.get('.plugin-foo').should('have.css', 'border-color', WHITE) - }) - - it('Loads plugin-foo module correctly', () => { - waitForApplication() - cy.visit('/plugin-foo') - cy.get('#foo-module').contains('The foo result is: 3') - }) -}) diff --git a/cypress/e2e/plugins/1-available-plugins-tests/available-plugins.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/available-plugins.cypress.js deleted file mode 100644 index 96896fc24a..0000000000 --- a/cypress/e2e/plugins/1-available-plugins-tests/available-plugins.cypress.js +++ /dev/null @@ -1,97 +0,0 @@ -// local dependencies -const { uninstallPlugin, restoreStarterFiles, log } = require('../../utils') -const { - managePluginsPagePath, - loadTemplatesPage, - loadPluginsPage, - manageTemplatesPagePath, - provePluginUninstalled, - performPluginAction, - initiatePluginAction, - provePluginTemplatesUninstalled, - provePluginTemplatesInstalled -} = require('../plugin-utils') - -const panelCompleteQuery = '[aria-live="polite"] #panel-complete' - -async function installPluginTests ({ plugin, templates, version }) { - describe(plugin, () => { - after(restoreStarterFiles) - - it(`The ${plugin} templates will be installed`, () => { - log(`The ${plugin} plugin templates are not available`) - uninstallPlugin(plugin) - loadTemplatesPage() - provePluginTemplatesUninstalled(plugin) - - // ------------------------ - - log(`Install the ${plugin} plugin`) - if (version) { - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(plugin)}&version=${version}`) - - cy.get('#plugin-action-button').click() - } else { - loadPluginsPage() - provePluginUninstalled(plugin) - log(`Install the ${plugin} plugin`) - performPluginAction('install', plugin) - } - - cy.get(panelCompleteQuery, { timeout: 20000 }) - .should('be.visible') - cy.get('a').contains('Back to plugins').click() - - // ------------------------ - - log(`The ${plugin} plugin templates are available`) - cy.get('a').contains('Templates').click() - - provePluginTemplatesInstalled(plugin) - - // ------------------------ - - templates.forEach(({ name, filename }) => { - log(`View ${name} template`) - cy.visit(manageTemplatesPagePath) - cy.get(`[data-plugin-package-name="${plugin}"] a`).contains(`View ${name} page`).click() - cy.url().then(url => expect(url).to.contain(filename)) - }) - - // ------------------------ - - log(`Uninstall the ${plugin} plugin`) - cy.visit(managePluginsPagePath) - - initiatePluginAction('uninstall', plugin) - - provePluginUninstalled(plugin) - }) - }) -} - -describe('Plugin tests', () => { - after(restoreStarterFiles) - - installPluginTests({ - plugin: '@govuk-prototype-kit/common-templates', - version: '1.1.1', - templates: [ - { name: 'Blank GOV.UK', filename: 'blank-govuk.html' }, - { name: 'Blank unbranded', filename: 'blank-unbranded.html' }, - { name: 'Check answers', filename: 'check-answers.html' }, - { name: 'Confirmation', filename: 'confirmation.html' }, - { name: 'Content', filename: 'content.html' }, - { name: 'Mainstream guide', filename: 'mainstream-guide.html' }, - { name: 'Start', filename: 'start.html' }, - { name: 'Question', filename: 'question.html' } - ] - }) - - installPluginTests({ - plugin: '@govuk-prototype-kit/task-list', - version: '1.0.0', - templates: [{ name: 'Task list', filename: 'task-list.html' }] - }) -}) diff --git a/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js deleted file mode 100644 index 327f6a9c8c..0000000000 --- a/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js +++ /dev/null @@ -1,180 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { deleteFile, uninstallPlugin, installPlugin, restoreStarterFiles, log } = require('../../utils') -const { - failAction, - performPluginAction, - managePluginsPagePath, - getTemplateLink, - loadInstalledPluginsPage, - loadPluginsPage, - manageInstalledPluginsPagePath, - initiatePluginAction, - provePluginUpdated -} = require('../plugin-utils') -const { showHideAllLinkQuery, assertVisible, assertHidden } = require('../../step-by-step-utils') - -const appViews = path.join(Cypress.env('projectFolder'), 'app', 'views') -const plugin = '@govuk-prototype-kit/step-by-step' -const version1 = '1.0.0' -const version2 = 'latest' -const pluginName = 'Step By Step' -const pluginPageTemplate = '/templates/step-by-step-navigation.html' -const pluginPageTitle = 'Step by step navigation' -const pluginPagePath = '/step-by-step-navigation' - -const provePluginFunctionalityWorks = () => { - log(`Prove ${pluginName} functionality works`) - - cy.visit(pluginPagePath) - - // click toggle button and check that all steps details are visible - cy.get(showHideAllLinkQuery).contains('Show all').click() - assertVisible(1) - assertVisible(2) - - // click toggle button and check that all steps details are hidden - cy.get(showHideAllLinkQuery).contains('Hide all').click() - assertHidden(1) - assertHidden(2) -} - -const provePluginFunctionalityFails = () => { - cy.on('uncaught:exception', (err) => { - console.log(err) - // returning false here prevents Cypress from - // failing a test when javascript in the browser fails - return false - }) - - log(`Prove ${pluginName} functionality fails`) - - cy.visit(pluginPagePath) - - cy.get(showHideAllLinkQuery).should('not.exist') -} - -describe('Management plugins: ', () => { - afterEach(restoreStarterFiles) - - it('CSRF Protection on POST action', () => { - // Load the plugins page, so we don't get any network errors when running the test - loadPluginsPage() - // Now run the test - const installUrl = `${managePluginsPagePath}/install` - log(`Posting to ${installUrl} without csrf protection`) - cy.request({ - url: `${managePluginsPagePath}/install`, - method: 'POST', - failOnStatusCode: false, - body: { package: plugin } - }).then(response => { - expect(response.status).to.eq(403) - expect(response.body).to.have.property('error', 'invalid csrf token') - }) - }) - - it(`Update the ${plugin} plugin`, () => { - log(`Install ${plugin}@${version1} directly`) - uninstallPlugin(plugin) - - loadPluginsPage() - - cy.get('#plugins-updates-available-message').should('not.exist') - - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(plugin)}&version=${version1}`) - - cy.get('#plugin-action-button').click() - - performPluginAction('install', plugin, pluginName) - - cy.get('#plugins-updates-available-message').contains('1 UPDATE AVAILABLE') - - // ------------------------ - - log(`Update the ${plugin}@${version1} plugin to ${plugin}@${version2}`) - installPlugin(plugin, version1) - - loadInstalledPluginsPage() - - log(`Update the ${plugin} plugin`) - initiatePluginAction('update', plugin, pluginName) - provePluginUpdated(plugin) - - cy.get('#plugins-updates-available-message').should('not.exist') - }) - - it(`Create a page using a template from the ${plugin} plugin`, () => { - log('Install the plugin, create the page, and test the functionality') - deleteFile(path.join(appViews, 'step-by-step-navigation.html')) - installPlugin(plugin, version2) - - loadInstalledPluginsPage() - cy.get('a[href*="/templates"]') - .contains('Templates').click() - - cy.get('h2').contains(pluginName) - - // ------------------------ - - log(`Create a new ${pluginPageTitle} page`) - - cy.get(`a[href="${getTemplateLink('install', '@govuk-prototype-kit/step-by-step', pluginPageTemplate)}"]`).click() - - // create step-by-step-navigation page - cy.get('.govuk-heading-l') - .contains(`Create new ${pluginPageTitle} page`) - cy.get('#chosen-url') - .type(pluginPagePath) - cy.get('.govuk-button').contains('Create page').click() - - provePluginFunctionalityWorks() - - // ------------------------ - - log(`Uninstall the ${plugin} plugin`) - - cy.visit(manageInstalledPluginsPagePath) - initiatePluginAction('uninstall', plugin, pluginName) - - provePluginFunctionalityFails() - - // ------------------------ - - log(`Reinstall the ${plugin} plugin`) - - cy.visit(managePluginsPagePath) - initiatePluginAction('install', plugin, pluginName) - - provePluginFunctionalityWorks() - }) - - it('Get plugin page directly', () => { - log('Pass when installing a plugin already installed') - cy.task('waitUntilAppRestarts') - log(`Simulate refreshing the install ${plugin} plugin confirmation page`) - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(plugin)}`) - - cy.get('#plugin-action-button').click() - - performPluginAction('install', plugin, pluginName) - - // ------------------------ - - log('Fail when installing a non existent plugin') - const pkg = 'invalid-prototype-kit-plugin' - const invalidPluginName = 'Invalid Prototype Kit Plugin' - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(pkg)}`) - cy.get('h2').contains(`Install ${invalidPluginName}`) - failAction('install') - - // ------------------------ - - log('Fail when installing a plugin with a non existent version') - cy.visit(`${managePluginsPagePath}/install?package=${encodeURIComponent(plugin)}&version=0.0.1`) - cy.get('h2').contains(`Install ${pluginName}`) - failAction('install') - }) -}) diff --git a/cypress/e2e/plugins/1-available-plugins-tests/install-common-templates-plugin-from-templates-page.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/install-common-templates-plugin-from-templates-page.cypress.js deleted file mode 100644 index 50de2257a7..0000000000 --- a/cypress/e2e/plugins/1-available-plugins-tests/install-common-templates-plugin-from-templates-page.cypress.js +++ /dev/null @@ -1,33 +0,0 @@ -const { waitForApplication, uninstallPlugin, restoreStarterFiles } = require('../../utils') -const { provePluginTemplatesInstalled } = require('../plugin-utils') - -const manageTemplatesPagePath = '/manage-prototype/templates' -const panelCompleteQuery = '[aria-live="polite"] #panel-complete' -const plugin = '@govuk-prototype-kit/common-templates' - -async function loadTemplatesPage () { - cy.task('log', 'Visit the manage prototype templates page') - await waitForApplication(manageTemplatesPagePath) -} - -describe('Install common templates from templates page', () => { - before(() => { - uninstallPlugin(plugin) - }) - - after(restoreStarterFiles) - - it('install', () => { - loadTemplatesPage() - cy.get('a').contains('Install common templates').click() - - cy.get(panelCompleteQuery, { timeout: 20000 }) - .should('be.visible') - - cy.get('a').contains('Back to templates').click() - - provePluginTemplatesInstalled(plugin) - - cy.get('a.govuk-button').should('not.exist') - }) -}) diff --git a/cypress/e2e/plugins/1-available-plugins-tests/preview-template-view.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/preview-template-view.cypress.js deleted file mode 100644 index f7cb4e03fd..0000000000 --- a/cypress/e2e/plugins/1-available-plugins-tests/preview-template-view.cypress.js +++ /dev/null @@ -1,35 +0,0 @@ -// local dependencies -const { installPlugin, waitForApplication, restoreStarterFiles, uninstallPlugin } = require('../../utils') -const { showHideAllLinkQuery, assertVisible, assertHidden } = require('../../step-by-step-utils') -const { manageTemplatesPagePath, getTemplateLink } = require('../plugin-utils') - -const plugin = '@govuk-prototype-kit/step-by-step' -const pluginName = 'Step By Step' -const pluginPageTemplate = '/templates/step-by-step-navigation.html' -const pluginPageTitle = 'Step by step navigation' - -describe('Management plugins: ', () => { - after(restoreStarterFiles) - - it(`Preview a ${plugin} template`, () => { - cy.task('log', 'Visit the manage prototype plugins page') - installPlugin(plugin, 'latest') - waitForApplication(manageTemplatesPagePath) - - cy.get('h2').contains(pluginName) - - cy.task('log', `Preview the ${pluginPageTitle} template`) - - cy.get(`a[href="${getTemplateLink('view', '@govuk-prototype-kit/step-by-step', pluginPageTemplate)}"]`).click() - - cy.task('log', `Prove the ${pluginName} functionality works`) - - assertHidden(1) - assertHidden(2) - cy.get(showHideAllLinkQuery).contains('Show all').click() - assertVisible(1) - assertVisible(2) - - uninstallPlugin(plugin) - }) -}) diff --git a/cypress/e2e/plugins/1-available-plugins-tests/view-template-with-default-layout.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/view-template-with-default-layout.cypress.js deleted file mode 100644 index 8afda8069f..0000000000 --- a/cypress/e2e/plugins/1-available-plugins-tests/view-template-with-default-layout.cypress.js +++ /dev/null @@ -1,59 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, installPlugin, restoreStarterFiles } = require('../../utils') -const { manageTemplatesPagePath, getTemplateLink } = require('../plugin-utils') - -const plugin = '@govuk-prototype-kit/step-by-step' -const pluginPageTemplate = '/templates/step-by-step-navigation.html' -const pluginPageTitle = 'Step by step navigation' - -const defaultLayoutFilePath = path.join('app', 'views', 'layouts', 'main.html') -const backupLayoutComment = '' - -const comments = el => cy.wrap( - [...el.childNodes] - .filter(node => node.nodeName === '#comment') - .map(commentNode => '') -) - -describe('view template with default layout', () => { - beforeEach(() => { - installPlugin(plugin, 'latest') - }) - - afterEach(restoreStarterFiles) - - it('deleting default layout does not cause viewing a template to fail to render', () => { - cy.task('log', 'Visit the manage prototype plugins page') - - waitForApplication(manageTemplatesPagePath) - cy.visit(manageTemplatesPagePath) - - cy.task('log', `Preview the ${pluginPageTitle} template`) - - cy.get(`a[href="${getTemplateLink('view', '@govuk-prototype-kit/step-by-step', pluginPageTemplate)}"]`).click() - - cy.document().then(doc => - comments(doc.head).should('not.contain', backupLayoutComment) - ) - - cy.task('deleteFile', { filename: path.join(Cypress.env('projectFolder'), defaultLayoutFilePath) }) - - waitForApplication(manageTemplatesPagePath) - cy.visit(manageTemplatesPagePath) - - cy.task('log', `Preview the ${pluginPageTitle} template`) - - cy.get(`a[href="${getTemplateLink('view', '@govuk-prototype-kit/step-by-step', pluginPageTemplate)}"]`).click() - - cy.visit('/', { failOnStatusCode: false }) - cy.get('body').should('not.contains.text', 'Error: template not found') - - cy.document().then(doc => { - cy.log('head content', doc.head.innerHTML) - comments(doc.head).should('contain', backupLayoutComment) - }) - }) -}) diff --git a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/allow-upgrade-in-url.cypress.js b/cypress/e2e/plugins/2-prototype-kit-plugin-tests/allow-upgrade-in-url.cypress.js deleted file mode 100644 index 22e59cf9dd..0000000000 --- a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/allow-upgrade-in-url.cypress.js +++ /dev/null @@ -1,29 +0,0 @@ -const { replaceInFile, waitForApplication, restoreStarterFiles, log } = require('../../utils') -const path = require('path') -const { performPluginAction } = require('../plugin-utils') -const plugin = '@govuk-prototype-kit/task-list' -const pluginVersion = '1.1.1' -const originalText = '"dependencies": {' -const replacementText = `"dependencies": { "${plugin}": "${pluginVersion}",` -const pkgJsonFile = path.join(Cypress.env('projectFolder'), 'package.json') - -describe('Allow upgrade in URLs', () => { - after(restoreStarterFiles) - - it('When updating the old upgrade URL should still work', () => { - waitForApplication() - - log(`Add an old version of ${plugin} within the package.json`) - replaceInFile(pkgJsonFile, originalText, '', replacementText) - cy.exec(`cd ${Cypress.env('projectFolder')} && npm install`) - - log('Make sure old upgrade URL still works') - cy.visit(`/manage-prototype/plugins/upgrade?package=${encodeURIComponent(plugin)}`) - cy.get('button#plugin-action-button') - .contains('Update') - .click() - - log('Force the plugins to be installed with an npm install') - performPluginAction('update', plugin, 'Task List') - }) -}) diff --git a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-installation-mismatch.cypress.js b/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-installation-mismatch.cypress.js deleted file mode 100644 index 77d68fdb83..0000000000 --- a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-installation-mismatch.cypress.js +++ /dev/null @@ -1,38 +0,0 @@ -const { - replaceInFile, - waitForApplication, - restoreStarterFiles, - log -} = require('../../utils') -const path = require('path') -const { - provePluginUninstalled, - provePluginInstalledOldVersion -} = require('../plugin-utils') -const plugin = '@govuk-prototype-kit/task-list' -const pluginVersion = '1.1.1' -const originalText = '"dependencies": {' -const replacementText = `"dependencies": { "${plugin}": "${pluginVersion}",` -const pkgJsonFile = path.join(Cypress.env('projectFolder'), 'package.json') - -describe('Handle a plugin installation mismatch', () => { - after(restoreStarterFiles) - - it('where the prototype package.json specifies a dependency that has not been installed', () => { - waitForApplication() - - log(`Add ${plugin} to the dependencies within the package.json`) - replaceInFile(pkgJsonFile, originalText, '', replacementText) - - log(`Make sure ${plugin} is displayed as not installed`) - provePluginUninstalled(plugin) - - log('Force the plugins to be installed with an npm install') - cy.exec(`cd ${Cypress.env('projectFolder')} && npm install`) - - log(`Make sure ${plugin} is displayed as installed`) - waitForApplication() - - provePluginInstalledOldVersion(plugin) - }) -}) diff --git a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-update-when-a-dependency-is-now-required.cypress.js b/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-update-when-a-dependency-is-now-required.cypress.js deleted file mode 100644 index 2425f3561d..0000000000 --- a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/handle-plugin-update-when-a-dependency-is-now-required.cypress.js +++ /dev/null @@ -1,60 +0,0 @@ -import path from 'path' - -import { - installPlugin, - restoreStarterFiles, - uninstallPlugin, - waitForApplication -} from '../../utils' - -import { - initiatePluginAction, - provePluginInstalled, - provePluginUninstalled -} from '../plugin-utils' - -const plugin = '@govuk-prototype-kit/common-templates' -const pluginVersion = '1.1.1' -const pluginsPage = '/manage-prototype/plugins' - -const dependencyPlugin = 'govuk-frontend' -const dependencyPluginName = 'GOV.UK Frontend' - -const additionalScssPath = path.join(Cypress.env('projectFolder'), 'app', 'assets', 'sass', 'settings.scss') -const additionalScssContents = ` -@mixin govuk-text-colour { - color: black; -} -@mixin govuk-font($size, $weight: regular, $tabular: false, $line-height: false) { - font-size: $size -} -@mixin govuk-media-query($from) { - @media (min-width: $from) { @content; } -}` - -describe('Handle a plugin update', () => { - after(restoreStarterFiles) - - it('when a dependency is now required', () => { - cy.task('createFile', { filename: additionalScssPath, data: additionalScssContents }) - installPlugin(plugin, pluginVersion) - uninstallPlugin(dependencyPlugin) - - waitForApplication(pluginsPage) - - provePluginUninstalled(dependencyPlugin) - - initiatePluginAction('update', plugin, null, { - confirmation: () => { - cy.get('#plugin-action-confirmation') - .find('ul') - .contains(dependencyPluginName) - - cy.get('#plugin-action-button').click() - } - }) - - provePluginInstalled(plugin) - provePluginInstalled(dependencyPlugin, dependencyPluginName) - }) -}) diff --git a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/prevent-uninstalling-kit-from-ui.cypress.js b/cypress/e2e/plugins/2-prototype-kit-plugin-tests/prevent-uninstalling-kit-from-ui.cypress.js deleted file mode 100644 index ddae11b29c..0000000000 --- a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/prevent-uninstalling-kit-from-ui.cypress.js +++ /dev/null @@ -1,18 +0,0 @@ -// local dependencies -const { failAction, managePluginsPagePath } = require('../plugin-utils') -const { restoreStarterFiles } = require('../../utils') - -const plugin = 'govuk-prototype-kit' -const pluginName = 'GOV.UK Prototype Kit' - -describe('Prevent uninstalling kit from ui', () => { - after(restoreStarterFiles) - - it('Should fail', () => { - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/uninstall?package=${plugin}`) - cy.get('h2') - .contains(`Uninstall ${pluginName}`) - failAction('uninstall') - }) -}) diff --git a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/remove-govuk-frontend.cypress.js b/cypress/e2e/plugins/2-prototype-kit-plugin-tests/remove-govuk-frontend.cypress.js deleted file mode 100644 index 872fe1987c..0000000000 --- a/cypress/e2e/plugins/2-prototype-kit-plugin-tests/remove-govuk-frontend.cypress.js +++ /dev/null @@ -1,53 +0,0 @@ -const { - managePluginsPagePath, - performPluginAction, - provePluginInstalled, - provePluginUninstalled, - initiatePluginAction -} = require('../plugin-utils') -const { uninstallPlugin, restoreStarterFiles } = require('../../utils') - -const plugin = 'govuk-frontend' -const pluginName = 'GOV.UK Frontend' -const dependentPlugin = '@govuk-prototype-kit/common-templates' - -describe('Manage prototype pages without govuk-frontend', () => { - afterEach(restoreStarterFiles) - - it('Uninstall govuk-frontend', () => { - cy.task('addToConfigJson', { allowGovukFrontendUninstall: true }) - - uninstallPlugin(dependentPlugin) - - cy.task('waitUntilAppRestarts') - cy.visit(`${managePluginsPagePath}/uninstall?package=${plugin}`) - - cy.get('#plugin-action-button').contains('Uninstall').click() - - performPluginAction('uninstall', plugin, pluginName) - - cy.task('log', 'Make sure govuk-frontend is uninstalled') - - provePluginUninstalled(plugin) - - cy.task('log', 'Test home page') - cy.get('a').contains('Home').click() - cy.get('h1').contains('Manage your prototype') - - cy.task('log', 'Test templates page') - cy.get('a').contains('Templates').click() - cy.get('h1').contains('Templates') - - cy.task('log', 'Test plugins page') - cy.get('a').contains('Plugins').click() - cy.get('h1').contains('Plugins') - - cy.task('log', `Install the ${plugin} plugin`) - - initiatePluginAction('install', plugin, pluginName) - - cy.task('log', 'Make sure govuk-frontend is installed') - - provePluginInstalled(plugin, pluginName) - }) -}) diff --git a/cypress/e2e/plugins/plugin-utils.js b/cypress/e2e/plugins/plugin-utils.js deleted file mode 100644 index 8a8416ab16..0000000000 --- a/cypress/e2e/plugins/plugin-utils.js +++ /dev/null @@ -1,214 +0,0 @@ -// npm dependencies -const { capitalize } = require('lodash') -const { urlencode } = require('nunjucks/src/filters') -const { waitForApplication } = require('../utils') - -const manageTemplatesPagePath = '/manage-prototype/templates' -const managePluginsPagePath = '/manage-prototype/plugins' -const manageInstalledPluginsPagePath = '/manage-prototype/plugins-installed' - -const panelProcessingQuery = '[aria-live="polite"] #panel-processing' -const panelCompleteQuery = '[aria-live="polite"] #panel-complete' -const panelErrorQuery = '[aria-live="polite"] #panel-error' - -function getTemplateLink (type, packageName, path) { - const queryString = `?package=${urlencode(packageName)}&template=${urlencode(path)}` - return `${manageTemplatesPagePath}/${type}${queryString}` -} - -async function loadPluginsPage () { - cy.task('log', 'Visit the manage prototype plugins page') - await waitForApplication(managePluginsPagePath) -} - -async function loadInstalledPluginsPage () { - cy.task('log', 'Visit the manage prototype plugins page') - await waitForApplication(manageInstalledPluginsPagePath) -} - -async function loadTemplatesPage () { - cy.task('log', 'Visit the manage prototype templates page') - await waitForApplication(manageTemplatesPagePath) -} - -function provePluginTemplatesInstalled (plugin) { - cy.visit(manageTemplatesPagePath) - cy.get(`[data-plugin-package-name="${plugin}"]`).should('exist') -} - -function provePluginTemplatesUninstalled (plugin) { - cy.visit(manageTemplatesPagePath) - cy.get(`[data-plugin-package-name="${plugin}"]`).should('not.exist') -} - -function initiatePluginAction (action, plugin, pluginName, options = {}) { - if (action === 'install') { - cy.visit(managePluginsPagePath) - } else { - cy.visit(manageInstalledPluginsPagePath) - } - - if (pluginName) { - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('h4') - .contains(pluginName) - } - - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('button') - .contains(capitalize(action)) - .click() - - if (options.confirmation) { - options.confirmation() - } - - performPluginAction(action, plugin, pluginName) -} - -function provePluginInstalled (plugin, pluginName) { - cy.visit(managePluginsPagePath) - if (pluginName) { - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('h4') - .contains(pluginName) - } - - cy.get(`[data-plugin-package-name="${plugin}"] strong.govuk-tag`) - .contains('Installed') - - cy.get('#installed-plugins-link').click() - - cy.get(`[data-plugin-package-name="${plugin}"]`) - .should('exist') -} - -function provePluginUninstalled (plugin, pluginName) { - cy.visit(managePluginsPagePath) - if (pluginName) { - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('h4') - .contains(pluginName) - } - - cy.get(`[data-plugin-package-name="${plugin}"] strong.govuk-tag`) - .should('not.exist') - - cy.get('#installed-plugins-link').click() - - cy.get(`[data-plugin-package-name="${plugin}"]`) - .should('not.exist') -} - -function provePluginUpdated (plugin, pluginName) { - provePluginInstalled(plugin, pluginName) - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('button') - .contains(capitalize('update')).should('not.exist') -} - -function provePluginInstalledOldVersion (plugin, pluginName) { - cy.visit(managePluginsPagePath) - if (pluginName) { - cy.get(`[data-plugin-package-name="${plugin}"]`) - .scrollIntoView() - .find('h4') - .contains(pluginName) - } - - cy.get('#installed-plugins-link').click() - - cy.get(`[data-plugin-package-name="${plugin}"]`) - .should('exist') -} - -function performPluginAction (action, plugin, pluginName) { - cy.task('log', `The ${plugin} plugin should be displayed`) - - if (pluginName) { - cy.get('h2') - .contains(pluginName) - } - - const processingText = `${action === 'update' ? 'Updat' : action}ing ...` - - if (Cypress.env('skipPluginActionInterimStep') !== 'true') { - cy.get(panelCompleteQuery, { timeout: 20000 }) - .should('not.be.visible') - cy.get(panelErrorQuery) - .should('not.be.visible') - cy.get(panelProcessingQuery) - .should('be.visible') - .contains(capitalize(processingText)) - } - - cy.task('log', `The ${plugin} plugin is ${action === 'update' ? 'updat' : action}ing`) - - cy.get(panelProcessingQuery, { timeout: 20000 }) - .should('not.be.visible') - cy.get(panelErrorQuery) - .should('not.be.visible') - cy.get(panelCompleteQuery) - .should('be.visible') - .contains(`${capitalize(action)} complete`) - - cy.task('log', `The ${plugin} plugin ${action} has completed`) - - cy.get('#instructions-complete a') - .contains('Back to plugins') - .click() - - cy.task('log', 'Returning to plugins page') - - cy.get('h1').contains('Plugins') -} - -function failAction (action) { - cy.get('#plugin-action-button').click() - - if (Cypress.env('skipPluginActionInterimStep') !== 'true') { - cy.get(panelCompleteQuery, { timeout: 20000 }) - .should('not.be.visible') - cy.get(panelErrorQuery) - .should('not.be.visible') - cy.get(panelProcessingQuery) - .should('be.visible') - .contains(`${capitalize(action === 'update' ? 'Updat' : action)}ing ...`) - } - - cy.get(panelProcessingQuery, { timeout: 40000 }) - .should('not.be.visible') - cy.get(panelCompleteQuery) - .should('not.be.visible') - cy.get(panelErrorQuery) - .should('be.visible') - - cy.get(`${panelErrorQuery} .govuk-panel__title`) - .contains(`There was a problem ${action === 'update' ? 'Updat' : action}ing`) - cy.get(`${panelErrorQuery} a`) - .contains('Please contact support') -} - -module.exports = { - managePluginsPagePath, - manageInstalledPluginsPagePath, - manageTemplatesPagePath, - loadPluginsPage, - loadInstalledPluginsPage, - loadTemplatesPage, - getTemplateLink, - initiatePluginAction, - performPluginAction, - provePluginInstalled, - provePluginUninstalled, - provePluginUpdated, - provePluginInstalledOldVersion, - provePluginTemplatesInstalled, - provePluginTemplatesUninstalled, - failAction -} diff --git a/cypress/e2e/prod/1-home-page-tests/home-page-in-production.cypress.js b/cypress/e2e/prod/1-home-page-tests/home-page-in-production.cypress.js deleted file mode 100644 index 9f51a6d3b9..0000000000 --- a/cypress/e2e/prod/1-home-page-tests/home-page-in-production.cypress.js +++ /dev/null @@ -1,13 +0,0 @@ -// local dependencies -const { authenticate, restoreStarterFiles } = require('../../utils') - -describe('home page in production', () => { - after(restoreStarterFiles) - - it('should load as expected', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/') - authenticate() - cy.get('h1').contains('Service name goes here') - }) -}) diff --git a/cypress/e2e/prod/2-management-tests/management-not-available.cypress.js b/cypress/e2e/prod/2-management-tests/management-not-available.cypress.js deleted file mode 100644 index 3a7b1e0841..0000000000 --- a/cypress/e2e/prod/2-management-tests/management-not-available.cypress.js +++ /dev/null @@ -1,56 +0,0 @@ -// local dependencies -const { authenticate, restoreStarterFiles } = require('../../utils') - -describe('management not available', () => { - after(restoreStarterFiles) - - it('when attempting to visit "/manage-prototype" page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/manage-prototype') - authenticate() - cy.get('h1').should('contain.text', 'How to manage your prototype') - }) - - it('when attempting to visit "/manage-prototype/templates" page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/manage-prototype/templates') - authenticate() - cy.get('h1').should('contain.text', 'How to manage your prototype') - }) - - it('when attempting to visit "/manage-prototype/plugins" page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/manage-prototype/plugins') - authenticate() - cy.get('h1').should('contain.text', 'How to manage your prototype') - }) - - it('when attempting to visit "/manage-prototype/foo" page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/manage-prototype/foo') - authenticate() - cy.get('h1').should('contain.text', 'How to manage your prototype') - }) - - it('manage prototype link should not exist on the home page', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/') - authenticate() - cy.get('main a[href="/manage-prototype"]').should('not.exist') - }) - - it('manage prototype link should not exist in the footer', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/') - authenticate() - cy.get('footer a[href="/manage-prototype"]').should('not.exist') - }) - - it('clear data link should exist in the footer and work correctly', () => { - cy.task('waitUntilAppRestarts') - cy.visit('/') - authenticate() - cy.get('footer a[href="/manage-prototype/clear-data"]').should('contain.text', 'Clear data').click() - cy.get('h1').should('contain.text', 'Clear session data') - }) -}) diff --git a/cypress/e2e/prod/2-management-tests/password-page.cypress.js b/cypress/e2e/prod/2-management-tests/password-page.cypress.js deleted file mode 100644 index 19b8755576..0000000000 --- a/cypress/e2e/prod/2-management-tests/password-page.cypress.js +++ /dev/null @@ -1,53 +0,0 @@ -const { restoreStarterFiles, log } = require('../../utils') -const homePath = '/index' -const passwordPath = '/manage-prototype/password' -const errorQuery = 'error=wrong-password' -const returnURLQuery = `returnURL=${encodeURIComponent(homePath)}` -const additionalPasswords = Cypress.env('additionalPasswords') || [] - -describe('password page', () => { - after(restoreStarterFiles) - - it('valid password', () => { - const password = Cypress.env('password') - cy.task('waitUntilAppRestarts') - cy.visit(homePath) - cy.url().then(passwordUrl => { - const urlObject = new URL(passwordUrl) - expect(passwordUrl).equal(`${urlObject.origin + passwordPath}?${returnURLQuery}`) - log(`Authenticating with ${password}`) - cy.get('input#password').type(password) - cy.get('form').submit() - cy.url().should('eq', urlObject.origin + homePath) - }) - }) - - it('invalid password', () => { - cy.task('waitUntilAppRestarts') - cy.visit(homePath) - cy.url().then(passwordUrl => { - const urlObject = new URL(passwordUrl) - expect(passwordUrl).equal(`${urlObject.origin + passwordPath}?${returnURLQuery}`) - cy.get('input#password').type('invalid') - cy.get('form').submit() - cy.get('.govuk-error-summary__list').contains('The password is not correct') - cy.get('#password-error').contains('The password is not correct') - cy.url().should('eq', `${urlObject.origin + passwordPath}?${errorQuery}&${returnURLQuery}`) - }) - }) - - additionalPasswords.map(password => - it(`valid additional password "${password}"`, () => { - cy.task('waitUntilAppRestarts') - cy.visit(homePath) - cy.url().then(passwordUrl => { - const urlObject = new URL(passwordUrl) - expect(passwordUrl).equal(`${urlObject.origin + passwordPath}?${returnURLQuery}`) - log(`Authenticating with ${password}`) - cy.get('input#password').type(password) - cy.get('form').submit() - cy.url().should('eq', urlObject.origin + homePath) - }) - }) - ) -}) diff --git a/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js b/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js deleted file mode 100644 index 1d5c2e8628..0000000000 --- a/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js +++ /dev/null @@ -1,25 +0,0 @@ -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -describe('smoke test', () => { - after(restoreStarterFiles) - - it('index page', () => { - waitForApplication('/') - cy.get('h2').contains('GOV.UK Prototype Kit') - }) - - it('GOV.UK Frontend fonts loaded', () => { - waitForApplication('/manage-prototype') - - const fontUrl = '/manage-prototype/dependencies/govuk-frontend/dist/govuk/assets/fonts/bold-b542beb274-v2.woff2' - - cy.task('log', 'Requesting govuk-frontend font') - cy.request(`/${fontUrl}`, { retryOnStatusCodeFailure: true }) - .then(response => expect(response.status).to.eq(200)) - - cy.task('log', 'Check page has loaded fonts successfully') - cy.document() - .invoke('fonts.check', '16px GDS Transport') - .should('be.true') - }) -}) diff --git a/cypress/e2e/step-by-step-utils.js b/cypress/e2e/step-by-step-utils.js deleted file mode 100644 index 81748d3a5a..0000000000 --- a/cypress/e2e/step-by-step-utils.js +++ /dev/null @@ -1,24 +0,0 @@ -const showHideAllLinkQuery = '.app-step-nav__controls button' -const toggleButtonQuery = (step) => `[data-position="${step}"]` -const showHideLinkQuery = (step) => `[data-position="${step}"]` -const panelQuery = (step) => `[data-position="${step}.1"]` -const titleQuery = (step) => `[data-position="${step}"] .js-step-title-text` - -const assertVisible = (step) => { - cy.get(showHideLinkQuery(step)).contains('Hide') - cy.get(panelQuery(step)).should('be.visible') -} - -const assertHidden = (step) => { - cy.get(panelQuery(step)).should('not.be.visible') - cy.get(showHideLinkQuery(step)).contains('Show') -} - -module.exports = { - showHideAllLinkQuery, - toggleButtonQuery, - showHideLinkQuery, - titleQuery, - assertVisible, - assertHidden -} diff --git a/cypress/e2e/styles/1-watch-styles/watch-custom-styles.cypress.js b/cypress/e2e/styles/1-watch-styles/watch-custom-styles.cypress.js deleted file mode 100644 index 8996acd2e8..0000000000 --- a/cypress/e2e/styles/1-watch-styles/watch-custom-styles.cypress.js +++ /dev/null @@ -1,63 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const customStylesFixture = 'custom-styles' -const customStylesFixtureName = `${customStylesFixture}.scss` -const customStylesFixturePath = path.join(Cypress.config('fixturesFolder'), 'sass', customStylesFixtureName) -const customStylesAppPath = path.join(Cypress.env('projectFolder'), 'app', 'assets', 'sass', customStylesFixtureName) -const customStylesPublicPath = 'public/stylesheets/custom-styles.css' - -const pageFixture = 'custom-styles' -const pageFixtureName = `${pageFixture}.html` -const pageFixturePath = path.join(Cypress.config('fixturesFolder'), 'views', pageFixtureName) -const pageAppPath = path.join(Cypress.env('projectFolder'), 'app', 'views', pageFixtureName) - -describe('watch custom sass files', () => { - describe(`sass file ${customStylesFixtureName} should be created and linked within ${pageFixturePath} and accessible from the browser as /${customStylesPublicPath}`, () => { - afterEach(restoreStarterFiles) - - it('The colour of the paragraph should be changed to green', () => { - waitForApplication() - cy.visit('/') - - // FIXME: the expected behaviour is that it shouldn't make a difference - // whether the stylesheet exists or not, but currently for Browsersync to - // update the page properly the stylesheet has to be created first, so we - // create an empty one. See issue #1440 for more details. - cy.task('log', 'Create an empty custom stylesheet') - cy.task('createFile', { filename: customStylesAppPath, data: '// Custom styles\n' }) - - cy.task('log', 'Create a page to view our custom styles') - cy.task('copyFile', { - source: pageFixturePath, - target: pageAppPath - }) - - cy.task('log', 'The colour of the paragraph should be black') - cy.visit(`/${pageFixture}`) - cy.get('p.app-custom-style').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') - - cy.task('log', `Create ${customStylesAppPath}`) - cy.task('copyFile', { - source: customStylesFixturePath, - target: customStylesAppPath - }) - - // When we add our stylesheet, we expect Browsersync to detect that and - // tell the browser to fetch it, so let's wait for that resource to - // become available. - cy.task('log', 'Wait for the stylesheet to be loaded') - cy.waitForResource(`${customStylesFixture}.css`) - - cy.task('log', 'The colour of the paragraph should be changed to green') - cy.get('p.app-custom-style').should('have.css', 'background-color', 'rgb(0, 255, 0)') - - cy.task('log', `Request ${customStylesPublicPath}`) - cy.request(`/${customStylesPublicPath}`, { retryOnNetworkFailure: true, timeout: 4000 }) - .then(response => expect(response.status).to.eq(200)) - }) - }) -}) diff --git a/cypress/e2e/styles/1-watch-styles/watch-settings-styles.cypress.js b/cypress/e2e/styles/1-watch-styles/watch-settings-styles.cypress.js deleted file mode 100644 index 6caba07cdb..0000000000 --- a/cypress/e2e/styles/1-watch-styles/watch-settings-styles.cypress.js +++ /dev/null @@ -1,48 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, createFile, deleteFile, replaceInFile, restoreStarterFiles } = require('../../utils') - -const appStylesPath = path.join('app', 'assets', 'sass') -const appStylesFolder = path.join(Cypress.env('projectFolder'), appStylesPath) - -const settingsStyle = path.join(appStylesFolder, 'settings.scss') - -const RED = 'rgb(255, 0, 0)' -const GREEN = 'rgb(0, 255, 0)' - -const settingsContent = `$govuk-brand-colour: ${RED}` -const changedSettingsContent = `$govuk-brand-colour: ${GREEN}` - -describe('watching settings.scss', () => { - afterEach(restoreStarterFiles) - - it('Successfully reload settings changes', () => { - waitForApplication() - - cy.task('log', 'The colour of the header bottom border should be as designed') - cy.get('.govuk-header__container').should('not.have.css', 'border-bottom-color', RED) - - createFile(settingsStyle, { data: settingsContent }) - - waitForApplication() - - cy.task('log', 'The colour of the header bottom border should be changed to red') - cy.get('.govuk-header__container').should('have.css', 'border-bottom-color', RED) - - replaceInFile(settingsStyle, settingsContent, '', changedSettingsContent) - - waitForApplication() - - cy.task('log', 'The colour of the header bottom border should be changed to green') - cy.get('.govuk-header__container').should('have.css', 'border-bottom-color', GREEN) - - deleteFile(settingsStyle) - - waitForApplication() - - cy.task('log', 'The colour of the header bottom border should be as designed') - cy.get('.govuk-header__container').should('not.have.css', 'border-bottom-color', GREEN) - }) -}) diff --git a/cypress/e2e/styles/1-watch-styles/watch-styles.cypress.js b/cypress/e2e/styles/1-watch-styles/watch-styles.cypress.js deleted file mode 100644 index df30e2a796..0000000000 --- a/cypress/e2e/styles/1-watch-styles/watch-styles.cypress.js +++ /dev/null @@ -1,51 +0,0 @@ -// core dependencies -const path = require('path') - -// local dependencies -const { waitForApplication, restoreStarterFiles } = require('../../utils') - -const appStylesPath = path.join('app', 'assets', 'sass') -const appStylesheetPath = path.join(appStylesPath, 'application.scss') -const appStylesFolder = path.join(Cypress.env('projectFolder'), appStylesPath) -const appStylesheet = path.join(Cypress.env('projectFolder'), appStylesheetPath) - -const cypressTestStyles = 'cypress-test' -const cypressTestStylePattern = path.join(appStylesFolder, 'patterns', `_${cypressTestStyles}.scss`) -const publicStylesheet = 'public/stylesheets/application.css' - -const RED = 'rgb(255, 0, 0)' -const BLACK = 'rgb(11, 12, 12)' - -describe('watch sass files', () => { - describe(`sass file ${cypressTestStylePattern} should be created and included within the ${appStylesheet} and accessible from the browser as /${publicStylesheet}`, () => { - const cssStatement = ` - .govuk-header { background: red; } - ` - - afterEach(restoreStarterFiles) - - it('The colour of the header should be changed to red then back to black', () => { - waitForApplication() - - cy.task('log', 'The colour of the header should be black') - cy.get('.govuk-header').should('have.css', 'background-color', BLACK) - - cy.task('log', `Create ${cypressTestStylePattern}`) - cy.task('createFile', { - filename: cypressTestStylePattern, - data: cssStatement - }) - - cy.task('log', `Amend ${appStylesheet} to import ${cypressTestStyles}`) - cy.task('appendFile', { - filename: appStylesheet, - data: ` - @import "patterns/${cypressTestStyles}"; - ` - }) - - cy.task('log', 'The colour of the header should be changed to red') - cy.get('.govuk-header').should('have.css', 'background-color', RED) - }) - }) -}) diff --git a/cypress/e2e/utils.js b/cypress/e2e/utils.js deleted file mode 100644 index 04ae35eead..0000000000 --- a/cypress/e2e/utils.js +++ /dev/null @@ -1,76 +0,0 @@ -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) - -const log = (message) => cy.task('log', message) - -const authenticate = () => { - const password = Cypress.env('password') - if (password) { - log(`Authenticating with ${password}`) - cy.get('input#password').type(password) - cy.get('form').submit() - } -} - -const waitForApplication = async (path = '/index') => { - log(`Waiting for app to restart and load ${path} page`) - cy.task('waitUntilAppRestarts') - cy.visit(path) - cy.get('.govuk-header__logotype') - .contains('GOV.UK') -} - -const copyFile = (source, target) => { - log(`Copy ${source} to ${target}`) - cy.task('copyFile', { source, target }) -} - -const deleteFile = (filename) => { - log(`Delete ${filename}`) - cy.task('deleteFile', { filename }) -} - -const createFile = (filename, options) => { - log(`Create ${filename}`) - cy.task('createFile', { filename, ...options }) -} - -const replaceInFile = (filename, originalText, source, newText) => { - cy.task('replaceTextInFile', { filename, originalText, source, newText }) -} - -const restoreStarterFiles = () => { - cy.task('restoreStarterFiles') -} - -function uninstallPlugin (plugin) { - log(`Uninstalling ${plugin}`) - cy.exec(`cd ${Cypress.env('projectFolder')} && npm uninstall ${plugin}`) - cy.task('pluginUninstalled', { plugin, timeout: 15000 }) -} - -function installPlugin (plugin, version = '') { - if (version) { - version = '@' + version - } - log(`Installing ${plugin}${version}`) - cy.exec(`cd ${Cypress.env('projectFolder')} && npm install ${plugin}${version} --save-exact `) - if (plugin.startsWith('file:')) { - plugin = plugin.substring(plugin.lastIndexOf('/') + 1) - } - log(`Waiting for ${plugin}${version} to be installed`) - cy.task('pluginInstalled', { plugin, version, timeout: 15000 }) -} - -module.exports = { - authenticate, - sleep, - log, - waitForApplication, - copyFile, - deleteFile, - createFile, - replaceInFile, - installPlugin, - uninstallPlugin, - restoreStarterFiles -} diff --git a/cypress/events/index.js b/cypress/events/index.js deleted file mode 100644 index 2161b7d4c7..0000000000 --- a/cypress/events/index.js +++ /dev/null @@ -1,385 +0,0 @@ -/// -// *********************************************************************************************** -// The setupNodeEvents function is where node events can be registered and config can be modified. -// This takes the place of the (removed) pluginFile option. -// -// You can read more here: -// https://docs.cypress.io/guides/references/configuration#setupNodeEvents -// *********************************************************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -// core dependencies -const fs = require('fs') -const fsp = fs.promises -const fse = require('fs-extra') -const path = require('path') - -// npm dependencies -const waitOn = require('wait-on') -const extract = require('extract-zip') -const https = require('https') - -// local dependencies -const { starterDir } = require('../../lib/utils/paths') -const { sleep } = require('../e2e/utils') -const { requestHttpsJson } = require('../../lib/utils/requestHttps') -const { getFileHash } = require('../../migrator/file-helpers') -const { exec } = require('../../lib/exec') - -const log = (message) => console.log(`${new Date().toLocaleTimeString()} => ${message}`) - -const createFolderForFile = async (filepath) => { - const dir = filepath.substring(0, filepath.lastIndexOf('/')) - if (dir && !fs.existsSync(dir)) { - await fsp.mkdir(dir, { - recursive: true - }) - } -} - -const validateExtractedVersion = async fullFilename => { - // Retrieve the name and version from the package.json from the extracted zip file - const extractFolder = path.resolve(fullFilename.substring(0, fullFilename.lastIndexOf('.zip'))) - log(`validating extract => ${extractFolder}`) - const data = fs.readFileSync(path.join(extractFolder, 'package.json')) - const { name, version } = JSON.parse(data) - // Retrieve the directory name of the extracted files - const separator = (extractFolder.indexOf('/') > -1) ? '/' : '\\' - const dirname = extractFolder.substring(extractFolder.lastIndexOf(separator) + 1) - // Make sure they match - if (`${name}-${version}` !== dirname) { - throw new Error(`Extracted folder ${extractFolder} contains wrong version in package.json >> ${name}-${version}`) - } -} - -const downloadsFolder = path.resolve('cypress', 'downloads') - -module.exports = function setupNodeEvents (on, config) { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - - config.env.password = process.env.PASSWORD - config.env.additionalPasswords = (process.env.PASSWORD_KEYS || '') - .split(',') - .map(passwordKey => process.env[passwordKey.trim()]) - .filter(password => !!password) - config.env.projectFolder = path.resolve(process.env.KIT_TEST_DIR || process.cwd()) - config.env.tempFolder = path.join(__dirname, '..', 'temp') - config.env.skipPluginActionInterimStep = process.env.SKIP_PLUGIN_ACTION_INTERIM_STEP - - const packagePath = path.join(config.env.projectFolder, 'package.json') - const packageContent = fs.readFileSync(packagePath, 'utf8') - const packageObject = JSON.parse(packageContent) - const dependencies = packageObject.dependencies || {} - - if ('govuk-prototype-kit' in dependencies) { - config.env.packageFolder = path.join(config.env.projectFolder, 'node_modules', 'govuk-prototype-kit') - } - - const waitUntilAppRestarts = (timeout = 20000) => waitOn({ - delay: 3000, - resources: [config.baseUrl], - timeout - }) - const getReplacementText = async (text, source) => source ? fsp.readFile(source) : text - const replaceText = ({ text, originalText, newText, source }) => { - return getReplacementText(newText, source) - .then((replacementText) => { - if (text.includes(originalText)) { - return text.replace(originalText, replacementText) - } else { - throw new Error('Text to be replaced not found') - } - }) - } - - const replaceMultipleText = async (text, list) => { - let resultText = text - let index = 0 - while (index < list.length) { - resultText = await replaceText({ text: resultText, ...list[index] }) - index++ - } - return resultText - } - - const makeSureCypressCanInterpretTheResult = () => null - - const existsFile = (filename, timeout = 0) => fsp.access(filename) - .then(makeSureCypressCanInterpretTheResult) - .catch((err) => err.code !== 'ENOENT' - ? err - : async () => { - if (timeout < 100) { - return null - } else { - await sleep(100) - return existsFile(filename, timeout - 100) - } - } - ) - - const notExistsFile = (filename, timeout = 0) => fsp.access(filename) - .then(async () => { - if (timeout < 100) { - return makeSureCypressCanInterpretTheResult() - } else { - await sleep(100) - return notExistsFile(filename, timeout - 100) - } - }) - .catch((err) => err.code !== 'ENOENT' ? err : makeSureCypressCanInterpretTheResult()) - - const deleteFile = (filename, timeout = 0) => fsp.unlink(filename) - .then(() => sleep(timeout)) - .catch((err) => err.code !== 'ENOENT' ? err : null - ) - - const deleteFolder = (folder, timeout = 0) => fsp.rmdir(folder, { recursive: true }) - .then(() => sleep(timeout)) - .catch((err) => err.code !== 'ENOENT' ? err : null - ) - - const download = async (url, zipFile) => { - return new Promise((resolve, reject) => { - log(`downloading => ${url}`) - const request = https.get(url, response => { - if (response.statusCode === 200) { - const filename = path.join(downloadsFolder, zipFile) - log(`writing => ${filename}`) - const file = fs.createWriteStream(filename, { flags: 'wx' }) - file.on('finish', () => resolve(filename)) - file.on('error', (err) => { - file.close() - fs.unlink(downloadsFolder, () => { - log(`writing => ${filename} => ${err.message}`) - reject(err.message) - }) // Delete temp file - }) - response.pipe(file) - } else if (response.statusCode === 302 || response.statusCode === 301) { - // Recursively follow redirects, only a 200 will resolve. - const { location } = response.headers - if (!zipFile && location.endsWith('.zip')) { - const uri = new URL(location) - zipFile = uri.pathname.split('/').pop() - } - return download(location, zipFile).then((filename) => - resolve(filename)) - } else { - reject(new Error(`Server responded with ${response.statusCode}: ${response.statusMessage}`)) - } - }) - - request.on('error', err => { - reject(err.message) - }) - }) - } - - const getPathFromProjectRoot = (...all) => path.join(...[config.env.projectFolder].concat(all)) - const pathToPackageFile = packageName => getPathFromProjectRoot('node_modules', packageName, 'package.json') - - const pluginInstalled = async (plugin, version, timeout) => { - const delay = 1000 - - if (version === '@latest') { - const packageInfo = await requestHttpsJson(`https://registry.npmjs.org/${encodeURIComponent(plugin)}`) - const { latest } = packageInfo['dist-tags'] - version = '@' + latest - } - - const retry = async () => { - log(`Will retry in ${delay} milliseconds`) - await sleep(delay) - await pluginInstalled(plugin, version, timeout - delay) - } - - return new Promise((resolve) => { - const pkgFilePath = pathToPackageFile(plugin) - log(`Waiting for ${pkgFilePath} to exist`) - return existsFile(pkgFilePath, timeout).then(() => { - if (version) { - const packageContent = fs.readFileSync(pkgFilePath, 'utf8') - const { version: currentVersion } = JSON.parse(packageContent) - log(`Current version of ${plugin} is @${currentVersion}`) - if (version === '@' + currentVersion) { - resolve(makeSureCypressCanInterpretTheResult) - } else { - retry().then(() => resolve(makeSureCypressCanInterpretTheResult)) - } - } else { - log('Skip version test') - resolve(makeSureCypressCanInterpretTheResult) - } - }) - }) - } - - const backupStarterFiles = () => { - const projectDir = path.join(config.env.projectFolder) - const backupDir = path.join(config.env.tempFolder, 'backupStarterFiles') - - // Define the filter function - const filter = (dir) => !dir.includes('node_modules') && !dir.includes('package-lock.json') - - return fse.emptyDir(backupDir) - // Copy the files using the filter - .then(() => fse.copy(projectDir, backupDir, { filter })) - .then(makeSureCypressCanInterpretTheResult) - } - - const restoreStarterFiles = async (remainingRetries = 4) => { - try { - const tmpDir = path.join(config.env.projectFolder, '.tmp') - const appDir = path.join(config.env.projectFolder, 'app') - const appViewsDir = path.join(appDir, 'views') - const appDataDir = path.join(appDir, 'data') - const appAssetsDir = path.join(appDir, 'assets') - const appSassDir = path.join(appAssetsDir, 'sass') - const appJSDir = path.join(appAssetsDir, 'javascripts') - const backupDir = path.join(config.env.tempFolder, 'backupStarterFiles') - const projectDir = path.join(config.env.projectFolder) - - const originalPackageJsonHash = await getFileHash(path.join(backupDir, 'package.json')) - const currentPackageJsonHash = await getFileHash(path.join(projectDir, 'package.json')) - - // Delete the files - await Promise.all([ - tmpDir, - appViewsDir, - appDataDir, - appJSDir, - appSassDir - ].map(async dir => fse.emptyDir(dir))) - - // Copy the files - await fse.copy(backupDir, projectDir) - if (originalPackageJsonHash !== currentPackageJsonHash) { - log('Restoring to starter plugins') - const command = 'npm prune && npm install' - await exec(command, { cwd: config.env.projectFolder }) - await sleep(1000) - // To allow for possible SASS recompilation, wait again - await waitUntilAppRestarts() - await sleep(1000) - log(`Completed ${command}`) - } - await waitUntilAppRestarts() - return makeSureCypressCanInterpretTheResult() - } catch (error) { - if (remainingRetries > 0) { - remainingRetries = typeof remainingRetries === 'number' ? remainingRetries - 1 : 0 - await sleep(1000) - log('Trying again') - return restoreStarterFiles(remainingRetries) - } else { - console.error(JSON.stringify({ error }, null, 2)) - } - } - } - - on('before:browser:launch', backupStarterFiles) - - on('task', { - copyFile: ({ source, target }) => createFolderForFile(target) - .then(() => fsp.copyFile(source, target)) - // The sleep of 2 seconds allows for the file to be copied completely to prevent - // it from not existing when the file is needed in a subsequent step - .then(() => sleep(2000)) // pause after the copy - .then(makeSureCypressCanInterpretTheResult), - - copyFromStarterFiles: ({ starterFilename = undefined, filename }) => { - const src = path.join(starterDir, starterFilename || filename) - const dest = path.join(config.env.projectFolder, filename) - return createFolderForFile(dest) - .then(() => fsp.copyFile(src, dest)) - // The sleep of 2 seconds allows for the file to be copied completely to prevent - // it from not existing when the file is needed in a subsequent step - .then(() => sleep(2000)) // pause after the copy - .then(makeSureCypressCanInterpretTheResult) - }, - - createFile: ({ filename, data, replace = false }) => createFolderForFile(filename) - .then(() => fsp.writeFile(filename, data, { - flag: replace ? 'w' : '' // Flag of w will overwrite - })) - .then(makeSureCypressCanInterpretTheResult), - - appendFile: ({ filename, data }) => fsp.appendFile(filename, data) - .then(makeSureCypressCanInterpretTheResult), - - deleteFile: ({ filename, timeout }) => deleteFile(filename, timeout) - .then(makeSureCypressCanInterpretTheResult), - - existsFile: ({ filename, timeout }) => existsFile(filename, timeout) - .then(makeSureCypressCanInterpretTheResult), - - notExistsFile: ({ filename, timeout }) => notExistsFile(filename, timeout) - .then(makeSureCypressCanInterpretTheResult), - - pluginInstalled: ({ plugin, version, timeout }) => pluginInstalled(plugin, version, timeout) - .then(makeSureCypressCanInterpretTheResult), - - pluginUninstalled: ({ plugin, timeout }) => { - const pkgFilePath = pathToPackageFile(plugin) - return notExistsFile(pkgFilePath, timeout) - .then(makeSureCypressCanInterpretTheResult) - }, - - waitUntilAppRestarts: (config) => { - const { timeout = 20000 } = config || {} - return waitUntilAppRestarts(timeout) - .then(makeSureCypressCanInterpretTheResult) - }, - - replaceTextInFile: ({ filename, ...options }) => fsp.readFile(filename) - .then((buffer) => replaceText({ text: buffer.toString(), ...options })) - .then((text) => fsp.writeFile(filename, text.toString())) - .then(makeSureCypressCanInterpretTheResult), - - replaceMultipleTextInFile: ({ filename, list }) => fsp.readFile(filename) - .then((buffer) => replaceMultipleText(buffer.toString(), list)) - .then((text) => fsp.writeFile(filename, text.toString())) - .then(makeSureCypressCanInterpretTheResult), - - download: async ({ filename }) => { - log(`deleting folder => ${downloadsFolder}`) - return deleteFolder(downloadsFolder, 2000) - .then(() => fsp.mkdir(downloadsFolder, { recursive: true })) - .then(() => download(filename)) - .then((fullFilename) => { - log(`extracting => ${fullFilename}`) - return extract(fullFilename, { dir: downloadsFolder }) - .then(() => { - return validateExtractedVersion(fullFilename) - }) - }) - .then(makeSureCypressCanInterpretTheResult) - }, - - addToConfigJson: (additionalConfig) => { - log(`Adding config JSON => ${downloadsFolder}`) - const appConfigPath = path.join(config.env.projectFolder, 'app', 'config.json') - return fse.readJson(appConfigPath) - .then(existingConfig => Object.assign({}, existingConfig, additionalConfig)) - .then(newConfig => fse.writeJson(appConfigPath, newConfig)) - .then(makeSureCypressCanInterpretTheResult) - }, - - restoreStarterFiles: () => { - log('Restoring to starter files') - return restoreStarterFiles() - .then(makeSureCypressCanInterpretTheResult) - }, - - log: (message) => { - log(message) - return makeSureCypressCanInterpretTheResult() - } - }) - - return config -} diff --git a/cypress/fixtures/completely-broken-routes.js b/cypress/fixtures/completely-broken-routes.js deleted file mode 100644 index d54d3384a7..0000000000 --- a/cypress/fixtures/completely-broken-routes.js +++ /dev/null @@ -1 +0,0 @@ -lkewjflkjadsf // eslint-disable-line diff --git a/cypress/fixtures/components/juggling-balls-component.html b/cypress/fixtures/components/juggling-balls-component.html deleted file mode 100644 index 91e0ff9026..0000000000 --- a/cypress/fixtures/components/juggling-balls-component.html +++ /dev/null @@ -1,30 +0,0 @@ -{% from "govuk/components/radios/macro.njk" import govukRadios %} - -{{ govukRadios({ - idPrefix: "how-many-balls", - name: "how-many-balls", - fieldset: { - legend: { - text: "How many balls can you juggle?", - isPageHeading: true, - classes: "govuk-fieldset__legend--l" - } - }, - items: [ - { - value: "3 or more", - text: "3 or more", - checked: checked("how-many-balls", "3 or more") - }, - { - value: "1 or 2", - text: "1 or 2", - checked: checked("how-many-balls", "1 or 2") - }, - { - value: "None - I cannot juggle", - text: "None - I cannot juggle", - checked: checked("how-many-balls", "None - I cannot juggle") - } - ] -}) }} \ No newline at end of file diff --git a/cypress/fixtures/components/juggling-balls-route-component.js b/cypress/fixtures/components/juggling-balls-route-component.js deleted file mode 100644 index b03c92bb69..0000000000 --- a/cypress/fixtures/components/juggling-balls-route-component.js +++ /dev/null @@ -1,15 +0,0 @@ -// Run this code when a form is submitted to 'juggling-balls-answer' -// eslint-disable-next-line -router.post('/juggling-balls-answer', (req, res) => { - // Make a variable and give it the value from 'how-many-balls' - const howManyBalls = req.session.data['how-many-balls'] - - // Check whether the variable matches a condition - if (howManyBalls === '3 or more') { - // Send user to next page - res.redirect('/juggling-trick') - } else { - // Send user to ineligible page - res.redirect('/ineligible') - } -}) diff --git a/cypress/fixtures/components/juggling-trick-component.html b/cypress/fixtures/components/juggling-trick-component.html deleted file mode 100644 index 4cf7ec55eb..0000000000 --- a/cypress/fixtures/components/juggling-trick-component.html +++ /dev/null @@ -1,12 +0,0 @@ -{% from "govuk/components/textarea/macro.njk" import govukTextarea %} - -{{ govukTextarea({ - name: "most-impressive-trick", - id: "most-impressive-trick", - value: data["most-impressive-trick"], - label: { - text: "What is your most impressive juggling trick?", - classes: "govuk-label--l", - isPageHeading: true - } -}) }} \ No newline at end of file diff --git a/cypress/fixtures/images/larry-the-cat.jpg b/cypress/fixtures/images/larry-the-cat.jpg deleted file mode 100644 index 9cef78cca5..0000000000 Binary files a/cypress/fixtures/images/larry-the-cat.jpg and /dev/null differ diff --git a/cypress/fixtures/plugins/plugin-bar/filters.js b/cypress/fixtures/plugins/plugin-bar/filters.js deleted file mode 100644 index 7f4b2ea0fa..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/filters.js +++ /dev/null @@ -1,6 +0,0 @@ -// local dependencies -const { addFilter } = require('govuk-prototype-kit').views - -addFilter('bar__link', - (content, url) => `${content}`, - { renderAsHtml: true }) diff --git a/cypress/fixtures/plugins/plugin-bar/govuk-prototype-kit.config.json b/cypress/fixtures/plugins/plugin-bar/govuk-prototype-kit.config.json deleted file mode 100644 index 1be250ad6c..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/govuk-prototype-kit.config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "nunjucksFilters": [ - "/filters.js" - ], - "nunjucksPaths": [ - "/views" - ], - "scripts": [ - "/scripts/bar.js" - ], - "sass": [ - "/sass/bar.scss" - ] -} diff --git a/cypress/fixtures/plugins/plugin-bar/package.json b/cypress/fixtures/plugins/plugin-bar/package.json deleted file mode 100644 index 38aeb333bb..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "plugin-bar", - "version": "1.0.0", - "dependencies": { - "govuk-prototype-kit": "file:../../../.." - } -} diff --git a/cypress/fixtures/plugins/plugin-bar/sass/bar.scss b/cypress/fixtures/plugins/plugin-bar/sass/bar.scss deleted file mode 100644 index a07c13bb36..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/sass/bar.scss +++ /dev/null @@ -1,8 +0,0 @@ -.plugin-bar { - border: rgb(0, 255, 0) solid 10px; - background: rgb(255, 0, 0); - &.plugin-bar-clicked { - border: rgb(255, 0, 0) solid 10px;; - background: rgb(0, 255, 0); - } -} \ No newline at end of file diff --git a/cypress/fixtures/plugins/plugin-bar/scripts/bar.js b/cypress/fixtures/plugins/plugin-bar/scripts/bar.js deleted file mode 100644 index b822779177..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/scripts/bar.js +++ /dev/null @@ -1,19 +0,0 @@ -window.BAR = window.BAR || {} -window.BAR.Modules = window.BAR.Modules || {}; - -((Modules) => { - class PluginBar { - constructor (query) { - console.log('Instantiate Plugin Bar') - const bar = document.querySelector(query) - bar.classList.add('plugin-bar') - bar.addEventListener('click', () => this.onClick(bar)) - } - - onClick (bar) { - bar.classList.add('plugin-bar-clicked') - } - } - - Modules.PluginBar = PluginBar -})(window.BAR.Modules) diff --git a/cypress/fixtures/plugins/plugin-bar/views/bar.html b/cypress/fixtures/plugins/plugin-bar/views/bar.html deleted file mode 100644 index cf2fd10d3d..0000000000 --- a/cypress/fixtures/plugins/plugin-bar/views/bar.html +++ /dev/null @@ -1 +0,0 @@ -

Plugin Bar

diff --git a/cypress/fixtures/plugins/plugin-baz/filters.js b/cypress/fixtures/plugins/plugin-baz/filters.js deleted file mode 100644 index de7e7af043..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/filters.js +++ /dev/null @@ -1,6 +0,0 @@ -// local dependencies -const { addFilter } = require('govuk-prototype-kit').views - -addFilter('baz__link', - (content, url) => `${content}`, - { renderAsHtml: true }) diff --git a/cypress/fixtures/plugins/plugin-baz/govuk-prototype-kit.config.json b/cypress/fixtures/plugins/plugin-baz/govuk-prototype-kit.config.json deleted file mode 100644 index c31f235a68..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/govuk-prototype-kit.config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "nunjucksFilters": "/filters.js", - "nunjucksPaths": "/views", - "scripts": "/scripts/baz.js", - "sass": "/sass/baz.scss" -} diff --git a/cypress/fixtures/plugins/plugin-baz/package.json b/cypress/fixtures/plugins/plugin-baz/package.json deleted file mode 100644 index a82a10fde4..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "plugin-baz", - "version": "1.0.0", - "dependencies": { - "govuk-prototype-kit": "file:../../../.." - } -} diff --git a/cypress/fixtures/plugins/plugin-baz/sass/baz.scss b/cypress/fixtures/plugins/plugin-baz/sass/baz.scss deleted file mode 100644 index 960759381a..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/sass/baz.scss +++ /dev/null @@ -1,8 +0,0 @@ -.plugin-baz { - border: rgb(0, 255, 255) solid 10px; - background: rgb(255, 0, 255); - &.plugin-baz-clicked { - border: rgb(255, 0, 255) solid 10px;; - background: rgb(0, 255, 255); - } -} \ No newline at end of file diff --git a/cypress/fixtures/plugins/plugin-baz/scripts/baz.js b/cypress/fixtures/plugins/plugin-baz/scripts/baz.js deleted file mode 100644 index abaa61e2f8..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/scripts/baz.js +++ /dev/null @@ -1,19 +0,0 @@ -window.BAZ = window.BAZ || {} -window.BAZ.Modules = window.BAZ.Modules || {}; - -((Modules) => { - class PluginBaz { - constructor (query) { - console.log('Instantiate Plugin Baz') - const baz = document.querySelector(query) - baz.classList.add('plugin-baz') - baz.addEventListener('click', () => this.onClick(baz)) - } - - onClick (baz) { - baz.classList.add('plugin-baz-clicked') - } - } - - Modules.PluginBaz = PluginBaz -})(window.BAZ.Modules) diff --git a/cypress/fixtures/plugins/plugin-baz/views/baz.njk b/cypress/fixtures/plugins/plugin-baz/views/baz.njk deleted file mode 100644 index 92c3ecd1a1..0000000000 --- a/cypress/fixtures/plugins/plugin-baz/views/baz.njk +++ /dev/null @@ -1 +0,0 @@ -

Plugin Baz

diff --git a/cypress/fixtures/plugins/plugin-fee/govuk-prototype-kit.config.json b/cypress/fixtures/plugins/plugin-fee/govuk-prototype-kit.config.json deleted file mode 100644 index d08deab419..0000000000 --- a/cypress/fixtures/plugins/plugin-fee/govuk-prototype-kit.config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "templates": [ - { - "name": "Plugin Fee page", - "path": "/templates/fee.njk", - "type": "nunjucks" - } - ], - "sass": "/sass/fee.scss", - "pluginDependencies": [{ - "packageName": "govuk-frontend", - "minVersion": "4.5.0" - }] -} diff --git a/cypress/fixtures/plugins/plugin-fee/package.json b/cypress/fixtures/plugins/plugin-fee/package.json deleted file mode 100644 index 8cd96be3f3..0000000000 --- a/cypress/fixtures/plugins/plugin-fee/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "plugin-fee", - "version": "1.0.0", - "dependencies": { - "govuk-prototype-kit": "file:../../../.." - } -} diff --git a/cypress/fixtures/plugins/plugin-fee/sass/fee.scss b/cypress/fixtures/plugins/plugin-fee/sass/fee.scss deleted file mode 100644 index 4b2b144f81..0000000000 --- a/cypress/fixtures/plugins/plugin-fee/sass/fee.scss +++ /dev/null @@ -1,4 +0,0 @@ -.plugin-fee-paragraph { - background: #d4351c; - color: #ffdd00; -} \ No newline at end of file diff --git a/cypress/fixtures/plugins/plugin-fee/templates/fee.njk b/cypress/fixtures/plugins/plugin-fee/templates/fee.njk deleted file mode 100644 index 376fd00812..0000000000 --- a/cypress/fixtures/plugins/plugin-fee/templates/fee.njk +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "layouts/main.html" %} - -{% block pageTitle %} - GOV.UK page template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block content %} -

Plugin fee styled paragraph

- -{% endblock %} diff --git a/cypress/fixtures/plugins/plugin-foo/filters.js b/cypress/fixtures/plugins/plugin-foo/filters.js deleted file mode 100644 index 268fa9ce6f..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/filters.js +++ /dev/null @@ -1,2 +0,0 @@ -const { addFilter } = require('govuk-prototype-kit').views -addFilter('foo__strong', (content) => `${content}`, { renderAsHtml: true }) diff --git a/cypress/fixtures/plugins/plugin-foo/functions.js b/cypress/fixtures/plugins/plugin-foo/functions.js deleted file mode 100644 index 1ba0031835..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/functions.js +++ /dev/null @@ -1,2 +0,0 @@ -const { addFunction } = require('govuk-prototype-kit').views -addFunction('fooEmphasize', (content) => `${content}`, { renderAsHtml: true }) diff --git a/cypress/fixtures/plugins/plugin-foo/govuk-prototype-kit.config.json b/cypress/fixtures/plugins/plugin-foo/govuk-prototype-kit.config.json deleted file mode 100644 index c99b343e55..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/govuk-prototype-kit.config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "assets": [ - "/scripts" - ], - "nunjucksFilters": [ - "/filters.js" - ], - "nunjucksFunctions": [ - "/functions.js" - ], - "nunjucksPaths": [ - "/views", - "/macros" - ], - "scripts": [ - "/scripts/foo.js", - { - "path": "/scripts/foo-module.js", - "type": "module" - } - ], - "sass": [ - "/sass/foo.scss" - ], - "nunjucksMacros": [ - { - "importFrom": "foo-field.njk", - "macroName": "fooField" - } - ] -} diff --git a/cypress/fixtures/plugins/plugin-foo/macros/foo-field.html b/cypress/fixtures/plugins/plugin-foo/macros/foo-field.html deleted file mode 100644 index da01c348bd..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/macros/foo-field.html +++ /dev/null @@ -1,6 +0,0 @@ -{% macro fooField(name, value='', type='text') %} -
- -
-{% endmacro %} \ No newline at end of file diff --git a/cypress/fixtures/plugins/plugin-foo/package.json b/cypress/fixtures/plugins/plugin-foo/package.json deleted file mode 100644 index 22686c3c61..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "plugin-foo", - "version": "1.0.0", - "dependencies": { - "govuk-prototype-kit": "file:../../../.." - } -} diff --git a/cypress/fixtures/plugins/plugin-foo/sass/foo.scss b/cypress/fixtures/plugins/plugin-foo/sass/foo.scss deleted file mode 100644 index e122853b82..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/sass/foo.scss +++ /dev/null @@ -1,7 +0,0 @@ -.plugin-foo { - border: rgb(255, 255, 255) solid 10px; - background: rgb(255, 255, 0); - &.plugin-foo-clicked { - background: rgb(0, 0, 255); - } -} diff --git a/cypress/fixtures/plugins/plugin-foo/scripts/foo-module.js b/cypress/fixtures/plugins/plugin-foo/scripts/foo-module.js deleted file mode 100644 index 30eb13cb46..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/scripts/foo-module.js +++ /dev/null @@ -1,10 +0,0 @@ -import fooSubmodule from './foo-submodule.js' - -const fooParagraph = document.getElementById('foo-module') - -if (fooParagraph) { - fooParagraph.hidden = false - setTimeout(() => { - fooParagraph.innerHTML = 'The foo result is: ' + fooSubmodule(1, 2) - }, 500) -} diff --git a/cypress/fixtures/plugins/plugin-foo/scripts/foo-submodule.js b/cypress/fixtures/plugins/plugin-foo/scripts/foo-submodule.js deleted file mode 100644 index 6fb4511de2..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/scripts/foo-submodule.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function fooSubmodule (x, y) { - return x + y -} diff --git a/cypress/fixtures/plugins/plugin-foo/scripts/foo.js b/cypress/fixtures/plugins/plugin-foo/scripts/foo.js deleted file mode 100644 index 612814c04c..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/scripts/foo.js +++ /dev/null @@ -1,19 +0,0 @@ -window.FOO = window.FOO || {} -window.FOO.Modules = window.FOO.Modules || {}; - -((Modules) => { - class PluginFoo { - constructor (query) { - console.log('Instantiate Plugin Foo') - const foo = document.querySelector(query) - foo.classList.add('plugin-foo') - foo.addEventListener('click', () => this.onClick(foo)) - } - - onClick (foo) { - foo.classList.add('plugin-foo-clicked') - } - } - - Modules.PluginFoo = PluginFoo -})(window.FOO.Modules) diff --git a/cypress/fixtures/plugins/plugin-foo/views/foo.njk b/cypress/fixtures/plugins/plugin-foo/views/foo.njk deleted file mode 100644 index 472b286b61..0000000000 --- a/cypress/fixtures/plugins/plugin-foo/views/foo.njk +++ /dev/null @@ -1,2 +0,0 @@ -

Plugin Foo

- diff --git a/cypress/fixtures/routes.js b/cypress/fixtures/routes.js deleted file mode 100644 index 774790a2db..0000000000 --- a/cypress/fixtures/routes.js +++ /dev/null @@ -1,23 +0,0 @@ -const router = require('govuk-prototype-kit').requests.setupRouter() - -router.get('/cypress-test', (req, res) => { - const heading = 'CYPRESS TEST PAGE' - res.send(` - - - ${heading} - - -

${heading}

- - -`) -}) - -router.get('/error', (req, res, next) => { - next(new Error('test error')) -}) - -router.get('/test-page', (req, res, next) => { - res.render('test-page.html') -}) diff --git a/cypress/fixtures/sass/broken-styles.scss b/cypress/fixtures/sass/broken-styles.scss deleted file mode 100644 index 642992b10d..0000000000 --- a/cypress/fixtures/sass/broken-styles.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Broken styles - -.broken-style { - color red -} diff --git a/cypress/fixtures/sass/custom-styles.scss b/cypress/fixtures/sass/custom-styles.scss deleted file mode 100644 index 5c6dd5bf89..0000000000 --- a/cypress/fixtures/sass/custom-styles.scss +++ /dev/null @@ -1,2 +0,0 @@ -// Custom styles -.app-custom-style { background: rgb(0, 255, 0); } diff --git a/cypress/fixtures/views/checkbox-test.html b/cypress/fixtures/views/checkbox-test.html deleted file mode 100644 index 7ceb841abc..0000000000 --- a/cypress/fixtures/views/checkbox-test.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} -{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} - -{% block content %} - -

Checkbox tests

- -
-
- -
- {{ govukCheckboxes({ - idPrefix: "vehicle-features", - name: "vehicle1[vehicle-features]", - fieldset: { - legend: { - text: "Which of these applies to your vehicle?" - } - }, - hint: { - text: "Select all that apply" - }, - items: [ - { - value: "Heated seats", - text: "Heated seats", - checked: checked("['vehicle1']['vehicle-features']", "Heated seats") - }, - { - value: "GPS", - text: "GPS", - checked: checked("['vehicle1']['vehicle-features']", "GPS") - }, - { - value: "Radio", - text: "Radio", - checked: checked("['vehicle1']['vehicle-features']", "Radio") - } - ] - }) }} - - - -
- -
-
-{% endblock %} diff --git a/cypress/fixtures/views/confirmation.html b/cypress/fixtures/views/confirmation.html deleted file mode 100644 index 5de7aeb2a3..0000000000 --- a/cypress/fixtures/views/confirmation.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block pageTitle %} - Confirmation page template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block content %} - -
-

Application complete

-
- -{% endblock %} diff --git a/cypress/fixtures/views/content.html b/cypress/fixtures/views/content.html deleted file mode 100644 index 7aec81ab90..0000000000 --- a/cypress/fixtures/views/content.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block pageTitle %} - Content page template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block beforeContent %} - Back -{% endblock %} - -{% block content %} - -
-
- -

- Heading goes here -

- -

This is a paragraph of text. It explains in more detail what has happened and wraps across several lines.

- -

Read more about this topic.

- -
-
- -{% endblock %} diff --git a/cypress/fixtures/views/custom-styles.html b/cypress/fixtures/views/custom-styles.html deleted file mode 100644 index ec50f5aa2d..0000000000 --- a/cypress/fixtures/views/custom-styles.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block stylesheets %} - {{ super() }} - -{% endblock stylesheets %} - -{% block pageTitle %} - Custom styles – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block content %} -

This is a paragraph of text. It has a custom style.

-{% endblock %} diff --git a/cypress/fixtures/views/juggling-check-answers.html b/cypress/fixtures/views/juggling-check-answers.html deleted file mode 100644 index aee7ca9d57..0000000000 --- a/cypress/fixtures/views/juggling-check-answers.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block pageTitle %} - Check your answers template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block beforeContent %} - Back -{% endblock %} - -{% block content %} -
-
- -

Check your answers before sending your application

- -

Personal details

- -
-
-
- Number of balls you can juggle -
-
- {{ data['how-many-balls'] }} -
-
- - Change - number of balls you can juggle - -
-
-
-
- Your most impressive juggling trick -
-
- {{ data['most-impressive-trick'] }} -
-
- - Change - your most impressive juggling trick - -
-
-
- -
- -
- -
-
-{% endblock %} diff --git a/cypress/fixtures/views/larry-the-cat.html b/cypress/fixtures/views/larry-the-cat.html deleted file mode 100644 index 9f977bca9b..0000000000 --- a/cypress/fixtures/views/larry-the-cat.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "layouts/main.html" %} - -{% block pageTitle %} - Larry the cat – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block content %} - Larry the cat, Chief Mouser to the Cabinet Office, sitting on a meeting table wearing a Union Jack bowtie. -{% endblock %} diff --git a/cypress/fixtures/views/question.html b/cypress/fixtures/views/question.html deleted file mode 100644 index d0051fb852..0000000000 --- a/cypress/fixtures/views/question.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% block pageTitle %} - Question page template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block beforeContent %} - Back -{% endblock %} - -{% block content %} - -
-
- -

Heading or question goes here

- -
- -

[Insert question content here]

- -

[See the GOV.UK Design System for examples]

- - - -
- -
-
- -{% endblock %} diff --git a/cypress/fixtures/views/start-with-step-by-step.html b/cypress/fixtures/views/start-with-step-by-step.html deleted file mode 100644 index 17998b9f90..0000000000 --- a/cypress/fixtures/views/start-with-step-by-step.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} - -{% from "govuk/components/header/macro.njk" import govukHeader %} - -{% block beforeContent %} - -{% endblock %} - -{% block content %} -

- Check what age you can drive -

- -
-
    -
  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. -
    - -
    - -
  2. -
  3. -
    - - Get a provisional licence - -
    - - - -
  4. -
-
-{% endblock %} diff --git a/cypress/fixtures/views/start.html b/cypress/fixtures/views/start.html deleted file mode 100644 index fe98923c8a..0000000000 --- a/cypress/fixtures/views/start.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} -{% from "govuk/components/header/macro.njk" import govukHeader %} - -{% block pageTitle %} - Start page template – {{ serviceName }} – GOV.UK Prototype Kit -{% endblock %} - -{% block header %} - - {{ govukHeader() }} -{% endblock %} - -{% block content %} - -

- {% if serviceName %} {{ serviceName }} {% endif %} -

- - - Start now - - -{% endblock %} diff --git a/cypress/fixtures/views/step-by-step-navigation.html b/cypress/fixtures/views/step-by-step-navigation.html deleted file mode 100644 index 051193e853..0000000000 --- a/cypress/fixtures/views/step-by-step-navigation.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.html" %} -{% from "govuk/components/header/macro.njk" import govukHeader %} - -{% block content %} -

- Learn to drive a car: step by 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. -
    -
    - -
  2. - -
  3. -
    - - Get a provisional licence - -
    - - -
  4. -
-
-{% endblock %} diff --git a/cypress/scripts/run-starter-prototype.js b/cypress/scripts/run-starter-prototype.js deleted file mode 100644 index 79667f1b40..0000000000 --- a/cypress/scripts/run-starter-prototype.js +++ /dev/null @@ -1,28 +0,0 @@ -// core dependencies -const os = require('os') -const path = require('path') - -// local dependencies -const { mkPrototype, startPrototype, installPlugins } = require('../../__tests__/utils') - -const defaultKitPath = path.join(os.tmpdir(), 'cypress', 'test-prototype') - -const testDir = path.resolve(process.env.KIT_TEST_DIR || defaultKitPath) - -;(async () => { - await mkPrototype(testDir, { overwrite: true, allowTracking: false, npmInstallLinks: true }) - - const fooLocation = path.join(__dirname, '..', 'fixtures', 'plugins', 'plugin-foo') - const barLocation = path.join(__dirname, '..', 'fixtures', 'plugins', 'plugin-bar') - - await installPlugins(testDir, [ - `"file:${fooLocation}"`, - `"file:${barLocation}"`] - ) - - if (process.argv.includes('--prodtest')) { - await startPrototype(testDir, 'production') - } else { - await startPrototype(testDir) - } -})() diff --git a/cypress/support/commands.js b/cypress/support/commands.js deleted file mode 100644 index ca8ea656a8..0000000000 --- a/cypress/support/commands.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Adds command "cy.waitForResource(name)" that checks performance entries - * for resource that ends with the given name. - * - * @see https://developers.google.com/web/tools/chrome-devtools/network/understanding-resource-timing - * - * Copied from https://github.com/cypress-io/cypress-example-recipes/blob/434f3b9b62555e53134c160b1f051b0c74a714e2/examples/testing-dom__wait-for-resource/cypress/e2e/spec.cy.js - */ -Cypress.Commands.add('waitForResource', (name, options = {}) => { - if (Cypress.browser.family === 'firefox') { - cy.log('Skip waitForResource in Firefox') - - return - } - - cy.log(`Waiting for resource ${name}`) - - const log = false // let's not log inner commands - const timeout = options.timeout || Cypress.config('defaultCommandTimeout') - - cy.window({ log }).then( - // note that ".then" method has options first, callback second - // https://on.cypress.io/then - { log, timeout }, - (win) => { - return new Cypress.Promise((resolve, reject) => { - let foundResource - - // control how long we should try finding the resource - // and if it is still not found. An explicit "reject" - // allows us to show nice informative message - setTimeout(() => { - if (foundResource) { - // nothing needs to be done, successfully found the resource - return - } - - clearInterval(interval) - reject(new Error(`Timed out waiting for resource ${name}`)) - }, timeout) - - const interval = setInterval(() => { - foundResource = win.performance - .getEntriesByType('resource') - .find((item) => item.name.endsWith(name)) - - if (!foundResource) { - // resource not found, will try again - return - } - - clearInterval(interval) - // because cy.log changes the subject, let's resolve the returned promise - // with log + returned actual result - resolve( - cy.log('✅ success').then(() => { - // let's resolve with the found performance object - // to allow tests to inspect it - return foundResource - }) - ) - }, 100) - }) - } - ) -}) - -Cypress.Commands.add( - 'download', - { prevSubject: true }, - (subject) => { - return cy.get(subject) - .invoke('attr', 'href') - .then((filename) => { - cy.url().then((uri) => { - const url = new URL(uri) - cy.task('log', `Downloading ${url.origin}${filename}`) - cy.task('download', { filename: `${url.origin}${filename}` }) - }) - }) - } -) - -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js deleted file mode 100644 index 0f22b26ae9..0000000000 --- a/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -// import './commands' - -// Alternatively you can use CommonJS syntax: -require('./commands') diff --git a/features/plugins/install-and-uninstall.feature b/features/plugins/install-and-uninstall.feature new file mode 100644 index 0000000000..6445258d76 --- /dev/null +++ b/features/plugins/install-and-uninstall.feature @@ -0,0 +1,24 @@ +@plugins +Feature: Installing and uninstalling plugins + + Scenario: Installed - show on installed plugins + When I visit the installed plugins page + Then I should see the plugin "Common Templates" in the list + + Scenario: Installed - tag as installed + When I visit the available plugins page + Then I should see the plugin "Common Templates" in the list + And The "Common Templates" plugin should be tagged as "Installed" + + Scenario: Uninstalled - hide on installed plugins + Given I uninstall the "installed:@govuk-prototype-kit/common-templates" plugin + And I wait for the uninstall to complete + When I visit the installed plugins page + Then I should not see the plugin "Common Templates" in the list + + Scenario: Uninstalled - don't tag as installed + Given I uninstall the "installed:@govuk-prototype-kit/common-templates" plugin + And I wait for the uninstall to complete + When I visit the available plugins page + Then I should see the plugin "Common Templates" in the list + And The "Common Templates" plugin should not be tagged as "Installed" diff --git a/features/plugins/update.feature b/features/plugins/update.feature new file mode 100644 index 0000000000..53c6d576fb --- /dev/null +++ b/features/plugins/update.feature @@ -0,0 +1,19 @@ +@plugins +Feature: Handle plugin update + + Scenario: When a dependency is now required + Given I have a the required SCSS to avoid plugins breaking when GOV.UK Frontend is uninstalled + And I install the "npm:@govuk-prototype-kit/common-templates:1.1.1" plugin + And I wait for the uninstall to complete + And I uninstall the "govuk-frontend" plugin using the command line + And I visit the installed plugins page + And I should not see the plugin "GOV.UK Frontend" in the list + When I update the "installed:@govuk-prototype-kit/common-templates" plugin + And I should be informed that "GOV.UK Frontend" will also be installed + And I continue with the update + And I wait for the update to complete + And I visit the installed plugins page + Then I should see the plugin "Common Templates" in the list + And I should see the plugin "GOV.UK Frontend" in the list + + diff --git a/features/screenshots/Installed - show on installed plugins1705851320555.png b/features/screenshots/Installed - show on installed plugins1705851320555.png new file mode 100644 index 0000000000..93b4125b30 Binary files /dev/null and b/features/screenshots/Installed - show on installed plugins1705851320555.png differ diff --git a/features/screenshots/Installed - show on installed plugins1705851862086.png b/features/screenshots/Installed - show on installed plugins1705851862086.png new file mode 100644 index 0000000000..4a4a29fff6 Binary files /dev/null and b/features/screenshots/Installed - show on installed plugins1705851862086.png differ diff --git a/features/screenshots/Installed - show on installed plugins1705851983005.png b/features/screenshots/Installed - show on installed plugins1705851983005.png new file mode 100644 index 0000000000..4a4a29fff6 Binary files /dev/null and b/features/screenshots/Installed - show on installed plugins1705851983005.png differ diff --git a/features/screenshots/Installed - show on installed plugins1705852212770.png b/features/screenshots/Installed - show on installed plugins1705852212770.png new file mode 100644 index 0000000000..4a4a29fff6 Binary files /dev/null and b/features/screenshots/Installed - show on installed plugins1705852212770.png differ diff --git a/features/screenshots/Installed - tag as installed1705851329511.png b/features/screenshots/Installed - tag as installed1705851329511.png new file mode 100644 index 0000000000..be647f2306 Binary files /dev/null and b/features/screenshots/Installed - tag as installed1705851329511.png differ diff --git a/features/screenshots/Installed - tag as installed1705852487701.png b/features/screenshots/Installed - tag as installed1705852487701.png new file mode 100644 index 0000000000..be647f2306 Binary files /dev/null and b/features/screenshots/Installed - tag as installed1705852487701.png differ diff --git a/features/screenshots/Installed - tag as installed1705852702154.png b/features/screenshots/Installed - tag as installed1705852702154.png new file mode 100644 index 0000000000..be647f2306 Binary files /dev/null and b/features/screenshots/Installed - tag as installed1705852702154.png differ diff --git a/features/screenshots/Installed - tag as installed1705852793308.png b/features/screenshots/Installed - tag as installed1705852793308.png new file mode 100644 index 0000000000..be647f2306 Binary files /dev/null and b/features/screenshots/Installed - tag as installed1705852793308.png differ diff --git a/features/screenshots/Uninstalled - don't tag as installed1705851465392.png b/features/screenshots/Uninstalled - don't tag as installed1705851465392.png new file mode 100644 index 0000000000..735c46f458 Binary files /dev/null and b/features/screenshots/Uninstalled - don't tag as installed1705851465392.png differ diff --git a/features/screenshots/Uninstalled - don't tag as installed1705852504805.png b/features/screenshots/Uninstalled - don't tag as installed1705852504805.png new file mode 100644 index 0000000000..7fabf82106 Binary files /dev/null and b/features/screenshots/Uninstalled - don't tag as installed1705852504805.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705851396257.png b/features/screenshots/Uninstalled - hide on installed plugins1705851396257.png new file mode 100644 index 0000000000..735c46f458 Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705851396257.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705852496272.png b/features/screenshots/Uninstalled - hide on installed plugins1705852496272.png new file mode 100644 index 0000000000..7fabf82106 Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705852496272.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705852907288.png b/features/screenshots/Uninstalled - hide on installed plugins1705852907288.png new file mode 100644 index 0000000000..7fabf82106 Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705852907288.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705853008419.png b/features/screenshots/Uninstalled - hide on installed plugins1705853008419.png new file mode 100644 index 0000000000..7fabf82106 Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705853008419.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705853139785.png b/features/screenshots/Uninstalled - hide on installed plugins1705853139785.png new file mode 100644 index 0000000000..735c46f458 Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705853139785.png differ diff --git a/features/screenshots/Uninstalled - hide on installed plugins1705859059804.png b/features/screenshots/Uninstalled - hide on installed plugins1705859059804.png new file mode 100644 index 0000000000..b32c06f25d Binary files /dev/null and b/features/screenshots/Uninstalled - hide on installed plugins1705859059804.png differ diff --git a/features/screenshots/When a dependency is now required1705851533948.png b/features/screenshots/When a dependency is now required1705851533948.png new file mode 100644 index 0000000000..6ea3b815dd Binary files /dev/null and b/features/screenshots/When a dependency is now required1705851533948.png differ diff --git a/features/screenshots/When a dependency is now required1705859719866.png b/features/screenshots/When a dependency is now required1705859719866.png new file mode 100644 index 0000000000..a3a6ec2831 Binary files /dev/null and b/features/screenshots/When a dependency is now required1705859719866.png differ diff --git a/features/support/global/DefaultCustomWorld.js b/features/support/global/DefaultCustomWorld.js new file mode 100644 index 0000000000..a353269051 --- /dev/null +++ b/features/support/global/DefaultCustomWorld.js @@ -0,0 +1,123 @@ +// CustomWorld.js +const { World } = require('@cucumber/cucumber') +const seleniumWebdriver = require('selenium-webdriver') +const chrome = require('selenium-webdriver/chrome') +const firefox = require('selenium-webdriver/firefox') +const { startKit, resetState } = require('./initKit') +const { verboseLogging, browserName, browserWidth, browserHeight, browserHeadless, fnRetryDelay, fnRetries } = require('./config') +const verboseLog = verboseLogging ? console.log : () => {} +const sharedState = {} + +class CustomWorld extends World { + driver = null + + async init () { + await Promise.all([ + this.startKitIfNotRunning(), + this.getDriver() + ]) + } + + async getDriver () { + const setOptions = (obj) => { + let objUpdated = obj + if (browserHeadless) { + objUpdated = objUpdated.headless() + } + objUpdated.windowSize({ + width: browserWidth, + height: browserHeight + }) + return objUpdated + } + if (!sharedState.driver) { + sharedState.driver = await new seleniumWebdriver.Builder().forBrowser(browserName) + .setChromeOptions(setOptions(new chrome.Options())) + .setFirefoxOptions(setOptions(new firefox.Options())) + .build() + + await sharedState.driver.manage().setTimeouts({ implicit: 2000 }) + } + this.driver = sharedState.driver + return this.driver + } + + async startKitIfNotRunning (config) { + if (!sharedState.runningKit) { + this.runningKit = sharedState.runningKit = await this.retryOnFailure(async ({ attemptNumber, previousError }) => { + verboseLog('Previous error', previousError) + verboseLog('starting kit, attempt [%s]', attemptNumber) + return await startKit(config) + }) + console.log('Kit running at:', this.runningKit.directory) + } + this.runningKit = sharedState.runningKit + return this.runningKit + } + + async resetState () { + if (!sharedState.runningKit) { + return + } + await resetState(sharedState.runningKit) + } + + static async CleanupEverything () { + if (sharedState.driver) { + sharedState.driver.quit() + } + if (sharedState.runningKit) { + sharedState.runningKit.close() + } + } + + async wait (millis) { + return new Promise((resolve, reject) => { + setTimeout(resolve, millis) + }) + } + + async retryOnFailure (fn) { + const delayBetweenRetries = fnRetryDelay + let attemptNumber = 1 + let retries = fnRetries + + let previousError + + while (true) { + try { + return await fn({ + attemptNumber: attemptNumber++, + previousError + }) + } catch (e) { + if (--retries > 0) { + previousError = e + await this.wait(delayBetweenRetries) + } else { + throw e + } + } + } + } + + async visit (relativeUrl, checkContent = async () => {}) { + if (!sharedState.driver) { + throw new Error(`Can't visit the URL ${relativeUrl} because WebDriver - fix this by running .init`) + } + if (!sharedState.runningKit) { + throw new Error(`Can't visit the URL ${relativeUrl} because the kit isn't running - fix this by running .startKitIfNotRunning or .startKitAndReplaceIfRunning`) + } + const url = `${sharedState.runningKit.serverAddress}${relativeUrl}` + await this.retryOnFailure(async ({ attemptNumber, previousError }) => { + verboseLog('previousError ' + previousError) + verboseLog('loading [%s] attempt [%s], time [%s]', url, attemptNumber++, new Date().toISOString()) + await sharedState.driver.get(url) + await checkContent(attemptNumber) + }) + } +} + +module.exports = { + CustomWorld +} diff --git a/features/support/global/config.js b/features/support/global/config.js new file mode 100644 index 0000000000..0af6d9e742 --- /dev/null +++ b/features/support/global/config.js @@ -0,0 +1,30 @@ +const os = require('os') +const verboseLogging = process.env.NPI_CUKE_VERBOSE === 'true' +const shortTimeout = Number(process.env.NPI_CUKE_SHORT_TIMEOUT || '3000') +const mediumTimeout = Number(process.env.NPI_CUKE_MEDIUM_TIMEOUT || '10000') +const longTimeout = Number(process.env.NPI_CUKE_LONG_TIMEOUT || '60000') +const fnRetries = Number(process.env.NPI_CUKE_FN_RETRIES || '10') +const fnRetryDelay = Number(process.env.NPI_CUKE_FN_RETRY_DELAY || '500') +const startingPort = Number(process.env.NPI_CUKE_STARTING_PORT || '18888') +const browserWidth = Number(process.env.NPI_CUKE_BROWSER_WIDTH || '1024') +const browserHeight = Number(process.env.NPI_CUKE_BROWSER_HEIGHT || '768') +const browserHeadless = process.env.NPI_CUKE_BROWSER_HEADLESS !== 'false' +const browserName = process.env.NPI_CUKE_BROWSER_NAME || 'chrome' +const screenshotOnFailure = process.env.NPI_CUKE_SCREENSHOT_ON_FAILURE !== false +const baseDir = process.env.NPI_CUKE_BASE_DIR || os.tmpdir() + +module.exports = { + verboseLogging, + shortTimeout, + mediumTimeout, + longTimeout, + startingPort, + browserName, + browserWidth, + browserHeight, + browserHeadless, + screenshotOnFailure, + baseDir, + fnRetries, + fnRetryDelay +} diff --git a/features/support/global/initKit.js b/features/support/global/initKit.js new file mode 100644 index 0000000000..38c6435e40 --- /dev/null +++ b/features/support/global/initKit.js @@ -0,0 +1,143 @@ +const { spawn: crossSpawn } = require('cross-spawn') +const cp = require('child_process') +const path = require('path') +const events = require('events') + +const { startingPort, verboseLogging, baseDir } = require('./config') + +let nextPort = startingPort + +function addInitialGitCommitToConfig (config) { + return new Promise((resolve, reject) => { + cp.exec('git log --pretty="%H"', { cwd: config.directory }, (err, stdout, stderr) => { + if (err) { + reject(err) + } + const result = (stdout || '').split(/[\n\r]+/)[0] + if (result && result.trim()) { + resolve({ ...config, initialCommit: result.trim() }) + } else { + resolve({ ...config }) + } + }) + }) +} + +function resetState (config) { + if (!config?.initialCommit) { + throw new Error('It\'s not possible to reset the state as no innitial commit exists in the config.') + } + return new Promise((resolve, reject) => { + cp.exec(`git reset --hard ${config.initialCommit} && npm prune && npm install`, { cwd: config.directory }, (err) => { + if (err) { + reject(err) + } else { + config.kitStartedEventEmitter.on('started', () => { + resolve() + }) + } + }) + }) +} + +function initKit (config) { + if (config.directory) { + return Promise.resolve({ startCommand: 'npm run dev', ...config }) + } + const tmpDir = config.directory || path.join(baseDir, new Date().getTime() + '_' + ('' + Math.random()).split('.')[1]) + const rootDir = path.resolve(__dirname, '../../..') + + return new Promise((resolve, reject) => { + let startCommand + const initProcess = crossSpawn('npx', [`now-prototype-it-govuk@${rootDir}`, 'create', `--version=${rootDir}`, tmpDir], { cwd: rootDir }) + initProcess.stderr.on('data', (data) => console.warn('[stderr]', data.toString())) + initProcess.stdout.on('data', (data) => { + const str = data.toString() + if (verboseLogging) { + console.log(str) + } + // eslint-disable-next-line no-unused-vars + const [_, command] = str.split('To run your prototype:') + if (command && command.trim()) { + startCommand = command.trim() + } + }) + initProcess.on('error', (error) => { + reject(error) + }) + + initProcess.on('close', code => { + if (startCommand) { + resolve({ ...config, startCommand, directory: tmpDir, kitStartedEventEmitter: new events.EventEmitter() }) + } else { + reject(new Error('initialisation failed')) + } + }) + }) +} + +async function setUsageDataPermission (config) { + // const filePath = path.join(config.directory, 'usage-data-config.json') + // console.log('Writing usage data file', filePath) + // await fse.writeJson(filePath, { collectUsageData: false }) + // console.log('Written usage data file', filePath) + return config +} + +function runKit (config) { + let hasReturned = false + + return new Promise((resolve, reject) => { + const [command, ...args] = config.startCommand.split(' ') + const kitProcess = crossSpawn(command, args, { + cwd: config.directory, + detached: true, + env: { + ...process.env, + PORT: nextPort++, + GPK_NO_STDIN: 'true' + } + }) + + kitProcess.stderr.on('data', (data) => console.warn('[stderr]', data.toString())) + kitProcess.stdout.on('data', (data) => { + const str = data.toString() + const regExpMatchArray = str.match(/(http:\/\/localhost:\d+)/) + if (regExpMatchArray && regExpMatchArray[1]) { + config.kitStartedEventEmitter.emit('started') + if (hasReturned) { + return + } + hasReturned = true + resolve({ + ...config, + serverAddress: regExpMatchArray[1].trim(), + close: () => { + kitProcess.stdin.pause() + try { + process.kill(-kitProcess.pid, 'SIGTERM') + } catch (err) { + console.error('error while stopping process') + console.error(err) + } + } + }) + } + }) + kitProcess.on('error', (error) => { + reject(error) + }) + + kitProcess.on('close', code => { + reject(new Error('initialisation failed')) + }) + }) +} + +module.exports = { + startKit: (config = {}) => initKit(config) + .then(setUsageDataPermission) + .then(addInitialGitCommitToConfig) + .then(runKit), + resetState +} diff --git a/features/support/setup.js b/features/support/setup.js new file mode 100644 index 0000000000..0ba2c6863d --- /dev/null +++ b/features/support/setup.js @@ -0,0 +1,31 @@ +// setup.js +const { Status, Before, After, setWorldConstructor, AfterAll } = require('@cucumber/cucumber') +const { CustomWorld } = require('./global/DefaultCustomWorld') +const fsp = require('fs/promises') +const fse = require('fs-extra') +const path = require('path') +const { longTimeout, screenshotOnFailure } = require('./global/config') + +setWorldConstructor(CustomWorld) + +Before({ timeout: longTimeout }, async function (scenario) { + await this.init(scenario) + await this.resetState() +}) + +After({ timeout: longTimeout }, async function (testCase) { + if (testCase.result.status === Status.FAILED && screenshotOnFailure) { + const screenshot = await this.driver.takeScreenshot() + const filePath = path.join(__dirname, '..', 'screenshots', `${testCase.pickle.name}${new Date().getTime()}.png`) + await fse.ensureDir(path.dirname(filePath)) + await fsp.writeFile(filePath, screenshot, 'base64') + this.attach(screenshot, { + mediaType: 'base64:image/png', + fileName: 'screenshot.png' + }) + } +}) + +AfterAll(async function () { + await CustomWorld.CleanupEverything() +}) diff --git a/features/support/steps.js b/features/support/steps.js new file mode 100644 index 0000000000..ee7c826686 --- /dev/null +++ b/features/support/steps.js @@ -0,0 +1,132 @@ +const { expect } = require('expect') +const { Given, When, Then } = require('@cucumber/cucumber') +const { By, until } = require('selenium-webdriver') +const { longTimeout } = require('./global/config') +const fsp = require('fs/promises') +const path = require('path') +const util = require('util') +const exec = util.promisify(require('child_process').exec) + +const installedPluginsUrl = '/manage-prototype/plugins-installed' +const pluginsUrl = '/manage-prototype/plugins' + +const lookForH1 = self => async () => { + return await self.driver.findElement(By.tagName('h1')) +} + +When('I visit the installed plugins page', { timeout: longTimeout }, async function () { + await this.visit(installedPluginsUrl, lookForH1(this)) +}) +When('I visit the available plugins page', { timeout: longTimeout }, async function () { + await this.visit(pluginsUrl, lookForH1(this)) +}) + +async function getPluginListItems (self) { + return await Promise.all(await self.driver.findElements(By.className('nowprototypeit-manage-prototype-plugin-list-plugin-list__item'))) +} + +async function getNamesOfPluginsOnPage (self) { + const pluginListItems = await getPluginListItems(self) + return await Promise.all(pluginListItems.map(async x => await x.findElement(By.className('plugin-details-link')).getText())) +} + +Then('I should see the plugin {string} in the list', async function (pluginName) { + expect(await getNamesOfPluginsOnPage(this)).toContain(pluginName) +}) + +Then('I should not see the plugin {string} in the list', async function (pluginName) { + expect(await getNamesOfPluginsOnPage(this)).not.toContain(pluginName) +}) + +async function getTagsForPlugin (self, pluginName) { + const tags = [] + await Promise.all((await getPluginListItems(self)).map(async x => { + const text = (await x.findElement(By.className('plugin-details-link')).getText()) + if (text === pluginName) { + const tagElems = await x.findElements(By.className('govuk-tag')) + const names = await Promise.all(tagElems.map(elem => elem.getText())) + names.forEach(z => tags.push(z)) + } + })) + return tags +} + +Then('The {string} plugin should be tagged as {string}', async function (pluginName, tag) { + expect((await getTagsForPlugin(this, pluginName)).map(x => x.toUpperCase())).toContain(tag.toUpperCase()) +}) + +Then('The {string} plugin should not be tagged as {string}', async function (pluginName, tag) { + expect(await getTagsForPlugin(this, pluginName)).not.toContain(tag.toUpperCase()) +}) + +Given('I install the {string} plugin', async function (pluginNameOrRef) { + // TODO: Support names as well as refs + await this.visit(`/manage-prototype/plugin/${encodeURIComponent(pluginNameOrRef)}/install`) + const installButton = await this.driver.findElement(By.id('plugin-action-button')) + installButton.click() +}) + +Given('I uninstall the {string} plugin', async function (pluginNameOrRef) { + // TODO: Support names as well as refs + await this.visit(`/manage-prototype/plugin/${encodeURIComponent(pluginNameOrRef)}`, lookForH1(this)) + const uninstallButton = await this.driver.findElement(By.className('nowprototypeit-plugin-uninstall-button')) + uninstallButton.click() +}) + +Given('I update the {string} plugin', async function (pluginNameOrRef) { + // TODO: Support names as well as refs + await this.visit(`/manage-prototype/plugin/${encodeURIComponent(pluginNameOrRef)}`, lookForH1(this)) + const updateButton = await this.driver.findElement(By.className('nowprototypeit-plugin-update-button')) + updateButton.click() +}) + +Given(/I wait for the (?:uninstall|uninstall|update) to complete/, { timeout: longTimeout }, async function () { + await this.retryOnFailure(async () => { + await this.retryOnFailure(async () => { + await this.driver.findElement(By.tagName('h1')) + }) + const completed = await this.driver.wait(until.elementLocated(By.id('instructions-complete'))) + await this.driver.wait(until.elementIsVisible(completed), 20000) + }) +}) + +Given('I uninstall the {string} plugin using the command line', { timeout: longTimeout }, async function (npmPluginName) { + await exec(`npm uninstall ${npmPluginName}`, { cwd: this.runningKit.directory }) + await new Promise((resolve) => { + this.runningKit.kitStartedEventEmitter.once('started', async () => { + await this.wait(1000) + resolve() + }) + }) +}) + +Given('I have a the required SCSS to avoid plugins breaking when GOV.UK Frontend is uninstalled', async function () { + const additionalScssPath = path.join(this.runningKit.directory, 'app', 'assets', 'sass', 'settings.scss') + const additionalScssContents = ` +@mixin govuk-text-colour { + color: black; +} +@mixin govuk-font($size, $weight: regular, $tabular: false, $line-height: false) { + font-size: $size +} +@mixin govuk-media-query($from) { + @media (min-width: $from) { @content; } +}` + await fsp.writeFile(additionalScssPath, additionalScssContents, 'utf8') +}) + +When('I should be informed that {string} will also be installed', async function (pluginName) { + const heading = await this.driver.findElement(By.id('dependency-heading')) + const listItems = await heading.findElements(By.tagName('li')) + const matchinglistItems = (await Promise.all(listItems.map(async listItem => await listItem.getText()))).filter(name => pluginName) + expect(matchinglistItems).toHaveLength(1) +}) + +When('I continue with the update', async function () { + const button = await this.driver.findElement(By.id('plugin-action-button')) + button.click() +}) + +Given('I wait {int} seconds', { timeout: longTimeout }, async function (seconds) { + await this.wait(seconds * 1000) +}) diff --git a/internal_docs/linting.md b/internal_docs/linting.md deleted file mode 100644 index 3e3ebd087e..0000000000 --- a/internal_docs/linting.md +++ /dev/null @@ -1,53 +0,0 @@ -# Linting - -The GOV.UK Prototype Kit uses [standardjs](http://standardjs.com/), an opinionated JavaScript linter. All JavaScript files follow its conventions, and it runs on CI to ensure that new pull requests are in line with them. - -## Running standard manually - -To check the whole codebase, run: - -```bash -npm run lint -``` - -## Running standard in your editor - -Easier than running standard manually is to install it as a plugin in your editor. This way, it will run automatically while you work, catching errors as they happen on a per-file basis. - -### Sublime Text - -Using [Package Control](https://packagecontrol.io/), install [SublimeLinter](http://www.sublimelinter.com/en/latest/) and [SublimeLinter-contrib-standard](https://packagecontrol.io/packages/SublimeLinter-contrib-standard). - -For automatic formatting on save, install [StandardFormat](https://packagecontrol.io/packages/StandardFormat). - -### Atom - -Install [linter-js-standard](https://atom.io/packages/linter-js-standard). - -For automatic formatting, install [standard-formatter](https://atom.io/packages/standard-formatter). For snippets, install [standardjs-snippets](https://atom.io/packages/standardjs-snippets). - -### Other editors - -There are [official guides for most of the popular editors](http://standardjs.com/index.html#text-editor-plugins). - -## Do I need to respect this? - -If you want to submit a pull request to the GOV.UK Prototype Kit, your code will need to pass the linter. - -If you're just using the GOV.UK Prototype Kit in a separate project, then no, you aren't forced to use standard, or any other linter for that matter. Just write code as you would normally. - -## Why lint? - -Automated linting ensures project-wide consistency and limits (ideally eliminates) bikeshedding discussions involving spacing, naming conventions, quotes, and others during the pull request review process. It frees the reviewer to focus on the actual substance rather than stylistic issues. - -More importantly, linting will catch some low hanging programmer errors, such as calling an undefined function or assigning a value and then never reading it. These allow the programmer to catch some bugs before having to test the code. - -## Why standard? - -Linting rules can be a contentious subject, and a lot of them are down to personal preference. The core idea of standard is to be opinionated and limit the amount of initial bikeshedding discussions around which linting rules to pick, because in the end, it's not as important which rules you pick as it is to just be consistent about it. This is why we chose standard: because we want to be consistent about how we write code, but don't want to spend unnecessary time picking different rules (which all have valid points). - -The standard docs have a [complete list of rules and some reasoning behind them](http://standardjs.com/rules.html). - -Standard is also [widely used (warning: large file)](https://github.com/feross/standard-packages/blob/master/all.json) (which means community familiarity) and has a [good ecosystem of plugins](http://standardjs.com/awesome.html). - -If we decide to move away from it, standard is effectively just a preconfigured bundle of eslint, so it can easily be replaced by switching to a generic `.eslintrc` setup. diff --git a/internal_docs/releasing/releasing-from-a-support-branch.md b/internal_docs/releasing/releasing-from-a-support-branch.md deleted file mode 100644 index 57b61b84d0..0000000000 --- a/internal_docs/releasing/releasing-from-a-support-branch.md +++ /dev/null @@ -1,124 +0,0 @@ -# Releasing from a support branch - -This document is for Prototype team developers who need to publish a support branch of the GOV.UK Prototype Kit. For example, you might need to release a fix as part of a: - -- patch release, after the team has started to merge changes for a new feature release into the `main` branch - for example, a 13.14.x release once we've started merging changes for 13.15.0 -- release, after the team has started to merge changes for a new breaking release into the `main` branch - for example, a 12.x.x release once we've started merging changes for 13.0.0 -- release for a previous major version - for example, a 12.x.x release after we've released 13.0.0 - -If you want to publish the `main` branch for the Prototype Kit, [follow the steps in releasing a new version of the Prototype Kit](./releasing.md). - -If the `main` branch only has a few unreleasable changes, you can temporarily revert these changes. - -1. Revert the unreleasable changes on the `main` branch. -2. Publish the Prototype Kit. -3. Add the reverted changes back into the `main` branch. - -However, this approach has risks. For example, it creates a messy commit history on the `main` branch. - -## Before you release - -1. Draft release notes in a Google Doc. - -## Release a new version of the Prototype Kit from the support branch - -### Change the code - -1. Find out which major version this release will be targeting, for example, if you're releasing v12.x.x, the major version is version 12. To check out the support branch for that major version, run `git checkout support/.x`. If the branch does not exist, follow these steps to create it: - - - make sure you have all tags locally by running `git fetch --all --tags --prune` - - run `git checkout tags/v -b support/.x` - for example, `git checkout tags/v12.1.1 -b support/12.x` - -2. Run `nvm use` to make sure you’re using the right version of Node.js and npm. - -3. Push the support branch to GitHub. The branch will automatically have branch protection rules applied. - -4. Create a new branch for your code changes (for example, `git checkout -b fix-the-thing`) from the `support/.x` branch. - -5. Run `npm install` to make sure you have the latest dependencies installed. - -6. Make your code changes, and test them following our [standard testing requirements](/docs/contributing/testing.md). - -7. Update the changelog with details of the fix. - -8. Commit your changes, then push your new branch (see step 4) to GitHub and raise a pull request, with `support/.x` as the base branch to merge into. - -9. Once a developer approves the pull request, merge it into `support/.x`. It’s usually a developer who reviews the pull request, but sometimes pull requests need an extra review from another role. For example, if the pull request involves a content change, you may need a review from a content designer. - -### Build a new release - -1. Check out `support/.x`. - -2. Create and check out a new branch, `support-release-[version-number]`. The version number of the new release depends on the type of release. New features correspond to a minor (X.1.X) change - for example, '12.1.0 (Feature release)'. Fixes correspond to a patch (X.X.1) change - for example, '12.1.1 (Fix release)'. In either case, refer to the previous release of that kind, and give the new release the logical next number. - -3. Run `nvm use` to make sure you’re using the right version of Node.js and npm. - -4. Run `npm install` to make sure you have the latest dependencies installed. - -5. Update the [CHANGELOG.md](/CHANGELOG.md) by: - - - changing the 'Unreleased' heading to the new version-number and release-type - for example, '12.0.1 (Fix release)' - - adding a new 'Unreleased' heading above the new version-number and release-type, so users will know where to add PRs to the changelog - -6. Update the version number in [VERSION.txt](/VERSION.txt) and update "version" in [package.json](/package.json#L4). - -7. Run `npm install` to update `npm-shrinkwrap.json`. - -8. Update `VERSION` in [update.sh](/update.sh#L5) (if it is present). - -9. Commit your changeds and open a new pull request, with `support/.x` as the base branch to merge into. Copy the relevant Changelog section into the description. - -10. Once a developer approves the pull request, merge it into `support/.x`. - -### Publish the release to GitHub - -1. [Draft a new release on GitHub](https://github.com/alphagov/govuk-prototype-kit/releases) with the target `support/.x`. - -2. In Tag version and Release title, put v[version-number], for example `v12.2.0`. - -3. In the description, paste the relevant section from the release notes in the Google Doc. - -4. Check out the `support/.x` branch and pull the latest changes. - -5. Sign in to npm (`npm login`), using the credentials for the govuk-prototype-kit npm user from Bitwarden. - -6. Run `npm publish --tag latest-` and enter the one-time password when prompted. - -7. Run `npm logout` to log out from npm. - -8. Click 'Publish release'. - -## After you publish the new release - -1. Let the community know about the release. - -Write a brief summary with highlights from the release then send it to the following slack channels: - -- X-GOV #govuk-design-system -- X-GOV #prototype-kit -- GDS #govuk-design-system - -Include a link to the install page: https://prototype-kit.service.gov.uk/docs/install. - -Include a link to the GitHub release page if there are actions for users that are not covered in the release notes. - -2. On the [GOV.UK Prototype team Sprintboard](https://github.com/orgs/alphagov/projects/15): - - - move any relevant issues from the 'Ready to Release' column to 'Done' - - close any associated milestones - -## Update the `main` branch (optional) - -1. Check out the `main` branch and pull the latest changes. - -2. Run `nvm use` and `npm install` to make sure you have the latest dependencies installed. - -3. Make the same changes as in the patch fix pull request, and test them using our [standard testing requirements](/docs/contributing/testing.md). Remember that `main` will contain changes the support branch did not have, which might affect the code changes you’ll need to make. - -4. Also update the [CHANGELOG.md](/CHANGELOG.md) with this change. Add a new ‘Unreleased’ heading above the change, so people raising new pull requests know where to add them in the changelog. Remember that the pull request links in the changelog notes will need to link to the pull requests against the `main` branch. - -5. Commit your changes. - -6. Push your branch to GitHub and raise a pull request, with `main` as the base branch to merge into. - -7. Once a developer approves the pull request, merge it into the `main` branch. diff --git a/internal_docs/technical-architecture.md b/internal_docs/technical-architecture.md deleted file mode 100644 index 85923e6a47..0000000000 --- a/internal_docs/technical-architecture.md +++ /dev/null @@ -1,112 +0,0 @@ -# Technical architecture of the GOV.UK Prototype Kit - -## How the kit is used - -Users create a new prototype as a Node.js project with the kit as a dependency. The kit is published as a package on npm (see the docs on [releasing](./releasing/releasing.md)) - -The kit includes a command line tool which users should run to start the kit (usually via an npm script). - -To help users to create a new prototype, the kit command line tool includes a `create` command. It creates a new prototype using the files from the folder [prototype-starter](/prototype-starter) in this repo. - -Users will normally start their prototype using the command `npm run dev`. This starts the prototype in a development configuration. - -When running a prototype, the kit creates a HTTP server, hosted at `localhost:3000`, which automatically renders any templates in the prototype matching `app/views/.html` folder at the URL path `/`. It will also host assets in the `app/assets` folder under the path `/public`, and compiles any Sass stylesheets matching `app/assets/stylesheets/*.scss` before hosting the stylesheets at `/public/stylesheets`. - -### Hosting prototypes - -When deploying a prototype to be shared on the internet most services will run the command `npm start`, so we reserve this command for running a hardened production configuration. - -We want prototypes to be hidden from search engines and hard for normal internet users to access, so we require that prototypes hosted online have a password. We also require HTTPS and change a few other things to improve the security of the prototype. - -### Manage prototype pages - -Some parts of the prototype can be configured or inspected via webpages at `http://localhost:3000/manage-prototype` (only when running locally). These web pages are created by a separate app, so that prototype authors are not able to modify these pages. - -### Plugins - -We want people to be able to extend the capabilities of the kit. We have a plugin subsystem that detects npm packages that contain a `govuk-prototype-kit-config.json` manifest and exposes files described by that manifest to the prototype. For instance, this can be used to share layouts and templates. - -## What the kit comes with - -To help users create prototypes, the kit has a number of dependencies of its own. - -These include: - -- [Node.js Express][expressjs] framework - -This provides the basic working of the kit - a web server that can handle requests, render templates to HTML and respond to the browser. - -- [Nunjucks][nunjucks] templating - -This allows us to keep each page template simple - and inherit the [GOV.UK Frontend][govuk-frontend] template from one place. It has powerful features such as looping over an array of variables, that make more complex prototypes much easier. - -- [Nodemon][nodemon] to watch files and restart the server - -This means any changes to routes.js or other server files are updated instantly in the prototype. - -- [Browsersync][browsersync] to reload the browser with any changes - -We serve pages in a user's prototype via Browsersync. This means any changes to user templates, Sass or JavaScript are updated instantly in the browser without needing to manually refresh. Note that this does not work for the manage prototype pages. - -- [Portscanner][portscanner] to find a free port to run the kit on - -This means if the user runs more than one prototype, the first will be on port 3000, the next on 3001 and so on. - -- [Jest][jest] and [Cypress][cypress] to run tests - -We write unit and integration tests using Jest, and acceptance tests using Cypress. - -[expressjs]: https://expressjs.com/ -[nunjucks]: https://mozilla.github.io/nunjucks/ -[govuk-frontend]: https://frontend.design-system.service.gov.uk/ -[nodemon]: https://nodemon.io/ -[browsersync]: https://browsersync.io/ -[portscanner]: https://www.npmjs.com/package/portscanner -[jest]: https://jestjs.io/ -[cypress]: https://www.cypress.io/ - -## Working on the kit - -When you want to see what a user would when working on a prototype, the kit package in this repo has a number of npm scripts for developers: - -- `npm run start:dev` - creates and starts a new prototype in a temporary folder, with the prototype kit package linked -- `npm run start:test` - creates and starts a new prototype in a temporary folder, making sure to install the kit and dependencies in the same way as users -- `npm run start:test:prod` - similar to above, this simulates running a prototype in production, but without having to worry about certificates for HTTPS -- `npm run lint` - run linter -- `npm run test` - runs unit and integration tests, and the linter -- `npm run test:acceptance:open` - open Cypress allowing running acceptance tests individually and debugging results (dev acceptance tests only) -- `npm run test:acceptance:dev` - runs main suite of acceptance tests in headless mode (there are other suites that are usually run in CI only) - -### Layout of code - -The code in this repo is roughly laid out as below: - -- `__tests__/` - integration tests -- `bin/` - the `govuk-prototype-kit` command line interface -- `cypress/` - acceptance tests -- `internal_docs/` - developer docs in Markdown, including this document -- `lib/` - most source code goes in here -- `migrator/` - to help users migrate an existing prototype using a version of the kit pre-13, we have a `migrate` command -- `prototype-starter/` - files that are are copied to create a new empty prototype kit project -- `scripts/` - small self-contained scripts used for development -- `tmp/` - some tests put files in here -- `/govuk-prototype-kit-config.json` - some parts of the kit are files that prototype authors can access, we use the plugin subsystem, this configuration file is the plugin manifest -- `/index.js` - exposes the Node api of the kit - -### Where tests are written - -Unit tests are written next to the module being tested, as `.spec.js`. - -Integration tests are kept in `__tests__/spec/.js`. - -Acceptance tests are kept in `cypress/e2e/**/*.js`. - -New features should have acceptance tests and unit tests. - -### How tests are run - -We use [GitHub Actions] for continuous integration. - -Tests are run only for branches that have pull requests, as there are lots of them and they can take a while. We currently do have a fair bit of flakiness as well, sometimes tests will fail but pass with a re-run. - -[GitHub Actions]: https://docs.github.com/en/actions diff --git a/known-plugins.json b/known-plugins.json index 77eb9cdfe1..1f8ab5d6a1 100644 --- a/known-plugins.json +++ b/known-plugins.json @@ -12,6 +12,29 @@ "required": [ "govuk-prototype-kit", "govuk-frontend" - ] + ], + "proxyConfig": { + "jquery": { + "scripts": ["/dist/jquery.js"], + "assets": ["/dist"], + "meta": { + "description": "jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers." + } + }, + "notifications-node-client": { + "meta": { + "description": "GOV.UK Notify makes it easy for public sector service teams to send emails, text messages and letters." + } + }, + "home-office-kit": {"pluginDependencies": ["govuk-frontend"]}, + "@govuk-prototype-kit/common-templates": {"pluginDependencies": ["govuk-frontend"]}, + "@govuk-prototype-kit/step-by-step": {"pluginDependencies": ["govuk-frontend"]}, + "@govuk-prototype-kit/task-list": {"pluginDependencies": ["govuk-frontend"]}, + "hmrc-frontend": {"pluginDependencies": ["govuk-frontend"]}, + "@hmcts/frontend": {"pluginDependencies": ["govuk-frontend"]}, + "@ministryofjustice/frontend": {"pluginDependencies": ["govuk-frontend"]}, + "@x-govuk/govuk-prototype-components": {"pluginDependencies": ["govuk-frontend"]} + } + } } diff --git a/lib/assets/javascripts/kit.js b/lib/assets/javascripts/kit.js index 41aea6971b..41480d50cf 100644 --- a/lib/assets/javascripts/kit.js +++ b/lib/assets/javascripts/kit.js @@ -14,7 +14,7 @@ window.GOVUKPrototypeKit = { // Warn about using the kit in production if (window.console && window.console.info) { - window.console.info('GOV.UK Prototype Kit - do not use for production') + window.console.info('Now Prototype It - do not use for production') } window.GOVUKPrototypeKit.documentReady(function () { diff --git a/lib/assets/javascripts/manage-prototype/manage-plugins.js b/lib/assets/javascripts/manage-prototype/manage-plugins.js index 1253e9b6d7..998a8ca828 100644 --- a/lib/assets/javascripts/manage-prototype/manage-plugins.js +++ b/lib/assets/javascripts/manage-prototype/manage-plugins.js @@ -1,15 +1,10 @@ ;(() => { - const params = new URLSearchParams(window.location.search) - const packageName = params.get('package') - const version = params.get('version') - const mode = params.get('mode') || window.location.pathname.split('/').pop() - const timeout = 30 * 1000 let requestTimeoutId let timedOut = false - let kitIsRestarting = false let actionTimeoutId + let localStorageKey const show = (id) => { const element = document.getElementById(id) @@ -25,6 +20,18 @@ } } + const storeInLocalStorage = (status) => { + window.localStorage.setItem(localStorageKey, JSON.stringify(status)) + } + + const cleanupLocalStorage = () => { + const maxAgeInDays = 30 + const maxAgeEopchTime = new Date().getTime() - 1000 * 60 * 24 * maxAgeInDays + const allKeys = Object.keys(window.localStorage) + const expiredKeys = allKeys.filter(x => Number(x.split('#')[1]) < maxAgeEopchTime) + expiredKeys.forEach(key => window.localStorage.removeItem(key)) + } + const showCompleteStatus = () => { if (actionTimeoutId) { clearTimeout(actionTimeoutId) @@ -32,6 +39,7 @@ hide('panel-processing') show('panel-complete') show('instructions-complete') + cleanupLocalStorage() } const showErrorStatus = () => { @@ -50,10 +58,19 @@ 'Content-Type': 'application/json', 'X-CSRF-TOKEN': token }, - body: JSON.stringify({ - package: packageName, - version + body: JSON.stringify({}) + }) + .then(response => { + return response.json() }) + } + + const getRequest = (url) => { + return fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json' + } }) .then(response => { return response.json() @@ -74,17 +91,13 @@ }) } - const pollStatus = () => { + const pollStatus = (statusUrl) => { // Be aware that changing this path for monitoring the status of a plugin will affect the // kit update process as the browser request and server route would be out of sync. - return postRequest(`/manage-prototype/plugins/${mode}/status`) + return getRequest(statusUrl) .then(data => { - if (kitIsRestarting) { - // kit has restarted as the request has returned with data - kitIsRestarting = false - if (data.status === 'processing') { - return makeRequest(performAction) - } + if (data.status) { + storeInLocalStorage({ status: data.status, statusUrl }) } switch (data.status) { case 'completed': @@ -94,14 +107,12 @@ } default: { // poll status again if prototype hasn't restarted - return makeRequest(pollStatus) + return makeRequest(() => pollStatus(statusUrl)) } } }) .catch(() => { - // kit must be restarting as the request failed - kitIsRestarting = true - return makeRequest(pollStatus) + return makeRequest(() => pollStatus(statusUrl)) }) } @@ -124,21 +135,16 @@ if (event && event.preventDefault) { event.preventDefault() hide('plugin-action-confirmation') + hide('plugin-action-confirmation-multiple') } show('panel-processing') return getToken() - .then((token) => postRequest(`/manage-prototype/plugins/${mode}`, token)) + .then((token) => postRequest(window.location.href, token)) .then(data => { - switch (data.status) { - case 'completed': - return showCompleteStatus() - case 'processing': - return makeRequest(pollStatus) - default: - return showErrorStatus() - } + storeInLocalStorage({ statusUrl: data.statusUrl }) + return makeRequest(() => pollStatus(data.statusUrl)) }) .catch(() => { // kit must be restarting as the request failed so try again @@ -146,16 +152,36 @@ }) } - const init = () => { - kitIsRestarting = false + const loadPreviousStatus = (previousRunObj) => { + console.log('previousState', previousRunObj) + switch (previousRunObj.status) { + case 'completed': + return showCompleteStatus() + case 'error': { + return showErrorStatus() + } + default: { + // poll status again if prototype hasn't restarted + return makeRequest(() => pollStatus(previousRunObj.statusUrl)) + } + } + } + + const init = (config) => { timedOut = false actionTimeoutId = null requestTimeoutId = null + if (window.location.hash) { + localStorageKey = ['manage-plugins', window.location.pathname, window.location.hash].join('__') + } hide('panel-manual-instructions') const actionButton = document.getElementById('plugin-action-button') + const previousRun = localStorageKey && window.localStorage.getItem(localStorageKey) - if (actionButton) { + if (previousRun) { + return loadPreviousStatus(JSON.parse(previousRun)) + } else if (actionButton) { actionButton.addEventListener('click', performAction) } else { return performAction() diff --git a/lib/assets/javascripts/manage-prototype/manage-plugins.test.js b/lib/assets/javascripts/manage-prototype/manage-plugins.test.js index aa25941c13..9f05af1945 100644 --- a/lib/assets/javascripts/manage-prototype/manage-plugins.test.js +++ b/lib/assets/javascripts/manage-prototype/manage-plugins.test.js @@ -62,8 +62,8 @@ describe('manage-plugins', () => { global.fetch = jest.fn().mockResolvedValue(null) const getTokenUrl = '/manage-prototype/csrf-token' - const performActionUrl = '/manage-prototype/plugins/' - const pollStatusUrl = '/manage-prototype/plugins//status' + const statusUrl = '/hello/world' + const selfUrl = 'http://localhost/' const { document, window } = global let managePlugins @@ -76,10 +76,20 @@ describe('manage-plugins', () => { if (status === 'throws') { return Promise.reject(new Error('The server is restarting')) } - return fetchResponse({ status }) + return fetchResponse(status) }) jest.spyOn(global, 'fetch').mockImplementation((url) => { - return responses[responseIndex++](url) + const index = responseIndex++ + const fn = responses[index] + if (!fn) { + throw new Error(`More responses than expected, ${responses.length} configured, ${index + 1} called + + Calls were [${fetchList.join(', ')}] + + Failing call was [${url}] + `) + } + return fn(url) }) } @@ -130,8 +140,9 @@ describe('manage-plugins', () => { it('completed', async () => { mockFetch([ - { token }, - 'completed' + { status: { token } }, + { statusUrl }, + { status: 'completed' } ]) await managePlugins.performAction() @@ -140,14 +151,16 @@ describe('manage-plugins', () => { expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ getTokenUrl, - performActionUrl + selfUrl, + statusUrl ]) }) it('error', async () => { mockFetch([ - { token }, - 'error' + { status: token }, + { statusUrl }, + { status: 'error' } ]) await managePlugins.performAction() @@ -156,17 +169,20 @@ describe('manage-plugins', () => { expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ getTokenUrl, - performActionUrl + selfUrl, + statusUrl ]) }) it('will restart', async () => { mockFetch([ - { token }, - 'throws', - { token }, - 'processing', - 'completed'] + { status: { token } }, + { statusUrl }, + { status: 'throws' }, + { status: { token } }, + { statusUrl }, + { status: 'processing' }, + { status: 'completed' }] ) await managePlugins.performAction() @@ -175,10 +191,12 @@ describe('manage-plugins', () => { expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ getTokenUrl, - performActionUrl, - getTokenUrl, - performActionUrl, - pollStatusUrl + selfUrl, + statusUrl, + statusUrl, + statusUrl, + statusUrl, + statusUrl ]) }) }) @@ -190,37 +208,37 @@ describe('manage-plugins', () => { it('completed', async () => { mockFetch([ - 'processing', - 'throws', - 'completed' + { status: 'processing' }, + { status: 'throws' }, + { status: 'completed' } ]) - await managePlugins.pollStatus() + await managePlugins.pollStatus(statusUrl) expect(document.body.innerHTML).toEqual(completedHTML) expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ - pollStatusUrl, - pollStatusUrl, - pollStatusUrl + statusUrl, + statusUrl, + statusUrl ]) }) it('error', async () => { mockFetch([ - 'processing', - 'throws', - 'error' + { status: 'processing' }, + { status: 'throws' }, + { status: 'error' } ]) - await managePlugins.pollStatus() + await managePlugins.pollStatus(statusUrl) expect(document.body.innerHTML).toEqual(errorHTML) expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ - pollStatusUrl, - pollStatusUrl, - pollStatusUrl + statusUrl, + statusUrl, + statusUrl ]) }) }) @@ -232,11 +250,12 @@ describe('manage-plugins', () => { it('completed', async () => { mockFetch([ - { token }, - 'processing', - 'processing', - 'throws', - 'completed' + { status: { token } }, + { statusUrl }, + { status: 'processing' }, + { status: 'processing' }, + { status: 'throws' }, + { status: 'completed' } ]) await managePlugins.init() @@ -245,20 +264,22 @@ describe('manage-plugins', () => { expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ getTokenUrl, - performActionUrl, - pollStatusUrl, - pollStatusUrl, - pollStatusUrl + selfUrl, + statusUrl, + statusUrl, + statusUrl, + statusUrl ]) }) it('error', async () => { mockFetch([ - { token }, - 'processing', - 'processing', - 'throws', - 'error' + { status: { token } }, + { statusUrl }, + { status: 'processing' }, + { status: 'processing' }, + { status: 'throws' }, + { status: 'error' } ]) await managePlugins.init() @@ -267,10 +288,11 @@ describe('manage-plugins', () => { expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) expect(fetchList).toEqual([ getTokenUrl, - performActionUrl, - pollStatusUrl, - pollStatusUrl, - pollStatusUrl + selfUrl, + statusUrl, + statusUrl, + statusUrl, + statusUrl ]) }) }) diff --git a/lib/assets/sass/includes/_error-page.scss b/lib/assets/sass/includes/_error-page.scss index b1f1724bef..2b77682697 100644 --- a/lib/assets/sass/includes/_error-page.scss +++ b/lib/assets/sass/includes/_error-page.scss @@ -1,11 +1,11 @@ -.govuk-prototype-kit-error-page-header { +.nowprototypeit-error-page-header { .govuk-header__container { border-color: govuk-colour("red"); } } -.govuk-prototype-kit-error-info { +.nowprototypeit-error-info { strong { min-width: 4em; display: inline-block; @@ -13,7 +13,7 @@ } -.govuk-prototype-kit-error-code { +.nowprototypeit-error-code { display: block; border: 1px solid black; padding: 1em; @@ -25,24 +25,24 @@ } } -.govuk-prototype-kit-error-code:focus{ +.nowprototypeit-error-code:focus{ outline: 3px solid govuk-colour("yellow"); outline-offset: 0; box-shadow: inset 0 0 0 2px; } -#govuk-prototype-kit-error-line { +#nowprototypeit-error-line { background: govuk-colour("red"); color: govuk-colour("white"); padding-right: 1em; } -#govuk-prototype-kit-error-stack { +#nowprototypeit-error-stack { overflow: auto; } .js-enabled { - #govuk-prototype-kit-error-stack.js-hidden { + #nowprototypeit-error-stack.js-hidden { display: none; } } diff --git a/lib/assets/sass/manage-prototype.scss b/lib/assets/sass/manage-prototype.scss index 977587e8a3..6dbc1d4385 100644 --- a/lib/assets/sass/manage-prototype.scss +++ b/lib/assets/sass/manage-prototype.scss @@ -4,7 +4,7 @@ // SASS for error page @import ".tmp/sass/error-page"; -.govuk-prototype-kit-manage-prototype-task-list { +.nowprototypeit-manage-prototype-task-list { list-style-type: none; padding-left: 0; margin-top: 0; @@ -14,12 +14,12 @@ } } -.govuk-prototype-kit-manage-prototype-task-list__section { +.nowprototypeit-manage-prototype-task-list__section { display: table; @include govuk-font($size:24, $weight: bold); } -.govuk-prototype-kit-manage-prototype-task-list__section-number { +.nowprototypeit-manage-prototype-task-list__section-number { display: table-cell; @include govuk-media-query($from: tablet) { @@ -28,7 +28,7 @@ } } -.govuk-prototype-kit-manage-prototype-task-list__items { +.nowprototypeit-manage-prototype-task-list__items { @include govuk-font($size: 19); @include govuk-responsive-margin(9, "bottom"); list-style: none; @@ -38,7 +38,7 @@ } } -.govuk-prototype-kit-manage-prototype-task-list__item { +.nowprototypeit-manage-prototype-task-list__item { border-bottom: 1px solid $govuk-border-colour; margin-bottom: 0 !important; padding-top: govuk-spacing(2); @@ -46,19 +46,19 @@ @include govuk-clearfix; } -.govuk-prototype-kit-manage-prototype-task-list__item:first-child { +.nowprototypeit-manage-prototype-task-list__item:first-child { border-top: 1px solid $govuk-border-colour; } -.govuk-prototype-kit-manage-prototype-task-list__task-name { +.nowprototypeit-manage-prototype-task-list__task-name { display: block; @include govuk-media-query($from: 450px) { float: left; } } -.govuk-prototype-kit-manage-prototype-task-list__tag, -.govuk-prototype-kit-manage-prototype-task-list__task-completed { +.nowprototypeit-manage-prototype-task-list__tag, +.nowprototypeit-manage-prototype-task-list__task-completed { margin-top: govuk-spacing(2); margin-bottom: govuk-spacing(1); @@ -69,45 +69,45 @@ } } -.govuk-prototype-kit-manage-prototype-navigation { +.nowprototypeit-manage-prototype-navigation { border-bottom: 1px solid #1d70b8; } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation { + .nowprototypeit-manage-prototype-navigation { display: block } } -.govuk-prototype-kit-manage-prototype-navigation [hidden], -.govuk-prototype-kit-manage-prototype-navigation[hidden] { +.nowprototypeit-manage-prototype-navigation [hidden], +.nowprototypeit-manage-prototype-navigation[hidden] { display: none } @media(max-width: 40.0525em) { - .no-js .govuk-prototype-kit-manage-prototype-navigation { + .no-js .nowprototypeit-manage-prototype-navigation { display: block } } -.govuk-prototype-kit-manage-prototype-navigation__list { +.nowprototypeit-manage-prototype-navigation__list { margin: 0; padding: 0; list-style: none } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list { + .nowprototypeit-manage-prototype-navigation__list { position: relative; } } -.govuk-prototype-kit-manage-prototype-navigation__list-item { +.nowprototypeit-manage-prototype-navigation__list-item { position: relative } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list-item { + .nowprototypeit-manage-prototype-navigation__list-item { font-family: "GDS Transport", arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -125,13 +125,13 @@ } @media print and (min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list-item { + .nowprototypeit-manage-prototype-navigation__list-item { font-family: sans-serif } } @media(min-width: 40.0625em) and (min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list-item { + .nowprototypeit-manage-prototype-navigation__list-item { font-size: 19px; font-size: 1.1875rem; line-height: 2.6315789474 @@ -139,20 +139,20 @@ } @media print and (min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list-item { + .nowprototypeit-manage-prototype-navigation__list-item { font-size: 14pt; line-height: 50px } } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__list-item--current { + .nowprototypeit-manage-prototype-navigation__list-item--current { border-bottom: 4px solid #1d70b8 } } -.govuk-prototype-kit-manage-prototype-navigation__link, -.govuk-prototype-kit-manage-prototype-navigation__button { +.nowprototypeit-manage-prototype-navigation__link, +.nowprototypeit-manage-prototype-navigation__button { margin: 15px 0; padding: 0; font-weight: 700; @@ -160,8 +160,8 @@ font-size: 1.1875rem } -.govuk-prototype-kit-manage-prototype-navigation__link:after, -.govuk-prototype-kit-manage-prototype-navigation__button:after { +.nowprototypeit-manage-prototype-navigation__link:after, +.nowprototypeit-manage-prototype-navigation__button:after { content: ""; position: absolute; top: 0; @@ -170,17 +170,17 @@ left: 0 } -.govuk-prototype-kit-manage-prototype-navigation__link:not([hidden]) { +.nowprototypeit-manage-prototype-navigation__link:not([hidden]) { display: inline-block } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__link:not([hidden]) { + .nowprototypeit-manage-prototype-navigation__link:not([hidden]) { display: inline } } -.govuk-prototype-kit-manage-prototype-navigation__button { +.nowprototypeit-manage-prototype-navigation__button { font-family: "GDS Transport", arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -193,12 +193,12 @@ } @media print { - .govuk-prototype-kit-manage-prototype-navigation__button { + .nowprototypeit-manage-prototype-navigation__button { font-family: sans-serif } } -.govuk-prototype-kit-manage-prototype-navigation__button:hover { +.nowprototypeit-manage-prototype-navigation__button:hover { text-decoration-thickness: max(3px, .1875rem, .12em); -webkit-text-decoration-skip-ink: none; text-decoration-skip-ink: none; @@ -206,7 +206,7 @@ text-decoration-skip: none } -.govuk-prototype-kit-manage-prototype-navigation__button:focus { +.nowprototypeit-manage-prototype-navigation__button:focus { outline: 3px solid rgba(0, 0, 0, 0); color: #0b0c0c; background-color: #fd0; @@ -215,61 +215,61 @@ text-decoration: none } -.govuk-prototype-kit-manage-prototype-navigation__button:not(:hover):not(:active) { +.nowprototypeit-manage-prototype-navigation__button:not(:hover):not(:active) { text-decoration: none } -.govuk-prototype-kit-manage-prototype-navigation__button:link { +.nowprototypeit-manage-prototype-navigation__button:link { color: #1d70b8 } -.govuk-prototype-kit-manage-prototype-navigation__button:visited { +.nowprototypeit-manage-prototype-navigation__button:visited { color: #1d70b8 } -.govuk-prototype-kit-manage-prototype-navigation__button:hover { +.nowprototypeit-manage-prototype-navigation__button:hover { color: #003078 } -.govuk-prototype-kit-manage-prototype-navigation__button:active { +.nowprototypeit-manage-prototype-navigation__button:active { color: #0b0c0c } -.govuk-prototype-kit-manage-prototype-navigation__button:focus { +.nowprototypeit-manage-prototype-navigation__button:focus { color: #0b0c0c } -.govuk-prototype-kit-manage-prototype-navigation__subnav { +.nowprototypeit-manage-prototype-navigation__subnav { margin: 0 -15px; padding: 10px 0; background-color: #fff; list-style: none } -.js-enabled .govuk-prototype-kit-manage-prototype-navigation__subnav--active { +.js-enabled .nowprototypeit-manage-prototype-navigation__subnav--active { display: block } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-navigation__subnav, - .js-enabled .govuk-prototype-kit-manage-prototype-navigation__subnav--active { + .nowprototypeit-manage-prototype-navigation__subnav, + .js-enabled .nowprototypeit-manage-prototype-navigation__subnav--active { display: none } } -.govuk-prototype-kit-manage-prototype-navigation__subnav-item { +.nowprototypeit-manage-prototype-navigation__subnav-item { display: block; position: relative; padding: 15px } -.govuk-prototype-kit-manage-prototype-navigation__subnav-item--current { +.nowprototypeit-manage-prototype-navigation__subnav-item--current { padding-left: 16px; border-left: 4px solid #1d70b8 } -.govuk-prototype-kit-manage-prototype-navigation__theme { +.nowprototypeit-manage-prototype-navigation__theme { font-family: "GDS Transport", arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -283,62 +283,62 @@ } @media print { - .govuk-prototype-kit-manage-prototype-navigation__theme { + .nowprototypeit-manage-prototype-navigation__theme { font-family: sans-serif } } -.govuk-prototype-kit-manage-prototype-navigation__menu-button { +.nowprototypeit-manage-prototype-navigation__menu-button { top: auto; right: 5px; bottom: 25px } -.govuk-prototype-kit-manage-prototype-navigation__menu-button[hidden] { +.nowprototypeit-manage-prototype-navigation__menu-button[hidden] { display: none } -.govuk-prototype-kit-manage-prototype-options { +.nowprototypeit-manage-prototype-options { margin-bottom: 10px; padding: 0 } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-options { + .nowprototypeit-manage-prototype-options { margin-bottom: 15px } } -.govuk-prototype-kit-manage-prototype-options__table { +.nowprototypeit-manage-prototype-options__table { max-width: 38em } @media(min-width: 20em) { - .govuk-prototype-kit-manage-prototype-options__table { + .nowprototypeit-manage-prototype-options__table { table-layout: fixed } - .govuk-prototype-kit-manage-prototype-options__table .govuk-table__header, - .govuk-prototype-kit-manage-prototype-options__table .govuk-table__cell { + .nowprototypeit-manage-prototype-options__table .govuk-table__header, + .nowprototypeit-manage-prototype-options__table .govuk-table__cell { padding-right: 10px; word-break: break-word } } @media(min-width: 20em) { - .govuk-prototype-kit-manage-prototype-options__limit-table-cell { + .nowprototypeit-manage-prototype-options__limit-table-cell { width: 29% } } -.govuk-prototype-kit-manage-prototype-page-navigation { +.nowprototypeit-manage-prototype-page-navigation { margin-top: 0; margin-bottom: 30px; padding-left: 20px; list-style: none } -.govuk-prototype-kit-manage-prototype-page-navigation__item { +.nowprototypeit-manage-prototype-page-navigation__item { font-family: "GDS Transport", arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -350,13 +350,13 @@ } @media print { - .govuk-prototype-kit-manage-prototype-page-navigation__item { + .nowprototypeit-manage-prototype-page-navigation__item { font-family: sans-serif } } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-page-navigation__item { + .nowprototypeit-manage-prototype-page-navigation__item { font-size: 19px; font-size: 1.1875rem; line-height: 1.3157894737 @@ -364,129 +364,138 @@ } @media print { - .govuk-prototype-kit-manage-prototype-page-navigation__item { + .nowprototypeit-manage-prototype-page-navigation__item { font-size: 14pt; line-height: 1.15 } } @media(min-width: 40.0625em) { - .govuk-prototype-kit-manage-prototype-page-navigation__item { + .nowprototypeit-manage-prototype-page-navigation__item { display: none } } -.govuk-prototype-kit-manage-prototype-page-navigation__item:before { +.nowprototypeit-manage-prototype-page-navigation__item:before { content: "—"; margin-left: -20px; padding-right: 5px } -.govuk-prototype-kit-manage-prototype-version{ +.nowprototypeit-manage-prototype-version{ color: #505a5f; } -.govuk-prototype-kit-manage-prototype-template-list-template-list { +.nowprototypeit-manage-prototype-template-list-template-list { border-top: 1px solid #b1b4b6; margin-bottom: 2em; } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item { +.nowprototypeit-manage-prototype-template-list-template-list__item { border-bottom: 1px solid #b1b4b6; padding: .6em 0 } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-name { +.nowprototypeit-manage-prototype-template-list-template-list__item-name { display: inline-block; width: 60% } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-links { +.nowprototypeit-manage-prototype-template-list-template-list__item-links { display: inline-block; } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-link { +.nowprototypeit-manage-prototype-template-list-template-list__item-link { margin-right: 1em; } -.govuk-prototype-kit-manage-prototype-template-list-template-list { +.nowprototypeit-manage-prototype-template-list-template-list { border-top: 1px solid #b1b4b6; margin-bottom: 2em; } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item { +.nowprototypeit-manage-prototype-template-list-template-list__item { border-bottom: 1px solid #b1b4b6; padding: .6em 0 } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-name { +.nowprototypeit-manage-prototype-template-list-template-list__item-name { display: inline-block; width: 60% } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-links { +.nowprototypeit-manage-prototype-template-list-template-list__item-links { display: inline-block; } -.govuk-prototype-kit-manage-prototype-template-list-template-list__item-link { +.nowprototypeit-manage-prototype-template-list-template-list__item-link { margin-right: 1em; } -body .govuk-prototype-kit-manage-prototype-govuk-tag { +body .nowprototypeit-manage-prototype-govuk-tag { text-transform: none; } -.govuk-prototype-kit-manage-prototype-task-list__items { +.nowprototypeit-manage-prototype-task-list__items { padding-left: 0 !important; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list { +.nowprototypeit-manage-prototype-plugin-heading { + margin-bottom: 0; +} + +.nowprototypeit-manage-prototype-plugin-sub-heading { + color: #505a5f; + margin-bottom: 15px; +} + +.nowprototypeit-manage-prototype-plugin-list-plugin-list { border-top: 1px solid #b1b4b6; margin-bottom: 2em; display: block; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item { border-bottom: 1px solid #b1b4b6; padding: .6em 0; display: flex; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-name { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-name { display: inline-block; width: 60% } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-link { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-link { margin-right: 1em; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list { +.nowprototypeit-manage-prototype-plugin-list-plugin-list { border-top: 1px solid #b1b4b6; margin-bottom: 2em; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item { border-bottom: 1px solid #b1b4b6; padding: .6em 0 } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-name { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-name { display: inline-block; width: 35%; vertical-align: middle; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-buttons, -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-version{ +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-buttons, +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-version{ display: inline-block; vertical-align: middle; } -.govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-version { +.nowprototypeit-manage-prototype-plugin-list-plugin-list__item-version { width: 15% } -.govuk-prototype-kit-manage-prototype-plugin-badge{ +.nowprototypeit-manage-prototype-plugin-badge{ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background: #1d70b8; @@ -504,26 +513,26 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { margin-left: 10px; } -.govuk-prototype-kit-manage-prototype-plugin-updates-message { +.nowprototypeit-manage-prototype-plugin-updates-message { background: #f3f2f1; color: #505a5f; } -.govuk-prototype-kit-manage-prototype-plugin-subnav--current { +.nowprototypeit-manage-prototype-plugin-subnav--current { margin-left: -14px; padding-left: 10px; border-left: 4px solid #1d70b8; background-color: #fff; } -.govuk-prototype-kit-manage-prototype-plugin-links-section{ +.nowprototypeit-manage-prototype-plugin-links-section{ border-top: black 1px solid; padding-top: 30px; margin-bottom: 30px; } @media(min-width: 40.0525em) { - .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-buttons { + .nowprototypeit-manage-prototype-plugin-list-plugin-list__item-buttons { .govuk-button, .govuk-link--no-visited-state { margin: 0 15px 0 0; } @@ -531,15 +540,15 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { } @media(max-width: 40.0525em) { - .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-name, - .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-buttons, - .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-version { + .nowprototypeit-manage-prototype-plugin-list-plugin-list__item-name, + .nowprototypeit-manage-prototype-plugin-list-plugin-list__item-buttons, + .nowprototypeit-manage-prototype-plugin-list-plugin-list__item-version { display: block; width: auto; } } -.govuk-prototype-kit-manage-prototype-plugin-instructions { +.nowprototypeit-manage-prototype-plugin-instructions { &.js-hidden { display: initial; } @@ -549,7 +558,7 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { } .js-enabled { - .govuk-prototype-kit-manage-prototype-plugin-instructions { + .nowprototypeit-manage-prototype-plugin-instructions { &.js-hidden { display: none; } @@ -559,7 +568,7 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { } } -.govuk-prototype-kit-manage-prototype-plugin-processing{ +.nowprototypeit-manage-prototype-plugin-processing{ .panel-processing{ background: #fff; color: #0b0c0c; @@ -574,3 +583,20 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { } } } + +.nowprototypeit-manage-prototype-plugin-details-links { + padding-top: 20px; + margin-bottom: 20px; + border-top: 2px solid #b1b4b6; + border-bottom: 2px solid #b1b4b6; +} + +.nowprototypeit-brand-header svg { + max-height: 1em; +} + +.nowprototypeit-page, +.nowprototypeit-page a, +.nowprototypeit-page p { + font-family: sans-serif; +} diff --git a/lib/authentication.js b/lib/authentication.js index 6a40b24a4e..46ca48e206 100644 --- a/lib/authentication.js +++ b/lib/authentication.js @@ -9,7 +9,7 @@ const { encryptPassword } = require('./utils') const allowedPathsWhenUnauthenticated = [ '/manage-prototype/password', '/public/stylesheets/unbranded.css', - '/plugin-assets/govuk-prototype-kit/lib/assets/images/unbranded.ico', + '/plugin-assets/%40nowprototypeit%2Fgovuk/lib/assets/images/unbranded.ico', // Keep /extension-assets path for backwards compatibility // TODO: remove in v14 '/extension-assets/govuk-prototype-kit/lib/assets/images/unbranded.ico'] @@ -32,7 +32,7 @@ function authentication () { return (req, res, next) => { if (allowedPathsWhenUnauthenticated.includes(req.path) || req.path.startsWith('/manage-prototype/dependencies') || - req.path.startsWith('/plugin-assets/govuk-prototype-kit') || + req.path.startsWith('/plugin-assets/%40nowprototypeit%2Fgovuk') || req.path === '/public/stylesheets/manage-prototype.css' ) { next() diff --git a/lib/build.js b/lib/build.js index 12615ec485..600aa51f2a 100644 --- a/lib/build.js +++ b/lib/build.js @@ -138,7 +138,7 @@ function sassVariables (contextPath = '', isLegacyGovukFrontend = false) { // TODO: remove in v14 fileContents += `$govuk-extensions-url-context: "${contextPath}";\n` fileContents += `$govuk-plugins-url-context: "${contextPath}";\n` - fileContents += '$govuk-prototype-kit-major-version: 13;\n' + fileContents += '$nowprototypeit-major-version: 13;\n' fileContents += '$govuk-suppressed-warnings: (legacy-colour-param);\n' // Patch missing 'init.scss' before GOV.UK Frontend v4.4.0 diff --git a/lib/config.js b/lib/config.js index f06da2bd4e..2ff7a36e66 100644 --- a/lib/config.js +++ b/lib/config.js @@ -96,11 +96,15 @@ function getConfig (config, swallowError = true) { overrideOrDefault('logPerformanceSummary', 'LOG_PERFORMANCE_SUMMARY', asNumber, undefined) overrideOrDefault('verbose', 'VERBOSE', asBoolean, false) overrideOrDefault('showPrereleases', 'SHOW_PRERELEASES', asBoolean, false) - overrideOrDefault('allowGovukFrontendUninstall', 'ALLOW_GOVUK_FRONTEND_UNINSTALL', asBoolean, false) + overrideOrDefault('allowGovukFrontendUninstall', 'ALLOW_GOVUK_FRONTEND_UNINSTALL', asBoolean, true) overrideOrDefault('passwordKeys', 'PASSWORD_KEYS', asString, '') + overrideOrDefault('showPluginLookup', 'SHOW_PLUGIN_LOOKUP', asBoolean, false) + overrideOrDefault('showPluginDowngradeButtons', 'SHOW_PLUGIN_DOWNGRADE_BUTTONS', asBoolean, false) + overrideOrDefault('showPluginDebugInfo', 'SHOW_PLUGIN_DEBUG_INFO', asBoolean, false) + overrideOrDefault('turnOffFunctionCaching', 'TURN_OFF_FUNCTION_CACHING', asBoolean, false) if (config.serviceName === undefined) { - config.serviceName = 'GOV.UK Prototype Kit' + config.serviceName = 'Now Prototype It - GOV.UK Prototype Kit' } config.passwords = (config.passwordKeys.split(',')) diff --git a/lib/config.test.js b/lib/config.test.js index d9fd591ac9..3b6070a832 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -21,7 +21,7 @@ describe('config', () => { 'govuk-frontend' ], port: 3000, - serviceName: 'GOV.UK Prototype Kit', + serviceName: 'Now Prototype It - GOV.UK Prototype Kit', useAuth: true, useHttps: true, useAutoStoreData: true, @@ -35,8 +35,12 @@ describe('config', () => { useNjkExtensions: false, logPerformance: false, showPrereleases: false, - allowGovukFrontendUninstall: false, - verbose: false + allowGovukFrontendUninstall: true, + verbose: false, + showPluginDebugInfo: false, + showPluginDowngradeButtons: false, + showPluginLookup: false, + turnOffFunctionCaching: false }) const mergeWithDefaults = (config) => Object.assign({}, defaultConfig, config) diff --git a/lib/dev-server.js b/lib/dev-server.js index a5ae807afa..95554c1f90 100644 --- a/lib/dev-server.js +++ b/lib/dev-server.js @@ -1,5 +1,7 @@ // node dependencies const path = require('path') +const fs = require('fs') +const fsp = fs.promises // npm dependencies const chokidar = require('chokidar') @@ -16,8 +18,9 @@ const { setNodemonInstance } = require('./build') const plugins = require('./plugins/plugins') -const { collectDataUsage } = require('./usage-data') const utils = require('./utils') +const { logPerformanceSummaryOnce, startPerformanceTimer, endPerformanceTimer } = require('./utils/performance') +const { exec } = require('./exec') const { packageDir, projectDir, @@ -25,14 +28,47 @@ const { appDir, appSassDir, - publicCssDir + publicCssDir, + + commandsDir } = require('./utils/paths') -const fs = require('fs') -const { logPerformanceSummaryOnce, startPerformanceTimer, endPerformanceTimer } = require('./utils/performance') + +const pollRateForCommandsDir = 1000 + +async function pollCommandsDir () { + await fse.ensureDir(commandsDir) + const commandFilesToRun = (await fsp.readdir(commandsDir)).sort() + while (commandFilesToRun.length > 0) { + const commandFilePath = path.join(commandsDir, commandFilesToRun.shift()) + let commandFileContents + try { + commandFileContents = await fse.readJson(commandFilePath) + } catch (err) { + await fse.writeJson(commandFilePath, { status: 'failed', error: 'File was malformed' }) + continue + } + if (commandFileContents.status !== 'pending') { + continue + } + try { + console.log('running command', commandFileContents.command) + await fse.writeJson(commandFilePath, { ...commandFileContents, status: 'processing', updated: new Date() }) + await exec(commandFileContents.command, { cwd: projectDir }) + await fse.writeJson(commandFilePath, { ...commandFileContents, status: 'completed', updated: new Date() }) + if (commandFileContents.restartOnCompletion) { + nodemonInstance.emit('restart') + } + } catch (err) { + console.error('Error running command') + console.error(err) + await fse.writeJson(commandFilePath, { ...commandFileContents, status: 'error', error: 'Command failed', errorMessage: err.message, errorStack: err.stack, updated: new Date() }) + } + } + setTimeout(pollCommandsDir, pollRateForCommandsDir) +} // Build watch and serve async function runDevServer () { - await collectDataUsage() let startupError try { @@ -49,7 +85,7 @@ async function runDevServer () { watchAfterStarting() if (startupError && nodemonInstance) { - nodemonInstance.emit('restart') + nodemonInstance.emit('nowprototypeit/layouts') } } @@ -96,7 +132,7 @@ function runNodemon (port) { } // Warn about npm install on crash const onCrash = () => { - console.log(colour.cyan('[nodemon] For missing modules try running `npm install`')) + console.log(colour.cyan('The kit crashed, feel free to try restarting it.')) } const onQuit = () => { @@ -158,6 +194,8 @@ function runNodemon (port) { setNodemonInstance(nodemonInstance) + pollCommandsDir() + endPerformanceTimer('runDevServer', timer) logPerformanceSummaryOnce() diff --git a/lib/errorServer.js b/lib/errorServer.js index c5e48e3c83..041dbde740 100644 --- a/lib/errorServer.js +++ b/lib/errorServer.js @@ -116,8 +116,6 @@ function runErrorServer (error) { } catch (ignoreThisError) { console.log(JSON.stringify({ ignoreThisError }, null, 2)) fileContentsParts.push('

There is an error

') - fileContentsParts.push('

') - fileContentsParts.push('You can try and fix this yourself or contact the GOV.UK Prototype Kit team if you need help.') fileContentsParts.push('

') fileContentsParts.push('
')
       fileContentsParts.push(error.stack)
diff --git a/lib/final-backup-nunjucks/layouts/main.html b/lib/final-backup-nunjucks/layouts/main.html
index 2ceb4be401..4951bd1d0f 100644
--- a/lib/final-backup-nunjucks/layouts/main.html
+++ b/lib/final-backup-nunjucks/layouts/main.html
@@ -1,6 +1,6 @@
 {# this file exists as a backup in case the user has deleted their `app/views/layouts/main.html` template #}
 
-{% extends "govuk-prototype-kit/layouts/govuk-branded.njk" %}
+{% extends "nowprototypeit/layouts/govuk-branded.njk" %}
 
 {% block meta %}
   
diff --git a/lib/manage-prototype-handlers.js b/lib/manage-prototype-handlers.js
index bfd51f90fd..516c948b15 100644
--- a/lib/manage-prototype-handlers.js
+++ b/lib/manage-prototype-handlers.js
@@ -9,20 +9,12 @@ const { doubleCsrf } = require('csrf-csrf')
 // local dependencies
 const config = require('./config')
 const plugins = require('./plugins/plugins')
-const { exec } = require('./exec')
 const { govukFrontendPaths } = require('./govukFrontendPaths')
 const { prototypeAppScripts } = require('./utils')
-const { projectDir, packageDir, appViewsDir } = require('./utils/paths')
+const { projectDir, packageDir, appViewsDir, commandsDir } = require('./utils/paths')
 const nunjucksConfiguration = require('./nunjucks/nunjucksConfiguration')
 const syncChanges = require('./sync-changes')
-const {
-  lookupPackageInfo,
-  getInstalledPackages,
-  getAllPackages,
-  getDependentPackages,
-  getDependencyPackages,
-  waitForPackagesCache
-} = require('./plugins/packages')
+const { plugins: knownPlugins } = require('../known-plugins.json')
 
 const contextPath = '/manage-prototype'
 
@@ -39,22 +31,22 @@ const nunjucksManagementEnv = nunjucksConfiguration.getNunjucksAppEnv(
   govukFrontendPaths([packageDir, projectDir])
 )
 
-let kitRestarted = false
-
 const {
-  name: currentKitName,
   version: currentKitVersion
 } = require(path.join(packageDir, 'package.json'))
-
-async function isValidVersion (packageName, version) {
-  const { versions = [], localVersion } = await lookupPackageInfo(packageName, version)
-  const validVersions = [...versions, localVersion].filter(version => version)
-  const isVersionValid = validVersions.includes(version)
-  if (!isVersionValid) {
-    console.log('version', version, ' is not valid, valid options are:\n\n', validVersions)
-  }
-  return isVersionValid
-}
+const pluginDetails = require('./utils/packageDetails')
+const {
+  getPluginDetailsFromFileSystem,
+  getPluginDetailsFromGithub,
+  getPluginDetailsFromNpm,
+  getLatestPluginDetailsFromNpm,
+  getPluginDetailsFromRef,
+  getInstalledPackages,
+  getKnownPlugins,
+  getInstalledPluginDetails,
+  isInstalled
+} = pluginDetails
+const { getConfig } = require('./config')
 
 function getManagementView (filename) {
   return ['views', 'manage-prototype', filename].join('/')
@@ -137,15 +129,6 @@ function developmentOnlyMiddleware (req, res, next) {
   }
 }
 
-// Middleware to ensure pages load when plugin cache has been initially loaded
-async function pluginCacheMiddleware (req, res, next) {
-  await Promise.race([
-    waitForPackagesCache(),
-    new Promise((resolve) => setTimeout(resolve, 1000))
-  ])
-  next()
-}
-
 const managementLinks = [
   {
     text: 'Home',
@@ -178,18 +161,19 @@ async function getHomeHandler (req, res) {
   const originalHomepage = await fse.readFile(path.join(packageDir, 'prototype-starter', 'app', 'views', 'index.html'), 'utf8')
   const currentHomepage = await readFileIfExists(path.join(appViewsDir, 'index.html'))
 
-  const kitPackage = await lookupPackageInfo('govuk-prototype-kit')
+  const kitPackage = await pluginDetails.getLatestPluginDetailsFromNpm('@nowprototypeit/govuk')
 
   const viewData = {
     ...req.app.locals,
     currentUrl: req.originalUrl,
     currentSection: pageName,
     links: managementLinks,
-    kitUpdateAvailable: kitPackage.latestVersion !== currentKitVersion,
-    latestAvailableKit: kitPackage.latestVersion,
+    kitUpdateAvailable: kitPackage?.latestVersion && kitPackage?.version !== currentKitVersion,
+    latestAvailableKit: kitPackage?.version,
+    latestKitUrl: kitPackage?.links?.pluginDetails,
     tasks: [
       {
-        done: serviceName !== 'Service name goes here' && serviceName !== 'GOV.UK Prototype Kit',
+        done: serviceName !== 'Service name goes here' && serviceName !== 'Now Prototype It - GOV.UK Prototype Kit',
         html: 'Change the service name in the file app/config.json'
       }, {
         done: currentHomepage !== undefined && originalHomepage !== currentHomepage,
@@ -243,11 +227,12 @@ async function getTemplatesHandler (req, res) {
   const commonTemplatesPackageName = '@govuk-prototype-kit/common-templates'
   const govukFrontendPackageName = 'govuk-frontend'
   let commonTemplatesDetails
-  const installedPlugins = (await getInstalledPackages()).map((pkg) => pkg.packageName)
+  const installedPlugins = (await pluginDetails.getInstalledPackages()).map((pkg) => pkg.packageName)
   if (installedPlugins.includes(govukFrontendPackageName) && !installedPlugins.includes(commonTemplatesPackageName)) {
+    const plugin = await getLatestPluginDetailsFromNpm(commonTemplatesPackageName)
     commonTemplatesDetails = {
-      pluginDisplayName: plugins.preparePackageNameForDisplay(commonTemplatesPackageName),
-      installLink: `${contextPath}/plugins/install?package=${encodeURIComponent(commonTemplatesPackageName)}&returnTo=templates`
+      pluginDisplayName: plugin?.name,
+      installLink: plugin?.links?.install
     }
   }
 
@@ -404,50 +389,47 @@ function getTemplatesPostInstallHandler (req, res) {
   }))
 }
 
-function buildPluginData (pluginData) {
-  if (pluginData === undefined) {
-    return
-  }
-  const {
-    packageName,
-    installed,
-    installedLocally,
-    updateAvailable,
-    latestVersion,
-    installedVersion,
-    required,
-    localVersion,
-    pluginConfig = {}
-  } = pluginData
-  const preparedPackageNameForDisplay = plugins.preparePackageNameForDisplay(packageName)
+async function buildPluginData (plugin) {
+  const latestVersion = (await getLatestPluginDetailsFromNpm(plugin.packageName))?.version
+  const installedPlugin = await getInstalledPluginDetails(plugin.packageName)
+  const installedVersion = installedPlugin?.version
+
   return {
-    ...preparedPackageNameForDisplay,
-    ...pluginConfig.meta,
-    packageName,
-    latestVersion,
-    installedLocally,
-    installLink: `${contextPath}/plugins/install?package=${encodeURIComponent(packageName)}`,
-    installCommand: `npm install ${packageName}`,
-    updateLink: updateAvailable ? `${contextPath}/plugins/update?package=${encodeURIComponent(packageName)}` : undefined,
-    updateCommand: latestVersion && `npm install ${packageName}@${latestVersion}`,
-    uninstallLink: installed && !required ? `${contextPath}/plugins/uninstall?package=${encodeURIComponent(packageName)}${installedLocally ? `&version=${encodeURIComponent(localVersion)}` : ''}` : undefined,
-    uninstallCommand: `npm uninstall ${packageName}`,
-    installedVersion
+    ...plugin,
+    installedVersion,
+    isInstalled: !!installedVersion,
+    updateAvailable: latestVersion && installedVersion && installedVersion !== latestVersion,
+    description: plugin.pluginConfig?.meta?.description,
+    pluginDetailsLink: installedPlugin?.links?.pluginDetails || plugin.links.pluginDetails
+  }
+}
+
+function getTimeSummary (date) {
+  const epochDate = date.getTime()
+  const epochNow = new Date().getTime()
+  const timeDifferenceInDays = (epochNow - epochDate) / 1000 / 60 / 60 / 24
+  if (timeDifferenceInDays < 1) {
+    return 'today'
+  }
+  if (timeDifferenceInDays < 2) {
+    return 'yesterday'
+  }
+  if (timeDifferenceInDays < 14) {
+    return Math.floor(timeDifferenceInDays) + ' days ago'
   }
+  return Math.floor(timeDifferenceInDays / 7) + ' weeks ago'
 }
 
 async function prepareForPluginPage (isInstalledPage, search) {
-  const allPackages = await getAllPackages()
-  const allPlugins = allPackages.filter(({ pluginConfig }) => !!pluginConfig)
-  const installedPackages = await getInstalledPackages()
-  const installedPlugins = installedPackages.filter(({ pluginConfig }) => !!pluginConfig)
+  const allPlugins = await getKnownPlugins()
+  const installedPlugins = await getInstalledPackages()
 
   const plugins = isInstalledPage
     ? installedPlugins
     : allPlugins.filter(plugin => {
-      const { packageName, available, installed } = plugin || {}
+      const { packageName } = plugin || {}
       const pluginName = packageName?.toLowerCase()
-      if (!pluginName || (!available && !installed)) {
+      if (!pluginName) {
         return false
       }
       return pluginName.indexOf(search.toLowerCase()) >= 0
@@ -455,50 +437,12 @@ async function prepareForPluginPage (isInstalledPage, search) {
 
   return {
     status: isInstalledPage ? 'installed' : 'search',
-    plugins: plugins.map(buildPluginData),
+    plugins: await Promise.all(plugins.map(buildPluginData)),
     found: plugins.length,
     updates: installedPlugins.filter(plugin => plugin.updateAvailable).length
   }
 }
 
-function getCommand (mode, chosenPlugin) {
-  let {
-    updateCommand,
-    installCommand,
-    uninstallCommand,
-    version,
-    dependencyPlugins,
-    dependentPlugins
-  } = chosenPlugin
-  const dependents = dependentPlugins?.map(({ packageName }) => packageName).join(' ')
-  const dependencies = dependencyPlugins?.map(({
-    packageName,
-    latestVersion
-  }) => packageName + '@' + latestVersion).join(' ')
-
-  if (version && installCommand) {
-    installCommand += `@${version}`
-  }
-
-  if (dependents) {
-    uninstallCommand += ' ' + dependents
-  }
-
-  if (dependencies) {
-    installCommand += ' ' + dependencies
-    updateCommand += ' ' + dependencies
-  }
-
-  switch (mode) {
-    case 'update':
-      return updateCommand + ' --save-exact'
-    case 'install':
-      return installCommand + ' --save-exact'
-    case 'uninstall':
-      return uninstallCommand
-  }
-}
-
 const verbs = {
   update: {
     title: 'Update',
@@ -525,7 +469,17 @@ const verbs = {
 
 async function getPluginsHandler (req, res) {
   const isInstalledPage = req.route.path.endsWith('installed')
-  const { search = '' } = req.query || {}
+  const {
+    search = '',
+    error,
+    fsPath,
+    githubOrg,
+    githubProject,
+    githubBranch,
+    npmPackage,
+    npmVersion,
+    source
+  } = req.query || {}
   const pageName = 'Plugins'
   const { plugins, status, updates = 0, found = 0 } = await prepareForPluginPage(isInstalledPage, search)
   const foundMessage = found === 1 ? found + ' Plugin found' : found + ' Plugins found'
@@ -535,14 +489,26 @@ async function getPluginsHandler (req, res) {
     currentSection: pageName,
     links: managementLinks,
     isInstalledPage,
+    showPluginLookup: getConfig().showPluginLookup,
     isSearchPage: !isInstalledPage,
     search,
     plugins,
     updatesMessage,
     foundMessage,
-    status
+    status,
+    playback: {
+      error,
+      fsPath,
+      githubOrg,
+      githubProject,
+      githubBranch,
+      npmPackage,
+      npmVersion,
+      source
+    }
   }
-  res.send(nunjucksManagementEnv.render(getManagementView('plugins.njk'), model))
+
+  res.render(getManagementView('plugins.njk'), model)
 }
 
 async function postPluginsHandler (req, res) {
@@ -550,91 +516,250 @@ async function postPluginsHandler (req, res) {
   res.redirect(contextPath + req.route.path + query)
 }
 
-async function getPluginForRequest (req) {
-  const packageName = req.query.package || req.body.package
-  const version = req.query.version || req.body.version
-  const mode = getModeFromRequest(req)
-  let chosenPlugin
+async function postPluginDetailsHandler (req, res) {
+  let found
+  const {
+    fsPath,
+    githubOrg,
+    githubProject,
+    githubBranch,
+    npmPackage,
+    npmVersion,
+    source,
+    notFoundErrorUrl
+  } = req.body
+
+  if (source === 'fs') {
+    found = await getPluginDetailsFromFileSystem(fsPath)
+  } else if (source === 'github') {
+    found = await getPluginDetailsFromGithub(githubOrg, githubProject, githubBranch)
+  } else if (source === 'npm' && npmVersion) {
+    found = await getPluginDetailsFromNpm(npmPackage, npmVersion)
+  } else if (source === 'npm') {
+    found = await getLatestPluginDetailsFromNpm(npmPackage)
+  }
+
+  if (found && found.exists && found.pluginConfig) {
+    res.redirect(found.links.pluginDetails)
+  } else {
+    const [url, query] = notFoundErrorUrl.split('?')
+    const queryParts = [query].concat([
+      'fsPath',
+      'githubOrg',
+      'githubProject',
+      'githubBranch',
+      'npmPackage',
+      'npmVersion',
+      'source'
+    ].map(x => {
+      return x && `${encodeURIComponent(x)}=${encodeURIComponent(req.body[x])}`
+    })).filter(x => x)
+    res.redirect([url, queryParts.join('&')].join('?'))
+  }
+}
+
+async function getPluginDetailsHandler (req, res, next) {
+  const config = getConfig()
+  const plugin = await getPluginDetailsFromRef(req.params.packageRef).catch(e => undefined)
+
+  if (!plugin?.pluginConfig) {
+    console.warn('No page found for plugin ref', req.params.packageRef)
+    const err = new Error('Plugin not found')
+    err.status = 404
+    next(err)
+    return
+  }
+
+  if (req.originalUrl !== plugin.links.pluginDetails) {
+    const redirectUrl = plugin.links.pluginDetails
+    console.log('redirecting from:', req.originalUrl)
+    console.log('redirecting to:', redirectUrl)
+    res.redirect(redirectUrl)
+    return
+  }
+
+  const latestVersionPromise = plugin.origin === 'NPM' ? getLatestPluginDetailsFromNpm(plugin.packageName) : Promise.resolve(undefined)
+  const installedVersionPromise = getInstalledPluginDetails(plugin.packageName)
 
-  if (packageName) {
-    chosenPlugin = buildPluginData(await lookupPackageInfo(packageName, version))
-    if (!chosenPlugin) {
-      return // chosen plugin will be invalid
+  function replaceUrlVars (url) {
+    return url && url
+      .replace('{{version}}', plugin.version || plugin.latestVersion)
+      .replace('{{kitVersion}}', currentKitVersion)
+  }
+
+  function getInThisPluginDetails () {
+    const list = []
+    if (plugin.pluginConfig.nunjucksMacros && plugin.pluginConfig.nunjucksMacros.length > 0) {
+      list.push({
+        title: 'Components',
+        items: plugin.pluginConfig.nunjucksMacros.map(x => x.macroName)
+      })
     }
-    if (version) {
-      if (await isValidVersion(packageName, version)) {
-        chosenPlugin.version = version
-      } else if (chosenPlugin.installedLocally) {
-        chosenPlugin.version = chosenPlugin.installedVersion
-      } else {
-        return // chosen plugin will be invalid
-      }
+    if (plugin.pluginConfig.templates && plugin.pluginConfig.templates.length > 0) {
+      list.push({
+        title: 'Templates',
+        items: plugin.pluginConfig.templates.map(x => x.name)
+      })
     }
+    return list
   }
 
-  const dependentPlugins = (await getDependentPackages(chosenPlugin.packageName, version, mode))
-    .filter(({ installed }) => installed || mode !== 'uninstall')
-    .map(buildPluginData)
+  const model = {
+    currentSection: 'Plugins',
+    links: managementLinks,
+    plugin,
+    pluginDescription: plugin?.pluginConfig?.meta?.description,
+    version: plugin.version,
+    releaseTimeSummary: plugin.releaseDateTime && getTimeSummary(new Date(plugin.releaseDateTime)),
+    inThisPlugin: getInThisPluginDetails(),
+    preparedPluginLinks: {
+      documentation: replaceUrlVars(plugin?.pluginConfig?.meta?.urls?.documentation),
+      versionHistory: replaceUrlVars(plugin?.pluginConfig?.meta?.urls?.versionHistory),
+      releaseNotes: replaceUrlVars(plugin?.pluginConfig?.meta?.urls?.releaseNotes)
+    }
+  }
+
+  const epochDateBookmark = '#' + new Date().getTime()
+  const latestVersion = await latestVersionPromise
+  const installedVersion = await installedVersionPromise
 
-  if (dependentPlugins.length) {
-    chosenPlugin.dependentPlugins = dependentPlugins
+  if (latestVersion?.version && latestVersion.version !== plugin.version) {
+    model.newerLink = latestVersion.links.pluginDetails
+    model.newerVersion = latestVersion.version
+  }
+  if (installedVersion?.version && installedVersion.version !== plugin.version) {
+    model.installedLinkAsDifferentLink = installedVersion.links.pluginDetails
+    model.installedLinkAsDifferentVersion = installedVersion.version
+  }
+  if (installedVersion?.version && latestVersion?.version !== installedVersion?.version) {
+    model.updateLink = latestVersion?.links?.update
+  }
+  if (await isInstalled(plugin.internalRef)) {
+    if (!getRequiredPlugins().includes(plugin.packageName)) {
+      model.uninstallLink = plugin.links.uninstall
+    }
+  } else {
+    model.installLink = plugin.links.install
+  }
+  if (config.showPluginDowngradeButtons && installedVersion?.version !== plugin.version) {
+    model.installLink = plugin.links.install
+    model.installLinkText = 'Install this version'
   }
 
-  const dependencyPlugins = (await getDependencyPackages(chosenPlugin.packageName, version, mode)).map(buildPluginData)
+  ;['updateLink', 'uninstallLink', 'installLink'].forEach(key => {
+    model[key] = model[key] && model[key] + epochDateBookmark
+  })
 
-  if (dependencyPlugins.length) {
-    chosenPlugin.dependencyPlugins = dependencyPlugins
+  if (config.showPluginDebugInfo) {
+    model.debugInfo = [
+      '',
+      'versions:',
+      '',
+      `viewing: ${plugin?.version}`,
+      `latest: ${latestVersion?.version}`,
+      `installed: ${installedVersion?.version}`,
+      '',
+      'origin:',
+      '',
+      `viewing: ${plugin?.origin}`,
+      `latest: ${latestVersion?.origin}`,
+      `installed: ${installedVersion?.origin}`
+    ].join('\n')
   }
 
-  return chosenPlugin
+  res.set('Cache-control', 'no-cache, no-store')
+
+  res.render(getManagementView('pluginDetails.njk'), model)
+}
+
+async function getRelatedPluginsForUninstall (chosenPlugin) {
+  const installed = await getInstalledPackages()
+  return installed.filter(x => {
+    return (x.pluginConfig?.pluginDependencies || []).some(y => (y.packageName || y) === chosenPlugin.packageName)
+  })
 }
 
-function modeIsComplete (mode, { installedVersion, latestVersion, version, installedLocally }) {
-  switch (mode) {
-    case 'update':
-      return installedVersion === latestVersion
-    case 'install':
-      return installedLocally || (version ? installedVersion === version : !!installedVersion)
-    case 'uninstall':
-      return !installedVersion
+async function getRelatedPluginsForInstallOrUpdate (chosenPlugin) {
+  const output = {}
+  const installed = (await getInstalledPackages()).map(x => x.packageName)
+
+  async function addDependenciesToOutputRecursive (plugin) {
+    const deps = plugin.pluginConfig?.pluginDependencies || []
+    const depsAsObjects = (await Promise.all(deps.map(dep => getLatestPluginDetailsFromNpm(dep.packageName || dep))))
+      .filter(depObj => {
+        return !Object.keys(output).includes(depObj.internalRef) && !installed.includes(depObj.packageName)
+      })
+
+    depsAsObjects.forEach(x => {
+      output[x.internalRef] = x
+    })
+
+    await Promise.all(depsAsObjects.map(depObj => addDependenciesToOutputRecursive(depObj)))
+
+    return output
   }
+
+  await addDependenciesToOutputRecursive(chosenPlugin)
+  return Object.values(output)
 }
 
-async function getPluginsModeHandler (req, res) {
+async function getPluginsModeHandler (req, res, next) {
   const isSameOrigin = req.headers['sec-fetch-site'] === 'same-origin'
-  const mode = getModeFromRequest(req)
-  const { version } = req.query
+  const { packageRef, mode } = req.params
   const verb = verbs[mode]
 
-  if (!verb) {
-    res.status(404).send(`Page not found: ${req.path}`)
-    return
-  }
+  const plugin = await getPluginDetailsFromRef(packageRef)
+
+  const err = getErrorIfModeNotAllowedForPlugin(mode, plugin)
 
-  const chosenPlugin = await getPluginForRequest(req) || plugins.preparePackageNameForDisplay(req.query.package, version)
+  if (err) {
+    return next(err)
+  }
 
-  const pageName = `${verb.title} ${chosenPlugin.name}`
+  const command = plugin?.commands && plugin?.commands[mode]
 
-  const templatesReturnLink = {
-    href: '/manage-prototype/templates',
-    text: 'Back to templates'
+  if (!plugin) {
+    const err = new Error('Plugin not found.')
+    err.status = 404
+    return next(err)
   }
-  const pluginsReturnLink = {
-    href: '/manage-prototype/plugins',
-    text: 'Back to plugins'
+
+  if (!command) {
+    const err = new Error(`Command not found for mode "${mode}", options are ${Object.keys(plugin?.commands || {}).join(', ')}`)
+    err.status = 404
+    return next(err)
   }
 
-  const returnLink = req.query.returnTo === 'templates' ? templatesReturnLink : pluginsReturnLink
+  const pageName = `${verb.title} ${plugin.name}`
 
-  const fullPluginName = `${chosenPlugin.name}${chosenPlugin.version ? ` version ${chosenPlugin.version} ` : ''}${chosenPlugin.scope ? ` from ${chosenPlugin.scope}` : ''}`
+  let returnLink
+  let cancelLink = plugin?.links.pluginDetails
+
+  if (req.query.returnTo === 'templates') {
+    returnLink = {
+      href: `${contextPath}/templates`,
+      text: 'Back to templates'
+    }
+    cancelLink = returnLink.href
+  } else if (mode === 'uninstall') {
+    returnLink = {
+      href: `${contextPath}/plugins`,
+      text: 'Back to plugins'
+    }
+  } else {
+    returnLink = {
+      href: `${contextPath}/plugin/installed:${encodeURIComponent(plugin.packageName)}`,
+      text: 'Back to plugin details'
+    }
+  }
 
-  const pluginHeading = `${verb.title} ${fullPluginName}`
   let dependencyHeading = ''
 
-  if (chosenPlugin?.dependentPlugins?.length) {
-    dependencyHeading = `Other plugins need ${fullPluginName}`
-  } else if (chosenPlugin?.dependencyPlugins?.length) {
-    dependencyHeading = `${fullPluginName} needs other plugins`
+  const relatedPlugins = mode === 'uninstall' ? await getRelatedPluginsForUninstall(plugin) : await getRelatedPluginsForInstallOrUpdate(plugin)
+
+  if (relatedPlugins.length > 0) {
+    const plural = relatedPlugins.length > 1
+    dependencyHeading = `To ${mode} this plugin, you also need to ${mode === 'update' ? 'install' : mode} ${(plural ? 'other plugins' : 'another plugin')}`
   }
 
   res.send(nunjucksManagementEnv.render(getManagementView('plugin-install-or-uninstall.njk'), {
@@ -643,115 +768,90 @@ async function getPluginsModeHandler (req, res) {
     pageName,
     currentUrl: req.originalUrl,
     links: managementLinks,
-    chosenPlugin,
-    command: getCommand(mode, chosenPlugin),
-    pluginHeading,
+    plugin,
+    command,
     dependencyHeading,
     verb,
     isSameOrigin,
-    returnLink
+    returnLink,
+    cancelLink,
+    relatedPlugins
   }))
 }
 
-function setKitRestarted (state) {
-  kitRestarted = state
-}
+async function queueCommand (command) {
+  await fse.ensureDir(commandsDir)
 
-function getModeFromRequest (req) {
-  const { mode } = req.params
-  if (mode === 'upgrade') {
-    return 'update'
-  }
-  return mode
+  const commandId = new Date().getTime()
+  const filePath = path.join(commandsDir, commandId + '.json')
+
+  await fse.writeJson(filePath, { command, restartOnCompletion: true, status: 'pending' })
+  return commandId
 }
 
-async function postPluginsStatusHandler (req, res) {
-  const mode = getModeFromRequest(req)
-  let status = 'processing'
-  try {
-    if (kitRestarted) {
-      const chosenPlugin = await getPluginForRequest(req)
-      if (chosenPlugin) {
-        if (modeIsComplete(mode, chosenPlugin)) {
-          status = 'completed'
-        } else if (chosenPlugin.installedLocally && mode === 'uninstall') {
-          status = 'completed'
-        }
-      }
+function getErrorIfModeNotAllowedForPlugin (mode, plugin) {
+  if (mode === 'uninstall') {
+    if (getRequiredPlugins().includes(plugin.packageName)) {
+      const err = new Error('Uninstall restricted for this plugin')
+      err.status = 403
+      return err
     }
-  } catch (e) {
-    if (mode !== 'uninstall') {
-      console.log(e)
-    }
-  }
-  if (status === 'completed') {
-    setKitRestarted(false)
   }
-  res.json({ status })
 }
 
-async function postPluginsModeMiddleware (req, res, next) {
-  // Redirect to the GET route of the same url when the post request is not an ajax request
-  if (req.headers['content-type'].indexOf('json') === -1) {
-    res.redirect(req.originalUrl)
-  } else {
-    next()
-  }
-}
+async function runPluginMode (req, res, next) {
+  const { mode, packageRef } = req.params
+  const plugin = await getPluginDetailsFromRef(packageRef)
 
-async function postPluginsModeHandler (req, res) {
-  const mode = getModeFromRequest(req)
+  const err = getErrorIfModeNotAllowedForPlugin(mode, plugin)
 
-  // Allow smooth update from 13.1.0 as the status route is incorrectly matched
-  if (mode === 'status') {
-    req.params.mode = 'update'
-    return postPluginsStatusHandler(req, res)
+  if (err) {
+    return next(err)
   }
 
-  // Reset to false so the status route will only return completed when the prototype has restarted
-  setKitRestarted(false)
+  let command = plugin.commands && plugin.commands[mode]
 
-  const verb = verbs[mode]
-
-  if (!verb) {
-    res.json({ status: 'error' })
-    return
+  if (mode === 'uninstall') {
+    const related = await getRelatedPluginsForUninstall(plugin)
+    command = command.replace('npm uninstall ', `npm uninstall ${related.map(x => x.packageName).join(' ')} `)
+  } else {
+    const related = await getRelatedPluginsForInstallOrUpdate(plugin)
+    command = command.replace('npm install ', `npm install ${related.map(x => x.packageName).join(' ')} `)
   }
 
-  // Prevent uninstalling the kit itself
-  if (req.body.package === currentKitName && mode === 'uninstall') {
-    res.json({ status: 'error' })
-    return
-  }
+  const commandId = await queueCommand(command)
 
-  let status = 'processing'
+  res.send({
+    mode,
+    commandId,
+    statusUrl: `${contextPath}/command/${commandId}/status`
+  })
+}
+
+async function getCommandStatus (req, res) {
+  const { commandId } = req.params
   try {
-    const chosenPlugin = await getPluginForRequest(req)
-    if (!chosenPlugin) {
-      status = 'error'
-    } else if (modeIsComplete(mode, chosenPlugin)) {
-      status = 'completed'
-    } else {
-      const command = getCommand(mode, chosenPlugin)
-      await exec(command, { cwd: projectDir })
-        .finally(() => {
-          console.log(`Completed ${command}`)
-          // force the application to stop after a delay as nodemon restart does not always work on Windows when running acceptance tests
-          setTimeout(() => {
-            process.exit(1)
-          }, 6000)
-        })
-    }
+    const status = await fse.readJson(path.join(commandsDir, commandId + '.json'))
+    res.send(status)
   } catch (e) {
-    console.log(e)
-    status = 'error'
+    console.error(e)
+    res.status(400).send(e)
+  }
+}
+
+function getRequiredPlugins () {
+  const output = knownPlugins.required.filter(pluginName => pluginName !== 'govuk-frontend' || !getConfig().allowGovukFrontendUninstall)
+  return output
+}
+
+function legacyUpdateStatusCompatibilityHandler (req, res) {
+  if (req.body.package === 'govuk-prototype-kit') {
+    res.send({ status: 'completed' })
   }
-  res.json({ status })
 }
 
 module.exports = {
   contextPath,
-  setKitRestarted,
   csrfProtection: [doubleCsrfProtection, csrfErrorHandler],
   getPageLoadedHandler,
   getCsrfTokenHandler,
@@ -760,7 +860,6 @@ module.exports = {
   getPasswordHandler,
   postPasswordHandler,
   developmentOnlyMiddleware,
-  pluginCacheMiddleware,
   getHomeHandler,
   getTemplatesHandler,
   getTemplatesViewHandler,
@@ -769,8 +868,10 @@ module.exports = {
   getTemplatesPostInstallHandler,
   getPluginsHandler,
   postPluginsHandler,
+  getPluginDetailsHandler,
+  postPluginDetailsHandler,
   getPluginsModeHandler,
-  postPluginsStatusHandler,
-  postPluginsModeMiddleware,
-  postPluginsModeHandler
+  getCommandStatus,
+  runPluginMode,
+  legacyUpdateStatusCompatibilityHandler
 }
diff --git a/lib/manage-prototype-handlers.test.js b/lib/manage-prototype-handlers.test.js
index a0d0b57385..c5ccd320cc 100644
--- a/lib/manage-prototype-handlers.test.js
+++ b/lib/manage-prototype-handlers.test.js
@@ -8,12 +8,9 @@ const fse = require('fs-extra')
 
 // local dependencies
 const config = require('./config')
-const { requestHttpsJson } = require('./utils/requestHttps')
-const exec = require('./exec')
 const plugins = require('./plugins/plugins')
-const packages = require('./plugins/packages')
-const projectPackage = require('../package.json')
-const knownPlugins = require('../known-plugins.json')
+
+const pluginDetails = require('./utils/packageDetails')
 
 const mockNunjucksRender = jest.fn()
 const mockNunjucksAppEnv = jest.fn(() => ({
@@ -27,30 +24,23 @@ jest.doMock('./nunjucks/nunjucksConfiguration', () => ({
 }))
 
 const {
-  setKitRestarted,
   getPasswordHandler,
   getClearDataHandler,
-  getHomeHandler,
   postClearDataHandler,
   postPasswordHandler,
   developmentOnlyMiddleware,
-  getTemplatesHandler,
   getTemplatesViewHandler,
   getTemplatesInstallHandler,
   postTemplatesInstallHandler,
   getTemplatesPostInstallHandler,
-  getPluginsHandler,
-  postPluginsStatusHandler,
-  postPluginsModeMiddleware,
-  getPluginsModeHandler,
-  postPluginsModeHandler,
-  postPluginsHandler
+  getHomeHandler,
+  getTemplatesHandler
 } = require('./manage-prototype-handlers')
-const { projectDir } = require('./utils/paths')
 
 // mocked dependencies
 jest.mock('../package.json', () => {
   return {
+    version: '1.0.0',
     dependencies: {}
   }
 })
@@ -91,53 +81,6 @@ jest.mock('./plugins/plugins', () => {
   }
 })
 
-jest.mock('./plugins/plugin-utils', () => {
-  return {
-    getProxyPluginConfig: jest.fn().mockReturnValue({})
-  }
-})
-
-jest.mock('./plugins/packages', () => {
-  const packageWithPluginConfig = {
-    packageName: 'test-package',
-    installed: false,
-    available: true,
-    required: false,
-    latestVersion: '2.0.0',
-    versions: [
-      '2.0.0',
-      '1.0.0'
-    ],
-    packageJson: {},
-    pluginConfig: {}
-  }
-  const packageWithoutPluginConfig = {
-    packageName: 'test-package-not-a-plugin',
-    installed: false,
-    available: true,
-    required: false,
-    latestVersion: '2.0.0',
-    versions: [
-      '2.0.0',
-      '1.0.0'
-    ],
-    packageJson: {}
-  }
-  return {
-    lookupPackageInfo: jest.fn().mockImplementation((packageName) => {
-      if (packageName === packageWithPluginConfig.packageName) {
-        return packageWithPluginConfig
-      } else {
-        return undefined
-      }
-    }),
-    getInstalledPackages: jest.fn().mockResolvedValue([]),
-    getAllPackages: jest.fn().mockResolvedValue([packageWithPluginConfig, packageWithoutPluginConfig]),
-    getDependentPackages: jest.fn().mockResolvedValue([]),
-    getDependencyPackages: jest.fn().mockResolvedValue([])
-  }
-})
-
 jest.mock('./exec', () => {
   return {
     exec: jest.fn().mockReturnValue({ finally: jest.fn() })
@@ -233,14 +176,26 @@ describe('manage-prototype-handlers', () => {
   })
 
   it('getHomeHandler', async () => {
-    packages.lookupPackageInfo.mockResolvedValue({ packageName: 'govuk-prototype-kit', latestVersion: '1.0.0' })
+    jest.spyOn(pluginDetails, 'getLatestPluginDetailsFromNpm').mockImplementation((packageName) => {
+      if (packageName === '@nowprototypeit/govuk') {
+        return {
+          version: '99.99.1',
+          latestVersion: '99.99.1',
+          links: {
+            pluginDetails: '/abc'
+          }
+        }
+      }
+    })
+
     await getHomeHandler(req, res)
     expect(mockNunjucksRender).toHaveBeenCalledWith(
       'views/manage-prototype/index.njk',
       expect.objectContaining({
-        ...req.app.locals,
         currentSection: 'Home',
-        latestAvailableKit: '1.0.0'
+        latestAvailableKit: '99.99.1',
+        kitUpdateAvailable: true,
+        latestKitUrl: '/abc'
       })
     )
   })
@@ -264,7 +219,6 @@ describe('manage-prototype-handlers', () => {
   describe('templates handlers', () => {
     const packageName = 'test-package'
     const templateName = 'A page with everything'
-    const pluginDisplayName = { name: 'Test Package' }
     const templatePath = '/template'
     const encodedTemplatePath = encodeURIComponent(templatePath)
     const chosenUrl = '/chosen-url'
@@ -284,6 +238,7 @@ describe('manage-prototype-handlers', () => {
     })
 
     it('getTemplatesHandler', async () => {
+      jest.spyOn(pluginDetails, 'getInstalledPackages').mockResolvedValue([])
       await getTemplatesHandler(req, res)
       expect(mockNunjucksRender).toHaveBeenCalledWith(
         'views/manage-prototype/templates.njk',
@@ -292,7 +247,7 @@ describe('manage-prototype-handlers', () => {
           currentSection: 'Templates',
           availableTemplates: [{
             packageName,
-            pluginDisplayName,
+            pluginDisplayName: { name: 'Test Package' },
             templates: [{
               installLink: `/manage-prototype/templates/install?package=${packageName}&template=${encodedTemplatePath}`,
               name: templateName,
@@ -417,7 +372,7 @@ describe('manage-prototype-handlers', () => {
           }).forEach(testPostTemplatesInstallHandler)
 
           // Test each invalid character
-          "!$&'()*+,;=:?#[]@.% "
+          '!$&\'()*+,;=:?#[]@.% '
             .split('')
             .map(invalidCharacter => ['invalid', `/${invalidCharacter}/abc`])
             .forEach(testPostTemplatesInstallHandler)
@@ -439,257 +394,4 @@ describe('manage-prototype-handlers', () => {
       )
     })
   })
-
-  describe('plugins handlers', () => {
-    const csrfToken = 'x-csrf-token'
-    const packageName = 'test-package'
-    const latestVersion = '2.0.0'
-    const previousVersion = '1.0.0'
-    const pluginDisplayName = { name: 'Test Package' }
-    const availablePlugin = {
-      installCommand: `npm install ${packageName}`,
-      installLink: `/manage-prototype/plugins/install?package=${packageName}`,
-      latestVersion,
-      name: pluginDisplayName.name,
-      packageName,
-      uninstallCommand: `npm uninstall ${packageName}`,
-      updateCommand: `npm install ${packageName}@${latestVersion}`
-    }
-
-    beforeEach(() => {
-      knownPlugins.plugins = { available: [packageName] }
-      projectPackage.dependencies = {}
-      const versions = {}
-      versions[latestVersion] = {}
-      versions[previousVersion] = {}
-      requestHttpsJson.mockResolvedValue({
-        name: packageName,
-        'dist-tags': {
-          latest: latestVersion,
-          'latest-1': previousVersion
-        },
-        versions
-      })
-      // mocking the reading of the local package.json
-      fse.readJsonSync.mockReturnValue(undefined)
-      packages.lookupPackageInfo.mockResolvedValue(Promise.resolve(availablePlugin))
-      res.json = jest.fn().mockReturnValue({})
-    })
-
-    describe('getPluginsHandler', () => {
-      it('plugins installed', async () => {
-        fse.readJsonSync.mockReturnValue(undefined)
-        req.route.path = 'plugins-installed'
-        await getPluginsHandler(req, res)
-        expect(mockNunjucksRender).toHaveBeenCalledWith(
-          'views/manage-prototype/plugins.njk',
-          expect.objectContaining({
-            ...req.app.locals,
-            currentSection: 'Plugins',
-            isSearchPage: false,
-            isInstalledPage: true,
-            plugins: [],
-            status: 'installed'
-          })
-        )
-      })
-      it('plugins available', async () => {
-        fse.readJsonSync.mockReturnValue(undefined)
-        req.route.path = 'plugins'
-        await getPluginsHandler(req, res)
-        expect(mockNunjucksRender).toHaveBeenCalledWith(
-          'views/manage-prototype/plugins.njk',
-          expect.objectContaining({
-            ...req.app.locals,
-            currentSection: 'Plugins',
-            isSearchPage: true,
-            isInstalledPage: false,
-            plugins: [availablePlugin],
-            status: 'search'
-          })
-        )
-      })
-    })
-
-    it('postPluginsHandler', async () => {
-      const search = 'task list'
-      const routePath = '/plugins-installed'
-      const fullPath = '/manage-prototype' + routePath
-      req.body.search = search
-      req.route.path = routePath
-      await postPluginsHandler(req, res)
-      expect(res.redirect).toHaveBeenCalledWith(fullPath + '?search=' + search)
-    })
-
-    it('getPluginsModeHandler', async () => {
-      req.params.mode = 'install'
-      req.query.package = packageName
-      req.csrfToken = jest.fn().mockReturnValue(csrfToken)
-      await getPluginsModeHandler(req, res)
-      expect(mockNunjucksRender).toHaveBeenCalledWith(
-        'views/manage-prototype/plugin-install-or-uninstall.njk',
-        expect.objectContaining({
-          ...req.app.locals,
-          chosenPlugin: availablePlugin,
-          command: `npm install ${packageName} --save-exact`,
-          currentSection: 'Plugins',
-          pageName: `Install ${pluginDisplayName.name}`,
-          currentUrl: req.originalUrl,
-          isSameOrigin: false,
-          returnLink: {
-            href: '/manage-prototype/plugins',
-            text: 'Back to plugins'
-          }
-        })
-      )
-    })
-
-    describe('postPluginsModeHandler', () => {
-      beforeEach(() => {
-        req.params.mode = 'install'
-        req.body.package = packageName
-      })
-
-      it('processing', async () => {
-        await postPluginsModeHandler(req, res)
-        expect(exec.exec).toHaveBeenCalledWith(
-          availablePlugin.installCommand + ' --save-exact',
-          { cwd: projectDir }
-        )
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'processing'
-          })
-        )
-      })
-
-      it('processing specific version', async () => {
-        packages.lookupPackageInfo.mockResolvedValue({
-          packageName: 'test-package',
-          installed: false,
-          versions: ['1.0.0']
-        })
-        req.body.version = previousVersion
-        const installSpecificCommand = availablePlugin.installCommand + `@${previousVersion}`
-        await postPluginsModeHandler(req, res)
-        expect(exec.exec).toHaveBeenCalledWith(
-          installSpecificCommand + ' --save-exact',
-          { cwd: projectDir }
-        )
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'processing'
-          })
-        )
-      })
-
-      it('error invalid package', async () => {
-        packages.lookupPackageInfo.mockResolvedValue(undefined)
-        req.body.package = 'invalid-package'
-        await postPluginsModeHandler(req, res)
-        expect(exec.exec).not.toHaveBeenCalled()
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'error'
-          })
-        )
-      })
-
-      it('error invalid version', async () => {
-        req.body.version = '1.0.0-invalid'
-        await postPluginsModeHandler(req, res)
-        expect(exec.exec).not.toHaveBeenCalled()
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'error'
-          })
-        )
-      })
-
-      it('is passed on to the postPluginsStatusHandler when status matches mode during update from 13.1 to 13.2.4 and upwards', async () => {
-        req.params.mode = 'status'
-        setKitRestarted(true)
-        await postPluginsModeHandler(req, res)
-
-        // req.params.mode should change to update
-        expect(req.params.mode).toEqual('update')
-
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'processing'
-          })
-        )
-      })
-    })
-
-    describe('postPluginsStatusHandler', () => {
-      let pkg
-
-      beforeEach(() => {
-        req.params.mode = 'install'
-        req.query.package = packageName
-        pkg = {
-          name: packageName,
-          version: latestVersion,
-          dependencies: { [packageName]: latestVersion }
-        }
-        fse.readJsonSync.mockReturnValue(pkg)
-      })
-
-      it('is processing', async () => {
-        await postPluginsStatusHandler(req, res)
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'processing'
-          })
-        )
-      })
-
-      it('is completed', async () => {
-        packages.lookupPackageInfo.mockResolvedValue({
-          packageName: 'test-package',
-          installedVersion: '2.0.0'
-        })
-        setKitRestarted(true)
-        await postPluginsStatusHandler(req, res)
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'completed'
-          })
-        )
-      })
-
-      it('uninstall local plugin is completed', async () => {
-        const localPlugin = 'local-plugin'
-        req.params.mode = 'uninstall'
-        req.query.package = localPlugin
-        pkg.dependencies[localPlugin] = 'file:../../local-plugin'
-        packages.lookupPackageInfo.mockResolvedValue({
-          packageName: 'test-package',
-          installed: false
-        })
-        setKitRestarted(true)
-        await postPluginsStatusHandler(req, res)
-        expect(res.json).toHaveBeenCalledWith(
-          expect.objectContaining({
-            status: 'completed'
-          })
-        )
-      })
-    })
-
-    describe('postPluginsModeMiddleware', () => {
-      it('with AJAX', async () => {
-        req.headers['content-type'] = 'application/json'
-        await postPluginsModeMiddleware(req, res, next)
-        expect(next).toHaveBeenCalled()
-      })
-
-      it('without AJAX', async () => {
-        req.headers['content-type'] = 'document/html'
-        await postPluginsModeMiddleware(req, res, next)
-        expect(res.redirect).toHaveBeenCalledWith(req.originalUrl)
-      })
-    })
-  })
 })
diff --git a/lib/manage-prototype-routes.js b/lib/manage-prototype-routes.js
index 42c824c03e..be02eb931f 100644
--- a/lib/manage-prototype-routes.js
+++ b/lib/manage-prototype-routes.js
@@ -3,7 +3,6 @@ const express = require('express')
 
 const {
   contextPath,
-  setKitRestarted,
   csrfProtection,
   getPageLoadedHandler,
   getCsrfTokenHandler,
@@ -19,12 +18,13 @@ const {
   postTemplatesInstallHandler,
   getTemplatesPostInstallHandler,
   getPluginsHandler,
+  postPluginsHandler,
+  getPluginDetailsHandler,
+  postPluginDetailsHandler,
+  runPluginMode,
+  getCommandStatus,
   getPluginsModeHandler,
-  postPluginsModeMiddleware,
-  postPluginsModeHandler,
-  postPluginsStatusHandler,
-  pluginCacheMiddleware,
-  postPluginsHandler
+  legacyUpdateStatusCompatibilityHandler
 } = require('./manage-prototype-handlers')
 const { packageDir, projectDir } = require('./utils/paths')
 const { govukFrontendPaths } = require('./govukFrontendPaths')
@@ -51,8 +51,6 @@ router.post('/password', postPasswordHandler)
 // view when the prototype is not running in development
 router.use(developmentOnlyMiddleware)
 
-router.use(pluginCacheMiddleware)
-
 router.get('/', getHomeHandler)
 
 router.get('/templates', getTemplatesHandler)
@@ -69,21 +67,33 @@ router.get('/plugins', getPluginsHandler)
 router.post('/plugins', postPluginsHandler)
 router.get('/plugins-installed', getPluginsHandler)
 
-// Be aware that changing this path for monitoring the status of a plugin will affect the
-// kit update process as the browser request and server route would be out of sync.
-router.post('/plugins/:mode/status', postPluginsStatusHandler)
-
-router.get('/plugins/:mode', csrfProtection, getPluginsModeHandler)
+router.get('/plugin/:packageRef', getPluginDetailsHandler)
+router.post('/plugin', postPluginDetailsHandler)
+router.get('/plugin/:packageRef/:mode', getPluginsModeHandler)
+router.post('/plugin/:packageRef/:mode', csrfProtection, runPluginMode)
 
-router.post('/plugins/:mode', postPluginsModeMiddleware)
+router.get('/command/:commandId/status', getCommandStatus)
 
-router.post('/plugins/:mode', csrfProtection, postPluginsModeHandler)
+// // Be aware that changing this path for monitoring the status of a plugin will affect the
+// // kit update process as the browser request and server route would be out of sync.
+router.post('/plugins/:mode', legacyUpdateStatusCompatibilityHandler)
 
 // Find GOV.UK Frontend (via internal package, project fallback)
 router.use('/dependencies/govuk-frontend', express.static(
   govukFrontendPaths([packageDir, projectDir]).baseDir)
 )
 
-setKitRestarted(true)
+router.use((err, req, res, next) => {
+  if (err.status === 404) {
+    next(err)
+  } else {
+    res.status(err.status || 500).render('views/error-handling/server-error.njk', {
+      error: {
+        message: err.message,
+        errorStack: err.stack
+      }
+    })
+  }
+})
 
 module.exports = router
diff --git a/lib/nunjucks/govuk-prototype-kit/includes/homepage-bottom.njk b/lib/nunjucks/nowprototypeit/includes/homepage-bottom.njk
similarity index 100%
rename from lib/nunjucks/govuk-prototype-kit/includes/homepage-bottom.njk
rename to lib/nunjucks/nowprototypeit/includes/homepage-bottom.njk
diff --git a/lib/nunjucks/govuk-prototype-kit/includes/homepage-top.njk b/lib/nunjucks/nowprototypeit/includes/homepage-top.njk
similarity index 62%
rename from lib/nunjucks/govuk-prototype-kit/includes/homepage-top.njk
rename to lib/nunjucks/nowprototypeit/includes/homepage-top.njk
index 3f15dcdeed..2a35ef2c83 100644
--- a/lib/nunjucks/govuk-prototype-kit/includes/homepage-top.njk
+++ b/lib/nunjucks/nowprototypeit/includes/homepage-top.njk
@@ -1,8 +1,8 @@
-

- GOV.UK Prototype Kit +

+ Now Prototype It Kit

-{% if (GOVUKPrototypeKit.isDevelopment) %} +{% if (NowPrototypeIt.isDevelopment) %}

Manage your prototype

diff --git a/lib/nunjucks/govuk-prototype-kit/includes/scripts.njk b/lib/nunjucks/nowprototypeit/includes/scripts.njk similarity index 100% rename from lib/nunjucks/govuk-prototype-kit/includes/scripts.njk rename to lib/nunjucks/nowprototypeit/includes/scripts.njk diff --git a/lib/nunjucks/govuk-prototype-kit/includes/stylesheets-plugins.njk b/lib/nunjucks/nowprototypeit/includes/stylesheets-plugins.njk similarity index 100% rename from lib/nunjucks/govuk-prototype-kit/includes/stylesheets-plugins.njk rename to lib/nunjucks/nowprototypeit/includes/stylesheets-plugins.njk diff --git a/lib/nunjucks/govuk-prototype-kit/includes/stylesheets.njk b/lib/nunjucks/nowprototypeit/includes/stylesheets.njk similarity index 100% rename from lib/nunjucks/govuk-prototype-kit/includes/stylesheets.njk rename to lib/nunjucks/nowprototypeit/includes/stylesheets.njk diff --git a/lib/nunjucks/nowprototypeit/layouts/branded.njk b/lib/nunjucks/nowprototypeit/layouts/branded.njk new file mode 100644 index 0000000000..ea3f1fdcb5 --- /dev/null +++ b/lib/nunjucks/nowprototypeit/layouts/branded.njk @@ -0,0 +1,67 @@ +{%- set assetPath = assetPath | default('/plugin-assets/govuk-frontend' + govukFrontend.assetPath) -%} + + + + {% block head %} + {% block meta %}{% endblock %} + {% block stylesheets %} + {% include "govuk-prototype-kit/includes/stylesheets.njk" %} + {% endblock %} + {% endblock %} + + {% block pageTitle %} + {% if pageName %}{{ pageName }} - {% endif %}{{ serviceName }} - Now Prototype It + {% endblock %} + + + +
+{% block header %} +
+{% endblock %} + +
+ {% block content %}{% endblock %} +
+
+{% if (GOVUKPrototypeKit.isDevelopment) %} + {% set footerItems = [ + { + href: "/manage-prototype", + text: "Manage your prototype" + }, { + href: "/manage-prototype/clear-data", + text: "Clear data" + } + ] %} +{% else %} + {% set footerItems = [ + { + href: "/manage-prototype/clear-data", + text: "Clear data" + } + ] %} +{% endif %} + +{% block footer %} +
This is the footer
+{% endblock %} +Light Bulb by Saepul Nahwan +
+{% block bodyEnd %} + {% block scripts %} + {% include "govuk-prototype-kit/includes/scripts.njk" %} + {% block pageScripts %}{% endblock %} + {% endblock %} +{% endblock %} + diff --git a/lib/nunjucks/govuk-prototype-kit/layouts/govuk-branded.njk b/lib/nunjucks/nowprototypeit/layouts/govuk-branded.njk similarity index 100% rename from lib/nunjucks/govuk-prototype-kit/layouts/govuk-branded.njk rename to lib/nunjucks/nowprototypeit/layouts/govuk-branded.njk diff --git a/lib/nunjucks/govuk-prototype-kit/layouts/unbranded.njk b/lib/nunjucks/nowprototypeit/layouts/unbranded.njk similarity index 62% rename from lib/nunjucks/govuk-prototype-kit/layouts/unbranded.njk rename to lib/nunjucks/nowprototypeit/layouts/unbranded.njk index a759fc767d..aeffb57036 100644 --- a/lib/nunjucks/govuk-prototype-kit/layouts/unbranded.njk +++ b/lib/nunjucks/nowprototypeit/layouts/unbranded.njk @@ -1,7 +1,7 @@ -{% extends "govuk-prototype-kit/layouts/govuk-branded.njk" %} +{% extends "nowprototypeit/layouts/govuk-branded.njk" %} {% block headIcons %} - + {% endblock %} {% block stylesheets %} diff --git a/lib/nunjucks/nunjucksLoader.js b/lib/nunjucks/nunjucksLoader.js index 7c5b06d5c0..822b2fb5c5 100644 --- a/lib/nunjucks/nunjucksLoader.js +++ b/lib/nunjucks/nunjucksLoader.js @@ -69,6 +69,12 @@ const NunjucksLoader = nunjucks.Loader.extend({ } if (!pathToFile) { + if (name.startsWith('govuk-prototype-kit')) { + try { + return this.getSource(name.replace('govuk-prototype-kit', 'nowprototypeit')) + } catch (e) {} + } + console.log('error getting', name) endPerformanceTimer('getSource (failure)', timer) const error = new Error(`template not found: ${name}`) error.internalErrorCode = 'TEMPLATE_NOT_FOUND' diff --git a/lib/nunjucks/views/backup-homepage.njk b/lib/nunjucks/views/backup-homepage.njk index fb31047f95..6756e8ef66 100644 --- a/lib/nunjucks/views/backup-homepage.njk +++ b/lib/nunjucks/views/backup-homepage.njk @@ -1,7 +1,7 @@ -{% extends "govuk-prototype-kit/layouts/unbranded.njk" %} +{% extends "now-prototype-it-govuk/layouts/unbranded.njk" %} {% block pageTitle %} -Home – GOV.UK Prototype Kit +Home – Now Prototype It Kit {% endblock %} {% block content %} diff --git a/lib/nunjucks/views/error-handling/page-not-found.njk b/lib/nunjucks/views/error-handling/page-not-found.njk index 25617cae87..657df6f7fb 100644 --- a/lib/nunjucks/views/error-handling/page-not-found.njk +++ b/lib/nunjucks/views/error-handling/page-not-found.njk @@ -1,7 +1,7 @@ {% extends "views/manage-prototype/layout.njk" %} {% block pageTitle %} - Page not found – {{ serviceName }} – GOV.UK Prototype Kit + Page not found – Now Prototype It {% endblock %} {% set mainClasses = "govuk-main-wrapper--l" %} @@ -21,9 +21,6 @@
  • a form in your code is wrong
  • you have not created the page yet
  • -

    - You can try and fix this yourself or contact the GOV.UK Prototype Kit team if you need help. -

    {% endblock %} diff --git a/lib/nunjucks/views/error-handling/server-error.njk b/lib/nunjucks/views/error-handling/server-error.njk index 7359f7b682..d8650d19e9 100644 --- a/lib/nunjucks/views/error-handling/server-error.njk +++ b/lib/nunjucks/views/error-handling/server-error.njk @@ -1,11 +1,11 @@ {% extends "views/manage-prototype/layout.njk" %} {% block pageTitle %} - Error {% if serviceName %}– {{ serviceName }}{% endif %} – GOV.UK Prototype Kit + Error – Now Prototype It {% endblock %} {% block header %} -
    +
    {{ super() }}
    {% endblock %} @@ -17,26 +17,26 @@

    There is an error

    {% if error.filePath %} -

    - File: {{ error.filePath }} {% if error.line %}(line {{ error.line }}){% endif %} +

    + File: {{ error.filePath }} {% if error.line %}(line {{ error.line }}){% endif %}

    {% endif %} -

    - Error: {{ error.message }} +

    + Error: {{ error.message }}

    {% if error.sourceCode %} -
    {{ error.sourceCode.before }}
    {{ error.sourceCode.error }}
    {{ error.sourceCode.after }}
    +
    {{ error.sourceCode.before }}
    {{ error.sourceCode.error }}
    {{ error.sourceCode.after }}
    {% endif %} - -
    {{ error.errorStack }}
    +
    {{ error.errorStack }}

    Get support @@ -49,11 +49,6 @@ {% endblock %} -{% block footer %} - {{ govukFooter({}) }} -{% endblock %} - - {% block pageScripts %} diff --git a/lib/nunjucks/views/manage-prototype/clear-data-success.njk b/lib/nunjucks/views/manage-prototype/clear-data-success.njk index 8671ef4725..7a03db3838 100644 --- a/lib/nunjucks/views/manage-prototype/clear-data-success.njk +++ b/lib/nunjucks/views/manage-prototype/clear-data-success.njk @@ -1,7 +1,7 @@ {% extends "views/manage-prototype/layout.njk" %} {% block pageTitle %} - Data cleared – GOV.UK Prototype Kit + Data cleared – Now Prototype It {% endblock %} {% block content %} diff --git a/lib/nunjucks/views/manage-prototype/clear-data.njk b/lib/nunjucks/views/manage-prototype/clear-data.njk index 483c12a013..5e87876f16 100644 --- a/lib/nunjucks/views/manage-prototype/clear-data.njk +++ b/lib/nunjucks/views/manage-prototype/clear-data.njk @@ -5,7 +5,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% block pageTitle %} - Clear session data – GOV.UK Prototype Kit + Clear session data – Now Prototype It {% endblock %} {% block beforeContent %} diff --git a/lib/nunjucks/views/manage-prototype/index.njk b/lib/nunjucks/views/manage-prototype/index.njk index ece33f1f15..0b2fbcccd7 100644 --- a/lib/nunjucks/views/manage-prototype/index.njk +++ b/lib/nunjucks/views/manage-prototype/index.njk @@ -8,13 +8,13 @@

    Manage your prototype

    -

    - GOV.UK Prototype Kit {{ releaseVersion }} +

    + Now Prototype It - GOV.UK Kit {{ releaseVersion }}

    {% if kitUpdateAvailable %}

    - - New version available: {{ latestAvailableKit }} + + New version available: v{{ latestAvailableKit }}

    {% endif %} @@ -28,13 +28,13 @@

    Start a new prototype

    -
      +
        {% for task in tasks %} -
      • - +
      • + {{ task.html | safe }} - + {% if task.done %}Done{% else %}To do{% endif %}
      • diff --git a/lib/nunjucks/views/manage-prototype/layout.njk b/lib/nunjucks/views/manage-prototype/layout.njk index 6e8eeb640c..45addae612 100644 --- a/lib/nunjucks/views/manage-prototype/layout.njk +++ b/lib/nunjucks/views/manage-prototype/layout.njk @@ -1,5 +1,5 @@ {%- set assetPath = '/manage-prototype/dependencies/govuk-frontend' + govukFrontendInternal.assetPath -%} -{% extends "govuk-prototype-kit/layouts/govuk-branded.njk" %} +{% extends "nowprototypeit/layouts/branded.njk" %} {% block pageTitle %} {% if pageName %} @@ -9,7 +9,7 @@ {% endif %} Manage your prototype - {{ serviceName }} - - GOV.UK Prototype Kit + - Now Prototype It {% endblock %} {% block stylesheets %} @@ -22,17 +22,17 @@ {% endblock %} {% block beforeContent %} -