cf3dbc9582
* fix(web): keep command palette selection on first result * test(web): fix launch session mock response shape * refactor(web): address command palette review feedback * fix(web): align command panel selection setter type
205 lines
7.4 KiB
TypeScript
205 lines
7.4 KiB
TypeScript
import { test, expect } from "../fixtures/test-base";
|
|
import { KanbanPage } from "../pages/kanban-page";
|
|
|
|
/**
|
|
* Helper: open the command panel via Cmd/Ctrl+K.
|
|
*/
|
|
async function openCommandPanel(page: import("@playwright/test").Page) {
|
|
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
|
await page.keyboard.press(`${modifier}+k`);
|
|
}
|
|
|
|
/**
|
|
* Helper: open the file search panel via Cmd/Ctrl+Shift+K.
|
|
*/
|
|
async function openFileSearch(page: import("@playwright/test").Page) {
|
|
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
|
await page.keyboard.press(`${modifier}+Shift+k`);
|
|
}
|
|
|
|
/** The command dialog (Radix Dialog with role="dialog"). */
|
|
function commandDialog(page: import("@playwright/test").Page) {
|
|
return page.getByRole("dialog");
|
|
}
|
|
|
|
test.describe("Command Panel", () => {
|
|
test("Cmd+K opens command panel and shows commands (not files)", async ({ testPage }) => {
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Should show command groups like Navigation, Settings
|
|
await expect(dialog.getByText("Navigation")).toBeVisible();
|
|
await expect(dialog.getByText("Go to Home")).toBeVisible();
|
|
|
|
// Type something that matches a navigation command
|
|
await dialog.locator("input").fill("Settings");
|
|
// Should find settings commands
|
|
await expect(dialog.getByText("Go to Settings")).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Should NOT show a "Files" group — file search is now separate
|
|
await expect(dialog.getByText("Files").first()).not.toBeVisible({ timeout: 2_000 });
|
|
});
|
|
|
|
test("Cmd+Shift+K opens file search mode with appropriate placeholder", async ({ testPage }) => {
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
await openFileSearch(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Should show the "Files" back-button breadcrumb
|
|
await expect(dialog.getByRole("button", { name: /Files/ })).toBeVisible();
|
|
|
|
// Should show empty state for file search
|
|
await expect(dialog.getByText("Type to search files...")).toBeVisible();
|
|
});
|
|
|
|
test("Cmd+K inline task search shows matching tasks", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
// Create a task to search for
|
|
await apiClient.createTask(seedData.workspaceId, "Searchable E2E Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
// Wait for the task to appear on the kanban board first
|
|
await expect(kanban.taskCardByTitle("Searchable E2E Task")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Type the task name — task search requires ≥2 characters
|
|
await dialog.locator("input").fill("Searchable");
|
|
|
|
// Should show the task in a "Tasks" group (inline search, debounced)
|
|
await expect(dialog.getByText("Tasks")).toBeVisible({ timeout: 10_000 });
|
|
await expect(dialog.getByText("Searchable E2E Task")).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test("inline task search shows workflow step badge", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await apiClient.createTask(seedData.workspaceId, "Badged Task E2E", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
await expect(kanban.taskCardByTitle("Badged Task E2E")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
await dialog.locator("input").fill("Badged Task");
|
|
|
|
// The task should appear with a workflow step badge
|
|
const taskRow = dialog.getByText("Badged Task E2E");
|
|
await expect(taskRow).toBeVisible({ timeout: 10_000 });
|
|
|
|
// The step badge should be present in the task result row
|
|
const startStep = seedData.steps.find((s) => s.id === seedData.startStepId)!;
|
|
const taskOption = dialog.getByRole("option", { name: /Badged Task E2E/ });
|
|
await expect(taskOption).toBeVisible({ timeout: 5_000 });
|
|
await expect(taskOption.getByText(startStep.name)).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test("inline task search selects the first matching task after loading", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await apiClient.createTask(seedData.workspaceId, "Palette Selection Alpha", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
await apiClient.createTask(seedData.workspaceId, "Palette Selection Beta", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
await expect(kanban.taskCardByTitle("Palette Selection Alpha")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(kanban.taskCardByTitle("Palette Selection Beta")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
const input = dialog.getByRole("combobox");
|
|
await input.fill("Palette Selection");
|
|
|
|
const taskOptions = dialog.getByRole("option").filter({ hasText: /Palette Selection/ });
|
|
await expect(taskOptions.first()).toBeVisible({ timeout: 10_000 });
|
|
await expect(taskOptions.first()).toHaveAttribute("data-selected", "true", {
|
|
timeout: 5_000,
|
|
});
|
|
});
|
|
|
|
test("Escape closes the command panel", async ({ testPage }) => {
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
await testPage.keyboard.press("Escape");
|
|
await expect(dialog).not.toBeVisible({ timeout: 3_000 });
|
|
});
|
|
|
|
test("Backspace in file search mode returns to commands mode", async ({ testPage }) => {
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
// Open file search mode
|
|
await openFileSearch(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
await expect(dialog.getByText("Type to search files...")).toBeVisible();
|
|
|
|
// Focus the input and press backspace on empty input to go back to commands mode
|
|
const input = dialog.getByRole("combobox");
|
|
await input.focus();
|
|
await input.press("Backspace");
|
|
|
|
// Should now show the commands mode (navigation commands visible)
|
|
await expect(dialog.getByText("Navigation")).toBeVisible({ timeout: 5_000 });
|
|
await expect(dialog.getByText("Go to Home")).toBeVisible({ timeout: 3_000 });
|
|
});
|
|
|
|
test("Cmd+K toggles the panel open and closed", async ({ testPage }) => {
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
|
|
// Open
|
|
await openCommandPanel(testPage);
|
|
const dialog = commandDialog(testPage);
|
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Close by pressing Cmd+K again
|
|
await openCommandPanel(testPage);
|
|
await expect(dialog).not.toBeVisible({ timeout: 3_000 });
|
|
});
|
|
});
|