From a0c0335b80f3d37ef4fbea47f408940a6ef842b4 Mon Sep 17 00:00:00 2001 From: mazuh Date: Sun, 4 Feb 2024 21:12:24 -0300 Subject: [PATCH] test cov for list, remove and create specs --- package.json | 3 +- src/App.test.tsx | 6 +- .../ProjectWorkspacePage.test.tsx | 207 +++++++++++++++++- .../opfs-project-service.test.ts | 50 +++++ .../ProjectsManagementPage.test.tsx | 10 +- .../opfs-projects-listing-service.test.ts | 6 +- .../origin-private-file-system.test.ts | 22 +- src/services/origin-private-file-system.ts | 3 +- 8 files changed, 274 insertions(+), 33 deletions(-) create mode 100644 src/features/project-workspace/opfs-project-service.test.ts diff --git a/package.json b/package.json index 967d4bb..2e8887c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "test": "jest --coverage --silent=true", + "test": "jest --ci --coverage --verbose --silent=true", + "test:watch": "jest --silent=true --watch", "test:verbose": "DEBUG_PRINT_LIMIT=9999999 npm run test -- --silent=false", "preview": "vite preview" }, diff --git a/src/App.test.tsx b/src/App.test.tsx index 32bb232..637feed 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -28,12 +28,12 @@ describe("App", () => { })); }); - it("Renders fine at first", async () => { + it("renders fine at first", async () => { render(); screen.getByText(/Postmaiden/i); }); - it("Suspends interaction if persisted session changes", async () => { + it("suspends interaction if persisted session changes", async () => { render(); await act(() => jest.runAllTimers()); @@ -52,7 +52,7 @@ describe("App", () => { ).toBeVisible(); }); - it("Blocks interaction if browser doesnt support the offline persistence", () => { + it("blocks interaction if browser doesnt support the offline persistence", () => { (isPersistenceSupported as jest.Mock).mockReturnValue(false); render(); diff --git a/src/features/project-workspace/ProjectWorkspacePage.test.tsx b/src/features/project-workspace/ProjectWorkspacePage.test.tsx index 9c3a79a..a4ec710 100644 --- a/src/features/project-workspace/ProjectWorkspacePage.test.tsx +++ b/src/features/project-workspace/ProjectWorkspacePage.test.tsx @@ -2,7 +2,7 @@ import "@testing-library/jest-dom"; import { act, render, screen } from "@testing-library/react"; import * as wouter from "wouter"; import { ProjectWorkspacePage } from "./ProjectWorkspacePage"; -import * as OPFSProjectService from "./opfs-project-service"; +import * as OPFSSharedInternalsService from "@/services/opfs-projects-shared-internals"; jest.mock("wouter", () => ({ useParams: jest.fn().mockReturnValue({}), @@ -10,6 +10,7 @@ jest.mock("wouter", () => ({ jest.mock("@/services/opfs-projects-shared-internals", () => ({ retrieveProject: jest.fn(), + persistProject: jest.fn(), })); describe("Projects workspace page", () => { @@ -20,21 +21,23 @@ describe("Projects workspace page", () => { uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", }); - jest.spyOn(OPFSProjectService, "retrieveProject").mockResolvedValue({ - uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", - name: "Umbrella Corp API", - sections: [], - specs: [], - }); + jest + .spyOn(OPFSSharedInternalsService, "retrieveProject") + .mockResolvedValue({ + uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", + name: "Umbrella Corp API", + sections: [], + specs: [], + }); }); - it("displays the name of the retrieved project matching the url params", async () => { + it("displays the name of the retrieved project matching the url params, by retrieving the project from OFPS", async () => { await act(async () => render()); expect(screen.getByText(/Umbrella Corp API/i)).toBeVisible(); }); - it("shows error if theres an invalid url params", async () => { + it("shows error if theres an invalid uuid as url param", async () => { jest.spyOn(wouter, "useParams").mockReturnValue({ uuid: "aaaa-bbbb-cccc-dddd-eeee", }); @@ -43,4 +46,190 @@ describe("Projects workspace page", () => { expect(screen.getByText(/Invalid project URL/i)).toBeVisible(); }); + + it("lists the request specs of the retrieved project, by retrieving the project from OFPS", async () => { + jest + .spyOn(OPFSSharedInternalsService, "retrieveProject") + .mockResolvedValue({ + uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", + name: "Umbrella Corp API", + specs: [ + { + uuid: "681d4cb2-2654-41b7-93b0-359703458299", + method: "GET", + url: "https://re.capcom.com/stars-members", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + ], + sections: [], + }); + + await act(async () => render()); + expect(screen.getByText(/stars-members/i)).toBeVisible(); + }); + + it("can easily create new request spec with everything empty but some default headers, by updating the project stored in OFPS", async () => { + jest + .spyOn(OPFSSharedInternalsService, "retrieveProject") + .mockResolvedValue({ + uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", + name: "Umbrella Corp API", + specs: [ + { + uuid: "681d4cb2-2654-41b7-93b0-359703458299", + method: "GET", + url: "https://re.capcom.com/stars-members", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + ], + sections: [], + }); + + jest.spyOn(OPFSSharedInternalsService, "persistProject"); + + await act(async () => render()); + + const button = screen.getByRole("button", { name: /Create request spec/i }); + await act(async () => button.click()); + + expect(OPFSSharedInternalsService.persistProject).toHaveBeenCalledTimes(1); + + const [lastCallArg] = ( + OPFSSharedInternalsService.persistProject as jest.Mock + ).mock.lastCall; + expect(lastCallArg.uuid).toEqual("2372aa5e-9042-4c47-a7e5-a01d9d6005ea"); + expect(lastCallArg.name).toEqual("Umbrella Corp API"); + expect(lastCallArg.specs).toHaveLength(2); + + const recentlyInsertedSpec = lastCallArg.specs[1]; + expect(recentlyInsertedSpec.headers).toEqual([ + { + key: "Content-Type", + value: "application/json", + isEnabled: false, + }, + { + key: "Accept", + value: "application/json", + isEnabled: true, + }, + ]); + expect(recentlyInsertedSpec.method).toEqual("GET"); + expect(recentlyInsertedSpec.url).toEqual(""); + expect(recentlyInsertedSpec.body).toEqual(""); + }); + + it("can remove a existing spec, by updating the project stored in OFPS", async () => { + jest + .spyOn(OPFSSharedInternalsService, "retrieveProject") + .mockResolvedValue({ + uuid: "2372aa5e-9042-4c47-a7e5-a01d9d6005ea", + name: "Umbrella Corp API", + specs: [ + { + uuid: "681d4cb2-2654-41b7-93b0-359703458299", + method: "GET", + url: "https://re.capcom.com/stars-members", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + { + uuid: "6203a158-66ab-446a-8c73-e3578d4f5951", + method: "GET", + url: "https://re.capcom.com/bio-weapons", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + { + uuid: "a127b563-1c28-4d97-9e34-0617499b228b", + method: "GET", + url: "https://re.capcom.com/nest-labs", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + ], + sections: [], + }); + + jest.spyOn(OPFSSharedInternalsService, "persistProject"); + + await act(async () => render()); + + const removeBtns = screen.getAllByRole("button", { name: /Remove/i }); + const removeBtn = removeBtns.at(1); // "bio-weapons" spec, the one in the middle + await act(async () => removeBtn!.click()); + + const confirmBtn = screen.getByRole("button", { + name: /Remove request spec/i, + }); + await act(async () => confirmBtn.click()); + + expect(OPFSSharedInternalsService.persistProject).toHaveBeenCalledTimes(1); + + const [lastCallArg] = ( + OPFSSharedInternalsService.persistProject as jest.Mock + ).mock.lastCall; + expect(lastCallArg.uuid).toEqual("2372aa5e-9042-4c47-a7e5-a01d9d6005ea"); + expect(lastCallArg.name).toEqual("Umbrella Corp API"); + expect(lastCallArg.specs).toEqual([ + { + uuid: "681d4cb2-2654-41b7-93b0-359703458299", + method: "GET", + url: "https://re.capcom.com/stars-members", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + { + uuid: "a127b563-1c28-4d97-9e34-0617499b228b", + method: "GET", + url: "https://re.capcom.com/nest-labs", + headers: [ + { + key: "Content-Type", + value: "application/json", + isEnabled: true, + }, + ], + body: "{}", + }, + ]); + }); }); diff --git a/src/features/project-workspace/opfs-project-service.test.ts b/src/features/project-workspace/opfs-project-service.test.ts new file mode 100644 index 0000000..1851fb7 --- /dev/null +++ b/src/features/project-workspace/opfs-project-service.test.ts @@ -0,0 +1,50 @@ +import { retrieveProject } from "./opfs-project-service"; +import * as opfsAdapters from "../../services/origin-private-file-system"; + +jest.mock("../../services/origin-private-file-system"); + +describe("OPFS project service", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("project retrieval is integrated with the most primitive OPFS service module", async () => { + (opfsAdapters.makeOpfsMainDirAdapter as jest.Mock).mockResolvedValueOnce({ + retrieveFilenames: jest + .fn() + .mockResolvedValue([ + "82184240-6b29-4ae8-82f5-fbe7d1bb814a_MyAPI.json", + "e5ae94f4-2c32-4129-a2b1-cdf2c4c770dc_Biohazard API.json", + ]), + }); + + (opfsAdapters.makeOpfsFileAdapter as jest.Mock).mockImplementationOnce( + (options: { filename: string }) => { + if ( + !options.filename.startsWith("e5ae94f4-2c32-4129-a2b1-cdf2c4c770dc") + ) { + throw new Error("Unexpected test case."); + } + + return { + retrieve: jest.fn().mockImplementation(async () => { + return { + uuid: "e5ae94f4-2c32-4129-a2b1-cdf2c4c770dc", + name: "Biohazard API", + mocked: "content", + }; + }), + }; + } + ); + + const content = await retrieveProject( + "e5ae94f4-2c32-4129-a2b1-cdf2c4c770dc" + ); + expect(content).toEqual({ + uuid: "e5ae94f4-2c32-4129-a2b1-cdf2c4c770dc", + name: "Biohazard API", + mocked: "content", + }); + }); +}); diff --git a/src/features/projects-management/ProjectsManagementPage.test.tsx b/src/features/projects-management/ProjectsManagementPage.test.tsx index cfc9a32..9b1b193 100644 --- a/src/features/projects-management/ProjectsManagementPage.test.tsx +++ b/src/features/projects-management/ProjectsManagementPage.test.tsx @@ -3,7 +3,7 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { ProjectsManagementPage } from "./ProjectsManagementPage"; import * as OPFSProjectsListingService from "./opfs-projects-listing-service"; -describe("Projects listing management page", () => { +describe("Projects listing page", () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -20,7 +20,7 @@ describe("Projects listing management page", () => { expect(screen.getByText(/Start now/i)).toBeVisible(); }); - it("lists projects", async () => { + it("lists projects by retrieving from OPFS", async () => { jest .spyOn(OPFSProjectsListingService, "retrieveProjectsListing") .mockResolvedValue({ @@ -42,7 +42,7 @@ describe("Projects listing management page", () => { expect(screen.getByText(/Some third thing/i)).toBeVisible(); }); - it("can create new project", async () => { + it("can create new project by storing it in OPFS", async () => { jest .spyOn(OPFSProjectsListingService, "retrieveProjectsListing") .mockResolvedValue({ @@ -69,7 +69,7 @@ describe("Projects listing management page", () => { ).toHaveBeenCalledWith("My favorite API"); }); - it("can remove a project", async () => { + it("can remove a project by removing the file from OPFS", async () => { jest .spyOn(OPFSProjectsListingService, "retrieveProjectsListing") .mockResolvedValue({ @@ -91,7 +91,7 @@ describe("Projects listing management page", () => { ).toHaveBeenCalledWith({ uuid: "123-123-123", name: "Some random API" }); }); - it("can update a project in a modal", async () => { + it("can update a project in a modal by persisting a updated version of it in OPFS", async () => { jest .spyOn(OPFSProjectsListingService, "retrieveProjectsListing") .mockResolvedValue({ diff --git a/src/features/projects-management/opfs-projects-listing-service.test.ts b/src/features/projects-management/opfs-projects-listing-service.test.ts index 7dce2a2..be3d6b4 100644 --- a/src/features/projects-management/opfs-projects-listing-service.test.ts +++ b/src/features/projects-management/opfs-projects-listing-service.test.ts @@ -1,3 +1,4 @@ +import * as uuid from "uuid"; import { retrieveProjectsListing, persistNewProjectListingItem, @@ -5,12 +6,11 @@ import { updateProjectListingItem, } from "./opfs-projects-listing-service"; import * as opfsAdapters from "../../services/origin-private-file-system"; -import * as uuid from "uuid"; - -jest.mock("../../services/origin-private-file-system"); jest.mock("uuid"); +jest.mock("../../services/origin-private-file-system"); + describe("OPFS project listing service", () => { beforeEach(() => { jest.resetAllMocks(); diff --git a/src/services/origin-private-file-system.test.ts b/src/services/origin-private-file-system.test.ts index af7717e..81123ce 100644 --- a/src/services/origin-private-file-system.test.ts +++ b/src/services/origin-private-file-system.test.ts @@ -32,7 +32,7 @@ describe("Origin private file system (OPFS) adapter", () => { }); }); - it("Can generate specific singletons (for more pragmatic usage)", async () => { + it("can generate specific singletons (for more pragmatic usage)", async () => { const getOpfsAdapter = makeOpfsFileAdapterSingleton({ filename: "something-in-the-way.txt", }); @@ -41,7 +41,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(opfs1).toBe(opfs2); }); - it("Necessarily means that two different specific singletons are indeed different references", async () => { + it("necessarily means that two different specific singletons are indeed different references", async () => { const getOpfsAdapter1 = makeOpfsFileAdapterSingleton({ filename: "the-man-who-sold-the-world.txt", }); @@ -55,7 +55,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(opfs1).not.toBe(opfs2); }); - it("Integrates with real OPFS to retrieve parsed JSON content", async () => { + it("integrates with real OPFS to retrieve parsed JSON content", async () => { const getFileHandleMock = jest.fn().mockResolvedValue({ getFile: jest.fn().mockResolvedValue({ text: jest @@ -88,7 +88,7 @@ describe("Origin private file system (OPFS) adapter", () => { }); }); - it("Retrieves null if there is no content on a given file", async () => { + it("retrieves null if there is no content on a given file", async () => { Object.defineProperty(global.navigator, "storage", { value: { getDirectory: jest.fn().mockResolvedValue({ @@ -113,7 +113,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(retrieved).toEqual(null); }); - it("Integrates with real OPFS to persist JSON content", async () => { + it("integrates with real OPFS to persist JSON content", async () => { const writeMock = jest.fn(); const closeMock = jest.fn(); const getFileHandleMock = jest.fn().mockResolvedValue({ @@ -150,7 +150,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(closeMock).toHaveBeenCalledTimes(1); }); - it("Closes the writable even if an error is throwed in the writing side effect", async () => { + it("closes the writable even if an error is throwed in the writing side effect", async () => { const writeMock = jest.fn().mockRejectedValue(new Error("Writing error!")); const closeMock = jest.fn(); const getFileHandleMock = jest.fn().mockResolvedValue({ @@ -184,7 +184,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(closeMock).toHaveBeenCalledTimes(1); }); - it("Integrates with real OPFS to remove the file", async () => { + it("integrates with real OPFS to remove the file", async () => { const removeEntryMock = jest.fn().mockResolvedValue(null); Object.defineProperty(global.navigator, "storage", { @@ -211,7 +211,7 @@ describe("Origin private file system (OPFS) adapter", () => { ); }); - it("Integrates with real OPFS to retrieve filenames from a sub diretory", async () => { + it("integrates with real OPFS to retrieve filenames from a sub diretory", async () => { Object.defineProperty(global.navigator, "storage", { value: { getDirectory: jest.fn().mockResolvedValue({ @@ -240,7 +240,7 @@ describe("Origin private file system (OPFS) adapter", () => { ]); }); - it("Integrates with real OPFS to remove entry of filenames", async () => { + it("integrates with real OPFS to remove entry of filenames", async () => { const removeEntryMock = jest.fn().mockResolvedValue(null); Object.defineProperty(global.navigator, "storage", { @@ -266,7 +266,7 @@ describe("Origin private file system (OPFS) adapter", () => { ); }); - it("Can confirm full support for the offline persistence", () => { + it("can confirm full support for the offline persistence", () => { Object.defineProperty(window, "FileSystemFileHandle", { value: { prototype: { @@ -279,7 +279,7 @@ describe("Origin private file system (OPFS) adapter", () => { expect(isPersistenceSupported()).toBe(true); }); - it("Can detect lack of full support for the offline persistence: writable async", () => { + it("can detect lack of full support for the offline persistence: writable async", () => { Object.defineProperty(window, "FileSystemFileHandle", { value: { prototype: {}, diff --git a/src/services/origin-private-file-system.ts b/src/services/origin-private-file-system.ts index 99b3c81..2a24942 100644 --- a/src/services/origin-private-file-system.ts +++ b/src/services/origin-private-file-system.ts @@ -3,7 +3,8 @@ * * It's important for the rest of the application don't use OPFS directly, * because it's a very delicate API with limited compatibility and low - * level details. + * level details. So for all other components and services using + * OPFS this must be the lowest level of abstraction. */ /** */