From 7907d0a2a1ee34ad393c7ad50fc9137a14b736fc Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:22:46 +0100 Subject: [PATCH] test[e2e]: Add PlanX input components to create-flow test (#3617) --- e2e/README.md | 8 + e2e/tests/api-driven/src/globalHelpers.ts | 2 +- .../api-driven/src/invite-to-pay/helpers.ts | 16 +- .../src/create-flow/create-flow.spec.ts | 100 +++++++---- .../ui-driven/src/helpers/addComponent.ts | 160 ++++++++++++++++++ .../ui-driven/src/{ => helpers}/context.ts | 6 +- .../helpers.ts => helpers/getPage.ts} | 7 +- .../ui-driven/src/helpers/globalHelpers.ts | 142 ++++++++++++++++ .../userActions.ts} | 145 +--------------- .../ui-driven/src/invite-to-pay/agent.spec.ts | 14 +- .../ui-driven/src/invite-to-pay/helpers.ts | 18 +- .../ui-driven/src/invite-to-pay/mocks.ts | 2 +- .../src/invite-to-pay/nominee.spec.ts | 15 +- e2e/tests/ui-driven/src/login.spec.ts | 8 +- e2e/tests/ui-driven/src/pay.spec.ts | 27 ++- .../ui-driven/src/save-and-return.spec.ts | 22 +-- e2e/tests/ui-driven/src/sections.spec.ts | 24 +-- editor.planx.uk/src/components/Toast/types.ts | 12 +- editor.planx.uk/src/contexts/ToastContext.tsx | 9 +- editor.planx.uk/src/reducers/toastReducer.ts | 2 +- 20 files changed, 477 insertions(+), 262 deletions(-) create mode 100644 e2e/tests/ui-driven/src/helpers/addComponent.ts rename e2e/tests/ui-driven/src/{ => helpers}/context.ts (100%) rename e2e/tests/ui-driven/src/{create-flow/helpers.ts => helpers/getPage.ts} (74%) create mode 100644 e2e/tests/ui-driven/src/helpers/globalHelpers.ts rename e2e/tests/ui-driven/src/{globalHelpers.ts => helpers/userActions.ts} (58%) diff --git a/e2e/README.md b/e2e/README.md index 0585a1bcc9..4bac3a74af 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -8,6 +8,14 @@ More info about e2e tests is available in our [testing approach documentation](h We use [Playwright](https://playwright.dev/docs/api/class-test) to run UI driven tests where user interactions are simulated via a web browser. +### Running UI-driven tests + +1. Navigate to `/tests/ui-driven` +2. Run `pnpm install` to install the Playwright package. +3. Run `pnpm exec playwright install` to install the Playwright test browsers. +4. Run the tests with `pnpm test` + + ## API driven tests diff --git a/e2e/tests/api-driven/src/globalHelpers.ts b/e2e/tests/api-driven/src/globalHelpers.ts index d0d1ba88a7..98371f73b7 100644 --- a/e2e/tests/api-driven/src/globalHelpers.ts +++ b/e2e/tests/api-driven/src/globalHelpers.ts @@ -1,4 +1,4 @@ -import { TEST_EMAIL } from "../../ui-driven/src/globalHelpers"; +import { TEST_EMAIL } from "../../ui-driven/src/helpers/globalHelpers"; import { $admin } from "./client"; export function createTeam( diff --git a/e2e/tests/api-driven/src/invite-to-pay/helpers.ts b/e2e/tests/api-driven/src/invite-to-pay/helpers.ts index a15293b7c3..d78807bb85 100644 --- a/e2e/tests/api-driven/src/invite-to-pay/helpers.ts +++ b/e2e/tests/api-driven/src/invite-to-pay/helpers.ts @@ -1,20 +1,20 @@ -import { CustomWorld } from "./steps"; -import axios from "axios"; -import { readFileSync } from "node:fs"; import type { FlowGraph, PaymentRequest, } from "@opensystemslab/planx-core/types"; +import axios from "axios"; +import gql from "graphql-tag"; +import { readFileSync } from "node:fs"; +import { TEST_EMAIL } from "../../../ui-driven/src/helpers/globalHelpers"; +import { $admin } from "../client"; +import { createTeam, createUser } from "../globalHelpers"; import { inviteToPayFlowGraph, - sendNodeWithDestination, mockBreadcrumbs, mockPassport, + sendNodeWithDestination, } from "./mocks"; -import { $admin } from "../client"; -import { TEST_EMAIL } from "../../../ui-driven/src/globalHelpers"; -import { createTeam, createUser } from "../globalHelpers"; -import gql from "graphql-tag"; +import { CustomWorld } from "./steps"; export async function setUpMocks() { const serverMockFile = readFileSync(`${__dirname}/mocks/server-mocks.yaml`); diff --git a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts index d30b6f269d..c7a9cc726b 100644 --- a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts +++ b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts @@ -1,16 +1,26 @@ -import { test, expect, Browser } from "@playwright/test"; +import { Browser, expect, test } from "@playwright/test"; +import { + createAddressInput, + createChecklist, + createContactInput, + createDateInput, + createNotice, + createNumberInput, + createQuestionWithOptions, + createTextInput, +} from "../helpers/addComponent"; +import type { Context } from "../helpers/context"; import { contextDefaults, setUpTestContext, tearDownTestContext, -} from "../context"; +} from "../helpers/context"; +import { getTeamPage } from "../helpers/getPage"; import { createAuthenticatedSession, - answerQuestion, - clickContinue, -} from "../globalHelpers"; -import type { Context } from "../context"; -import { getTeamPage, isGetUserRequest } from "./helpers"; + isGetUserRequest, +} from "../helpers/globalHelpers"; +import { answerQuestion, clickContinue } from "../helpers/userActions"; test.describe("Navigation", () => { let context: Context = { @@ -102,47 +112,79 @@ test.describe("Navigation", () => { // update context to allow flow to be torn down context.flow = { ...serviceProps }; - await page.locator("li.hanger > a").click(); - await page.getByRole("dialog").waitFor(); + const firstNode = page.locator("li.hanger > a").first(); const questionText = "Is this a test?"; - await page.getByPlaceholder("Text").fill(questionText); - - await page.locator("button").filter({ hasText: "add new" }).click(); - await page.getByPlaceholder("Option").fill("Yes"); - - await page.locator("button").filter({ hasText: "add new" }).click(); - await page.getByPlaceholder("Option").nth(1).fill("No"); - - await page.locator("button").filter({ hasText: "Create question" }).click(); + await createQuestionWithOptions(page, firstNode, questionText, [ + "Yes", + "No", + ]); await expect( page.locator("a").filter({ hasText: questionText }), ).toBeVisible(); // Add a notice to the "Yes" path const yesBranch = page.locator("#flow .card .options .option").nth(0); - await yesBranch.locator(".hanger > a").click(); - await page.getByRole("dialog").waitFor(); - await page.locator("select").selectOption({ label: "Notice" }); const yesBranchNoticeText = "Yes! this is a test"; - await page.getByPlaceholder("Notice").fill(yesBranchNoticeText); - await page.locator("button").filter({ hasText: "Create notice" }).click(); + await createNotice( + page, + yesBranch.locator(".hanger > a"), + yesBranchNoticeText, + ); // Add a notice to the "No" path const noBranch = page.locator("#flow .card .options .option").nth(1); - await noBranch.locator(".hanger > a").click(); - await page.getByRole("dialog").waitFor(); - - await page.locator("select").selectOption({ label: "Notice" }); const noBranchNoticeText = "Sorry, this is a test"; - await page.getByPlaceholder("Notice").fill(noBranchNoticeText); - await page.locator("button").filter({ hasText: "Create notice" }).click(); + await createNotice( + page, + noBranch.locator(".hanger > a"), + noBranchNoticeText, + ); + + // TODO: find a nicer way to find the next node + let nextNode = page.locator(".hanger > a").nth(5); + await createChecklist(page, nextNode, "A checklist title", [ + "Checklist item 1", + "Second checklist item", + "The third checklist item", + ]); + + nextNode = page.locator(".hanger > a").nth(7); + await createTextInput(page, nextNode, "Tell us about your trees."); + + nextNode = page.locator(".hanger > a").nth(8); + await createNumberInput(page, nextNode, "How old are you?", "years"); + + nextNode = page.locator(".hanger > a").nth(9); + await createDateInput(page, nextNode, "When is your birthday?"); + + nextNode = page.locator(".hanger > a").nth(10); + await createAddressInput( + page, + nextNode, + "What is your address?", + "some data field", + ); + + nextNode = page.locator(".hanger > a").nth(11); + await createContactInput( + page, + nextNode, + "What is your contact info?", + "some data field", + ); const nodes = page.locator(".card"); await expect(nodes.getByText(questionText)).toBeVisible(); await expect(nodes.getByText(yesBranchNoticeText)).toBeVisible(); await expect(nodes.getByText(noBranchNoticeText)).toBeVisible(); + await expect(nodes.getByText("Checklist item 1")).toBeVisible(); + await expect(nodes.getByText("Tell us about your trees.")).toBeVisible(); + await expect(nodes.getByText("How old are you?")).toBeVisible(); + await expect(nodes.getByText("When is your birthday?")).toBeVisible(); + await expect(nodes.getByText("What is your address?")).toBeVisible(); + await expect(nodes.getByText("What is your contact info?")).toBeVisible(); }); test("Cannot preview an unpublished flow", async ({ diff --git a/e2e/tests/ui-driven/src/helpers/addComponent.ts b/e2e/tests/ui-driven/src/helpers/addComponent.ts new file mode 100644 index 0000000000..9411c2d955 --- /dev/null +++ b/e2e/tests/ui-driven/src/helpers/addComponent.ts @@ -0,0 +1,160 @@ +import { Locator, Page } from "@playwright/test"; + +const createBaseInput = async ( + page: Page, + locatingNode: Locator, + type: string, + title?: string, + options?: string[], +) => { + await locatingNode.click(); + await page.getByRole("dialog").waitFor(); + await page.locator("select").selectOption({ label: type }); + + switch (type) { + case "Question": + await page.getByPlaceholder("Text").fill(title || ""); + if (options) { + await createOptions(options, "add new", page); + } + break; + case "Notice": + await page.getByPlaceholder("Notice").fill(title || ""); + break; + case "Checklist": + await page.getByPlaceholder("Text").fill(title || ""); + if (options) { + await createOptions(options, "add new option", page); + } + break; + case "Text Input": + await page.getByPlaceholder("Title").fill(title || ""); + break; + case "Number Input": + await page.getByPlaceholder("Title").fill(title || ""); + await page.getByPlaceholder("eg square metres").fill(options?.[0] || ""); + break; + case "Date Input": + await page.getByPlaceholder("Title").fill(title || ""); + // fill with hardcoded dates for now + await page.locator("id=undefined-min-day").fill("01"); + await page.locator("id=undefined-min-month").fill("01"); + await page.locator("id=undefined-min-year").fill("1800"); + await page.locator("id=undefined-max-day").fill("31"); + await page.locator("id=undefined-max-month").fill("12"); + await page.locator("id=undefined-max-year").fill("2199"); + break; + case "Address Input": + await page.getByPlaceholder("Title").fill(title || ""); + await page.getByPlaceholder("Data Field").fill(options?.[0] || ""); + break; + case "Contact Input": + await page.getByPlaceholder("Title").fill(title || ""); + await page.getByPlaceholder("Data Field").fill(options?.[0] || ""); + break; + default: + throw new Error(`Unsupported type: ${type}`); + } + + // convert type name to lowercase, with dashes if there are spaces + const buttonName = type.toLowerCase().replace(/\s/g, "-"); + await page + .locator("button") + .filter({ + hasText: `Create ${buttonName}`, + }) + .click(); +}; + +export const createQuestionWithOptions = async ( + page: Page, + locatingNode: Locator, + questionText: string, + options: string[], +) => { + await createBaseInput(page, locatingNode, "Question", questionText, options); +}; + +export const createNotice = async ( + page: Page, + locatingNode: Locator, + noticeText: string, +) => { + await createBaseInput(page, locatingNode, "Notice", noticeText); +}; + +export const createChecklist = async ( + page: Page, + locatingNode: Locator, + checklistTitle: string, + checklistOptions: string[], +) => { + createBaseInput( + page, + locatingNode, + "Checklist", + checklistTitle, + checklistOptions, + ); +}; + +export const createTextInput = async ( + page: Page, + locatingNode: Locator, + inputTitle: string, +) => { + await createBaseInput(page, locatingNode, "Text Input", inputTitle); +}; + +export const createNumberInput = async ( + page: Page, + locatingNode: Locator, + inputTitle: string, + inputUnits: string, +) => { + await createBaseInput(page, locatingNode, "Number Input", inputTitle, [ + inputUnits, + ]); +}; + +export const createDateInput = async ( + page: Page, + locatingNode: Locator, + inputTitle: string, +) => { + await createBaseInput(page, locatingNode, "Date Input", inputTitle); +}; + +export const createAddressInput = async ( + page: Page, + locatingNode: Locator, + inputTitle: string, + inputDataField: string, +) => { + await createBaseInput(page, locatingNode, "Address Input", inputTitle, [ + inputDataField, + ]); +}; + +export const createContactInput = async ( + page: Page, + locatingNode: Locator, + inputTitle: string, + inputDataField: string, +) => { + await createBaseInput(page, locatingNode, "Contact Input", inputTitle, [ + inputDataField, + ]); +}; +async function createOptions( + options: string[], + buttonText: string, + page: Page, +) { + let index = 0; + for (const option of options) { + await page.locator("button").filter({ hasText: buttonText }).click(); + await page.getByPlaceholder("Option").nth(index).fill(option); + index++; + } +} diff --git a/e2e/tests/ui-driven/src/context.ts b/e2e/tests/ui-driven/src/helpers/context.ts similarity index 100% rename from e2e/tests/ui-driven/src/context.ts rename to e2e/tests/ui-driven/src/helpers/context.ts index 3ef2a4f3cf..2e60786e54 100644 --- a/e2e/tests/ui-driven/src/context.ts +++ b/e2e/tests/ui-driven/src/helpers/context.ts @@ -1,8 +1,8 @@ -import assert from "node:assert"; -import { log } from "./globalHelpers"; -import { sign } from "jsonwebtoken"; import { CoreDomainClient } from "@opensystemslab/planx-core"; import { GraphQLClient, gql } from "graphql-request"; +import { sign } from "jsonwebtoken"; +import assert from "node:assert"; +import { log } from "./globalHelpers"; type NewTeam = Parameters[0]; diff --git a/e2e/tests/ui-driven/src/create-flow/helpers.ts b/e2e/tests/ui-driven/src/helpers/getPage.ts similarity index 74% rename from e2e/tests/ui-driven/src/create-flow/helpers.ts rename to e2e/tests/ui-driven/src/helpers/getPage.ts index 842cf6e430..8d9880c12e 100644 --- a/e2e/tests/ui-driven/src/create-flow/helpers.ts +++ b/e2e/tests/ui-driven/src/helpers/getPage.ts @@ -1,8 +1,5 @@ -import { Browser, Page, Request } from "@playwright/test"; -import { createAuthenticatedSession } from "../globalHelpers"; - -export const isGetUserRequest = (req: Request) => - req.url().includes("/user/me"); +import { Browser, Page } from "@playwright/test"; +import { createAuthenticatedSession } from "./globalHelpers"; export async function getAdminPage({ browser, diff --git a/e2e/tests/ui-driven/src/helpers/globalHelpers.ts b/e2e/tests/ui-driven/src/helpers/globalHelpers.ts new file mode 100644 index 0000000000..29e00f7e64 --- /dev/null +++ b/e2e/tests/ui-driven/src/helpers/globalHelpers.ts @@ -0,0 +1,142 @@ +import { FlowGraph } from "@opensystemslab/planx-core/types"; +import type { Browser, Page, Request } from "@playwright/test"; +import { gql } from "graphql-request"; +import type { Context } from "./context"; +import { generateAuthenticationToken, getGraphQLClient } from "./context"; + +// Test card numbers to be used in gov.uk sandbox environment +// reference: https://docs.payments.service.gov.uk/testing_govuk_pay/#if-you-39-re-using-a-test-39-sandbox-39-account +export const cards = { + successful_card_number: "4444333322221111", + invalid_card_number: "4000000000000002", +}; + +// Gov.uk Notify requests testing service use smoke test email addresses +// see https://docs.notifications.service.gov.uk/rest-api.html#smoke-testing +export const TEST_EMAIL = + "simulate-delivered@notifications.service.gov.uk" as const; + +// utility functions + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function log(...args: any[]) { + process.env.DEBUG_LOG && console.log(...args); +} + +// a collection of useful playwright actions +// these could evolve into fixtures: https://playwright.dev/docs/test-fixtures + +export function debugPageConsole(page: Page) { + page.on("console", (msg) => console.log(msg.text())); +} + +// used to detect `{ "setItem": ... }`, `{"getItem": ... }` +// and "section state updated" debug messages on state transitions +export async function waitForDebugLog(page: Page) { + return new Promise((resolve) => { + page.on("console", (msg) => { + if (msg.type() == "debug") { + resolve(true); + } + }); + }); +} + +export async function createAuthenticatedSession({ + browser, + userId, +}: { + browser: Browser; + userId: number; +}): Promise { + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + const token = generateAuthenticationToken(`${userId}`); + await browserContext.addCookies([ + { + name: "jwt", + domain: "localhost", + path: "/", + value: token, + }, + { + name: "auth", + domain: "localhost", + path: "/", + value: JSON.stringify({ loggedIn: true }), + }, + ]); + return page; +} + +export async function setFeatureFlag(page: Page, featureFlag: string) { + await page.addInitScript( + (featureFlag: string) => + window.localStorage.setItem( + "FEATURE_FLAGS", + JSON.stringify([featureFlag]), + ), + featureFlag, + ); +} + +export async function getSessionId(page: Page): Promise { + // @ts-expect-error - Property api does not exist on type Window & typeof globalThis + const sessionId = page.evaluate(() => window.api.getState().sessionId); + if (!sessionId) throw Error("Session ID missing from window"); + return sessionId; +} + +export async function addSessionToContext(page: Page, context: Context) { + const sessionId = await getSessionId(page); + context.sessionIds!.push(sessionId); + return sessionId; +} + +export async function waitForPaymentResponse( + page: Page, + context: Context, +): Promise<{ paymentId: string; state?: { status: string } }> { + const { payment_id: paymentId, state } = await page + .waitForResponse((response) => { + return response.url().includes(`pay/${context.team!.slug!}`); + }) + .then((req) => req.json()); + if (!paymentId) throw new Error("Bad payment response"); + return { paymentId, state }; +} + +export async function modifyFlow({ + context, + modifiedFlow, +}: { + context: Context; + modifiedFlow: FlowGraph; +}) { + const adminGQLClient = getGraphQLClient(); + if (!context.flow?.id || !context.user?.id) { + throw new Error("context must have a flow and user"); + } + await adminGQLClient.request( + gql` + mutation UpdateTestFlow($flowId: uuid!, $userId: Int!, $data: jsonb!) { + update_flows_by_pk(pk_columns: { id: $flowId }, _set: { data: $data }) { + id + data + } + insert_published_flows_one( + object: { flow_id: $flowId, data: $data, publisher_id: $userId } + ) { + id + } + } + `, + { + flowId: context.flow!.id, + userId: context.user!.id, + data: modifiedFlow, + }, + ); +} +export const isGetUserRequest = (req: Request) => + req.url().includes("/user/me"); diff --git a/e2e/tests/ui-driven/src/globalHelpers.ts b/e2e/tests/ui-driven/src/helpers/userActions.ts similarity index 58% rename from e2e/tests/ui-driven/src/globalHelpers.ts rename to e2e/tests/ui-driven/src/helpers/userActions.ts index 56331da06b..bb2a048125 100644 --- a/e2e/tests/ui-driven/src/globalHelpers.ts +++ b/e2e/tests/ui-driven/src/helpers/userActions.ts @@ -1,76 +1,9 @@ -import { mockOSPlacesResponse } from "./mocks/osPlacesResponse"; +import type { Locator, Page } from "@playwright/test"; import { expect } from "@playwright/test"; -import type { Page, Browser, Locator } from "@playwright/test"; -import { findSessionId, generateAuthenticationToken } from "./context"; +import { mockOSPlacesResponse } from "../mocks/osPlacesResponse"; import type { Context } from "./context"; -import { getGraphQLClient } from "./context"; -import { gql } from "graphql-request"; -import { FlowGraph } from "@opensystemslab/planx-core/types"; - -// Test card numbers to be used in gov.uk sandbox environment -// reference: https://docs.payments.service.gov.uk/testing_govuk_pay/#if-you-39-re-using-a-test-39-sandbox-39-account -export const cards = { - successful_card_number: "4444333322221111", - invalid_card_number: "4000000000000002", -}; - -// Gov.uk Notify requests testing service use smoke test email addresses -// see https://docs.notifications.service.gov.uk/rest-api.html#smoke-testing -export const TEST_EMAIL = - "simulate-delivered@notifications.service.gov.uk" as const; - -// utility functions - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function log(...args: any[]) { - process.env.DEBUG_LOG && console.log(...args); -} - -// a collection of useful playwright actions -// these could evolve into fixtures: https://playwright.dev/docs/test-fixtures - -export function debugPageConsole(page: Page) { - page.on("console", (msg) => console.log(msg.text())); -} - -// used to detect `{ "setItem": ... }`, `{"getItem": ... }` -// and "section state updated" debug messages on state transitions -export async function waitForDebugLog(page: Page) { - return new Promise((resolve) => { - page.on("console", (msg) => { - if (msg.type() == "debug") { - resolve(true); - } - }); - }); -} - -export async function createAuthenticatedSession({ - browser, - userId, -}: { - browser: Browser; - userId: number; -}): Promise { - const browserContext = await browser.newContext(); - const page = await browserContext.newPage(); - const token = generateAuthenticationToken(`${userId}`); - await browserContext.addCookies([ - { - name: "jwt", - domain: "localhost", - path: "/", - value: token, - }, - { - name: "auth", - domain: "localhost", - path: "/", - value: JSON.stringify({ loggedIn: true }), - }, - ]); - return page; -} +import { findSessionId, getGraphQLClient } from "./context"; +import { TEST_EMAIL, log, waitForDebugLog } from "./globalHelpers"; export async function saveSession({ page, @@ -299,73 +232,3 @@ export async function answerContactInput( await page.getByLabel("Phone number").fill(phoneNumber); await page.getByLabel("Email address").fill(email); } - -export async function setFeatureFlag(page: Page, featureFlag: string) { - await page.addInitScript( - (featureFlag: string) => - window.localStorage.setItem( - "FEATURE_FLAGS", - JSON.stringify([featureFlag]), - ), - featureFlag, - ); -} - -export async function getSessionId(page: Page): Promise { - // @ts-expect-error - Property api does not exist on type Window & typeof globalThis - const sessionId = page.evaluate(() => window.api.getState().sessionId); - if (!sessionId) throw Error("Session ID missing from window"); - return sessionId; -} - -export async function addSessionToContext(page: Page, context: Context) { - const sessionId = await getSessionId(page); - context.sessionIds!.push(sessionId); - return sessionId; -} - -export async function waitForPaymentResponse( - page: Page, - context: Context, -): Promise<{ paymentId: string; state?: { status: string } }> { - const { payment_id: paymentId, state } = await page - .waitForResponse((response) => { - return response.url().includes(`pay/${context.team!.slug!}`); - }) - .then((req) => req.json()); - if (!paymentId) throw new Error("Bad payment response"); - return { paymentId, state }; -} - -export async function modifyFlow({ - context, - modifiedFlow, -}: { - context: Context; - modifiedFlow: FlowGraph; -}) { - const adminGQLClient = getGraphQLClient(); - if (!context.flow?.id || !context.user?.id) { - throw new Error("context must have a flow and user"); - } - await adminGQLClient.request( - gql` - mutation UpdateTestFlow($flowId: uuid!, $userId: Int!, $data: jsonb!) { - update_flows_by_pk(pk_columns: { id: $flowId }, _set: { data: $data }) { - id - data - } - insert_published_flows_one( - object: { flow_id: $flowId, data: $data, publisher_id: $userId } - ) { - id - } - } - `, - { - flowId: context.flow!.id, - userId: context.user!.id, - data: modifiedFlow, - }, - ); -} diff --git a/e2e/tests/ui-driven/src/invite-to-pay/agent.spec.ts b/e2e/tests/ui-driven/src/invite-to-pay/agent.spec.ts index 7442170cf2..23d94b2609 100644 --- a/e2e/tests/ui-driven/src/invite-to-pay/agent.spec.ts +++ b/e2e/tests/ui-driven/src/invite-to-pay/agent.spec.ts @@ -1,13 +1,18 @@ -import { test, expect, Page, BrowserContext } from "@playwright/test"; -import { addSessionToContext, modifyFlow } from "../globalHelpers"; -import inviteToPayFlow from "../mocks/flows/invite-to-pay-flow"; +import { BrowserContext, Page, expect, test } from "@playwright/test"; import { Context, contextDefaults, getGraphQLClient, setUpTestContext, tearDownTestContext, -} from "../context"; +} from "../helpers/context"; +import { addSessionToContext, modifyFlow } from "../helpers/globalHelpers"; +import { + clickContinue, + returnToSession, + saveSession, +} from "../helpers/userActions"; +import inviteToPayFlow from "../mocks/flows/invite-to-pay-flow"; import { answerInviteToPayForm, getPaymentRequestBySessionId, @@ -15,7 +20,6 @@ import { navigateToPayComponent, } from "./helpers"; import { mockPaymentRequest, modifiedInviteToPayFlow } from "./mocks"; -import { saveSession, returnToSession, clickContinue } from "../globalHelpers"; let context: Context = { ...contextDefaults, diff --git a/e2e/tests/ui-driven/src/invite-to-pay/helpers.ts b/e2e/tests/ui-driven/src/invite-to-pay/helpers.ts index 36e60c26ec..10347d7a81 100644 --- a/e2e/tests/ui-driven/src/invite-to-pay/helpers.ts +++ b/e2e/tests/ui-driven/src/invite-to-pay/helpers.ts @@ -1,16 +1,14 @@ +import { PaymentRequest } from "@opensystemslab/planx-core/dist/types"; +import type { Page } from "@playwright/test"; +import { GraphQLClient, gql } from "graphql-request"; +import { Context } from "../helpers/context"; +import { TEST_EMAIL, addSessionToContext, log } from "../helpers/globalHelpers"; import { answerChecklist, - log, - answerFindProperty, answerContactInput, - addSessionToContext, - TEST_EMAIL, -} from "../globalHelpers"; -import type { Page } from "@playwright/test"; -import { gql, GraphQLClient } from "graphql-request"; -import { fillInEmail } from "../globalHelpers"; -import { PaymentRequest } from "@opensystemslab/planx-core/dist/types"; -import { Context } from "../context"; + answerFindProperty, + fillInEmail, +} from "../helpers/userActions"; /** * Navigates to pay component whilst completing the minimum requirements for an Invite to Pay flow diff --git a/e2e/tests/ui-driven/src/invite-to-pay/mocks.ts b/e2e/tests/ui-driven/src/invite-to-pay/mocks.ts index f9f64e1956..0006a617a5 100644 --- a/e2e/tests/ui-driven/src/invite-to-pay/mocks.ts +++ b/e2e/tests/ui-driven/src/invite-to-pay/mocks.ts @@ -4,8 +4,8 @@ import { PaymentRequest, SessionData, } from "@opensystemslab/planx-core/types"; +import { TEST_EMAIL } from "../helpers/globalHelpers"; import inviteToPayFlow from "../mocks/flows/invite-to-pay-flow"; -import { TEST_EMAIL } from "../globalHelpers"; export const mockPaymentRequest: Partial = { payeeEmail: TEST_EMAIL, diff --git a/e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts b/e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts index 1d2276a048..bd78120699 100644 --- a/e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts +++ b/e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts @@ -1,18 +1,19 @@ -import { test, expect, Page, APIRequestContext } from "@playwright/test"; +import { PaymentRequest, Session } from "@opensystemslab/planx-core/types"; +import { APIRequestContext, Page, expect, test } from "@playwright/test"; +import { GraphQLClient, gql } from "graphql-request"; import { v4 as uuidV4 } from "uuid"; -import { fillGovUkCardDetails, cards } from "../globalHelpers"; -import inviteToPayFlow from "../mocks/flows/invite-to-pay-flow"; import { Context, contextDefaults, getGraphQLClient, setUpTestContext, tearDownTestContext, -} from "../context"; -import { mockPaymentRequestDetails, mockSessionData } from "./mocks"; -import { GraphQLClient, gql } from "graphql-request"; -import { PaymentRequest, Session } from "@opensystemslab/planx-core/types"; +} from "../helpers/context"; +import { cards } from "../helpers/globalHelpers"; +import { fillGovUkCardDetails } from "../helpers/userActions"; +import inviteToPayFlow from "../mocks/flows/invite-to-pay-flow"; import { getPaymentRequestBySessionId } from "./helpers"; +import { mockPaymentRequestDetails, mockSessionData } from "./mocks"; let context: Context = { ...contextDefaults, diff --git a/e2e/tests/ui-driven/src/login.spec.ts b/e2e/tests/ui-driven/src/login.spec.ts index 62481fc98c..23bfbecf83 100644 --- a/e2e/tests/ui-driven/src/login.spec.ts +++ b/e2e/tests/ui-driven/src/login.spec.ts @@ -1,11 +1,11 @@ -import { test, expect } from "@playwright/test"; -import { createAuthenticatedSession } from "./globalHelpers"; +import { expect, test } from "@playwright/test"; +import type { Context } from "./helpers/context"; import { contextDefaults, setUpTestContext, tearDownTestContext, -} from "./context"; -import type { Context } from "./context"; +} from "./helpers/context"; +import { createAuthenticatedSession } from "./helpers/globalHelpers"; test.describe("Login", () => { let context: Context = { diff --git a/e2e/tests/ui-driven/src/pay.spec.ts b/e2e/tests/ui-driven/src/pay.spec.ts index d7e6e9b56a..edf63ffdb1 100644 --- a/e2e/tests/ui-driven/src/pay.spec.ts +++ b/e2e/tests/ui-driven/src/pay.spec.ts @@ -1,23 +1,22 @@ -import { test, expect } from "@playwright/test"; -import { - cards, - fillGovUkCardDetails, - getSessionId, - log, - submitCardDetails, - waitForPaymentResponse, -} from "./globalHelpers"; -import type { Page } from "@playwright/test"; -import payFlow from "./mocks/flows/pay-flow.json"; -import { gql, GraphQLClient } from "graphql-request"; import type { SessionData } from "@opensystemslab/planx-core/types"; -import type { Context } from "./context"; +import type { Page } from "@playwright/test"; +import { expect, test } from "@playwright/test"; +import { GraphQLClient, gql } from "graphql-request"; +import type { Context } from "./helpers/context"; import { contextDefaults, getGraphQLClient, setUpTestContext, tearDownTestContext, -} from "./context"; +} from "./helpers/context"; +import { + cards, + getSessionId, + log, + waitForPaymentResponse, +} from "./helpers/globalHelpers"; +import { fillGovUkCardDetails, submitCardDetails } from "./helpers/userActions"; +import payFlow from "./mocks/flows/pay-flow.json"; let context: Context = { ...contextDefaults, diff --git a/e2e/tests/ui-driven/src/save-and-return.spec.ts b/e2e/tests/ui-driven/src/save-and-return.spec.ts index 162615bad5..c2d01b954e 100644 --- a/e2e/tests/ui-driven/src/save-and-return.spec.ts +++ b/e2e/tests/ui-driven/src/save-and-return.spec.ts @@ -1,23 +1,23 @@ -import { test, expect } from "@playwright/test"; -import { - simpleSendFlow, - modifiedSimpleSendFlow, -} from "./mocks/flows/save-and-return-flows"; +import { expect, test } from "@playwright/test"; +import type { Context } from "./helpers/context"; import { contextDefaults, setUpTestContext, tearDownTestContext, -} from "./context"; +} from "./helpers/context"; +import { modifyFlow } from "./helpers/globalHelpers"; import { - fillInEmail, + answerQuestion, clickContinue, + fillInEmail, findQuestion, - answerQuestion, returnToSession, saveSession, - modifyFlow, -} from "./globalHelpers"; -import type { Context } from "./context"; +} from "./helpers/userActions"; +import { + modifiedSimpleSendFlow, + simpleSendFlow, +} from "./mocks/flows/save-and-return-flows"; test.describe("Save and return", () => { let context: Context = { diff --git a/e2e/tests/ui-driven/src/sections.spec.ts b/e2e/tests/ui-driven/src/sections.spec.ts index 2a44985e8a..0aca98272b 100644 --- a/e2e/tests/ui-driven/src/sections.spec.ts +++ b/e2e/tests/ui-driven/src/sections.spec.ts @@ -1,26 +1,26 @@ +import type { FlowGraph } from "@opensystemslab/planx-core/types"; import { expect, test } from "@playwright/test"; -import { flow, updatedQuestionAnswers } from "./mocks/flows/sections-flow"; +import { gql } from "graphql-request"; +import type { Context } from "./helpers/context"; import { contextDefaults, + getGraphQLClient, setUpTestContext, tearDownTestContext, - getGraphQLClient, -} from "./context"; +} from "./helpers/context"; import { - fillInEmail, - answerQuestion, answerChecklist, - clickContinue, + answerQuestion, clickBack, + clickContinue, + expectConfirmation, expectNotice, expectSections, - expectConfirmation, - saveSession, + fillInEmail, returnToSession, -} from "./globalHelpers"; -import { gql } from "graphql-request"; -import type { Context } from "./context"; -import type { FlowGraph } from "@opensystemslab/planx-core/types"; + saveSession, +} from "./helpers/userActions"; +import { flow, updatedQuestionAnswers } from "./mocks/flows/sections-flow"; // TODO: move this type to planx-core // also defined in editor.planx.uk/src/types.ts diff --git a/editor.planx.uk/src/components/Toast/types.ts b/editor.planx.uk/src/components/Toast/types.ts index 99c7e6d614..f45873940a 100644 --- a/editor.planx.uk/src/components/Toast/types.ts +++ b/editor.planx.uk/src/components/Toast/types.ts @@ -1,7 +1,7 @@ export interface Toast { message: string; type: ToastType; - id: number; + id: string; } export type ToastType = "success" | "warning" | "info" | "error"; @@ -9,21 +9,21 @@ export type ToastState = { toasts: Toast[]; }; -export type ToastAction = AddToast | DeleteToast; +export type ToastAction = AddToast | RemoveToast; type AddToast = { type: "ADD_TOAST"; payload: Toast; }; -type DeleteToast = { - type: "DELETE_TOAST"; - payload: { id: number }; +type RemoveToast = { + type: "REMOVE_TOAST"; + payload: { id: string }; }; export type ToastContextType = { addToast: (type: ToastType, message: string) => void; - remove: (id: number) => void; + remove: (id: string) => void; success: (message: string) => void; warning: (message: string) => void; info: (message: string) => void; diff --git a/editor.planx.uk/src/contexts/ToastContext.tsx b/editor.planx.uk/src/contexts/ToastContext.tsx index b340c8428c..a331a9e13d 100644 --- a/editor.planx.uk/src/contexts/ToastContext.tsx +++ b/editor.planx.uk/src/contexts/ToastContext.tsx @@ -7,9 +7,10 @@ import { } from "components/Toast/types"; import React, { createContext, ReactNode, useReducer } from "react"; import { toastReducer } from "reducers/toastReducer"; +import { v4 as uuidv4 } from "uuid"; const defaultCreateContextValue = { - remove: (_id: number) => {}, + remove: (_id: string) => {}, addToast: (_type: ToastType, _message: string) => {}, success: (_message: string) => {}, warning: (_message: string) => {}, @@ -30,11 +31,11 @@ export const ToastContextProvider = ({ }: Readonly<{ children: ReactNode }>) => { const [state, dispatch] = useReducer(toastReducer, initialState); const addToast = (type: ToastType, message: string) => { - const id = Math.floor(Math.random() * 10_000_000); + const id = uuidv4(); dispatch({ type: "ADD_TOAST", payload: { id, message, type } }); }; - const remove = (id: number) => { - dispatch({ type: "DELETE_TOAST", payload: { id } }); + const remove = (id: string) => { + dispatch({ type: "REMOVE_TOAST", payload: { id } }); }; const success = (message: string) => { addToast("success", message); diff --git a/editor.planx.uk/src/reducers/toastReducer.ts b/editor.planx.uk/src/reducers/toastReducer.ts index bf22a221ad..c15986d465 100644 --- a/editor.planx.uk/src/reducers/toastReducer.ts +++ b/editor.planx.uk/src/reducers/toastReducer.ts @@ -7,7 +7,7 @@ export const toastReducer = (state: ToastState, action: ToastAction) => { ...state, toasts: [...state.toasts, action.payload], }; - case "DELETE_TOAST": { + case "REMOVE_TOAST": { const updatedToasts = state.toasts.filter( (toast) => toast.id !== action.payload.id, );