Files
paseo/packages/app/src/components/attachment-lightbox.test.tsx
T
Mohamed BoudraandElsa Granger e0f545fed4 fix(app): use the iPhone Duo top band beside the status region (#8901)
On the iPhone Duo inner screen in portrait, the workspace header, the left
sidebar's first row and the explorer tab rail now use the top band beside
the status region. The status region is published as the window's top-right
obstruction, the same slot desktop window controls use, so the row owning
that corner pads past it. Content gains 56pt.

Refs #7924

Co-authored-by: Elsa Granger <zeyugao@outlook.com>
2026-11-09 22:03:21 +07:00

251 lines
7.6 KiB
TypeScript

import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { JSDOM } from "jsdom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AttachmentMetadata } from "@/attachments/types";
import { AttachmentLightbox, type ImageLightboxSource } from "./attachment-lightbox";
const { theme, imageMetadata, useAttachmentPreviewUrlMock } = vi.hoisted(() => {
const hoistedTheme = {
spacing: { 1: 4, 2: 8, 3: 12, 4: 16 },
iconSize: { sm: 14, md: 18, lg: 22 },
borderWidth: { 1: 1 },
opacity: { 50: 0.5 },
borderRadius: { full: 999, md: 6, lg: 8 },
fontSize: { xs: 11, sm: 13, base: 15 },
fontWeight: { normal: "400" },
colors: {
surface1: "#111",
surface2: "#222",
foreground: "#fff",
foregroundMuted: "#aaa",
border: "#555",
borderAccent: "#444",
},
};
const hoistedImageMetadata: AttachmentMetadata = {
id: "img-1",
mimeType: "image/png",
storageType: "web-indexeddb",
storageKey: "img-1",
fileName: "img-1.png",
byteSize: 42,
createdAt: 1,
};
return {
theme: hoistedTheme,
imageMetadata: hoistedImageMetadata,
useAttachmentPreviewUrlMock: vi.fn<(metadata: AttachmentMetadata | null) => string | null>(
() => "blob:preview",
),
};
});
const attachmentSource: ImageLightboxSource = { type: "attachment", metadata: imageMetadata };
const assistantImageSource: ImageLightboxSource = {
type: "uri",
uri: "blob:assistant-image",
};
vi.mock("react-native-unistyles", () => ({
StyleSheet: {
create: (factory: unknown) => (typeof factory === "function" ? factory(theme) : factory),
},
useUnistyles: () => ({ theme }),
}));
vi.mock("@/constants/platform", () => ({
isWeb: true,
isNative: false,
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) =>
({
"message.attachments.closeImage": "Close image",
"message.attachments.dismissImage": "Dismiss image",
"message.attachments.imageLoadFailed": "Couldn't load image",
})[key] ?? key,
}),
}));
vi.mock("react-native-safe-area-context", () => ({
SafeAreaProvider: ({ children }: { children?: React.ReactNode }) => children,
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
}));
vi.mock("react-native-gesture-handler", () => ({
GestureHandlerRootView: ({ children }: { children?: React.ReactNode }) =>
React.createElement("div", { "data-testid": "gesture-handler-root" }, children),
}));
vi.mock("lucide-react-native", () => {
const createIcon = (name: string) => (props: Record<string, unknown>) =>
React.createElement("span", { ...props, "data-icon": name });
return {
X: createIcon("X"),
};
});
vi.mock("@/components/zoomable-viewport/image", () => ({
ZoomableImage: (props: Record<string, unknown>) => {
const actions = props.actions as Array<{
label: string;
onPress: () => void;
testID?: string;
}>;
return React.createElement(
"div",
{
"data-testid": `${String(props.testID)}-image`,
"data-source": props.uri,
role: "img",
},
actions.map((action) =>
React.createElement(
"button",
{
"aria-label": action.label,
"data-testid": action.testID,
key: action.label,
onClick: action.onPress,
type: "button",
},
action.label,
),
),
);
},
}));
vi.mock("react-native", async () => {
const actual = await vi.importActual<Record<string, unknown>>("react-native");
const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) =>
visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null;
return { ...actual, Modal };
});
vi.mock("@/attachments/use-attachment-preview-url", () => ({
useAttachmentPreviewUrl: (metadata: AttachmentMetadata | null) =>
useAttachmentPreviewUrlMock(metadata),
}));
let root: Root | null = null;
let container: HTMLElement | null = null;
beforeEach(() => {
const dom = new JSDOM("<!doctype html><html><body></body></html>");
dom.window.requestAnimationFrame = vi.fn(() => 1);
dom.window.cancelAnimationFrame = vi.fn();
vi.stubGlobal("React", React);
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("window", dom.window);
vi.stubGlobal("document", dom.window.document);
vi.stubGlobal("HTMLElement", dom.window.HTMLElement);
vi.stubGlobal("Node", dom.window.Node);
vi.stubGlobal("navigator", dom.window.navigator);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
useAttachmentPreviewUrlMock.mockReturnValue("blob:preview");
});
afterEach(() => {
if (root) {
act(() => {
root?.unmount();
});
}
root = null;
container = null;
vi.unstubAllGlobals();
});
function render(element: React.ReactElement) {
act(() => {
root?.render(element);
});
}
function click(element: Element) {
act(() => {
element.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
});
}
function queryByTestId(testID: string): HTMLElement | null {
return document.querySelector(`[data-testid="${testID}"]`);
}
describe("AttachmentLightbox", () => {
it("renders nothing when the source is null", () => {
render(<AttachmentLightbox source={null} onClose={vi.fn()} />);
expect(queryByTestId("attachment-lightbox-backdrop")).toBeNull();
expect(queryByTestId("attachment-lightbox-image")).toBeNull();
});
it("renders the image when metadata is provided", () => {
render(<AttachmentLightbox source={attachmentSource} onClose={vi.fn()} />);
const image = queryByTestId("attachment-lightbox-image");
expect(image).not.toBeNull();
expect(image?.getAttribute("data-source")).toBe("blob:preview");
});
it("renders a direct timeline image without resolving attachment storage", () => {
render(<AttachmentLightbox source={assistantImageSource} onClose={vi.fn()} />);
const image = queryByTestId("attachment-lightbox-image");
expect(image?.getAttribute("data-source")).toBe("blob:assistant-image");
expect(useAttachmentPreviewUrlMock).toHaveBeenLastCalledWith(null);
});
it("calls onClose when the backdrop is pressed", () => {
const onClose = vi.fn();
render(<AttachmentLightbox source={attachmentSource} onClose={onClose} />);
const backdrop = queryByTestId("attachment-lightbox-backdrop");
expect(backdrop).not.toBeNull();
click(backdrop!);
expect(onClose).toHaveBeenCalledTimes(1);
});
it("calls onClose when the close button is pressed", () => {
const onClose = vi.fn();
render(<AttachmentLightbox source={attachmentSource} onClose={onClose} />);
const closeButton = document.querySelector(
'[aria-label="Close image"][data-testid="attachment-lightbox-close"]',
);
expect(closeButton).not.toBeNull();
click(closeButton!);
expect(onClose).toHaveBeenCalledTimes(1);
});
it("shows error text when the preview URL resolves to null", () => {
useAttachmentPreviewUrlMock.mockReturnValue(null);
render(<AttachmentLightbox source={attachmentSource} onClose={vi.fn()} />);
expect(queryByTestId("attachment-lightbox-image")).toBeNull();
expect(document.body.textContent ?? "").toContain("Couldn't load image");
});
it("closes on Escape key on web", () => {
const onClose = vi.fn();
render(<AttachmentLightbox source={attachmentSource} onClose={onClose} />);
act(() => {
window.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Escape" }));
});
expect(onClose).toHaveBeenCalledTimes(1);
});
});