Skip to content

Commit b9cc08e

Browse files
authored
chore: bump @cloudflare/kumo to 2.3 (#1177)
* chore: bump @cloudflare/kumo to 2.3 Migrates two internal call sites to the Kumo 2 API: - accordion block: Collapsible refactored to a compound component. Use Collapsible.Root / .DefaultTrigger / .DefaultPanel instead of <Collapsible label=...>. - chart block: ChartPalette.color renamed to ChartPalette.categorical. Admin tests that asserted on Button's native title attribute now read aria-label instead, because Kumo 2 wraps <Button title> in a Tooltip popup rather than setting the DOM title attribute. Updated the @cloudflare/kumo mock in the blocks renderer tests to match the new Collapsible compound shape. No public API changes; consumers see identical behaviour. * test(e2e): update title selectors for Kumo 2 Button tooltip Kumo 2.x wraps <Button title> in a Tooltip popup rather than setting a DOM title attribute. Switch redirect and revision history e2e selectors from button[title=...] to button[aria-label=...] / [aria-label^=...]. The components already exposed accurate aria-labels (and in the redirects case, more specific per-row labels via the source path).
1 parent e05491e commit b9cc08e

11 files changed

Lines changed: 143 additions & 67 deletions

File tree

.changeset/bump-kumo-to-2.x.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@emdash-cms/admin": patch
3+
"@emdash-cms/blocks": patch
4+
"@emdash-cms/auth-atproto": patch
5+
---
6+
7+
Bumps `@cloudflare/kumo` from 1.16 to 2.3. Two internal call sites picked up breaking API changes from Kumo 2.0: `Collapsible` is now a compound component (`Collapsible.Root` / `.DefaultTrigger` / `.DefaultPanel` instead of `<Collapsible label=...>`), used by the accordion block; and `ChartPalette.color()` was renamed to `ChartPalette.categorical()` in the chart block. No public API changes -- consumers see identical behaviour. Tests in `@emdash-cms/admin` that asserted on `Button`'s native `title` attribute now read `aria-label` instead, because Kumo 2 wraps `<Button title>` in a Tooltip popup rather than setting the DOM attribute.

e2e/tests/redirects.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ test.describe("Redirects", () => {
7373
// Wait for the redirect to appear
7474
await expect(page.locator("text=/edit-source").first()).toBeVisible();
7575

76-
// Click the edit button on that row (use .first() to avoid ancestor div ambiguity)
77-
await page.locator('button[title="Edit redirect"]').first().click();
76+
// Click the edit button on that row (use .first() to avoid ancestor div ambiguity).
77+
// Kumo 2.x renders <Button title> as a Tooltip popup rather than a DOM
78+
// title attribute; aria-label is on the button itself.
79+
await page.locator('button[aria-label^="Edit redirect"]').first().click();
7880

7981
// Edit dialog should open
8082
await expect(page.locator('[role="dialog"]')).toBeVisible();
@@ -110,8 +112,9 @@ test.describe("Redirects", () => {
110112
// Wait for it to appear
111113
await expect(page.locator("text=/to-delete").first()).toBeVisible();
112114

113-
// Click the delete button on that row (use .first() to avoid ancestor div ambiguity)
114-
await page.locator('button[title="Delete redirect"]').first().click();
115+
// Click the delete button on that row (use .first() to avoid ancestor div ambiguity).
116+
// See note above re: Kumo 2.x Button title -> Tooltip.
117+
await page.locator('button[aria-label^="Delete redirect"]').first().click();
115118

116119
// Confirm deletion in the ConfirmDialog
117120
await expect(page.locator('[role="dialog"]')).toBeVisible();

e2e/tests/revisions.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,11 @@ test.describe("Revisions", () => {
286286
const revisionItems = page.locator(".rounded-md.border.p-3");
287287
await expect(revisionItems.first()).toBeVisible({ timeout: 10000 });
288288

289-
// Find the restore button on the older revision (not the "Current" one)
290-
// The restore button uses ArrowCounterClockwise icon and title="Restore this version"
291-
const restoreButton = page.locator('button[title="Restore this version"]').first();
289+
// Find the restore button on the older revision (not the "Current" one).
290+
// The restore button uses ArrowCounterClockwise icon. Kumo 2.x renders
291+
// <Button title> as a Tooltip popup rather than a DOM title attribute,
292+
// so we locate by aria-label instead.
293+
const restoreButton = page.locator('button[aria-label="Restore this version"]').first();
292294
await expect(restoreButton).toBeVisible({ timeout: 5000 });
293295

294296
// Click restore -- this opens a ConfirmDialog

packages/admin/tests/components/Header.test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const { Header } = await import("../../src/components/Header");
5353
// Constants
5454
// ---------------------------------------------------------------------------
5555

56-
const THEME_BUTTON_REGEX = /Theme:/;
56+
const THEME_BUTTON_REGEX = /Toggle theme/;
5757

5858
function TestWrapper({ children }: { children: React.ReactNode }) {
5959
const qc = new QueryClient({
@@ -80,8 +80,9 @@ describe("Header", () => {
8080
<Header />
8181
</TestWrapper>,
8282
);
83-
// ThemeToggle renders a button with title containing "Theme:"
84-
const themeButton = screen.getByTitle(THEME_BUTTON_REGEX);
83+
// ThemeToggle renders a button with aria-label "Toggle theme (current: …)"
84+
// (Kumo 2.x wraps `<Button title>` as a Tooltip popup, not a DOM title.)
85+
const themeButton = screen.getByLabelText(THEME_BUTTON_REGEX);
8586
await expect.element(themeButton).toBeInTheDocument();
8687
});
8788

packages/admin/tests/components/Sections.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ describe("Sections", () => {
162162
</Wrapper>,
163163
);
164164
await expect.element(screen.getByText("Call to Action")).toBeInTheDocument();
165-
// Click delete on the user section
166-
const deleteButton = screen.getByTitle("Delete");
165+
// Click delete on the user section. Kumo 2.x wraps `<Button title>` as
166+
// a Tooltip popup rather than a DOM `title` attribute, so we locate the
167+
// button by its aria-label instead.
168+
const deleteButton = screen.getByLabelText("Delete Call to Action");
167169
await deleteButton.click();
168170
await expect.element(screen.getByText("Delete Section?")).toBeInTheDocument();
169171
await expect.element(screen.getByText(DELETE_SECTION_MSG_REGEX)).toBeInTheDocument();
@@ -186,7 +188,11 @@ describe("Sections", () => {
186188
</Wrapper>,
187189
);
188190
await expect.element(screen.getByText("Hero Section")).toBeInTheDocument();
189-
const deleteButton = screen.getByTitle("Cannot delete theme sections");
191+
// Kumo 2.x's `<Button title>` is rendered as a Tooltip popup, not a DOM
192+
// `title`. Locate the button by aria-label and assert the disabled state
193+
// directly; the "Cannot delete theme sections" copy is now in the
194+
// hover Tooltip rather than a queryable attribute.
195+
const deleteButton = screen.getByLabelText("Delete Hero Section");
190196
await expect.element(deleteButton).toBeDisabled();
191197
});
192198

packages/admin/tests/components/ThemeToggle.test.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,42 @@ describe("ThemeToggle", () => {
1919
document.documentElement.removeAttribute("data-theme");
2020
});
2121

22+
// Kumo 2.x's <Button title="..."> wraps the button in a Tooltip popup
23+
// rather than setting the native `title` attribute. The current theme is
24+
// also exposed in `aria-label`, which is what these assertions read.
25+
2226
it("renders with system theme by default", async () => {
2327
const screen = await render(<TestThemeToggle />);
2428
const button = screen.getByRole("button");
2529
await expect.element(button).toBeInTheDocument();
26-
// System theme shows Monitor icon - check the title
27-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("System"));
30+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("System"));
2831
});
2932

3033
it("cycles from system to light on click", async () => {
3134
const screen = await render(<TestThemeToggle />);
3235
const button = screen.getByRole("button");
3336
await button.click();
34-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("Light"));
37+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("Light"));
3538
});
3639

3740
it("cycles through system -> light -> dark -> system", async () => {
3841
const screen = await render(<TestThemeToggle />);
3942
const button = screen.getByRole("button");
4043

4144
// Start: system
42-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("System"));
45+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("System"));
4346

4447
// Click 1: light
4548
await button.click();
46-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("Light"));
49+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("Light"));
4750

4851
// Click 2: dark
4952
await button.click();
50-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("Dark"));
53+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("Dark"));
5154

5255
// Click 3: back to system
5356
await button.click();
54-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("System"));
57+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("System"));
5558
});
5659

