Files
SnapOtter/tests/unit/web/tool-card-pin.test.tsx
T
SnapOtterandGitHub a731c3d1fe fix: reliable, self-healing AI feature-bundle installs (#472)
Make on-demand AI feature-bundle installs reliable and self-healing, closing
the failure modes behind most "some tool doesn't work" reports.

Multi-bundle installs: tools needing more than one bundle (Passport Photo,
Enhance Faces) install every required bundle from one action and stay
not-installed until all are present. Verified across all 19 AI tools.

Downloads: self-heal the accelerated Hugging Face (Xet) client so an upgraded
venv no longer silently falls back to slow urllib; restart instead of
corrupting a resumed partial when a proxy ignores Range and returns 200;
verify the completed size; fail fast on disk-full and HTTP 4xx; retry
transient errors five times; add hf_transfer fallback and document Xet egress.

Install integrity: crash-atomic venv writes so a killed or out-of-space
install can no longer tear the shared venv and break other tools; a boot
breadcrumb reseeds a torn venv to a clean state automatically; a post-install
smoke import test refuses to record a bundle whose libraries cannot load; an
install watchdog stops a wedged installer that would otherwise hold the venv
writer lock forever.

Adds unit and end-to-end tests for every failure mode above.
2026-07-10 07:32:48 +00:00

113 lines
3.6 KiB
TypeScript

// @vitest-environment jsdom
import type { FeatureBundleState } from "@snapotter/shared";
import { TOOLS } from "@snapotter/shared";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ToolCard } from "@/components/common/tool-card";
import { useFeaturesStore } from "@/stores/features-store";
import { usePinnedToolsStore } from "@/stores/pinned-tools-store";
// Make the store's optimistic persistence a no-op so the test stays server-free.
vi.mock("@/lib/api", () => ({
apiGet: vi.fn(() => Promise.resolve({ preferences: {} })),
apiPut: vi.fn(() => Promise.resolve({ ok: true })),
}));
const resize = TOOLS.find((tool) => tool.id === "resize");
if (!resize) throw new Error("resize tool missing from TOOLS");
const passportPhoto = TOOLS.find((tool) => tool.id === "passport-photo");
if (!passportPhoto) throw new Error("passport-photo tool missing from TOOLS");
function makeBundleState(overrides: Partial<FeatureBundleState> = {}): FeatureBundleState {
return {
id: "background-removal",
name: "Background Removal",
description: "Remove backgrounds",
status: "not_installed",
installedVersion: null,
estimatedSize: "4-5 GB",
enablesTools: ["remove-background", "passport-photo"],
progress: null,
error: null,
...overrides,
};
}
afterEach(cleanup);
beforeEach(() => {
usePinnedToolsStore.setState({
pinnedTools: [],
lastConfirmed: [],
loaded: true,
loadError: false,
});
useFeaturesStore.setState({
bundles: [],
loaded: true,
loadError: false,
installing: {},
errors: {},
queued: [],
installAllActive: false,
startTimes: {},
});
});
function renderCard(showPin: boolean) {
return render(
<MemoryRouter>
<ToolCard tool={resize} variant="descriptive" showPin={showPin} />
</MemoryRouter>,
);
}
describe("ToolCard pin button", () => {
it("renders no pin button unless showPin is set", () => {
renderCard(false);
expect(screen.queryByTestId("pin-toggle-resize")).toBeNull();
});
it("toggles pinned state and aria label when clicked", () => {
renderCard(true);
const btn = screen.getByTestId("pin-toggle-resize");
expect(btn.getAttribute("aria-label")).toBe("Pin");
expect(btn.getAttribute("aria-pressed")).toBe("false");
fireEvent.click(btn);
expect(usePinnedToolsStore.getState().pinnedTools).toEqual(["resize"]);
const pinnedBtn = screen.getByTestId("pin-toggle-resize");
expect(pinnedBtn.getAttribute("aria-label")).toBe("Unpin");
expect(pinnedBtn.getAttribute("aria-pressed")).toBe("true");
fireEvent.click(pinnedBtn);
expect(usePinnedToolsStore.getState().pinnedTools).toEqual([]);
});
});
describe("ToolCard AI bundle status", () => {
it("treats multi-bundle tools as not installed when an extra bundle is missing", () => {
useFeaturesStore.setState({
bundles: [
makeBundleState({ id: "background-removal", status: "installed" }),
makeBundleState({
id: "face-detection",
name: "Face Detection",
status: "not_installed",
enablesTools: ["blur-faces", "red-eye-removal", "smart-crop"],
}),
],
});
const { container } = render(
<MemoryRouter>
<ToolCard tool={passportPhoto} variant="descriptive" />
</MemoryRouter>,
);
// One SVG is the tool icon, the second is the missing-AI-bundle indicator.
expect(container.querySelectorAll("svg")).toHaveLength(2);
});
});