Skip to content

Commit

Permalink
Cucumber tests pass.
Browse files Browse the repository at this point in the history
  • Loading branch information
nataliecarey committed Jan 21, 2024
1 parent b911507 commit 80ad9da
Show file tree
Hide file tree
Showing 36 changed files with 5,814 additions and 2,304 deletions.
24 changes: 24 additions & 0 deletions features/plugins/install-and-uninstall.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@plugins
Feature: Installing and uninstalling plugins

Scenario: Installed - show on installed plugins
When I visit the installed plugins page
Then I should see the plugin "Common Templates" in the list

Scenario: Installed - tag as installed
When I visit the available plugins page
Then I should see the plugin "Common Templates" in the list
And The "Common Templates" plugin should be tagged as "Installed"

Scenario: Uninstalled - hide on installed plugins
Given I uninstall the "installed:@govuk-prototype-kit/common-templates" plugin
And I wait for the uninstall to complete
When I visit the installed plugins page
Then I should not see the plugin "Common Templates" in the list

Scenario: Uninstalled - don't tag as installed
Given I uninstall the "installed:@govuk-prototype-kit/common-templates" plugin
And I wait for the uninstall to complete
When I visit the available plugins page
Then I should see the plugin "Common Templates" in the list
And The "Common Templates" plugin should not be tagged as "Installed"
19 changes: 19 additions & 0 deletions features/plugins/update.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@plugins
Feature: Handle plugin update

Scenario: When a dependency is now required
Given I have a the required SCSS to avoid plugins breaking when GOV.UK Frontend is uninstalled
And I install the "npm:@govuk-prototype-kit/common-templates:1.1.1" plugin
And I wait for the uninstall to complete
And I uninstall the "govuk-frontend" plugin using the command line
And I visit the installed plugins page
And I should not see the plugin "GOV.UK Frontend" in the list
When I update the "installed:@govuk-prototype-kit/common-templates" plugin
And I should be informed that "GOV.UK Frontend" will also be installed
And I continue with the update
And I wait for the update to complete
And I visit the installed plugins page
Then I should see the plugin "Common Templates" in the list
And I should see the plugin "GOV.UK Frontend" in the list


Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 123 additions & 0 deletions features/support/global/DefaultCustomWorld.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// CustomWorld.js
const { World } = require('@cucumber/cucumber')
const seleniumWebdriver = require('selenium-webdriver')
const chrome = require('selenium-webdriver/chrome')
const firefox = require('selenium-webdriver/firefox')
const { startKit, resetState } = require('./initKit')
const { verboseLogging, browserName, browserWidth, browserHeight, browserHeadless, fnRetryDelay, fnRetries } = require('./config')
const verboseLog = verboseLogging ? console.log : () => {}
const sharedState = {}

class CustomWorld extends World {
driver = null

async init () {
await Promise.all([
this.startKitIfNotRunning(),
this.getDriver()
])
}

async getDriver () {
const setOptions = (obj) => {
let objUpdated = obj
if (browserHeadless) {
objUpdated = objUpdated.headless()
}
objUpdated.windowSize({
width: browserWidth,
height: browserHeight
})
return objUpdated
}
if (!sharedState.driver) {
sharedState.driver = await new seleniumWebdriver.Builder().forBrowser(browserName)
.setChromeOptions(setOptions(new chrome.Options()))
.setFirefoxOptions(setOptions(new firefox.Options()))
.build()

await sharedState.driver.manage().setTimeouts({ implicit: 2000 })
}
this.driver = sharedState.driver
return this.driver
}

async startKitIfNotRunning (config) {
if (!sharedState.runningKit) {
this.runningKit = sharedState.runningKit = await this.retryOnFailure(async ({ attemptNumber, previousError }) => {
verboseLog('Previous error', previousError)
verboseLog('starting kit, attempt [%s]', attemptNumber)
return await startKit(config)
})
console.log('Kit running at:', this.runningKit.directory)
}
this.runningKit = sharedState.runningKit
return this.runningKit
}

async resetState () {
if (!sharedState.runningKit) {
return
}
await resetState(sharedState.runningKit)
}

static async CleanupEverything () {
if (sharedState.driver) {
sharedState.driver.quit()
}
if (sharedState.runningKit) {
sharedState.runningKit.close()
}
}

async wait (millis) {
return new Promise((resolve, reject) => {
setTimeout(resolve, millis)
})
}

async retryOnFailure (fn) {
const delayBetweenRetries = fnRetryDelay
let attemptNumber = 1
let retries = fnRetries

let previousError

while (true) {
try {
return await fn({
attemptNumber: attemptNumber++,
previousError
})
} catch (e) {
if (--retries > 0) {
previousError = e
await this.wait(delayBetweenRetries)
} else {
throw e
}
}
}
}

async visit (relativeUrl, checkContent = async () => {}) {
if (!sharedState.driver) {
throw new Error(`Can't visit the URL ${relativeUrl} because WebDriver - fix this by running .init`)
}
if (!sharedState.runningKit) {
throw new Error(`Can't visit the URL ${relativeUrl} because the kit isn't running - fix this by running .startKitIfNotRunning or .startKitAndReplaceIfRunning`)
}
const url = `${sharedState.runningKit.serverAddress}${relativeUrl}`
await this.retryOnFailure(async ({ attemptNumber, previousError }) => {
verboseLog('previousError ' + previousError)
verboseLog('loading [%s] attempt [%s], time [%s]', url, attemptNumber++, new Date().toISOString())
await sharedState.driver.get(url)
await checkContent(attemptNumber)
})
}
}