5760
it("persists theme to localStorage", async () => {
@@ -64,6 +67,6 @@ describe("ThemeToggle", () => {
6467
it("starts with light theme when defaultTheme is light", async () => {
6568
const screen = await render(<TestThemeToggle defaultTheme="light" />);
6669
const button = screen.getByRole("button");
67-
await expect.element(button).toHaveAttribute("title", expect.stringContaining("Light"));
70+
await expect.element(button).toHaveAttribute("aria-label", expect.stringContaining("Light"));
6871
});
6972
});

packages/blocks/src/blocks/accordion.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ export function AccordionBlockComponent({
1414
const [open, setOpen] = useState(block.default_open ?? false);
1515

1616
return (
17-
<Collapsible label={block.label} open={open} onOpenChange={setOpen}>
18-
<BlockRenderer blocks={block.blocks} onAction={onAction} />
19-
</Collapsible>
17+
<Collapsible.Root open={open} onOpenChange={setOpen} data-testid="collapsible" data-open={open}>
18+
<Collapsible.DefaultTrigger>{block.label}</Collapsible.DefaultTrigger>
19+
<Collapsible.DefaultPanel>
20+
<BlockRenderer blocks={block.blocks} onAction={onAction} />
21+
</Collapsible.DefaultPanel>
22+
</Collapsible.Root>
2023
);
2124
}

packages/blocks/src/blocks/chart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function TimeseriesChartBlock({ block, isDarkMode }: { block: ChartBlock; isDark
100100
config.series.map((s, i) => ({
101101
name: escapeHtml(s.name),
102102
data: s.data,
103-
color: s.color ?? ChartPalette.color(i, isDarkMode),
103+
color: s.color ?? ChartPalette.categorical(i, isDarkMode),
104104
})),
105105
[config.series, isDarkMode],
106106
);

packages/blocks/tests/renderer.test.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import type { Block, BlockInteraction } from "../src/types.js";
77

88
// ── Mocks ────────────────────────────────────────────────────────────────────
99

10+
// Shared between the Collapsible.* mocks so DefaultTrigger / DefaultPanel can
11+
// react to open state set on Collapsible.Root.
12+
const CollapsibleContext = React.createContext<{
13+
open?: boolean;
14+
onOpenChange?: (next: boolean) => void;
15+
}>({});
16+
1017
vi.mock("@cloudflare/kumo", () => ({
1118
Button: ({ children, onClick, variant, type }: any) => (
1219
<button onClick={onClick} data-variant={variant} type={type || "button"}>
@@ -159,13 +166,48 @@ vi.mock("@cloudflare/kumo", () => ({
159166
</label>
160167
),
161168
},
162-
Collapsible: ({ children, label, open, onOpenChange }: any) => (
163-
<div data-testid="collapsible" data-open={open ? "true" : "false"}>
164-
<button type="button" onClick={() => onOpenChange?.(!open)}>
165-
{label}
166-
</button>
167-
{open && <div data-testid="collapsible-content">{children}</div>}
168-
</div>
169+
Collapsible: Object.assign(
170+
// `Collapsible` is also `Collapsible.Root` in Kumo 2.x.
171+
({ children, open, onOpenChange, ...rest }: any) => (
172+
<div data-testid="collapsible" data-open={open ? "true" : "false"} {...rest}>
173+
<CollapsibleContext.Provider value={{ open, onOpenChange }}>
174+
{children}
175+
</CollapsibleContext.Provider>
176+
</div>
177+
),
178+
{
179+
Root: ({ children, open, onOpenChange, ...rest }: any) => (
180+
<div data-testid="collapsible" data-open={open ? "true" : "false"} {...rest}>
181+
<CollapsibleContext.Provider value={{ open, onOpenChange }}>
182+
{children}
183+
</CollapsibleContext.Provider>
184+
</div>
185+
),
186+
Trigger: ({ children }: any) => {
187+
const ctx = React.useContext(CollapsibleContext);
188+
return (
189+
<button type="button" onClick={() => ctx.onOpenChange?.(!ctx.open)}>
190+
{children}
191+
</button>
192+
);
193+
},
194+
DefaultTrigger: ({ children }: any) => {
195+
const ctx = React.useContext(CollapsibleContext);
196+
return (
197+
<button type="button" onClick={() => ctx.onOpenChange?.(!ctx.open)}>
198+
{children}
199+
</button>
200+
);
201+
},
202+
Panel: ({ children }: any) => {
203+
const ctx = React.useContext(CollapsibleContext);
204+
return ctx.open ? <div data-testid="collapsible-content">{children}</div> : null;
205+
},
206+
DefaultPanel: ({ children }: any) => {
207+
const ctx = React.useContext(CollapsibleContext);
208+
return ctx.open ? <div data-testid="collapsible-content">{children}</div> : null;
209+
},
210+
},
169211
),
170212
Combobox: Object.assign(
171213
({ children, label }: any) => (

0 commit comments

Comments
 (0)