From b80ba46bb42b825b1567003ae0f5181c16c8caea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 11 Apr 2024 16:03:48 +0100 Subject: [PATCH] web: Add type checking and fix tests --- .../storage/BootSelectionDialog.test.jsx | 2 + .../storage/DeviceSelectionDialog.test.jsx | 2 + .../storage/ProposalDeviceSection.test.jsx | 6 +- .../components/storage/ProposalPage.test.jsx | 148 +++++++++++++----- .../storage/ProposalSettingsSection.test.jsx | 81 +++++++--- 5 files changed, 178 insertions(+), 61 deletions(-) diff --git a/web/src/components/storage/BootSelectionDialog.test.jsx b/web/src/components/storage/BootSelectionDialog.test.jsx index 7e759a1115..395c0e4ebb 100644 --- a/web/src/components/storage/BootSelectionDialog.test.jsx +++ b/web/src/components/storage/BootSelectionDialog.test.jsx @@ -19,6 +19,8 @@ * find current contact information at www.suse.com. */ +// @ts-check + import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; diff --git a/web/src/components/storage/DeviceSelectionDialog.test.jsx b/web/src/components/storage/DeviceSelectionDialog.test.jsx index 8171def342..8dc9a217e1 100644 --- a/web/src/components/storage/DeviceSelectionDialog.test.jsx +++ b/web/src/components/storage/DeviceSelectionDialog.test.jsx @@ -19,6 +19,8 @@ * find current contact information at www.suse.com. */ +// @ts-check + import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; diff --git a/web/src/components/storage/ProposalDeviceSection.test.jsx b/web/src/components/storage/ProposalDeviceSection.test.jsx index 9e3b3666db..bdbab62055 100644 --- a/web/src/components/storage/ProposalDeviceSection.test.jsx +++ b/web/src/components/storage/ProposalDeviceSection.test.jsx @@ -19,13 +19,15 @@ * find current contact information at www.suse.com. */ +// @ts-check + import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalDeviceSection } from "~/components/storage"; const sda = { - sid: "59", + sid: 59, isDrive: true, type: "disk", vendor: "Micron", @@ -46,7 +48,7 @@ const sda = { }; const sdb = { - sid: "62", + sid: 62, isDrive: true, type: "disk", vendor: "Samsung", diff --git a/web/src/components/storage/ProposalPage.test.jsx b/web/src/components/storage/ProposalPage.test.jsx index 516fff030f..4a21a31bd0 100644 --- a/web/src/components/storage/ProposalPage.test.jsx +++ b/web/src/components/storage/ProposalPage.test.jsx @@ -19,13 +19,23 @@ * find current contact information at www.suse.com. */ +// @ts-check + import React from "react"; import { act, screen, waitFor } from "@testing-library/react"; import { createCallbackMock, installerRender } from "~/test-utils"; import { createClient } from "~/client"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { StorageClient } from "~/client/storage"; import { IDLE } from "~/client/status"; import { ProposalPage } from "~/components/storage"; +/** + * @typedef {import ("~/client/storage").ProposalResult} ProposalResult + * @typedef {import ("~/client/storage").StorageDevice} StorageDevice + * @typedef {import ("~/client/storage").Volume} Volume + */ + jest.mock("~/client"); jest.mock("@patternfly/react-core", () => { const original = jest.requireActual("@patternfly/react-core"); @@ -46,9 +56,12 @@ jest.mock("~/context/product", () => ({ }) })); +/** @type {StorageDevice} */ const vda = { - sid: "59", + sid: 59, type: "disk", + isDrive: true, + description: "", vendor: "Micron", model: "Micron 1100 SATA", driver: ["ahci", "mmcblk"], @@ -62,12 +75,14 @@ const vda = { systems : ["Windows 11", "openSUSE Leap 15.2"], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], - partitionTable: { type: "gpt", partitions: ["/dev/vda1", "/dev/vda2"] } }; +/** @type {StorageDevice} */ const vdb = { - sid: "60", + sid: 60, type: "disk", + isDrive: true, + description: "", vendor: "Seagate", model: "Unknown", driver: ["ahci", "mmcblk"], @@ -76,32 +91,84 @@ const vdb = { size: 1e+6 }; -const storageMock = { - probe: jest.fn().mockResolvedValue(0), - proposal: { - getAvailableDevices: jest.fn().mockResolvedValue([vda, vdb]), - getEncryptionMethods: jest.fn().mockResolvedValue([]), - getProductMountPoints: jest.fn().mockResolvedValue([]), - getResult: jest.fn().mockResolvedValue(undefined), - defaultVolume: jest.fn(mountPath => Promise.resolve({ mountPath })), - calculate: jest.fn().mockResolvedValue(0), - }, - system: { - getDevices: jest.fn().mockResolvedValue([vda, vdb]) - }, - staging: { - getDevices: jest.fn().mockResolvedValue([vda]) - }, - getErrors: jest.fn().mockResolvedValue([]), - isDeprecated: jest.fn().mockResolvedValue(false), - onDeprecate: jest.fn(), - onStatusChange: jest.fn() +/** + * @param {string} mountPath + * @returns {Volume} + */ +const volume = (mountPath) => { + return ( + { + mountPath, + target: "DEFAULT", + fsType: "Btrfs", + minSize: 1024, + maxSize: 1024, + autoSize: false, + snapshots: false, + transactional: false, + outline: { + required: false, + fsTypes: ["Btrfs"], + supportAutoSize: false, + snapshotsConfigurable: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [], + adjustByRam: false + } + } + ); }; +/** @type {StorageClient} */ let storage; +/** @type {ProposalResult} */ +let proposalResult; + beforeEach(() => { - storage = { ...storageMock, proposal: { ...storageMock.proposal } }; + proposalResult = { + settings: { + target: "DISK", + targetPVDevices: [], + configureBoot: false, + bootDevice: "", + defaultBootDevice: "", + encryptionPassword: "", + encryptionMethod: "", + spacePolicy: "", + spaceActions: [], + volumes: [], + installationDevices: [] + }, + actions: [] + }; + + storage = { + probe: jest.fn().mockResolvedValue(0), + // @ts-expect-error Some methods have to be private to avoid type complaint. + proposal: { + getAvailableDevices: jest.fn().mockResolvedValue([vda, vdb]), + getEncryptionMethods: jest.fn().mockResolvedValue([]), + getProductMountPoints: jest.fn().mockResolvedValue([]), + getResult: jest.fn().mockResolvedValue(proposalResult), + defaultVolume: jest.fn(mountPath => Promise.resolve(volume(mountPath))), + calculate: jest.fn().mockResolvedValue(0), + }, + // @ts-expect-error Some methods have to be private to avoid type complaint. + system: { + getDevices: jest.fn().mockResolvedValue([vda, vdb]) + }, + // @ts-expect-error Some methods have to be private to avoid type complaint. + staging: { + getDevices: jest.fn().mockResolvedValue([vda]) + }, + getErrors: jest.fn().mockResolvedValue([]), + isDeprecated: jest.fn().mockResolvedValue(false), + onDeprecate: jest.fn(), + onStatusChange: jest.fn() + }; + + // @ts-expect-error createClient.mockImplementation(() => ({ storage })); }); @@ -117,9 +184,8 @@ it("does not probe storage if the storage devices are not deprecated", async () }); it("loads the proposal data", async () => { - storage.proposal.getResult = jest.fn().mockResolvedValue( - { settings: { target: "disk", targetDevice: vda.name } } - ); + proposalResult.settings.target = "DISK"; + proposalResult.settings.targetDevice = vda.name; installerRender(); @@ -152,8 +218,8 @@ describe("when the storage devices become deprecated", () => { }); it("loads the proposal data", async () => { - const result = { settings: { target: "disk", targetDevice: vda.name } }; - storage.proposal.getResult = jest.fn().mockResolvedValue(result); + proposalResult.settings.target = "DISK"; + proposalResult.settings.targetDevice = vda.name; const [mockFunction, callbacks] = createCallbackMock(); storage.onDeprecate = mockFunction; @@ -162,7 +228,7 @@ describe("when the storage devices become deprecated", () => { await screen.findByText(/\/dev\/vda/); - result.settings.targetDevice = vdb.name; + proposalResult.settings.targetDevice = vdb.name; const [onDeprecateCb] = callbacks; await act(() => onDeprecateCb()); @@ -172,11 +238,9 @@ describe("when the storage devices become deprecated", () => { }); describe("when there is no proposal yet", () => { - beforeEach(() => { - storage.proposal.getResult = jest.fn().mockResolvedValue(undefined); - }); - it("shows the page as loading", async () => { + proposalResult = undefined; + installerRender(); screen.getAllByText(/PFSkeleton/); @@ -184,6 +248,9 @@ describe("when there is no proposal yet", () => { }); it("loads the proposal when the service finishes to calculate", async () => { + const defaultResult = proposalResult; + proposalResult = undefined; + const [mockFunction, callbacks] = createCallbackMock(); storage.onStatusChange = mockFunction; @@ -191,9 +258,9 @@ describe("when there is no proposal yet", () => { screen.getAllByText(/PFSkeleton/); - storage.proposal.getResult = jest.fn().mockResolvedValue( - { settings: { target: "disk", targetDevice: vda.name } } - ); + proposalResult = defaultResult; + proposalResult.settings.target = "DISK"; + proposalResult.settings.targetDevice = vda.name; const [onStatusChangeCb] = callbacks; await act(() => onStatusChangeCb(IDLE)); @@ -203,14 +270,13 @@ describe("when there is no proposal yet", () => { describe("when there is a proposal", () => { beforeEach(() => { - storage.proposal.getResult = jest.fn().mockResolvedValue( - { settings: { target: "disk", targetDevice: vda.name } } - ); + proposalResult.settings.target = "DISK"; + proposalResult.settings.targetDevice = vda.name; }); it("does not load the proposal when the service finishes to calculate", async () => { const [mockFunction, callbacks] = createCallbackMock(); - storage.proposal.onStatusChange = mockFunction; + storage.onStatusChange = mockFunction; installerRender(); diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 2b15463f95..03e05df65b 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -19,11 +19,19 @@ * find current contact information at www.suse.com. */ +// @ts-check + import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalSettingsSection } from "~/components/storage"; +/** + * @typedef {import ("~/components/storage/ProposalSettingsSection").ProposalSettingsSectionProps} ProposalSettingsSectionProps + * @typedef {import ("~/client/storage").StorageDevice} StorageDevice + * @typedef {import ("~/client/storage").Volume} Volume + */ + jest.mock("@patternfly/react-core", () => { const original = jest.requireActual("@patternfly/react-core"); @@ -33,20 +41,57 @@ jest.mock("@patternfly/react-core", () => { }; }); +/** @type {Volume} */ +let volume; + +/** @type {ProposalSettingsSectionProps} */ let props; beforeEach(() => { + volume = { + mountPath: "/", + target: "DEFAULT", + fsType: "Btrfs", + minSize: 1024, + maxSize: 2048, + autoSize: false, + snapshots: false, + transactional: false, + outline: { + required: true, + fsTypes: ["Btrfs", "Ext4"], + supportAutoSize: true, + snapshotsConfigurable: true, + snapshotsAffectSizes: true, + sizeRelevantVolumes: [], + adjustByRam: false + } + }; + props = { - settings: {}, + settings: { + target: "DISK", + targetPVDevices: [], + configureBoot: false, + bootDevice: "", + defaultBootDevice: "", + encryptionPassword: "", + encryptionMethod: "", + spacePolicy: "", + spaceActions: [], + volumes: [], + installationDevices: [] + }, + availableDevices: [], + encryptionMethods: [], + volumeTemplates: [], onChange: jest.fn() }; }); -const rootVolume = { mountPath: "/", fsType: "Btrfs", outline: { snapshotsConfigurable: true } }; - describe("if snapshots are configurable", () => { beforeEach(() => { - props.settings = { volumes: [rootVolume] }; + props.settings.volumes = [volume]; }); it("renders the snapshots switch", () => { @@ -58,7 +103,7 @@ describe("if snapshots are configurable", () => { describe("if snapshots are not configurable", () => { beforeEach(() => { - props.settings = { volumes: [{ ...rootVolume, outline: { ...rootVolume.outline, snapshotsConfigurable: false } }] }; + volume.outline.snapshotsConfigurable = false; }); it("does not render the snapshots switch", () => { @@ -91,9 +136,10 @@ it("requests a volume change when onChange callback is triggered", async () => { }); describe("Encryption field", () => { - describe("if encryption password setting is not set yet", () => { + describe.skip("if encryption password setting is not set yet", () => { beforeEach(() => { - props.settings = {}; + // Currently settings cannot be undefined. + props.settings = undefined; }); it("does not render the encryption switch", () => { @@ -105,7 +151,7 @@ describe("Encryption field", () => { describe("if encryption password setting is set", () => { beforeEach(() => { - props.settings = { encryptionPassword: "" }; + props.settings.encryptionPassword = ""; }); it("renders the encryption switch", () => { @@ -117,8 +163,7 @@ describe("Encryption field", () => { describe("if encryption password is not empty", () => { beforeEach(() => { - props.settings = { encryptionPassword: "1234" }; - props.onChange = jest.fn(); + props.settings.encryptionPassword = "1234"; }); it("renders the encryption switch as selected", () => { @@ -179,8 +224,7 @@ describe("Encryption field", () => { describe("if encryption password is empty", () => { beforeEach(() => { - props.settings = { encryptionPassword: "" }; - props.onChange = jest.fn(); + props.settings.encryptionPassword = ""; }); it("renders the encryption switch as not selected", () => { @@ -239,9 +283,10 @@ describe("Encryption field", () => { }); describe("Space policy field", () => { - describe("if there is no space policy", () => { + describe.skip("if there is no space policy", () => { beforeEach(() => { - props.settings = {}; + // Currently settings cannot be undefined. + props.settings = undefined; }); it("does not render the space policy field", () => { @@ -253,15 +298,15 @@ describe("Space policy field", () => { describe("if there is a space policy", () => { beforeEach(() => { - props.settings = { - spacePolicy: "delete" - }; + props.settings.spacePolicy = "delete"; }); it("renders the button with a text according to given policy", () => { const { rerender } = plainRender(); screen.getByRole("button", { name: /deleting/ }); - rerender(); + + props.settings.spacePolicy = "resize"; + rerender(); screen.getByRole("button", { name: /shrinking/ }); });