Generate Playwright + Cucumber E2E tests for a NRF frontend user journey
You are generating Playwright + Cucumber E2E tests for the Nature Restoration Fund (NRF) frontend service (nrf-frontend, Hapi.js + Nunjucks + GOV.UK Frontend).
Before writing any code, read .ai/coding-rules.md for conventions, patterns, and what to avoid.
NRF (Nature Restoration Fund) is a UK government scheme that allows property developers to calculate and pay an environmental impact levy. The frontend guides developers through a quote journey to estimate their levy.
Key terms:
| Journey | Status | Description |
|---|---|---|
| Quote | Active | Developer calculates levy estimate |
| Apply | Planned | Full application submission |
| Verify | Planned | LPA staff verify RLB details |
These are the actual routes and form field names from the source. Always use these exact names.
| Property | Value |
|---|---|
| Route | GET / |
| Page title | Nature Restoration Fund - Gov.uk |
| Page heading (h1) | Nature Restoration Fund |
| Key element | Start button → /quote/boundary-type |
| Property | Value |
|---|---|
| Route | GET /quote/boundary-type, POST /quote/boundary-type |
| Page heading (h1) | Choose how you would like to show us the boundary of your development |
| Field name | boundaryEntryType |
| Options | draw (radio), upload (radio) |
| Validation | Required |
| Next page | /quote/next (placeholder — map/upload pages not yet wired) |
| Property | Value |
|---|---|
| Route | GET /quote/development-types, POST /quote/development-types |
| Field name | developmentTypes |
| Options | housing (checkbox), other-residential (checkbox) |
| Validation | At least one required |
| Next page | If housing selected → /quote/residential; else → /quote/next |
| Property | Value |
|---|---|
| Route | GET /quote/residential, POST /quote/residential |
| Field name | residentialBuildingCount |
| Input type | Numeric, pattern [0-9]*, width class govuk-input--width-1 |
| Validation | Required |
| Next page | /quote/next (placeholder) |
| Property | Value |
|---|---|
| Route | GET /quote/email, POST /quote/email |
| Field name | email |
| Input type | Email, max 254 characters |
| Validation | Required, valid email format, no spaces, max 254 chars |
| Next page | /quote/next (placeholder) |
| Property | Value |
|---|---|
| Route | GET /quote/upload-boundary, POST /quote/upload-boundary |
| Field name | file |
| Input type | File (multipart form) |
| Accepted formats | .shp, .geojson, .json, .kml |
| Max size | 2MB |
| Validation | Required, file type, file size |
| Next page | TBD |
| Property | Value |
|---|---|
| Route | GET /quote/no-edp |
| Type | Informational — no form |
| Purpose | Shown when no EDP exists for the development area |
| Property | Value |
|---|---|
| Route | GET /quote/upload-received |
| Type | Success/confirmation panel — no form |
| Purpose | Confirmation that boundary file was received |
Use role-based selectors first. These are the standard GOV.UK Frontend patterns:
// Radio buttons
page.getByRole('radio', { name: 'Draw the boundary on a map' })
page.getByRole('radio', { name: 'Upload a file' })
// Checkboxes
page.getByRole('checkbox', { name: 'Housing' })
page.getByRole('checkbox', { name: 'Other residential' })
// Text/email/number inputs — use label text
page.getByLabel('Email address')
page.getByLabel('Number of residential buildings')
// File upload
page.getByLabel('Upload a boundary file')
// Continue/Submit button
page.getByRole('button', { name: 'Continue' })
// Start button on home page
page.getByRole('link', { name: 'Start now' })
// Error summary (GOV.UK pattern)
page.locator('.govuk-error-summary')
page.locator('.govuk-error-message')
// Page heading
page.locator('h1')
// Back link
page.locator('.govuk-back-link')
Always check the actual Nunjucks template in ../nrf-frontend/src/server/<page>/ for exact label text before writing selectors.
ENABLE_DEFRA_ID=false disables authentication for tests. Do not test the OIDC flow unless explicitly asked.For each page, test in this order:
Do not test:
flows/<journey>.md — if it does not exist, stop and ask the user to create it.../nrf-frontend/src/server/<page>/ for exact field names and label text. Always read the current source — never assume from memory.../nrf-frontend/src page.test.js files and ../nrf-backend/src *.test.js files for every AC in scope.test/page-objects/<name>.page.js — extend Page, expose locators as getters and actions as async methods, no assertionstest/support/world.js under this.pageObjectstest/features/<journey>.feature — tag every scenario @smoke and/or @regressiontest/step-definitions/<journey>.steps.js — assertions go here, not in page objectsnpm run test:e2e:debugnpm run lint && npm run format:checkimport { Page } from './page.js'
export class BoundaryTypePage extends Page {
constructor(page, baseUrl) {
super(page, baseUrl)
}
async open() {
await this.goto('/quote/boundary-type')
}
get drawRadio() {
return this.page.getByRole('radio', { name: 'Draw the boundary on a map' })
}
get uploadRadio() {
return this.page.getByRole('radio', { name: 'Upload a file' })
}
get continueButton() {
return this.page.getByRole('button', { name: 'Continue' })
}
get errorSummary() {
return this.page.locator('.govuk-error-summary')
}
async selectBoundaryType(type) {
if (type === 'draw') await this.drawRadio.click()
if (type === 'upload') await this.uploadRadio.click()
}
async continue() {
await this.continueButton.click()
}
}
@smoke @regression
Feature: Boundary type selection
Scenario: Developer selects upload boundary type and continues
Given I am on the boundary type page
When I select "upload"
And I continue
Then I should be on the upload boundary page
Scenario: Developer selects draw boundary type and continues
Given I am on the boundary type page
When I select "draw"
And I continue
Then I should be on the draw boundary page
Validation errors (empty submission, invalid input, error messages) are tested at the nrf-frontend unit/integration level — do not duplicate them as E2E scenarios.
import assert from 'node:assert/strict'
import { Given, When, Then } from '@cucumber/cucumber'
Given('I am on the boundary type page', async function () {
await this.pageObjects.boundaryTypePage.open()
})
When('I select {string}', async function (type) {
await this.pageObjects.boundaryTypePage.selectBoundaryType(type)
})
When('I continue', async function () {
await this.pageObjects.boundaryTypePage.continue()
})
Then('I should be on the upload boundary page', async function () {
await this.pageObjects.uploadBoundaryPage.waitFor()
assert.equal(
await this.page.title(),
'Upload boundary - Nature Restoration Fund - Gov.uk'
)
})