Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 252 additions & 74 deletions web/src/components/core/Page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,33 @@
*/

import React from "react";
import { screen, within } from "@testing-library/react";
import { installerRender, mockNavigateFn, mockRoutes, plainRender } from "~/test-utils";
import { screen, waitFor, within } from "@testing-library/react";
import {
installerRender,
mockNavigateFn,
mockProgresses,
mockRoutes,
plainRender,
} from "~/test-utils";
import useTrackQueriesRefetch from "~/hooks/use-track-queries-refetch";
import { COMMON_PROPOSAL_KEYS } from "~/hooks/model/proposal";
import { PRODUCT, ROOT } from "~/routes/paths";
import { _ } from "~/i18n";
import Page from "./Page";

let consoleErrorSpy: jest.SpyInstance;
let mockStartTracking: jest.Mock = jest.fn();

jest.mock("~/hooks/use-track-queries-refetch", () => ({
__esModule: true,
default: jest.fn(),
}));

jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
<div>ProductRegistrationAlertMock</div>
));

const mockUseStatus = jest.fn();
jest.mock("~/hooks/model/status", () => ({
useStatus: () => mockUseStatus(),
}));

const mockOnProposalUpdated = jest.fn();
jest.mock("~/hooks/model/proposal", () => ({
onProposalUpdated: (callback: (detail: any) => void) => mockOnProposalUpdated(callback),
}));
const mockUseTrackQueriesRefetch = jest.mocked(useTrackQueriesRefetch);

