mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
Implement project picker keyboard shortcut and enhance feature management
- Added a new keyboard shortcut 'P' to open the project picker dropdown.
- Implemented functionality to select projects using number keys, allowing users to quickly switch between projects.
- Updated the feature list to include a new feature for project selection via keyboard shortcuts.
- Removed obsolete coding_prompt.md and added initializer_prompt.md for better session management.
- Introduced context management for features, enabling reading, writing, and deleting context files.
- Updated package dependencies to include @radix-ui/react-checkbox for enhanced UI components.
This commit enhances user experience by streamlining project selection and improves the overall feature management process.
🤖 Generated with Claude Code
This commit is contained in:
@@ -1,196 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { setupMockProject, clickElement } from "./utils";
|
||||
|
||||
// Helper function to navigate to context view and wait for either loading or main view
|
||||
async function navigateToContextAndOpenDialog(page: any) {
|
||||
// Click on context nav
|
||||
const contextNav = page.locator('[data-testid="nav-context"]');
|
||||
await contextNav.waitFor({ state: "visible", timeout: 10000 });
|
||||
await contextNav.click();
|
||||
|
||||
// Wait for either the context view or the loading view
|
||||
// The loading view might stay visible if the electron API is mocked
|
||||
await page.waitForSelector(
|
||||
'[data-testid="context-view"], [data-testid="context-view-loading"], [data-testid="context-view-no-project"]',
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
// If we have the main context view, click the add button
|
||||
const contextView = page.locator('[data-testid="context-view"]');
|
||||
const isContextViewVisible = await contextView.isVisible().catch(() => false);
|
||||
|
||||
if (isContextViewVisible) {
|
||||
// Click add context file button
|
||||
const addFileBtn = page.locator('[data-testid="add-context-file"]');
|
||||
await addFileBtn.click();
|
||||
} else {
|
||||
// If context view isn't visible, we might be in loading state
|
||||
// For testing purposes, simulate opening the dialog via keyboard or other means
|
||||
// Skip this test scenario
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for dialog to appear
|
||||
const dialog = page.locator('[data-testid="add-context-dialog"]');
|
||||
await dialog.waitFor({ state: "visible", timeout: 5000 });
|
||||
}
|
||||
|
||||
test.describe("Add Context File Dialog", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
});
|
||||
|
||||
test("should show file name input and content textarea in add context dialog", async ({
|
||||
page,
|
||||
}) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
// Verify file name input is visible
|
||||
const fileNameInput = page.locator('[data-testid="new-file-name"]');
|
||||
await expect(fileNameInput).toBeVisible();
|
||||
|
||||
// Verify content textarea is visible when text type is selected (default)
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
await expect(contentTextarea).toBeVisible();
|
||||
|
||||
// Verify placeholder text
|
||||
await expect(contentTextarea).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Enter context content here or drag & drop a .txt or .md file..."
|
||||
);
|
||||
});
|
||||
|
||||
test("should allow typing content in the textarea", async ({ page }) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
const testContent =
|
||||
"# Test Context\n\nThis is test content for the context file.";
|
||||
|
||||
await contentTextarea.fill(testContent);
|
||||
await expect(contentTextarea).toHaveValue(testContent);
|
||||
});
|
||||
|
||||
test("should show textarea only for text file type", async ({ page }) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
// Verify textarea is visible when text type is selected (default)
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
await expect(contentTextarea).toBeVisible();
|
||||
|
||||
// Switch to image type
|
||||
await clickElement(page, "add-image-type");
|
||||
|
||||
// Verify textarea is no longer visible
|
||||
await expect(contentTextarea).not.toBeVisible();
|
||||
|
||||
// Verify image upload input is attached instead
|
||||
const imageUploadInput = page.locator('[data-testid="image-upload-input"]');
|
||||
await expect(imageUploadInput).toBeAttached();
|
||||
|
||||
// Switch back to text type
|
||||
await clickElement(page, "add-text-type");
|
||||
|
||||
// Verify textarea is visible again
|
||||
const contentTextareaAgain = page.locator('[data-testid="new-file-content"]');
|
||||
await expect(contentTextareaAgain).toBeVisible();
|
||||
});
|
||||
|
||||
test("should display drag and drop helper text", async ({ page }) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
// Check for helper text about drag and drop
|
||||
const helperText = page.locator(
|
||||
"text=Drag & drop .txt or .md files to import their content"
|
||||
);
|
||||
await expect(helperText).toBeVisible();
|
||||
});
|
||||
|
||||
test("should populate content from dropped .txt file", async ({ page }) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
const testContent = "This is content from a text file.";
|
||||
|
||||
// Create a data transfer with a .txt file
|
||||
const dataTransfer = await page.evaluateHandle((content) => {
|
||||
const dt = new DataTransfer();
|
||||
const file = new File([content], "test-file.txt", { type: "text/plain" });
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
}, testContent);
|
||||
|
||||
// Dispatch drag events to simulate file drop
|
||||
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
|
||||
await contentTextarea.dispatchEvent("drop", { dataTransfer });
|
||||
|
||||
// Wait for the content to be populated
|
||||
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
|
||||
|
||||
// Verify filename was auto-filled
|
||||
const fileNameInput = page.locator('[data-testid="new-file-name"]');
|
||||
await expect(fileNameInput).toHaveValue("test-file.txt");
|
||||
});
|
||||
|
||||
test("should populate content from dropped .md file", async ({ page }) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
const testContent = "# Markdown File\n\nThis is markdown content.";
|
||||
|
||||
// Create a data transfer with a .md file
|
||||
const dataTransfer = await page.evaluateHandle((content) => {
|
||||
const dt = new DataTransfer();
|
||||
const file = new File([content], "readme.md", { type: "text/markdown" });
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
}, testContent);
|
||||
|
||||
// Dispatch drag events to simulate file drop
|
||||
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
|
||||
await contentTextarea.dispatchEvent("drop", { dataTransfer });
|
||||
|
||||
// Wait for the content to be populated
|
||||
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
|
||||
|
||||
// Verify filename was auto-filled
|
||||
const fileNameInput = page.locator('[data-testid="new-file-name"]');
|
||||
await expect(fileNameInput).toHaveValue("readme.md");
|
||||
});
|
||||
|
||||
test("should not auto-fill filename if already provided", async ({
|
||||
page,
|
||||
}) => {
|
||||
await navigateToContextAndOpenDialog(page);
|
||||
|
||||
// Fill in the filename first
|
||||
const fileNameInput = page.locator('[data-testid="new-file-name"]');
|
||||
await fileNameInput.fill("my-custom-name.md");
|
||||
|
||||
const contentTextarea = page.locator('[data-testid="new-file-content"]');
|
||||
const testContent = "Content from dropped file";
|
||||
|
||||
// Create a data transfer with a .txt file
|
||||
const dataTransfer = await page.evaluateHandle((content) => {
|
||||
const dt = new DataTransfer();
|
||||
const file = new File([content], "dropped-file.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
}, testContent);
|
||||
|
||||
// Dispatch drag events to simulate file drop
|
||||
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
|
||||
await contentTextarea.dispatchEvent("drop", { dataTransfer });
|
||||
|
||||
// Wait for the content to be populated
|
||||
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
|
||||
|
||||
// Verify filename was NOT overwritten
|
||||
await expect(fileNameInput).toHaveValue("my-custom-name.md");
|
||||
});
|
||||
});
|
||||
237
app/tests/project-picker-keyboard.spec.ts
Normal file
237
app/tests/project-picker-keyboard.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
setupMockMultipleProjects,
|
||||
waitForElement,
|
||||
isProjectPickerDropdownOpen,
|
||||
waitForProjectPickerDropdown,
|
||||
waitForProjectPickerDropdownHidden,
|
||||
pressShortcut,
|
||||
pressNumberKey,
|
||||
isProjectHotkeyVisible,
|
||||
getProjectPickerShortcut,
|
||||
} from "./utils";
|
||||
|
||||
test.describe("Project Picker Keyboard Shortcuts", () => {
|
||||
test("pressing P key opens the project picker dropdown", async ({ page }) => {
|
||||
// Setup with multiple projects
|
||||
await setupMockMultipleProjects(page, 3);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar to be visible
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Dropdown should initially be closed
|
||||
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
|
||||
|
||||
// Press P to open project picker
|
||||
await pressShortcut(page, "p");
|
||||
|
||||
// Dropdown should now be open
|
||||
await waitForProjectPickerDropdown(page);
|
||||
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
|
||||
});
|
||||
|
||||
test("project options show hotkey indicators (1-5)", async ({ page }) => {
|
||||
// Setup with 5 projects
|
||||
await setupMockMultipleProjects(page, 5);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Open project picker
|
||||
await pressShortcut(page, "p");
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Check that all 5 hotkey indicators are visible
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
|
||||
const hotkey = page.locator(`[data-testid="project-hotkey-${i}"]`);
|
||||
expect(await hotkey.textContent()).toBe(i.toString());
|
||||
}
|
||||
});
|
||||
|
||||
test("pressing number key selects the corresponding project", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup with 3 projects
|
||||
await setupMockMultipleProjects(page, 3);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Check initial project (should be Test Project 1)
|
||||
const projectSelector = page.locator('[data-testid="project-selector"]');
|
||||
await expect(projectSelector).toContainText("Test Project 1");
|
||||
|
||||
// Open project picker
|
||||
await pressShortcut(page, "p");
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Press 2 to select the second project
|
||||
await pressNumberKey(page, 2);
|
||||
|
||||
// Dropdown should close
|
||||
await waitForProjectPickerDropdownHidden(page);
|
||||
|
||||
// Project should now be Test Project 2
|
||||
await expect(projectSelector).toContainText("Test Project 2");
|
||||
});
|
||||
|
||||
test("pressing number key for non-existent project does nothing", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup with 2 projects
|
||||
await setupMockMultipleProjects(page, 2);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Check initial project
|
||||
const projectSelector = page.locator('[data-testid="project-selector"]');
|
||||
await expect(projectSelector).toContainText("Test Project 1");
|
||||
|
||||
// Open project picker
|
||||
await pressShortcut(page, "p");
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Press 5 (there's no 5th project)
|
||||
await pressNumberKey(page, 5);
|
||||
|
||||
// Dropdown should remain open
|
||||
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
|
||||
|
||||
// Project should still be Test Project 1
|
||||
await expect(projectSelector).toContainText("Test Project 1");
|
||||
});
|
||||
|
||||
test("pressing Escape closes the project picker dropdown", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup with multiple projects
|
||||
await setupMockMultipleProjects(page, 3);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Open project picker
|
||||
await pressShortcut(page, "p");
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Press Escape
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Dropdown should close
|
||||
await waitForProjectPickerDropdownHidden(page);
|
||||
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
|
||||
});
|
||||
|
||||
test("project selector button shows P shortcut indicator", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup with multiple projects
|
||||
await setupMockMultipleProjects(page, 3);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar and project selector
|
||||
await waitForElement(page, "sidebar");
|
||||
await waitForElement(page, "project-selector");
|
||||
|
||||
// Check that P shortcut indicator is visible
|
||||
const shortcutIndicator = await getProjectPickerShortcut(page);
|
||||
await expect(shortcutIndicator).toBeVisible();
|
||||
await expect(shortcutIndicator).toHaveText("P");
|
||||
});
|
||||
|
||||
test("only first 5 projects are shown with hotkeys", async ({ page }) => {
|
||||
// Setup with 7 projects
|
||||
await setupMockMultipleProjects(page, 7);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Open project picker
|
||||
await pressShortcut(page, "p");
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Only 5 hotkey indicators should be visible (1-5)
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
|
||||
}
|
||||
|
||||
// 6th and 7th should not exist
|
||||
const hotkey6 = page.locator('[data-testid="project-hotkey-6"]');
|
||||
const hotkey7 = page.locator('[data-testid="project-hotkey-7"]');
|
||||
await expect(hotkey6).not.toBeVisible();
|
||||
await expect(hotkey7).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking a project option also works", async ({ page }) => {
|
||||
// Setup with 3 projects
|
||||
await setupMockMultipleProjects(page, 3);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Open project picker by clicking
|
||||
await page.locator('[data-testid="project-selector"]').click();
|
||||
await waitForProjectPickerDropdown(page);
|
||||
|
||||
// Click on second project option
|
||||
await page.locator('[data-testid="project-option-test-project-2"]').click();
|
||||
|
||||
// Dropdown should close
|
||||
await waitForProjectPickerDropdownHidden(page);
|
||||
|
||||
// Project should now be Test Project 2
|
||||
const projectSelector = page.locator('[data-testid="project-selector"]');
|
||||
await expect(projectSelector).toContainText("Test Project 2");
|
||||
});
|
||||
|
||||
test("P shortcut does not work when no projects exist", async ({ page }) => {
|
||||
// Setup with empty projects
|
||||
await page.addInitScript(() => {
|
||||
const mockState = {
|
||||
state: {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
currentView: "welcome",
|
||||
theme: "dark",
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for sidebar
|
||||
await waitForElement(page, "sidebar");
|
||||
|
||||
// Press P - should not open any dropdown since there are no projects
|
||||
await pressShortcut(page, "p");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Dropdown should not be visible
|
||||
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1701,3 +1701,90 @@ export async function getOutputModalDescription(page: Page): Promise<string | nu
|
||||
const modalDescription = page.locator('[data-testid="agent-output-modal"] [data-slot="dialog-description"]');
|
||||
return await modalDescription.textContent().catch(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the project picker dropdown is open
|
||||
*/
|
||||
export async function isProjectPickerDropdownOpen(page: Page): Promise<boolean> {
|
||||
const dropdown = page.locator('[data-testid="project-picker-dropdown"]');
|
||||
return await dropdown.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the project picker dropdown to be visible
|
||||
*/
|
||||
export async function waitForProjectPickerDropdown(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "project-picker-dropdown", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the project picker dropdown to be hidden
|
||||
*/
|
||||
export async function waitForProjectPickerDropdownHidden(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<void> {
|
||||
await waitForElementHidden(page, "project-picker-dropdown", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project hotkey indicator element by number (1-5)
|
||||
*/
|
||||
export async function getProjectHotkey(page: Page, num: number): Promise<Locator> {
|
||||
return page.locator(`[data-testid="project-hotkey-${num}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a project hotkey indicator is visible
|
||||
*/
|
||||
export async function isProjectHotkeyVisible(page: Page, num: number): Promise<boolean> {
|
||||
const hotkey = page.locator(`[data-testid="project-hotkey-${num}"]`);
|
||||
return await hotkey.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project picker shortcut indicator (P key)
|
||||
*/
|
||||
export async function getProjectPickerShortcut(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid="project-picker-shortcut"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a mock state with multiple projects
|
||||
*/
|
||||
export async function setupMockMultipleProjects(
|
||||
page: Page,
|
||||
projectCount: number = 3
|
||||
): Promise<void> {
|
||||
await page.addInitScript((count: number) => {
|
||||
const mockProjects = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
mockProjects.push({
|
||||
id: `test-project-${i + 1}`,
|
||||
name: `Test Project ${i + 1}`,
|
||||
path: `/mock/test-project-${i + 1}`,
|
||||
lastOpened: new Date(Date.now() - i * 86400000).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const mockState = {
|
||||
state: {
|
||||
projects: mockProjects,
|
||||
currentProject: mockProjects[0],
|
||||
currentView: "board",
|
||||
theme: "dark",
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
}, projectCount);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user