diff --git a/lib/_update_javascripts.js b/lib/_update_javascripts.js index 7ff0f359d1..cc29ca5ad4 100644 --- a/lib/_update_javascripts.js +++ b/lib/_update_javascripts.js @@ -1,43 +1,13 @@ const fs = require('fs').promises -const https = require('https') const path = require('path') -const { projectDir, packageDir } = require('./path-utils') +const { projectDir } = require('./path-utils') +const { getProjectVersion, fetchOriginal } = require('./update-utils') -async function getProjectVersion () { - return (await fs.readFile(path.join(projectDir, 'VERSION.txt'), 'utf8')).trim() -} - -async function fetchOriginal (version, filePath) { - const remoteUrl = `https://raw.githubusercontent.com/alphagov/govuk-prototype-kit/v${version}/${filePath}` - - let data = '' - return new Promise((resolve, reject) => { - https.get(remoteUrl, (response) => { - let error - - if (response.statusCode !== 200) { - error = new Error(`could not fetch ${remoteUrl}: status code ${response.statusCode}`) - Object.assign(error, response) - response.resume() - reject(error) - } - - response.setEncoding('utf8') - - response.on('data', (chunk) => { - data += chunk - }) - - response.on('end', () => { - resolve(data) - }) - }) - }) -} +const updateDir = path.join(projectDir, 'update') async function removeKitJsFromApplicationJs () { - const userVersion = getProjectVersion() + const userVersion = await getProjectVersion() // If the user already has version 13 or greater of the kit installed then // their application.js file is all their code and we don't don't want to @@ -52,17 +22,17 @@ async function removeKitJsFromApplicationJs () { // If the user hasn't changed their application.js file we can just replace it completely if (original === theirs) { - return fs.copyFile(path.join(packageDir, 'lib', assetPath), path.join(projectDir, 'app', assetPath)) + return fs.copyFile(path.join(updateDir, 'app', assetPath), path.join(projectDir, 'app', assetPath)) } // Otherwise, if the original code is contained as-is in their file, we can // remove the shared lines, and add our hints if (theirs.includes(original)) { - const ours = await fs.readFile(path.resolve(packageDir, 'app', assetPath), 'utf8') + const ours = await fs.readFile(path.resolve(updateDir, 'app', assetPath), 'utf8') let merged - merged = theirs.replace(original, '\n') - merged = ours + '\n' + merged + merged = theirs.replace(original, '') + merged = ours + merged return fs.writeFile(path.resolve(projectDir, 'app', assetPath), merged, 'utf8') } @@ -88,6 +58,11 @@ async function main () { await removeKitJsFromApplicationJs() } +module.exports = { + /* exported for tests only */ + removeKitJsFromApplicationJs +} + if (require.main === module) { main() } diff --git a/lib/_update_javascripts.test.js b/lib/_update_javascripts.test.js new file mode 100644 index 0000000000..42b53f9ff0 --- /dev/null +++ b/lib/_update_javascripts.test.js @@ -0,0 +1,103 @@ +/* eslint-env jest */ + +const fs = require('fs').promises +const path = require('path') + +const updateUtils = require('./update-utils') +updateUtils.getProjectVersion = jest.fn() +updateUtils.fetchOriginal = jest.fn() + +const updatejs = require('./_update_javascripts') + +const oldApplicationJs = `/* global $ */ + +// Warn about using the kit in production +if (window.console && window.console.info) { + window.console.info('GOV.UK Prototype Kit - do not use for production') +} + +$(document).ready(function () { + window.GOVUKFrontend.initAll() +}) +` + +const newApplicationJs = `// Add extra JavaScript here + +ready(() => { + // Add JavaScript that needs to be run when the page is loaded +}) +` + +describe('removeKitJsFromApplicationJs', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + + beforeEach(() => { + jest.spyOn(updateUtils, 'getProjectVersion').mockImplementation( + async () => '12.1.1' + ) + + jest.spyOn(updateUtils, 'fetchOriginal').mockImplementation( + async () => oldApplicationJs + ) + }) + + it('replaces application.js if the user has not updated it', async () => { + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => oldApplicationJs + ) + + const mockCopyFile = jest.spyOn(fs, 'copyFile').mockImplementation( + async () => {} + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockCopyFile).toHaveBeenCalledWith( + expect.stringContaining(path.join('update', 'app', 'assets', 'javascripts', 'application.js')), + expect.stringContaining(path.join('app', 'assets', 'javascripts', 'application.js')) + ) + }) + + it('rewrites application.js if the user has added lines to the bottom of the file', async () => { + // theirs + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => oldApplicationJs + '\ncallMyCode()\n' + ) + // ours + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => newApplicationJs + ) + + const mockWriteFile = jest.spyOn(fs, 'writeFile').mockImplementation( + async () => {} + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockWriteFile).toHaveBeenCalledWith( + expect.stringContaining(path.join('app', 'assets', 'javascripts', 'application.js')), + newApplicationJs + '\ncallMyCode()\n', + 'utf8' + + it('does not touch application.js if the user has rewritten it a lot', async () => { + // theirs + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => 'justMyCode()\n' + ) + + const mockWriteFile = jest.spyOn(fs, 'writeFile').mockImplementation( + async () => {} + ) + + const mockCopyFile = jest.spyOn(fs, 'copyFile').mockImplementation( + async () => {} + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockWriteFile).not.toHaveBeenCalled() + expect(mockCopyFile).not.toHaveBeenCalled() + }) +}) diff --git a/lib/update-utils.js b/lib/update-utils.js new file mode 100644 index 0000000000..bac3037100 --- /dev/null +++ b/lib/update-utils.js @@ -0,0 +1,42 @@ +const fs = require('fs').promises +const https = require('https') +const path = require('path') + +const { projectDir } = require('./path-utils') + +async function getProjectVersion () { + return (await fs.readFile(path.join(projectDir, 'VERSION.txt'), 'utf8')).trim() +} + +async function fetchOriginal (version, filePath) { + const remoteUrl = `https://raw.githubusercontent.com/alphagov/govuk-prototype-kit/v${version}/${filePath}` + + let data = '' + return new Promise((resolve, reject) => { + https.get(remoteUrl, (response) => { + let error + + if (response.statusCode !== 200) { + error = new Error(`could not fetch ${remoteUrl}: status code ${response.statusCode}`) + Object.assign(error, response) + response.resume() + reject(error) + } + + response.setEncoding('utf8') + + response.on('data', (chunk) => { + data += chunk + }) + + response.on('end', () => { + resolve(data) + }) + }) + }) +} + +module.exports = { + fetchOriginal, + getProjectVersion +} diff --git a/lib/update-utils.test.js b/lib/update-utils.test.js new file mode 100644 index 0000000000..9c0e1b96c2 --- /dev/null +++ b/lib/update-utils.test.js @@ -0,0 +1,48 @@ +/* eslint-env jest */ + +const fs = require('fs').promises +const https = require('https') +const stream = require('stream') + +const { fetchOriginal, getProjectVersion } = require('./update-utils.js') + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe('fetchOriginal', () => { + it('gets the contents of a file as of version from GitHub', async () => { + const mockHttpsGet = jest.spyOn(https, 'get').mockImplementation((url, callback) => { + const response = new stream.PassThrough() + response.statusCode = 200 + + callback(response) + + response.write('foo\n') + response.write('bar\n') + response.end() + }) + + await expect(fetchOriginal('99.99.99', 'app/views/foo.html')).resolves.toEqual( + 'foo\nbar\n' + ) + expect(mockHttpsGet).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/alphagov/govuk-prototype-kit/v99.99.99/app/views/foo.html', + expect.anything() + ) + }) +}) + +describe('getProjectVersion', () => { + it('reads the VERSION.txt file to get the version number', async () => { + const mockReadFile = jest.spyOn(fs, 'readFile').mockImplementation( + async () => '99.99.99\n' + ) + + await expect(getProjectVersion()).resolves.toEqual('99.99.99') + expect(mockReadFile).toHaveBeenCalledWith( + expect.stringMatching(/.*\/VERSION.txt$/), + 'utf8' + ) + }) +})