describe("Page", () => {
beforeAll(() => {
Expand All @@ -54,12 +60,11 @@ describe("Page", () => {
});

beforeEach(() => {
mockUseStatus.mockReturnValue({
progresses: [],
// Set up default mock for useTrackQueriesRefetch
mockUseTrackQueriesRefetch.mockReturnValue({
startTracking: mockStartTracking,
});

mockOnProposalUpdated.mockReturnValue(() => {});


mockNavigateFn.mockClear();
});

Expand Down Expand Up @@ -191,6 +196,7 @@ describe("Page", () => {
expect(onClick).toHaveBeenCalled();
});
});

describe("Page.Header", () => {
it("renders a node that sticks to top", () => {
const { container } = plainRender(<Page.Header>The Header</Page.Header>);
Expand Down Expand Up @@ -266,93 +272,265 @@ describe("Page", () => {

describe("when progress scope is provided but no matching progress exists", () => {
it("does not render the backdrop", () => {
mockUseStatus.mockReturnValue({
progresses: [],
});

installerRender(<Page progressScope="software">Content</Page>);
expect(screen.queryByRole("alert")).toBeNull();
});
});

describe("when progress scope matches an active progress", () => {
it("renders the backdrop with progress information", () => {
mockUseStatus.mockReturnValue({
progresses: [
{
scope: "software",
step: "Installing packages",
index: 2,
size: 5,
},
],
});

mockProgresses([
{
scope: "software",
step: "Installing packages",
steps: [],
index: 2,
size: 5,
},
]);
installerRender(<Page progressScope="software">Content</Page>);

const backdrop = screen.getByRole("alert", { name: /Installing packages/ });
expect(backdrop.classList).toContain("agm-main-content-overlay");
within(backdrop).getByText(/step 2 of 5/);
});
});

describe("when progress finishes", () => {
it.todo("shows 'Refreshing data...' message temporarily");
it.todo("hides backdrop after proposal update event");
let mockStartTracking: jest.Mock;
let mockCallback: (startedAt: number, completedAt: number) => void;

beforeEach(() => {
mockStartTracking = jest.fn();
mockUseTrackQueriesRefetch.mockImplementation((keys, callback) => {
mockCallback = callback;
return { startTracking: mockStartTracking };
});
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("shows 'Refreshing data...' message temporarily", async () => {
// Start with active progress
mockProgresses([
{
scope: "storage",
step: "Calculating proposal",
steps: ["Calculating proposal"],
index: 1,
size: 1,
},
]);

const { rerender } = installerRender(<Page progressScope="storage">Content</Page>);

const backdrop = screen.getByRole("alert", { name: /Calculating proposal/ });

// Progress finishes
mockProgresses([]);

rerender(<Page progressScope="storage">Content</Page>);

// Should show "Refreshing data..." message
await waitFor(() => {
within(backdrop).getByText(/Refreshing data/);
});

// Should start tracking queries
expect(mockStartTracking).toHaveBeenCalled();
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("hides backdrop after queries are refetched", async () => {
// Start with active progress
mockProgresses([
{
scope: "storage",
step: "Calculating proposal",
steps: ["Calculating proposal"],
index: 1,
size: 1,
},
]);

const { rerender } = installerRender(<Page progressScope="storage">Content</Page>);

// Progress finishes
mockProgresses([]);

const backdrop = screen.getByRole("alert", { name: /Calculating proposal/ });

rerender(<Page progressScope="storage">Content</Page>);

// Should show refreshing message
await waitFor(() => {
within(backdrop).getByText(/Refreshing data/);
});

// Simulate queries completing by calling the callback
const startedAt = Date.now();
mockCallback(startedAt, startedAt + 100);

// Backdrop should be hidden
await waitFor(() => {
expect(screen.queryByRole("alert")).toBeNull();
});
});
});

describe("when progress scope does not match", () => {
it("does not show backdrop for different scope", () => {
mockUseStatus.mockReturnValue({
progresses: [
{
scope: "software",
step: "Installing packages",
index: 2,
size: 5,
},
],
});

mockProgresses([
{
scope: "software",
step: "Installing packages",
steps: [],
index: 2,
size: 5,
},
]);
installerRender(<Page progressScope="storage">Content</Page>);

expect(screen.queryByRole("alert", { name: /Installing pckages/ })).toBeNull();
expect(screen.queryByRole("alert", { name: /Installing packages/ })).toBeNull();
});
});

describe("multiple progress updates", () => {
it("updates the backdrop message when progress changes", () => {
mockUseStatus.mockReturnValue({
progresses: [
{
scope: "software",
step: "Downloading packages",
index: 1,
size: 5,
},
],
});

mockProgresses([
{
scope: "software",
step: "Downloading packages",
steps: [],
index: 1,
size: 5,
},
]);
const { rerender } = installerRender(<Page progressScope="software">Content</Page>);
const backdrop = screen.getByRole("alert", { name: /Downloading packages/ });
within(backdrop).getByText(/step 1 of 5/);

mockProgresses([
{
scope: "software",
step: "Installing packages",
steps: [],
index: 3,
size: 5,
},
]);
rerender(<Page progressScope="software">Content</Page>);
within(backdrop).getByText(/Installing packages/);
within(backdrop).getByText(/step 3 of 5/);
});
});

expect(screen.getByText(/Downloading packages/)).toBeInTheDocument();
expect(screen.getByText(/step 1 of 5/)).toBeInTheDocument();

mockUseStatus.mockReturnValue({
progresses: [
{
scope: "software",
step: "Installing packages",
index: 3,
size: 5,
},
],
});
describe("additionalProgressKeys prop", () => {
it("tracks common proposal keys by default", () => {
mockProgresses([
{
scope: "software",
step: "Installing packages",
steps: [],
index: 1,
size: 3,
},
]);

rerender(<Page progressScope="software">Content</Page>);
installerRender(<Page progressScope="software">Content</Page>);

expect(screen.getByText(/Installing packages/)).toBeInTheDocument();
expect(screen.getByText(/step 3 of 5/)).toBeInTheDocument();
// Should be called with COMMON_PROPOSAL_KEYS and undefined additionalKeys
expect(mockUseTrackQueriesRefetch).toHaveBeenCalledWith(
expect.arrayContaining(COMMON_PROPOSAL_KEYS),
expect.any(Function),
);
});

it("tracks additional query key along with common ones", () => {
mockProgresses([
{
scope: "storage",
step: "Calculating proposal",
steps: [],
index: 1,
size: 1,
},
]);

installerRender(
<Page progressScope="storage" additionalProgressKeys="storageModel">
Content
</Page>,
);

// Should be called with COMMON_PROPOSAL_KEYS + storageModel
expect(mockUseTrackQueriesRefetch).toHaveBeenCalledWith(
expect.arrayContaining([...COMMON_PROPOSAL_KEYS, "storageModel"]),
expect.any(Function),
);
});

it("tracks multiple additional query keys along with common ones", () => {
mockProgresses([
{
scope: "network",
step: "Configuring network",
steps: [],
index: 1,
size: 2,
},
]);

installerRender(
<Page progressScope="network" additionalProgressKeys={["networkConfig", "connections"]}>
Content
</Page>,
);

// Should be called with COMMON_PROPOSAL_KEYS + networkConfig + connections
expect(mockUseTrackQueriesRefetch).toHaveBeenCalledWith(
expect.arrayContaining([...COMMON_PROPOSAL_KEYS, "networkConfig", "connections"]),
expect.any(Function),
);
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("starts tracking when progress finishes", async () => {
// Start with active progress
mockProgresses([
{
scope: "storage",
step: "Calculating proposal",
steps: ["Calculating proposal"],
index: 1,
size: 1,
},
]);

const { rerender } = installerRender(
<Page progressScope="storage" additionalProgressKeys="storageModel">
Content
</Page>,
);

// Progress finishes
mockProgresses([]);

rerender(
<Page progressScope="storage" additionalProgressKeys="storageModel">
Content
</Page>,
);
rerender(
<Page progressScope="storage" additionalProgressKeys="storageModel">
Content
</Page>,
);

// Should have called startTracking
await waitFor(() => {
expect(mockStartTracking).toHaveBeenCalled();
});
});
});
});
Expand Down
Loading
Loading