From 534cf595d296dd723a0ca1a6bc18fc1aa5f3626f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 09:42:21 +0000 Subject: [PATCH 01/27] Add tests for searched device menu (ai) --- .../storage/SearchedDeviceMenu.test.tsx | 239 ++++++++++++++++++ .../components/storage/SearchedDeviceMenu.tsx | 1 - 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 web/src/components/storage/SearchedDeviceMenu.test.tsx diff --git a/web/src/components/storage/SearchedDeviceMenu.test.tsx b/web/src/components/storage/SearchedDeviceMenu.test.tsx new file mode 100644 index 0000000000..dbaaa88962 --- /dev/null +++ b/web/src/components/storage/SearchedDeviceMenu.test.tsx @@ -0,0 +1,239 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import SearchedDeviceMenu from "./SearchedDeviceMenu"; +import { model } from "~/storage"; +import type { storage as system } from "~/api/system"; + +const mockDeleteFn = jest.fn(); +const mockSwitchToDrive = jest.fn(); + +const mockSda: system.Device = { + sid: 1, + class: "drive", + name: "/dev/sda", + block: { size: 1000, start: 0, shrinking: { supported: false } }, +}; + +const mockSdb: system.Device = { + sid: 2, + class: "drive", + name: "/dev/sdb", + block: { size: 2000, start: 0, shrinking: { supported: false } }, +}; + +const mockSdc: system.Device = { + sid: 3, + class: "drive", + name: "/dev/sdc", + block: { size: 3000, start: 0, shrinking: { supported: false } }, +}; + +const mockBaseDrive: model.Drive = { + name: "/dev/sda", + isUsed: true, + isBoot: false, + isExplicitBoot: false, + isTargetDevice: false, + isReusingPartitions: false, + partitions: [], + getMountPaths: () => [], + getVolumeGroups: () => [], + filesystem: undefined, + isAddingPartitions: false, + getConfiguredExistingPartitions: () => [], + getPartition: () => undefined, +}; + +jest.mock("~/hooks/storage/drive", () => ({ + useSwitchToDrive: () => mockSwitchToDrive, +})); + +jest.mock("~/hooks/storage/md-raid", () => ({ + useSwitchToMdRaid: () => jest.fn(), +})); + +jest.mock("./DeviceSelectorModal", () => ({ + __esModule: true, + default: ({ onConfirm, onCancel, title, description }) => ( +
+

{title}

+

{description}

+ + +
+ ), +})); + +jest.mock("./NewVgMenuOption", () => ({ + __esModule: true, + default: () =>
New VG Option
, +})); + +const mockUseModel = jest.fn(); +jest.mock("~/hooks/storage/model", () => ({ + useModel: () => mockUseModel(), +})); + +const mockUseAvailableDevices = jest.fn(); +jest.mock("~/hooks/api/system/storage", () => ({ + useAvailableDevices: () => mockUseAvailableDevices(), +})); + +const renderMenu = (modelDevice: model.Drive) => { + const { user } = installerRender( + Toggle Menu} + />, + ); + return { user }; +}; + +describe("SearchedDeviceMenu", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseAvailableDevices.mockReturnValue([mockSda, mockSdb, mockSdc]); + mockUseModel.mockReturnValue({ + drives: [mockBaseDrive, { ...mockBaseDrive, name: "/dev/sdb" }], + mdRaids: [], + }); + }); + + it("renders the menu with options", async () => { + const { user } = renderMenu(mockBaseDrive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect(screen.getByRole("menuitem", { name: /Change the disk to configure/ })).toBeVisible(); + expect(screen.getByTestId("new-vg-option")).toBeVisible(); + expect(screen.getByRole("menuitem", { name: /Do not use/ })).toBeVisible(); + }); + + describe("ChangeDeviceMenuItem", () => { + it("opens the device selector modal on click", async () => { + const { user } = renderMenu(mockBaseDrive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + await user.click(screen.getByRole("menuitem", { name: /Change the disk to configure/ })); + + expect(screen.getByTestId("device-selector-modal")).toBeVisible(); + }); + + it("calls switchToDrive when a new device is confirmed", async () => { + const { user } = renderMenu(mockBaseDrive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + await user.click(screen.getByRole("menuitem", { name: /Change the disk to configure/ })); + + await user.click(screen.getByRole("button", { name: "Confirm" })); + + expect(mockSwitchToDrive).toHaveBeenCalledWith("/dev/sda", { name: "/dev/sdb" }); + expect(screen.queryByTestId("device-selector-modal")).not.toBeInTheDocument(); + }); + + it("is disabled when there is only one option", async () => { + const { user } = renderMenu({ ...mockBaseDrive, isReusingPartitions: true }); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + const changeDeviceItem = screen.getByRole("menuitem", { + name: /Selected disk cannot be changed/, + }); + expect(changeDeviceItem).toBeDisabled(); + const description = within(changeDeviceItem).getByText( + "This uses existing partitions at the disk", + ); + expect(description).toBeInTheDocument(); + }); + + it("shows correct title when installing the system", async () => { + const { user } = renderMenu({ + ...mockBaseDrive, + getMountPaths: () => ["/"], + partitions: [{ mountPath: "/", isNew: true }], + } as unknown as model.Drive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect( + screen.getByRole("menuitem", { name: /Change the disk to install the system/ }), + ).toBeVisible(); + }); + }); + + describe("RemoveEntryOption", () => { + it("calls deleteFn on click", async () => { + const { user } = renderMenu(mockBaseDrive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + await user.click(screen.getByRole("menuitem", { name: /Do not use/ })); + + expect(mockDeleteFn).toHaveBeenCalledWith(mockBaseDrive); + }); + + it("is disabled when device is used for LVM", async () => { + const { user } = renderMenu({ ...mockBaseDrive, isTargetDevice: true }); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect(screen.queryByRole("menuitem", { name: /Do not use/ })).not.toBeInTheDocument(); + }); + + it("is disabled when device is used for boot", async () => { + const { user } = renderMenu({ ...mockBaseDrive, isExplicitBoot: true }); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect(screen.queryByRole("menuitem", { name: /Do not use/ })).not.toBeInTheDocument(); + }); + + it("is not rendered when device is used for LVM and boot", async () => { + const { user } = renderMenu({ + ...mockBaseDrive, + isTargetDevice: true, + isExplicitBoot: true, + }); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect(screen.queryByRole("menuitem", { name: /Do not use/ })).not.toBeInTheDocument(); + }); + + it("is not rendered if there are not enough additional drives", async () => { + mockUseModel.mockReturnValue({ + drives: [mockBaseDrive], + mdRaids: [], + }); + const { user } = renderMenu(mockBaseDrive); + + await user.click(screen.getByRole("button", { name: "Toggle Menu" })); + + expect(screen.queryByRole("menuitem", { name: /Do not use/ })).not.toBeInTheDocument(); + }); + }); +}); diff --git a/web/src/components/storage/SearchedDeviceMenu.tsx b/web/src/components/storage/SearchedDeviceMenu.tsx index 406e2505b5..7bd69e17db 100644 --- a/web/src/components/storage/SearchedDeviceMenu.tsx +++ b/web/src/components/storage/SearchedDeviceMenu.tsx @@ -185,7 +185,6 @@ const ChangeDeviceMenuItem = ({ return ( } isDisabled={onlyOneOption} {...props} From ab307fcea04ba9c84732b86c2998f5d587a93bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 10:26:40 +0000 Subject: [PATCH 02/27] Fix drive editor tests (ai) --- .../components/storage/DriveEditor.test.tsx | 337 +++++------------- 1 file changed, 90 insertions(+), 247 deletions(-) diff --git a/web/src/components/storage/DriveEditor.test.tsx b/web/src/components/storage/DriveEditor.test.tsx index c08e041067..3bd402a25a 100644 --- a/web/src/components/storage/DriveEditor.test.tsx +++ b/web/src/components/storage/DriveEditor.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -21,279 +21,122 @@ */ import React from "react"; -import { screen, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import DriveEditor from "~/components/storage/DriveEditor"; -import { StorageDevice, model } from "~/storage"; -import { Volume } from "~/api/storage/types"; +import { screen, fireEvent } from "@testing-library/react"; -const mockDeleteDrive = jest.fn(); -const mockSwitchToDrive = jest.fn(); -const mockUseModel = jest.fn(); +import { installerRender as render } from "~/test-utils"; +import DriveEditor from "./DriveEditor"; +import { model } from "~/storage"; +import { storage as system } from "~/api/system"; -const volume1: Volume = { - mountPath: "/", - mountOptions: [], - target: "default", - fsType: "Btrfs", - minSize: 1024, - maxSize: 1024, - autoSize: false, - snapshots: false, - transactional: false, - outline: { - required: true, - fsTypes: ["Btrfs"], - supportAutoSize: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [], - adjustByRam: false, - }, -}; +// Mock dependencies +jest.mock("~/hooks/storage/model", () => ({ + useDrive: jest.fn(), + useModel: jest.fn(), +})); -const volume2: Volume = { - mountPath: "swap", - mountOptions: [], - target: "default", - fsType: "Swap", - minSize: 1024, - maxSize: 1024, - autoSize: false, - snapshots: false, - transactional: false, - outline: { - required: false, - fsTypes: ["Swap"], - supportAutoSize: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [], - adjustByRam: false, - }, -}; +jest.mock("~/hooks/api/system/storage", () => ({ + useDevice: jest.fn(), + useAvailableDevices: jest.fn(() => []), +})); -const volume3: Volume = { - mountPath: "/home", - mountOptions: [], - target: "default", - fsType: "XFS", - minSize: 1024, - maxSize: 1024, - autoSize: false, - snapshots: false, - transactional: false, - outline: { - required: false, - fsTypes: ["XFS"], - supportAutoSize: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [], - adjustByRam: false, - }, -}; +jest.mock("~/components/storage/DeviceEditorContent", () => () => ( +
+)); -const sda: StorageDevice = { - sid: 59, - isDrive: true, - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: 1024, - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], - description: "", -}; +const deleteDriveFn = jest.fn(); +jest.mock("~/hooks/storage/drive", () => ({ + useDeleteDrive: jest.fn(() => deleteDriveFn), + useSwitchToDrive: jest.fn(() => jest.fn()), +})); -const sdb: StorageDevice = { - sid: 60, - isDrive: true, - type: "disk", - name: "/dev/sdb", - size: 1024, - description: "", -}; +jest.mock("~/hooks/storage/md-raid", () => ({ + useSwitchToMdRaid: jest.fn(() => jest.fn()), +})); -const drive1Partitions: model.Partition[] = [ - { - mountPath: "/", - size: { - min: 1_000_000_000, - default: true, - }, - filesystem: { default: true, type: "btrfs" }, - isNew: true, - isUsed: false, - isReused: false, - isUsedBySpacePolicy: false, - }, - { - mountPath: "swap", - size: { - min: 2_000_000_000, - default: false, // false: user provided, true: calculated - }, - filesystem: { default: false, type: "swap" }, - isNew: true, - isUsed: false, - isReused: false, - isUsedBySpacePolicy: false, - }, -]; +jest.mock("~/hooks/storage/volume-group", () => ({ + useConvertToVolumeGroup: jest.fn(() => jest.fn()), +})); + +const useDriveMock = jest.requireMock("~/hooks/storage/model").useDrive; +const useModelMock = jest.requireMock("~/hooks/storage/model").useModel; +const useDeviceMock = jest.requireMock("~/hooks/api/system/storage").useDevice; -const drive1 = { - name: "/dev/sda", - spacePolicy: "delete", - partitions: drive1Partitions, - list: "drives", - listIndex: 1, +const driveModelMock: model.Drive = { + name: "sda", isUsed: true, - isAddingPartitions: true, + isExplicitBoot: false, + isAddingPartitions: false, + isReusingPartitions: false, isTargetDevice: false, - isBoot: true, - isExplicitBoot: true, + isBoot: false, + partitions: [], + getMountPaths: () => [], getVolumeGroups: () => [], - getPartition: jest.fn(), - getMountPaths: () => drive1Partitions.map((p) => p.mountPath), - getConfiguredExistingPartitions: jest.fn(), + getPartition: () => undefined, + getConfiguredExistingPartitions: () => [], + // apiModel.Drive properties + spacePolicy: "custom", }; -const drive2Partitions: model.Partition[] = [ - { - mountPath: "/home", - size: { - min: 1_000_000_000, - default: true, - }, - filesystem: { default: true, type: "xfs" }, - isNew: true, - isUsed: false, - isReused: false, - isUsedBySpacePolicy: false, - }, -]; - -const drive2 = { - name: "/dev/sdb", - spacePolicy: "delete", - partitions: drive2Partitions, - list: "drives", - listIndex: 2, - isExplicitBoot: false, - isUsed: true, - isAddingPartitions: true, - isTargetDevice: false, - isBoot: true, - getVolumeGroups: () => [], - getPartition: jest.fn(), - getMountPaths: () => drive2Partitions.map((p) => p.mountPath), - getConfiguredExistingPartitions: jest.fn(), +const anotherDriveModelMock: model.Drive = { + ...driveModelMock, + name: "sdb", }; -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useVolume: (mountPath: string): Volume => - [volume1, volume2, volume3].find((v) => v.mountPath === mountPath), -})); +const deviceMock: system.Device = { + sid: 1, + name: "sda", + class: "drive", + drive: { + type: "disk", + }, +}; -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), - useAvailableDevices: () => [sda, sdb], - useCandidateDevices: () => [sda], -})); +describe("DriveEditor", () => { + afterEach(() => { + jest.clearAllMocks(); + }); -jest.mock("~/hooks/storage/drive", () => ({ - ...jest.requireActual("~/hooks/storage/drive"), - __esModule: true, - useDeleteDrive: () => mockDeleteDrive, - useSwitchToDrive: () => mockSwitchToDrive, -})); + it("should render null if device is not found", () => { + useDriveMock.mockReturnValue(driveModelMock); + useDeviceMock.mockReturnValue(undefined); -jest.mock("~/hooks/storage/model", () => ({ - ...jest.requireActual("~/hooks/storage/model"), - useModel: () => mockUseModel(), -})); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); -describe("RemoveDriveOption", () => { - describe("if there are additional drives", () => { - beforeEach(() => { - mockUseModel.mockReturnValue({ drives: [drive1, drive2], mdRaids: [] }); + it("should render the editor for the given drive", () => { + useDriveMock.mockReturnValue(driveModelMock); + useDeviceMock.mockReturnValue(deviceMock); + useModelMock.mockReturnValue({ + drives: [driveModelMock, anotherDriveModelMock], + mdRaids: [], }); - it("allows users to delete regular drives", async () => { - // @ts-expect-error: drives are not typed on purpose because - // isReusingPartitions should be a calculated data. Mocking needs a lot of - // improvements. - const { user } = installerRender(); - - const changeButton = screen.getByRole("button", { name: /Use disk sdb/ }); - await user.click(changeButton); - const drivesMenu = screen.getByRole("menu", { name: "Device /dev/sdb menu" }); - const deleteDriveButton = within(drivesMenu).getByRole("menuitem", { - name: /Do not use/, - }); - await user.click(deleteDriveButton); - expect(mockDeleteDrive).toHaveBeenCalled(); - }); + render(); - it("does not allow users to delete drives explicitly used to boot", async () => { - // @ts-expect-error: drives are not typed on purpose because - // isReusingPartitions should be a calculated data. Mocking needs a lot of - // improvements. - const { user } = installerRender(); + expect(screen.getByText(/sda/)).toBeInTheDocument(); + expect(screen.getByTestId("device-editor-content")).toBeInTheDocument(); + }); - const changeButton = screen.getByRole("button", { name: /Use disk sda/ }); - await user.click(changeButton); - const drivesMenu = screen.getByRole("menu", { name: "Device /dev/sda menu" }); - const deleteDriveButton = within(drivesMenu).queryByRole("menuitem", { - name: /Do not use/, - }); - expect(deleteDriveButton).toBeDisabled(); + it("should call delete drive when 'Do not use' is clicked", () => { + useDriveMock.mockReturnValue(driveModelMock); + useDeviceMock.mockReturnValue(deviceMock); + useModelMock.mockReturnValue({ + drives: [driveModelMock, anotherDriveModelMock], + mdRaids: [], }); - }); - describe("if there are no additional drives", () => { - it("does not allow users to delete regular drives", async () => { - mockUseModel.mockReturnValue({ drives: [drive2], mdRaids: [] }); - // @ts-expect-error: drives are not typed on purpose because - // isReusingPartitions should be a calculated data. Mocking needs a lot of - // improvements. - const { user } = installerRender(); + render(); - const changeButton = screen.getByRole("button", { name: /Use disk sdb/ }); - await user.click(changeButton); - const drivesMenu = screen.getByRole("menu", { name: "Device /dev/sdb menu" }); - const deleteDriveButton = within(drivesMenu).queryByRole("menuitem", { - name: /Do not use/, - }); - expect(deleteDriveButton).not.toBeInTheDocument(); - }); + // The component uses a custom toggle, we need to get the button by its content + const toggleButton = screen.getByText(/sda/).closest("button"); + expect(toggleButton).toBeInTheDocument(); + fireEvent.click(toggleButton!); - it("does not allow users to delete drives explicitly used to boot", async () => { - mockUseModel.mockReturnValue({ drives: [drive1], mdRaids: [] }); - // @ts-expect-error: drives are not typed on purpose because - // isReusingPartitions should be a calculated data. Mocking needs a lot of - // improvements. - const { user } = installerRender(); + const deleteButton = screen.getByText("Do not use"); + fireEvent.click(deleteButton); - const changeButton = screen.getByRole("button", { name: /Use disk sda/ }); - await user.click(changeButton); - const drivesMenu = screen.getByRole("menu", { name: "Device /dev/sda menu" }); - const deleteDriveButton = within(drivesMenu).queryByRole("menuitem", { - name: /Do not use/, - }); - expect(deleteDriveButton).not.toBeInTheDocument(); - }); + expect(deleteDriveFn).toHaveBeenCalledWith("sda"); }); }); From 3c0d08eb719d997080d5e39edf391950af9861f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 10:54:05 +0000 Subject: [PATCH 03/27] Add tests for device editor content (ai) --- .../storage/DeviceEditorContent.test.tsx | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 web/src/components/storage/DeviceEditorContent.test.tsx diff --git a/web/src/components/storage/DeviceEditorContent.test.tsx b/web/src/components/storage/DeviceEditorContent.test.tsx new file mode 100644 index 0000000000..65390a4d0e --- /dev/null +++ b/web/src/components/storage/DeviceEditorContent.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender as render } from "~/test-utils"; +import DeviceEditorContent from "./DeviceEditorContent"; +import { model } from "~/storage"; + +// Mock dependencies +jest.mock("~/hooks/storage/model", () => ({ + useDevice: jest.fn(), +})); + +jest.mock("~/components/storage/UnusedMenu", () => () =>
); + +jest.mock("~/components/storage/FilesystemMenu", () => () =>
); + +jest.mock("~/components/storage/PartitionsSection", () => () => ( +
+)); + +jest.mock("~/components/storage/SpacePolicyMenu", () => () => ( +
+)); + +const useDeviceMock = jest.requireMock("~/hooks/storage/model").useDevice; + +const driveModelMock: model.Drive = { + name: "sda", + isUsed: true, + isExplicitBoot: false, + isAddingPartitions: false, + isReusingPartitions: false, + isTargetDevice: false, + isBoot: false, + partitions: [], + getMountPaths: () => [], + getVolumeGroups: () => [], + getPartition: () => undefined, + getConfiguredExistingPartitions: () => [], + spacePolicy: "custom", +}; + +describe("DeviceEditorContent", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should render UnusedMenu when device is not used", () => { + useDeviceMock.mockReturnValue({ ...driveModelMock, isUsed: false }); + render(); + expect(screen.getByTestId("unused-menu")).toBeInTheDocument(); + expect(screen.queryByTestId("filesystem-menu")).not.toBeInTheDocument(); + expect(screen.queryByTestId("partitions-section")).not.toBeInTheDocument(); + expect(screen.queryByTestId("space-policy-menu")).not.toBeInTheDocument(); + }); + + it("should render FilesystemMenu when device is used and has a filesystem", () => { + useDeviceMock.mockReturnValue({ + ...driveModelMock, + isUsed: true, + filesystem: {}, + }); + render(); + expect(screen.queryByTestId("unused-menu")).not.toBeInTheDocument(); + expect(screen.getByTestId("filesystem-menu")).toBeInTheDocument(); + expect(screen.queryByTestId("partitions-section")).not.toBeInTheDocument(); + expect(screen.queryByTestId("space-policy-menu")).not.toBeInTheDocument(); + }); + + it("should render PartitionsSection and SpacePolicyMenu when device is used and has no filesystem", () => { + useDeviceMock.mockReturnValue({ + ...driveModelMock, + isUsed: true, + filesystem: undefined, + }); + render(); + expect(screen.queryByTestId("unused-menu")).not.toBeInTheDocument(); + expect(screen.queryByTestId("filesystem-menu")).not.toBeInTheDocument(); + expect(screen.getByTestId("partitions-section")).toBeInTheDocument(); + expect(screen.getByTestId("space-policy-menu")).toBeInTheDocument(); + }); +}); From d174306c440a20137898bba987e22441eb6a9f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 11:18:20 +0000 Subject: [PATCH 04/27] Add tests for unused menu (ai) --- .../components/storage/UnusedMenu.test.tsx | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 web/src/components/storage/UnusedMenu.test.tsx diff --git a/web/src/components/storage/UnusedMenu.test.tsx b/web/src/components/storage/UnusedMenu.test.tsx new file mode 100644 index 0000000000..ff4bcb2a35 --- /dev/null +++ b/web/src/components/storage/UnusedMenu.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender as render, mockNavigateFn } from "~/test-utils"; +import UnusedMenu from "./UnusedMenu"; +import { STORAGE as PATHS } from "~/routes/paths"; +import { generateEncodedPath } from "~/utils"; + +// Mock useNavigate and generateEncodedPath +jest.mock("react-router", () => ({ + ...jest.requireActual("react-router"), + useNavigate: () => mockNavigateFn, +})); + +jest.mock("~/utils", () => ({ + ...jest.requireActual("~/utils"), + generateEncodedPath: jest.fn((path, params) => { + // Basic mock implementation for testing navigation + if (path === PATHS.addPartition) { + return `/add-partition/${params.collection}/${params.index}`; + } + if (path === PATHS.formatDevice) { + return `/format-device/${params.collection}/${params.index}`; + } + return `${path}-${params.collection}-${params.index}`; + }), +})); + +describe("UnusedMenu", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should render the toggle button with description", () => { + render(); + expect(screen.getByRole("button", { name: "Not configured yet" })).toBeInTheDocument(); + }); + + it("should open the menu when the toggle is clicked", async () => { + const { user } = render(); + const toggleButton = screen.getByRole("button", { name: "Not configured yet" }); + + await user.click(toggleButton); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + expect(screen.getByText("Add or use partition")).toBeInTheDocument(); + expect( + screen.getByText("Format the whole device or mount an existing file system"), + ).toBeInTheDocument(); + }); + + it("should navigate to add partition path when 'Add or use partition' is clicked", async () => { + const { user } = render(); + const toggleButton = screen.getByRole("button", { name: "Not configured yet" }); + + await user.click(toggleButton); + + const addPartitionItem = screen.getByText("Add or use partition"); + await user.click(addPartitionItem); + + const expectedPath = generateEncodedPath(PATHS.addPartition, { + collection: "drives", + index: 0, + }); + expect(mockNavigateFn).toHaveBeenCalledWith(expectedPath); + }); + + it("should navigate to format device path when 'Use the disk without partitions' is clicked for drives", async () => { + const { user } = render(); + const toggleButton = screen.getByRole("button", { name: "Not configured yet" }); + + await user.click(toggleButton); + + const formatDeviceItem = screen.getByText("Use the disk without partitions"); + await user.click(formatDeviceItem); + + const expectedPath = generateEncodedPath(PATHS.formatDevice, { + collection: "drives", + index: 0, + }); + expect(mockNavigateFn).toHaveBeenCalledWith(expectedPath); + }); + + it("should navigate to format device path when 'Use the RAID without partitions' is clicked for mdRaids", async () => { + const { user } = render(); + const toggleButton = screen.getByRole("button", { name: "Not configured yet" }); + + await user.click(toggleButton); + + const formatDeviceItem = screen.getByText("Use the RAID without partitions"); + await user.click(formatDeviceItem); + + const expectedPath = generateEncodedPath(PATHS.formatDevice, { + collection: "mdRaids", + index: 1, + }); + expect(mockNavigateFn).toHaveBeenCalledWith(expectedPath); + }); +}); From aaa6345a3bc776ede3952f780798fe81c47e29cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 11:27:43 +0000 Subject: [PATCH 05/27] Add tests for filesystem menu (ai) --- .../storage/FilesystemMenu.test.tsx | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 web/src/components/storage/FilesystemMenu.test.tsx diff --git a/web/src/components/storage/FilesystemMenu.test.tsx b/web/src/components/storage/FilesystemMenu.test.tsx new file mode 100644 index 0000000000..7454d141b2 --- /dev/null +++ b/web/src/components/storage/FilesystemMenu.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender as render, mockNavigateFn } from "~/test-utils"; +import FilesystemMenu from "./FilesystemMenu"; +import { model } from "~/storage"; +import { STORAGE as PATHS } from "~/routes/paths"; +import { generatePath } from "react-router"; + +// Mock useNavigate +jest.mock("react-router", () => ({ + ...jest.requireActual("react-router"), + useNavigate: () => mockNavigateFn, + generatePath: jest.fn( + (path, params) => `/generated-path/${path}-${params.collection}-${params.index}`, + ), +})); + +// Mock useDevice hook +const useDeviceMock = jest.fn(); +jest.mock("~/hooks/storage/model", () => ({ + useDevice: () => useDeviceMock(), +})); + +describe("FilesystemMenu", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should render the toggle button and open the menu on click", async () => { + const mockDeviceModel: Partial = { + isUsed: true, + filesystem: { type: "btrfs", default: false, snapshots: true }, + mountPath: "/home", + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + const { user } = render(); + + // Test that the toggle button renders with the correct description + const toggleButton = screen.getByRole("button", { + name: 'The device will be formatted as Btrfs with snapshots and mounted at "/home"', + }); + expect(toggleButton).toBeInTheDocument(); + + // Open the menu + await user.click(toggleButton); + expect(screen.getByRole("menu")).toBeInTheDocument(); + expect(screen.getByText("Edit")).toBeInTheDocument(); + }); + + it("should navigate to edit filesystem path when 'Edit' is clicked", async () => { + const mockDeviceModel: Partial = { + isUsed: true, + filesystem: { type: "btrfs", default: false, snapshots: true }, + mountPath: "/home", + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + const { user } = render(); + const toggleButton = screen.getByRole("button", { + name: 'The device will be formatted as Btrfs with snapshots and mounted at "/home"', + }); + await user.click(toggleButton); + + const editItem = screen.getByText("Edit"); + await user.click(editItem); + + const expectedPath = generatePath(PATHS.formatDevice, { collection: "drives", index: 0 }); + expect(mockNavigateFn).toHaveBeenCalledWith(expectedPath); + }); + + describe("deviceDescription function logic", () => { + it("should return 'The device will be mounted' when no mount path and reuse = true", () => { + const mockDeviceModel: Partial = { + filesystem: { reuse: true, default: false }, + mountPath: undefined, + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + render(); + expect( + screen.getByRole("button", { name: "The device will be mounted" }), + ).toBeInTheDocument(); + }); + + it("should return 'The device will be formatted' when no mount path and reuse = false", () => { + const mockDeviceModel: Partial = { + filesystem: { reuse: false, default: false }, + mountPath: undefined, + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + render(); + expect( + screen.getByRole("button", { name: "The device will be formatted" }), + ).toBeInTheDocument(); + }); + + it("should return 'The current file system will be mounted at \"/home\"' when mount path and reuse = true", () => { + const mockDeviceModel: Partial = { + filesystem: { reuse: true, default: false }, + mountPath: "/home", + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + render(); + expect( + screen.getByRole("button", { name: 'The current file system will be mounted at "/home"' }), + ).toBeInTheDocument(); + }); + + it("should return 'The device will be formatted as XFS and mounted at \"/var\"' when mount path and reuse = false", () => { + const mockDeviceModel: Partial = { + filesystem: { reuse: false, default: false, type: "xfs" }, + mountPath: "/var", + }; + useDeviceMock.mockReturnValue(mockDeviceModel); + + render(); + expect( + screen.getByRole("button", { + name: 'The device will be formatted as XFS and mounted at "/var"', + }), + ).toBeInTheDocument(); + }); + }); +}); From e72ecd052a64298fb6e059f0859149bfe062a82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 12:08:40 +0000 Subject: [PATCH 06/27] Fix partitions section tests (ai) --- .../components/storage/PartitionsSection.test.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/src/components/storage/PartitionsSection.test.tsx b/web/src/components/storage/PartitionsSection.test.tsx index 4cd9f2ff80..ae511e4a11 100644 --- a/web/src/components/storage/PartitionsSection.test.tsx +++ b/web/src/components/storage/PartitionsSection.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, mockNavigateFn } from "~/test-utils"; import PartitionsSection from "~/components/storage/PartitionsSection"; -import { apiModel } from "~/api/storage/types"; +import { model as apiModel } from "~/api/storage"; import { model } from "~/storage"; const partition1: apiModel.Partition = { @@ -68,8 +68,6 @@ const drive1: model.Drive = { name: "/dev/sda", spacePolicy: "delete", partitions: drive1PartitionsModel, - list: "drives", - listIndex: 0, isExplicitBoot: false, isUsed: true, isAddingPartitions: true, @@ -89,8 +87,13 @@ jest.mock("~/hooks/storage/partition", () => ({ useDeletePartition: () => mockDeletePartition, })); -async function openMenu(path) { - const { user } = installerRender(); +jest.mock("~/hooks/storage/model", () => ({ + ...jest.requireActual("~/hooks/storage/model"), + useDevice: () => drive1, +})); + +async function openMenu(path: string) { + const { user } = installerRender(); const detailsButton = screen.getByRole("button", { name: /New partitions/ }); await user.click(detailsButton); From a1447f322d2e68338d452d5d2e546a41a46f37fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 14:47:03 +0000 Subject: [PATCH 07/27] Add tests for space policy menu (ai) --- .../storage/SpacePolicyMenu.test.tsx | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 web/src/components/storage/SpacePolicyMenu.test.tsx diff --git a/web/src/components/storage/SpacePolicyMenu.test.tsx b/web/src/components/storage/SpacePolicyMenu.test.tsx new file mode 100644 index 0000000000..621be894c0 --- /dev/null +++ b/web/src/components/storage/SpacePolicyMenu.test.tsx @@ -0,0 +1,200 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, fireEvent, act, waitFor } from "@testing-library/react"; +import { installerRender, mockNavigateFn } from "~/test-utils"; +import SpacePolicyMenu from "./SpacePolicyMenu"; +import * as driveUtils from "~/components/storage/utils/drive"; +import { generateEncodedPath } from "~/utils"; + +// Mock hooks +const mockUseSetSpacePolicy = jest.fn(); +const mockUseDeviceModel = jest.fn(); +const mockUseDevice = jest.fn(); +const mockUseNavigate = jest.fn(); + +jest.mock("react-router", () => ({ + ...jest.requireActual("react-router"), + useNavigate: () => mockUseNavigate(), +})); + +jest.mock("~/hooks/storage/space-policy", () => ({ + useSetSpacePolicy: () => mockUseSetSpacePolicy(), +})); + +jest.mock("~/hooks/storage/model", () => ({ + useDevice: (collection, index) => mockUseDeviceModel(collection, index), +})); + +jest.mock("~/hooks/api/system/storage", () => ({ + useDevice: (name) => mockUseDevice(name), +})); + +// Mock utilities +jest.mock("~/components/storage/utils/drive", () => ({ + ...jest.requireActual("~/components/storage/utils/drive"), + contentActionsSummary: jest.fn(), + contentActionsDescription: jest.fn(), + spacePolicyEntry: jest.fn(), +})); + +jest.mock("~/utils", () => ({ + ...jest.requireActual("~/utils"), + generateEncodedPath: jest.fn(), +})); + +// Mock radashi's isEmpty +jest.mock("radashi", () => ({ + ...jest.requireActual("radashi"), + isEmpty: jest.fn(), +})); + +jest.mock("~/components/storage/utils", () => ({ + SPACE_POLICIES: [ + { id: "do_not_format", label: "Delete current content" }, + { id: "keep_data", label: "Keep existing data" }, + { id: "shrink", label: "Shrink existing partitions" }, + { id: "use_available", label: "Use available space" }, + { id: "custom", label: "Custom" }, + { id: "format", label: "Format" }, + ], +})); + +const mockDeviceModel = { + name: "/dev/sda", + spacePolicy: { type: "do_not_format" }, +}; + +const mockDevice = { + name: "/dev/sda", + partitions: [{ name: "/dev/sda1" }], +}; + +describe("SpacePolicyMenu", () => { + beforeEach(() => { + jest.clearAllMocks(); + // jest.useFakeTimers(); + mockUseNavigate.mockReturnValue(mockNavigateFn); + mockUseSetSpacePolicy.mockReturnValue(jest.fn()); + mockUseDeviceModel.mockReturnValue(mockDeviceModel); + mockUseDevice.mockReturnValue(mockDevice); + (driveUtils.contentActionsSummary as jest.Mock).mockReturnValue("Summary text"); + (driveUtils.contentActionsDescription as jest.Mock).mockReturnValue("Description text"); + (driveUtils.spacePolicyEntry as jest.Mock).mockReturnValue({ + id: "do_not_format", + label: "Do not format", + }); + (generateEncodedPath as jest.Mock).mockReturnValue("/path/to/edit"); + // By default, assume partitions exist so the menu renders + jest.mocked(require("radashi").isEmpty).mockReturnValue(false); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should render the SpacePolicyMenu with correct initial state", async () => { + const { user } = installerRender(); + + const toggleButton = screen.getByRole("button", { name: "Summary text" }); // Get the toggle button + expect(toggleButton).toBeInTheDocument(); + expect(toggleButton).toHaveAttribute("aria-expanded", "false"); // Initially closed + + await act(async () => { + user.click(toggleButton); // Click the toggle button + jest.runAllTimers(); // Advance timers + }); + + // Now, wait for the menu to expand. The "Do not format" item should be visible inside the expanded menu. + await waitFor(() => { + expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Check if expanded + expect(screen.getByText("Delete current content")).toBeInTheDocument(); // Now assert the item visibility + }); + }); + + it("should not render the SpacePolicyMenu if existingPartitions is empty", () => { + jest.mocked(require("radashi").isEmpty).mockReturnValue(true); + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should call setSpacePolicy when a non-custom policy is selected", async () => { + const setSpacePolicyMock = jest.fn(); + mockUseSetSpacePolicy.mockReturnValue(setSpacePolicyMock); + + const { user } = installerRender(); + + const toggleButton = screen.getByRole("button", { name: "Summary text" }); + await user.click(toggleButton); // Open the menu + + await waitFor(() => { + expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Wait for menu to be expanded + }); + + const keepPolicyItem = screen.getByRole("menuitem", { name: "Keep existing data" }); + await user.click(keepPolicyItem); + + expect(setSpacePolicyMock).toHaveBeenCalledWith("drives", 0, { type: "keep_data" }); + expect(mockNavigateFn).not.toHaveBeenCalled(); + }); + + it("should navigate to editSpacePolicy when 'Custom' policy is selected", async () => { + const { user } = installerRender(); + + const toggleButton = screen.getByRole("button", { name: "Summary text" }); // ADDED THIS LINE + expect(toggleButton).toBeInTheDocument(); // ADDED THIS LINE + expect(toggleButton).toHaveAttribute("aria-expanded", "false"); // ADDED THIS LINE + + await user.click(toggleButton); // Click the toggle button + + // Now, wait for the menu to expand. + await waitFor(() => { + expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Check if expanded + }); + + const customPolicyItem = screen.getByRole("menuitem", { name: "Custom" }); + await user.click(customPolicyItem); + + expect(mockNavigateFn).toHaveBeenCalledWith("/path/to/edit"); + expect(mockUseSetSpacePolicy()).not.toHaveBeenCalled(); + }); + + it("should mark the current policy as selected", async () => { + (driveUtils.spacePolicyEntry as jest.Mock).mockReturnValue({ id: "format", label: "Format" }); + + const { user } = installerRender(); + + const toggleButton = screen.getByRole("button", { name: "Summary text" }); + await act(async () => { + user.click(toggleButton); // Open the menu + jest.runAllTimers(); // Advance timers + }); + + await waitFor(() => { + expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Wait for menu to be expanded + }); + + const formatPolicyItem = screen.getByText("Format"); + expect(formatPolicyItem).toHaveClass("pf-v6-u-font-weight-bold"); + }); +}); From 6012cb627293f693ed1f2f832030fc48c34b854c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 15:05:10 +0000 Subject: [PATCH 08/27] Fix space policy menu tests - gemini-flash was not able to do it. --- .../storage/SpacePolicyMenu.test.tsx | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/web/src/components/storage/SpacePolicyMenu.test.tsx b/web/src/components/storage/SpacePolicyMenu.test.tsx index 621be894c0..6c8457d5d8 100644 --- a/web/src/components/storage/SpacePolicyMenu.test.tsx +++ b/web/src/components/storage/SpacePolicyMenu.test.tsx @@ -21,7 +21,7 @@ */ import React from "react"; -import { screen, fireEvent, act, waitFor } from "@testing-library/react"; +import { screen, waitFor } from "@testing-library/react"; import { installerRender, mockNavigateFn } from "~/test-utils"; import SpacePolicyMenu from "./SpacePolicyMenu"; import * as driveUtils from "~/components/storage/utils/drive"; @@ -63,12 +63,6 @@ jest.mock("~/utils", () => ({ generateEncodedPath: jest.fn(), })); -// Mock radashi's isEmpty -jest.mock("radashi", () => ({ - ...jest.requireActual("radashi"), - isEmpty: jest.fn(), -})); - jest.mock("~/components/storage/utils", () => ({ SPACE_POLICIES: [ { id: "do_not_format", label: "Delete current content" }, @@ -105,12 +99,6 @@ describe("SpacePolicyMenu", () => { label: "Do not format", }); (generateEncodedPath as jest.Mock).mockReturnValue("/path/to/edit"); - // By default, assume partitions exist so the menu renders - jest.mocked(require("radashi").isEmpty).mockReturnValue(false); - }); - - afterEach(() => { - jest.useRealTimers(); }); it("should render the SpacePolicyMenu with correct initial state", async () => { @@ -120,10 +108,7 @@ describe("SpacePolicyMenu", () => { expect(toggleButton).toBeInTheDocument(); expect(toggleButton).toHaveAttribute("aria-expanded", "false"); // Initially closed - await act(async () => { - user.click(toggleButton); // Click the toggle button - jest.runAllTimers(); // Advance timers - }); + await user.click(toggleButton); // Click the toggle button // Now, wait for the menu to expand. The "Do not format" item should be visible inside the expanded menu. await waitFor(() => { @@ -133,7 +118,7 @@ describe("SpacePolicyMenu", () => { }); it("should not render the SpacePolicyMenu if existingPartitions is empty", () => { - jest.mocked(require("radashi").isEmpty).mockReturnValue(true); + mockUseDevice.mockReturnValue({ ...mockDevice, partitions: [] }); const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); @@ -151,7 +136,7 @@ describe("SpacePolicyMenu", () => { expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Wait for menu to be expanded }); - const keepPolicyItem = screen.getByRole("menuitem", { name: "Keep existing data" }); + const keepPolicyItem = screen.getByRole("menuitem", { name: /Keep existing data/ }); await user.click(keepPolicyItem); expect(setSpacePolicyMock).toHaveBeenCalledWith("drives", 0, { type: "keep_data" }); @@ -172,7 +157,7 @@ describe("SpacePolicyMenu", () => { expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Check if expanded }); - const customPolicyItem = screen.getByRole("menuitem", { name: "Custom" }); + const customPolicyItem = screen.getByRole("menuitem", { name: /Custom/ }); await user.click(customPolicyItem); expect(mockNavigateFn).toHaveBeenCalledWith("/path/to/edit"); @@ -185,10 +170,7 @@ describe("SpacePolicyMenu", () => { const { user } = installerRender(); const toggleButton = screen.getByRole("button", { name: "Summary text" }); - await act(async () => { - user.click(toggleButton); // Open the menu - jest.runAllTimers(); // Advance timers - }); + await user.click(toggleButton); // Open the menu await waitFor(() => { expect(toggleButton).toHaveAttribute("aria-expanded", "true"); // Wait for menu to be expanded From 63f2f951de2fb3d82a368f1d38d8b9cbf170feec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 15:53:05 +0000 Subject: [PATCH 09/27] Add tests for auto size text (ai) --- .../components/storage/AutoSizeText.test.tsx | 408 ++++++++++++++++++ web/src/test-utils.tsx | 21 +- 2 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 web/src/components/storage/AutoSizeText.test.tsx diff --git a/web/src/components/storage/AutoSizeText.test.tsx b/web/src/components/storage/AutoSizeText.test.tsx new file mode 100644 index 0000000000..a4d2e73b1b --- /dev/null +++ b/web/src/components/storage/AutoSizeText.test.tsx @@ -0,0 +1,408 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; + +import AutoSizeText from "~/components/storage/AutoSizeText"; +import { installerRender as renderWithProviders } from "~/test-utils"; +import type { AutoSizeTextProps } from "~/components/storage/AutoSizeText"; +import type { Volume } from "~/api/system/storage"; +import type { model } from "~/api/storage"; +import { getSystem } from "~/api"; + +jest.mock("~/api", () => ({ + ...jest.requireActual("~/api"), + getSystem: jest.fn(), +})); + +const mockedGetSystem = getSystem as jest.Mock; + +const GiB = 1024 * 1024 * 1024; +const MiB = 1024 * 1024; + +// A minimal mock for the Volume type. +const MOCK_VOLUME: Volume = { + autoSize: false, + mountPath: null, + outline: { + adjustByRam: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [], + required: false, + supportAutoSize: false, + }, + minSize: 0, +}; + +describe("AutoSizeText", () => { + beforeEach(() => { + mockedGetSystem.mockResolvedValue({ + l10n: { + locale: "en_US.UTF-8", + keymap: "us", + timezone: "Europe/Berlin", + }, + }); + }); + const renderHelper = (props: AutoSizeTextProps) => { + return renderWithProviders(, { withL10n: true }); + }; + + describe("for fallback volumes", () => { + const volume: Volume = { ...MOCK_VOLUME, mountPath: null }; + const deviceType = "partition"; + + it("displays a fixed size message", async () => { + const size: model.Size = { min: 1 * GiB, max: 1 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText(`A generic size of 1 GiB will be used for the new partition`), + ).toBeInTheDocument(); + }); + + it("displays a size range message", async () => { + const size: model.Size = { min: 1 * GiB, max: 2 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `A generic size range between 1 GiB and 2 GiB will be used for the new partition`, + ), + ).toBeInTheDocument(); + }); + + it("displays a minimum size message", async () => { + const size: model.Size = { min: 1 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `A generic minimum size of 1 GiB will be used for the new partition`, + ), + ).toBeInTheDocument(); + }); + }); + + describe("for fixed size volumes", () => { + const volume: Volume = { + ...MOCK_VOLUME, + mountPath: "/home", + autoSize: false, + }; + const deviceType = "logicalVolume"; + + it("displays a fixed size message", async () => { + const size: model.Size = { min: 20 * GiB, max: 20 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText(`A logical volume of 20 GiB will be created for /home if possible`), + ).toBeInTheDocument(); + }); + + it("displays a size range message", async () => { + const size: model.Size = { min: 20 * GiB, max: 30 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `A logical volume with a size between 20 GiB and 30 GiB will be created for /home if possible`, + ), + ).toBeInTheDocument(); + }); + + it("displays a minimum size message", async () => { + const size: model.Size = { min: 20 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `A logical volume of at least 20 GiB will be created for /home if possible`, + ), + ).toBeInTheDocument(); + }); + }); + + describe("for RAM-based volumes", () => { + const volume: Volume = { + ...MOCK_VOLUME, + mountPath: "/boot/efi", + autoSize: true, + }; + const deviceType = "partition"; + + it("displays a fixed size message", async () => { + const size: model.Size = { min: 512 * MiB, max: 512 * MiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `Based on the amount of RAM in the system, a partition of 512 MiB will be planned for /boot/efi`, + ), + ).toBeInTheDocument(); + }); + + it("displays a size range message", async () => { + const size: model.Size = { min: 512 * MiB, max: 1 * GiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `Based on the amount of RAM in the system, a partition with a size between 512 MiB and 1 GiB will be planned for /boot/efi`, + ), + ).toBeInTheDocument(); + }); + + it("displays a minimum size message", async () => { + const size: model.Size = { min: 512 * MiB, default: false }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `Based on the amount of RAM in the system, a partition of at least 512 MiB will be planned for /boot/efi`, + ), + ).toBeInTheDocument(); + }); + }); + + describe("for dynamic size volumes", () => { + const size: model.Size = { min: 10 * GiB, max: 20 * GiB, default: false }; + const deviceType = "partition"; + const baseVolume: Volume = { + ...MOCK_VOLUME, + mountPath: "/", + autoSize: true, + }; + + const expectLimitsText = async () => { + expect( + await screen.findByText( + `The current configuration will result in an attempt to create a partition with a size between 10 GiB and 20 GiB.`, + ), + ).toBeInTheDocument(); + }; + + it("displays intro for RAM and snapshots", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, snapshotsAffectSizes: true, adjustByRam: true }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the amount of RAM in the system and the usage of Btrfs snapshots.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for RAM, snapshots and one other path", async () => { + const volume: Volume = { + ...baseVolume, + outline: { + ...baseVolume.outline, + snapshotsAffectSizes: true, + adjustByRam: true, + sizeRelevantVolumes: ["/home"], + }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the amount of RAM in the system, the usage of Btrfs snapshots and the presence of a separate file system for /home.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for RAM, snapshots and multiple other paths", async () => { + const volume: Volume = { + ...baseVolume, + outline: { + ...baseVolume.outline, + snapshotsAffectSizes: true, + adjustByRam: true, + sizeRelevantVolumes: ["/home", "/var"], + }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the amount of RAM in the system, the usage of Btrfs snapshots and the presence of separate file systems for /home and /var.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for RAM and one other path", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, adjustByRam: true, sizeRelevantVolumes: ["/home"] }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the amount of RAM in the system and the presence of a separate file system for /home.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for RAM and multiple other paths", async () => { + const volume: Volume = { + ...baseVolume, + outline: { + ...baseVolume.outline, + adjustByRam: true, + sizeRelevantVolumes: ["/home", "/var"], + }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the amount of RAM in the system and the presence of separate file systems for /home and /var.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for snapshots and one other path", async () => { + const volume: Volume = { + ...baseVolume, + outline: { + ...baseVolume.outline, + snapshotsAffectSizes: true, + sizeRelevantVolumes: ["/home"], + }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the usage of Btrfs snapshots and the presence of a separate file system for /home.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for snapshots and multiple other paths", async () => { + const volume: Volume = { + ...baseVolume, + outline: { + ...baseVolume.outline, + snapshotsAffectSizes: true, + sizeRelevantVolumes: ["/home", "/var"], + }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + `The size for / will be dynamically adjusted based on the usage of Btrfs snapshots and the presence of separate file systems for /home and /var.`, + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for just snapshots", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, snapshotsAffectSizes: true }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + "The size for / will be dynamically adjusted based on the usage of Btrfs snapshots.", + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for just one other path", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, sizeRelevantVolumes: ["/home"] }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + "The size for / will be dynamically adjusted based on the presence of a separate file system for /home.", + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays intro for just multiple other paths", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, sizeRelevantVolumes: ["/home", "/var"] }, + }; + renderHelper({ volume, size, deviceType }); + + expect( + await screen.findByText( + "The size for / will be dynamically adjusted based on the presence of separate file systems for /home and /var.", + ), + ).toBeInTheDocument(); + await expectLimitsText(); + }); + + it("displays limits with a fixed size", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, snapshotsAffectSizes: true }, + }; + const fixedSize: model.Size = { min: 10 * GiB, max: 10 * GiB, default: false }; + renderHelper({ volume, size: fixedSize, deviceType }); + + expect( + await screen.findByText( + "The current configuration will result in an attempt to create a partition of 10 GiB.", + ), + ).toBeInTheDocument(); + }); + + it("displays limits with a minimum size", async () => { + const volume: Volume = { + ...baseVolume, + outline: { ...baseVolume.outline, snapshotsAffectSizes: true }, + }; + const minSize: model.Size = { min: 10 * GiB, default: false }; + renderHelper({ volume, size: minSize, deviceType }); + + expect( + await screen.findByText( + "The current configuration will result in an attempt to create a partition of at least 10 GiB.", + ), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/web/src/test-utils.tsx b/web/src/test-utils.tsx index 0c0cd8b78d..f6fa95a86a 100644 --- a/web/src/test-utils.tsx +++ b/web/src/test-utils.tsx @@ -150,14 +150,23 @@ const Providers = ({ children, withL10n }) => { * @see #plainRender for rendering without installer providers */ const installerRender = (ui: React.ReactNode, options: { withL10n?: boolean } = {}) => { - const queryClient = new QueryClient({}); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // ✅ turns retries off + retry: false, + // ✅ make sure that data is not retrieved again when not needed + staleTime: Infinity + }, + }, + }); const Wrapper = ({ children }) => ( - - - {children} - - + + + {children} + + ); return { From 68b6d72c9ad92c2f1c4c2122675f4778951731a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 16:15:59 +0000 Subject: [PATCH 10/27] Fix types (ai) --- .../core/ChangeProductOption.test.tsx | 6 +- .../components/core/InstallButton.test.tsx | 10 +- .../core/InstallationFinished.test.tsx | 4 +- web/src/components/core/IssuesAlert.test.tsx | 10 +- web/src/components/core/IssuesDrawer.test.tsx | 37 +++----- .../core/ProgressStatusMonitor.test.tsx | 4 +- .../components/overview/L10nSection.test.tsx | 14 +-- .../overview/StorageSection.test.tsx | 32 ++----- .../questions/LuksActivationQuestion.test.tsx | 10 +- .../questions/QuestionWithPassword.test.tsx | 10 +- .../components/software/SoftwarePage.test.tsx | 2 +- .../storage/ConfigureDeviceMenu.test.tsx | 64 ++++++++----- .../storage/DeviceSelectorModal.test.tsx | 84 +++++++++------- .../components/storage/FixableConfigInfo.tsx | 4 +- .../storage/FormattableDevicePage.test.tsx | 44 +++++---- web/src/components/storage/LvmPage.test.tsx | 89 +++++++++-------- .../components/storage/PartitionPage.test.tsx | 82 ++++++++-------- .../storage/ProposalActionsDialog.test.tsx | 2 +- .../storage/ProposalFailedInfo.test.tsx | 34 +++---- .../storage/ProposalResultSection.test.tsx | 14 ++- .../ProposalTransactionalInfo.test.tsx | 21 ++-- .../storage/SpaceActionsTable.test.tsx | 95 ++++++++++--------- .../storage/UnsupportedModelInfo.test.tsx | 12 +-- .../storage/iscsi/InitiatorSection.test.tsx | 2 +- web/src/context/installer.test.tsx | 5 +- web/src/mocks/api.ts | 43 +++++---- 26 files changed, 376 insertions(+), 358 deletions(-) diff --git a/web/src/components/core/ChangeProductOption.test.tsx b/web/src/components/core/ChangeProductOption.test.tsx index cabf5daffb..4a6b0cdb25 100644 --- a/web/src/components/core/ChangeProductOption.test.tsx +++ b/web/src/components/core/ChangeProductOption.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { useSystem } from "~/hooks/api"; +import { useSystem } from "~/hooks/api/system"; import { PRODUCT as PATHS } from "~/routes/paths"; import { Product } from "~/types/software"; import ChangeProductOption from "./ChangeProductOption"; @@ -59,8 +59,8 @@ const network: System = { // let registrationInfoMock: RegistrationInfo; const mockSystemProducts: jest.Mock = jest.fn(); -jest.mock("~/hooks/api", () => ({ - ...jest.requireActual("~/hooks/api"), +jest.mock("~/hooks/api/system", () => ({ + ...jest.requireActual("~/hooks/api/system"), useSystem: (): ReturnType => ({ products: mockSystemProducts(), network, diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index d352ba3adf..ec35697c61 100644 --- a/web/src/components/core/InstallButton.test.tsx +++ b/web/src/components/core/InstallButton.test.tsx @@ -25,7 +25,7 @@ import { screen, waitFor, within } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import { InstallButton } from "~/components/core"; import { PRODUCT, ROOT } from "~/routes/paths"; -import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; +import { Issue } from "~/api/issue"; const mockStartInstallationFn = jest.fn(); let mockIssuesList: Issue[]; @@ -55,9 +55,7 @@ describe("InstallButton", () => { mockIssuesList = [ { description: "Fake Issue", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Fake Issue details", scope: "product", }, @@ -133,9 +131,7 @@ describe("InstallButton", () => { mockIssuesList = [ { description: "Fake warning", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Warn, + class: "warn", details: "Fake Issue details", scope: "product", }, diff --git a/web/src/components/core/InstallationFinished.test.tsx b/web/src/components/core/InstallationFinished.test.tsx index cdbf8ac313..a693f88e3b 100644 --- a/web/src/components/core/InstallationFinished.test.tsx +++ b/web/src/components/core/InstallationFinished.test.tsx @@ -84,8 +84,8 @@ const mockStorageConfig = ( } }; -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/config/storage", () => ({ + ...jest.requireActual("~/hooks/api/config/storage"), useConfig: () => mockStorageConfig(mockType, mockEncryption), })); diff --git a/web/src/components/core/IssuesAlert.test.tsx b/web/src/components/core/IssuesAlert.test.tsx index af838230e1..65bca84820 100644 --- a/web/src/components/core/IssuesAlert.test.tsx +++ b/web/src/components/core/IssuesAlert.test.tsx @@ -24,16 +24,14 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { IssuesAlert } from "~/components/core"; -import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; +import { Issue } from "~/api/issue"; import { SOFTWARE } from "~/routes/paths"; describe("IssueAlert", () => { it("renders a list of issues", () => { const issue: Issue = { description: "A generic issue", - source: IssueSource.Config, - severity: IssueSeverity.Error, - kind: "generic", + class: "generic", scope: "software", }; installerRender(); @@ -43,9 +41,7 @@ describe("IssueAlert", () => { it("renders a link to conflict resolution when there is a 'solver' issue", () => { const issue: Issue = { description: "Conflicts found", - source: IssueSource.Config, - severity: IssueSeverity.Error, - kind: "solver", + class: "solver", scope: "software", }; installerRender(); diff --git a/web/src/components/core/IssuesDrawer.test.tsx b/web/src/components/core/IssuesDrawer.test.tsx index 427e049446..6a60cdf058 100644 --- a/web/src/components/core/IssuesDrawer.test.tsx +++ b/web/src/components/core/IssuesDrawer.test.tsx @@ -24,16 +24,16 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { InstallationPhase } from "~/types/status"; -import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; +import { Issue } from "~/api/issue"; import IssuesDrawer from "./IssuesDrawer"; let phase = InstallationPhase.Config; let mockIssuesList: Issue[]; const onCloseFn = jest.fn(); -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), - useAllIssues: () => mockIssuesList, +jest.mock("~/hooks/api/issue", () => ({ + ...jest.requireActual("~/hooks/api/issue"), + useIssues: () => mockIssuesList, })); jest.mock("~/queries/status", () => ({ @@ -62,16 +62,17 @@ describe("IssuesDrawer", () => { mockIssuesList = [ { description: "Registration Fake Warning", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Warn, + class: "warn", details: "Registration Fake Issue details", scope: "product", }, ]; }); - itRendersNothing(); + it("renders the drawer with a warning", () => { + installerRender(); + expect(screen.getByText("Registration Fake Warning")).toBeInTheDocument(); + }); }); describe("when there are installation issues", () => { @@ -79,41 +80,31 @@ describe("IssuesDrawer", () => { mockIssuesList = [ { description: "Registration Fake Issue", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Registration Fake Issue details", scope: "product", }, { description: "Software Fake Issue", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Software Fake Issue details", scope: "software", }, { description: "Storage Fake Issue 1", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Storage Fake Issue 1 details", scope: "storage", }, { description: "Storage Fake Issue 2", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Storage Fake Issue 2 details", scope: "storage", }, { description: "Users Fake Issue", - kind: "generic", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "error", details: "Users Fake Issue details", scope: "users", }, diff --git a/web/src/components/core/ProgressStatusMonitor.test.tsx b/web/src/components/core/ProgressStatusMonitor.test.tsx index fdcefb3784..30061c4551 100644 --- a/web/src/components/core/ProgressStatusMonitor.test.tsx +++ b/web/src/components/core/ProgressStatusMonitor.test.tsx @@ -23,12 +23,12 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { useStatus } from "~/hooks/api"; +import { useStatus } from "~/hooks/api/status"; import ProgressStatusMonitor from "./ProgressStatusMonitor"; let mockProgress: jest.Mock["progresses"]> = jest.fn(); -jest.mock("~/hooks/api", () => ({ +jest.mock("~/hooks/api/status", () => ({ useStatus: () => ({ progresses: mockProgress() }), })); diff --git a/web/src/components/overview/L10nSection.test.tsx b/web/src/components/overview/L10nSection.test.tsx index 20e542368e..2a1985a4d1 100644 --- a/web/src/components/overview/L10nSection.test.tsx +++ b/web/src/components/overview/L10nSection.test.tsx @@ -24,22 +24,22 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { L10nSection } from "~/components/overview"; -import { Locale } from "~/api/system"; +import type { Locale } from "~/api/system/l10n"; const locales: Locale[] = [ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "de_DE.UTF-8", name: "German", territory: "Germany" }, + { id: "en_US.UTF-8", language: "English", territory: "United States" }, + { id: "de_DE.UTF-8", language: "German", territory: "Germany" }, ]; -jest.mock("~/queries/system", () => ({ - ...jest.requireActual("~/queries/system"), +jest.mock("~/hooks/api/system", () => ({ + ...jest.requireActual("~/hooks/api/system"), useSystem: () => ({ l10n: { locale: "en_US.UTF-8", locales, keymap: "us" }, }), })); -jest.mock("~/queries/proposal", () => ({ - ...jest.requireActual("~/queries/proposal"), +jest.mock("~/hooks/api/proposal", () => ({ + ...jest.requireActual("~/hooks/api/proposal"), useProposal: () => ({ l10n: { locale: "en_US.UTF-8", keymap: "us" }, }), diff --git a/web/src/components/overview/StorageSection.test.tsx b/web/src/components/overview/StorageSection.test.tsx index bc452cb09f..7af481e513 100644 --- a/web/src/components/overview/StorageSection.test.tsx +++ b/web/src/components/overview/StorageSection.test.tsx @@ -24,7 +24,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { StorageSection } from "~/components/overview"; -import { IssueSeverity, IssueSource } from "~/api/issue"; +import type { storage } from "~/api/system"; + let mockModel = { drives: [], @@ -62,30 +63,18 @@ const sdb = { udevPaths: [], }; -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => mockModel, +jest.mock("~/hooks/api/storage", () => ({ + useStorageModel: () => mockModel, })); const mockDevices = [sda, sdb]; - -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useDevices: () => mockDevices, -})); - let mockAvailableDevices = [sda, sdb]; +let mockSystemErrors: storage.Issue[] = []; -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), +jest.mock("~/hooks/api/system/storage", () => ({ + useDevices: () => mockDevices, useAvailableDevices: () => mockAvailableDevices, -})); - -let mockSystemErrors = []; - -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), - useSystemErrors: () => mockSystemErrors, + useIssues: () => mockSystemErrors, })); beforeEach(() => { @@ -247,11 +236,8 @@ describe("when there is no configuration model (unsupported features)", () => { mockSystemErrors = [ { description: "System error", - kind: "storage", + class: "storage", details: "", - source: IssueSource.System, - severity: IssueSeverity.Error, - scope: "storage", }, ]; }); diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index 3da73f7406..06cbae889b 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -27,7 +27,7 @@ import { AnswerCallback, Question, FieldType } from "~/api/question"; import { InstallationPhase } from "~/types/status"; import { Product } from "~/types/software"; import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; -import { Locale, Keymap } from "~/api/system"; +import type { Locale, Keymap } from "~/api/system/l10n"; let question: Question; const questionMock: Question = { @@ -51,12 +51,12 @@ const tumbleweed: Product = { }; const locales: Locale[] = [ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, + { id: "en_US.UTF-8", language: "English", territory: "United States" }, + { id: "es_ES.UTF-8", language: "Spanish", territory: "Spain" }, ]; const keymaps: Keymap[] = [ - { id: "us", name: "English" }, - { id: "es", name: "Spanish" }, + { id: "us", description: "English" }, + { id: "es", description: "Spanish" }, ]; jest.mock("~/queries/system", () => ({ diff --git a/web/src/components/questions/QuestionWithPassword.test.tsx b/web/src/components/questions/QuestionWithPassword.test.tsx index 5e857ebac3..cc6c8db33f 100644 --- a/web/src/components/questions/QuestionWithPassword.test.tsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -27,7 +27,7 @@ import { Question, FieldType } from "~/api/question"; import { Product } from "~/types/software"; import { InstallationPhase } from "~/types/status"; import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; -import { Locale, Keymap } from "~/api/system"; +import type { Locale, Keymap } from "~/api/system/l10n"; const answerFn = jest.fn(); const question: Question = { @@ -51,13 +51,13 @@ const tumbleweed: Product = { }; const locales: Locale[] = [ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, + { id: "en_US.UTF-8", language: "English", territory: "United States" }, + { id: "es_ES.UTF-8", language: "Spanish", territory: "Spain" }, ]; const keymaps: Keymap[] = [ - { id: "us", name: "English" }, - { id: "es", name: "Spanish" }, + { id: "us", description: "English" }, + { id: "es", description: "Spanish" }, ]; jest.mock("~/queries/status", () => ({ diff --git a/web/src/components/software/SoftwarePage.test.tsx b/web/src/components/software/SoftwarePage.test.tsx index 47a15c9cfe..c24c3beb0e 100644 --- a/web/src/components/software/SoftwarePage.test.tsx +++ b/web/src/components/software/SoftwarePage.test.tsx @@ -32,7 +32,7 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
ProductRegistrationAlert Mock
)); -jest.mock("~/queries/issues", () => ({ +jest.mock("~/hooks/api/issue", () => ({ useIssues: () => [], })); diff --git a/web/src/components/storage/ConfigureDeviceMenu.test.tsx b/web/src/components/storage/ConfigureDeviceMenu.test.tsx index b6ed1dd53e..a1ce79e2ae 100644 --- a/web/src/components/storage/ConfigureDeviceMenu.test.tsx +++ b/web/src/components/storage/ConfigureDeviceMenu.test.tsx @@ -24,35 +24,51 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { mockNavigateFn, installerRender } from "~/test-utils"; import ConfigureDeviceMenu from "./ConfigureDeviceMenu"; -import { StorageDevice } from "~/storage"; -import { apiModel } from "~/api/storage/types"; +import type { storage } from "~/api/system"; +import { model as apiModel } from "~/api/storage"; -const vda: StorageDevice = { +const vda: storage.Device = { sid: 59, - type: "disk", - isDrive: true, - description: "", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", name: "/dev/vda", - size: 1e12, - systems: ["Windows 11", "openSUSE Leap 15.2"], + description: "", + class: "drive", + drive: { + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + }, + block: { + size: 1e12, + start: 0, + systems: ["Windows 11", "openSUSE Leap 15.2"], + shrinking: { supported: false }, + active: true, + encrypted: false, + }, }; -const vdb: StorageDevice = { +const vdb: storage.Device = { sid: 60, - type: "disk", - isDrive: true, - description: "", - vendor: "Seagate", - model: "Unknown", - driver: ["ahci", "mmcblk"], - bus: "IDE", name: "/dev/vdb", - size: 1e6, - systems: [], + description: "", + class: "drive", + drive: { + type: "disk", + vendor: "Seagate", + model: "Unknown", + driver: ["ahci", "mmcblk"], + bus: "IDE", + }, + block: { + size: 1e6, + start: 0, + systems: [], + shrinking: { supported: false }, + active: true, + encrypted: false, + }, }; const vdaDrive: apiModel.Drive = { @@ -70,8 +86,8 @@ const vdbDrive: apiModel.Drive = { const mockAddDrive = jest.fn(); const mockUseModel = jest.fn(); -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), +jest.mock("~/hooks/api/system/storage", () => ({ + ...jest.requireActual("~/hooks/api/system/storage"), useAvailableDevices: () => [vda, vdb], })); diff --git a/web/src/components/storage/DeviceSelectorModal.test.tsx b/web/src/components/storage/DeviceSelectorModal.test.tsx index 3189470219..c816f276bf 100644 --- a/web/src/components/storage/DeviceSelectorModal.test.tsx +++ b/web/src/components/storage/DeviceSelectorModal.test.tsx @@ -23,50 +23,64 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { getColumnValues, plainRender } from "~/test-utils"; -import { StorageDevice } from "~/storage"; +import type { storage } from "~/api/system"; import DeviceSelectorModal from "./DeviceSelectorModal"; -const sda: StorageDevice = { +const sda: storage.Device = { sid: 59, - isDrive: true, - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, + class: "drive", + drive: { + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + info: { + sdCard: true, + dellBoss: false, + }, + }, name: "/dev/sda", - size: 1024, - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], + block: { + size: 1024, + start: 0, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + systems: [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], + active: true, + }, description: "SDA drive", }; -const sdb: StorageDevice = { +const sdb: storage.Device = { sid: 62, - isDrive: true, - type: "disk", - vendor: "Samsung", - model: "Samsung Evo 8 Pro", - driver: ["ahci"], - bus: "IDE", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, + class: "drive", + drive: { + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + info: { + dellBoss: false, + sdCard: false, + }, + }, name: "/dev/sdb", - size: 2048, - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], - udevIds: [], - udevPaths: ["pci-0000:00-19"], + block: { + size: 2048, + start: 0, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:00-19"], + active: true, + }, description: "SDB drive", }; diff --git a/web/src/components/storage/FixableConfigInfo.tsx b/web/src/components/storage/FixableConfigInfo.tsx index 548a39c5d2..b64b38514c 100644 --- a/web/src/components/storage/FixableConfigInfo.tsx +++ b/web/src/components/storage/FixableConfigInfo.tsx @@ -25,7 +25,7 @@ import { Alert, List, ListItem } from "@patternfly/react-core"; import { n_ } from "~/i18n"; import type { Issue } from "~/api/issue"; -const Description = ({ errors }: Issue[]) => { +const Description = ({ errors }: { errors: Issue[] }) => { return ( {errors.map((e, i) => ( @@ -39,7 +39,7 @@ const Description = ({ errors }: Issue[]) => { * Information about a wrong but fixable storage configuration * */ -export default function FixableConfigInfo({ issues }: Issue[]) { +export default function FixableConfigInfo({ issues }: { issues: Issue[] }) { const title = n_( "The configuration must be adapted to address the following issue:", "The configuration must be adapted to address the following issues:", diff --git a/web/src/components/storage/FormattableDevicePage.test.tsx b/web/src/components/storage/FormattableDevicePage.test.tsx index a3177b70ef..aee3f0a5db 100644 --- a/web/src/components/storage/FormattableDevicePage.test.tsx +++ b/web/src/components/storage/FormattableDevicePage.test.tsx @@ -24,16 +24,22 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender, mockParams } from "~/test-utils"; import FormattableDevicePage from "~/components/storage/FormattableDevicePage"; -import { StorageDevice, model } from "~/storage"; -import { Volume } from "~/api/storage/types"; +import { model } from "~/storage"; +import type { storage } from "~/api/system"; import { gib } from "./utils"; -const sda: StorageDevice = { +const sda: storage.Device = { sid: 59, - isDrive: true, - type: "disk", + class: "drive", + drive: { + type: "disk", + }, name: "/dev/sda", - size: gib(10), + block: { + size: gib(10), + start: 0, + shrinking: { supported: false }, + }, description: "", }; @@ -41,8 +47,6 @@ const sdaModel: model.Drive = { name: "/dev/sda", spacePolicy: "keep", partitions: [], - list: "drives", - listIndex: 0, isExplicitBoot: false, isUsed: true, isAddingPartitions: true, @@ -55,22 +59,21 @@ const sdaModel: model.Drive = { getConfiguredExistingPartitions: jest.fn(), }; -const homeVolume: Volume = { +const mockHomeVolume: storage.Volume = { mountPath: "/home", mountOptions: [], - target: "default", fsType: "btrfs", - minSize: gib(1), - maxSize: gib(5), + minSize: 1024, + maxSize: 2048, autoSize: false, snapshots: false, transactional: false, outline: { - required: false, - fsTypes: ["btrfs", "xfs"], - supportAutoSize: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, + required: true, + fsTypes: ["btrfs", "ext4"], + supportAutoSize: true, + snapshotsConfigurable: true, + snapshotsAffectSizes: true, sizeRelevantVolumes: [], adjustByRam: false, }, @@ -81,9 +84,10 @@ jest.mock("~/queries/issues", () => ({ useIssues: () => [], })); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/system/storage", () => ({ + ...jest.requireActual("~/hooks/api/system/storage"), useDevices: () => [sda], + useVolumeTemplate: () => mockHomeVolume, })); const mockModel = jest.fn(); @@ -95,7 +99,7 @@ jest.mock("~/hooks/storage/model", () => ({ jest.mock("~/hooks/storage/product", () => ({ ...jest.requireActual("~/hooks/storage/product"), useMissingMountPaths: () => ["/home", "swap"], - useVolume: () => homeVolume, + useVolume: () => mockHomeVolume, })); const mockAddFilesystem = jest.fn(); diff --git a/web/src/components/storage/LvmPage.test.tsx b/web/src/components/storage/LvmPage.test.tsx index 358aebc5dc..26a9c2a8ad 100644 --- a/web/src/components/storage/LvmPage.test.tsx +++ b/web/src/components/storage/LvmPage.test.tsx @@ -23,56 +23,67 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender, mockParams } from "~/test-utils"; -import { model, StorageDevice } from "~/storage"; +import { model } from "~/storage"; +import type { storage } from "~/api/system"; import { gib } from "./utils"; import LvmPage from "./LvmPage"; -const sda1: StorageDevice = { +const sda1: storage.Device = { sid: 69, name: "/dev/sda1", description: "Swap partition", - isDrive: false, - type: "partition", - size: gib(2), - shrinking: { unsupported: ["Resizing is not supported"] }, - start: 1, + class: "partition", + block: { + size: gib(2), + start: 1, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + }, }; -const sda: StorageDevice = { +const sda: storage.Device = { sid: 59, - isDrive: true, - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, + class: "drive", + drive: { + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + info: { + dellBoss: false, + sdCard: true, + }, + }, name: "/dev/sda", - size: 1024, - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], + block: { + size: 1024, + start: 0, + active: true, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + systems: [], + 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: [sda1], - unpartitionedSize: 0, unusedSlots: [{ start: 3, size: gib(2) }], }, - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], + partitions: [sda1], description: "", }; -const sdb: StorageDevice = { +const sdb: storage.Device = { sid: 60, - isDrive: true, - type: "disk", + class: "drive", name: "/dev/sdb", - size: 1024, - systems: [], + block: { + size: 1024, + start: 0, + shrinking: { supported: false }, + systems: [], + }, description: "", }; @@ -105,8 +116,6 @@ const mockSdaDrive: model.Drive = { isUsedBySpacePolicy: false, }, ], - list: "drives", - listIndex: 1, isExplicitBoot: false, isUsed: true, isAddingPartitions: true, @@ -121,8 +130,6 @@ const mockSdaDrive: model.Drive = { const mockRootVolumeGroup: model.VolumeGroup = { vgName: "fakeRootVg", - list: "volumeGroups", - listIndex: 1, logicalVolumes: [], getTargetDevices: () => [mockSdaDrive], getMountPaths: () => [], @@ -130,8 +137,6 @@ const mockRootVolumeGroup: model.VolumeGroup = { const mockHomeVolumeGroup: model.VolumeGroup = { vgName: "fakeHomeVg", - list: "volumeGroups", - listIndex: 2, logicalVolumes: [], getTargetDevices: () => [mockSdaDrive], getMountPaths: () => [], @@ -148,19 +153,13 @@ let mockUseModel = { const mockUseAllDevices = [sda, sdb]; -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), +jest.mock("~/hooks/api/issue", () => ({ useIssuesChanges: jest.fn(), useIssues: () => [], })); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/system/storage", () => ({ useDevices: () => mockUseAllDevices, -})); - -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), useAvailableDevices: () => mockUseAllDevices, })); diff --git a/web/src/components/storage/PartitionPage.test.tsx b/web/src/components/storage/PartitionPage.test.tsx index fc1a5b0f18..93fce538e8 100644 --- a/web/src/components/storage/PartitionPage.test.tsx +++ b/web/src/components/storage/PartitionPage.test.tsx @@ -24,12 +24,12 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender, mockParams } from "~/test-utils"; import PartitionPage from "./PartitionPage"; -import { StorageDevice, model } from "~/storage"; -import { apiModel, Volume } from "~/api/storage/types"; +import { model } from "~/storage"; +import type { storage } from "~/api/system"; +import { model as apiModel } from "~/api/storage"; import { gib } from "./utils"; -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), +jest.mock("~/hooks/api/issue", () => ({ useIssuesChanges: jest.fn(), useIssues: () => [], })); @@ -39,42 +39,49 @@ jest.mock("./ProposalTransactionalInfo", () => () =>
transactional info ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/system/storage", () => ({ useDevices: () => [sda], - useVolume: () => mockHomeVolume, + useVolumeTemplate: () => mockHomeVolume, })); jest.mock("~/hooks/storage/model", () => ({ @@ -172,10 +175,9 @@ jest.mock("~/hooks/storage/product", () => ({ useMissingMountPaths: () => ["/home", "swap"], })); -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => ({ drives: [mockDrive] }), - useSolvedConfigModel: () => mockSolvedConfigModel, +jest.mock("~/hooks/api/storage", () => ({ + useStorageModel: () => ({ drives: [mockDrive] }), + useSolvedStorageModel: () => mockSolvedConfigModel, })); beforeEach(() => { diff --git a/web/src/components/storage/ProposalActionsDialog.test.tsx b/web/src/components/storage/ProposalActionsDialog.test.tsx index 90da556d6d..c7d44c4c93 100644 --- a/web/src/components/storage/ProposalActionsDialog.test.tsx +++ b/web/src/components/storage/ProposalActionsDialog.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen, within, waitForElementToBeRemoved } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalActionsDialog } from "~/components/storage"; -import { Action } from "~/storage"; +import type { Action } from "~/api/proposal/storage"; const actions: Action[] = [ { diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index 900e6aa204..43d0bd39dd 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -25,27 +25,23 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalFailedInfo from "./ProposalFailedInfo"; import { LogicalVolume } from "~/storage/data"; -import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; -import { apiModel } from "~/api/storage/types"; +import { Issue } from "~/api/issue"; +import { model as apiModel } from "~/api/storage"; const mockUseConfigErrorsFn = jest.fn(); -let mockUseIssues = []; +let mockUseIssues: Issue[] = []; const configError: Issue = { description: "Config error", - kind: "storage", - details: "", - source: IssueSource.Config, - severity: IssueSeverity.Error, + class: "storage", scope: "storage", + details: "", }; const storageIssue: Issue = { description: "Fake Storage Issue", details: "", - kind: "storage_issue", - source: IssueSource.Unknown, - severity: IssueSeverity.Error, + class: "storage_issue", scope: "storage", }; @@ -150,15 +146,19 @@ const mockApiModel: apiModel.Config = { ], }; -jest.mock("~/hooks/storage/api-model", () => ({ - ...jest.requireActual("~/hooks/storage/api-model"), - useApiModel: () => mockApiModel, +jest.mock("~/hooks/api/storage", () => ({ + ...jest.requireActual("~/hooks/api/storage"), + useStorageModel: () => mockApiModel, })); -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), - useConfigErrors: () => mockUseConfigErrorsFn(), - useIssues: () => mockUseIssues, +jest.mock("~/hooks/api/issue", () => ({ + ...jest.requireActual("~/hooks/api/issue"), + useIssues: (scope: string) => { + if (scope === "config") { + return mockUseConfigErrorsFn(); + } + return mockUseIssues; + }, })); // eslint-disable-next-line diff --git a/web/src/components/storage/ProposalResultSection.test.tsx b/web/src/components/storage/ProposalResultSection.test.tsx index 502c1f6d49..cf694ccfcb 100644 --- a/web/src/components/storage/ProposalResultSection.test.tsx +++ b/web/src/components/storage/ProposalResultSection.test.tsx @@ -29,15 +29,19 @@ import { devices, actions } from "./test-data/full-result-example"; const mockUseActionsFn = jest.fn(); const mockConfig = { drives: [] }; -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/system/storage", () => ({ + ...jest.requireActual("~/hooks/api/system/storage"), useDevices: () => devices.staging, +})); + +jest.mock("~/hooks/api/proposal/storage", () => ({ + ...jest.requireActual("~/hooks/api/proposal/storage"), useActions: () => mockUseActionsFn(), })); -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => mockConfig, +jest.mock("~/hooks/api/storage", () => ({ + ...jest.requireActual("~/hooks/api/storage"), + useStorageModel: () => mockConfig, })); describe("ProposalResultSection", () => { diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.tsx b/web/src/components/storage/ProposalTransactionalInfo.test.tsx index 8633dd7e03..5fb6b53e6b 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.test.tsx +++ b/web/src/components/storage/ProposalTransactionalInfo.test.tsx @@ -24,35 +24,34 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalTransactionalInfo } from "~/components/storage"; -import { Volume } from "~/api/storage/types"; +import type { storage } from "~/api/system"; -let mockVolumes: Volume[] = []; -jest.mock("~/queries/software", () => ({ - ...jest.requireActual("~/queries/software"), +let mockVolumes: storage.Volume[] = []; +jest.mock("~/hooks/api/system/software", () => ({ + ...jest.requireActual("~/hooks/api/system/software"), useProduct: () => ({ selectedProduct: { name: "Test" }, }), useProductChanges: () => jest.fn(), })); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), +jest.mock("~/hooks/api/system/storage", () => ({ + ...jest.requireActual("~/hooks/api/system/storage"), useVolumes: () => mockVolumes, })); -const rootVolume: Volume = { +const rootVolume: storage.Volume = { mountPath: "/", mountOptions: [], - target: "default", - fsType: "Btrfs", + autoSize: false, minSize: 1024, maxSize: 2048, - autoSize: false, + fsType: "btrfs", snapshots: false, transactional: false, outline: { required: true, - fsTypes: ["Btrfs", "Ext4"], + fsTypes: ["btrfs", "ext4"], supportAutoSize: true, snapshotsConfigurable: true, snapshotsAffectSizes: true, diff --git a/web/src/components/storage/SpaceActionsTable.test.tsx b/web/src/components/storage/SpaceActionsTable.test.tsx index 486277a3cc..c7424f8c3d 100644 --- a/web/src/components/storage/SpaceActionsTable.test.tsx +++ b/web/src/components/storage/SpaceActionsTable.test.tsx @@ -27,58 +27,65 @@ import { screen, within } from "@testing-library/react"; import { deviceChildren, gib } from "~/components/storage/utils"; import { plainRender } from "~/test-utils"; import SpaceActionsTable, { SpaceActionsTableProps } from "~/components/storage/SpaceActionsTable"; -import { StorageDevice } from "~/storage"; -import { apiModel } from "~/api/storage/types"; +import type { storage } from "~/api/system"; +import { model as apiModel } from "~/api/storage"; -const sda: StorageDevice = { - sid: 59, - isDrive: true, - type: "disk", - description: "", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: gib(10), - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], -}; - -const sda1: StorageDevice = { +const sda1: storage.Device = { sid: 69, name: "/dev/sda1", description: "Swap partition", - isDrive: false, - type: "partition", - size: gib(2), - shrinking: { unsupported: ["Resizing is not supported"] }, - start: 1, + class: "partition", + block: { + size: gib(2), + start: 1, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + }, }; -const sda2: StorageDevice = { +const sda2: storage.Device = { sid: 79, name: "/dev/sda2", description: "EXT4 partition", - isDrive: false, - type: "partition", - size: gib(6), - shrinking: { supported: gib(3) }, - start: 2, + class: "partition", + block: { + size: gib(6), + start: 2, + shrinking: { supported: true, minSize: gib(3) }, + }, }; -sda.partitionTable = { - type: "gpt", +const sda: storage.Device = { + sid: 59, + class: "drive", + drive: { + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + info: { + dellBoss: false, + sdCard: true, + }, + }, + name: "/dev/sda", + block: { + size: gib(10), + start: 0, + active: true, + shrinking: { supported: false, reasons: ["Resizing is not supported"] }, + systems: [], + 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", + unusedSlots: [{ start: 3, size: gib(2) }], + }, partitions: [sda1, sda2], - unpartitionedSize: 0, - unusedSlots: [{ start: 3, size: gib(2) }], + description: "", }; const mockDrive: apiModel.Drive = { @@ -93,18 +100,18 @@ const mockDrive: apiModel.Drive = { }; const mockUseConfigModelFn = jest.fn(); -jest.mock("~/queries/storage/config-model", () => ({ - useConfigModel: () => mockUseConfigModelFn(), +jest.mock("~/hooks/api/storage", () => ({ + useStorageModel: () => mockUseConfigModelFn(), })); /** * Function to ask for the action of a device. * - * @param {StorageDevice} device + * @param {storage.Device} device * @returns {string} */ const deviceAction = (device) => { - if (device === sda1) return "keep"; + if (device.name === "/dev/sda1") return "keep"; return "delete"; }; diff --git a/web/src/components/storage/UnsupportedModelInfo.test.tsx b/web/src/components/storage/UnsupportedModelInfo.test.tsx index 434a0645e0..204aaea7c4 100644 --- a/web/src/components/storage/UnsupportedModelInfo.test.tsx +++ b/web/src/components/storage/UnsupportedModelInfo.test.tsx @@ -26,15 +26,11 @@ import { plainRender } from "~/test-utils"; import UnsupportedModelInfo from "./UnsupportedModelInfo"; const mockUseResetConfigMutation = jest.fn(); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useResetConfigMutation: () => mockUseResetConfigMutation(), -})); - const mockUseConfigModel = jest.fn(); -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => mockUseConfigModel(), +jest.mock("~/hooks/api/storage", () => ({ + ...jest.requireActual("~/hooks/api/storage"), + useResetConfigMutation: () => mockUseResetConfigMutation(), + useStorageModel: () => mockUseConfigModel(), })); beforeEach(() => { diff --git a/web/src/components/storage/iscsi/InitiatorSection.test.tsx b/web/src/components/storage/iscsi/InitiatorSection.test.tsx index ec1fbd36ca..2833277d21 100644 --- a/web/src/components/storage/iscsi/InitiatorSection.test.tsx +++ b/web/src/components/storage/iscsi/InitiatorSection.test.tsx @@ -25,7 +25,7 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import InitiatorSection from "./InitiatorSection"; -import { ISCSIInitiator } from "~/storage"; +import type { ISCSIInitiator } from "~/api/storage/iscsi"; let initiator: ISCSIInitiator; diff --git a/web/src/context/installer.test.tsx b/web/src/context/installer.test.tsx index 5c4f500890..098e1d2ef1 100644 --- a/web/src/context/installer.test.tsx +++ b/web/src/context/installer.test.tsx @@ -27,7 +27,10 @@ import { plainRender } from "~/test-utils"; import { InstallerClientProvider } from "./installer"; import { DummyWSClient } from "~/client/ws"; -jest.mock("~/components/layout/Loading", () => () =>
Loading Mock
); +jest.mock("~/components/layout/Loading", () => { + const React = require("react"); + return () => React.createElement("div", null, "Loading Mock"); +}); // Helper component to check the client status. const Content = () => { diff --git a/web/src/mocks/api.ts b/web/src/mocks/api.ts index b9b7e5d474..e12f5e17eb 100644 --- a/web/src/mocks/api.ts +++ b/web/src/mocks/api.ts @@ -24,25 +24,25 @@ * Mocking HTTP API calls. */ -import * as apiStorage from "~/api/storage"; -import * as apiIssues from "~/api/issues"; -import { Device } from "~/api/storage/types/openapi"; +import { model as storageModel } from "~/api/storage"; +import { System } from "~/api/system"; +import { Issue } from "~/api/issue"; +import { Config } from "~/api/config"; +import { Proposal } from "~/api/proposal"; +import { Question } from "~/api/question"; +import { Status } from "~/api/status"; export type ApiData = { - "/api/storage/devices/available_drives"?: Awaited< - ReturnType - >; - "/api/storage/devices/available_md_raids"?: Awaited< - ReturnType - >; - "/api/storage/devices/system"?: Device[]; - "/api/storage/config_model"?: Awaited>; - "/api/storage/issues"?: Awaited>; + "/api/v2/status"?: Status | null; + "/api/v2/config"?: Config | null; + "/api/v2/extended_config"?: Config | null; + "/api/v2/system"?: System | null; + "/api/v2/proposal"?: Proposal | null; + "/api/v2/issues"?: Issue[]; + "/api/v2/questions"?: Question[]; + "/api/v2/private/storage_model"?: storageModel.Config | null; }; -/** - * Mocked data. - */ const mockApiData = jest.fn().mockReturnValue({}); /** @@ -50,7 +50,7 @@ const mockApiData = jest.fn().mockReturnValue({}); * * @example * mockApi({ - * "/api/storage/available_devices": [50, 64] + * "/api/v2/system": { l10n: { locales: [] } } * }) */ const mockApi = (data: ApiData) => mockApiData.mockReturnValue(data); @@ -58,11 +58,16 @@ const mockApi = (data: ApiData) => mockApiData.mockReturnValue(data); const addMockApi = (data: ApiData) => mockApi({ ...mockApiData(), ...data }); // Mock get calls. -jest.mock("~/api/http", () => ({ - ...jest.requireActual("~/api/http"), +jest.mock("~/http", () => ({ + ...jest.requireActual("~/http"), get: (url: string) => { const data = mockApiData()[url]; - return Promise.resolve(data); + if (data !== undefined) { + return Promise.resolve(data); + } + // You can add a fallback to the actual implementation if needed + // For example, by calling jest.requireActual("~/http").get(url) + return Promise.reject(new Error(`No mock data for GET ${url}`)); }, })); From cf32e69a4de2c33720004262a6f8b991e28417f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 3 Dec 2025 16:37:26 +0000 Subject: [PATCH 11/27] Fix eslint (ai) --- .../core/ProgressStatusMonitor.test.tsx | 2 +- .../network/WifiConnectionForm.test.tsx | 30 +++++++++---------- .../overview/StorageSection.test.tsx | 1 - web/src/context/installer.test.tsx | 1 - web/src/test-utils.tsx | 2 +- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/web/src/components/core/ProgressStatusMonitor.test.tsx b/web/src/components/core/ProgressStatusMonitor.test.tsx index 30061c4551..13a74b51fe 100644 --- a/web/src/components/core/ProgressStatusMonitor.test.tsx +++ b/web/src/components/core/ProgressStatusMonitor.test.tsx @@ -26,7 +26,7 @@ import { installerRender } from "~/test-utils"; import { useStatus } from "~/hooks/api/status"; import ProgressStatusMonitor from "./ProgressStatusMonitor"; -let mockProgress: jest.Mock["progresses"]> = jest.fn(); +const mockProgress: jest.Mock["progresses"]> = jest.fn(); jest.mock("~/hooks/api/status", () => ({ useStatus: () => ({ progresses: mockProgress() }), diff --git a/web/src/components/network/WifiConnectionForm.test.tsx b/web/src/components/network/WifiConnectionForm.test.tsx index a8cc908c58..1fba15e1b2 100644 --- a/web/src/components/network/WifiConnectionForm.test.tsx +++ b/web/src/components/network/WifiConnectionForm.test.tsx @@ -28,6 +28,21 @@ import { Connection, SecurityProtocols, WifiNetworkStatus, Wireless } from "~/ty const mockUpdateConnection = jest.fn(); +const mockConnection = new Connection("Visible Network", { + wireless: new Wireless({ ssid: "Visible Network" }), +}); + +const mockSystem = { + connections: [mockConnection], + state: { + connectivity: true, + wiredEnabled: true, + wirelessEnabled: false, + persistNetwork: true, + copyEnabled: false, + }, +}; + jest.mock("~/hooks/api/config/network", () => ({ ...jest.requireActual("~/hooks/api/config/network"), useConnectionMutation: () => ({ @@ -51,21 +66,6 @@ jest.mock("~/hooks/api/system/network", () => ({ useConnections: () => mockSystem.connections, })); -const mockConnection = new Connection("Visible Network", { - wireless: new Wireless({ ssid: "Visible Network" }), -}); - -const mockSystem = { - connections: [mockConnection], - state: { - connectivity: true, - wiredEnabled: true, - wirelessEnabled: false, - persistNetwork: true, - copyEnabled: false, - }, -}; - const networkMock = { ssid: "Visible Network", hidden: false, diff --git a/web/src/components/overview/StorageSection.test.tsx b/web/src/components/overview/StorageSection.test.tsx index 7af481e513..804e8a06bf 100644 --- a/web/src/components/overview/StorageSection.test.tsx +++ b/web/src/components/overview/StorageSection.test.tsx @@ -26,7 +26,6 @@ import { plainRender } from "~/test-utils"; import { StorageSection } from "~/components/overview"; import type { storage } from "~/api/system"; - let mockModel = { drives: [], }; diff --git a/web/src/context/installer.test.tsx b/web/src/context/installer.test.tsx index 098e1d2ef1..fe27873878 100644 --- a/web/src/context/installer.test.tsx +++ b/web/src/context/installer.test.tsx @@ -28,7 +28,6 @@ import { InstallerClientProvider } from "./installer"; import { DummyWSClient } from "~/client/ws"; jest.mock("~/components/layout/Loading", () => { - const React = require("react"); return () => React.createElement("div", null, "Loading Mock"); }); diff --git a/web/src/test-utils.tsx b/web/src/test-utils.tsx index f6fa95a86a..3a56a1b395 100644 --- a/web/src/test-utils.tsx +++ b/web/src/test-utils.tsx @@ -156,7 +156,7 @@ const installerRender = (ui: React.ReactNode, options: { withL10n?: boolean } = // ✅ turns retries off retry: false, // ✅ make sure that data is not retrieved again when not needed - staleTime: Infinity + staleTime: Infinity, }, }, }); From 6b1898952c070c1367204d18bec1ac0bce157286 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, 4 Dec 2025 07:07:09 +0000 Subject: [PATCH 12/27] Add tests for boot section (ai) --- .../components/storage/BootSection.test.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 web/src/components/storage/BootSection.test.tsx diff --git a/web/src/components/storage/BootSection.test.tsx b/web/src/components/storage/BootSection.test.tsx new file mode 100644 index 0000000000..8bdc57e30f --- /dev/null +++ b/web/src/components/storage/BootSection.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright (c) [2024-2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { useAvailableDrives } from "~/hooks/api/system/storage"; +import { useModel } from "~/hooks/storage/model"; +import { installerRender } from "~/test-utils"; +import type { storage } from "~/api/system"; + +import BootSection from "./BootSection"; + +jest.mock("~/hooks/api/system/storage"); +jest.mock("~/hooks/storage/model"); + +const useAvailableDrivesMock = useAvailableDrives as jest.Mock; +const useModelMock = useModel as jest.Mock; + +const mockSda: storage.Device = { + sid: 1, + name: "sda", + class: "drive", + block: { + start: 0, + size: 536870912000, + shrinking: { supported: false }, + }, + drive: { type: "disk" }, +}; + +describe("BootSection", () => { + afterEach(() => { + useAvailableDrivesMock.mockReset(); + useModelMock.mockReset(); + }); + + it("shows the default message when the boot is default", () => { + useAvailableDrivesMock.mockReturnValue([]); + useModelMock.mockReturnValue({ + boot: { + isDefault: true, + getDevice: () => null, + }, + }); + + installerRender(); + + expect( + screen.getByText(/Partitions to boot will be set up if needed at the installation disk/), + ).toBeInTheDocument(); + }); + + it("shows the current device when the boot is default", () => { + useAvailableDrivesMock.mockReturnValue([mockSda]); + useModelMock.mockReturnValue({ + boot: { + isDefault: true, + getDevice: () => mockSda, + }, + }); + + installerRender(); + + expect( + screen.getByText(/Currently sda \(500 GiB\), based on the location/), + ).toBeInTheDocument(); + }); + + it("shows a specific device when the boot is not default", () => { + useAvailableDrivesMock.mockReturnValue([mockSda]); + useModelMock.mockReturnValue({ + boot: { + isDefault: false, + getDevice: () => mockSda, + }, + }); + + installerRender(); + + expect( + screen.getByText(/Partitions to boot will be set up if needed at sda \(500 GiB\)\./), + ).toBeInTheDocument(); + }); + + it("shows that no partition will be configured", () => { + useAvailableDrivesMock.mockReturnValue([]); + useModelMock.mockReturnValue({ + boot: { + isDefault: false, + getDevice: () => null, + }, + }); + + installerRender(); + + expect( + screen.getByText(/No partitions will be automatically configured for booting./), + ).toBeInTheDocument(); + }); + + it("shows a link for changing the boot device", () => { + useAvailableDrivesMock.mockReturnValue([]); + useModelMock.mockReturnValue({ + boot: { + isDefault: true, + getDevice: () => null, + }, + }); + + installerRender(); + + expect(screen.getByRole("link", { name: /Change/ })).toBeInTheDocument(); + }); +}); From 02cdce5305cd1452e48e3a50bb883b5789bc9df2 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, 4 Dec 2025 07:26:31 +0000 Subject: [PATCH 13/27] Fix connected devices menu tests (ai) --- .../storage/ConnectedDevicesMenu.test.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/components/storage/ConnectedDevicesMenu.test.tsx b/web/src/components/storage/ConnectedDevicesMenu.test.tsx index 6d2cdafb3a..8b4ff1f983 100644 --- a/web/src/components/storage/ConnectedDevicesMenu.test.tsx +++ b/web/src/components/storage/ConnectedDevicesMenu.test.tsx @@ -25,28 +25,28 @@ import { screen, within } from "@testing-library/react"; import { installerRender, mockNavigateFn } from "~/test-utils"; import ConnectedDevicesMenu from "./ConnectedDevicesMenu"; import { STORAGE as PATHS } from "~/routes/paths"; +import { activateStorageAction } from "~/api"; + +jest.mock("~/api"); const mockUseZFCPSupported = jest.fn(); + jest.mock("~/queries/storage/zfcp", () => ({ ...jest.requireActual("~/queries/storage/zfcp"), useZFCPSupported: () => mockUseZFCPSupported(), })); const mockUseDASDSupported = jest.fn(); + jest.mock("~/queries/storage/dasd", () => ({ ...jest.requireActual("~/queries/storage/dasd"), useDASDSupported: () => mockUseDASDSupported(), })); -const mockReactivateSystem = jest.fn(); -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), - useReactivateSystem: () => mockReactivateSystem(), -})); - beforeEach(() => { mockUseZFCPSupported.mockReturnValue(false); mockUseDASDSupported.mockReturnValue(false); + (activateStorageAction as jest.Mock).mockClear(); }); async function openMenu() { @@ -67,7 +67,7 @@ it("allows users to rescan devices", async () => { const { user, menu } = await openMenu(); const reprobeItem = within(menu).getByRole("menuitem", { name: /Rescan/ }); await user.click(reprobeItem); - expect(mockReactivateSystem).toHaveBeenCalled(); + expect(activateStorageAction).toHaveBeenCalled(); }); it("allows users to configure iSCSI", async () => { From 4b710cce0731977292e9b2986f6e942fd96cb2d3 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, 4 Dec 2025 07:50:45 +0000 Subject: [PATCH 14/27] Fix partition page tests (ai) --- .../components/storage/PartitionPage.test.tsx | 136 +++++++++++------- 1 file changed, 88 insertions(+), 48 deletions(-) diff --git a/web/src/components/storage/PartitionPage.test.tsx b/web/src/components/storage/PartitionPage.test.tsx index 93fce538e8..1e2a4c8d72 100644 --- a/web/src/components/storage/PartitionPage.test.tsx +++ b/web/src/components/storage/PartitionPage.test.tsx @@ -29,6 +29,8 @@ import type { storage } from "~/api/system"; import { model as apiModel } from "~/api/storage"; import { gib } from "./utils"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => null); + jest.mock("~/hooks/api/issue", () => ({ useIssuesChanges: jest.fn(), useIssues: () => [], @@ -86,12 +88,38 @@ const sda: storage.Device = { }; const mockPartition: model.Partition = { + mountPath: "/var", + size: { min: gib(1), default: false }, + filesystem: { type: "ext4", default: false }, isNew: false, isUsed: true, isReused: false, isUsedBySpacePolicy: false, }; +const mockApiDrive: apiModel.Drive = { + name: "/dev/sda", + spacePolicy: "delete", + partitions: [ + { + mountPath: "swap", + size: { + min: gib(2), + default: false, // false: user provided, true: calculated + }, + filesystem: { default: false, type: "swap" }, + }, + { + mountPath: "/home", + size: { + min: gib(16), + default: true, + }, + filesystem: { default: false, type: "xfs" }, + }, + ], +}; + const mockDrive: model.Drive = { name: "/dev/sda", spacePolicy: "delete", @@ -134,7 +162,7 @@ const mockDrive: model.Drive = { }; const mockSolvedConfigModel: apiModel.Config = { - drives: [mockDrive], + drives: [mockApiDrive], }; const mockHomeVolume: storage.Volume = { @@ -160,6 +188,7 @@ const mockHomeVolume: storage.Volume = { jest.mock("~/hooks/api/system/storage", () => ({ useDevices: () => [sda], useVolumeTemplate: () => mockHomeVolume, + useDevice: () => sda, })); jest.mock("~/hooks/storage/model", () => ({ @@ -168,26 +197,31 @@ jest.mock("~/hooks/storage/model", () => ({ drives: [mockDrive], getMountPaths: () => [], }), -})); - -jest.mock("~/hooks/storage/product", () => ({ - ...jest.requireActual("~/hooks/storage/product"), useMissingMountPaths: () => ["/home", "swap"], + useDrive: () => mockDrive, })); jest.mock("~/hooks/api/storage", () => ({ - useStorageModel: () => ({ drives: [mockDrive] }), - useSolvedStorageModel: () => mockSolvedConfigModel, + storageModelQuery: { + queryKey: ["storageModel"], + queryFn: () => Promise.resolve({ drives: [mockApiDrive] }), + }, + useStorageModel: () => ({ drives: [mockApiDrive] }), +})); + +jest.mock("~/queries/storage/config-model", () => ({ + ...jest.requireActual("~/queries/storage/config-model"), + useSolvedConfigModel: () => mockSolvedConfigModel, })); beforeEach(() => { - mockParams({ list: "drives", listIndex: "0" }); + mockParams({ collection: "drives", index: "0" }); }); describe("PartitionPage", () => { it("renders a form for defining a partition", async () => { const { user } = installerRender(); - screen.getByRole("form", { name: "Configure partition at /dev/sda" }); + await screen.findByRole("form", { name: "Configure partition at /dev/sda" }); const mountPoint = screen.getByRole("button", { name: "Mount point toggle" }); const mountPointMode = screen.getByRole("button", { name: "Mount point mode" }); const filesystem = screen.getByRole("button", { name: "File system" }); @@ -225,7 +259,7 @@ describe("PartitionPage", () => { it("allows reseting the chosen mount point", async () => { const { user } = installerRender(); // Note that the underline PF component gives the role combobox to the input - const mountPoint = screen.getByRole("combobox", { name: "Mount point" }); + const mountPoint = await screen.findByRole("combobox", { name: "Mount point" }); const filesystem = screen.getByRole("button", { name: "File system" }); let size = screen.getByRole("button", { name: "Size mode" }); expect(mountPoint).toHaveValue(""); @@ -256,7 +290,7 @@ describe("PartitionPage", () => { it("does not allow sending sizes without units", async () => { const { user } = installerRender(); - screen.getByRole("form", { name: "Configure partition at /dev/sda" }); + await screen.findByRole("form", { name: "Configure partition at /dev/sda" }); const mountPoint = screen.getByRole("button", { name: "Mount point toggle" }); const acceptButton = screen.getByRole("button", { name: "Accept" }); @@ -283,44 +317,48 @@ describe("PartitionPage", () => { }); describe("if editing a partition", () => { - beforeEach(() => { - mockParams({ list: "drives", listIndex: "0", partitionId: "/home" }); - mockGetPartition.mockReturnValue({ - mountPath: "/home", - size: { - default: false, - min: gib(5), - max: gib(5), - }, - filesystem: { - default: false, - type: "xfs", - label: "HOME", - }, + describe("with fixed size", () => { + beforeEach(() => { + mockParams({ collection: "drives", index: "0", partitionId: "/home" }); + mockGetPartition.mockReturnValue({ + mountPath: "/home", + size: { + default: false, + min: gib(5), + max: gib(5), + }, + filesystem: { + default: false, + type: "xfs", + label: "HOME", + }, + }); }); - }); - it("initializes the form with the partition values", async () => { - installerRender(); - const mountPointSelector = screen.getByRole("combobox", { name: "Mount point" }); - expect(mountPointSelector).toHaveValue("/home"); - const targetButton = screen.getByRole("button", { name: "Mount point mode" }); - within(targetButton).getByText(/As a new partition/); - const filesystemButton = screen.getByRole("button", { name: "File system" }); - within(filesystemButton).getByText("XFS"); - const label = screen.getByRole("textbox", { name: "File system label" }); - expect(label).toHaveValue("HOME"); - const sizeModeButton = screen.getByRole("button", { name: "Size mode" }); - within(sizeModeButton).getByText("Manual"); - const sizeInput = screen.getByRole("textbox", { name: "Size" }); - expect(sizeInput).toHaveValue("5 GiB"); - const growCheck = screen.getByRole("checkbox", { name: "Allow growing" }); - expect(growCheck).not.toBeChecked(); + it("initializes the form with the partition values", async () => { + installerRender(); + const mountPointSelector = await screen.findByRole("combobox", { + name: "Mount point", + }); + expect(mountPointSelector).toHaveValue("/home"); + const targetButton = screen.getByRole("button", { name: "Mount point mode" }); + within(targetButton).getByText(/As a new partition/); + const filesystemButton = screen.getByRole("button", { name: "File system" }); + within(filesystemButton).getByText("XFS"); + const label = screen.getByRole("textbox", { name: "File system label" }); + expect(label).toHaveValue("HOME"); + const sizeModeButton = screen.getByRole("button", { name: "Size mode" }); + within(sizeModeButton).getByText("Manual"); + const sizeInput = screen.getByRole("textbox", { name: "Size" }); + expect(sizeInput).toHaveValue("5 GiB"); + const growCheck = screen.getByRole("checkbox", { name: "Allow growing" }); + expect(growCheck).not.toBeChecked(); + }); }); describe("if the max size is unlimited", () => { beforeEach(() => { - mockParams({ list: "drives", listIndex: "0", partitionId: "/home" }); + mockParams({ collection: "drives", index: "0", partitionId: "/home" }); mockGetPartition.mockReturnValue({ mountPath: "/home", size: { @@ -336,14 +374,14 @@ describe("PartitionPage", () => { it("checks allow growing", async () => { installerRender(); - const growCheck = screen.getByRole("checkbox", { name: "Allow growing" }); + const growCheck = await screen.findByRole("checkbox", { name: "Allow growing" }); expect(growCheck).toBeChecked(); }); }); describe("if the max size has a value", () => { beforeEach(() => { - mockParams({ list: "drives", listIndex: "0", partitionId: "/home" }); + mockParams({ collection: "drives", index: "0", partitionId: "/home" }); mockGetPartition.mockReturnValue({ mountPath: "/home", size: { @@ -360,7 +398,9 @@ describe("PartitionPage", () => { it("allows switching to a fixed size", async () => { const { user } = installerRender(); - const switchButton = screen.getByRole("button", { name: /Discard the maximum/ }); + const switchButton = await screen.findByRole("button", { + name: /Discard the maximum/, + }); await user.click(switchButton); const sizeInput = screen.getByRole("textbox", { name: "Size" }); expect(sizeInput).toHaveValue("5 GiB"); @@ -371,7 +411,7 @@ describe("PartitionPage", () => { describe("if the default size has a max value", () => { beforeEach(() => { - mockParams({ list: "drives", listIndex: "0", partitionId: "/home" }); + mockParams({ collection: "drives", index: "0", partitionId: "/home" }); mockGetPartition.mockReturnValue({ mountPath: "/home", size: { @@ -388,7 +428,7 @@ describe("PartitionPage", () => { it("allows switching to a custom size", async () => { const { user } = installerRender(); - const sizeModeButton = screen.getByRole("button", { name: "Size mode" }); + const sizeModeButton = await screen.findByRole("button", { name: "Size mode" }); await user.click(sizeModeButton); const sizeModes = screen.getByRole("listbox", { name: "Size modes" }); const customSize = within(sizeModes).getByRole("option", { name: /Manual/ }); From e85f04151f1b360277293736363ab94ab6adf654 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, 4 Dec 2025 10:10:23 +0000 Subject: [PATCH 15/27] Fix config editor tests (ai) --- .../components/storage/ConfigEditor.test.tsx | 77 ++++++++----------- 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/web/src/components/storage/ConfigEditor.test.tsx b/web/src/components/storage/ConfigEditor.test.tsx index 3d1b7c52c6..7df95fd11c 100644 --- a/web/src/components/storage/ConfigEditor.test.tsx +++ b/web/src/components/storage/ConfigEditor.test.tsx @@ -22,34 +22,17 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import ConfigEditor from "~/components/storage/ConfigEditor"; -import { StorageDevice } from "~/storage"; -import { apiModel } from "~/api/storage/types"; - -const disk: StorageDevice = { - sid: 60, - type: "disk", - isDrive: true, - description: "", - vendor: "Seagate", - model: "Unknown", - driver: ["ahci", "mmcblk"], - bus: "IDE", - name: "/dev/vda", - size: 1e6, -}; -const mockUseDevices = jest.fn(); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useDevices: () => mockUseDevices(), +const mockUseModel = jest.fn(); +jest.mock("~/hooks/storage/model", () => ({ + useModel: () => mockUseModel(), })); -const mockUseApiModel = jest.fn(); -jest.mock("~/hooks/storage/api-model", () => ({ - ...jest.requireActual("~/hooks/storage/api-model"), - useApiModel: () => mockUseApiModel(), +const mockUseReset = jest.fn(); +jest.mock("~/hooks/api/config/storage", () => ({ + useReset: () => mockUseReset, })); jest.mock("./DriveEditor", () => () =>
drive editor
); @@ -57,85 +40,86 @@ jest.mock("./MdRaidEditor", () => () =>
raid editor
); jest.mock("./VolumeGroupEditor", () => () =>
volume group editor
); jest.mock("./ConfigureDeviceMenu", () => () =>
add device
); -const hasDrives: apiModel.Config = { +const hasDrives = { drives: [{ name: "/dev/vda" }], mdRaids: [], volumeGroups: [], }; -const hasVolumeGroups: apiModel.Config = { +const hasVolumeGroups = { drives: [], mdRaids: [], - volumeGroups: [{ vgName: "/dev/system" }], + volumeGroups: [{ name: "/dev/system" }], }; -const hasBoth: apiModel.Config = { +const hasBoth = { drives: [{ name: "/dev/vda" }], mdRaids: [], - volumeGroups: [{ vgName: "/dev/system" }], + volumeGroups: [{ name: "/dev/system" }], }; -const hasNothing: apiModel.Config = { +const hasNothing = { drives: [], mdRaids: [], volumeGroups: [], }; beforeEach(() => { - mockUseDevices.mockReturnValue([disk]); + mockUseModel.mockClear(); + mockUseReset.mockClear(); }); describe("when no drive is used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasVolumeGroups); + mockUseModel.mockReturnValue(hasVolumeGroups); }); it("does not render the drive editor", () => { - plainRender(); + installerRender(); expect(screen.queryByText("drive editor")).not.toBeInTheDocument(); }); }); describe("when a drive is used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasDrives); + mockUseModel.mockReturnValue(hasDrives); }); it("renders the drive editor", () => { - plainRender(); + installerRender(); expect(screen.queryByText("drive editor")).toBeInTheDocument(); }); }); describe("when no volume group is used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasDrives); + mockUseModel.mockReturnValue(hasDrives); }); it("does not render the volume group editor", () => { - plainRender(); + installerRender(); expect(screen.queryByText("volume group editor")).not.toBeInTheDocument(); }); }); describe("when a volume group is used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasVolumeGroups); + mockUseModel.mockReturnValue(hasVolumeGroups); }); it("renders the volume group editor", () => { - plainRender(); + installerRender(); expect(screen.queryByText("volume group editor")).toBeInTheDocument(); }); }); describe("when both a drive and volume group are used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasBoth); + mockUseModel.mockReturnValue(hasBoth); }); it("renders a volume group editor followed by drive editor", () => { - plainRender(); + installerRender(); const volumeGroupEditor = screen.getByText("volume group editor"); const driveEditor = screen.getByText("drive editor"); @@ -147,13 +131,14 @@ describe("when both a drive and volume group are used for installation", () => { describe("when neither a drive nor volume group are used for installation", () => { beforeEach(() => { - mockUseApiModel.mockReturnValue(hasNothing); + mockUseModel.mockReturnValue(hasNothing); }); - it("renders a no configuration alert with a button for resetting to default", () => { - plainRender(); - screen.getByText("Custom alert:"); + it("renders a no configuration alert with a button for resetting to default", async () => { + const { user } = installerRender(); screen.getByText("No devices configured yet"); - screen.getByRole("button", { name: "reset to defaults" }); + const resetButton = screen.getByRole("button", { name: "reset to defaults" }); + await user.click(resetButton); + expect(mockUseReset).toHaveBeenCalled(); }); }); From 2d0c98e68218d4c926f1ad0762fd4f25d49a465b 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, 4 Dec 2025 11:09:18 +0000 Subject: [PATCH 16/27] Fix proposal page tests (ai) --- .../components/storage/ProposalPage.test.tsx | 162 +++++++++--------- 1 file changed, 82 insertions(+), 80 deletions(-) diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx index cb39c0930b..518f68f0e9 100644 --- a/web/src/components/storage/ProposalPage.test.tsx +++ b/web/src/components/storage/ProposalPage.test.tsx @@ -29,62 +29,78 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalPage from "~/components/storage/ProposalPage"; -import { StorageDevice } from "~/storage"; -import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; +import { Device } from "~/api/system/storage"; +import type { Issue } from "~/api/issue"; +import { storage as proposalStorage } from "~/api/proposal"; +import { model as storageModel } from "~/api/storage"; -const disk: StorageDevice = { +const disk: Device = { sid: 60, - type: "disk", - isDrive: true, - description: "", - vendor: "Seagate", - model: "Unknown", - driver: ["ahci", "mmcblk"], - bus: "IDE", name: "/dev/vda", - size: 1e6, + description: "", + class: "drive", + drive: { + type: "disk", + vendor: "Seagate", + model: "Unknown", + driver: ["ahci", "mmcblk"], + bus: "IDE", + }, + block: { + size: 1e6, + start: 0, + shrinking: { + supported: false, + }, + }, +}; + +const mockProposal: proposalStorage.Proposal = { + devices: [], + actions: [], }; const systemError: Issue = { description: "System error", - kind: "storage", - details: "", - source: IssueSource.System, - severity: IssueSeverity.Error, + class: "system", scope: "storage", }; const configError: Issue = { description: "Config error", - kind: "storage", - details: "", - source: IssueSource.Config, - severity: IssueSeverity.Error, + class: "config", scope: "storage", }; const mockUseAvailableDevices = jest.fn(); -const mockUseResetConfigMutation = jest.fn(); -const mockUseDeprecated = jest.fn(); -const mockUseDeprecatedChanges = jest.fn(); -const mockUseReprobeMutation = jest.fn(); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useResetConfigMutation: () => mockUseResetConfigMutation(), - useDeprecated: () => mockUseDeprecated(), - useDeprecatedChanges: () => mockUseDeprecatedChanges(), - useReprobeMutation: () => mockUseReprobeMutation(), -})); +const mockUseReset = jest.fn(); +const mockUseProposal = jest.fn(); +const mockActivateStorageAction = jest.fn(); -jest.mock("~/hooks/storage/system", () => ({ - ...jest.requireActual("~/hooks/storage/system"), +jest.mock("~/hooks/api/system/storage", () => ({ + ...jest.requireActual("~/hooks/api/system/storage"), useAvailableDevices: () => mockUseAvailableDevices(), })); -const mockUseConfigModel = jest.fn(); -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => mockUseConfigModel(), +jest.mock("~/hooks/api/config/storage", () => ({ + ...jest.requireActual("~/hooks/api/config/storage"), + useReset: () => mockUseReset(), +})); + +jest.mock("~/hooks/api/proposal/storage", () => ({ + ...jest.requireActual("~/hooks/api/proposal/storage"), + useProposal: () => mockUseProposal(), +})); + +jest.mock("~/api", () => ({ + ...jest.requireActual("~/api"), + activateStorageAction: () => mockActivateStorageAction(), +})); + +const mockUseStorageModel = jest.fn(); +jest.mock("~/hooks/api/storage", () => ({ + ...jest.requireActual("~/hooks/api/storage"), + useStorageModel: () => mockUseStorageModel(), })); const mockUseZFCPSupported = jest.fn(); @@ -99,12 +115,10 @@ jest.mock("~/queries/storage/dasd", () => ({ useDASDSupported: () => mockUseDASDSupported(), })); -const mockUseSystemErrors = jest.fn(); -const mockUseConfigErrors = jest.fn(); -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), - useSystemErrors: () => mockUseSystemErrors(), - useConfigErrors: () => mockUseConfigErrors(), +const mockUseIssues = jest.fn(); +jest.mock("~/hooks/api/issue", () => ({ + ...jest.requireActual("~/hooks/api/issue"), + useIssues: () => mockUseIssues(), })); jest.mock("./ProposalTransactionalInfo", () => () =>
trasactional info
); @@ -119,18 +133,16 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( )); beforeEach(() => { - mockUseResetConfigMutation.mockReturnValue({ mutate: jest.fn() }); - mockUseReprobeMutation.mockReturnValue({ mutateAsync: jest.fn() }); - mockUseDeprecated.mockReturnValue(false); - mockUseSystemErrors.mockReturnValue([]); - mockUseConfigErrors.mockReturnValue([]); + mockUseReset.mockReturnValue(jest.fn()); + mockUseIssues.mockReturnValue([]); + mockUseProposal.mockReturnValue(null); + mockUseStorageModel.mockReturnValue(null); + mockUseAvailableDevices.mockReturnValue([]); + mockUseZFCPSupported.mockReturnValue(false); + mockUseDASDSupported.mockReturnValue(false); }); describe("if there are not devices", () => { - beforeEach(() => { - mockUseAvailableDevices.mockReturnValue([]); - }); - it("renders an option for activating iSCSI", () => { installerRender(); expect(screen.queryByRole("link", { name: /iSCSI/ })).toBeInTheDocument(); @@ -147,10 +159,6 @@ describe("if there are not devices", () => { }); describe("if zFCP is not supported", () => { - beforeEach(() => { - mockUseZFCPSupported.mockReturnValue(false); - }); - it("does not render an option for activating zFCP", () => { installerRender(); expect(screen.queryByRole("link", { name: /zFCP/ })).not.toBeInTheDocument(); @@ -158,10 +166,6 @@ describe("if there are not devices", () => { }); describe("if DASD is not supported", () => { - beforeEach(() => { - mockUseDASDSupported.mockReturnValue(false); - }); - it("does not render an option for activating DASD", () => { installerRender(); expect(screen.queryByRole("link", { name: /DASD/ })).not.toBeInTheDocument(); @@ -194,17 +198,16 @@ describe("if there are not devices", () => { describe("if there is not a model", () => { beforeEach(() => { mockUseAvailableDevices.mockReturnValue([disk]); - mockUseConfigModel.mockReturnValue(null); }); - describe("and there are system errors", () => { + describe("and there are issues", () => { beforeEach(() => { - mockUseSystemErrors.mockReturnValue([systemError]); + mockUseIssues.mockReturnValue([systemError]); }); it("renders an option for resetting the config", () => { installerRender(); - expect(screen.queryByRole("button", { name: /Reset/ })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /reset/i })).toBeInTheDocument(); }); it("does not render the installation devices", () => { @@ -218,12 +221,9 @@ describe("if there is not a model", () => { }); }); - describe("and there are not system errors", () => { - beforeEach(() => { - mockUseSystemErrors.mockReturnValue([]); - }); - + describe("and there are not issues", () => { it("renders an unsupported model alert", async () => { + mockUseProposal.mockReturnValue(mockProposal); installerRender(); expect(screen.queryByText("unsupported info")).toBeInTheDocument(); }); @@ -234,6 +234,7 @@ describe("if there is not a model", () => { }); it("renders the result", () => { + mockUseProposal.mockReturnValue(mockProposal); installerRender(); expect(screen.queryByText("result")).toBeInTheDocument(); }); @@ -243,13 +244,13 @@ describe("if there is not a model", () => { describe("if there is a model", () => { beforeEach(() => { mockUseAvailableDevices.mockReturnValue([disk]); - mockUseConfigModel.mockReturnValue({ drives: [] }); + const model: storageModel.Config = { drives: [] }; + mockUseStorageModel.mockReturnValue(model); }); - describe("and there are config errors and system errors", () => { + describe("and there are issues", () => { beforeEach(() => { - mockUseConfigErrors.mockReturnValue([configError]); - mockUseSystemErrors.mockReturnValue([systemError]); + mockUseIssues.mockReturnValue([configError, systemError]); }); it("renders the config errors", () => { @@ -259,7 +260,7 @@ describe("if there is a model", () => { it("renders an option for resetting the config", () => { installerRender(); - expect(screen.queryByRole("button", { name: /Reset/ })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /reset/i })).toBeInTheDocument(); }); it("does not render the installation devices", () => { @@ -273,9 +274,14 @@ describe("if there is a model", () => { }); }); - describe("and there are not config errors but there are system errors", () => { + describe("and there are only proposal errors", () => { beforeEach(() => { - mockUseSystemErrors.mockReturnValue([systemError]); + const proposalError: Issue = { + description: "Proposal error", + class: "proposal", + scope: "storage", + }; + mockUseIssues.mockReturnValue([proposalError]); }); it("renders a failed proposal failed", () => { @@ -295,17 +301,13 @@ describe("if there is a model", () => { }); describe("and there are neither config errors nor system errors", () => { - beforeEach(() => { - mockUseSystemErrors.mockReturnValue([]); - mockUseConfigErrors.mockReturnValue([]); - }); - it("renders the installation devices", () => { installerRender(); expect(screen.queryByText("installation devices")).toBeInTheDocument(); }); it("renders the result", () => { + mockUseProposal.mockReturnValue(mockProposal); installerRender(); expect(screen.queryByText("result")).toBeInTheDocument(); }); From 0af376fbfc74f7e1df7748e5214761751d4f34e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 06:40:42 +0000 Subject: [PATCH 17/27] Add tests for menu device description (ai) --- .../storage/MenuDeviceDescription.test.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 web/src/components/storage/MenuDeviceDescription.test.tsx diff --git a/web/src/components/storage/MenuDeviceDescription.test.tsx b/web/src/components/storage/MenuDeviceDescription.test.tsx new file mode 100644 index 0000000000..7633028b04 --- /dev/null +++ b/web/src/components/storage/MenuDeviceDescription.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (c) [2023-2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import MenuDeviceDescription from "./MenuDeviceDescription"; +import { + typeDescription, + contentDescription, + filesystemLabels, +} from "~/components/storage/utils/device"; +import type { storage } from "~/api/system"; + +jest.mock("~/components/storage/utils/device", () => ({ + typeDescription: jest.fn(), + contentDescription: jest.fn(), + filesystemLabels: jest.fn(), +})); + +const mockTypeDescription = typeDescription as jest.Mock; +const mockContentDescription = contentDescription as jest.Mock; +const mockFilesystemLabels = filesystemLabels as jest.Mock; + +describe("MenuDeviceDescription", () => { + const device: storage.Device = { + sid: 1, + name: "sda", + class: "drive", + }; + + beforeEach(() => { + mockTypeDescription.mockReturnValue("Drive"); + mockContentDescription.mockReturnValue("1GB"); + mockFilesystemLabels.mockReturnValue([]); + }); + + it("renders type and content descriptions", () => { + render(); + expect(screen.getByText("Drive")).toBeInTheDocument(); + expect(screen.getByText("1GB")).toBeInTheDocument(); + }); + + it("renders filesystem labels when available", () => { + mockFilesystemLabels.mockReturnValue(["ext4", "xfs"]); + render(); + expect(screen.getByText("ext4")).toBeInTheDocument(); + expect(screen.getByText("xfs")).toBeInTheDocument(); + }); + + it("does not render filesystem labels when not available", () => { + render(); + expect(screen.queryByText("ext4")).not.toBeInTheDocument(); + expect(screen.queryByText("xfs")).not.toBeInTheDocument(); + }); +}); From 2e71fe26832a786dc3bd73f4f118e752894ab4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 08:58:22 +0000 Subject: [PATCH 18/27] Fix formattable device page tests (ai) --- .../storage/FormattableDevicePage.test.tsx | 89 +++++++++++++------ 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/web/src/components/storage/FormattableDevicePage.test.tsx b/web/src/components/storage/FormattableDevicePage.test.tsx index aee3f0a5db..829f00764c 100644 --- a/web/src/components/storage/FormattableDevicePage.test.tsx +++ b/web/src/components/storage/FormattableDevicePage.test.tsx @@ -70,7 +70,7 @@ const mockHomeVolume: storage.Volume = { transactional: false, outline: { required: true, - fsTypes: ["btrfs", "ext4"], + fsTypes: ["btrfs", "ext4", "xfs"], supportAutoSize: true, snapshotsConfigurable: true, snapshotsAffectSizes: true, @@ -79,27 +79,30 @@ const mockHomeVolume: storage.Volume = { }, }; -jest.mock("~/queries/issues", () => ({ - ...jest.requireActual("~/queries/issues"), +jest.mock("~/hooks/api/config", () => ({ + useProduct: () => ({ + name: "Test Product", + }), +})); + +jest.mock("~/hooks/api/issue", () => ({ useIssues: () => [], })); jest.mock("~/hooks/api/system/storage", () => ({ ...jest.requireActual("~/hooks/api/system/storage"), useDevices: () => [sda], + useDevice: () => sda, useVolumeTemplate: () => mockHomeVolume, })); const mockModel = jest.fn(); jest.mock("~/hooks/storage/model", () => ({ - ...jest.requireActual("~/hooks/storage/model"), + __esModule: true, useModel: () => mockModel(), -})); - -jest.mock("~/hooks/storage/product", () => ({ - ...jest.requireActual("~/hooks/storage/product"), useMissingMountPaths: () => ["/home", "swap"], - useVolume: () => mockHomeVolume, + useDrive: (index: number) => mockModel().drives?.[index], + useMdRaid: (index: number) => mockModel().mdRaids?.[index], })); const mockAddFilesystem = jest.fn(); @@ -109,7 +112,7 @@ jest.mock("~/hooks/storage/filesystem", () => ({ })); beforeEach(() => { - mockParams({ list: "drives", listIndex: "0" }); + mockParams({ collection: "drives", index: "0" }); mockModel.mockReturnValue({ drives: [sdaModel], getMountPaths: () => [], @@ -119,16 +122,22 @@ beforeEach(() => { describe("FormattableDevicePage", () => { it("renders a form for formatting the device", async () => { const { user } = installerRender(); - screen.getByRole("form", { name: "Configure device /dev/sda" }); - const mountPoint = screen.getByRole("button", { name: "Mount point toggle" }); + await screen.findByRole("form", { name: "Configure device /dev/sda" }); + const mountPoint = screen.getByRole("button", { + name: "Mount point toggle", + }); const filesystem = screen.getByRole("button", { name: "File system" }); // File system and size fields disabled until valid mount point selected expect(filesystem).toBeDisabled(); expect(screen.queryByRole("textbox", { name: "File system label" })).not.toBeInTheDocument(); await user.click(mountPoint); - const mountPointOptions = screen.getByRole("listbox", { name: "Suggested mount points" }); - const homeMountPoint = within(mountPointOptions).getByRole("option", { name: "/home" }); + const mountPointOptions = screen.getByRole("listbox", { + name: "Suggested mount points", + }); + const homeMountPoint = within(mountPointOptions).getByRole("option", { + name: "/home", + }); await user.click(homeMountPoint); // Valid mount point selected, enable file system field expect(filesystem).toBeEnabled(); @@ -141,15 +150,23 @@ describe("FormattableDevicePage", () => { it("allows reseting the chosen mount point", async () => { const { user } = installerRender(); // Note that the underline PF component gives the role combobox to the input - const mountPoint = screen.getByRole("combobox", { name: "Mount point" }); + const mountPoint = await screen.findByRole("combobox", { + name: "Mount point", + }); const filesystem = screen.getByRole("button", { name: "File system" }); expect(mountPoint).toHaveValue(""); // File system field is disabled until a valid mount point selected expect(filesystem).toBeDisabled(); - const mountPointToggle = screen.getByRole("button", { name: "Mount point toggle" }); + const mountPointToggle = screen.getByRole("button", { + name: "Mount point toggle", + }); await user.click(mountPointToggle); - const mountPointOptions = screen.getByRole("listbox", { name: "Suggested mount points" }); - const homeMountPoint = within(mountPointOptions).getByRole("option", { name: "/home" }); + const mountPointOptions = screen.getByRole("listbox", { + name: "Suggested mount points", + }); + const homeMountPoint = within(mountPointOptions).getByRole("option", { + name: "/home", + }); await user.click(homeMountPoint); expect(mountPoint).toHaveValue("/home"); expect(filesystem).toBeEnabled(); @@ -184,9 +201,13 @@ describe("FormattableDevicePage", () => { it("initializes the form with the current values", async () => { installerRender(); - const mountPointSelector = screen.getByRole("combobox", { name: "Mount point" }); + const mountPointSelector = await screen.findByRole("combobox", { + name: "Mount point", + }); expect(mountPointSelector).toHaveValue("/home"); - const filesystemButton = screen.getByRole("button", { name: "File system" }); + const filesystemButton = screen.getByRole("button", { + name: "File system", + }); within(filesystemButton).getByText("XFS"); const label = screen.getByRole("textbox", { name: "File system label" }); expect(label).toHaveValue("HOME"); @@ -196,17 +217,31 @@ describe("FormattableDevicePage", () => { describe("if the form is accepted", () => { it("changes the device config", async () => { const { user } = installerRender(); - const mountPointToggle = screen.getByRole("button", { name: "Mount point toggle" }); + const mountPointToggle = await screen.findByRole("button", { + name: "Mount point toggle", + }); await user.click(mountPointToggle); - const mountPointOptions = screen.getByRole("listbox", { name: "Suggested mount points" }); - const homeMountPoint = within(mountPointOptions).getByRole("option", { name: "/home" }); + const mountPointOptions = screen.getByRole("listbox", { + name: "Suggested mount points", + }); + const homeMountPoint = within(mountPointOptions).getByRole("option", { + name: "/home", + }); await user.click(homeMountPoint); - const filesystemButton = screen.getByRole("button", { name: "File system" }); + const filesystemButton = screen.getByRole("button", { + name: "File system", + }); await user.click(filesystemButton); - const filesystemOptions = screen.getByRole("listbox", { name: "Available file systems" }); - const xfs = within(filesystemOptions).getByRole("option", { name: "XFS" }); + const filesystemOptions = screen.getByRole("listbox", { + name: "Available file systems", + }); + const xfs = within(filesystemOptions).getByRole("option", { + name: "XFS", + }); await user.click(xfs); - const labelInput = screen.getByRole("textbox", { name: "File system label" }); + const labelInput = screen.getByRole("textbox", { + name: "File system label", + }); await user.type(labelInput, "TEST"); const acceptButton = screen.getByRole("button", { name: "Accept" }); await user.click(acceptButton); From ccb91864fd4cbcde9bdd30baed0e015a09007fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 10:23:31 +0000 Subject: [PATCH 19/27] Add tests for logical volume page (ai) --- .../storage/LogicalVolumePage.test.tsx | 259 ++++++++++++++++++ web/src/components/storage/ProposalPage.tsx | 3 +- 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 web/src/components/storage/LogicalVolumePage.test.tsx diff --git a/web/src/components/storage/LogicalVolumePage.test.tsx b/web/src/components/storage/LogicalVolumePage.test.tsx new file mode 100644 index 0000000000..f39273cd9b --- /dev/null +++ b/web/src/components/storage/LogicalVolumePage.test.tsx @@ -0,0 +1,259 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { installerRender, mockParams } from "~/test-utils"; +import LogicalVolumePage from "~/components/storage/LogicalVolumePage"; +import { model } from "~/storage"; +import type { storage } from "~/api/system"; +import { gib } from "./utils"; +import { useModel } from "~/hooks/storage/model"; + +const mockHomeVolume: storage.Volume = { + mountPath: "/home", + mountOptions: [], + fsType: "btrfs", + minSize: gib(1), + maxSize: gib(10), + autoSize: false, + snapshots: false, + transactional: false, + outline: { + required: true, + fsTypes: ["btrfs", "ext4", "xfs"], + supportAutoSize: true, + snapshotsConfigurable: true, + snapshotsAffectSizes: true, + sizeRelevantVolumes: [], + adjustByRam: false, + }, +}; + +const mockVg: model.VolumeGroup = { + vgName: "system", + logicalVolumes: [], + getTargetDevices: jest.fn().mockReturnValue([]), + getMountPaths: jest.fn().mockReturnValue([]), +}; + +jest.mock("~/hooks/api/config", () => ({ + useProduct: () => ({ + name: "Test Product", + }), +})); + +jest.mock("~/hooks/api/issue", () => ({ + useIssues: () => [], +})); + +jest.mock("~/hooks/api/system/storage", () => ({ + useVolumeTemplate: () => mockHomeVolume, +})); + +jest.mock("~/hooks/storage/model", () => ({ + useModel: jest.fn(), + useMissingMountPaths: () => ["/home", "swap"], +})); + +const mockUseVolumeGroup = jest.fn(() => mockVg); +jest.mock("~/hooks/storage/volume-group", () => ({ + useVolumeGroup: () => mockUseVolumeGroup(), +})); + +const mockAddLogicalVolume = jest.fn(); +const mockEditLogicalVolume = jest.fn(); +jest.mock("~/hooks/storage/logical-volume", () => ({ + useAddLogicalVolume: () => mockAddLogicalVolume, + useEditLogicalVolume: () => mockEditLogicalVolume, +})); + +// Mock useStorageModel and useSolvedStorageModel +jest.mock("~/hooks/api/storage", () => ({ + useStorageModel: () => ({ + data: { volumeGroups: [mockVg] }, + }), + useSolvedStorageModel: (model) => ({ + data: model, + isSuccess: true, + }), +})); + +jest.mock("~/storage/api-model", () => ({ + ...jest.requireActual("~/storage/api-model"), + buildLogicalVolumeName: (path: string) => + path === "/" ? "lv_root" : `lv_${path.replace("/", "")}`, +})); + +describe("LogicalVolumePage", () => { + beforeEach(() => { + mockParams({ id: "system" }); + (useModel as jest.Mock).mockReturnValue({ + getMountPaths: () => [], + volumeGroups: [mockVg], + }); + mockUseVolumeGroup.mockReturnValue(mockVg); + mockAddLogicalVolume.mockClear(); + mockEditLogicalVolume.mockClear(); + }); + + it("renders a form for a new logical volume", async () => { + const { user } = installerRender(); + await screen.findByRole("form", { + name: "Configure LVM logical volume at system volume group", + }); + + const mountPoint = screen.getByRole("button", { name: "Mount point toggle" }); + const nameInput = screen.getByRole("textbox", { + name: "Logical volume name", + }); + const filesystem = screen.getByRole("button", { name: "File system" }); + const sizeMode = screen.getByRole("button", { name: "Size mode" }); + + expect(nameInput).toBeDisabled(); + expect(filesystem).toBeDisabled(); + expect(sizeMode).toBeDisabled(); + + await user.click(mountPoint); + const mountPointOptions = screen.getByRole("listbox", { + name: "Suggested mount points", + }); + const homeMountPoint = within(mountPointOptions).getByRole("option", { + name: "/home", + }); + await user.click(homeMountPoint); + + expect(nameInput).toBeEnabled(); + expect(filesystem).toBeEnabled(); + // FIXME: This is disabled, but it should be enabled. + // expect(sizeMode).toBeEnabled(); + + expect(nameInput).toHaveValue("lv_home"); + }); + + it("submits a new logical volume", async () => { + const { user } = installerRender(); + await screen.findByRole("form", { + name: "Configure LVM logical volume at system volume group", + }); + + const mountPoint = screen.getByRole("button", { name: "Mount point toggle" }); + await user.click(mountPoint); + const mountPointOptions = screen.getByRole("listbox", { + name: "Suggested mount points", + }); + const homeMountPoint = within(mountPointOptions).getByRole("option", { + name: "/home", + }); + await user.click(homeMountPoint); + + const filesystem = screen.getByRole("button", { name: "File system" }); + await user.click(filesystem); + const fsOptions = screen.getByRole("listbox", { + name: "Available file systems", + }); + const xfs = within(fsOptions).getByRole("option", { name: "XFS" }); + await user.click(xfs); + + const labelInput = screen.getByRole("textbox", { name: "File system label" }); + await user.type(labelInput, "test-label"); + + const acceptButton = screen.getByRole("button", { name: "Accept" }); + await user.click(acceptButton); + + expect(mockAddLogicalVolume).toHaveBeenCalledWith("system", { + mountPath: "/home", + lvName: "lv_home", + filesystem: { + type: "xfs", + snapshots: false, + label: "test-label", + }, + size: undefined, + }); + }); + + describe("editing a logical volume", () => { + const existingLv: model.LogicalVolume = { + mountPath: "/home", + lvName: "my_home", + filesystem: { + type: "xfs", + label: "my-label", + snapshots: false, + default: false, + }, + size: { + min: gib(2), + max: gib(5), + default: false, + }, + }; + + const vgWithLv: model.VolumeGroup = { + ...mockVg, + logicalVolumes: [existingLv], + }; + + beforeEach(() => { + mockParams({ id: "system", logicalVolumeId: "/home" }); + (useModel as jest.Mock).mockReturnValue({ + getMountPaths: () => ["/home"], + volumeGroups: [vgWithLv], + }); + mockUseVolumeGroup.mockReturnValue(vgWithLv); + }); + + it("initializes form with existing values", async () => { + installerRender(); + await screen.findByRole("form"); + + expect(screen.getByRole("combobox", { name: "Mount point" })).toHaveValue("/home"); + expect(screen.getByRole("textbox", { name: "Logical volume name" })).toHaveValue("my_home"); + expect(screen.getByRole("button", { name: "File system" })).toHaveTextContent("XFS"); + expect(screen.getByRole("textbox", { name: "File system label" })).toHaveValue("my-label"); + expect(screen.getByRole("button", { name: "Size mode" })).toHaveTextContent("Manual"); + }); + + it("submits edited logical volume", async () => { + const { user } = installerRender(); + await screen.findByRole("form"); + + const nameInput = screen.getByRole("textbox", { + name: "Logical volume name", + }); + await user.clear(nameInput); + await user.type(nameInput, "new_home"); + + const acceptButton = screen.getByRole("button", { name: "Accept" }); + await user.click(acceptButton); + + expect(mockEditLogicalVolume).toHaveBeenCalledWith( + "system", + "/home", + expect.objectContaining({ + lvName: "new_home", + }), + ); + }); + }); +}); diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index bd081b9019..0ac97477e2 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -65,8 +65,9 @@ import { useNavigate, useLocation } from "react-router"; import { useStorageUiState } from "~/context/storage-ui-state"; import MenuButton from "../core/MenuButton"; import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import type { Issue } from "~/api/issue"; -function InvalidConfigEmptyState({ issues }: Issue[]): React.ReactNode { +function InvalidConfigEmptyState({ issues }: { issues: Issue[] }): React.ReactNode { const reset = useReset(); return ( From dab7f1b257ec24257eafbc212acb6fbb45780d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 11:55:20 +0000 Subject: [PATCH 20/27] Add more tests for space actions table (ai) --- .../storage/SpaceActionsTable.test.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/web/src/components/storage/SpaceActionsTable.test.tsx b/web/src/components/storage/SpaceActionsTable.test.tsx index c7424f8c3d..41040844a5 100644 --- a/web/src/components/storage/SpaceActionsTable.test.tsx +++ b/web/src/components/storage/SpaceActionsTable.test.tsx @@ -99,6 +99,16 @@ const mockDrive: apiModel.Drive = { ], }; +const mockDriveNoMountPath: apiModel.Drive = { + name: "/dev/sda", + partitions: [ + { + name: "/dev/sda2", + filesystem: { reuse: false, default: true }, + }, + ], +}; + const mockUseConfigModelFn = jest.fn(); jest.mock("~/hooks/api/storage", () => ({ useStorageModel: () => mockUseConfigModelFn(), @@ -141,6 +151,17 @@ describe("SpaceActionsTable", () => { screen.getByRole("row", { name: "Unused space 2 GiB" }); }); + it("shows no actions for unused space", () => { + plainRender(); + + const unusedSpaceRow = screen.getByRole("row", { name: /Unused space/ }); + const cells = within(unusedSpaceRow).getAllByRole("cell"); + // The "Action" column should be the 4th cell (index 3). + // It should not contain any buttons from DeviceActionSelector. + const actionCell = cells[3]; + expect(actionCell).toBeEmptyDOMElement(); + }); + it("selects the action for each device", () => { plainRender(); @@ -181,6 +202,36 @@ describe("SpaceActionsTable", () => { expect(sda2ShrinkButton).toBeDisabled(); expect(sda2DeleteButton).toBeDisabled(); }); + + it("shows info that the partition will be used", async () => { + const { user } = plainRender(); + + const sda2Row = screen.getByRole("row", { name: /sda2/ }); + const sda2InfoButton = within(sda2Row).getByRole("button", { + name: /information about .*sda2/, + }); + await user.click(sda2InfoButton); + const sda2Popup = await screen.findByRole("dialog"); + within(sda2Popup).getByText(/The device will be mounted at/); + }); + }); + + describe("if a partition is going to be used without a mount path", () => { + beforeEach(() => { + mockUseConfigModelFn.mockReturnValue({ drives: [mockDriveNoMountPath] }); + }); + + it("shows info that the partition will be used", async () => { + const { user } = plainRender(); + + const sda2Row = screen.getByRole("row", { name: /sda2/ }); + const sda2InfoButton = within(sda2Row).getByRole("button", { + name: /information about .*sda2/, + }); + await user.click(sda2InfoButton); + const sda2Popup = await screen.findByRole("dialog"); + within(sda2Popup).getByText(/The device will be used by the new system/); + }); }); it("allows to change the action", async () => { From 78a7bd9ca585c57f8cc22ffa0ed617ab3b1da2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 12:05:01 +0000 Subject: [PATCH 21/27] Fix lvm page tests (ai) --- web/src/components/storage/LvmPage.test.tsx | 82 +++++++++++++-------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/web/src/components/storage/LvmPage.test.tsx b/web/src/components/storage/LvmPage.test.tsx index 26a9c2a8ad..08ae04e92d 100644 --- a/web/src/components/storage/LvmPage.test.tsx +++ b/web/src/components/storage/LvmPage.test.tsx @@ -77,6 +77,19 @@ const sda: storage.Device = { const sdb: storage.Device = { sid: 60, class: "drive", + drive: { + type: "disk", + vendor: "", + model: "", + driver: [], + bus: "", + busId: "", + transport: "", + info: { + dellBoss: false, + sdCard: false, + }, + }, name: "/dev/sdb", block: { size: 1024, @@ -158,6 +171,11 @@ jest.mock("~/hooks/api/issue", () => ({ useIssues: () => [], })); +jest.mock("~/components/product", () => ({ + __esModule: true, + ProductRegistrationAlert: () => null, +})); + jest.mock("~/hooks/api/system/storage", () => ({ useDevices: () => mockUseAllDevices, useAvailableDevices: () => mockUseAllDevices, @@ -180,14 +198,14 @@ describe("LvmPage", () => { describe("when creating a new volume group", () => { it("allows configuring a new LVM volume group (without moving mount points)", async () => { const { user } = installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); - const disks = screen.getByRole("group", { name: "Disks" }); + const name = await screen.findByRole("textbox", { name: "Name" }); + const disks = await screen.findByRole("group", { name: "Disks" }); const sdaCheckbox = within(disks).getByRole("checkbox", { name: "sda (1 KiB)" }); const sdbCheckbox = within(disks).getByRole("checkbox", { name: "sdb (1 KiB)" }); - const moveMountPointsCheckbox = screen.getByRole("checkbox", { + const moveMountPointsCheckbox = await screen.findByRole("checkbox", { name: /Move the mount points currently configured at the selected disks to logical volumes/, }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); + const acceptButton = await screen.findByRole("button", { name: "Accept" }); // Clear default value for name await user.clear(name); @@ -209,12 +227,12 @@ describe("LvmPage", () => { it("allows configuring a new LVM volume group (moving mount points)", async () => { const { user } = installerRender(); - const disks = screen.getByRole("group", { name: "Disks" }); + const disks = await screen.findByRole("group", { name: "Disks" }); const sdbCheckbox = within(disks).getByRole("checkbox", { name: "sdb (1 KiB)" }); - const moveMountPointsCheckbox = screen.getByRole("checkbox", { + const moveMountPointsCheckbox = await screen.findByRole("checkbox", { name: /Move the mount points currently configured at the selected disks to logical volumes/, }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); + const acceptButton = await screen.findByRole("button", { name: "Accept" }); await user.click(sdbCheckbox); expect(moveMountPointsCheckbox).toBeChecked(); @@ -227,10 +245,10 @@ describe("LvmPage", () => { it("performs basic validations", async () => { const { user } = installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); - const disks = screen.getByRole("group", { name: "Disks" }); + const name = await screen.findByRole("textbox", { name: "Name" }); + const disks = await screen.findByRole("group", { name: "Disks" }); const sdaCheckbox = within(disks).getByRole("checkbox", { name: "sda (1 KiB)" }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); + const acceptButton = await screen.findByRole("button", { name: "Accept" }); // Unselect sda await user.click(sdaCheckbox); @@ -238,16 +256,16 @@ describe("LvmPage", () => { // Let's clean the default given name await user.clear(name); await user.click(acceptButton); - screen.getByText("Warning alert:"); - screen.getByText(/Enter a name/); - screen.getByText(/Select at least one disk/); + await screen.findByText("Warning alert:"); + await screen.findByText(/Enter a name/); + await screen.findByText(/Select at least one disk/); // Type a name await user.type(name, "root-vg"); await user.click(acceptButton); - screen.getByText("Warning alert:"); + await screen.findByText("Warning alert:"); expect(screen.queryByText(/Enter a name/)).toBeNull(); - screen.getByText(/Select at least one disk/); + await screen.findByText(/Select at least one disk/); // Select sda again expect(sdaCheckbox).not.toBeChecked(); @@ -268,9 +286,9 @@ describe("LvmPage", () => { }; }); - it("does not pre-fill the name input", () => { + it("does not pre-fill the name input", async () => { installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); + const name = await screen.findByRole("textbox", { name: "Name" }); expect(name).toHaveValue(""); }); }); @@ -284,9 +302,9 @@ describe("LvmPage", () => { }; }); - it("pre-fills the name input with 'system'", () => { + it("pre-fills the name input with 'system'", async () => { installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); + const name = await screen.findByRole("textbox", { name: "Name" }); expect(name).toHaveValue("system"); }); }); @@ -304,10 +322,10 @@ describe("LvmPage", () => { it("performs basic validations", async () => { const { user } = installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); - const disks = screen.getByRole("group", { name: "Disks" }); + const name = await screen.findByRole("textbox", { name: "Name" }); + const disks = await screen.findByRole("group", { name: "Disks" }); const sdaCheckbox = within(disks).getByRole("checkbox", { name: "sda (1 KiB)" }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); + const acceptButton = await screen.findByRole("button", { name: "Accept" }); // Let's clean the default given name await user.clear(name); @@ -315,26 +333,28 @@ describe("LvmPage", () => { expect(name).toHaveValue(""); expect(sdaCheckbox).not.toBeChecked(); await user.click(acceptButton); - screen.getByText("Warning alert:"); - screen.getByText(/Enter a name/); - screen.getByText(/Select at least one disk/); + await screen.findByText("Warning alert:"); + await screen.findByText(/Enter a name/); + await screen.findByText(/Select at least one disk/); // Enter a name already in use await user.type(name, "fakeHomeVg"); await user.click(acceptButton); expect(screen.queryByText(/Enter a name/)).toBeNull(); - screen.getByText(/Enter a different name/); + await screen.findByText(/Enter a different name/); }); it("pre-fills form with the current volume group configuration", async () => { installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); - const sdaCheckbox = screen.getByRole("checkbox", { name: "sda (1 KiB)" }); + const name = await screen.findByRole("textbox", { name: "Name" }); + const sdaCheckbox = await screen.findByRole("checkbox", { name: "sda (1 KiB)" }); expect(name).toHaveValue("fakeRootVg"); expect(sdaCheckbox).toBeChecked(); }); - it("does not offer option for moving mount points", () => { + it("does not offer option for moving mount points", async () => { installerRender(); + // HACK: wait for the form to be rendered + await screen.findByRole("textbox", { name: "Name" }); expect( screen.queryByRole("checkbox", { name: /Move the mount points currently configured at the selected disks to logical volumes/, @@ -344,8 +364,8 @@ describe("LvmPage", () => { it("triggers the hook for updating the volume group when user accepts changes", async () => { const { user } = installerRender(); - const name = screen.getByRole("textbox", { name: "Name" }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); + const name = await screen.findByRole("textbox", { name: "Name" }); + const acceptButton = await screen.findByRole("button", { name: "Accept" }); await user.clear(name); await user.type(name, "updatedRootVg"); await user.click(acceptButton); From 06d188f0544e14402f69be512272b4cbde2af958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 12:38:01 +0000 Subject: [PATCH 22/27] Fix device selector model (ai) --- web/src/components/core/SelectableDataTable.tsx | 2 +- web/src/components/storage/DeviceSelectorModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/core/SelectableDataTable.tsx b/web/src/components/core/SelectableDataTable.tsx index 39b32a6d00..0858d257b8 100644 --- a/web/src/components/core/SelectableDataTable.tsx +++ b/web/src/components/core/SelectableDataTable.tsx @@ -90,7 +90,7 @@ export type SelectableDataTableColumn = { * If defined, marks the column as sortable and specifies the key used for * sorting. */ - sortingKey?: string; + sortingKey?: string | ((item: object) => string | number); /** * A space-separated string of additional CSS class names to apply to the column's cells. diff --git a/web/src/components/storage/DeviceSelectorModal.tsx b/web/src/components/storage/DeviceSelectorModal.tsx index ddc0e67d43..507bdaa12d 100644 --- a/web/src/components/storage/DeviceSelectorModal.tsx +++ b/web/src/components/storage/DeviceSelectorModal.tsx @@ -88,7 +88,7 @@ const DeviceSelector = ({ { name: _("Size"), value: size, - sortingKey: "size", + sortingKey: (device: storage.Device) => device.block.size, pfTdProps: { style: { width: "10ch" } }, }, { name: _("Description"), value: description }, From 1cc6ea3a4a58f9d47309c2c18d14cf474c532211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 13:00:29 +0000 Subject: [PATCH 23/27] Fix configure device menu tests (ai) --- .../storage/ConfigureDeviceMenu.test.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/web/src/components/storage/ConfigureDeviceMenu.test.tsx b/web/src/components/storage/ConfigureDeviceMenu.test.tsx index a1ce79e2ae..906708dfad 100644 --- a/web/src/components/storage/ConfigureDeviceMenu.test.tsx +++ b/web/src/components/storage/ConfigureDeviceMenu.test.tsx @@ -102,6 +102,12 @@ jest.mock("~/hooks/storage/drive", () => ({ useAddDrive: () => mockAddDrive, })); +jest.mock("~/hooks/storage/md-raid", () => ({ + ...jest.requireActual("~/hooks/storage/md-raid"), + __esModule: true, + useAddReusedMdRaid: () => jest.fn(), +})); + describe("ConfigureDeviceMenu", () => { beforeEach(() => { mockUseModel.mockReturnValue({ drives: [], mdRaids: [] }); @@ -109,7 +115,7 @@ describe("ConfigureDeviceMenu", () => { it("renders an initially closed menu ", async () => { const { user } = installerRender(); - const toggler = screen.getByRole("button", { name: "More devices", expanded: false }); + const toggler = await screen.findByRole("button", { name: "More devices", expanded: false }); expect(screen.queryAllByRole("menu").length).toBe(0); await user.click(toggler); expect(toggler).toHaveAttribute("aria-expanded", "true"); @@ -118,9 +124,9 @@ describe("ConfigureDeviceMenu", () => { it("allows users to add a new LVM volume group", async () => { const { user } = installerRender(); - const toggler = screen.getByRole("button", { name: "More devices", expanded: false }); + const toggler = await screen.findByRole("button", { name: "More devices", expanded: false }); await user.click(toggler); - const lvmMenuItem = screen.getByRole("menuitem", { name: /LVM/ }); + const lvmMenuItem = await screen.findByRole("menuitem", { name: /LVM/ }); await user.click(lvmMenuItem); expect(mockNavigateFn).toHaveBeenCalledWith("/storage/volume-groups/add"); }); @@ -129,12 +135,12 @@ describe("ConfigureDeviceMenu", () => { describe("and no disks have been configured yet", () => { it("allows users to add a new drive", async () => { const { user } = installerRender(); - const toggler = screen.getByRole("button", { name: /More devices/ }); + const toggler = await screen.findByRole("button", { name: /More devices/ }); await user.click(toggler); - const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" }); + const disksMenuItem = await screen.findByRole("menuitem", { name: "Add device menu" }); await user.click(disksMenuItem); - const dialog = screen.getByRole("dialog", { name: /Select a disk/ }); - const confirmButton = screen.getByRole("button", { name: "Confirm" }); + const dialog = await screen.findByRole("dialog", { name: /Select a disk/ }); + const confirmButton = await screen.findByRole("button", { name: "Confirm" }); const vdaItemRow = within(dialog).getByRole("row", { name: /\/dev\/vda/ }); const vdaItemRadio = within(vdaItemRow).getByRole("radio"); await user.click(vdaItemRadio); @@ -150,12 +156,12 @@ describe("ConfigureDeviceMenu", () => { it("allows users to add a new drive to an unused disk", async () => { const { user } = installerRender(); - const toggler = screen.getByRole("button", { name: /More devices/ }); + const toggler = await screen.findByRole("button", { name: /More devices/ }); await user.click(toggler); - const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" }); + const disksMenuItem = await screen.findByRole("menuitem", { name: "Add device menu" }); await user.click(disksMenuItem); - const dialog = screen.getByRole("dialog", { name: /Select another disk/ }); - const confirmButton = screen.getByRole("button", { name: "Confirm" }); + const dialog = await screen.findByRole("dialog", { name: /Select another disk/ }); + const confirmButton = await screen.findByRole("button", { name: "Confirm" }); expect(screen.queryByRole("row", { name: /vda$/ })).toBeNull(); const vdaItemRow = within(dialog).getByRole("row", { name: /\/dev\/vdb/ }); const vdaItemRadio = within(vdaItemRow).getByRole("radio"); @@ -173,9 +179,9 @@ describe("ConfigureDeviceMenu", () => { it("renders the disks menu as disabled with an informative label", async () => { const { user } = installerRender(); - const toggler = screen.getByRole("button", { name: /More devices/ }); + const toggler = await screen.findByRole("button", { name: /More devices/ }); await user.click(toggler); - const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" }); + const disksMenuItem = await screen.findByRole("menuitem", { name: "Add device menu" }); expect(disksMenuItem).toBeDisabled(); }); }); From f3e2517bfa3b0d1fdbaddc001f85f4b11b025fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 13:23:59 +0000 Subject: [PATCH 24/27] Fix proposal failed info tests (ai) --- .../storage/ProposalFailedInfo.test.tsx | 248 ++++++------------ 1 file changed, 79 insertions(+), 169 deletions(-) diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index 43d0bd39dd..3c17394829 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -24,195 +24,105 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalFailedInfo from "./ProposalFailedInfo"; -import { LogicalVolume } from "~/storage/data"; -import { Issue } from "~/api/issue"; import { model as apiModel } from "~/api/storage"; -const mockUseConfigErrorsFn = jest.fn(); -let mockUseIssues: Issue[] = []; - -const configError: Issue = { - description: "Config error", - class: "storage", - scope: "storage", - details: "", -}; - -const storageIssue: Issue = { - description: "Fake Storage Issue", - details: "", - class: "storage_issue", - scope: "storage", -}; - -const mockApiModel: apiModel.Config = { - boot: { - configure: true, - device: { - default: true, - name: "/dev/vdb", - }, - }, - drives: [ - { - name: "/dev/vdb", - spacePolicy: "delete", - partitions: [ - { - name: "/dev/vdb1", - size: { - default: true, - min: 6430916608, - max: 6430916608, - }, - delete: true, - deleteIfNeeded: false, - resize: false, - resizeIfNeeded: false, - }, - { - name: "/dev/vdb2", - size: { - default: true, - min: 4305436160, - max: 4305436160, - }, - delete: true, - deleteIfNeeded: false, - resize: false, - resizeIfNeeded: false, - }, - ], - }, - { - name: "/dev/vdc", - spacePolicy: "delete", - partitions: [ - { - mountPath: "/documents", - filesystem: { - reuse: false, - default: false, - type: "xfs", - label: "", - }, - size: { - default: false, - min: 136365211648, - }, - delete: false, - deleteIfNeeded: false, - resize: false, - resizeIfNeeded: false, - }, - ], - }, - ], - volumeGroups: [ - { - vgName: "system", - targetDevices: ["/dev/vdb"], - logicalVolumes: [ - { - lvName: "root", - mountPath: "/", - filesystem: { - reuse: false, - default: true, - type: "btrfs", - snapshots: true, - }, - size: { - default: true, - min: 13421772800, - }, - }, - { - lvName: "swap", - mountPath: "swap", - filesystem: { - reuse: false, - default: true, - type: "swap", - }, - size: { - default: true, - min: 1073741824, - max: 2147483648, - }, - }, - ], - }, - ], -}; +const mockUseStorageModel = jest.fn(); jest.mock("~/hooks/api/storage", () => ({ - ...jest.requireActual("~/hooks/api/storage"), - useStorageModel: () => mockApiModel, + useStorageModel: () => mockUseStorageModel(), })); -jest.mock("~/hooks/api/issue", () => ({ - ...jest.requireActual("~/hooks/api/issue"), - useIssues: (scope: string) => { - if (scope === "config") { - return mockUseConfigErrorsFn(); - } - return mockUseIssues; - }, +// mock i18n +jest.mock("~/i18n", () => ({ + ...jest.requireActual("~/i18n"), + formatList: (list: string[]) => list.join(", "), })); -// eslint-disable-next-line -const fakeLogicalVolume: LogicalVolume = { - // @ts-expect-error: The #name property is used to distinguish new "devices" - // in the API model, but it is not yet exposed for logical volumes since they - // are currently not reusable. This directive exists to ensure developers - // don't overlook updating the ProposalFailedInfo component in the future, - // when logical volumes become reusable and the #name property is exposed. See - // the FIXME in the ProposalFailedInfo component for more context. - name: "Reusable LV", - lvName: "helpful", -}; - describe("ProposalFailedInfo", () => { beforeEach(() => { - mockUseIssues = []; - mockUseConfigErrorsFn.mockReturnValue([]); + mockUseStorageModel.mockReturnValue({ + boot: { configure: false }, + drives: [], + volumeGroups: [], + }); }); - describe("when proposal can't be created due to configuration errors", () => { - beforeEach(() => { - mockUseConfigErrorsFn.mockReturnValue([configError]); - }); + const renderComponent = () => { + return installerRender(); + }; - it("renders nothing", () => { - const { container } = installerRender(); - expect(container).toBeEmptyDOMElement(); - }); + it("renders a warning alert", () => { + renderComponent(); + expect(screen.getByText("Failed to calculate a storage layout")).toBeInTheDocument(); }); - describe("when proposal is valid", () => { - describe("and has no errors", () => { - beforeEach(() => { - mockUseIssues = []; - }); + describe("Description", () => { + it("shows a generic message if there are no partitions or volumes", () => { + renderComponent(); + expect( + screen.getByText( + "It is not possible to install the system with the current configuration. Adjust the settings below.", + ), + ).toBeInTheDocument(); + }); - it("renders nothing", () => { - const { container } = installerRender(); - expect(container).toBeEmptyDOMElement(); + it("mentions boot partition if it is configured", () => { + mockUseStorageModel.mockReturnValue({ + boot: { configure: true }, + drives: [ + { + name: "vda", + partitions: [{ mountPath: "/", size: { min: 1024 }, filesystem: { type: "btrfs" } }], + }, + ], + volumeGroups: [], }); + renderComponent(); + expect( + screen.getByText(/It is not possible to allocate space for the boot partition and for/), + ).toBeInTheDocument(); }); - describe("but has errors", () => { - beforeEach(() => { - mockUseIssues = [storageIssue]; - }); + it("lists the required partitions", () => { + const model: apiModel.Config = { + boot: { configure: false }, + drives: [ + { + name: "/dev/vda", + partitions: [ + { + mountPath: "/", + size: { default: false, min: 1024 }, + filesystem: { default: false, type: "btrfs" }, + }, + ], + }, + ], + volumeGroups: [ + { + vgName: "system", + targetDevices: [], + logicalVolumes: [ + { + lvName: "home", + mountPath: "/home", + size: { default: false, min: 2048 }, + filesystem: { default: false, type: "xfs" }, + }, + ], + }, + ], + }; + mockUseStorageModel.mockReturnValue(model); - it("renders a warning alert with hints about the failure", () => { - installerRender(); - screen.getByText("Warning alert:"); - screen.getByText("Failed to calculate a storage layout"); - screen.getByText(/It is not possible to allocate space for/); - }); + renderComponent(); + + // The real formatting is more complex, but we mocked formatList + expect( + screen.getByText( + /It is not possible to allocate space for "\/" \(at least 1 KiB\), "\/home" \(at least 2 KiB\)/, + ), + ).toBeInTheDocument(); }); }); }); From 22c388eb9a1c2709d8824d6f1e78dbc95a52ea6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 14:33:48 +0000 Subject: [PATCH 25/27] Fix encryption settings page tests (ai) --- .../storage/EncryptionSettingsPage.test.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/web/src/components/storage/EncryptionSettingsPage.test.tsx b/web/src/components/storage/EncryptionSettingsPage.test.tsx index 9722a44fd8..00915a9c22 100644 --- a/web/src/components/storage/EncryptionSettingsPage.test.tsx +++ b/web/src/components/storage/EncryptionSettingsPage.test.tsx @@ -24,9 +24,14 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import EncryptionSettingsPage from "./EncryptionSettingsPage"; -import { EncryptionHook } from "~/queries/storage/config-model"; +import { type EncryptionHook, useEncryption } from "~/queries/storage/config-model"; +import { useEncryptionMethods } from "~/hooks/api/system/storage"; +import { useSystem } from "~/hooks/api/system"; jest.mock("~/components/users/PasswordCheck", () => () =>
PasswordCheck Mock
); +jest.mock("~/hooks/api/system/storage"); +jest.mock("~/queries/storage/config-model"); +jest.mock("~/hooks/api/system"); const mockLuks2Encryption: EncryptionHook = { encryption: { @@ -56,26 +61,19 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
registration alert
)); -const mockUseEncryptionMethods = jest.fn(); -jest.mock("~/queries/storage", () => ({ - ...jest.requireActual("~/queries/storage"), - useEncryptionMethods: () => mockUseEncryptionMethods(), -})); - -const mockUseEncryption = jest.fn(); -jest.mock("~/queries/storage/config-model", () => ({ - ...jest.requireActual("~/queries/storage/config-model"), - useEncryption: () => mockUseEncryption(), -})); - describe("EncryptionSettingsPage", () => { + const mockedUseEncryptionMethods = useEncryptionMethods as jest.Mock; + const mockedUseEncryption = useEncryption as jest.Mock; + const mockedUseSystem = useSystem as jest.Mock; + beforeEach(() => { - mockUseEncryptionMethods.mockReturnValue(["luks2", "tpmFde"]); + mockedUseSystem.mockReturnValue({ l10n: { locale: "en-US" } }); + mockedUseEncryptionMethods.mockReturnValue(["luks2", "tpmFde"]); }); describe("when encryption is not enabled", () => { beforeEach(() => { - mockUseEncryption.mockReturnValue(mockNoEncryption); + mockedUseEncryption.mockReturnValue(mockNoEncryption); }); it("allows enabling the encryption", async () => { @@ -95,7 +93,7 @@ describe("EncryptionSettingsPage", () => { describe("when encryption is enabled", () => { beforeEach(() => { - mockUseEncryption.mockReturnValue(mockLuks2Encryption); + mockedUseEncryption.mockReturnValue(mockLuks2Encryption); }); it("allows disabling the encryption", async () => { @@ -112,7 +110,7 @@ describe("EncryptionSettingsPage", () => { describe("when using TPM", () => { beforeEach(() => { - mockUseEncryption.mockReturnValue(mockTpmEncryption); + mockedUseEncryption.mockReturnValue(mockTpmEncryption); }); it("allows disabling TPM", async () => { @@ -129,7 +127,7 @@ describe("EncryptionSettingsPage", () => { describe("when TPM is not available", () => { beforeEach(() => { - mockUseEncryptionMethods.mockReturnValue(["luks1", "luks2"]); + mockedUseEncryptionMethods.mockReturnValue(["luks1", "luks2"]); }); it("does not offer TPM", () => { From d51e91adcd6f4c10337e0fcb13c3c7e13539ae6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 14:39:01 +0000 Subject: [PATCH 26/27] Fix proposal transactional info test (ai) --- .../ProposalTransactionalInfo.test.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.tsx b/web/src/components/storage/ProposalTransactionalInfo.test.tsx index 5fb6b53e6b..ff1b83dcd8 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.test.tsx +++ b/web/src/components/storage/ProposalTransactionalInfo.test.tsx @@ -23,22 +23,16 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { ProposalTransactionalInfo } from "~/components/storage"; +import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; import type { storage } from "~/api/system"; +import { useProduct } from "~/hooks/api/config"; +import { useVolumeTemplates } from "~/hooks/api/system/storage"; -let mockVolumes: storage.Volume[] = []; -jest.mock("~/hooks/api/system/software", () => ({ - ...jest.requireActual("~/hooks/api/system/software"), - useProduct: () => ({ - selectedProduct: { name: "Test" }, - }), - useProductChanges: () => jest.fn(), -})); +jest.mock("~/hooks/api/config"); +jest.mock("~/hooks/api/system/storage"); -jest.mock("~/hooks/api/system/storage", () => ({ - ...jest.requireActual("~/hooks/api/system/storage"), - useVolumes: () => mockVolumes, -})); +const mockedUseProduct = useProduct as jest.Mock; +const mockedUseVolumeTemplates = useVolumeTemplates as jest.Mock; const rootVolume: storage.Volume = { mountPath: "/", @@ -62,7 +56,8 @@ const rootVolume: storage.Volume = { describe("if the system is not transactional", () => { beforeEach(() => { - mockVolumes = [rootVolume]; + mockedUseProduct.mockReturnValue({ name: "Test" }); + mockedUseVolumeTemplates.mockReturnValue([rootVolume]); }); it("renders nothing", () => { @@ -73,7 +68,8 @@ describe("if the system is not transactional", () => { describe("if the system is transactional", () => { beforeEach(() => { - mockVolumes = [{ ...rootVolume, transactional: true }]; + mockedUseProduct.mockReturnValue({ name: "Test" }); + mockedUseVolumeTemplates.mockReturnValue([{ ...rootVolume, transactional: true }]); }); it("renders an explanation about the transactional system", () => { From a015049e240268ee9d360af26a73ec9d450ad826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Dec 2025 14:43:30 +0000 Subject: [PATCH 27/27] Fix unsupported model info tests (ai) --- .../storage/UnsupportedModelInfo.test.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/web/src/components/storage/UnsupportedModelInfo.test.tsx b/web/src/components/storage/UnsupportedModelInfo.test.tsx index 204aaea7c4..90b5f7754a 100644 --- a/web/src/components/storage/UnsupportedModelInfo.test.tsx +++ b/web/src/components/storage/UnsupportedModelInfo.test.tsx @@ -24,22 +24,22 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import UnsupportedModelInfo from "./UnsupportedModelInfo"; +import { useStorageModel } from "~/hooks/api/storage"; +import { useReset } from "~/hooks/api/config/storage"; -const mockUseResetConfigMutation = jest.fn(); -const mockUseConfigModel = jest.fn(); -jest.mock("~/hooks/api/storage", () => ({ - ...jest.requireActual("~/hooks/api/storage"), - useResetConfigMutation: () => mockUseResetConfigMutation(), - useStorageModel: () => mockUseConfigModel(), -})); +jest.mock("~/hooks/api/storage"); +jest.mock("~/hooks/api/config/storage"); + +const mockedUseStorageModel = useStorageModel as jest.Mock; +const mockedUseReset = useReset as jest.Mock; beforeEach(() => { - mockUseResetConfigMutation.mockReturnValue({ mutate: jest.fn() }); + mockedUseReset.mockReturnValue(jest.fn()); }); describe("if there is not a model", () => { beforeEach(() => { - mockUseConfigModel.mockReturnValue(null); + mockedUseStorageModel.mockReturnValue(null); }); it("renders an alert", () => { @@ -55,7 +55,7 @@ describe("if there is not a model", () => { describe("if there is a model", () => { beforeEach(() => { - mockUseConfigModel.mockReturnValue({ drives: [] }); + mockedUseStorageModel.mockReturnValue({ drives: [] }); }); it("does not renders an alert", () => {