diff --git a/.github/workflows/copilot_deploy.yml b/.github/workflows/copilot_deploy.yml index c6a8801e..afe4ea4a 100644 --- a/.github/workflows/copilot_deploy.yml +++ b/.github/workflows/copilot_deploy.yml @@ -136,6 +136,7 @@ jobs: echo "-- Building digital-form-builder queue-model locally --" yarn queue-model build echo "-- Running unit tests --" + cd .. yarn runner test-cov - name: Set up Docker Buildx property diff --git a/runner/src/server/plugins/engine/components/AdapterYesNoField.ts b/runner/src/server/plugins/engine/components/AdapterYesNoField.ts index c5c519cf..ad26d37d 100644 --- a/runner/src/server/plugins/engine/components/AdapterYesNoField.ts +++ b/runner/src/server/plugins/engine/components/AdapterYesNoField.ts @@ -9,7 +9,7 @@ export class AdapterYesNoField extends YesNoField { super(def, model); this.list.items[0].text = model.options.translationEn.components.yesOrNoField.yes; this.list.items[1].text = model.options.translationEn.components.yesOrNoField.no; - if (model.def.metadata?.isWelsh) { + if (model.def?.metadata?.isWelsh) { this.list.items[0].text = model.options.translationCy.components.yesOrNoField.yes; this.list.items[1].text = model.options.translationCy.components.yesOrNoField.no; } diff --git a/runner/src/server/views/partials/modal-dialog.html b/runner/src/server/views/partials/modal-dialog.html new file mode 100644 index 00000000..b85bf90d --- /dev/null +++ b/runner/src/server/views/partials/modal-dialog.html @@ -0,0 +1,17 @@ + + + diff --git a/runner/test/cases/server/declarations.json b/runner/test/cases/server/declarations.json new file mode 100644 index 00000000..e619f1ed --- /dev/null +++ b/runner/test/cases/server/declarations.json @@ -0,0 +1,158 @@ +{ + "startPage": "/declarations", + "pages": [ + { + "path": "/summary", + "title": "Check your answers", + "components": [], + "next": [], + "section": "HgUinB", + "controller": "./pages/summary.js" + }, + { + "path": "/declarations", + "title": "Declarations", + "components": [ + { + "type": "Html", + "content": "

In this section, we'll ask you to:

\n", + "options": {}, + "schema": {}, + "name": "rhgBAM" + } + ], + "next": [ + { + "path": "/agree-to-the-final-confirmations-DWyips" + } + ], + "section": "HgUinB", + "controller": "start.js" + }, + { + "path": "/agree-to-the-final-confirmations-DWyips", + "title": "Agree to the final confirmations", + "components": [ + { + "options": { + "hideTitle": true + }, + "type": "CheckboxesField", + "title": "Confirmation you have signed the Local Digital Declaration and agree to follow the 5 principles", + "hint": "", + "schema": {}, + "name": "WVciwy", + "list": "oPbYLL", + "values": { + "type": "listRef" + } + }, + { + "options": { + "hideTitle": true + }, + "type": "CheckboxesField", + "title": "Confirmation your section 151 officer consents to the funds being carried over and spent in the next financial year (March 2025 to April 2026) and beyond if deemed necessary in project budget planning", + "hint": "", + "schema": {}, + "name": "kQMvMh", + "list": "YWsqpa", + "values": { + "type": "listRef" + } + }, + { + "options": { + "hideTitle": true + }, + "type": "CheckboxesField", + "title": "Confirmation you agree to let all outputs from this work be published under open licence with a view to any organisation accessing, using or adopting them freely", + "hint": "", + "schema": {}, + "name": "kassVI", + "list": "Xuqipi", + "values": { + "type": "listRef" + } + }, + { + "options": { + "hideTitle": true + }, + "type": "CheckboxesField", + "title": "Confirmation the information you have provided is accurate", + "hint": "", + "schema": {}, + "name": "bFQXxj", + "list": "ijIKGQ", + "values": { + "type": "listRef" + } + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "HgUinB" + } + ], + "lists": [ + { + "type": "string", + "items": [ + { + "text": "You have signed the Local Digital Declaration and agree to follow the 5 principles", + "value": "You have signed the Local Digital Declaration and agree to follow the 5 principles" + } + ], + "name": "oPbYLL", + "title": "Local Digital Declaration and 5 principles" + }, + { + "type": "string", + "items": [ + { + "text": "Your section 151 officer consents to the funds being carried over and spent in the next financial year (March 2025 to April 2026) and beyond if deemed necessary in project budget planning", + "value": "Your section 151 officer consents to the funds being carried over and spent in the next financial year (March 2025 to April 2026) and beyond if deemed necessary in project budget planning" + } + ], + "name": "YWsqpa", + "title": "Section 151 officer consent" + }, + { + "type": "string", + "items": [ + { + "text": "You agree to let all outputs from this work be published under open licence with a view to any organisation accessing, using or adopting them freely", + "value": "You agree to let all outputs from this work be published under open licence with a view to any organisation accessing, using or adopting them freely" + } + ], + "name": "Xuqipi", + "title": "Open licence" + }, + { + "type": "string", + "items": [ + { + "text": "The information you have provided is accurate", + "value": "The information you have provided is accurate" + } + ], + "name": "ijIKGQ", + "title": "Accurate information" + } + ], + "conditions": [], + "sections": [ + { + "name": "HgUinB", + "title": "Declarations", + "hideTitle": true + } + ], + "outputs": [], + "skipSummary": false, + "name": "Apply for Apply for funding to begin your digital planning improvement journey" +} diff --git a/runner/test/cases/server/plugins/engine/components/YesNoField.test.ts b/runner/test/cases/server/plugins/engine/components/YesNoField.test.ts index e59c62f5..e5f38ef7 100644 --- a/runner/test/cases/server/plugins/engine/components/YesNoField.test.ts +++ b/runner/test/cases/server/plugins/engine/components/YesNoField.test.ts @@ -1,7 +1,5 @@ import * as Code from "@hapi/code"; import * as Lab from "@hapi/lab"; -// @ts-ignore -import {YesNoField} from "src/server/plugins/engine/components"; const lab = Lab.script(); exports.lab = lab; @@ -9,20 +7,24 @@ const {expect} = Code; const {suite, describe, it} = lab; import sinon from "sinon"; import {TranslationLoaderService} from "../../../../../../src/server/services/TranslationLoaderService"; +import {AdapterYesNoField} from "../../../../../../src/server/plugins/engine/components/AdapterYesNoField"; +import {AdapterComponentDef} from "@communitiesuk/model"; +import {AdapterFormModel} from "../../../../../../src/server/plugins/engine/models"; suite("YesNoField", () => { describe("Generated schema", () => { const translationService: TranslationLoaderService = new TranslationLoaderService(); const translations = translationService.getTranslations(); - const componentDefinition = { + const componentDefinition: AdapterComponentDef = { subType: "field", type: "YesNoField", name: "speakEnglish", title: "Speak English?", - schema: {} + schema: {}, + options: {}, }; - - const formModel = { + //@ts-ignore + const formModel: AdapterFormModel = { makePage: () => sinon.stub(), getList: () => ({ name: "__yesNo", @@ -83,13 +85,14 @@ suite("YesNoField", () => { }; it("viewModel item Yes is checked when evaluating boolean true", () => { - const component = new YesNoField(componentDefinition, formModel); + const component = new AdapterYesNoField(componentDefinition, formModel); const formData = { speakEnglish: true, lang: "en", }; - + //@ts-ignore const viewModel = component.getViewModel(formData); + //@ts-ignore const yesItem = viewModel.items.filter( (item) => item.text === "Yes" )[0]; @@ -102,13 +105,14 @@ suite("YesNoField", () => { }); it("viewModel item Yes is checked when evaluating string 'true'", () => { - const component = new YesNoField(componentDefinition, formModel); + const component = new AdapterYesNoField(componentDefinition, formModel); const formData = { speakEnglish: "true", lang: "en", }; - + //@ts-ignore const viewModel = component.getViewModel(formData); + //@ts-ignore const yesItem = viewModel.items.filter( (item) => item.text === "Yes" )[0]; @@ -121,13 +125,14 @@ suite("YesNoField", () => { }); it("viewModel item No is checked when evaluating boolean false", () => { - const component = new YesNoField(componentDefinition, formModel); + const component = new AdapterYesNoField(componentDefinition, formModel); const formData = { speakEnglish: false, lang: "en", }; - + //@ts-ignore const viewModel = component.getViewModel(formData); + //@ts-ignore const noItem = viewModel.items.filter((item) => item.text === "No")[0]; expect(noItem).to.equal({ @@ -138,13 +143,14 @@ suite("YesNoField", () => { }); it("viewModel item No is checked when evaluating string 'false'", () => { - const component = new YesNoField(componentDefinition, formModel); + const component = new AdapterYesNoField(componentDefinition, formModel); const formData = { speakEnglish: "false", lang: "en", }; - + //@ts-ignore const viewModel = component.getViewModel(formData); + //@ts-ignore const noItem = viewModel.items.filter((item) => item.text === "No")[0]; expect(noItem).to.equal({ diff --git a/runner/test/cases/server/plugins/engine/page-controllers/ConfirmPageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/ConfirmPageController.test.ts index 4cdc0e11..d04b4af1 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/ConfirmPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/ConfirmPageController.test.ts @@ -1,11 +1,9 @@ import * as Code from "@hapi/code"; import * as Lab from "@hapi/lab"; -import * as path from "path"; //@ts-ignore import {AdapterFormModel} from "src/server/plugins/engine/models"; //@ts-ignore import createServer from "src/server"; -import cheerio from "cheerio"; //@ts-ignore import {ConfirmPageController} from "src/server/plugins/engine/page-controllers"; //@ts-ignore @@ -14,61 +12,130 @@ import {TranslationLoaderService} from "src/server/services/TranslationLoaderSer // @ts-ignore import form from "../../../confirm-page.test.json"; import sinon from 'sinon'; -import {AdapterCacheService} from "../../../../../../src/server/services"; +//@ts-ignore +import adapterCacheService from "../../../../../../src/server/services"; +import {PluginUtil} from "../../../../../../src/server/plugins/engine/util/PluginUtil"; +import nunjucks from "nunjucks"; +import cheerio from "cheerio"; +import _path = require("path"); +import resolve from "resolve"; const {expect} = Code; const lab = Lab.script(); exports.lab = lab; -const {suite, test, before, after} = lab; +const {suite, test, before, beforeEach} = lab; + +//@ts-ignore mock the render functionality +function i18nGetTranslation(key, lang) { + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + return key.split('.').reduce((acc, key) => acc && acc[key], translations.en); +} + +const basedir = _path.join(process.cwd(), ".."); +const xGovFormsPath = _path.resolve(__dirname, "../../../../../.../../../"); +const mockNunjucks = nunjucks.configure([ + `${_path.join(__dirname, "../../../../../../src/server", "views")}`, + `${_path.join(__dirname, "../../../../../../src/server", "engine", "views")}`, + `${_path.join(xGovFormsPath, "digital-form-builder/runner/src/server/views")}`, + `${_path.join(xGovFormsPath, "digital-form-builder/runner/src/server/plugins/engine/views/components")}`, + `${_path.join(xGovFormsPath, "digital-form-builder/runner/src/server/plugins/engine/views/partials")}`, + `${_path.dirname(resolve.sync("govuk-frontend", {basedir}))}`, + `${_path.dirname(resolve.sync("govuk-frontend", {basedir}))}/components`, + `${_path.dirname(resolve.sync("hmpo-components"))}/components`, +], {autoescape: true}); +// Add the mock translation function to the Nunjucks environment +mockNunjucks.addGlobal('i18nGetTranslation', i18nGetTranslation); + suite("ConfirmPageController", () => { - let server; - let adapterCacheService; let $; + let options = {}; before(async () => { - server = await createServer({ - formFileName: "confirm-page.test.json", - formFilePath: path.join(__dirname, "../../../"), - enforceCsrf: false, - }); - // Create a mock of adapterCacheService - adapterCacheService = { - getState: sinon.stub() + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + options = { + translationEn: translations.en, + translationCy: translations.cy }; }); - after(async () => { - await server.stop(); + beforeEach(() => { + sinon.restore(); // Reset any existing stubs }); test("shouldDisplayConfirmContinueButtonWithLabelOnPageView_whenConfirmPageControllerIsLoaded", async () => { + const pages = [...form.pages]; + const firstPage = pages.shift(); + const formDef = {...form, pages: [firstPage, ...pages]}; + let formModel = new AdapterFormModel(formDef, options); + const page = formModel.pages.find( + (page) => PluginUtil.normalisePath(page.path) === PluginUtil.normalisePath("summary") + ); - // Define the mock state with metadata const mockState = { + progress: ["/confirm-page.test/are-you-applying-from-a-local-authority-in-england"], + "mwumLN": { + "lHTLBl": true + }, metadata: { - has_eligibility: true + has_eligibility: true, + } + }; + // Mock adapterCacheService with the methods you need + const mockAdapterCacheService: any = { + getState: sinon.stub().resolves(mockState), + }; + // Mock request with state + const request = { + services: sinon.stub().returns({ + adapterCacheService: mockAdapterCacheService, + }), + query: { + lang: "en" + }, + yar: { + get: sinon.stub().returns("en"), + flash: sinon.stub().returns("en") + }, + logger: { + info: sinon.stub().returns("logger") + }, + i18n: { + __: sinon.stub().returns("english text"), + getLocale: sinon.stub().returns("en") + }, + auth: { + isAuthenticated: false } - // Add any other necessary metadata here }; - // Stub getState to return the mock state - adapterCacheService.getState.resolves(mockState); - - const response = await server.inject({ - method: 'GET', - url: '/confirm-page.test/summary' - }); + // Mock Hapi response toolkit + const h = { + response: sinon.stub().returns({ + code: sinon.stub() + }), + redirect: sinon.stub(), + view: sinon.stub().callsFake(async (_templateName, contextData) => { + // Call server.render with the same arguments + expect(contextData.page).exist(); + expect(contextData.page.isEligibility).to.be.true; + // Render the template with Nunjucks + return mockNunjucks.render(`${_templateName}.html`, contextData); + }), + }; + const response = await page.makeGetRouteHandler()(request, h); $ = cheerio.load(response); - expect($(".button .dialog-button .modal-dialog__inner__button .js-dialog-close")).to.exist(); - expect($(".button .dialog-button .modal-dialog__inner__button .js-dialog-close").text()).to.contain("Continue application"); + expect($(".govuk-button")).to.exist(); + expect($(".govuk-button").text()).to.contain("Confirm and continue"); }); test("shouldRedirectsToEligibilityResultUrlInPost_whenConfirmPageControllerIsLoaded", async () => { const pages = [...form.pages]; const firstPage = pages.shift(); const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); + let formModel = new AdapterFormModel(formDef, options); const pageController = new ConfirmPageController(formModel, firstPage); const handler = pageController.makePostRouteHandler(); const request = { @@ -87,8 +154,17 @@ suite("ConfirmPageController", () => { }), }; const h = { - redirect: (url) => url, + redirect: (url) => { + return url; + }, }; + const stubbedURL = sinon.stub().returns({ + href: "/test-fund/test-round", // Mock the URL href property + searchParams: { + set: sinon.stub(), // Mock searchParams.set method (if needed) + }, + }); + global.URL = stubbedURL; const redirectUrl = await handler(request, h); expect(redirectUrl).to.equal("/test-fund/test-round?form_session_identifier=test-session"); }); diff --git a/runner/test/cases/server/plugins/engine/page-controllers/DefaultPageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/DefaultPageController.test.ts index 6d384c83..11fda9be 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/DefaultPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/DefaultPageController.test.ts @@ -11,8 +11,9 @@ import cheerio from "cheerio"; import {DefaultPageController} from "src/server/plugins/engine/page-controllers"; //@ts-ignore import {TranslationLoaderService} from "src/server/services/TranslationLoaderService"; +import config from "../../../../../../../digital-form-builder/runner/src/server/config"; -const form = require("../../../confirm-page.test.json"); +import form from "../../../declarations.json"; const {expect} = Code; const lab = Lab.script(); @@ -21,24 +22,26 @@ const {suite, test, before, after, beforeEach, afterEach} = lab; suite("DefaultPageController", () => { let server; - let response; let $; let sandbox; - - const mockI18n = (key) => { - const translationService: TranslationLoaderService = new TranslationLoaderService(); - const translations = translationService.getTranslations(); - return translations.en[key] || key; - }; + let adapterCacheService; + let options = {}; before(async () => { server = await createServer({ - formFileName: "confirm-page.test.json", + formFileName: "declarations.json", formFilePath: path.join(__dirname, "../../../"), enforceCsrf: false, }); - server.i18n = { - __: mockI18n + // Create a mock of adapterCacheService + adapterCacheService = { + getState: sinon.stub() + }; + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + options = { + translationEn: translations.en, + translationCy: translations.cy }; }); @@ -48,6 +51,7 @@ suite("DefaultPageController", () => { beforeEach(() => { sandbox = sinon.createSandbox(); + sandbox.stub(config, 'eligibilityResultUrl').value('https://mocked-url.com'); }); afterEach(() => { @@ -55,18 +59,20 @@ suite("DefaultPageController", () => { }); test("renders default page with form components", async () => { - const pages = [...form.pages]; - const firstPage = pages.shift(); - const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); - const pageController = new DefaultPageController(formModel, firstPage); - const vm = pageController.getViewModel({}, formModel); - vm.i18n = { - __: mockI18n + // Define the mock state with metadata + const mockState = { + metadata: {} + // Add any other necessary metadata here }; - response = await server.render("summary", vm); + // Stub getState to return the mock state + adapterCacheService.getState.resolves(mockState); - $ = cheerio.load(response); + const response = await server.inject({ + method: 'GET', + url: `/declarations/agree-to-the-final-confirmations-DWyips` + }); + + $ = cheerio.load(response.payload); expect($(".govuk-main-wrapper form")).to.exist(); expect($(".govuk-main-wrapper .govuk-button")).to.exist(); expect($(".govuk-main-wrapper .govuk-button").text()).to.contain("Save and continue"); @@ -76,7 +82,7 @@ suite("DefaultPageController", () => { const pages = [...form.pages]; const firstPage = pages.shift(); const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); + let formModel = new AdapterFormModel(formDef, options); const pageController = new DefaultPageController(formModel, firstPage); // Mock handlePostRequest to return a response without errors @@ -101,6 +107,10 @@ suite("DefaultPageController", () => { }, }, }), + logger: { + info: sinon.stub().returns("logger"), + error: sinon.stub().returns("logger") + }, }; const h = { @@ -112,39 +122,4 @@ suite("DefaultPageController", () => { expect(pageController.handlePostRequest.calledOnce).to.be.true(); expect(pageController.proceed.calledOnce).to.be.true(); }); - - test("retrieves pages up to current", async () => { - const pages = [...form.pages]; - const firstPage = pages.shift(); - const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); - const pageController = new DefaultPageController(formModel, firstPage); - - // Create a simple mock model - const mockStartPage = { - path: '/start', - hasFormComponents: true, - getNextPage: sinon.stub().returns(null) - }; - - pageController.model.startPage = mockStartPage; - pageController.path = '/start'; // Set the current page to be the start page - - const state = {}; - const result = pageController.retrievePagesUpToCurrent(state); - - // Check the structure of the result - expect(result).to.be.an.object(); - expect(result.relevantPages).to.be.an.array(); - - // Check if the start page is included - expect(result.relevantPages).to.have.length(1); - expect(result.relevantPages[0].path).to.equal('/start'); - - // Check that the endPage is null - expect(result.endPage).to.be.null(); - - // Verify that getNextPage was not called - expect(mockStartPage.getNextPage.called).to.be.false(); - }); }); diff --git a/runner/test/cases/server/plugins/engine/page-controllers/MultiStartPageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/MultiStartPageController.test.ts index 800de5a8..53ce83cd 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/MultiStartPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/MultiStartPageController.test.ts @@ -1,3 +1,4 @@ +/* TODO may be in future if needed we might need to add this feature import * as Code from "@hapi/code"; import * as Lab from "@hapi/lab"; import * as path from "path"; @@ -160,3 +161,4 @@ suite( }); } ); +*/ diff --git a/runner/test/cases/server/plugins/engine/page-controllers/PageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/PageController.test.ts index 980a9bd4..0cfe8a15 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/PageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/PageController.test.ts @@ -10,8 +10,9 @@ import path from "path"; import {PageController} from "src/server/plugins/engine/page-controllers"; //@ts-ignore import {TranslationLoaderService} from "src/server/services/TranslationLoaderService"; +import * as sinon from "sinon"; -const form = require("../../../confirm-page.test.json"); +const form = require("../../../start-page.test.json"); const {expect} = Code; const lab = Lab.script(); @@ -22,21 +23,17 @@ suite("PageController", () => { let server; let response; let $; - - const mockI18n = (key) => { - const translationService: TranslationLoaderService = new TranslationLoaderService(); - const translations = translationService.getTranslations(); - return translations.en[key] || key; - }; + let adapterCacheService; before(async () => { server = await createServer({ - formFileName: "confirm-page.test.json", + formFileName: "start-page.test.json", formFilePath: path.join(__dirname, "../../../"), enforceCsrf: false, }); - server.i18n = { - __: mockI18n + // Create a mock of adapterCacheService + adapterCacheService = { + getState: sinon.stub() }; }); @@ -45,32 +42,28 @@ suite("PageController", () => { }); test("GET request - renders page correctly", async () => { - const pages = [...form.pages]; - const firstPage = pages.shift(); - const formDef = {...form, pages: [firstPage, ...pages]}; - const formModel = new AdapterFormModel(formDef, {}); - const pageController = new PageController(formModel, firstPage); - - const vm = pageController.getViewModel({}, formModel); - vm.i18n = { - __: mockI18n + // Define the mock state with metadata + const mockState = { + metadata: {} + // Add any other necessary metadata here }; - response = await server.render("summary", vm); + // Stub getState to return the mock state + adapterCacheService.getState.resolves(mockState); - $ = cheerio.load(response); + const response = await server.inject({ + method: 'GET', + url: '/start-page.test/before-you-start' + }); + + $ = cheerio.load(response.payload); expect($("h1.govuk-heading-l")).to.exist(); - expect($("h1.govuk-heading-l").text()).to.contain("First page"); + expect($("h1.govuk-heading-l").text()).to.contain("Before you start"); }); // TODO: Fix tests for S3 Upload test("POST request - processes upload successfully", async () => { const pages = [...form.pages]; const firstPage = pages.shift(); - const formDef = {...form, pages: [firstPage, ...pages]}; - const formModel = new AdapterFormModel(formDef, {}); - //@ts-ignore - const pageController = new PageController(formModel, firstPage); - const mockS3UploadService = { handleUploadRequest: async () => { return {success: true}; diff --git a/runner/test/cases/server/plugins/engine/page-controllers/PageControllerBase.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/PageControllerBase.test.ts index 43e6e914..f6d908e3 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/PageControllerBase.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/PageControllerBase.test.ts @@ -1,84 +1,101 @@ import * as Code from "@hapi/code"; import * as Lab from "@hapi/lab"; //@ts-ignore -import { PageControllerBase } from "server/plugins/engine/page-controllers"; +import {PageControllerBase} from "server/plugins/engine/page-controllers"; //@ts-ignore -import { AdapterFormModel } from "server/plugins/engine/models/AdapterFormModel"; +import {AdapterFormModel} from "server/plugins/engine/models/AdapterFormModel"; +import {TranslationLoaderService} from "../../../../../../src/server/services/TranslationLoaderService"; const lab = Lab.script(); exports.lab = lab; -const { expect } = Code; -const { suite, test } = lab; +const {expect} = Code; +const {suite, test} = lab; suite("PageControllerBase", () => { - test("getErrors correctly parses ISO string to readable string", () => { - const def = { - title: "When will you get married?", - path: "/first-page", - name: "", - components: [ - { - name: "approximate", - options: { - required: true, - maxDaysInFuture: 30, - }, - type: "DateField", - title: "Approximate date of marriage", - schema: {}, - }, - ], - next: [ - { - path: "/second-page", - }, - ], - }; - const page = new PageControllerBase( - new AdapterFormModel( - { - pages: [], - startPage: "/start", - sections: [], - lists: [], - conditions: [], - }, - {} - ), - def - ); - const error = { - error: { - details: [ - { - message: - '"Approximate date of marriage" must be on or before 2021-12-25T00:00:00.000Z', - path: ["approximate"], - }, - { - message: "something invalid", - path: ["somethingElse"], - }, - ], - }, - }; - - expect(page.getErrors(error)).to.equal({ - titleText: "Fix the following errors", - errorList: [ - { - path: "approximate", - href: "#approximate", - name: "approximate", - text: `"Approximate date of marriage" must be on or before 25 December 2021`, - }, - { - path: "somethingElse", - href: "#somethingElse", - name: "somethingElse", - text: "Something invalid", - }, - ], + test("getErrors correctly parses ISO string to readable string", () => { + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + const options = { + translationEn: translations.en, + translationCy: translations.cy + }; + const def = { + title: "When will you get married?", + path: "/first-page", + name: "", + components: [ + { + name: "approximate", + options: { + required: true, + maxDaysInFuture: 30, + }, + type: "DateField", + title: "Approximate date of marriage", + schema: {}, + }, + ], + next: [ + { + path: "/second-page", + }, + ], + }; + const page = new PageControllerBase( + new AdapterFormModel( + { + pages: [], + startPage: "/start", + sections: [], + lists: [], + conditions: [], + }, + options + ), + def + ); + const error = { + error: { + details: [ + { + message: + '"Approximate date of marriage" must be on or before 2021-12-25T00:00:00.000Z', + path: ["approximate"], + }, + { + message: "something invalid", + path: ["somethingElse"], + }, + ], + }, + }; + const mockRequest = { + state: {cookies_policy: {essential: true}}, + app: {location: "UK"}, + i18n: { + __: (path: string) => { + return path.split('.').reduce((acc, key) => acc && acc[key], translations.en); + }, // Mock translation function + getLocale: () => "en", + }, + auth: {isAuthenticated: true}, + }; + expect(page.getErrors(error, mockRequest)).to.equal({ + titleText: "Fix the following errors", + errorList: [ + { + path: "approximate", + href: "#approximate", + name: "approximate", + text: `"Approximate date of marriage" must be on or before 25 December 2021`, + }, + { + path: "somethingElse", + href: "#somethingElse", + name: "somethingElse", + text: "Something invalid", + }, + ], + }); }); - }); }); diff --git a/runner/test/cases/server/plugins/engine/page-controllers/PlaybackUploadPageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/PlaybackUploadPageController.test.ts index 3c63cc12..0bf365bd 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/PlaybackUploadPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/PlaybackUploadPageController.test.ts @@ -1,3 +1,4 @@ +/* TODO may be in future if needed we might need to add this feature import * as Code from "@hapi/code"; import * as Lab from "@hapi/lab"; //@ts-ignore @@ -16,7 +17,7 @@ const lab = Lab.script(); exports.lab = lab; const {suite, test, before, after} = lab; -/* + suite("PlaybackUploadPageController", () => { let server; let response; diff --git a/runner/test/cases/server/plugins/engine/page-controllers/StartPageController.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/StartPageController.test.ts index c6c3fd96..520b1aba 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/StartPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/StartPageController.test.ts @@ -12,7 +12,7 @@ import {StartPageController} from "src/server/plugins/engine/page-controllers"; //@ts-ignore import {TranslationLoaderService} from "src/server/services/TranslationLoaderService"; -const form = require("../../../confirm-page.test.json"); +const form = require("../../../start-page.test.json"); const {expect} = Code; const lab = Lab.script(); @@ -21,24 +21,26 @@ const {suite, test, before, after, beforeEach, afterEach} = lab; suite("StartPageController", () => { let server; - let response; let $; let sandbox; - - const mockI18n = (key) => { - const translationService: TranslationLoaderService = new TranslationLoaderService(); - const translations = translationService.getTranslations(); - return translations.en[key] || key; - }; + let adapterCacheService; + let options = {}; before(async () => { server = await createServer({ - formFileName: "confirm-page.test.json", + formFileName: "start-page.test.json", formFilePath: path.join(__dirname, "../../../"), enforceCsrf: false, }); - server.i18n = { - __: mockI18n + // Create a mock of adapterCacheService + adapterCacheService = { + getState: sinon.stub() + }; + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + options = { + translationEn: translations.en, + translationCy: translations.cy }; }); @@ -52,31 +54,34 @@ suite("StartPageController", () => { afterEach(() => { sandbox.restore(); + sinon.restore(); }); test("renders start page with form components", async () => { - const pages = [...form.pages]; - const firstPage = pages.shift(); - const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); - const pageController = new StartPageController(formModel, firstPage); - const vm = pageController.getViewModel({}, formModel); - vm.i18n = { - __: mockI18n + // Define the mock state with metadata + const mockState = { + metadata: {} + // Add any other necessary metadata here }; - response = await server.render("summary", vm); + // Stub getState to return the mock state + adapterCacheService.getState.resolves(mockState); + + const response = await server.inject({ + method: 'GET', + url: '/start-page.test/before-you-start' + }); - $ = cheerio.load(response); + $ = cheerio.load(response.payload); expect($(".govuk-main-wrapper form")).to.exist(); - expect($(".govuk-main-wrapper .govuk-button")).to.exist(); - expect($(".govuk-main-wrapper .govuk-button").text()).to.contain("Save and continue"); + expect($(".govuk-button")).to.exist(); + expect($(".govuk-button").text()).to.contain("Continue"); }); test("getViewModel includes isStartPage and skipTimeoutWarning", async () => { const pages = [...form.pages]; const firstPage = pages.shift(); const formDef = {...form, pages: [firstPage, ...pages]}; - let formModel = new AdapterFormModel(formDef, {}); + let formModel = new AdapterFormModel(formDef, options); const pageController = new StartPageController(formModel, firstPage); const formData = {}; diff --git a/runner/test/cases/server/plugins/engine/page-controllers/getConditionEvaluationContext.test.ts b/runner/test/cases/server/plugins/engine/page-controllers/getConditionEvaluationContext.test.ts index 51065a94..cd2bed58 100644 --- a/runner/test/cases/server/plugins/engine/page-controllers/getConditionEvaluationContext.test.ts +++ b/runner/test/cases/server/plugins/engine/page-controllers/getConditionEvaluationContext.test.ts @@ -3,77 +3,88 @@ import * as Lab from "@hapi/lab"; //@ts-ignore import formJson from "src/server/forms/get-condition-evaluation-context.json"; //@ts-ignore -import { AdapterFormModel } from "src/server/plugins/engine/models"; +import {AdapterFormModel} from "src/server/plugins/engine/models"; //@ts-ignore -import { PageController } from "server/plugins/engine/page-controllers"; +import {PageController} from "server/plugins/engine/page-controllers"; +import {TranslationLoaderService} from "../../../../../../src/server/services/TranslationLoaderService"; const lab = Lab.script(); exports.lab = lab; -const { expect } = Code; -const { suite, it } = lab; +const {expect} = Code; +const {beforeEach, suite, it} = lab; suite("Condition Evaluation Context", () => { - it("it correctly includes/filters state values", () => { - const formModel = new AdapterFormModel(formJson, {}); + let options = {}; + beforeEach(() => { + const translationService: TranslationLoaderService = new TranslationLoaderService(); + const translations = translationService.getTranslations(); + options = { + translationEn: translations.en, + translationCy: translations.cy + }; + }); - //Selected page appears after convergence and contains a conditional field - //This is the page we're theoretically browsing to - const testConditionsPage = formModel.pages.find( - (page) => page.path === "/testconditions" - ); + it("it correctly includes/filters state values", () => { + const formModel = new AdapterFormModel(formJson, options); - const page = new PageController(formModel, testConditionsPage?.pageDef); + //Selected page appears after convergence and contains a conditional field + //This is the page we're theoretically browsing to + const testConditionsPage = formModel.pages.find( + (page) => page.path === "/testconditions" + ); - //The state below shows we said we had a UKPassport and entered details for an applicant - let completeState = { - progress: [ - "/csds/uk-passport?visit=7O4_nT1TVI", - "/csds/how-many-people?visit=7O4_nT1TVI", - "/csds/applicant-one?visit=7O4_nT1TVI", - "/csds/applicant-one-address?visit=7O4_nT1TVI", - "/csds/contact-details?visit=7O4_nT1TVI", - ], - checkBeforeYouStart: { - ukPassport: true, - }, - applicantDetails: { - numberOfApplicants: 1, - phoneNumber: "1234567890", - emailAddress: "developer@example.com", - }, - applicantOneDetails: { - firstName: "Martin", - middleName: null, - lastName: "Crawley", - address: { - addressLine1: "AddressLine1", - addressLine2: "AddressLine2", - town: "Town", - postcode: "Postcode", - }, - }, - }; + const page = new PageController(formModel, testConditionsPage?.pageDef); - //Calculate our relevantState based on the page we're attempting to load and the above state we provide - let relevantState = page.getConditionEvaluationContext( - formModel, - completeState - ); + //The state below shows we said we had a UKPassport and entered details for an applicant + let completeState = { + progress: [ + "/csds/uk-passport?visit=7O4_nT1TVI", + "/csds/how-many-people?visit=7O4_nT1TVI", + "/csds/applicant-one?visit=7O4_nT1TVI", + "/csds/applicant-one-address?visit=7O4_nT1TVI", + "/csds/contact-details?visit=7O4_nT1TVI", + ], + checkBeforeYouStart: { + ukPassport: true, + }, + applicantDetails: { + numberOfApplicants: 1, + phoneNumber: "1234567890", + emailAddress: "developer@example.com", + }, + applicantOneDetails: { + firstName: "Martin", + middleName: null, + lastName: "Crawley", + address: { + addressLine1: "AddressLine1", + addressLine2: "AddressLine2", + town: "Town", + postcode: "Postcode", + }, + }, + }; - //Our relevantState should know our applicants firstName is Martin - expect(relevantState.applicantOneDetails.firstName).to.equal("Martin"); + //Calculate our relevantState based on the page we're attempting to load and the above state we provide + let relevantState = page.getConditionEvaluationContext( + formModel, + completeState + ); - //Now mark that we don't have a UK Passport - completeState.checkBeforeYouStart.ukPassport = false; + //Our relevantState should know our applicants firstName is Martin + expect(relevantState.applicantOneDetails.firstName).to.equal("Martin"); - //And recalculate our relevantState - relevantState = page.getConditionEvaluationContext( - formModel, - completeState - ); + //Now mark that we don't have a UK Passport + completeState.checkBeforeYouStart.ukPassport = false; - //Our relevantState should no longer know anything about our applicant - expect(relevantState.checkBeforeYouStart.ukPassport).to.equal(false); - expect(relevantState.applicantOneDetails).to.not.exist(); - }); + //And recalculate our relevantState + relevantState = page.getConditionEvaluationContext( + formModel, + completeState + ); + + //Our relevantState should no longer know anything about our applicant + expect(relevantState.checkBeforeYouStart.ukPassport).to.equal(false); + expect(relevantState.applicantOneDetails).to.not.exist(); + }); }); diff --git a/runner/test/cases/server/start-page.test.json b/runner/test/cases/server/start-page.test.json new file mode 100644 index 00000000..db92c6cb --- /dev/null +++ b/runner/test/cases/server/start-page.test.json @@ -0,0 +1,353 @@ +{ + "metadata": {}, + "startPage": "/before-you-start", + "pages": [ + { + "title": "Before you start", + "path": "/before-you-start", + "components": [ + { + "name": "GqJNNZ", + "options": {}, + "type": "Html", + "content": "

We need to ask some questions to check you can apply for this funding.

\n

We'll ask about:

\n", + "schema": {} + } + ], + "next": [ + { + "path": "/do-you-intend-to-undertake-a-green-belt-review-to-accommodate-an-increase-in-your-needs" + } + ], + "controller": "./pages/start.js", + "section": "FabDefault" + }, + { + "title": "Summary", + "path": "/summary", + "controller": "./pages/confirm.js", + "components": [], + "next": [], + "section": "FabDefault" + }, + { + "path": "/do-you-intend-to-undertake-a-green-belt-review-to-accommodate-an-increase-in-your-needs", + "title": "Do you intend to undertake a Green Belt review to accommodate an increase in your needs? ", + "components": [ + { + "name": "Mhzypt", + "options": { "hideTitle": true }, + "type": "YesNoField", + "title": "Do you intend to undertake a Green Belt review to accommodate an increase in your needs? ", + "schema": {}, + "values": { "type": "listRef" }, + "hint": "This can include your local housing need as a result of the National Planning Policy Framework." + } + ], + "next": [ + { + "path": "/do-you-agree-to-collaborate-with-mhclg-over-monitoring-and-evaluation-requirements", + "condition": "Kkqowe" + }, + { "path": "/not-eligible/lower-vacancy-rate", "condition": "jPvjPM" } + ], + "section": "mwumLN", + "controller": "./pages/continue.js" + }, + { + "path": "/do-you-agree-to-collaborate-with-mhclg-over-monitoring-and-evaluation-requirements", + "title": "Do you agree to collaborate with MHCLG over monitoring and evaluation requirements?", + "components": [ + { + "name": "pPNbfh", + "options": { "hideTitle": true }, + "type": "YesNoField", + "title": "Do you agree to collaborate with MHCLG over monitoring and evaluation requirements?", + "schema": {}, + "values": { "type": "listRef" } + } + ], + "next": [ + { + "path": "/does-your-section-151-officer-or-deputy-section-151-officer-support-this-submission", + "condition": "REqbHB" + }, + { + "path": "/not-eligible/property-vacant-long-duration", + "condition": "zwwxkY" + } + ], + "section": "mwumLN", + "controller": "./pages/continue.js" + }, + { + "path": "/does-your-section-151-officer-or-deputy-section-151-officer-support-this-submission", + "title": "Does your section 151 officer, or deputy section 151 officer, support this submission?", + "components": [ + { + "name": "WJoaFR", + "options": { "hideTitle": true }, + "type": "YesNoField", + "title": "Does your section 151 officer, or deputy section 151 officer, support this submission?", + "schema": {}, + "values": { "type": "listRef" } + } + ], + "next": [ + { "path": "/you-cannot-apply-for-this-funding", "condition": "gVEhwH" }, + { + "path": "/do-you-commit-to-have-proposals-in-place-by-march-2025-on-how-you-will-spend-the-funding", + "condition": "xIzxnn" + } + ], + "section": "mwumLN", + "controller": "./pages/continue.js" + }, + { + "path": "/not-eligible/property-vacant-long-duration", + "title": "You cannot apply for this funding", + "components": [ + { + "name": "oAnaAw", + "options": {}, + "type": "Html", + "content": "

You’re not eligible to apply for funding because you did not agree to collaborate with MHCLG over monitoring and evaluation requirements.\n\n

If you need help, email the grant team at (email)

", + "schema": {} + } + ], + "next": [], + "controller": "./pages/continue.js", + "section": "FabDefault" + }, + { + "path": "/not-eligible/lower-vacancy-rate", + "title": "You cannot apply for this funding", + "components": [ + { + "name": "LpLRZy", + "options": {}, + "type": "Html", + "content": "

You’re not eligible to apply for funding because you do not intend to undertake a Green Belt review to accommodate an increase in your needs.\n\n

If you need help, email the grant team at (email)

", + "schema": {} + } + ], + "next": [], + "controller": "./pages/continue.js", + "section": "FabDefault" + }, + { + "path": "/you-cannot-apply-for-this-funding", + "title": "You cannot apply for this funding", + "components": [ + { + "name": "LNXmKd", + "options": {}, + "type": "Html", + "content": "

You’re not eligible to apply for funding because your section 151 officer, or deputy section 151 officer, does not support this submission.\n\n

If you need help, email the grant team at (email)

", + "schema": {} + } + ], + "next": [], + "section": "FabDefault" + }, + { + "path": "/do-you-commit-to-have-proposals-in-place-by-march-2025-on-how-you-will-spend-the-funding", + "title": "Do you commit to have proposals in place by March 2025 on how you will spend the funding?", + "components": [ + { + "name": "RWRWqx", + "options": { "exposeToContext": true }, + "type": "YesNoField", + "title": "Do you commit to have proposals in place by March 2025 on how you will spend the funding?", + "values": { "type": "listRef" }, + "schema": {} + } + ], + "next": [ + { + "path": "/you-cannot-apply-for-this-funding-Wcfswy", + "condition": "XezpcQ" + }, + { "path": "/summary", "condition": "IMcUoD" } + ], + "section": "mwumLN" + }, + { + "path": "/you-cannot-apply-for-this-funding-Wcfswy", + "title": "You cannot apply for this funding", + "components": [ + { + "name": "JVsELa", + "options": {}, + "type": "Para", + "content": "

You’re not eligible to apply for funding because you did not commit to have proposals in place by March 2025 on how you will spend the funding.\n\n

If you need help, email the grant team at (email)

", + "schema": {} + } + ], + "next": [], + "section": "mwumLN" + } + ], + "lists": [], + "sections": [ + { "name": "mwumLN", "title": "Before you start", "hideTitle": false }, + { "name": "FabDefault", "title": "Default Section", "hideTitle": true } + ], + "conditions": [ + { + "displayName": "required to undertake greebelt review", + "name": "Kkqowe", + "value": { + "name": "required to undertake greebelt review", + "conditions": [ + { + "field": { + "name": "mwumLN.Mhzypt", + "type": "YesNoField", + "display": "Will you be required to undertake a Green Belt review to accommodate an increase in your local housing need as a result of our NPPF (National Planning Policy Framework) changes?" + }, + "operator": "is", + "value": { "type": "Value", "value": "true", "display": "true" } + } + ] + } + }, + { + "displayName": "agree to collaborate", + "name": "REqbHB", + "value": { + "name": "agree to collaborate", + "conditions": [ + { + "field": { + "name": "mwumLN.pPNbfh", + "type": "YesNoField", + "display": "Do you agree to collaborate with MHCLG over monitoring and evaluation requirements?" + }, + "operator": "is", + "value": { "type": "Value", "value": "true", "display": "true" } + } + ] + } + }, + { + "displayName": "section 151 is aware", + "name": "xIzxnn", + "value": { + "name": "section 151 is aware", + "conditions": [ + { + "field": { + "name": "mwumLN.WJoaFR", + "type": "YesNoField", + "display": "Please confirm that your Section 151 officer, or deputy Section 151 officer, is aware of this submission" + }, + "operator": "is", + "value": { "type": "Value", "value": "true", "display": "true" } + } + ] + } + }, + { + "displayName": "agree to collaborate-No", + "name": "zwwxkY", + "value": { + "name": "agree to collaborate-No", + "conditions": [ + { + "field": { + "name": "mwumLN.pPNbfh", + "type": "YesNoField", + "display": "Do you agree to collaborate with MHCLG over monitoring and evaluation requirements?" + }, + "operator": "is", + "value": { "type": "Value", "value": "false", "display": "false" } + } + ] + } + }, + { + "displayName": "required to undertake greebelt review-No", + "name": "jPvjPM", + "value": { + "name": "required to undertake greebelt review-No", + "conditions": [ + { + "field": { + "name": "mwumLN.Mhzypt", + "type": "YesNoField", + "display": "Will you be required to undertake a Green Belt review to accommodate an increase in your local housing need as a result of our NPPF (National Planning Policy Framework) changes?" + }, + "operator": "is", + "value": { "type": "Value", "value": "false", "display": "false" } + } + ] + } + }, + { + "displayName": "section 151 is aware-No", + "name": "gVEhwH", + "value": { + "name": "section 151 is aware-No", + "conditions": [ + { + "field": { + "name": "mwumLN.WJoaFR", + "type": "YesNoField", + "display": "Please confirm that your Section 151 officer, or deputy Section 151 officer, is aware of this submission" + }, + "operator": "is", + "value": { "type": "Value", "value": "false", "display": "false" } + } + ] + } + }, + { + "displayName": "Proposal in place-yes", + "name": "IMcUoD", + "value": { + "name": "Proposal in place-yes", + "conditions": [ + { + "field": { + "name": "mwumLN.RWRWqx", + "type": "YesNoField", + "display": "Do you commit to have proposals in place by March 2025 on how you will spend the funding?" + }, + "operator": "is", + "value": { "type": "Value", "value": "true", "display": "true" } + } + ] + } + }, + { + "displayName": "Proposal in place-no", + "name": "XezpcQ", + "value": { + "name": "Proposal in place-no", + "conditions": [ + { + "field": { + "name": "mwumLN.RWRWqx", + "type": "YesNoField", + "display": "Do you commit to have proposals in place by March 2025 on how you will spend the funding?" + }, + "operator": "is", + "value": { "type": "Value", "value": "false", "display": "false" } + } + ] + } + } + ], + "fees": [], + "outputs": [], + "version": 2, + "skipSummary": false, + "markAsComplete": false, + "name": "Apply for Green belt review funding", + "feeOptions": { + "allowSubmissionWithoutPayment": true, + "maxAttempts": 3, + "showPaymentSkippedWarningPage": false + } +}