// @vitest-environment jsdom
import * as React from "react"
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react"
import { I18nProvider } from "@workspace/i18n"
import {
createOverlayScope,
globalOverlayScope,
OverlayScopeProvider,
useOverlayScope,
type OverlayScope,
} from "@workspace/overlays"
import { AlertHost, openAlert } from "@workspace/overlays/alert"
import { OverlayHost } from "@workspace/overlays/all"
import { openConfirm } from "@workspace/overlays/confirm"
import { openDrawer } from "@workspace/overlays/drawer"
import { openModal } from "@workspace/overlays/modal"
import {
OverlayNavigationGuard,
type OverlayNavigationAdapter,
type OverlayNavigationAttempt,
} from "@workspace/overlays/navigation"
import { openPrompt } from "@workspace/overlays/prompt"
import { openSheet } from "@workspace/overlays/sheet"
import { openToast } from "@workspace/overlays/toast"
import { Button } from "@workspace/ui/components/button"
import { afterEach, describe, expect, it, vi } from "vitest"
import { messages as englishMessages } from "./locales/en-US"
class ResizeObserverStub {
disconnect() {}
observe() {}
unobserve() {}
}
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
Object.defineProperty(Element.prototype, "getAnimations", {
configurable: true,
value: () => [],
})
afterEach(cleanup)
function renderHosts(scope = createOverlayScope()) {
const result = render(
)
return { scope, ...result }
}
describe("atomic overlays", () => {
it("uses the global scope when scope is omitted", async () => {
render(
)
let result: Promise | undefined
act(() => {
result = openAlert({ title: "Global alert" })
})
expect(globalOverlayScope.getTopmostLayer()).toBeTruthy()
fireEvent.click(screen.getByRole("button", { name: "OK" }))
await expect(result).resolves.toBeUndefined()
})
it("resolves alert, confirm, and prompt results", async () => {
const { scope } = renderHosts()
let alertResult: Promise | undefined
act(() => {
alertResult = openAlert({ scope, title: "Export complete" })
})
fireEvent.click(screen.getByRole("button", { name: "OK" }))
await expect(alertResult).resolves.toBeUndefined()
let confirmation: Promise | undefined
act(() => {
confirmation = openConfirm({ scope, title: "Publish changes?" })
})
fireEvent.click(screen.getByRole("button", { name: "Confirm" }))
await expect(confirmation).resolves.toBe(true)
await waitFor(() =>
expect(screen.queryByText("Publish changes?")).toBeNull()
)
let promptResult: Promise | undefined
act(() => {
promptResult = openPrompt({ scope, title: "Document name" })
})
fireEvent.change(screen.getByLabelText("Value"), {
target: { value: "Release notes" },
})
fireEvent.click(screen.getByRole("button", { name: "Confirm" }))
await expect(promptResult).resolves.toBe("Release notes")
})
it("returns typed custom content results", async () => {
const { scope } = renderHosts()
let modalResult: Promise | undefined
act(() => {
modalResult = openModal({
content: ({ close }) => (
),
scope,
title: "Choose a value",
})
})
fireEvent.click(screen.getByRole("button", { name: "Choose 42" }))
await expect(modalResult).resolves.toBe(42)
await waitFor(() => expect(screen.queryByText("Choose a value")).toBeNull())
act(() => {
void openSheet({
content: Account details
,
scope,
title: "Account",
})
void openDrawer({
content: Filters
,
scope,
title: "Filter products",
})
})
expect(screen.getByText("Account details")).toBeTruthy()
expect(screen.getByText("Filter products")).toBeTruthy()
})
it("renders scoped toasts", async () => {
const { scope } = renderHosts()
act(() => {
openToast({
description: "Your changes were saved.",
scope,
title: "Saved",
type: "success",
})
})
expect(await screen.findByText("Your changes were saved.")).toBeTruthy()
})
it("provides current and named local scopes", () => {
const editor = createOverlayScope({ name: "editor" })
const picker = createOverlayScope({ name: "picker" })
let current: OverlayScope | undefined
let named: OverlayScope | undefined
function CaptureScopes() {
current = useOverlayScope()
named = useOverlayScope("picker")
return null
}
render(
)
expect(current).toBe(editor)
expect(named).toBe(picker)
})
it("queues requests for the same atomic host", async () => {
const { scope } = renderHosts()
let first: Promise | undefined
let second: Promise | undefined
act(() => {
first = openConfirm({ scope, title: "First confirmation" })
second = openConfirm({ scope, title: "Second confirmation" })
})
expect(screen.getByText("First confirmation")).toBeTruthy()
expect(screen.queryByText("Second confirmation")).toBeNull()
fireEvent.click(screen.getByRole("button", { name: "Confirm" }))
await expect(first).resolves.toBe(true)
expect(await screen.findByText("Second confirmation")).toBeTruthy()
fireEvent.click(screen.getByRole("button", { name: "Cancel" }))
await expect(second).resolves.toBe(false)
})
it("settles pending requests when their host unmounts", async () => {
const { scope, unmount } = renderHosts()
let result: Promise | undefined
act(() => {
result = openConfirm({ scope, title: "Pending action" })
})
unmount()
await expect(result).resolves.toBe(false)
})
it("intercepts navigation through a router-neutral adapter", async () => {
const scope = createOverlayScope()
let listener: ((attempt: OverlayNavigationAttempt) => void) | undefined
const adapter: OverlayNavigationAdapter = {
subscribe(nextListener) {
listener = nextListener
return () => {
listener = undefined
}
},
}
const cancel = vi.fn()
const proceed = vi.fn()
render(
)
let result: Promise | undefined
act(() => {
result = openConfirm({ scope, title: "Discard changes?" })
})
act(() => listener?.({ cancel, proceed }))
expect(cancel).toHaveBeenCalledOnce()
expect(proceed).not.toHaveBeenCalled()
await expect(result).resolves.toBe(false)
await waitFor(() => expect(scope.getTopmostLayer()).toBeUndefined())
act(() => listener?.({ cancel, proceed }))
expect(proceed).toHaveBeenCalledOnce()
})
it("keeps non-dismissible content open while blocking navigation", () => {
const scope = createOverlayScope()
let listener: ((attempt: OverlayNavigationAttempt) => void) | undefined
const adapter: OverlayNavigationAdapter = {
subscribe(nextListener) {
listener = nextListener
return () => {
listener = undefined
}
},
}
const cancel = vi.fn()
render(
)
act(() => {
void openModal({
content: ({ close }) => (
),
dismissible: false,
scope,
title: "Required step",
})
})
act(() => listener?.({ cancel, proceed: vi.fn() }))
expect(cancel).toHaveBeenCalledOnce()
expect(screen.getByText("Required step")).toBeTruthy()
})
})