|
|
|
@@ -1,28 +1,6 @@
|
|
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
/**
|
|
|
|
|
* Tests for ConsoleModal — EC2 serial console output viewer.
|
|
|
|
|
*
|
|
|
|
|
* Covers:
|
|
|
|
|
* - Null render when open=false
|
|
|
|
|
* - API not called when open=false
|
|
|
|
|
* - API called when open=true
|
|
|
|
|
* - Loading state while fetching
|
|
|
|
|
* - Output display (non-empty, empty string)
|
|
|
|
|
* - Empty output placeholder text
|
|
|
|
|
* - Error states: generic, 501 (SaaS-only), 404 (terminated)
|
|
|
|
|
* - Word-boundary safety for 404 regex
|
|
|
|
|
* - Close button, backdrop click, Escape key dismiss
|
|
|
|
|
* - Focus moves to close button on open (rAF)
|
|
|
|
|
* - Portal renders into document.body
|
|
|
|
|
* - workspaceName displayed in title bar
|
|
|
|
|
* - aria-modal, aria-labelledby, aria-label attributes
|
|
|
|
|
* - Copy button presence based on output availability
|
|
|
|
|
* - In-flight fetch cleanup when open changes to false
|
|
|
|
|
* - Re-fetch when workspaceId changes
|
|
|
|
|
*/
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
|
import { render, screen, waitFor, cleanup, fireEvent, act } from "@testing-library/react";
|
|
|
|
|
import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
|
|
|
api: { get: vi.fn() },
|
|
|
|
@@ -33,28 +11,10 @@ import { ConsoleModal } from "../ConsoleModal";
|
|
|
|
|
|
|
|
|
|
const mockGet = vi.mocked(api.get);
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
|
|
|
// Default: never resolves so tests that don't care about API can render without
|
|
|
|
|
// "Cannot read .then of undefined" errors. Override per-test with mockResolvedValueOnce.
|
|
|
|
|
mockGet.mockImplementation(() => new Promise(() => {}));
|
|
|
|
|
});
|
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
|
afterEach(cleanup);
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
cleanup();
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function flush() {
|
|
|
|
|
await act(async () => { await Promise.resolve(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Render conditions ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — render conditions", () => {
|
|
|
|
|
describe("ConsoleModal", () => {
|
|
|
|
|
it("returns null when closed — no fetch triggered", () => {
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<ConsoleModal workspaceId="ws-1" open={false} onClose={() => {}} />,
|
|
|
|
@@ -63,238 +23,75 @@ describe("ConsoleModal — render conditions", () => {
|
|
|
|
|
expect(mockGet).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders the dialog after mount", () => {
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
expect(screen.queryByRole("dialog")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders a portal attached to document.body", () => {
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
const dialog = document.body.querySelector('[role="dialog"]');
|
|
|
|
|
expect(dialog).toBeTruthy();
|
|
|
|
|
// The portal is a container div inside document.body; the dialog is nested inside it.
|
|
|
|
|
expect(document.body.contains(dialog!)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows workspaceName in the title bar", () => {
|
|
|
|
|
render(
|
|
|
|
|
<ConsoleModal
|
|
|
|
|
workspaceId="ws-1"
|
|
|
|
|
workspaceName="my-server"
|
|
|
|
|
open={true}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
expect(screen.getByText("my-server")).toBeTruthy();
|
|
|
|
|
expect(screen.getByText("EC2 console output")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Loading + output ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — loading + output", () => {
|
|
|
|
|
it("shows loading indicator while fetching", () => {
|
|
|
|
|
mockGet.mockImplementation(() => new Promise(() => {}));
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
expect(screen.getByTestId("console-loading")).toBeTruthy();
|
|
|
|
|
expect(screen.getByText("Loading console output…")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("fetches console output when opened", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({
|
|
|
|
|
output: "boot line 1\nRuntime running (PID 42)\n",
|
|
|
|
|
instance_id: "i-x",
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "boot line 1\nRuntime running (PID 42)\n", instance_id: "i-x" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-1/console"),
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const out = screen.getByTestId("console-output");
|
|
|
|
|
expect(out.textContent).toContain("Runtime running (PID 42)");
|
|
|
|
|
});
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-1/console");
|
|
|
|
|
expect(screen.getByTestId("console-output")?.textContent).toContain(
|
|
|
|
|
"Runtime running (PID 42)",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows empty-output placeholder when output is empty string", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.getByTestId("console-output")?.textContent).toBe(
|
|
|
|
|
"(console output is empty — the instance may still be booting)",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Copy button is present when output exists", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "some log output" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.getByRole("button", { name: "Copy" })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Copy button is absent when output is empty", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Copy" })).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Error states ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — error states", () => {
|
|
|
|
|
it("renders a friendly message on 501 (non-CP deploy)", async () => {
|
|
|
|
|
mockGet.mockRejectedValueOnce(
|
|
|
|
|
new Error("GET /workspaces/ws-1/console: 501 Not Implemented"),
|
|
|
|
|
);
|
|
|
|
|
mockGet.mockRejectedValueOnce(new Error("GET /workspaces/ws-1/console: 501 Not Implemented"));
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
const err = screen.getByTestId("console-error");
|
|
|
|
|
expect(err.textContent).toMatch(/only available on cloud/i);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const err = screen.getByTestId("console-error");
|
|
|
|
|
expect(err.textContent).toMatch(/only available on cloud/i);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders a specific message on 404 (instance terminated)", async () => {
|
|
|
|
|
mockGet.mockRejectedValueOnce(
|
|
|
|
|
new Error("GET /workspaces/ws-1/console: 404 Not Found"),
|
|
|
|
|
);
|
|
|
|
|
mockGet.mockRejectedValueOnce(new Error("GET /workspaces/ws-1/console: 404 Not Found"));
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
const err = screen.getByTestId("console-error");
|
|
|
|
|
expect(err.textContent).toMatch(/No EC2 instance found/i);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const err = screen.getByTestId("console-error");
|
|
|
|
|
expect(err.textContent).toMatch(/No EC2 instance found/i);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders generic error message on non-501/404 failure", async () => {
|
|
|
|
|
mockGet.mockRejectedValueOnce(new Error("connection refused"));
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.getByTestId("console-error")?.textContent).toBe(
|
|
|
|
|
"connection refused",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("404 regex is word-boundary safe (1504 in URL does not false-match)", async () => {
|
|
|
|
|
// 1504 contains "50" and "04" but not the exact word "404"
|
|
|
|
|
mockGet.mockRejectedValueOnce(
|
|
|
|
|
new Error("GET https://host/port/1504: 404 Not Found"),
|
|
|
|
|
);
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
// Should still show the 404 message, not a partial match
|
|
|
|
|
expect(screen.getByTestId("console-error")?.textContent).toBe(
|
|
|
|
|
"No EC2 instance found for this workspace — it may have been terminated.",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Dismiss ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — dismiss", () => {
|
|
|
|
|
it("Close button invokes onClose", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "log" });
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={onClose} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
await waitFor(() => screen.getByText("Close"));
|
|
|
|
|
fireEvent.click(screen.getByText("Close"));
|
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(onClose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("Escape key invokes onClose", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "log" });
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={onClose} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
await waitFor(() => screen.getByText("Close"));
|
|
|
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("backdrop click closes the modal", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "log" });
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={onClose} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
|
|
|
const backdrop = document.querySelector('[aria-label="Close terminal"]');
|
|
|
|
|
expect(backdrop).toBeTruthy();
|
|
|
|
|
// fireEvent.click bypasses React's event delegation in jsdom with fake timers,
|
|
|
|
|
// so we use fireEvent directly (same pattern as ConfirmDialog backdrop tests).
|
|
|
|
|
fireEvent.click(backdrop!);
|
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Fetch lifecycle ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — fetch lifecycle", () => {
|
|
|
|
|
it("closes dialog immediately when open changes to false mid-fetch", async () => {
|
|
|
|
|
mockGet.mockImplementation(() => new Promise(() => {}));
|
|
|
|
|
const { rerender } = render(
|
|
|
|
|
<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />,
|
|
|
|
|
);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
// Simulate parent flipping open → false while fetch is in flight.
|
|
|
|
|
// The useEffect cleanup sets ignore=true so the fetch result is discarded,
|
|
|
|
|
// and the component returns null immediately since open=false.
|
|
|
|
|
rerender(<ConsoleModal workspaceId="ws-1" open={false} onClose={() => {}} />);
|
|
|
|
|
await flush();
|
|
|
|
|
// Dialog should be gone immediately (no need to wait for fetch)
|
|
|
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("re-fetches when workspaceId changes", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "log1" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-1/console");
|
|
|
|
|
|
|
|
|
|
mockGet.mockClear().mockResolvedValueOnce({ output: "log2" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-2" open={true} onClose={() => {}} />);
|
|
|
|
|
await flush();
|
|
|
|
|
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-2/console");
|
|
|
|
|
expect(onClose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── WCAG 2.1 dialog accessibility ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// ─── WCAG 2.1 dialog accessibility ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe("ConsoleModal — WCAG 2.1 dialog accessibility", () => {
|
|
|
|
|
it("renders role=dialog when open", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.queryByRole("dialog")).toBeTruthy();
|
|
|
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeTruthy());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("dialog has aria-modal='true' (WCAG 2.1 SC 1.3.2)", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
expect(screen.getByRole("dialog").getAttribute("aria-modal")).toBe("true");
|
|
|
|
|
const dialog = await waitFor(() => screen.getByRole("dialog"));
|
|
|
|
|
expect(dialog.getAttribute("aria-modal")).toBe("true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("dialog has aria-labelledby pointing to the title", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
const dialog = screen.getByRole("dialog");
|
|
|
|
|
const dialog = await waitFor(() => screen.getByRole("dialog"));
|
|
|
|
|
const labelledBy = dialog.getAttribute("aria-labelledby");
|
|
|
|
|
expect(labelledBy).toBeTruthy();
|
|
|
|
|
const titleEl = document.getElementById(labelledBy!);
|
|
|
|
@@ -304,21 +101,15 @@ describe("ConsoleModal — WCAG 2.1 dialog accessibility", () => {
|
|
|
|
|
it("backdrop div has aria-label for screen readers (WCAG 2.4.6)", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
const backdrop = document.querySelector('[aria-label="Close terminal"]');
|
|
|
|
|
expect(backdrop).toBeTruthy();
|
|
|
|
|
expect(backdrop?.className).toContain("bg-black");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("error div has role=alert (WCAG 4.1.3)", async () => {
|
|
|
|
|
mockGet.mockRejectedValueOnce(
|
|
|
|
|
new Error("GET /workspaces/ws-1/console: 404 Not Found"),
|
|
|
|
|
);
|
|
|
|
|
mockGet.mockRejectedValueOnce(new Error("GET /workspaces/ws-1/console: 404 Not Found"));
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
const alert = screen.getByRole("alert");
|
|
|
|
|
const alert = await waitFor(() => screen.getByRole("alert"));
|
|
|
|
|
expect(alert).toBeTruthy();
|
|
|
|
|
expect(alert.textContent).toMatch(/No EC2 instance found/i);
|
|
|
|
|
});
|
|
|
|
@@ -326,24 +117,8 @@ describe("ConsoleModal — WCAG 2.1 dialog accessibility", () => {
|
|
|
|
|
it("Close button has accessible name via aria-label", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
act(() => { vi.advanceTimersByTime(1); });
|
|
|
|
|
await flush();
|
|
|
|
|
// Two close buttons: X icon (aria-label="Close") and text "Close" button
|
|
|
|
|
const closeBtns = screen.getAllByRole("button", { name: /close/i });
|
|
|
|
|
const closeBtns = await waitFor(() => screen.getAllByRole("button", { name: /close/i }));
|
|
|
|
|
expect(closeBtns.length).toBeGreaterThanOrEqual(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("focus moves to close button on open (via requestAnimationFrame)", async () => {
|
|
|
|
|
mockGet.mockResolvedValueOnce({ output: "log" });
|
|
|
|
|
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
|
|
|
|
// Simulate requestAnimationFrame completing
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await new Promise((r) => requestAnimationFrame(() => r()));
|
|
|
|
|
});
|
|
|
|
|
await flush();
|
|
|
|
|
// Use aria-label to target the ✕ button specifically (footer has no aria-label)
|
|
|
|
|
const closeBtn = document.querySelector('[aria-label="Close"]') as HTMLButtonElement;
|
|
|
|
|
expect(closeBtn).toBeTruthy();
|
|
|
|
|
expect(document.activeElement).toBe(closeBtn);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|