module.exports = {
CustomWorld
}
30 changes: 30 additions & 0 deletions features/support/global/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const os = require('os')
const verboseLogging = process.env.NPI_CUKE_VERBOSE === 'true'
const shortTimeout = Number(process.env.NPI_CUKE_SHORT_TIMEOUT || '3000')
const mediumTimeout = Number(process.env.NPI_CUKE_MEDIUM_TIMEOUT || '10000')
const longTimeout = Number(process.env.NPI_CUKE_LONG_TIMEOUT || '60000')
const fnRetries = Number(process.env.NPI_CUKE_FN_RETRIES || '10')
const fnRetryDelay = Number(process.env.NPI_CUKE_FN_RETRY_DELAY || '500')
const startingPort = Number(process.env.NPI_CUKE_STARTING_PORT || '18888')
const browserWidth = Number(process.env.NPI_CUKE_BROWSER_WIDTH || '1024')
const browserHeight = Number(process.env.NPI_CUKE_BROWSER_HEIGHT || '768')
const browserHeadless = process.env.NPI_CUKE_BROWSER_HEADLESS !== 'false'
const browserName = process.env.NPI_CUKE_BROWSER_NAME || 'chrome'
const screenshotOnFailure = process.env.NPI_CUKE_SCREENSHOT_ON_FAILURE !== false
const baseDir = process.env.NPI_CUKE_BASE_DIR || os.tmpdir()

module.exports = {
verboseLogging,
shortTimeout,
mediumTimeout,
longTimeout,
startingPort,
browserName,
browserWidth,
browserHeight,
browserHeadless,
screenshotOnFailure,
baseDir,
fnRetries,
fnRetryDelay
}
144 changes: 144 additions & 0 deletions features/support/global/initKit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const { spawn: crossSpawn } = require('cross-spawn')
const cp = require('child_process')
const path = require('path')
const events = require('events')

const { startingPort, verboseLogging, baseDir } = require('./config')
const fse = require('fs-extra')

let nextPort = startingPort

function addInitialGitCommitToConfig (config) {
return new Promise((resolve, reject) => {
cp.exec('git log --pretty="%H"', { cwd: config.directory }, (err, stdout, stderr) => {
if (err) {
reject(err)
}
const result = (stdout || '').split(/[\n\r]+/)[0]
if (result && result.trim()) {
resolve({ ...config, initialCommit: result.trim() })
} else {
resolve({ ...config })
}
})
})
}

function resetState (config) {
if (!config?.initialCommit) {
throw new Error('It\'s not possible to reset the state as no innitial commit exists in the config.')
}
return new Promise((resolve, reject) => {
cp.exec(`git reset --hard ${config.initialCommit} && npm prune && npm install`, { cwd: config.directory }, (err) => {
if (err) {
reject(err)
} else {
config.kitStartedEventEmitter.on('started', () => {
resolve()
})
}
})
})
}

