From 0f86a498e5a3840a8efa9e01184133045b6f9423 Mon Sep 17 00:00:00 2001 From: bunsy-0900 <148200748+bunsy-0900@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:04:02 +0700 Subject: [PATCH] ES-1011: Verification Screen Unit Test Integration (#295) * fix: move from msw service to worker Signed-off-by: bunsy-0900 * wip: testing mock stomp broker Signed-off-by: bunsy-0900 * fix: add todos and jest-websocket-mock Signed-off-by: bunsy-0900 * chore: add what to do Signed-off-by: bunsy-0900 * fix: add unit test for web socket indicator Signed-off-by: bunsy-0900 --------- Signed-off-by: bunsy-0900 Signed-off-by: Aravindhan Alagesan Co-authored-by: Aravindhan Alagesan --- signup-ui/package.json | 5 + signup-ui/src/global.d.ts | 36 +++ signup-ui/src/index.tsx | 5 + signup-ui/src/mocks/handlers/index.ts | 8 +- .../src/mocks/handlers/test-connection.ts | 16 ++ signup-ui/src/mocks/msw-browser.ts | 5 + signup-ui/src/mocks/msw-server.ts | 5 - .../VerificationScreen/VerificationScreen.tsx | 2 +- .../__tests__/VerificationScreen.test.tsx | 222 ++++++++++++++++++ signup-ui/src/services/api.service.ts | 6 +- signup-ui/src/setupTests.ts | 6 +- signup-ui/src/typings/stompBroker.d.ts | 1 + signup-ui/src/utils/stompBroker/waitUntil.ts | 29 +++ signup-ui/tsconfig.json | 3 +- 14 files changed, 333 insertions(+), 16 deletions(-) create mode 100644 signup-ui/src/mocks/handlers/test-connection.ts create mode 100644 signup-ui/src/mocks/msw-browser.ts delete mode 100644 signup-ui/src/mocks/msw-server.ts create mode 100644 signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx create mode 100644 signup-ui/src/typings/stompBroker.d.ts create mode 100644 signup-ui/src/utils/stompBroker/waitUntil.ts diff --git a/signup-ui/package.json b/signup-ui/package.json index 4a309e11..9a4012a8 100644 --- a/signup-ui/package.json +++ b/signup-ui/package.json @@ -59,9 +59,11 @@ "react-webcam": "^7.2.0", "rooks": "^7.14.1", "socket.io-client": "^4.7.5", + "stomp-broker-js": "^1.3.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "typescript": "^4.9.5", + "uuid": "^10.0.0", "web-vitals": "^2.1.4", "yup": "^1.3.2", "zustand": "^4.4.6" @@ -107,6 +109,8 @@ "@types/platform": "^1.3.6", "@types/react-detect-offline": "^2.4.5", "@types/react-google-recaptcha": "^2.1.7", + "@types/text-encoding": "^0.0.39", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.16", "craco-alias": "^3.0.1", "eslint": "^8.52.0", @@ -123,6 +127,7 @@ "storybook": "^7.6.0", "tailwindcss": "^3.4.1", "tailwindcss-dir": "^4.0.0", + "text-encoding": "^0.7.0", "ts-jest": "^29.1.4", "tsconfig-paths-webpack-plugin": "^4.1.0", "type-fest": "^4.6.0", diff --git a/signup-ui/src/global.d.ts b/signup-ui/src/global.d.ts index 335d5d12..31e077ab 100644 --- a/signup-ui/src/global.d.ts +++ b/signup-ui/src/global.d.ts @@ -1 +1,37 @@ /// + +declare module "mock-stomp-broker" { + interface Config { + port?: number; + portRange?: [number, number]; + endpoint?: string; + } + + class MockStompBroker { + private static PORTS_IN_USE; + private static BASE_SESSION; + private static getRandomInt; + private static getPort; + private readonly port; + private readonly httpServer; + private readonly stompServer; + private readonly sentMessageIds; + private queriedSessionIds; + private sessions; + private thereAreNewSessions; + private setMiddleware; + private registerMiddlewares; + + constructor({ port, portRange, endpoint }?: Config); + + public newSessionsConnected(): Promise; + public subscribed(sessionId: string): Promise; + public scheduleMessage(topic: string, payload: any, headers?: {}): string; + public messageSent(messageId: string): Promise; + public disconnected(sessionId: string): Promise; + public kill(): void; + public getPort(): number; + } + + export default MockStompBroker; +} diff --git a/signup-ui/src/index.tsx b/signup-ui/src/index.tsx index 37f81d8d..6764e446 100644 --- a/signup-ui/src/index.tsx +++ b/signup-ui/src/index.tsx @@ -6,9 +6,14 @@ import App from "./App"; import "./services/i18n.service"; import "react-tooltip/dist/react-tooltip.css"; +if (process.env.NODE_ENV === "development") { + import("./mocks/msw-browser").then(({ mswWorker }) => mswWorker.start()); +} + const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); + root.render( diff --git a/signup-ui/src/mocks/handlers/index.ts b/signup-ui/src/mocks/handlers/index.ts index 5d80e72c..797100e2 100644 --- a/signup-ui/src/mocks/handlers/index.ts +++ b/signup-ui/src/mocks/handlers/index.ts @@ -1,3 +1,9 @@ import { checkSlotHandlers } from "./slot-checking"; +import { testConnectionHandlers } from "./test-connection"; -export const handlers = [...checkSlotHandlers]; +export const handlers = [ + // intercept the "test connection" endpoint + ...testConnectionHandlers, + // intercept the "check slot" endpoint + ...checkSlotHandlers, +]; diff --git a/signup-ui/src/mocks/handlers/test-connection.ts b/signup-ui/src/mocks/handlers/test-connection.ts new file mode 100644 index 00000000..f97c5409 --- /dev/null +++ b/signup-ui/src/mocks/handlers/test-connection.ts @@ -0,0 +1,16 @@ +import { http, HttpResponse } from "msw"; + +// endpoint to be intercepted +const testConnectionEP = "http://localhost:8088/v1/signup/test"; + +const testConnectionSuccess = http.get(testConnectionEP, async () => { + return HttpResponse.json({ + responseTime: "2024-03-25T18:10:18.520Z", + response: { + connection: true, + }, + errors: [], + }); +}); + +export const testConnectionHandlers = [testConnectionSuccess]; diff --git a/signup-ui/src/mocks/msw-browser.ts b/signup-ui/src/mocks/msw-browser.ts new file mode 100644 index 00000000..307ece06 --- /dev/null +++ b/signup-ui/src/mocks/msw-browser.ts @@ -0,0 +1,5 @@ +import { setupWorker } from "msw/browser"; + +import { handlers } from "./handlers"; + +export const mswWorker = setupWorker(...handlers); diff --git a/signup-ui/src/mocks/msw-server.ts b/signup-ui/src/mocks/msw-server.ts deleted file mode 100644 index 2e664c6f..00000000 --- a/signup-ui/src/mocks/msw-server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { setupServer } from "msw/node"; - -import { handlers } from "./handlers"; - -export const mswServer = setupServer(...handlers); diff --git a/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/VerificationScreen.tsx b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/VerificationScreen.tsx index 71f744f9..a0c2c352 100644 --- a/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/VerificationScreen.tsx +++ b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/VerificationScreen.tsx @@ -469,4 +469,4 @@ export const VerificationScreen = ({ ); -}; +}; \ No newline at end of file diff --git a/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx new file mode 100644 index 00000000..34cf750b --- /dev/null +++ b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx @@ -0,0 +1,222 @@ +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { screen } from "@testing-library/react"; + +import { renderWithClient } from "~utils/test"; +import * as useStompClient from "~pages/shared/stompWs"; +import { SettingsDto } from "~typings/types"; + +import { VerificationScreen } from "../VerificationScreen"; + +describe("VerificationScreen (vs)", () => { + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + const settings = { + response: { + configs: { + "signin.redirect-url": + "https://esignet.camdgc-dev1.mosip.net/authorize", + }, + }, + } as SettingsDto; + const cancelPopup = jest.fn(); + + it("should have a connected indicator when web socket is connected", async () => { + // Arrange + jest.spyOn(useStompClient, "default").mockReturnValue({ + client: null, + connected: true, + subscribe: jest.fn(), + publish: jest.fn(), + unsubscribe: jest.fn(), + }); + + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const statusConnected = await screen.findByTestId("websocket-connected"); + expect(statusConnected).toBeInTheDocument(); + }); + + it("should have a disconnected indicator when web socket is disconnected", async () => { + // Arrange + jest.spyOn(useStompClient, "default").mockReturnValue({ + client: null, + connected: false, + subscribe: jest.fn(), + publish: jest.fn(), + unsubscribe: jest.fn(), + }); + + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const statusDisconnected = await screen.findByTestId( + "websocket-disconnected" + ); + expect(statusDisconnected).toBeInTheDocument(); + }); + + it("should show onscreen instructions above the video frame", async () => { + // Arrange + jest.spyOn(useStompClient, "default").mockReturnValue({ + client: null, + connected: true, + subscribe: jest.fn(), + publish: jest.fn(), + unsubscribe: jest.fn(), + }); + + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + // 1. `vs-onscreen-instruction` is in the document + // 2. `vs-onscreen-instruction` should say "Welcome! Initiating Identity verification process in..." + const vsOnScreenInstruction = await screen.findByTestId( + "vs-onscreen-instruction" + ); + expect(vsOnScreenInstruction).toBeInTheDocument(); + }); + + // Currently not sure since liveness depends on the web socket's response + it("should show liveliness verification screen", () => { + // Arrange + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const vsLiveliness = screen.getByTestId("vs-liveliness"); + expect(vsLiveliness).toBeInTheDocument(); + }); + + // Currently not sure since liveness depends on the web socket's response + it("should show solid colors across the full screen for color based frame verification", async () => { + // Arrange + renderWithClient( + queryClient, + + ); + + // Act + // TODO: add wait for x seconds + + // Assert + const vsSolidColorScreen = screen.getByTestId("vs-solid-color-screen"); + expect(vsSolidColorScreen).toBeInTheDocument(); + }); + + // Currently not sure since liveness depends on the web socket's response + it("should show NID verification screen", () => { + // Arrange + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const vsNID = screen.getByTestId("vs-nid"); + expect(vsNID).toBeInTheDocument(); + }); + + // This one should be moved to IdentityVerificationScreen instead + // https://xd.adobe.com/view/d1ca3fd3-a54c-4055-b7a2-ee5ad0389788-8499/screen/ba9b246e-7658-4c2b-adb5-b908ecdc3825/specs/ + // https://xd.adobe.com/view/d1ca3fd3-a54c-4055-b7a2-ee5ad0389788-8499/screen/8f43b20a-1a6a-4a49-b751-e1e2ccb2346b/specs/ + it("should show feedback message when verification fails", () => { + // Arrange + // TODO: mock failed verification + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const vsFailedVerification = screen.getByTestId("vs-failed-verification"); + expect(vsFailedVerification).toBeInTheDocument(); + }); + + // This one should be moved to IdentityVerificationScreen instead + // https://xd.adobe.com/view/d1ca3fd3-a54c-4055-b7a2-ee5ad0389788-8499/screen/ba9b246e-7658-4c2b-adb5-b908ecdc3825/specs/ + // https://xd.adobe.com/view/d1ca3fd3-a54c-4055-b7a2-ee5ad0389788-8499/screen/8f43b20a-1a6a-4a49-b751-e1e2ccb2346b/specs/ + it("should show warning message if there is any technical issue", () => { + // Arrange + // TODO: mock technical issue: internet connection lost, ... + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + const vsTechnicalIssueWarningMsg = screen.getByTestId( + "vs-technical-issue-warning-msg" + ); + expect(vsTechnicalIssueWarningMsg).toBeInTheDocument(); + }); + + // This one should be moved to IdentityVerificationScreen instead + // https://xd.adobe.com/view/d1ca3fd3-a54c-4055-b7a2-ee5ad0389788-8499/screen/8ee5c56c-1cd5-4b52-adc4-4350f88e8973/specs/ + it("should be redirected to the leading screen when the verification is successful", () => { + // Arrange + // TODO: mock successful verification + renderWithClient( + queryClient, + + ); + + // Act + + // Assert + // to be redirected to and land on the leading screen + }); +}); diff --git a/signup-ui/src/services/api.service.ts b/signup-ui/src/services/api.service.ts index 098d1592..c91d7fb7 100644 --- a/signup-ui/src/services/api.service.ts +++ b/signup-ui/src/services/api.service.ts @@ -9,9 +9,9 @@ const API_BASE_URL = : window.origin + "/v1/signup"; export const WS_BASE_URL = - process.env.NODE_ENV === "development" - ? `ws://${process.env.REACT_APP_API_BASE_URL?.split("://")[1]}` - : `wss://${window.location.host}/v1/signup`; + process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" + ? `ws://${process.env.REACT_APP_API_BASE_URL?.split("://")[1]}` + : `wss://${window.location.host}/v1/signup`; export class HttpError extends Error { code: number; diff --git a/signup-ui/src/setupTests.ts b/signup-ui/src/setupTests.ts index 02edb9ee..05a539b7 100644 --- a/signup-ui/src/setupTests.ts +++ b/signup-ui/src/setupTests.ts @@ -4,7 +4,7 @@ // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; -import { mswServer } from "./mocks/msw-server"; +import { mswWorker } from "./mocks/msw-browser"; require("whatwg-fetch"); @@ -26,7 +26,3 @@ jest.mock("react-i18next", () => ({ }; }, })); - -beforeAll(() => mswServer.listen()); -afterEach(() => mswServer.resetHandlers()); -afterAll(() => mswServer.close()); diff --git a/signup-ui/src/typings/stompBroker.d.ts b/signup-ui/src/typings/stompBroker.d.ts new file mode 100644 index 00000000..42b9131d --- /dev/null +++ b/signup-ui/src/typings/stompBroker.d.ts @@ -0,0 +1 @@ +declare module 'stomp-broker-js'; diff --git a/signup-ui/src/utils/stompBroker/waitUntil.ts b/signup-ui/src/utils/stompBroker/waitUntil.ts new file mode 100644 index 00000000..17209845 --- /dev/null +++ b/signup-ui/src/utils/stompBroker/waitUntil.ts @@ -0,0 +1,29 @@ +const MAX_MILLIS = 2000; + +const waitUntil = ( + predicate: () => boolean, + errorMessage: string = `Predicate did not become true in ${MAX_MILLIS}ms` +): Promise => { + let timedOut = false; + const timeout = setTimeout(() => (timedOut = true), MAX_MILLIS); + const recursivelyResolve = ( + resolve: () => void, + reject: (message: string) => void + ) => { + if (timedOut) { + reject(errorMessage); + } + if (predicate()) { + clearTimeout(timeout); + resolve(); + } else { + setTimeout(() => recursivelyResolve(resolve, reject), 10); + } + }; + + return new Promise((resolve, reject) => { + recursivelyResolve(resolve, reject); + }); +}; + +export default waitUntil; diff --git a/signup-ui/tsconfig.json b/signup-ui/tsconfig.json index 372f4e32..433b7575 100644 --- a/signup-ui/tsconfig.json +++ b/signup-ui/tsconfig.json @@ -22,7 +22,8 @@ "baseUrl": "." }, "include": [ - "./src" + "./src", + "./src/**/*.d.ts" ], "extends": "./tsconfig.paths.json" }