[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 %} - -Answer: {{ data['most-impressive-trick'] }}
-[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" %} -{{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" %} -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') %} -calculating
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(` - - -This is a paragraph of text. It explains in more detail what has happened and wraps across several lines.
- -Read more about this topic.
- -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 %} -') - 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) %}
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 %}
+
+
+
+ Now Prototype It
+
+
+{% endblock %}
+
+
+ {% block content %}{% endblock %}
+
+
+{% 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 %}
-