function initKit (config) {
if (config.directory) {
return Promise.resolve({ startCommand: 'npm run dev', ...config })
}
const tmpDir = config.directory || path.join(baseDir, new Date().getTime() + '_' + ('' + Math.random()).split('.')[1])
const rootDir = path.resolve(__dirname, '../../..')

return new Promise((resolve, reject) => {
let startCommand
const initProcess = crossSpawn('npx', [`now-prototype-it-govuk@${rootDir}`, 'create', `--version=${rootDir}`, tmpDir], { cwd: rootDir })
initProcess.stderr.on('data', (data) => console.warn('[stderr]', data.toString()))
initProcess.stdout.on('data', (data) => {
const str = data.toString()
if (verboseLogging) {
console.log(str)
}
// eslint-disable-next-line no-unused-vars
const [_, command] = str.split('To run your prototype:')
if (command && command.trim()) {
startCommand = command.trim()
}
})
initProcess.on('error', (error) => {
reject(error)
})

initProcess.on('close', code => {
if (startCommand) {
resolve({ ...config, startCommand, directory: tmpDir, kitStartedEventEmitter: new events.EventEmitter() })
} else {
reject(new Error('initialisation failed'))
}
})
})
}

async function setUsageDataPermission(config) {
// const filePath = path.join(config.directory, 'usage-data-config.json')
// console.log('Writing usage data file', filePath)
// await fse.writeJson(filePath, { collectUsageData: false })
// console.log('Written usage data file', filePath)
return config
}

function runKit (config) {
let hasReturned = false

return new Promise((resolve, reject) => {
const [command, ...args] = config.startCommand.split(' ')
const kitProcess = crossSpawn(command, args, {
cwd: config.directory,
detached: true,
env: {
...process.env,
PORT: nextPort++,
GPK_NO_STDIN: 'true'
}
})

kitProcess.stderr.on('data', (data) => console.warn('[stderr]', data.toString()))
kitProcess.stdout.on('data', (data) => {
const str = data.toString()
const regExpMatchArray = str.match(/(http:\/\/localhost:\d+)/)
if (regExpMatchArray && regExpMatchArray[1]) {
config.kitStartedEventEmitter.emit('started')
if (hasReturned) {
return
}
hasReturned = true
resolve({
...config,
serverAddress: regExpMatchArray[1].trim(),
close: () => {
kitProcess.stdin.pause()
try {
process.kill(-kitProcess.pid, 'SIGTERM')
} catch (err) {
console.error('error while stopping process')
console.error(err)
}
}
})
}
})
kitProcess.on('error', (error) => {
reject(error)
})

kitProcess.on('close', code => {
reject(new Error('initialisation failed'))
})
})
}

module.exports = {
startKit: (config = {}) => initKit(config)
.then(setUsageDataPermission)
.then(addInitialGitCommitToConfig)
.then(runKit),
resetState
}
31 changes: 31 additions & 0 deletions features/support/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// setup.js
const { Status, Before, After, setWorldConstructor, AfterAll } = require('@cucumber/cucumber')
const { CustomWorld } = require('./global/DefaultCustomWorld')
const fsp = require('fs/promises')
const fse = require('fs-extra')
const path = require('path')
const { longTimeout, screenshotOnFailure } = require('./global/config')

setWorldConstructor(CustomWorld)

Before({ timeout: longTimeout }, async function (scenario) {
await this.init(scenario)
await this.resetState()
})

After({ timeout: longTimeout }, async function (testCase) {
if (testCase.result.status === Status.FAILED && screenshotOnFailure) {
const screenshot = await this.driver.takeScreenshot()
const filePath = path.join(__dirname, '..', 'screenshots', `${testCase.pickle.name}${new Date().getTime()}.png`)
await fse.ensureDir(path.dirname(filePath))
await fsp.writeFile(filePath, screenshot, 'base64')
this.attach(screenshot, {
mediaType: 'base64:image/png',
fileName: 'screenshot.png'
})
}
})

AfterAll(async function () {
await CustomWorld.CleanupEverything()
})
Loading

0 comments on commit 80ad9da

Please sign in to comment.