Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ jobs:
- run: pnpm exec playwright install --with-deps chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: pnpm run --filter @emdash-cms/admin test
# Upload vitest browser-mode failure screenshots so we can diagnose
# CI-only flakes (e.g. the recurring slash-menu race). Only runs on
# failure, only uploads the screenshot directories.
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-test-screenshots
path: |
packages/admin/tests/**/__screenshots__/**
if-no-files-found: ignore
retention-days: 1

test-e2e-rollup:
name: E2E Tests
Expand Down
64 changes: 58 additions & 6 deletions packages/admin/tests/editor/slash-menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,57 @@ function isItemSelected(el: HTMLElement): boolean {
return el.className.split(WHITESPACE_SPLIT_REGEX).includes("bg-kumo-tint");
}

/**
* DIAGNOSTIC (temporary, see #1004 follow-up): dump everything useful about
* the slash menu state to console.log. Called from a catch block when
* vi.waitFor times out, so the failing CI log contains the actual menu
* contents and DOM state at the moment of failure. Remove once the
* underlying race is understood and fixed.
*/
function dumpMenuState(label: string): void {
const menu = getSlashMenu();
const lines: string[] = [
`[slash-menu diag] === ${label} ===`,
`[slash-menu diag] menu present: ${menu !== null}`,
`[slash-menu diag] activeElement: ${document.activeElement?.tagName} (class=${document.activeElement?.className?.slice(0, 80)})`,
`[slash-menu diag] body > div count: ${document.querySelectorAll("body > div").length}`,
];
if (menu) {
const items = getSlashMenuItems(menu);
lines.push(`[slash-menu diag] items rendered: ${items.length}`);
lines.push(`[slash-menu diag] menu textContent length: ${menu.textContent?.length ?? 0}`);
lines.push(`[slash-menu diag] menu first 200 chars: ${menu.textContent?.slice(0, 200)}`);
items.forEach((el, i) => {
const dataIndex = el.getAttribute("data-index");
const selected = isItemSelected(el);
const classes = el.className;
lines.push(
`[slash-menu diag] item ${i} (data-index=${dataIndex}) selected=${selected} classes=${classes.slice(0, 200)}`,
);
});
// menu.outerHTML truncated to 2000 chars
lines.push(`[slash-menu diag] menu outerHTML: ${menu.outerHTML.slice(0, 2000)}`);
}
console.log(lines.join("\n"));
}

/**
* DIAGNOSTIC wrapper around vi.waitFor that dumps menu state on timeout.
* Same semantics as vi.waitFor; only adds logging on failure.
*/
async function diagWaitFor<T>(
label: string,
predicate: () => T | Promise<T>,
options?: { timeout?: number; interval?: number },
): Promise<T> {
try {
return await vi.waitFor(predicate, options);
} catch (err) {
dumpMenuState(label);
throw err;
}
}

// =============================================================================
// Slash Command Menu
// =============================================================================
Expand Down Expand Up @@ -287,7 +338,8 @@ describe("Slash Command Menu", () => {

await waitForSlashMenu();

await vi.waitFor(
await diagWaitFor(
"highlights the first item by default",
() => {
const menu = getSlashMenu()!;
const items = getSlashMenuItems(menu);
Expand All @@ -305,7 +357,7 @@ describe("Slash Command Menu", () => {

await userEvent.keyboard("{ArrowDown}");

await vi.waitFor(() => {
await diagWaitFor("moves selection down with ArrowDown", () => {
const menu = getSlashMenu()!;
const items = getSlashMenuItems(menu);
expect(isItemSelected(items[1]!)).toBe(true);
Expand All @@ -321,13 +373,13 @@ describe("Slash Command Menu", () => {

// Move down, then back up
await userEvent.keyboard("{ArrowDown}");
await vi.waitFor(() => {
await diagWaitFor("ArrowUp from second item: first ArrowDown", () => {
const items = getSlashMenuItems(getSlashMenu()!);
expect(isItemSelected(items[1]!)).toBe(true);
});

await userEvent.keyboard("{ArrowUp}");
await vi.waitFor(() => {
await diagWaitFor("ArrowUp from second item: back to first", () => {
const items = getSlashMenuItems(getSlashMenu()!);
expect(isItemSelected(items[0]!)).toBe(true);
});
Expand All @@ -341,7 +393,7 @@ describe("Slash Command Menu", () => {

await userEvent.keyboard("{ArrowUp}");

await vi.waitFor(() => {
await diagWaitFor("wraps selection around (ArrowUp from first)", () => {
const menu = getSlashMenu()!;
const items = getSlashMenuItems(menu);
const lastItem = items.at(-1)!;
Expand All @@ -360,7 +412,7 @@ describe("Slash Command Menu", () => {

await waitForSlashMenuClosed();

await vi.waitFor(() => {
await diagWaitFor("Enter converts to heading: h1 should exist", () => {
expect(pm.querySelector("h1")).toBeTruthy();
});
});
Expand Down
Loading