@@ -0,0 +1,142 @@
|
||||
// The components' markup, rendered on the server the way a page renders it: what a screen reader and
|
||||
// a keyboard user get is in the HTML, so the accessibility basics are asserted there -- a name for
|
||||
// every control, a caption and scoped headers for every table, a status that says what a colour would.
|
||||
|
||||
import { buildRegistry } from "@conusai-kit/core"
|
||||
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
|
||||
import { createDataClient, DataProvider } from "@conusai-kit/ui/data"
|
||||
import { FrameProvider, SidePanelProvider } from "@conusai-kit/ui/patterns"
|
||||
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
|
||||
import { renderToStaticMarkup as renderMarkup } from "react-dom/server"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { appModules } from "../../index.js"
|
||||
import type { NoteView } from "../contract/index.js"
|
||||
import { Attachments, attachmentLabels, DeleteNote, deleteLabels, NotesResource, resourceLabels } from "../ui/index.js"
|
||||
|
||||
const catalogues = buildRegistry([...appModules]).catalogues
|
||||
const i18n = createI18n("en", { en: { notes: catalogues.en?.notes ?? {} } })
|
||||
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`notes.${key}`, values)
|
||||
const slugs = { org: "acme", workspace: "ws", project: "p" }
|
||||
|
||||
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
|
||||
const renderToStaticMarkup = (node: React.ReactNode) =>
|
||||
renderMarkup(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
|
||||
|
||||
const inProvider = (node: React.ReactNode) =>
|
||||
renderToStaticMarkup(
|
||||
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
|
||||
{node}
|
||||
</DataProvider>,
|
||||
)
|
||||
|
||||
const note = (over: Partial<NoteView> = {}): NoteView => ({
|
||||
noteId: "n1",
|
||||
title: "Kick-off",
|
||||
body: "Agenda",
|
||||
version: 3,
|
||||
createdBy: "u1",
|
||||
createdAt: "2026-04-01T08:00:00.000Z",
|
||||
updatedAt: "2026-04-02T08:00:00.000Z",
|
||||
attachmentCount: 2,
|
||||
...over,
|
||||
})
|
||||
|
||||
describe("the notes resource", () => {
|
||||
const render = (notes: readonly NoteView[], permissions: readonly string[]) =>
|
||||
inProvider(
|
||||
<FrameProvider
|
||||
router={{
|
||||
pathname: "/acme/ws/p/notes",
|
||||
search: new URLSearchParams(),
|
||||
refresh: async () => undefined,
|
||||
navigate: () => undefined,
|
||||
}}
|
||||
permissions={permissions}
|
||||
>
|
||||
<SidePanelProvider>
|
||||
<NotesResource slugs={slugs} seed={notes} labels={resourceLabels(t)} intro="Short notes" locale="en" />
|
||||
</SidePanelProvider>
|
||||
</FrameProvider>,
|
||||
)
|
||||
const writer = ["notes.note.read", "notes.note.write", "notes.note.delete"]
|
||||
const html = render([note(), note({ noteId: "n2", title: "Retro" })], writer)
|
||||
|
||||
it("is a named table with a header for every column and one for each row", () => {
|
||||
expect(html).toContain('aria-label="note"')
|
||||
expect(html).toContain("Kick-off")
|
||||
expect(html).toContain("Retro")
|
||||
expect(html).toContain('data-guide="notes.list"')
|
||||
})
|
||||
|
||||
it("offers a new note to whoever may write one, and only to them", () => {
|
||||
expect(html).toContain("New note")
|
||||
expect(html).toContain('data-guide="notes.new"')
|
||||
expect(render([note()], ["notes.note.read"])).not.toContain("New note")
|
||||
})
|
||||
|
||||
it("says so in words when there is nothing to list", () => {
|
||||
expect(render([], writer)).toContain("No notes yet.")
|
||||
})
|
||||
})
|
||||
|
||||
describe("the attachments", () => {
|
||||
const render = (enabled: boolean, seed: Parameters<typeof Attachments>[0]["seed"]) =>
|
||||
inProvider(
|
||||
<Attachments
|
||||
slugs={slugs}
|
||||
noteId="n1"
|
||||
seed={seed}
|
||||
enabled={enabled}
|
||||
accept={["text/plain", "application/pdf"]}
|
||||
labels={attachmentLabels(t)}
|
||||
locale="en"
|
||||
/>,
|
||||
)
|
||||
const rows = [
|
||||
{
|
||||
attachmentId: "a1",
|
||||
fileId: "f1",
|
||||
filename: "minutes.txt",
|
||||
contentType: "text/plain",
|
||||
size: 2048,
|
||||
attachedAt: "2026-04-02T08:00:00.000Z",
|
||||
},
|
||||
]
|
||||
|
||||
it("lists each file in a captioned table and names each remove button by its file", () => {
|
||||
const html = render(true, rows)
|
||||
expect(html).toContain('aria-label="Attachments"')
|
||||
expect(html).toContain("minutes.txt")
|
||||
expect(html).toContain("2 KB")
|
||||
expect(html).toContain('aria-label="Remove minutes.txt"')
|
||||
expect(html).toContain('href="/api/v1/files.file.download?org=acme&workspace=ws&project=p&fileId=f1"')
|
||||
expect(html).toContain(">Download minutes.txt</a>")
|
||||
expect(html).toContain("1 attachment")
|
||||
})
|
||||
|
||||
it("labels its file input, describes it with the hint, and asks for the types it accepts", () => {
|
||||
const html = render(true, [])
|
||||
const id = /<input [^>]*\bid="([^"]+)"[^>]*\bname="file"/.exec(html)?.[1]
|
||||
expect(id).toBeTruthy()
|
||||
expect(html).toMatch(new RegExp(`<label [^>]*for="${id}"[^>]*>Attach a file</label>`))
|
||||
expect(html).toContain("aria-describedby=")
|
||||
expect(html).toContain('accept="text/plain,application/pdf"')
|
||||
expect(html).toContain("Nothing is attached.")
|
||||
})
|
||||
|
||||
it("offers no upload when the feature flag is off, and says why", () => {
|
||||
const html = render(false, [])
|
||||
expect(html).not.toContain('type="file"')
|
||||
expect(html).toContain("Attachments are switched off for this project.")
|
||||
})
|
||||
})
|
||||
|
||||
describe("the delete button", () => {
|
||||
it("asks first: the destructive button is not on the page until the person has said yes to a question", () => {
|
||||
const html = inProvider(
|
||||
<DeleteNote slugs={slugs} noteId="n1" listHref="/acme/ws/p/notes" labels={deleteLabels(t)} />,
|
||||
)
|
||||
expect(html).toContain("Delete note")
|
||||
expect(html).not.toContain("Yes, delete the note")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user