diff --git a/signup-service/src/main/java/io/mosip/signup/controllers/WebSocketController.java b/signup-service/src/main/java/io/mosip/signup/controllers/WebSocketController.java
index 4d9d1668d..b93ab3098 100644
--- a/signup-service/src/main/java/io/mosip/signup/controllers/WebSocketController.java
+++ b/signup-service/src/main/java/io/mosip/signup/controllers/WebSocketController.java
@@ -23,6 +23,7 @@
import javax.validation.Valid;
import java.util.Objects;
+import java.util.Optional;
import static io.mosip.signup.api.util.ErrorConstants.PLUGIN_NOT_FOUND;
import static io.mosip.signup.util.SignUpConstants.SOCKET_USERNAME_SEPARATOR;
diff --git a/signup-ui/package.json b/signup-ui/package.json
index deda76af2..a09e943dd 100644
--- a/signup-ui/package.json
+++ b/signup-ui/package.json
@@ -58,9 +58,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"
@@ -105,6 +107,8 @@
"@types/lodash": "^4.14.200",
"@types/platform": "^1.3.6",
"@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",
@@ -121,6 +125,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 335d5d121..31e077abe 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 37f81d8d1..6764e4469 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 5d80e72c2..797100e2e 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 000000000..f97c54096
--- /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 000000000..307ece064
--- /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 2e664c6f7..000000000
--- 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/__tests__/VerificationScreen.test.tsx b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx
new file mode 100644
index 000000000..1e4984367
--- /dev/null
+++ b/signup-ui/src/pages/EkycVerificationPage/VerificationScreen/__tests__/VerificationScreen.test.tsx
@@ -0,0 +1,134 @@
+import { QueryCache, QueryClient } from "@tanstack/react-query";
+import { screen } from "@testing-library/react";
+
+import { renderWithClient } from "~utils/test";
+
+import { VerificationScreen } from "../VerificationScreen";
+import { SettingsDto } from "~typings/types";
+
+describe("Web socket connection between the front end and back end", () => {
+ const queryCache = new QueryCache();
+ const queryClient = new QueryClient({ queryCache });
+
+ const settings = {
+ response: {
+ configs: {
+ "slot.request.limit": 2,
+ "slot.request.delay": 1,
+ },
+ },
+ } as SettingsDto;
+ const cancelPopup = jest.fn();
+
+ it("should be on", () => {
+ // TODO: will add the test implementation once some web socket structure is given
+
+ // Arrange
+
+ // Act
+ renderWithClient(queryClient, );
+
+ // Assert
+ // the connection should be on
+ });
+});
+
+describe("VerificationScreen (vs)", () => {
+ const queryCache = new QueryCache();
+ const queryClient = new QueryClient({ queryCache });
+
+ it("should render correctly", () => {
+ // Arrange
+ renderWithClient(queryClient, );
+
+ // Act
+
+ // Assert
+ const vs = screen.getByTestId("vs");
+ expect(vs).toBeInTheDocument();
+ });
+
+ it("should show onscreen instructions above the video frame sent from eKYC provider", () => {
+ // Arrange
+ // TODO: mock instruction of an eKYC provider
+
+ renderWithClient(queryClient, );
+
+ // Act
+
+ // Assert
+ const vsOnScreenInstruction = screen.getByTestId("vs-onscreen-instruction");
+ expect(vsOnScreenInstruction).toBeInTheDocument();
+ });
+
+ it("should show liveliness verification screen", () => {
+ // Arrange
+ renderWithClient(queryClient, );
+
+ // Act
+
+ // Assert
+ const vsLiveliness = screen.getByTestId("vs-liveliness");
+ expect(vsLiveliness).toBeInTheDocument();
+ });
+
+ 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();
+ });
+
+ it("should show NID verification screen", () => {
+ // Arrange
+ renderWithClient(queryClient, );
+
+ // Act
+
+ // Assert
+ const vsNID = screen.getByTestId("vs-nid");
+ expect(vsNID).toBeInTheDocument();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ 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/setupTests.ts b/signup-ui/src/setupTests.ts
index 02edb9eee..05a539b7e 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 000000000..42b9131da
--- /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/getStompClient.ts b/signup-ui/src/utils/stompBroker/getStompClient.ts
new file mode 100644
index 000000000..1daa2744a
--- /dev/null
+++ b/signup-ui/src/utils/stompBroker/getStompClient.ts
@@ -0,0 +1,31 @@
+import { Message, Client } from "@stomp/stompjs";
+
+interface ClientArgs {
+ port: number;
+ topic?: string;
+ onMessage?: (message: Message) => void;
+ endpoint?: string;
+}
+
+const getStompClient = ({
+ port,
+ topic,
+ onMessage = jest.fn(),
+ endpoint = "/websocket"
+}: ClientArgs): Client => {
+ const client = new Client({
+ brokerURL: `ws://localhost:${port}${endpoint}`
+ });
+
+ if (topic) {
+ client.onConnect = () => {
+ client.subscribe(topic, onMessage);
+ };
+ }
+
+ client.activate();
+
+ return client;
+};
+
+export default getStompClient;
diff --git a/signup-ui/src/utils/stompBroker/mockStompBroker.ts b/signup-ui/src/utils/stompBroker/mockStompBroker.ts
new file mode 100644
index 000000000..613f29e22
--- /dev/null
+++ b/signup-ui/src/utils/stompBroker/mockStompBroker.ts
@@ -0,0 +1,206 @@
+import {v4 as uuid} from "uuid";
+import http, { Server } from "http";
+import StompServer from "stomp-broker-js";
+import waitUntil from "./waitUntil";
+
+// interface Global extends NodeJS.Global {
+// TextEncoder: TextEncoder;
+// TextDecoder: TextDecoder;
+// }
+
+// declare var global: Global;
+
+// global.TextEncoder = global.TextEncoder || TextEncoder;
+// global.TextDecoder = global.TextDecoder || TextDecoder;
+
+type CallNextMiddleWare = () => boolean;
+type MiddlewareStrategy = [
+ string,
+ (args: { sessionId: string; frame: Frame }) => void
+];
+
+interface Socket {
+ sessionId: string;
+}
+
+interface MiddlewareArgs {
+ frame: Frame;
+}
+
+interface Frame {
+ headers: {
+ mockMessageId: string;
+ };
+}
+
+interface Session {
+ sessionId: string;
+ hasConnected: boolean;
+ hasReceivedSubscription: boolean;
+ hasSentMessage: boolean;
+ hasDisconnected: boolean;
+}
+
+interface Sessions {
+ [sessionId: string]: Session;
+}
+
+interface Config {
+ port?: number;
+ portRange?: [number, number];
+ endpoint?: string;
+}
+
+class MockStompBroker {
+ private static PORTS_IN_USE: number[] = [];
+ private static BASE_SESSION = {
+ hasConnected: false,
+ hasReceivedSubscription: false,
+ hasSentMessage: false,
+ hasDisconnected: false
+ };
+
+ private static getRandomInt(min: number, max: number): number {
+ return Math.floor(Math.random() * (max - min)) + min;
+ }
+
+ private static getPort(portRange: [number, number] = [8000, 9001]): number {
+ const minInclusive = portRange[0];
+ const maxExclusive = portRange[1];
+ const port = this.getRandomInt(minInclusive, maxExclusive);
+
+ return this.PORTS_IN_USE.includes(port) ? this.getPort() : port;
+ }
+
+ private readonly port: number;
+ private readonly httpServer: Server;
+ private readonly stompServer: any;
+ private readonly sentMessageIds: string[] = [];
+ private queriedSessionIds: string[] = [];
+ private sessions: Sessions = {};
+
+ constructor({ port, portRange, endpoint = "/websocket" }: Config = {}) {
+ this.thereAreNewSessions = this.thereAreNewSessions.bind(this);
+ this.registerMiddlewares = this.registerMiddlewares.bind(this);
+ this.setMiddleware = this.setMiddleware.bind(this);
+
+ this.port = port || MockStompBroker.getPort(portRange);
+ this.httpServer = http.createServer();
+
+ this.stompServer = new StompServer({
+ server: this.httpServer,
+ path: endpoint
+ });
+
+ this.registerMiddlewares();
+ this.httpServer.listen(this.port);
+ }
+
+ public async newSessionsConnected(): Promise {
+ await waitUntil(this.thereAreNewSessions, "No new sessions established");
+
+ const newSessionsIds = Object.values(this.sessions)
+ .filter(({ sessionId }) => !this.queriedSessionIds.includes(sessionId))
+ .filter(({ hasConnected }) => hasConnected)
+ .map(({ sessionId }) => sessionId);
+
+ this.queriedSessionIds = this.queriedSessionIds.concat(newSessionsIds);
+
+ return newSessionsIds;
+ }
+
+ public subscribed(sessionId: string) {
+ return waitUntil(() => {
+ const session = this.sessions[sessionId];
+ return Boolean(session && session.hasReceivedSubscription);
+ }, `Session ${sessionId} never subscribed to a topic`);
+ }
+
+ public scheduleMessage(
+ topic: string,
+ payload: any,
+ headers: {} = {
+ "content-type": "application/json;charset=UTF-8"
+ }
+ ): string {
+ const body = JSON.stringify(payload);
+ const mockMessageId = uuid();
+ this.stompServer.send(`/${topic}`, { ...headers, mockMessageId }, body);
+
+ return mockMessageId;
+ }
+
+ public messageSent(messageId: string) {
+ return waitUntil(
+ () => this.sentMessageIds.includes(messageId),
+ `Message ${messageId} was never sent`
+ );
+ }
+
+ public disconnected(sessionId: string) {
+ return waitUntil(() => {
+ const session = this.sessions[sessionId];
+
+ return Boolean(session && session.hasDisconnected);
+ }, `Session ${sessionId} never disconnected`);
+ }
+
+ public kill() {
+ this.httpServer.close();
+ }
+
+ public getPort() {
+ return this.port;
+ }
+
+ private thereAreNewSessions(): boolean {
+ const numberOfSessions = Object.entries(this.sessions).length;
+ const numberOfSessionsQueried = this.queriedSessionIds.length;
+
+ return numberOfSessions - numberOfSessionsQueried > 0;
+ }
+
+ private setMiddleware([event, middlewareHook]: MiddlewareStrategy) {
+ this.stompServer.setMiddleware(
+ event,
+ (socket: Socket, args: MiddlewareArgs, next: CallNextMiddleWare) => {
+ process.nextTick(() =>
+ middlewareHook({ sessionId: socket.sessionId, frame: args.frame })
+ );
+
+ return next();
+ }
+ );
+ }
+
+ private registerMiddlewares() {
+ const strategies: MiddlewareStrategy[] = [
+ [
+ "connect",
+ ({ sessionId }) =>
+ (this.sessions[sessionId] = {
+ ...MockStompBroker.BASE_SESSION,
+ sessionId,
+ hasConnected: true
+ })
+ ],
+ [
+ "subscribe",
+ ({ sessionId }) =>
+ (this.sessions[sessionId].hasReceivedSubscription = true)
+ ],
+ [
+ "send",
+ ({ frame }) => this.sentMessageIds.push(frame.headers.mockMessageId)
+ ],
+ [
+ "disconnect",
+ ({ sessionId }) => (this.sessions[sessionId].hasDisconnected = true)
+ ]
+ ];
+
+ strategies.forEach(this.setMiddleware);
+ }
+}
+
+export default MockStompBroker;
diff --git a/signup-ui/src/utils/stompBroker/waitUntil.ts b/signup-ui/src/utils/stompBroker/waitUntil.ts
new file mode 100644
index 000000000..17209845d
--- /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 372f4e329..433b7575c 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"
}