Skip to content

Commit 3b790ed

Browse files
committed
test(view): expand E2E test coverage
Add 5 new E2E test files based on test-targets.md: - Deep nested group creation (3+ levels) tests - Icon picker full workflow tests - Multiple scope data isolation tests - Responsive layout mobile viewport (375x667) tests - Unsaved changes dialog all actions tests Total 18 test cases added (691 lines)
1 parent d90cb74 commit 3b790ed

5 files changed

Lines changed: 691 additions & 0 deletions
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
import { openAddCommandDialog, saveCommandDialog } from "./helpers/test-helpers";
4+
5+
const SELECTORS = {
6+
ADD_COMMAND_BUTTON: "Add new command",
7+
ADD_GROUP_BUTTON: "Add new group",
8+
COMMAND_NAME_INPUT: "Command Name",
9+
COMMAND_NAME_PLACEHOLDER: "Command name",
10+
COMMAND_PLACEHOLDER: "Command (e.g., npm start)",
11+
GROUP_COMMAND_ITEM: '[data-testid="group-command-item"]',
12+
GROUP_COMMANDS_RADIO: "Group Commands",
13+
GROUP_NAME_PLACEHOLDER: "Group name",
14+
SAVE_BUTTON: "Save",
15+
} as const;
16+
17+
test.describe("Deep Nested Group Creation (3+ levels)", () => {
18+
test.beforeEach(async ({ page }) => {
19+
await page.goto("/");
20+
});
21+
22+
test("should create and manage 3-level nested groups", async ({ page }) => {
23+
// Step 1: Create a root group command
24+
await openAddCommandDialog(page);
25+
26+
// Fill group name and switch to Group Commands type
27+
await page.getByRole("textbox", { name: SELECTORS.COMMAND_NAME_INPUT }).fill("Level 1 Group");
28+
await page.getByRole("radio", { name: SELECTORS.GROUP_COMMANDS_RADIO }).click();
29+
30+
// Get the main dialog
31+
const mainDialog = page.getByRole("dialog", { name: /Add New Command/i });
32+
33+
// Add a nested group at level 2
34+
await mainDialog.getByRole("button", { name: SELECTORS.ADD_GROUP_BUTTON }).click();
35+
36+
// Find the newly added group item and fill its name
37+
const level2GroupItem = mainDialog.locator(SELECTORS.GROUP_COMMAND_ITEM).last();
38+
await level2GroupItem.getByPlaceholder(SELECTORS.GROUP_NAME_PLACEHOLDER).fill("Level 2 Group");
39+
40+
// Click edit button to open Level 2 Group dialog
41+
const editLevel2Button = level2GroupItem.getByRole("button", { name: /edit/i });
42+
await editLevel2Button.click();
43+
44+
// Get Level 2 dialog
45+
const level2Dialog = page.getByRole("dialog", { name: /Edit Group: Level 2 Group/i });
46+
await expect(level2Dialog).toBeVisible();
47+
48+
// Add a nested group at level 3
49+
await level2Dialog.getByRole("button", { name: SELECTORS.ADD_GROUP_BUTTON }).click();
50+
51+
// Find the newly added group item and fill its name
52+
const level3GroupItem = level2Dialog.locator(SELECTORS.GROUP_COMMAND_ITEM).last();
53+
await level3GroupItem.getByPlaceholder(SELECTORS.GROUP_NAME_PLACEHOLDER).fill("Level 3 Group");
54+
55+
// Click edit button to open Level 3 Group dialog
56+
const editLevel3Button = level3GroupItem.getByRole("button", { name: /edit/i });
57+
await editLevel3Button.click();
58+
59+
// Get Level 3 dialog
60+
const level3Dialog = page.getByRole("dialog", { name: /Edit Group: Level 3 Group/i });
61+
await expect(level3Dialog).toBeVisible();
62+
63+
// Add a command at level 3
64+
await level3Dialog.getByRole("button", { name: SELECTORS.ADD_COMMAND_BUTTON }).click();
65+
66+
// Fill the command in level 3
67+
const level3CommandItem = level3Dialog.locator(SELECTORS.GROUP_COMMAND_ITEM).last();
68+
await level3CommandItem.getByPlaceholder(SELECTORS.COMMAND_NAME_PLACEHOLDER).fill("Deep Command");
69+
await level3CommandItem
70+
.getByPlaceholder(SELECTORS.COMMAND_PLACEHOLDER)
71+
.fill("echo deep nested");
72+
73+
// Save Level 3 dialog
74+
await level3Dialog.getByRole("button", { name: SELECTORS.SAVE_BUTTON }).click();
75+
await expect(level3Dialog).not.toBeVisible();
76+
77+
// Save Level 2 dialog
78+
await level2Dialog.getByRole("button", { name: SELECTORS.SAVE_BUTTON }).click();
79+
await expect(level2Dialog).not.toBeVisible();
80+
81+
// Save main dialog
82+
await saveCommandDialog(page);
83+
84+
// Verify Level 1 Group was created with proper nested structure
85+
const level1Card = page.locator('[data-testid="command-card"]', { hasText: "Level 1 Group" });
86+
await expect(level1Card).toBeVisible();
87+
88+
// The card should show it has nested commands (displayed as "X commands")
89+
await expect(level1Card.getByText(/command/i)).toBeVisible();
90+
});
91+
92+
test("dialog layers correctly stack at 3 levels", async ({ page }) => {
93+
// Create a root group command
94+
await openAddCommandDialog(page);
95+
96+
// Fill group name and switch to Group Commands type
97+
await page.getByRole("textbox", { name: SELECTORS.COMMAND_NAME_INPUT }).fill("Stack Test");
98+
await page.getByRole("radio", { name: SELECTORS.GROUP_COMMANDS_RADIO }).click();
99+
100+
const mainDialog = page.getByRole("dialog", { name: /Add New Command/i });
101+
102+
// Add level 2 group
103+
await mainDialog.getByRole("button", { name: SELECTORS.ADD_GROUP_BUTTON }).click();
104+
const level2GroupItem = mainDialog.locator(SELECTORS.GROUP_COMMAND_ITEM).last();
105+
await level2GroupItem.getByPlaceholder(SELECTORS.GROUP_NAME_PLACEHOLDER).fill("L2");
106+
await level2GroupItem.getByRole("button", { name: /edit/i }).click();
107+
108+
const level2Dialog = page.getByRole("dialog", { name: /Edit Group: L2/i });
109+
await expect(level2Dialog).toBeVisible();
110+
111+
// Add level 3 group
112+
await level2Dialog.getByRole("button", { name: SELECTORS.ADD_GROUP_BUTTON }).click();
113+
const level3GroupItem = level2Dialog.locator(SELECTORS.GROUP_COMMAND_ITEM).last();
114+
await level3GroupItem.getByPlaceholder(SELECTORS.GROUP_NAME_PLACEHOLDER).fill("L3");
115+
await level3GroupItem.getByRole("button", { name: /edit/i }).click();
116+
117+
const level3Dialog = page.getByRole("dialog", { name: /Edit Group: L3/i });
118+
await expect(level3Dialog).toBeVisible();
119+
120+
// At this point we have 3 dialogs open
121+
// Verify all three dialogs exist (some may be overlapped but should be in DOM)
122+
await expect(mainDialog).toBeAttached();
123+
await expect(level2Dialog).toBeAttached();
124+
await expect(level3Dialog).toBeVisible(); // Top-most should be visible
125+
126+
// Close level 3
127+
await level3Dialog.getByRole("button", { name: /cancel/i }).click();
128+
await expect(level3Dialog).not.toBeVisible();
129+
130+
// Level 2 should now be visible/interactive
131+
await expect(level2Dialog).toBeVisible();
132+
133+
// Close level 2
134+
await level2Dialog.getByRole("button", { name: /cancel/i }).click();
135+
await expect(level2Dialog).not.toBeVisible();
136+
137+
// Main dialog should now be visible/interactive
138+
await expect(mainDialog).toBeVisible();
139+
140+
// Close main dialog
141+
await page.getByRole("button", { name: /cancel/i }).click();
142+
await expect(mainDialog).not.toBeVisible();
143+
});
144+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { expect, test, type Page } from "@playwright/test";
2+
3+
import {
4+
clearAllCommands,
5+
fillCommandForm,
6+
openAddCommandDialog,
7+
saveCommandDialog,
8+
} from "./helpers/test-helpers";
9+
10+
const openIconPicker = async (page: Page) => {
11+
await page.getByRole("button", { name: /open icon picker/i }).click();
12+
};
13+
14+
const getIconPickerPopover = (page: Page) => {
15+
return page.getByTestId("icon-picker-popover");
16+
};
17+
18+
const getIconSearchInput = (page: Page) => {
19+
return page.getByPlaceholder(/search icons/i);
20+
};
21+
22+
const getIconPickerButton = (page: Page) => {
23+
return page.getByRole("button", { name: /open icon picker/i });
24+
};
25+
26+
const getDisplayTextInput = (page: Page) => {
27+
return page.getByPlaceholder(/terminal.*build.*deploy/i);
28+
};
29+
30+
const getIconByName = (page: Page, iconName: string) => {
31+
return page.locator(`button[title='${iconName}']`);
32+
};
33+
34+
test.describe("Icon Picker Full Workflow", () => {
35+
test.beforeEach(async ({ page }) => {
36+
await page.goto("/");
37+
await clearAllCommands(page);
38+
});
39+
40+
test("should complete full icon picker workflow (search, select, change, clear)", async ({
41+
page,
42+
}) => {
43+
// Step 1: Open add command dialog
44+
await openAddCommandDialog(page);
45+
46+
// Step 2: Open icon picker
47+
await openIconPicker(page);
48+
await expect(getIconPickerPopover(page)).toBeVisible();
49+
50+
// Step 3: Search for "terminal"
51+
await getIconSearchInput(page).fill("terminal");
52+
53+
// Step 4: Select terminal icon
54+
await getIconByName(page, "terminal").click();
55+
await expect(getIconPickerPopover(page)).toBeHidden();
56+
57+
// Verify icon is shown in button
58+
await expect(getIconPickerButton(page).locator(".codicon-terminal")).toBeVisible();
59+
60+
// Step 5: Reopen picker and click "Show all" to load all icons
61+
await openIconPicker(page);
62+
const showAllButton = page.getByRole("button", { name: /show all.*icons/i });
63+
64+
// Only click if visible (might already be showing all)
65+
if (await showAllButton.isVisible()) {
66+
await showAllButton.click();
67+
}
68+
69+
// Wait for all icons to load
70+
await page.waitForTimeout(300);
71+
72+
// Step 6: Change to different icon (folder)
73+
await getIconSearchInput(page).fill("folder");
74+
await getIconByName(page, "folder").click();
75+
76+
// Verify icon changed
77+
await expect(getIconPickerButton(page).locator(".codicon-folder")).toBeVisible();
78+
79+
// Step 7: Remove icon with "Clear" button
80+
await openIconPicker(page);
81+
82+
// Look for clear button (might be "Clear" or "Delete" or similar)
83+
const clearButton = page.getByRole("button", { name: /clear|delete|remove/i }).first();
84+
if (await clearButton.isVisible()) {
85+
await clearButton.click();
86+
87+
// Verify icon is removed (button shows "Icon" text)
88+
await expect(getIconPickerButton(page)).toContainText(/icon/i);
89+
}
90+
91+
// Step 8: Save command without icon
92+
await getDisplayTextInput(page).fill("No Icon Command");
93+
await fillCommandForm(page, { command: "echo test" });
94+
await saveCommandDialog(page);
95+
96+
// Verify command saved
97+
const commandCard = page.locator('[data-testid="command-card"]', {
98+
hasText: "No Icon Command",
99+
});
100+
await expect(commandCard).toBeVisible();
101+
});
102+
103+
test("should show selected badge for currently selected icon", async ({ page }) => {
104+
// Open add command dialog and select an icon
105+
await openAddCommandDialog(page);
106+
await openIconPicker(page);
107+
await getIconSearchInput(page).fill("terminal");
108+
await getIconByName(page, "terminal").click();
109+
110+
// Reopen icon picker
111+
await openIconPicker(page);
112+
113+
// Search for the same icon
114+
await getIconSearchInput(page).fill("terminal");
115+
116+
// The selected icon should have a "Selected" badge or highlight
117+
const terminalButton = getIconByName(page, "terminal");
118+
await expect(terminalButton).toHaveClass(/bg-accent/);
119+
120+
// Close picker
121+
await page.keyboard.press("Escape");
122+
});
123+
124+
test("should maintain icon selection when form has validation errors", async ({ page }) => {
125+
// Open add command dialog and select an icon
126+
await openAddCommandDialog(page);
127+
await openIconPicker(page);
128+
await getIconSearchInput(page).fill("rocket");
129+
await getIconByName(page, "rocket").click();
130+
131+
// Try to save without filling required fields (should trigger validation)
132+
await saveCommandDialog(page);
133+
134+
// Icon should still be visible in picker button after validation error
135+
await expect(getIconPickerButton(page).locator(".codicon-rocket")).toBeVisible();
136+
});
137+
138+
test("should filter icons with partial search terms", async ({ page }) => {
139+
await openAddCommandDialog(page);
140+
await openIconPicker(page);
141+
142+
// Search with partial term
143+
await getIconSearchInput(page).fill("git");
144+
145+
// Should find git-related icons
146+
await expect(page.locator("button[title*='git']").first()).toBeVisible();
147+
148+
// Clear search
149+
await getIconSearchInput(page).clear();
150+
151+
// Should show initial icons again
152+
const iconButtons = page.getByTestId("icon-grid-button");
153+
await expect(iconButtons.first()).toBeVisible();
154+
});
155+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { expect, test } from "@playwright/test";
2+
import type { Page } from "@playwright/test";
3+
4+
import { fillCommandForm, openAddCommandDialog, saveCommandDialog } from "./helpers/test-helpers";
5+
6+
const BUTTON_NAMES = {
7+
SWITCH_TO_GLOBAL: /Global scope/,
8+
SWITCH_TO_LOCAL: /Local scope/,
9+
SWITCH_TO_WORKSPACE: /Workspace scope/,
10+
} as const;
11+
12+
const switchToGlobal = async (page: Page) => {
13+
await page.getByRole("radio", { name: BUTTON_NAMES.SWITCH_TO_GLOBAL }).click();
14+
};
15+
16+
const switchToWorkspace = async (page: Page) => {
17+
await page.getByRole("radio", { name: BUTTON_NAMES.SWITCH_TO_WORKSPACE }).click();
18+
};
19+
20+
const switchToLocal = async (page: Page) => {
21+
await page.getByRole("radio", { name: BUTTON_NAMES.SWITCH_TO_LOCAL }).click();
22+
};
23+
24+
test.describe("Multiple Scope Data Isolation", () => {
25+
test.beforeEach(async ({ page }) => {
26+
await page.goto("/");
27+
});
28+
29+
test("each scope maintains isolated command data across all three scopes", async ({ page }) => {
30+
// Step 1: Add "Workspace Cmd" in Workspace scope
31+
await openAddCommandDialog(page);
32+
await fillCommandForm(page, { name: "Workspace Cmd", command: "echo workspace" });
33+
await saveCommandDialog(page);
34+
35+
// Save and switch to Global
36+
await switchToGlobal(page);
37+
await page.getByRole("button", { name: "Save & Switch" }).click();
38+
39+
// Step 2: Add "Global Cmd" in Global scope
40+
await openAddCommandDialog(page);
41+
await fillCommandForm(page, { name: "Global Cmd", command: "echo global" });
42+
await saveCommandDialog(page);
43+
44+
// Verify only Global Cmd is visible (not Workspace Cmd)
45+
await expect(page.getByText("Global Cmd")).toBeVisible();
46+
await expect(page.getByText("Workspace Cmd")).not.toBeVisible();
47+
48+
// Save and switch to Local
49+
await switchToLocal(page);
50+
await page.getByRole("button", { name: "Save & Switch" }).click();
51+
52+
// Step 3: Add "Local Cmd" in Local scope
53+
await openAddCommandDialog(page);
54+
await fillCommandForm(page, { name: "Local Cmd", command: "echo local" });
55+
await saveCommandDialog(page);
56+
57+
// Verify only Local Cmd is visible
58+
await expect(page.getByText("Local Cmd")).toBeVisible();
59+
await expect(page.getByText("Global Cmd")).not.toBeVisible();
60+
await expect(page.getByText("Workspace Cmd")).not.toBeVisible();
61+
62+
// Save and switch to Workspace to verify isolation
63+
await switchToWorkspace(page);
64+
await page.getByRole("button", { name: "Save & Switch" }).click();
65+
66+
// Verify only Workspace Cmd is visible
67+
await expect(page.getByText("Workspace Cmd")).toBeVisible();
68+
await expect(page.getByText("Global Cmd")).not.toBeVisible();
69+
await expect(page.getByText("Local Cmd")).not.toBeVisible();
70+
71+
// Switch to Global to verify
72+
await switchToGlobal(page);
73+
await expect(page.getByText("Global Cmd")).toBeVisible();
74+
await expect(page.getByText("Workspace Cmd")).not.toBeVisible();
75+
await expect(page.getByText("Local Cmd")).not.toBeVisible();
76+
77+
// Switch to Local to verify
78+
await switchToLocal(page);
79+
await expect(page.getByText("Local Cmd")).toBeVisible();
80+
await expect(page.getByText("Global Cmd")).not.toBeVisible();
81+
await expect(page.getByText("Workspace Cmd")).not.toBeVisible();
82+
});
83+
});

0 commit comments

Comments
 (0)