Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get live reloading working with the error screen #2164

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-acceptance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
node-version: [18.x]
os: [macos-latest, windows-latest, ubuntu-latest]
type: [smoke, plugins, dev, prod]
type: [smoke, plugins, dev, prod, errors]

name: Acceptance ${{ matrix.type }} test kit on Node v${{ matrix.node-version }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New features

- [#2164: Get live reloading working with the error screen](https://github.com/alphagov/govuk-prototype-kit/pull/2164)

## 13.7.0

### New features
Expand Down
18 changes: 10 additions & 8 deletions cypress/e2e/dev/1-watch-files/watch-config.cypress.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ 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 serverNameQuery = 'h1.govuk-heading-xl'

function restore () {
// Restore config.json from prototype starter
cy.task('copyFromStarterFiles', { filename: appConfigPath })
}

describe('watch config file', () => {
describe(`service name in config file ${appConfig} should be changed and restored`, () => {
before(() => {
// Restore config.json from prototype starter
cy.task('copyFromStarterFiles', { filename: appConfigPath })
})
before(restore)
after(restore)

it('The service name should change to "cypress test"', () => {
waitForApplication()
cy.visit('/')
waitForApplication('/')
cy.get(serverNameQuery).contains(originalText)
cy.task('replaceTextInFile', { filename: appConfig, originalText, newText })
waitForApplication()
waitForApplication('/')
cy.get(serverNameQuery).contains(newText)
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,34 @@ const pageName = 'There is an error'
const contactSupportText = 'You can try and fix this yourself or contact the GOV.UK Prototype Kit team if you need help.'
const expectedErrorText = 'ReferenceError: lkewjflkjadsf is not defined'

describe('Fatal Error Test', () => {
before(() => {
cy.task('log', `Replace ${appRoutes} with Cypress routes`)
cy.task('copyFile', { source: completelyBrokenRoutesFixture, target: appRoutes })
})
const homePageName = 'GOV.UK Prototype Kit'

after(() => {
cy.task('log', `Restore ${appRoutesPath}`)
cy.task('copyFromStarterFiles', { filename: appRoutesPath })
})
function restore () {
// Restore config.json from prototype starter
cy.task('log', `Restore ${appRoutesPath}`)
cy.task('copyFromStarterFiles', { filename: appRoutesPath })
}

describe('Fatal Error Test', () => {
before(restore)
after(restore)

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.get('.govuk-heading-l').contains(pageName)
cy.get('.govuk-body').contains(contactSupportText)
cy.get('code').contains(expectedErrorText)

cy.task('log', `Restore ${appRoutes} with original routes`)
restore()

cy.get('.govuk-heading-l').contains(homePageName)
})
})
9 changes: 9 additions & 0 deletions lib/assets/javascripts/kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,12 @@ window.GOVUKPrototypeKit = {
if (window.console && window.console.info) {
window.console.info('GOV.UK Prototype Kit - do not use for production')
}

window.GOVUKPrototypeKit.documentReady(function () {
const sendPageLoadedRequest = function () {
fetch('/manage-prototype/page-loaded').catch(() => {
setTimeout(sendPageLoadedRequest, 500)
})
}
sendPageLoadedRequest()
})
26 changes: 24 additions & 2 deletions lib/errorServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const path = require('path')
const fs = require('fs')

const { getNunjucksAppEnv } = require('./nunjucks/nunjucksConfiguration')
const syncChanges = require('./sync-changes')

function runErrorServer (error) {
let port
Expand All @@ -10,6 +11,7 @@ function runErrorServer (error) {
} catch (e) {
port = process.env.PORT || 3000
}
const proxyPort = port - 50
const http = require('http')
const fileExtensionsToMimeTypes = {
js: 'application/javascript',
Expand All @@ -28,6 +30,14 @@ function runErrorServer (error) {
}

const requestListener = function (req, res) {
if (req.url.startsWith('/browser-sync')) {
return
}
if (req.url.startsWith('/manage-prototype/page-loaded')) {
const result = syncChanges.pageLoaded()
res.end(JSON.stringify(result))
return
}
if (knownPaths[req.url]) {
res.setHeader('Content-Type', knownPaths[req.url].type)
res.writeHead(200)
Expand Down Expand Up @@ -56,7 +66,14 @@ function runErrorServer (error) {
path.join(process.cwd(), 'node_modules', 'govuk-frontend')
])
res.end(nunjucksAppEnv.render('views/error-handling/server-error', {
errorStack: error.stack
errorStack: error.stack,
pluginConfig: {
scripts: [
{
src: '/plugin-assets/govuk-prototype-kit/lib/assets/javascripts/kit.js'
}
]
}
}))
} catch (ignoreThisError) {
fileContentsParts.push('<h1 class="govuk-heading-l">There is an error</h1>')
Expand All @@ -72,7 +89,7 @@ function runErrorServer (error) {

const server = http.createServer(requestListener)

server.listen(port, () => {
server.listen(proxyPort, () => {
console.log('')
console.log('')
console.log(`There's an error, you can see it below or at http://localhost:${port}`)
Expand All @@ -83,6 +100,11 @@ function runErrorServer (error) {
console.log('')
console.log('')
console.log('')
syncChanges.sync({
port,
proxyPort,
files: ['app/**/*.*']
})
})
}

Expand Down
7 changes: 7 additions & 0 deletions lib/manage-prototype-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { exec } = require('./exec')
const { requestHttpsJson, prototypeAppScripts, sortByObjectKey } = require('./utils')
const { projectDir, packageDir, appViewsDir } = require('./utils/paths')
const nunjucksConfiguration = require('./nunjucks/nunjucksConfiguration')
const syncChanges = require('./sync-changes')

const contextPath = '/manage-prototype'

Expand Down Expand Up @@ -115,6 +116,11 @@ const csrfErrorHandler = (error, req, res, next) => {
}
}

function getPageLoadedHandler (req, res) {
const result = syncChanges.pageLoaded()
return res.json(result)
}

function getCsrfTokenHandler (req, res) {
const token = generateToken(res, req)
return res.json({ token })
Expand Down Expand Up @@ -704,6 +710,7 @@ module.exports = {
contextPath,
setKitRestarted,
csrfProtection: [doubleCsrfProtection, csrfErrorHandler],
getPageLoadedHandler,
getCsrfTokenHandler,
getClearDataHandler,
postClearDataHandler,
Expand Down
4 changes: 4 additions & 0 deletions lib/manage-prototype-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
contextPath,
setKitRestarted,
csrfProtection,
getPageLoadedHandler,
getCsrfTokenHandler,
getClearDataHandler,
postClearDataHandler,
Expand Down Expand Up @@ -35,6 +36,9 @@ redirectingRouter.use((req, res) => {

router.get('/csrf-token', getCsrfTokenHandler)

// Indicates page has loaded
router.get('/page-loaded', getPageLoadedHandler)

// Clear all data in session
router.get('/clear-data', getClearDataHandler)

Expand Down
1 change: 1 addition & 0 deletions lib/nunjucks/govuk-prototype-kit/includes/scripts.njk
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
<script src="{{scriptConfig.src}}"></script>
{% endif %}
{% endfor %}

44 changes: 44 additions & 0 deletions lib/sync-changes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// dependencies
const EventEmitter = require('events')

// npm dependencies
const browserSync = require('browser-sync')

const eventEmitter = new EventEmitter()

const pageLoadedEvent = 'sync-changes:page-loaded'

function pageLoaded () {
eventEmitter.emit(pageLoadedEvent)
return { status: 'received ok' }
}

function sync ({ port, proxyPort, files }) {
browserSync({
ws: true,
proxy: 'localhost:' + proxyPort,
port,
ui: false,
files,
ghostMode: false,
open: false,
notify: false,
logLevel: 'error',
callbacks: {
ready: (_, bs) => {
// Repeat browser sync reload every 1000 milliseconds until it is successful
const intervalId = setInterval(browserSync.reload, 1000)
eventEmitter.once(pageLoadedEvent, () => {
bs.events.once('browser:reload', () => {
clearInterval(intervalId)
})
})
}
}
})
}

module.exports = {
sync,
pageLoaded
}
16 changes: 6 additions & 10 deletions listen-on-port.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@

// npm dependencies
const browserSync = require('browser-sync')
const { runErrorServer } = require('./lib/errorServer')

try {
// local dependencies
const syncChanges = require('./lib/sync-changes')
const server = require('./server.js')
const { generateAssetsSync } = require('./lib/build')
const config = require('./lib/config.js').getConfig(null, false)

const port = config.port
const proxyPort = port - 50

generateAssetsSync()

Expand All @@ -28,16 +29,11 @@ try {
if (config.isProduction || !config.useBrowserSync) {
server.listen(port)
} else {
server.listen(port - 50, () => {
browserSync({
proxy: 'localhost:' + (port - 50),
server.listen(proxyPort, () => {
syncChanges.sync({
port,
ui: false,
files: ['.tmp/public/**/*.*', 'app/views/**/*.*', 'app/assets/**/*.*', 'app/config.json'],
ghostMode: false,
open: false,
notify: false,
logLevel: 'error'
proxyPort,
files: ['.tmp/public/**/*.*', 'app/views/**/*.*', 'app/assets/**/*.*', 'app/config.json']
})
})
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"cypress:e2e:prod": "cypress run --spec \"cypress/e2e/prod/*/*\"",
"cypress:e2e:smoke": "cypress run --spec \"cypress/e2e/smoke/*/*\"",
"cypress:e2e:plugins": "cypress run --spec \"cypress/e2e/plugins/*/*\"",
"cypress:e2e:errors": "cypress run --spec \"cypress/e2e/errors/*/*\"",
"test:heroku": "start-server-and-test start:test:heroku 3000 cypress:e2e:smoke",
"test:acceptance:dev": "cross-env KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test 3000 cypress:e2e:dev",
"test:acceptance:prod": "cross-env PASSWORD=password KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test:prod 3000 cypress:e2e:prod",
"test:acceptance:smoke": "cross-env KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test 3000 cypress:e2e:smoke",
"test:acceptance:plugins": "cross-env KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test 3000 cypress:e2e:plugins",
"test:acceptance:errors": "cross-env KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test 3000 cypress:e2e:errors",
"test:acceptance:open": "cross-env KIT_TEST_DIR=tmp/test-prototype start-server-and-test start:test 3000 cypress:open",
"test:unit": "jest --detectOpenHandles lib bin migrator",
"test:integration": "cross-env CREATE_KIT_TIMEOUT=240000 jest --detectOpenHandles --testTimeout=60000 __tests__",
Expand Down