Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/deploy-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
HUSKY: "0"
VITE_RUN_ENVIRONMENT: dev
RunEnvironment: dev
VITE_BUILD_HASH: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Upload Build files
uses: actions/upload-artifact@v7
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "infra-core",
"version": "4.9.6",
"version": "4.9.7",
"private": true,
"type": "module",
"workspaces": [
Expand Down
26 changes: 24 additions & 2 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,43 @@ import "@mantine/core/styles.css";
import "@mantine/dropzone/styles.css";
import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css";
import { MantineProvider } from "@mantine/core";
import { Button, Group, MantineProvider, Text } from "@mantine/core";
import { cssVariablesResolver, theme } from "./theme";
import { useColorScheme, useLocalStorage } from "@mantine/hooks";
import { Notifications } from "@mantine/notifications";
import { Notifications, notifications } from "@mantine/notifications";
import { useEffect } from "react";

import ColorSchemeContext from "./ColorSchemeContext";
import { Router } from "./Router";
import { UserResolverProvider } from "./components/NameOptionalCard";
import { forceRefresh, startVersionPolling } from "./versionCheck";

export default function App() {
const preferredColorScheme = useColorScheme();
const [colorScheme, setColorScheme] = useLocalStorage({
key: "acm-manage-color-scheme",
defaultValue: preferredColorScheme,
});

useEffect(() => {
startVersionPolling(() => {
notifications.show({
id: "version-update",
title: "Update available",
message: (
<Group gap="xs" mt={4}>
<Text size="sm">A new version of the app is available.</Text>
<Button size="xs" variant="light" onClick={forceRefresh}>
Refresh to update
</Button>
</Group>
),
color: "blue",
autoClose: false,
});
});
}, []);
Comment on lines +23 to +40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Return a cleanup from the polling effect.

startVersionPolling() registers long-lived state, but this effect never tears it down. In React Strict Mode or on remount, that can leave duplicate timers/listeners and duplicate update notifications.

Have the helper return a stop function and return it from this effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/App.tsx` around lines 23 - 40, The effect calls startVersionPolling
which sets up long-lived polling/listeners but never cleans them up; change
startVersionPolling to return a stop/cleanup function (e.g., stopVersionPolling)
and update the useEffect in App.tsx to capture that return value and return it
as the cleanup (return () => stop()). Keep existing callback that calls
notifications.show and preserve forceRefresh usage when wiring the cleanup.


return (
<ColorSchemeContext.Provider
value={{ colorScheme, onChange: setColorScheme }}
Expand Down
268 changes: 268 additions & 0 deletions src/ui/versionCheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const CURRENT = "abc123";
const NEWER = "xyz789";

// Drain the async microtask queue (fetch -> json -> callback chain needs a few ticks)
const flushPromises = async () => {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
};

function makeResponse(
version: string | null,
opts: { ok?: boolean; contentType?: string } = {},
) {
const { ok = true, contentType = "application/json" } = opts;
return Promise.resolve({
ok,
headers: { get: () => contentType },
json: () => Promise.resolve(version !== null ? { version } : {}),

Check warning on line 21 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3w&open=AZ3gwzV0NLg40HRg8O3w&pullRequest=691
} as unknown as Response);
}

describe("versionCheck", () => {
let startVersionPolling: (onUpdate?: () => void) => void;
let forceRefresh: () => void;
let reloadSpy: ReturnType<typeof vi.fn>;
const registeredListeners: Array<
[string, EventListenerOrEventListenerObject]
> = [];

beforeEach(async () => {
vi.resetModules();
vi.useFakeTimers();
vi.stubEnv("VITE_BUILD_HASH", CURRENT);
vi.spyOn(console, "warn").mockImplementation(() => {});

// Track visibilitychange listeners so we can remove them between tests
const origAdd = document.addEventListener.bind(document);
vi.spyOn(document, "addEventListener").mockImplementation(
(event, handler, ...args) => {
if (event === "visibilitychange") {
registeredListeners.push([event, handler]);
}
return origAdd(event, handler, ...(args as []));
},
);

reloadSpy = vi.fn();
Object.defineProperty(window, "location", {

Check warning on line 51 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3x&open=AZ3gwzV0NLg40HRg8O3x&pullRequest=691
configurable: true,
value: { reload: reloadSpy },
});
Object.defineProperty(window, "caches", {

Check warning on line 55 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3y&open=AZ3gwzV0NLg40HRg8O3y&pullRequest=691
configurable: true,
value: { keys: vi.fn().mockResolvedValue([]), delete: vi.fn() },
});

const mod = await import("./versionCheck");
startVersionPolling = mod.startVersionPolling;
forceRefresh = mod.forceRefresh;
});

afterEach(() => {
// Remove any listeners registered during the test to prevent cross-test bleed
registeredListeners.forEach(([event, handler]) =>
document.removeEventListener(event, handler),
);
registeredListeners.length = 0;
vi.useRealTimers();
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

describe("startVersionPolling", () => {
it("runs an immediate check on startup", async () => {
global.fetch = vi.fn().mockReturnValue(makeResponse(CURRENT));

Check warning on line 78 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3z&open=AZ3gwzV0NLg40HRg8O3z&pullRequest=691
startVersionPolling();
await flushPromises();
expect(global.fetch).toHaveBeenCalledOnce();

Check warning on line 81 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O30&open=AZ3gwzV0NLg40HRg8O30&pullRequest=691
});

it("rechecks every 5 minutes after the initial check", async () => {
global.fetch = vi.fn().mockReturnValue(makeResponse(CURRENT));

Check warning on line 85 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O31&open=AZ3gwzV0NLg40HRg8O31&pullRequest=691
startVersionPolling();
await flushPromises();
expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 88 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O32&open=AZ3gwzV0NLg40HRg8O32&pullRequest=691

await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect(global.fetch).toHaveBeenCalledTimes(2);

Check warning on line 91 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O33&open=AZ3gwzV0NLg40HRg8O33&pullRequest=691

await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect(global.fetch).toHaveBeenCalledTimes(3);

Check warning on line 94 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O34&open=AZ3gwzV0NLg40HRg8O34&pullRequest=691
});

it("registers a visibilitychange listener", () => {
global.fetch = vi.fn().mockReturnValue(new Promise(() => {}));

Check warning on line 98 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O35&open=AZ3gwzV0NLg40HRg8O35&pullRequest=691
const addSpy = vi.spyOn(document, "addEventListener");
startVersionPolling();
expect(addSpy).toHaveBeenCalledWith(
"visibilitychange",
expect.any(Function),
);
});
});

describe("checkForUpdate", () => {
it("does nothing when version matches current", async () => {
const onUpdate = vi.fn();
global.fetch = vi.fn().mockReturnValue(makeResponse(CURRENT));

Check warning on line 111 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O36&open=AZ3gwzV0NLg40HRg8O36&pullRequest=691
startVersionPolling(onUpdate);
await flushPromises();
expect(onUpdate).not.toHaveBeenCalled();
expect(reloadSpy).not.toHaveBeenCalled();
});

it("calls onUpdate when a newer version is detected", async () => {
const onUpdate = vi.fn();
global.fetch = vi.fn().mockReturnValue(makeResponse(NEWER));

Check warning on line 120 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O37&open=AZ3gwzV0NLg40HRg8O37&pullRequest=691
startVersionPolling(onUpdate);
await flushPromises();
expect(onUpdate).toHaveBeenCalledOnce();
expect(reloadSpy).not.toHaveBeenCalled();
});

it("calls forceRefresh when version differs and no callback is set", async () => {
global.fetch = vi.fn().mockReturnValue(makeResponse(NEWER));

Check warning on line 128 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O38&open=AZ3gwzV0NLg40HRg8O38&pullRequest=691
startVersionPolling();
await flushPromises();
expect(reloadSpy).toHaveBeenCalledOnce();
});

it("does nothing when CURRENT_VERSION is undefined (unbuilt dev asset)", async () => {
vi.unstubAllEnvs();
vi.resetModules();
const mod = await import("./versionCheck");
const onUpdate = vi.fn();
global.fetch = vi.fn().mockReturnValue(makeResponse(NEWER));

Check warning on line 139 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O39&open=AZ3gwzV0NLg40HRg8O39&pullRequest=691
mod.startVersionPolling(onUpdate);
await flushPromises();
expect(onUpdate).not.toHaveBeenCalled();
expect(reloadSpy).not.toHaveBeenCalled();
});

it("stops polling after an update is detected", async () => {
const onUpdate = vi.fn();
global.fetch = vi.fn().mockReturnValue(makeResponse(NEWER));

Check warning on line 148 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3-&open=AZ3gwzV0NLg40HRg8O3-&pullRequest=691
startVersionPolling(onUpdate);
await flushPromises();
expect(onUpdate).toHaveBeenCalledTimes(1);

await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect(onUpdate).toHaveBeenCalledTimes(1);
});

it("stops polling when response is not JSON", async () => {
global.fetch = vi

Check warning on line 158 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O3_&open=AZ3gwzV0NLg40HRg8O3_&pullRequest=691
.fn()
.mockReturnValue(makeResponse(null, { contentType: "text/html" }));
startVersionPolling();
await flushPromises();
expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 163 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4A&open=AZ3gwzV0NLg40HRg8O4A&pullRequest=691

await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 166 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4B&open=AZ3gwzV0NLg40HRg8O4B&pullRequest=691
});

it("does nothing when response is not ok", async () => {
const onUpdate = vi.fn();
global.fetch = vi

Check warning on line 171 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4C&open=AZ3gwzV0NLg40HRg8O4C&pullRequest=691
.fn()
.mockReturnValue(makeResponse(NEWER, { ok: false }));
startVersionPolling(onUpdate);
await flushPromises();
expect(onUpdate).not.toHaveBeenCalled();
expect(reloadSpy).not.toHaveBeenCalled();
});

it("keeps polling after fewer than 10 fetch failures", async () => {
global.fetch = vi.fn().mockRejectedValue(new Error("network error"));

Check warning on line 181 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4D&open=AZ3gwzV0NLg40HRg8O4D&pullRequest=691
startVersionPolling();

for (let i = 0; i < 5; i++) {
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
}

// 1 immediate + 5 interval = 6 total calls, polling still active
expect(global.fetch).toHaveBeenCalledTimes(6);

Check warning on line 189 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4E&open=AZ3gwzV0NLg40HRg8O4E&pullRequest=691
});

it("stops polling after 10 consecutive failures", async () => {
global.fetch = vi.fn().mockRejectedValue(new Error("network error"));

Check warning on line 193 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4F&open=AZ3gwzV0NLg40HRg8O4F&pullRequest=691
startVersionPolling();

// 1 immediate + 10 interval ticks exhausts the counter
for (let i = 0; i < 11; i++) {
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
}

const callCount = (global.fetch as ReturnType<typeof vi.fn>).mock.calls

Check warning on line 201 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4G&open=AZ3gwzV0NLg40HRg8O4G&pullRequest=691

Check warning on line 201 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4H&open=AZ3gwzV0NLg40HRg8O4H&pullRequest=691
.length;
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
expect((global.fetch as ReturnType<typeof vi.fn>).mock.calls.length).toBe(

Check warning on line 204 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4J&open=AZ3gwzV0NLg40HRg8O4J&pullRequest=691

Check warning on line 204 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4I&open=AZ3gwzV0NLg40HRg8O4I&pullRequest=691
callCount,
);
});
});

describe("visibilitychange listener", () => {
it("triggers a check when the tab becomes visible", async () => {
global.fetch = vi.fn().mockReturnValue(makeResponse(CURRENT));

Check warning on line 212 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4K&open=AZ3gwzV0NLg40HRg8O4K&pullRequest=691
startVersionPolling();
await flushPromises();
expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 215 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4L&open=AZ3gwzV0NLg40HRg8O4L&pullRequest=691

Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "visible",
});
document.dispatchEvent(new Event("visibilitychange"));
await flushPromises();

expect(global.fetch).toHaveBeenCalledTimes(2);

Check warning on line 224 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4M&open=AZ3gwzV0NLg40HRg8O4M&pullRequest=691
});

it("does not trigger a check when the tab becomes hidden", async () => {
global.fetch = vi.fn().mockReturnValue(makeResponse(CURRENT));

Check warning on line 228 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4N&open=AZ3gwzV0NLg40HRg8O4N&pullRequest=691
startVersionPolling();
await flushPromises();
expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 231 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4O&open=AZ3gwzV0NLg40HRg8O4O&pullRequest=691

Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});
document.dispatchEvent(new Event("visibilitychange"));
await flushPromises();

expect(global.fetch).toHaveBeenCalledTimes(1);

Check warning on line 240 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4P&open=AZ3gwzV0NLg40HRg8O4P&pullRequest=691
});
});

describe("forceRefresh", () => {
it("reloads the page", () => {
forceRefresh();
expect(reloadSpy).toHaveBeenCalledOnce();
});

it("deletes all caches before reloading", async () => {
const deleteSpy = vi.fn();
Object.defineProperty(window, "caches", {

Check warning on line 252 in src/ui/versionCheck.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=acm-uiuc_core&issues=AZ3gwzV0NLg40HRg8O4Q&open=AZ3gwzV0NLg40HRg8O4Q&pullRequest=691
configurable: true,
value: {
keys: vi.fn().mockResolvedValue(["v1", "v2"]),
delete: deleteSpy,
},
});

forceRefresh();
await flushPromises();

expect(deleteSpy).toHaveBeenCalledWith("v1");
expect(deleteSpy).toHaveBeenCalledWith("v2");
expect(reloadSpy).toHaveBeenCalledOnce();
});
});
});
Loading
Loading