181 lines
5.0 KiB
TypeScript
181 lines
5.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import * as React from "react"
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
|
|
import {
|
|
AppLayout,
|
|
createLayoutConfig,
|
|
defaultLayoutConfig,
|
|
useLayoutConfig,
|
|
} from "@workspace/layout"
|
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
|
|
afterEach(cleanup)
|
|
|
|
function LayoutControls() {
|
|
const { layout, toggleAside, toggleSidebar } = useLayoutConfig()
|
|
|
|
return (
|
|
<div>
|
|
<output aria-label="layout-state">
|
|
{layout.sidebarOpen ? "sidebar-open" : "sidebar-closed"}|
|
|
{layout.asideOpen ? "aside-open" : "aside-closed"}
|
|
</output>
|
|
<button type="button" onClick={toggleSidebar}>
|
|
Toggle sidebar
|
|
</button>
|
|
<button type="button" onClick={toggleAside}>
|
|
Toggle aside
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Header() {
|
|
return <span>Header component</span>
|
|
}
|
|
|
|
function PreferencesProbe() {
|
|
const { layout } = useLayoutConfig()
|
|
return (
|
|
<output aria-label="preferences">
|
|
{String(layout.sidebarOpen)}|{String(layout.asideOpen)}
|
|
</output>
|
|
)
|
|
}
|
|
|
|
describe("layout config", () => {
|
|
it("creates complete configurations from partial input", () => {
|
|
expect(createLayoutConfig({ asideOpen: true })).toEqual({
|
|
...defaultLayoutConfig,
|
|
asideOpen: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("AppLayout", () => {
|
|
it("renders component and node slots in a generated grid", () => {
|
|
render(
|
|
<AppLayout
|
|
header={Header}
|
|
rail={<span>Rail node</span>}
|
|
sidebar={<span>Sidebar node</span>}
|
|
aside={<span>Aside node</span>}
|
|
footer={<span>Footer node</span>}
|
|
layout={{
|
|
asideOpen: true,
|
|
footerStart: "sidebar",
|
|
headerStart: "sidebar",
|
|
}}
|
|
>
|
|
Content
|
|
</AppLayout>
|
|
)
|
|
|
|
const root = document.querySelector<HTMLElement>('[data-slot="app-layout"]')
|
|
|
|
expect(root?.style.gridTemplateAreas).toBe(
|
|
'"rail header header header" "rail sidebar content aside" "rail footer footer aside"'
|
|
)
|
|
expect(root?.dataset.headerStart).toBe("sidebar")
|
|
expect(root?.dataset.footerStart).toBe("sidebar")
|
|
expect(screen.getByText("Header component")).toBeTruthy()
|
|
expect(screen.getByText("Rail node")).toBeTruthy()
|
|
expect(screen.getByText("Sidebar node")).toBeTruthy()
|
|
expect(screen.getByText("Aside node")).toBeTruthy()
|
|
expect(screen.getByText("Footer node")).toBeTruthy()
|
|
})
|
|
|
|
it("moves sidebar starts inward when the rail is absent", () => {
|
|
render(
|
|
<AppLayout
|
|
header={<span>Header</span>}
|
|
sidebar={<span>Sidebar</span>}
|
|
aside={<span>Aside</span>}
|
|
footer={<span>Footer</span>}
|
|
layout={{
|
|
asideOpen: true,
|
|
footerStart: "sidebar",
|
|
headerStart: "sidebar",
|
|
}}
|
|
>
|
|
Content
|
|
</AppLayout>
|
|
)
|
|
|
|
const root = document.querySelector<HTMLElement>('[data-slot="app-layout"]')
|
|
|
|
expect(root?.style.gridTemplateAreas).toBe(
|
|
'"sidebar header header" "sidebar content aside" "sidebar footer aside"'
|
|
)
|
|
expect(root?.dataset.headerStart).toBe("content")
|
|
expect(root?.dataset.footerStart).toBe("content")
|
|
})
|
|
|
|
it("applies visibility capabilities without changing stored preferences", () => {
|
|
render(
|
|
<AppLayout
|
|
sidebar={<span>Sidebar</span>}
|
|
aside={<span>Aside</span>}
|
|
layout={{
|
|
asideEnabled: false,
|
|
asideOpen: true,
|
|
sidebarCollapsible: false,
|
|
sidebarOpen: false,
|
|
}}
|
|
>
|
|
<PreferencesProbe />
|
|
</AppLayout>
|
|
)
|
|
|
|
expect(screen.getByText("Sidebar")).toBeTruthy()
|
|
expect(screen.queryByText("Aside")).toBeNull()
|
|
expect(screen.getByLabelText("preferences").textContent).toBe("false|true")
|
|
})
|
|
|
|
it("manages uncontrolled sidebar and aside state through the hook", () => {
|
|
render(
|
|
<AppLayout sidebar={<span>Sidebar</span>} aside={<span>Aside</span>}>
|
|
<LayoutControls />
|
|
</AppLayout>
|
|
)
|
|
|
|
expect(screen.getByText("Sidebar")).toBeTruthy()
|
|
expect(screen.queryByText("Aside")).toBeNull()
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Toggle sidebar" }))
|
|
fireEvent.click(screen.getByRole("button", { name: "Toggle aside" }))
|
|
|
|
expect(screen.queryByText("Sidebar")).toBeNull()
|
|
expect(screen.getByText("Aside")).toBeTruthy()
|
|
expect(screen.getByLabelText("layout-state").textContent).toBe(
|
|
"sidebar-closed|aside-open"
|
|
)
|
|
})
|
|
|
|
it("reports controlled changes without mutating controlled state", () => {
|
|
const onLayoutChange = vi.fn()
|
|
|
|
render(
|
|
<AppLayout
|
|
aside={<span>Aside</span>}
|
|
layout={{ asideOpen: false }}
|
|
onLayoutChange={onLayoutChange}
|
|
>
|
|
<LayoutControls />
|
|
</AppLayout>
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Toggle aside" }))
|
|
|
|
expect(onLayoutChange).toHaveBeenCalledWith({
|
|
...defaultLayoutConfig,
|
|
asideOpen: true,
|
|
})
|
|
expect(screen.queryByText("Aside")).toBeNull()
|
|
expect(screen.getByLabelText("layout-state").textContent).toContain(
|
|
"aside-closed"
|
|
)
|
|
})
|
|
})
|