style: fix formatting with Prettier

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:31:57 -05:00
parent 584f5a3426
commit 8d578558ff
295 changed files with 9088 additions and 10546 deletions

View File

@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
resetContextDirectory,
createContextFileOnDisk,
@@ -19,19 +19,18 @@ import {
fillInput,
getByTestId,
waitForNetworkIdle,
} from "./utils";
} from './utils';
const WORKSPACE_ROOT = path.resolve(process.cwd(), "../..");
const TEST_IMAGE_SRC = path.join(WORKSPACE_ROOT, "apps/ui/public/logo.png");
const WORKSPACE_ROOT = path.resolve(process.cwd(), '../..');
const TEST_IMAGE_SRC = path.join(WORKSPACE_ROOT, 'apps/ui/public/logo.png');
// Configure all tests to run serially to prevent interference with shared context directory
test.describe.configure({ mode: "serial" });
test.describe.configure({ mode: 'serial' });
// ============================================================================
// Test Suite 1: Context View - File Management
// ============================================================================
test.describe("Context View - File Management", () => {
test.describe('Context View - File Management', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
@@ -40,31 +39,31 @@ test.describe("Context View - File Management", () => {
resetContextDirectory();
});
test("should create a new MD context file", async ({ page }) => {
test('should create a new MD context file', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click Add File button
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
// Select text type (should be default)
await clickElement(page, "add-text-type");
await clickElement(page, 'add-text-type');
// Enter filename
await fillInput(page, "new-file-name", "test-context.md");
await fillInput(page, 'new-file-name', 'test-context.md');
// Enter content
const testContent = "# Test Context\n\nThis is test content";
await fillInput(page, "new-file-content", testContent);
const testContent = '# Test Context\n\nThis is test content';
await fillInput(page, 'new-file-content', testContent);
// Click confirm
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -73,14 +72,14 @@ test.describe("Context View - File Management", () => {
);
// Wait for file list to refresh (file should appear)
await waitForContextFile(page, "test-context.md", 10000);
await waitForContextFile(page, 'test-context.md', 10000);
// Verify file appears in list
const fileButton = await getByTestId(page, "context-file-test-context.md");
const fileButton = await getByTestId(page, 'context-file-test-context.md');
await expect(fileButton).toBeVisible();
// Click on the file and wait for it to be selected
await selectContextFile(page, "test-context.md");
await selectContextFile(page, 'test-context.md');
// Wait for content to load
await waitForFileContentToLoad(page);
@@ -98,19 +97,19 @@ test.describe("Context View - File Management", () => {
expect(editorContent).toBe(testContent);
});
test("should edit an existing MD context file", async ({ page }) => {
test('should edit an existing MD context file', async ({ page }) => {
// Create a test file on disk first
const originalContent = "# Original Content\n\nThis will be edited.";
createContextFileOnDisk("edit-test.md", originalContent);
const originalContent = '# Original Content\n\nThis will be edited.';
createContextFileOnDisk('edit-test.md', originalContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the existing file and wait for it to be selected
await selectContextFile(page, "edit-test.md");
await selectContextFile(page, 'edit-test.md');
// Wait for file content to load
await waitForFileContentToLoad(page);
@@ -124,18 +123,16 @@ test.describe("Context View - File Management", () => {
});
// Modify content
const newContent = "# Modified Content\n\nThis has been edited.";
const newContent = '# Modified Content\n\nThis has been edited.';
await setContextEditorContent(page, newContent);
// Click save
await clickElement(page, "save-context-file");
await clickElement(page, 'save-context-file');
// Wait for save to complete
await page.waitForFunction(
() =>
document
.querySelector('[data-testid="save-context-file"]')
?.textContent?.includes("Saved"),
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
{ timeout: 5000 }
);
@@ -147,7 +144,7 @@ test.describe("Context View - File Management", () => {
await navigateToContext(page);
// Wait for file to appear after reload and select it
await selectContextFile(page, "edit-test.md");
await selectContextFile(page, 'edit-test.md');
// Wait for content to load
await waitForFileContentToLoad(page);
@@ -164,23 +161,23 @@ test.describe("Context View - File Management", () => {
expect(persistedContent).toBe(newContent);
});
test("should remove an MD context file", async ({ page }) => {
test('should remove an MD context file', async ({ page }) => {
// Create a test file on disk first
createContextFileOnDisk("delete-test.md", "# Delete Me");
createContextFileOnDisk('delete-test.md', '# Delete Me');
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the file to select it
const fileButton = await getByTestId(page, "context-file-delete-test.md");
await fileButton.waitFor({ state: "visible", timeout: 5000 });
const fileButton = await getByTestId(page, 'context-file-delete-test.md');
await fileButton.waitFor({ state: 'visible', timeout: 5000 });
await fileButton.click();
// Click delete button
await clickElement(page, "delete-context-file");
await clickElement(page, 'delete-context-file');
// Wait for delete dialog
await page.waitForSelector('[data-testid="delete-context-dialog"]', {
@@ -188,7 +185,7 @@ test.describe("Context View - File Management", () => {
});
// Confirm deletion
await clickElement(page, "confirm-delete-file");
await clickElement(page, 'confirm-delete-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -197,44 +194,41 @@ test.describe("Context View - File Management", () => {
);
// Verify file is removed from list
const deletedFile = await getByTestId(page, "context-file-delete-test.md");
const deletedFile = await getByTestId(page, 'context-file-delete-test.md');
await expect(deletedFile).not.toBeVisible();
// Verify file is removed from disk
expect(contextFileExistsOnDisk("delete-test.md")).toBe(false);
expect(contextFileExistsOnDisk('delete-test.md')).toBe(false);
});
test("should upload an image context file", async ({ page }) => {
test('should upload an image context file', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click Add File button
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
// Select image type
await clickElement(page, "add-image-type");
await clickElement(page, 'add-image-type');
// Enter filename
await fillInput(page, "new-file-name", "test-image.png");
await fillInput(page, 'new-file-name', 'test-image.png');
// Upload image using file input
await page.setInputFiles(
'[data-testid="image-upload-input"]',
TEST_IMAGE_SRC
);
await page.setInputFiles('[data-testid="image-upload-input"]', TEST_IMAGE_SRC);
// Wait for image preview to appear (indicates upload success)
const addDialog = await getByTestId(page, "add-context-dialog");
await addDialog.locator("img").waitFor({ state: "visible" });
const addDialog = await getByTestId(page, 'add-context-dialog');
await addDialog.locator('img').waitFor({ state: 'visible' });
// Click confirm
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -243,7 +237,7 @@ test.describe("Context View - File Management", () => {
);
// Verify file appears in list
const fileButton = await getByTestId(page, "context-file-test-image.png");
const fileButton = await getByTestId(page, 'context-file-test-image.png');
await expect(fileButton).toBeVisible();
// Click on the image to view it
@@ -253,31 +247,31 @@ test.describe("Context View - File Management", () => {
await page.waitForSelector('[data-testid="image-preview"]', {
timeout: 5000,
});
const imagePreview = await getByTestId(page, "image-preview");
const imagePreview = await getByTestId(page, 'image-preview');
await expect(imagePreview).toBeVisible();
});
test("should remove an image context file", async ({ page }) => {
test('should remove an image context file', async ({ page }) => {
// Create a test image file on disk as base64 data URL (matching app's storage format)
const imageContent = fs.readFileSync(TEST_IMAGE_SRC);
const base64DataUrl = `data:image/png;base64,${imageContent.toString("base64")}`;
const contextPath = path.join(getFixturePath(), ".automaker/context");
fs.writeFileSync(path.join(contextPath, "delete-image.png"), base64DataUrl);
const base64DataUrl = `data:image/png;base64,${imageContent.toString('base64')}`;
const contextPath = path.join(getFixturePath(), '.automaker/context');
fs.writeFileSync(path.join(contextPath, 'delete-image.png'), base64DataUrl);
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Wait for the image file and select it
await selectContextFile(page, "delete-image.png");
await selectContextFile(page, 'delete-image.png');
// Wait for file content (image preview) to load
await waitForFileContentToLoad(page);
// Click delete button
await clickElement(page, "delete-context-file");
await clickElement(page, 'delete-context-file');
// Wait for delete dialog
await page.waitForSelector('[data-testid="delete-context-dialog"]', {
@@ -285,7 +279,7 @@ test.describe("Context View - File Management", () => {
});
// Confirm deletion
await clickElement(page, "confirm-delete-file");
await clickElement(page, 'confirm-delete-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -294,37 +288,37 @@ test.describe("Context View - File Management", () => {
);
// Verify file is removed from list
const deletedImageFile = await getByTestId(page, "context-file-delete-image.png");
const deletedImageFile = await getByTestId(page, 'context-file-delete-image.png');
await expect(deletedImageFile).not.toBeVisible();
});
test("should toggle markdown preview mode", async ({ page }) => {
test('should toggle markdown preview mode', async ({ page }) => {
// Create a markdown file with content
const mdContent =
"# Heading\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2";
createContextFileOnDisk("preview-test.md", mdContent);
'# Heading\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2';
createContextFileOnDisk('preview-test.md', mdContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the markdown file
const fileButton = await getByTestId(page, "context-file-preview-test.md");
await fileButton.waitFor({ state: "visible", timeout: 5000 });
const fileButton = await getByTestId(page, 'context-file-preview-test.md');
await fileButton.waitFor({ state: 'visible', timeout: 5000 });
await fileButton.click();
// Wait for content to load (markdown files open in preview mode by default)
await waitForFileContentToLoad(page);
// Check if preview button is visible (indicates it's a markdown file)
const previewToggle = await getByTestId(page, "toggle-preview-mode");
const previewToggle = await getByTestId(page, 'toggle-preview-mode');
await expect(previewToggle).toBeVisible();
// Markdown files always open in preview mode by default (see context-view.tsx:163)
// Verify we're in preview mode
const markdownPreview = await getByTestId(page, "markdown-preview");
const markdownPreview = await getByTestId(page, 'markdown-preview');
await expect(markdownPreview).toBeVisible();
// Click to switch to edit mode
@@ -334,7 +328,7 @@ test.describe("Context View - File Management", () => {
});
// Verify editor is shown
const editor = await getByTestId(page, "context-editor");
const editor = await getByTestId(page, 'context-editor');
await expect(editor).toBeVisible();
await expect(markdownPreview).not.toBeVisible();
@@ -352,7 +346,7 @@ test.describe("Context View - File Management", () => {
// ============================================================================
// Test Suite 2: Context View - Drag and Drop
// ============================================================================
test.describe("Context View - Drag and Drop", () => {
test.describe('Context View - Drag and Drop', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
@@ -361,36 +355,34 @@ test.describe("Context View - Drag and Drop", () => {
resetContextDirectory();
});
test("should handle drag and drop of MD file onto textarea in add dialog", async ({
page,
}) => {
test('should handle drag and drop of MD file onto textarea in add dialog', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Open add file dialog
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
// Ensure text type is selected
await clickElement(page, "add-text-type");
await clickElement(page, 'add-text-type');
// Simulate drag and drop of a .md file onto the textarea
const droppedContent = "# Dropped Content\n\nThis was dragged and dropped.";
const droppedContent = '# Dropped Content\n\nThis was dragged and dropped.';
await simulateFileDrop(
page,
'[data-testid="new-file-content"]',
"dropped-file.md",
'dropped-file.md',
droppedContent
);
// Wait for content to be populated in textarea
const textarea = await getByTestId(page, "new-file-content");
await textarea.waitFor({ state: "visible" });
const textarea = await getByTestId(page, 'new-file-content');
await textarea.waitFor({ state: 'visible' });
await expect(textarea).toHaveValue(droppedContent);
// Verify content is populated in textarea
@@ -398,13 +390,11 @@ test.describe("Context View - Drag and Drop", () => {
expect(textareaContent).toBe(droppedContent);
// Verify filename is auto-filled
const filenameValue = await page
.locator('[data-testid="new-file-name"]')
.inputValue();
expect(filenameValue).toBe("dropped-file.md");
const filenameValue = await page.locator('[data-testid="new-file-name"]').inputValue();
expect(filenameValue).toBe('dropped-file.md');
// Confirm and create the file
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -413,15 +403,13 @@ test.describe("Context View - Drag and Drop", () => {
);
// Verify file was created
const droppedFile = await getByTestId(page, "context-file-dropped-file.md");
const droppedFile = await getByTestId(page, 'context-file-dropped-file.md');
await expect(droppedFile).toBeVisible();
});
test("should handle drag and drop of file onto main view", async ({
page,
}) => {
test('should handle drag and drop of file onto main view', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
@@ -432,19 +420,19 @@ test.describe("Context View - Drag and Drop", () => {
});
// Simulate drag and drop onto the drop zone
const droppedContent = "This is a text file dropped onto the main view.";
const droppedContent = 'This is a text file dropped onto the main view.';
await simulateFileDrop(
page,
'[data-testid="context-drop-zone"]',
"main-drop.txt",
'main-drop.txt',
droppedContent
);
// Wait for file to appear in the list (drag-drop triggers file creation)
await waitForContextFile(page, "main-drop.txt", 15000);
await waitForContextFile(page, 'main-drop.txt', 15000);
// Verify file appears in the file list
const fileButton = await getByTestId(page, "context-file-main-drop.txt");
const fileButton = await getByTestId(page, 'context-file-main-drop.txt');
await expect(fileButton).toBeVisible();
// Select file and verify content
@@ -461,7 +449,7 @@ test.describe("Context View - Drag and Drop", () => {
// ============================================================================
// Test Suite 3: Context View - Edge Cases
// ============================================================================
test.describe("Context View - Edge Cases", () => {
test.describe('Context View - Edge Cases', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
@@ -470,33 +458,31 @@ test.describe("Context View - Edge Cases", () => {
resetContextDirectory();
});
test("should handle duplicate filename (overwrite behavior)", async ({
page,
}) => {
test('should handle duplicate filename (overwrite behavior)', async ({ page }) => {
// Create an existing file
createContextFileOnDisk("test.md", "# Original Content");
createContextFileOnDisk('test.md', '# Original Content');
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Verify the original file exists
const originalFile = await getByTestId(page, "context-file-test.md");
const originalFile = await getByTestId(page, 'context-file-test.md');
await expect(originalFile).toBeVisible();
// Try to create another file with the same name
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
await clickElement(page, "add-text-type");
await fillInput(page, "new-file-name", "test.md");
await fillInput(page, "new-file-content", "# New Content - Overwritten");
await clickElement(page, 'add-text-type');
await fillInput(page, 'new-file-name', 'test.md');
await fillInput(page, 'new-file-content', '# New Content - Overwritten');
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
// Wait for dialog to close
await page.waitForFunction(
@@ -521,54 +507,54 @@ test.describe("Context View - Edge Cases", () => {
});
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe("# New Content - Overwritten");
expect(editorContent).toBe('# New Content - Overwritten');
});
test("should handle special characters in filename", async ({ page }) => {
test('should handle special characters in filename', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Test file with parentheses
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
await clickElement(page, "add-text-type");
await fillInput(page, "new-file-name", "context (1).md");
await fillInput(page, "new-file-content", "Content with parentheses in filename");
await clickElement(page, 'add-text-type');
await fillInput(page, 'new-file-name', 'context (1).md');
await fillInput(page, 'new-file-content', 'Content with parentheses in filename');
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
await page.waitForFunction(
() => !document.querySelector('[data-testid="add-context-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created - use CSS escape for special characters
const fileWithParens = await getByTestId(page, "context-file-context (1).md");
const fileWithParens = await getByTestId(page, 'context-file-context (1).md');
await expect(fileWithParens).toBeVisible();
// Test file with hyphens and underscores
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
await clickElement(page, "add-text-type");
await fillInput(page, "new-file-name", "test-file_v2.md");
await fillInput(page, "new-file-content", "Content with hyphens and underscores");
await clickElement(page, 'add-text-type');
await fillInput(page, 'new-file-name', 'test-file_v2.md');
await fillInput(page, 'new-file-content', 'Content with hyphens and underscores');
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
await page.waitForFunction(
() => !document.querySelector('[data-testid="add-context-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created
const fileWithHyphens = await getByTestId(page, "context-file-test-file_v2.md");
const fileWithHyphens = await getByTestId(page, 'context-file-test-file_v2.md');
await expect(fileWithHyphens).toBeVisible();
// Verify both files are accessible
@@ -585,34 +571,34 @@ test.describe("Context View - Edge Cases", () => {
});
const content = await getContextEditorContent(page);
expect(content).toBe("Content with hyphens and underscores");
expect(content).toBe('Content with hyphens and underscores');
});
test("should handle empty content", async ({ page }) => {
test('should handle empty content', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Create file with empty content
await clickElement(page, "add-context-file");
await clickElement(page, 'add-context-file');
await page.waitForSelector('[data-testid="add-context-dialog"]', {
timeout: 5000,
});
await clickElement(page, "add-text-type");
await fillInput(page, "new-file-name", "empty-file.md");
await clickElement(page, 'add-text-type');
await fillInput(page, 'new-file-name', 'empty-file.md');
// Don't fill any content - leave it empty
await clickElement(page, "confirm-add-file");
await clickElement(page, 'confirm-add-file');
await page.waitForFunction(
() => !document.querySelector('[data-testid="add-context-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created
const emptyFile = await getByTestId(page, "context-file-empty-file.md");
const emptyFile = await getByTestId(page, 'context-file-empty-file.md');
await expect(emptyFile).toBeVisible();
// Select file and verify editor shows empty content
@@ -629,38 +615,36 @@ test.describe("Context View - Edge Cases", () => {
});
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe("");
expect(editorContent).toBe('');
// Verify save works with empty content
// The save button should be disabled when there are no changes
// Let's add some content first, then clear it and save
await setContextEditorContent(page, "temporary");
await setContextEditorContent(page, "");
await setContextEditorContent(page, 'temporary');
await setContextEditorContent(page, '');
// Save should work
await clickElement(page, "save-context-file");
await clickElement(page, 'save-context-file');
await page.waitForFunction(
() =>
document
.querySelector('[data-testid="save-context-file"]')
?.textContent?.includes("Saved"),
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
{ timeout: 5000 }
);
});
test("should verify persistence across page refresh", async ({ page }) => {
test('should verify persistence across page refresh', async ({ page }) => {
// Create a file directly on disk to ensure it persists across refreshes
const testContent = "# Persistence Test\n\nThis content should persist.";
createContextFileOnDisk("persist-test.md", testContent);
const testContent = '# Persistence Test\n\nThis content should persist.';
createContextFileOnDisk('persist-test.md', testContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Verify file exists before refresh
await waitForContextFile(page, "persist-test.md", 10000);
await waitForContextFile(page, 'persist-test.md', 10000);
// Refresh the page
await page.reload();
@@ -670,7 +654,7 @@ test.describe("Context View - Edge Cases", () => {
await navigateToContext(page);
// Select the file after refresh (uses robust clicking mechanism)
await selectContextFile(page, "persist-test.md");
await selectContextFile(page, 'persist-test.md');
// Wait for file content to load
await waitForFileContentToLoad(page);

View File

@@ -5,8 +5,8 @@
* the available window space without dead space or content being cut off.
*/
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import {
waitForNetworkIdle,
@@ -15,17 +15,17 @@ import {
createTempDirPath,
setupProjectWithPathNoWorktrees,
waitForBoardView,
} from "./utils";
} from './utils';
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath("kanban-responsive-tests");
const TEST_TEMP_DIR = createTempDirPath('kanban-responsive-tests');
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
test.describe("Kanban Responsive Scaling Tests", () => {
test.describe('Kanban Responsive Scaling Tests', () => {
let testRepo: TestRepo;
test.beforeAll(async () => {
@@ -52,12 +52,12 @@ test.describe("Kanban Responsive Scaling Tests", () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test("kanban columns should scale to fill available width at different viewport sizes", async ({
test('kanban columns should scale to fill available width at different viewport sizes', async ({
page,
}) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
@@ -122,12 +122,10 @@ test.describe("Kanban Responsive Scaling Tests", () => {
}
});
test("kanban columns should be centered in the viewport", async ({
page,
}) => {
test('kanban columns should be centered in the viewport', async ({ page }) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
@@ -181,12 +179,12 @@ test.describe("Kanban Responsive Scaling Tests", () => {
}
});
test("kanban columns should have no horizontal scrollbar at standard viewport width", async ({
test('kanban columns should have no horizontal scrollbar at standard viewport width', async ({
page,
}) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
* Provides type-safe wrappers around common API operations
*/
import { Page, APIResponse } from "@playwright/test";
import { API_ENDPOINTS } from "../core/constants";
import { Page, APIResponse } from '@playwright/test';
import { API_ENDPOINTS } from '../core/constants';
// ============================================================================
// Types

View File

@@ -1,12 +1,10 @@
import { Page, Locator } from "@playwright/test";
import { waitForElement, waitForElementHidden } from "../core/waiting";
import { Page, Locator } from '@playwright/test';
import { waitForElement, waitForElementHidden } from '../core/waiting';
/**
* Check if the category autocomplete dropdown is visible
*/
export async function isCategoryAutocompleteListVisible(
page: Page
): Promise<boolean> {
export async function isCategoryAutocompleteListVisible(page: Page): Promise<boolean> {
const list = page.locator('[data-testid="category-autocomplete-list"]');
return await list.isVisible();
}
@@ -18,7 +16,7 @@ export async function waitForCategoryAutocompleteList(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "category-autocomplete-list", options);
return await waitForElement(page, 'category-autocomplete-list', options);
}
/**
@@ -28,19 +26,14 @@ export async function waitForCategoryAutocompleteListHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "category-autocomplete-list", options);
await waitForElementHidden(page, 'category-autocomplete-list', options);
}
/**
* Click a category option in the autocomplete dropdown
*/
export async function clickCategoryOption(
page: Page,
categoryName: string
): Promise<void> {
const optionTestId = `category-option-${categoryName
.toLowerCase()
.replace(/\s+/g, "-")}`;
export async function clickCategoryOption(page: Page, categoryName: string): Promise<void> {
const optionTestId = `category-option-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
const option = page.locator(`[data-testid="${optionTestId}"]`);
await option.click();
}
@@ -48,22 +41,15 @@ export async function clickCategoryOption(
/**
* Get a category option element by name
*/
export async function getCategoryOption(
page: Page,
categoryName: string
): Promise<Locator> {
const optionTestId = `category-option-${categoryName
.toLowerCase()
.replace(/\s+/g, "-")}`;
export async function getCategoryOption(page: Page, categoryName: string): Promise<Locator> {
const optionTestId = `category-option-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
return page.locator(`[data-testid="${optionTestId}"]`);
}
/**
* Click the "Create new" option for a category that doesn't exist
*/
export async function clickCreateNewCategoryOption(
page: Page
): Promise<void> {
export async function clickCreateNewCategoryOption(page: Page): Promise<void> {
const option = page.locator('[data-testid="category-option-create-new"]');
await option.click();
}
@@ -71,8 +57,6 @@ export async function clickCreateNewCategoryOption(
/**
* Get the "Create new" option element for categories
*/
export async function getCreateNewCategoryOption(
page: Page
): Promise<Locator> {
export async function getCreateNewCategoryOption(page: Page): Promise<Locator> {
return page.locator('[data-testid="category-option-create-new"]');
}

View File

@@ -1,6 +1,6 @@
import { Page, Locator } from "@playwright/test";
import { clickElement } from "../core/interactions";
import { waitForElement, waitForElementHidden } from "../core/waiting";
import { Page, Locator } from '@playwright/test';
import { clickElement } from '../core/interactions';
import { waitForElement, waitForElementHidden } from '../core/waiting';
/**
* Check if the add feature dialog is visible
@@ -33,36 +33,29 @@ export async function waitForEditFeatureDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "edit-feature-dialog", options);
return await waitForElement(page, 'edit-feature-dialog', options);
}
/**
* Get the edit feature description input/textarea element
*/
export async function getEditFeatureDescriptionInput(
page: Page
): Promise<Locator> {
export async function getEditFeatureDescriptionInput(page: Page): Promise<Locator> {
return page.locator('[data-testid="edit-feature-description"]');
}
/**
* Check if the edit feature description field is a textarea
*/
export async function isEditFeatureDescriptionTextarea(
page: Page
): Promise<boolean> {
export async function isEditFeatureDescriptionTextarea(page: Page): Promise<boolean> {
const element = page.locator('[data-testid="edit-feature-description"]');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
return tagName === "textarea";
return tagName === 'textarea';
}
/**
* Open the edit dialog for a specific feature
*/
export async function openEditFeatureDialog(
page: Page,
featureId: string
): Promise<void> {
export async function openEditFeatureDialog(page: Page, featureId: string): Promise<void> {
await clickElement(page, `edit-feature-${featureId}`);
await waitForEditFeatureDialog(page);
}
@@ -70,10 +63,7 @@ export async function openEditFeatureDialog(
/**
* Fill the edit feature description field
*/
export async function fillEditFeatureDescription(
page: Page,
value: string
): Promise<void> {
export async function fillEditFeatureDescription(page: Page, value: string): Promise<void> {
const input = await getEditFeatureDescriptionInput(page);
await input.fill(value);
}
@@ -82,24 +72,20 @@ export async function fillEditFeatureDescription(
* Click the confirm edit feature button
*/
export async function confirmEditFeature(page: Page): Promise<void> {
await clickElement(page, "confirm-edit-feature");
await clickElement(page, 'confirm-edit-feature');
}
/**
* Get the delete confirmation dialog
*/
export async function getDeleteConfirmationDialog(
page: Page
): Promise<Locator> {
export async function getDeleteConfirmationDialog(page: Page): Promise<Locator> {
return page.locator('[data-testid="delete-confirmation-dialog"]');
}
/**
* Check if the delete confirmation dialog is visible
*/
export async function isDeleteConfirmationDialogVisible(
page: Page
): Promise<boolean> {
export async function isDeleteConfirmationDialogVisible(page: Page): Promise<boolean> {
const dialog = page.locator('[data-testid="delete-confirmation-dialog"]');
return await dialog.isVisible().catch(() => false);
}
@@ -111,7 +97,7 @@ export async function waitForDeleteConfirmationDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "delete-confirmation-dialog", options);
return await waitForElement(page, 'delete-confirmation-dialog', options);
}
/**
@@ -121,21 +107,21 @@ export async function waitForDeleteConfirmationDialogHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "delete-confirmation-dialog", options);
await waitForElementHidden(page, 'delete-confirmation-dialog', options);
}
/**
* Click the confirm delete button in the delete confirmation dialog
*/
export async function clickConfirmDeleteButton(page: Page): Promise<void> {
await clickElement(page, "confirm-delete-button");
await clickElement(page, 'confirm-delete-button');
}
/**
* Click the cancel delete button in the delete confirmation dialog
*/
export async function clickCancelDeleteButton(page: Page): Promise<void> {
await clickElement(page, "cancel-delete-button");
await clickElement(page, 'cancel-delete-button');
}
/**
@@ -153,7 +139,7 @@ export async function waitForFollowUpDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "follow-up-dialog", options);
return await waitForElement(page, 'follow-up-dialog', options);
}
/**
@@ -163,14 +149,14 @@ export async function waitForFollowUpDialogHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "follow-up-dialog", options);
await waitForElementHidden(page, 'follow-up-dialog', options);
}
/**
* Click the confirm follow-up button in the follow-up dialog
*/
export async function clickConfirmFollowUp(page: Page): Promise<void> {
await clickElement(page, "confirm-follow-up");
await clickElement(page, 'confirm-follow-up');
}
/**
@@ -188,7 +174,7 @@ export async function waitForProjectInitDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "project-init-dialog", options);
return await waitForElement(page, 'project-init-dialog', options);
}
/**

View File

@@ -1,5 +1,5 @@
import { Page, Locator } from "@playwright/test";
import { waitForElement, waitForElementHidden } from "../core/waiting";
import { Page, Locator } from '@playwright/test';
import { waitForElement, waitForElementHidden } from '../core/waiting';
/**
* Check if the agent output modal is visible
@@ -16,7 +16,7 @@ export async function waitForAgentOutputModal(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "agent-output-modal", options);
return await waitForElement(page, 'agent-output-modal', options);
}
/**
@@ -26,28 +26,22 @@ export async function waitForAgentOutputModalHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "agent-output-modal", options);
await waitForElementHidden(page, 'agent-output-modal', options);
}
/**
* Get the modal title/description text to verify which feature's output is being shown
*/
export async function getAgentOutputModalDescription(
page: Page
): Promise<string | null> {
export async function getAgentOutputModalDescription(page: Page): Promise<string | null> {
const modal = page.locator('[data-testid="agent-output-modal"]');
const description = modal
.locator('[id="radix-\\:r.+\\:-description"]')
.first();
const description = modal.locator('[id="radix-\\:r.+\\:-description"]').first();
return await description.textContent().catch(() => null);
}
/**
* Check the dialog description content in the agent output modal
*/
export async function getOutputModalDescription(
page: Page
): Promise<string | null> {
export async function getOutputModalDescription(page: Page): Promise<string | null> {
const modalDescription = page.locator(
'[data-testid="agent-output-modal"] [data-slot="dialog-description"]'
);
@@ -57,18 +51,14 @@ export async function getOutputModalDescription(
/**
* Get the agent output modal description element
*/
export async function getAgentOutputModalDescriptionElement(
page: Page
): Promise<Locator> {
export async function getAgentOutputModalDescriptionElement(page: Page): Promise<Locator> {
return page.locator('[data-testid="agent-output-description"]');
}
/**
* Check if the agent output modal description is scrollable
*/
export async function isAgentOutputDescriptionScrollable(
page: Page
): Promise<boolean> {
export async function isAgentOutputDescriptionScrollable(page: Page): Promise<boolean> {
const description = page.locator('[data-testid="agent-output-description"]');
const scrollInfo = await description.evaluate((el) => {
return {
@@ -83,9 +73,7 @@ export async function isAgentOutputDescriptionScrollable(
/**
* Get scroll dimensions of the agent output modal description
*/
export async function getAgentOutputDescriptionScrollDimensions(
page: Page
): Promise<{
export async function getAgentOutputDescriptionScrollDimensions(page: Page): Promise<{
scrollHeight: number;
clientHeight: number;
maxHeight: string;

View File

@@ -10,7 +10,7 @@
/**
* Base URL for the API server
*/
export const API_BASE_URL = "http://localhost:3008";
export const API_BASE_URL = 'http://localhost:3008';
/**
* API endpoints for worktree operations
@@ -74,55 +74,55 @@ export const TIMEOUTS = {
*/
export const TEST_IDS = {
// Sidebar & Navigation
sidebar: "sidebar",
navBoard: "nav-board",
navSpec: "nav-spec",
navContext: "nav-context",
navAgent: "nav-agent",
navProfiles: "nav-profiles",
settingsButton: "settings-button",
openProjectButton: "open-project-button",
sidebar: 'sidebar',
navBoard: 'nav-board',
navSpec: 'nav-spec',
navContext: 'nav-context',
navAgent: 'nav-agent',
navProfiles: 'nav-profiles',
settingsButton: 'settings-button',
openProjectButton: 'open-project-button',
// Views
boardView: "board-view",
specView: "spec-view",
contextView: "context-view",
agentView: "agent-view",
profilesView: "profiles-view",
settingsView: "settings-view",
welcomeView: "welcome-view",
setupView: "setup-view",
boardView: 'board-view',
specView: 'spec-view',
contextView: 'context-view',
agentView: 'agent-view',
profilesView: 'profiles-view',
settingsView: 'settings-view',
welcomeView: 'welcome-view',
setupView: 'setup-view',
// Board View Components
addFeatureButton: "add-feature-button",
addFeatureDialog: "add-feature-dialog",
confirmAddFeature: "confirm-add-feature",
featureBranchInput: "feature-input",
featureCategoryInput: "feature-category-input",
worktreeSelector: "worktree-selector",
addFeatureButton: 'add-feature-button',
addFeatureDialog: 'add-feature-dialog',
confirmAddFeature: 'confirm-add-feature',
featureBranchInput: 'feature-input',
featureCategoryInput: 'feature-category-input',
worktreeSelector: 'worktree-selector',
// Spec Editor
specEditor: "spec-editor",
specEditor: 'spec-editor',
// File Browser Dialog
pathInput: "path-input",
goToPathButton: "go-to-path-button",
pathInput: 'path-input',
goToPathButton: 'go-to-path-button',
// Profiles View
addProfileButton: "add-profile-button",
addProfileDialog: "add-profile-dialog",
editProfileDialog: "edit-profile-dialog",
deleteProfileConfirmDialog: "delete-profile-confirm-dialog",
saveProfileButton: "save-profile-button",
confirmDeleteProfileButton: "confirm-delete-profile-button",
cancelDeleteButton: "cancel-delete-button",
profileNameInput: "profile-name-input",
profileDescriptionInput: "profile-description-input",
refreshProfilesButton: "refresh-profiles-button",
addProfileButton: 'add-profile-button',
addProfileDialog: 'add-profile-dialog',
editProfileDialog: 'edit-profile-dialog',
deleteProfileConfirmDialog: 'delete-profile-confirm-dialog',
saveProfileButton: 'save-profile-button',
confirmDeleteProfileButton: 'confirm-delete-profile-button',
cancelDeleteButton: 'cancel-delete-button',
profileNameInput: 'profile-name-input',
profileDescriptionInput: 'profile-description-input',
refreshProfilesButton: 'refresh-profiles-button',
// Context View
contextFileList: "context-file-list",
addContextButton: "add-context-button",
contextFileList: 'context-file-list',
addContextButton: 'add-context-button',
} as const;
// ============================================================================
@@ -134,17 +134,17 @@ export const TEST_IDS = {
*/
export const CSS_SELECTORS = {
/** CodeMirror editor content area */
codeMirrorContent: ".cm-content",
codeMirrorContent: '.cm-content',
/** Dialog elements */
dialog: '[role="dialog"]',
/** Sonner toast notifications */
toast: "[data-sonner-toast]",
toast: '[data-sonner-toast]',
toastError: '[data-sonner-toast][data-type="error"]',
toastSuccess: '[data-sonner-toast][data-type="success"]',
/** Command/combobox input (shadcn-ui cmdk) */
commandInput: "[cmdk-input]",
commandInput: '[cmdk-input]',
/** Radix dialog overlay */
dialogOverlay: "[data-radix-dialog-overlay]",
dialogOverlay: '[data-radix-dialog-overlay]',
} as const;
// ============================================================================
@@ -155,8 +155,8 @@ export const CSS_SELECTORS = {
* localStorage keys used by the application
*/
export const STORAGE_KEYS = {
appStorage: "automaker-storage",
setupStorage: "automaker-setup",
appStorage: 'automaker-storage',
setupStorage: 'automaker-setup',
} as const;
// ============================================================================
@@ -169,7 +169,7 @@ export const STORAGE_KEYS = {
* @returns Sanitized name suitable for directory paths
*/
export function sanitizeBranchName(branchName: string): string {
return branchName.replace(/[^a-zA-Z0-9_-]/g, "-");
return branchName.replace(/[^a-zA-Z0-9_-]/g, '-');
}
// ============================================================================
@@ -180,8 +180,8 @@ export function sanitizeBranchName(branchName: string): string {
* Default values used in test setup
*/
export const DEFAULTS = {
projectName: "Test Project",
projectPath: "/mock/test-project",
theme: "dark" as const,
projectName: 'Test Project',
projectPath: '/mock/test-project',
theme: 'dark' as const,
maxConcurrency: 3,
} as const;

View File

@@ -1,22 +1,16 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get an element by its data-testid attribute
*/
export async function getByTestId(
page: Page,
testId: string
): Promise<Locator> {
export async function getByTestId(page: Page, testId: string): Promise<Locator> {
return page.locator(`[data-testid="${testId}"]`);
}
/**
* Get a button by its text content
*/
export async function getButtonByText(
page: Page,
text: string
): Promise<Locator> {
export async function getButtonByText(page: Page, text: string): Promise<Locator> {
return page.locator(`button:has-text("${text}")`);
}
@@ -25,7 +19,7 @@ export async function getButtonByText(
*/
export async function getCategoryAutocompleteInput(
page: Page,
testId: string = "feature-category-input"
testId: string = 'feature-category-input'
): Promise<Locator> {
return page.locator(`[data-testid="${testId}"]`);
}
@@ -33,8 +27,6 @@ export async function getCategoryAutocompleteInput(
/**
* Get the category autocomplete dropdown list
*/
export async function getCategoryAutocompleteList(
page: Page
): Promise<Locator> {
export async function getCategoryAutocompleteList(page: Page): Promise<Locator> {
return page.locator('[data-testid="category-autocomplete-list"]');
}

View File

@@ -1,12 +1,12 @@
import { Page } from "@playwright/test";
import { getByTestId, getButtonByText } from "./elements";
import { Page } from '@playwright/test';
import { getByTestId, getButtonByText } from './elements';
/**
* Get the platform-specific modifier key (Meta for Mac, Control for Windows/Linux)
* This is used for keyboard shortcuts like Cmd+Enter or Ctrl+Enter
*/
export function getPlatformModifier(): "Meta" | "Control" {
return process.platform === "darwin" ? "Meta" : "Control";
export function getPlatformModifier(): 'Meta' | 'Control' {
return process.platform === 'darwin' ? 'Meta' : 'Control';
}
/**
@@ -28,10 +28,7 @@ export async function clickElement(page: Page, testId: string): Promise<void> {
/**
* Click a button by its text content
*/
export async function clickButtonByText(
page: Page,
text: string
): Promise<void> {
export async function clickButtonByText(page: Page, text: string): Promise<void> {
const button = await getButtonByText(page, text);
await button.click();
}
@@ -39,11 +36,7 @@ export async function clickButtonByText(
/**
* Fill an input field by its data-testid attribute
*/
export async function fillInput(
page: Page,
testId: string,
value: string
): Promise<void> {
export async function fillInput(page: Page, testId: string, value: string): Promise<void> {
const input = await getByTestId(page, testId);
await input.fill(value);
}
@@ -75,11 +68,11 @@ export async function focusOnInput(page: Page, testId: string): Promise<void> {
* Waits for dialog to be removed from DOM rather than using arbitrary timeout
*/
export async function closeDialogWithEscape(page: Page): Promise<void> {
await page.keyboard.press("Escape");
await page.keyboard.press('Escape');
// Wait for any dialog overlay to disappear
await page
.locator('[data-radix-dialog-overlay], [role="dialog"]')
.waitFor({ state: "hidden", timeout: 5000 })
.waitFor({ state: 'hidden', timeout: 5000 })
.catch(() => {
// Dialog may have already closed or not exist
});

View File

@@ -1,11 +1,11 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Wait for the page to reach network idle state
* This is commonly used after navigation or page reload to ensure all network requests have completed
*/
export async function waitForNetworkIdle(page: Page): Promise<void> {
await page.waitForLoadState("networkidle");
await page.waitForLoadState('networkidle');
}
/**
@@ -14,12 +14,12 @@ export async function waitForNetworkIdle(page: Page): Promise<void> {
export async function waitForElement(
page: Page,
testId: string,
options?: { timeout?: number; state?: "attached" | "visible" | "hidden" }
options?: { timeout?: number; state?: 'attached' | 'visible' | 'hidden' }
): Promise<Locator> {
const element = page.locator(`[data-testid="${testId}"]`);
await element.waitFor({
timeout: options?.timeout ?? 5000,
state: options?.state ?? "visible",
state: options?.state ?? 'visible',
});
return element;
}
@@ -35,6 +35,6 @@ export async function waitForElementHidden(
const element = page.locator(`[data-testid="${testId}"]`);
await element.waitFor({
timeout: options?.timeout ?? 5000,
state: "hidden",
state: 'hidden',
});
}

View File

@@ -1,4 +1,4 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Perform a drag and drop operation that works with @dnd-kit
@@ -13,8 +13,8 @@ export async function dragAndDropWithDndKit(
targetLocator: Locator
): Promise<void> {
// Ensure elements are visible and stable before getting bounding boxes
await sourceLocator.waitFor({ state: "visible", timeout: 5000 });
await targetLocator.waitFor({ state: "visible", timeout: 5000 });
await sourceLocator.waitFor({ state: 'visible', timeout: 5000 });
await targetLocator.waitFor({ state: 'visible', timeout: 5000 });
// Small delay to ensure layout is stable
await page.waitForTimeout(100);
@@ -23,7 +23,7 @@ export async function dragAndDropWithDndKit(
const targetBox = await targetLocator.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error("Could not find source or target element bounds");
throw new Error('Could not find source or target element bounds');
}
// Start drag from the center of the source element

View File

@@ -1,4 +1,4 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get the skip tests checkbox element in the add feature dialog
@@ -20,8 +20,8 @@ export async function toggleSkipTestsCheckbox(page: Page): Promise<void> {
*/
export async function isSkipTestsChecked(page: Page): Promise<boolean> {
const checkbox = page.locator('[data-testid="skip-tests-checkbox"]');
const state = await checkbox.getAttribute("data-state");
return state === "checked";
const state = await checkbox.getAttribute('data-state');
return state === 'checked';
}
/**
@@ -44,17 +44,14 @@ export async function toggleEditSkipTestsCheckbox(page: Page): Promise<void> {
*/
export async function isEditSkipTestsChecked(page: Page): Promise<boolean> {
const checkbox = page.locator('[data-testid="edit-skip-tests-checkbox"]');
const state = await checkbox.getAttribute("data-state");
return state === "checked";
const state = await checkbox.getAttribute('data-state');
return state === 'checked';
}
/**
* Check if the skip tests badge is visible on a kanban card
*/
export async function isSkipTestsBadgeVisible(
page: Page,
featureId: string
): Promise<boolean> {
export async function isSkipTestsBadgeVisible(page: Page, featureId: string): Promise<boolean> {
const badge = page.locator(`[data-testid="skip-tests-badge-${featureId}"]`);
return await badge.isVisible().catch(() => false);
}
@@ -62,20 +59,14 @@ export async function isSkipTestsBadgeVisible(
/**
* Get the skip tests badge element for a kanban card
*/
export async function getSkipTestsBadge(
page: Page,
featureId: string
): Promise<Locator> {
export async function getSkipTestsBadge(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="skip-tests-badge-${featureId}"]`);
}
/**
* Click the manual verify button for a skipTests feature
*/
export async function clickManualVerify(
page: Page,
featureId: string
): Promise<void> {
export async function clickManualVerify(page: Page, featureId: string): Promise<void> {
const button = page.locator(`[data-testid="manual-verify-${featureId}"]`);
await button.click();
}
@@ -83,10 +74,7 @@ export async function clickManualVerify(
/**
* Check if the manual verify button is visible for a feature
*/
export async function isManualVerifyButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
export async function isManualVerifyButtonVisible(page: Page, featureId: string): Promise<boolean> {
const button = page.locator(`[data-testid="manual-verify-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
@@ -94,10 +82,7 @@ export async function isManualVerifyButtonVisible(
/**
* Click the move back button for a verified skipTests feature
*/
export async function clickMoveBack(
page: Page,
featureId: string
): Promise<void> {
export async function clickMoveBack(page: Page, featureId: string): Promise<void> {
const button = page.locator(`[data-testid="move-back-${featureId}"]`);
await button.click();
}
@@ -105,10 +90,7 @@ export async function clickMoveBack(
/**
* Check if the move back button is visible for a feature
*/
export async function isMoveBackButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
export async function isMoveBackButtonVisible(page: Page, featureId: string): Promise<boolean> {
const button = page.locator(`[data-testid="move-back-${featureId}"]`);
return await button.isVisible().catch(() => false);
}

View File

@@ -1,12 +1,9 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get the count up timer element for a specific feature card
*/
export async function getTimerForFeature(
page: Page,
featureId: string
): Promise<Locator> {
export async function getTimerForFeature(page: Page, featureId: string): Promise<Locator> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
return card.locator('[data-testid="count-up-timer"]');
}
@@ -26,10 +23,7 @@ export async function getTimerDisplayForFeature(
/**
* Check if a timer is visible for a specific feature
*/
export async function isTimerVisibleForFeature(
page: Page,
featureId: string
): Promise<boolean> {
export async function isTimerVisibleForFeature(page: Page, featureId: string): Promise<boolean> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
const timer = card.locator('[data-testid="count-up-timer"]');
return await timer.isVisible().catch(() => false);

View File

@@ -1,22 +1,16 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get the follow-up button for a waiting_approval feature
*/
export async function getFollowUpButton(
page: Page,
featureId: string
): Promise<Locator> {
export async function getFollowUpButton(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="follow-up-${featureId}"]`);
}
/**
* Click the follow-up button for a waiting_approval feature
*/
export async function clickFollowUpButton(
page: Page,
featureId: string
): Promise<void> {
export async function clickFollowUpButton(page: Page, featureId: string): Promise<void> {
const button = page.locator(`[data-testid="follow-up-${featureId}"]`);
await button.click();
}
@@ -24,10 +18,7 @@ export async function clickFollowUpButton(
/**
* Check if the follow-up button is visible for a feature
*/
export async function isFollowUpButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
export async function isFollowUpButtonVisible(page: Page, featureId: string): Promise<boolean> {
const button = page.locator(`[data-testid="follow-up-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
@@ -35,20 +26,14 @@ export async function isFollowUpButtonVisible(
/**
* Get the commit button for a waiting_approval feature
*/
export async function getCommitButton(
page: Page,
featureId: string
): Promise<Locator> {
export async function getCommitButton(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="commit-${featureId}"]`);
}
/**
* Click the commit button for a waiting_approval feature
*/
export async function clickCommitButton(
page: Page,
featureId: string
): Promise<void> {
export async function clickCommitButton(page: Page, featureId: string): Promise<void> {
const button = page.locator(`[data-testid="commit-${featureId}"]`);
await button.click();
}
@@ -56,10 +41,7 @@ export async function clickCommitButton(
/**
* Check if the commit button is visible for a feature
*/
export async function isCommitButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
export async function isCommitButtonVisible(page: Page, featureId: string): Promise<boolean> {
const button = page.locator(`[data-testid="commit-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
@@ -74,9 +56,7 @@ export async function getWaitingApprovalColumn(page: Page): Promise<Locator> {
/**
* Check if the waiting_approval column is visible
*/
export async function isWaitingApprovalColumnVisible(
page: Page
): Promise<boolean> {
export async function isWaitingApprovalColumnVisible(page: Page): Promise<boolean> {
const column = page.locator('[data-testid="kanban-column-waiting_approval"]');
return await column.isVisible().catch(() => false);
}

View File

@@ -1,4 +1,4 @@
import { Page } from "@playwright/test";
import { Page } from '@playwright/test';
/**
* Simulate drag and drop of a file onto an element
@@ -8,7 +8,7 @@ export async function simulateFileDrop(
targetSelector: string,
fileName: string,
fileContent: string,
mimeType: string = "text/plain"
mimeType: string = 'text/plain'
): Promise<void> {
await page.evaluate(
({ selector, content, name, mime }) => {
@@ -21,13 +21,13 @@ export async function simulateFileDrop(
// Dispatch drag events
target.dispatchEvent(
new DragEvent("dragover", {
new DragEvent('dragover', {
dataTransfer,
bubbles: true,
})
);
target.dispatchEvent(
new DragEvent("drop", {
new DragEvent('drop', {
dataTransfer,
bubbles: true,
})
@@ -45,7 +45,7 @@ export async function simulateImagePaste(
page: Page,
targetSelector: string,
imageBase64: string,
mimeType: string = "image/png"
mimeType: string = 'image/png'
): Promise<void> {
await page.evaluate(
({ selector, base64, mime }) => {
@@ -62,14 +62,14 @@ export async function simulateImagePaste(
const blob = new Blob([byteArray], { type: mime });
// Create a File from Blob
const file = new File([blob], "pasted-image.png", { type: mime });
const file = new File([blob], 'pasted-image.png', { type: mime });
// Create a DataTransfer with clipboard items
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Create ClipboardEvent with the image data
const clipboardEvent = new ClipboardEvent("paste", {
const clipboardEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dataTransfer,

View File

@@ -1,11 +1,9 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get the concurrency slider container
*/
export async function getConcurrencySliderContainer(
page: Page
): Promise<Locator> {
export async function getConcurrencySliderContainer(page: Page): Promise<Locator> {
return page.locator('[data-testid="concurrency-slider-container"]');
}
@@ -37,7 +35,7 @@ export async function setConcurrencyValue(
const sliderBounds = await slider.boundingBox();
if (!sliderBounds) {
throw new Error("Concurrency slider not found or not visible");
throw new Error('Concurrency slider not found or not visible');
}
// Calculate position for target value

View File

@@ -1,5 +1,5 @@
import { Page, Locator } from "@playwright/test";
import { clickElement } from "../core/interactions";
import { Page, Locator } from '@playwright/test';
import { clickElement } from '../core/interactions';
/**
* Get the log viewer header element (contains type counts and expand/collapse buttons)
@@ -26,30 +26,21 @@ export async function getLogEntriesContainer(page: Page): Promise<Locator> {
/**
* Get a log entry by its type
*/
export async function getLogEntryByType(
page: Page,
type: string
): Promise<Locator> {
export async function getLogEntryByType(page: Page, type: string): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`).first();
}
/**
* Get all log entries of a specific type
*/
export async function getAllLogEntriesByType(
page: Page,
type: string
): Promise<Locator> {
export async function getAllLogEntriesByType(page: Page, type: string): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`);
}
/**
* Count log entries of a specific type
*/
export async function countLogEntriesByType(
page: Page,
type: string
): Promise<number> {
export async function countLogEntriesByType(page: Page, type: string): Promise<number> {
const entries = page.locator(`[data-testid="log-entry-${type}"]`);
return await entries.count();
}
@@ -57,20 +48,14 @@ export async function countLogEntriesByType(
/**
* Get the log type count badge by type
*/
export async function getLogTypeCountBadge(
page: Page,
type: string
): Promise<Locator> {
export async function getLogTypeCountBadge(page: Page, type: string): Promise<Locator> {
return page.locator(`[data-testid="log-type-count-${type}"]`);
}
/**
* Check if a log type count badge is visible
*/
export async function isLogTypeCountBadgeVisible(
page: Page,
type: string
): Promise<boolean> {
export async function isLogTypeCountBadgeVisible(page: Page, type: string): Promise<boolean> {
const badge = page.locator(`[data-testid="log-type-count-${type}"]`);
return await badge.isVisible().catch(() => false);
}
@@ -79,14 +64,14 @@ export async function isLogTypeCountBadgeVisible(
* Click the expand all button in the log viewer
*/
export async function clickLogExpandAll(page: Page): Promise<void> {
await clickElement(page, "log-expand-all");
await clickElement(page, 'log-expand-all');
}
/**
* Click the collapse all button in the log viewer
*/
export async function clickLogCollapseAll(page: Page): Promise<void> {
await clickElement(page, "log-collapse-all");
await clickElement(page, 'log-collapse-all');
}
/**
@@ -107,31 +92,22 @@ export async function isLogEntryBadgeVisible(page: Page): Promise<boolean> {
/**
* Get the view mode toggle button (parsed/raw)
*/
export async function getViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<Locator> {
export async function getViewModeButton(page: Page, mode: 'parsed' | 'raw'): Promise<Locator> {
return page.locator(`[data-testid="view-mode-${mode}"]`);
}
/**
* Click a view mode toggle button
*/
export async function clickViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<void> {
export async function clickViewModeButton(page: Page, mode: 'parsed' | 'raw'): Promise<void> {
await clickElement(page, `view-mode-${mode}`);
}
/**
* Check if a view mode button is active (selected)
*/
export async function isViewModeActive(
page: Page,
mode: "parsed" | "raw"
): Promise<boolean> {
export async function isViewModeActive(page: Page, mode: 'parsed' | 'raw'): Promise<boolean> {
const button = page.locator(`[data-testid="view-mode-${mode}"]`);
const classes = await button.getAttribute("class");
return classes?.includes("text-purple-300") ?? false;
const classes = await button.getAttribute('class');
return classes?.includes('text-purple-300') ?? false;
}

View File

@@ -1,4 +1,4 @@
import { Locator } from "@playwright/test";
import { Locator } from '@playwright/test';
/**
* Check if an element is scrollable (has scrollable content)

View File

@@ -1,49 +1,49 @@
// Re-export all utilities from their respective modules
// Core utilities
export * from "./core/elements";
export * from "./core/interactions";
export * from "./core/waiting";
export * from "./core/constants";
export * from './core/elements';
export * from './core/interactions';
export * from './core/waiting';
export * from './core/constants';
// API utilities
export * from "./api/client";
export * from './api/client';
// Git utilities
export * from "./git/worktree";
export * from './git/worktree';
// Project utilities
export * from "./project/setup";
export * from "./project/fixtures";
export * from './project/setup';
export * from './project/fixtures';
// Navigation utilities
export * from "./navigation/views";
export * from './navigation/views';
// View-specific utilities
export * from "./views/board";
export * from "./views/context";
export * from "./views/spec-editor";
export * from "./views/agent";
export * from "./views/settings";
export * from "./views/setup";
export * from "./views/profiles";
export * from './views/board';
export * from './views/context';
export * from './views/spec-editor';
export * from './views/agent';
export * from './views/settings';
export * from './views/setup';
export * from './views/profiles';
// Component utilities
export * from "./components/dialogs";
export * from "./components/toasts";
export * from "./components/modals";
export * from "./components/autocomplete";
export * from './components/dialogs';
export * from './components/toasts';
export * from './components/modals';
export * from './components/autocomplete';
// Feature utilities
export * from "./features/kanban";
export * from "./features/timers";
export * from "./features/skip-tests";
export * from "./features/waiting-approval";
export * from './features/kanban';
export * from './features/timers';
export * from './features/skip-tests';
export * from './features/waiting-approval';
// Helper utilities
export * from "./helpers/scroll";
export * from "./helpers/log-viewer";
export * from "./helpers/concurrency";
export * from './helpers/scroll';
export * from './helpers/log-viewer';
export * from './helpers/concurrency';
// File utilities
export * from "./files/drag-drop";
export * from './files/drag-drop';

View File

@@ -1,22 +1,22 @@
import { Page } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
import { Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
/**
* Resolve the workspace root - handle both running from apps/ui and from root
*/
export function getWorkspaceRoot(): string {
const cwd = process.cwd();
if (cwd.includes("apps/ui")) {
return path.resolve(cwd, "../..");
if (cwd.includes('apps/ui')) {
return path.resolve(cwd, '../..');
}
return cwd;
}
const WORKSPACE_ROOT = getWorkspaceRoot();
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");
const CONTEXT_PATH = path.join(FIXTURE_PATH, ".automaker/context");
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, '.automaker/app_spec.txt');
const CONTEXT_PATH = path.join(FIXTURE_PATH, '.automaker/context');
// Original spec content for resetting between tests
const ORIGINAL_SPEC_CONTENT = `<app_spec>
@@ -76,8 +76,8 @@ export async function setupProjectWithFixture(
): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-fixture",
name: "projectA",
id: 'test-project-fixture',
name: 'projectA',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -86,10 +86,10 @@ export async function setupProjectWithFixture(
state: {
projects: [mockProject],
currentProject: mockProject,
currentView: "board",
theme: "dark",
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -97,19 +97,19 @@ export async function setupProjectWithFixture(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
}

View File

@@ -1,4 +1,4 @@
import { Page } from "@playwright/test";
import { Page } from '@playwright/test';
/**
* Set up a mock project in localStorage to bypass the welcome screen
@@ -7,9 +7,9 @@ import { Page } from "@playwright/test";
export async function setupMockProject(page: Page): Promise<void> {
await page.addInitScript(() => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -17,9 +17,9 @@ export async function setupMockProject(page: Page): Promise<void> {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -27,7 +27,7 @@ export async function setupMockProject(page: Page): Promise<void> {
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
});
}
@@ -40,9 +40,9 @@ export async function setupMockProjectWithConcurrency(
): Promise<void> {
await page.addInitScript((maxConcurrency: number) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -50,9 +50,9 @@ export async function setupMockProjectWithConcurrency(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: maxConcurrency,
@@ -60,7 +60,7 @@ export async function setupMockProjectWithConcurrency(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
}, concurrency);
}
@@ -70,20 +70,14 @@ export async function setupMockProjectWithConcurrency(
export async function setupMockProjectAtConcurrencyLimit(
page: Page,
maxConcurrency: number = 1,
runningTasks: string[] = ["running-task-1"]
runningTasks: string[] = ['running-task-1']
): Promise<void> {
await page.addInitScript(
({
maxConcurrency,
runningTasks,
}: {
maxConcurrency: number;
runningTasks: string[];
}) => {
({ maxConcurrency, runningTasks }: { maxConcurrency: number; runningTasks: string[] }) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -91,9 +85,9 @@ export async function setupMockProjectAtConcurrencyLimit(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: maxConcurrency,
@@ -104,7 +98,7 @@ export async function setupMockProjectAtConcurrencyLimit(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
},
{ maxConcurrency, runningTasks }
);
@@ -122,16 +116,16 @@ export async function setupMockProjectWithFeatures(
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
status: 'backlog' | 'in_progress' | 'verified';
steps?: string[];
}>;
}
): Promise<void> {
await page.addInitScript((opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -141,9 +135,9 @@ export async function setupMockProjectWithFeatures(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
@@ -155,7 +149,7 @@ export async function setupMockProjectWithFeatures(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system
@@ -170,20 +164,14 @@ export async function setupMockProjectWithFeatures(
export async function setupMockProjectWithContextFile(
page: Page,
featureId: string,
contextContent: string = "# Agent Context\n\nPrevious implementation work..."
contextContent: string = '# Agent Context\n\nPrevious implementation work...'
): Promise<void> {
await page.addInitScript(
({
featureId,
contextContent,
}: {
featureId: string;
contextContent: string;
}) => {
({ featureId, contextContent }: { featureId: string; contextContent: string }) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -191,9 +179,9 @@ export async function setupMockProjectWithContextFile(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -201,7 +189,7 @@ export async function setupMockProjectWithContextFile(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Set up mock file system with a context file for the feature
// This will be used by the mock electron API
@@ -228,7 +216,7 @@ export async function setupMockProjectWithInProgressFeatures(
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
status: 'backlog' | 'in_progress' | 'verified';
steps?: string[];
startedAt?: string;
}>;
@@ -236,9 +224,9 @@ export async function setupMockProjectWithInProgressFeatures(
): Promise<void> {
await page.addInitScript((opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -248,9 +236,9 @@ export async function setupMockProjectWithInProgressFeatures(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
@@ -262,7 +250,7 @@ export async function setupMockProjectWithInProgressFeatures(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system
@@ -273,15 +261,12 @@ export async function setupMockProjectWithInProgressFeatures(
/**
* Set up a mock project with a specific current view for route persistence testing
*/
export async function setupMockProjectWithView(
page: Page,
view: string
): Promise<void> {
export async function setupMockProjectWithView(page: Page, view: string): Promise<void> {
await page.addInitScript((currentView: string) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -290,9 +275,9 @@ export async function setupMockProjectWithView(
projects: [mockProject],
currentProject: mockProject,
currentView: currentView,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -300,7 +285,7 @@ export async function setupMockProjectWithView(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
}, view);
}
@@ -313,38 +298,36 @@ export async function setupEmptyLocalStorage(page: Page): Promise<void> {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
});
}
/**
* Set up mock projects in localStorage but with no current project (for recent projects list)
*/
export async function setupMockProjectsWithoutCurrent(
page: Page
): Promise<void> {
export async function setupMockProjectsWithoutCurrent(page: Page): Promise<void> {
await page.addInitScript(() => {
const mockProjects = [
{
id: "test-project-1",
name: "Test Project 1",
path: "/mock/test-project-1",
id: 'test-project-1',
name: 'Test Project 1',
path: '/mock/test-project-1',
lastOpened: new Date().toISOString(),
},
{
id: "test-project-2",
name: "Test Project 2",
path: "/mock/test-project-2",
id: 'test-project-2',
name: 'Test Project 2',
path: '/mock/test-project-2',
lastOpened: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
},
];
@@ -353,10 +336,10 @@ export async function setupMockProjectsWithoutCurrent(
state: {
projects: mockProjects,
currentProject: null,
currentView: "welcome",
theme: "dark",
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -364,7 +347,7 @@ export async function setupMockProjectsWithoutCurrent(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
});
}
@@ -380,7 +363,7 @@ export async function setupMockProjectWithSkipTestsFeatures(
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
status: 'backlog' | 'in_progress' | 'verified';
steps?: string[];
startedAt?: string;
skipTests?: boolean;
@@ -389,9 +372,9 @@ export async function setupMockProjectWithSkipTestsFeatures(
): Promise<void> {
await page.addInitScript((opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -401,9 +384,9 @@ export async function setupMockProjectWithSkipTestsFeatures(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
@@ -415,7 +398,7 @@ export async function setupMockProjectWithSkipTestsFeatures(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
}, options);
}
@@ -441,10 +424,10 @@ export async function setupMockMultipleProjects(
state: {
projects: mockProjects,
currentProject: mockProjects[0],
currentView: "board",
theme: "dark",
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -452,7 +435,7 @@ export async function setupMockMultipleProjects(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
}, projectCount);
}
@@ -465,17 +448,11 @@ export async function setupMockProjectWithAgentOutput(
outputContent: string
): Promise<void> {
await page.addInitScript(
({
featureId,
outputContent,
}: {
featureId: string;
outputContent: string;
}) => {
({ featureId, outputContent }: { featureId: string; outputContent: string }) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -483,9 +460,9 @@ export async function setupMockProjectWithAgentOutput(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -493,7 +470,7 @@ export async function setupMockProjectWithAgentOutput(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Set up mock file system with output content for the feature
// Now uses features/{id}/agent-output.md path
@@ -519,7 +496,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "waiting_approval" | "verified";
status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified';
steps?: string[];
startedAt?: string;
skipTests?: boolean;
@@ -528,9 +505,9 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
): Promise<void> {
await page.addInitScript((opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
@@ -540,9 +517,9 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
@@ -554,7 +531,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use
(window as any).__mockFeatures = mockFeatures;
@@ -567,20 +544,20 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
export async function setupFirstRun(page: Page): Promise<void> {
await page.addInitScript(() => {
// Clear any existing setup state to simulate first run
localStorage.removeItem("automaker-setup");
localStorage.removeItem("automaker-storage");
localStorage.removeItem('automaker-setup');
localStorage.removeItem('automaker-storage');
// Set up the setup store state for first run
const setupState = {
state: {
isFirstRun: true,
setupComplete: false,
currentStep: "welcome",
currentStep: 'welcome',
claudeCliStatus: null,
claudeAuthStatus: null,
claudeInstallProgress: {
isInstalling: false,
currentStep: "",
currentStep: '',
progress: 0,
output: [],
},
@@ -589,28 +566,28 @@ export async function setupFirstRun(page: Page): Promise<void> {
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
// Also set up app store to show setup view
const appState = {
state: {
projects: [],
currentProject: null,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
isAutoModeRunning: false,
runningAutoTasks: [],
autoModeActivityLog: [],
currentView: "setup",
currentView: 'setup',
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(appState));
localStorage.setItem('automaker-storage', JSON.stringify(appState));
});
}
@@ -624,13 +601,13 @@ export async function setupComplete(page: Page): Promise<void> {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
});
}
@@ -647,45 +624,44 @@ export async function setupMockProjectWithProfiles(
): Promise<void> {
await page.addInitScript((opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
id: 'test-project-1',
name: 'Test Project',
path: '/mock/test-project',
lastOpened: new Date().toISOString(),
};
// Default built-in profiles (same as DEFAULT_AI_PROFILES from app-store.ts)
const builtInProfiles = [
{
id: "profile-heavy-task",
name: "Heavy Task",
id: 'profile-heavy-task',
name: 'Heavy Task',
description:
"Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.",
model: "opus" as const,
thinkingLevel: "ultrathink" as const,
provider: "claude" as const,
'Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.',
model: 'opus' as const,
thinkingLevel: 'ultrathink' as const,
provider: 'claude' as const,
isBuiltIn: true,
icon: "Brain",
icon: 'Brain',
},
{
id: "profile-balanced",
name: "Balanced",
description:
"Claude Sonnet with medium thinking for typical development tasks.",
model: "sonnet" as const,
thinkingLevel: "medium" as const,
provider: "claude" as const,
id: 'profile-balanced',
name: 'Balanced',
description: 'Claude Sonnet with medium thinking for typical development tasks.',
model: 'sonnet' as const,
thinkingLevel: 'medium' as const,
provider: 'claude' as const,
isBuiltIn: true,
icon: "Scale",
icon: 'Scale',
},
{
id: "profile-quick-edit",
name: "Quick Edit",
description: "Claude Haiku for fast, simple edits and minor fixes.",
model: "haiku" as const,
thinkingLevel: "none" as const,
provider: "claude" as const,
id: 'profile-quick-edit',
name: 'Quick Edit',
description: 'Claude Haiku for fast, simple edits and minor fixes.',
model: 'haiku' as const,
thinkingLevel: 'none' as const,
provider: 'claude' as const,
isBuiltIn: true,
icon: "Zap",
icon: 'Zap',
},
];
@@ -697,56 +673,51 @@ export async function setupMockProjectWithProfiles(
id: `custom-profile-${i + 1}`,
name: `Custom Profile ${i + 1}`,
description: `Test custom profile ${i + 1}`,
model: ["haiku", "sonnet", "opus"][i % 3] as
| "haiku"
| "sonnet"
| "opus",
thinkingLevel: ["none", "low", "medium", "high"][i % 4] as
| "none"
| "low"
| "medium"
| "high",
provider: "claude" as const,
model: ['haiku', 'sonnet', 'opus'][i % 3] as 'haiku' | 'sonnet' | 'opus',
thinkingLevel: ['none', 'low', 'medium', 'high'][i % 4] as
| 'none'
| 'low'
| 'medium'
| 'high',
provider: 'claude' as const,
isBuiltIn: false,
icon: ["Brain", "Zap", "Scale", "Cpu", "Rocket", "Sparkles"][i % 6],
icon: ['Brain', 'Zap', 'Scale', 'Cpu', 'Rocket', 'Sparkles'][i % 6],
});
}
// Combine profiles (built-in first, then custom)
const includeBuiltIn = opts?.includeBuiltIn !== false; // Default to true
const aiProfiles = includeBuiltIn
? [...builtInProfiles, ...customProfiles]
: customProfiles;
const aiProfiles = includeBuiltIn ? [...builtInProfiles, ...customProfiles] : customProfiles;
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "", openai: "" },
apiKeys: { anthropic: '', google: '', openai: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
aiProfiles: aiProfiles,
features: [],
currentView: "board", // Start at board, will navigate to profiles
currentView: 'board', // Start at board, will navigate to profiles
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also mark setup as complete to skip the setup wizard
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, options);
}

View File

@@ -1,5 +1,5 @@
import { Page, Locator } from "@playwright/test";
import { waitForElement } from "../core/waiting";
import { Page, Locator } from '@playwright/test';
import { waitForElement } from '../core/waiting';
/**
* Get the session list element
@@ -26,20 +26,14 @@ export async function clickNewSessionButton(page: Page): Promise<void> {
/**
* Get a session item by its ID
*/
export async function getSessionItem(
page: Page,
sessionId: string
): Promise<Locator> {
export async function getSessionItem(page: Page, sessionId: string): Promise<Locator> {
return page.locator(`[data-testid="session-item-${sessionId}"]`);
}
/**
* Click the archive button for a session
*/
export async function clickArchiveSession(
page: Page,
sessionId: string
): Promise<void> {
export async function clickArchiveSession(page: Page, sessionId: string): Promise<void> {
const button = page.locator(`[data-testid="archive-session-${sessionId}"]`);
await button.click();
}
@@ -47,9 +41,7 @@ export async function clickArchiveSession(
/**
* Check if the no session placeholder is visible
*/
export async function isNoSessionPlaceholderVisible(
page: Page
): Promise<boolean> {
export async function isNoSessionPlaceholderVisible(page: Page): Promise<boolean> {
const placeholder = page.locator('[data-testid="no-session-placeholder"]');
return await placeholder.isVisible();
}
@@ -61,7 +53,7 @@ export async function waitForNoSessionPlaceholder(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "no-session-placeholder", options);
return await waitForElement(page, 'no-session-placeholder', options);
}
/**
@@ -76,23 +68,18 @@ export async function isMessageListVisible(page: Page): Promise<boolean> {
* Count the number of session items in the session list
*/
export async function countSessionItems(page: Page): Promise<number> {
const sessionList = page.locator(
'[data-testid="session-list"] [data-testid^="session-item-"]'
);
const sessionList = page.locator('[data-testid="session-list"] [data-testid^="session-item-"]');
return await sessionList.count();
}
/**
* Wait for a new session to be created (by checking if a session item appears)
*/
export async function waitForNewSession(
page: Page,
options?: { timeout?: number }
): Promise<void> {
export async function waitForNewSession(page: Page, options?: { timeout?: number }): Promise<void> {
// Wait for any session item to appear
const sessionItem = page.locator('[data-testid^="session-item-"]').first();
await sessionItem.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
state: 'visible',
});
}

View File

@@ -1,19 +1,19 @@
import { Page, Locator } from "@playwright/test";
import { clickElement, fillInput } from "../core/interactions";
import { waitForElement, waitForElementHidden } from "../core/waiting";
import { getByTestId } from "../core/elements";
import { navigateToView } from "../navigation/views";
import { Page, Locator } from '@playwright/test';
import { clickElement, fillInput } from '../core/interactions';
import { waitForElement, waitForElementHidden } from '../core/waiting';
import { getByTestId } from '../core/elements';
import { navigateToView } from '../navigation/views';
/**
* Navigate to the profiles view
*/
export async function navigateToProfiles(page: Page): Promise<void> {
// Click the profiles navigation button
await navigateToView(page, "profiles");
await navigateToView(page, 'profiles');
// Wait for profiles view to be visible
await page.waitForSelector('[data-testid="profiles-view"]', {
state: "visible",
state: 'visible',
timeout: 10000,
});
}
@@ -25,10 +25,7 @@ export async function navigateToProfiles(page: Page): Promise<void> {
/**
* Get a specific profile card by ID
*/
export async function getProfileCard(
page: Page,
profileId: string
): Promise<Locator> {
export async function getProfileCard(page: Page, profileId: string): Promise<Locator> {
return getByTestId(page, `profile-card-${profileId}`);
}
@@ -84,10 +81,10 @@ export async function getCustomProfileIds(page: Page): Promise<string[]> {
const builtInText = card.locator('text="Built-in"');
const isBuiltIn = (await builtInText.count()) > 0;
if (!isBuiltIn) {
const testId = await card.getAttribute("data-testid");
const testId = await card.getAttribute('data-testid');
if (testId) {
// Extract ID from "profile-card-{id}"
const profileId = testId.replace("profile-card-", "");
const profileId = testId.replace('profile-card-', '');
customIds.push(profileId);
}
}
@@ -112,8 +109,8 @@ export async function getFirstCustomProfileId(page: Page): Promise<string | null
* Click the "New Profile" button in the header
*/
export async function clickNewProfileButton(page: Page): Promise<void> {
await clickElement(page, "add-profile-button");
await waitForElement(page, "add-profile-dialog");
await clickElement(page, 'add-profile-button');
await waitForElement(page, 'add-profile-dialog');
}
/**
@@ -124,7 +121,7 @@ export async function clickEmptyState(page: Page): Promise<void> {
'.group.rounded-xl.border.border-dashed[class*="cursor-pointer"]'
);
await emptyState.click();
await waitForElement(page, "add-profile-dialog");
await waitForElement(page, 'add-profile-dialog');
}
/**
@@ -161,10 +158,10 @@ export async function fillProfileForm(
* Click the save button to create/update a profile
*/
export async function saveProfile(page: Page): Promise<void> {
await clickElement(page, "save-profile-button");
await clickElement(page, 'save-profile-button');
// Wait for dialog to close
await waitForElementHidden(page, "add-profile-dialog").catch(() => {});
await waitForElementHidden(page, "edit-profile-dialog").catch(() => {});
await waitForElementHidden(page, 'add-profile-dialog').catch(() => {});
await waitForElementHidden(page, 'edit-profile-dialog').catch(() => {});
}
/**
@@ -175,46 +172,40 @@ export async function cancelProfileDialog(page: Page): Promise<void> {
const cancelButton = page.locator('button:has-text("Cancel")');
await cancelButton.click();
// Wait for dialog to close
await waitForElementHidden(page, "add-profile-dialog").catch(() => {});
await waitForElementHidden(page, "edit-profile-dialog").catch(() => {});
await waitForElementHidden(page, 'add-profile-dialog').catch(() => {});
await waitForElementHidden(page, 'edit-profile-dialog').catch(() => {});
}
/**
* Click the edit button for a specific profile
*/
export async function clickEditProfile(
page: Page,
profileId: string
): Promise<void> {
export async function clickEditProfile(page: Page, profileId: string): Promise<void> {
await clickElement(page, `edit-profile-${profileId}`);
await waitForElement(page, "edit-profile-dialog");
await waitForElement(page, 'edit-profile-dialog');
}
/**
* Click the delete button for a specific profile
*/
export async function clickDeleteProfile(
page: Page,
profileId: string
): Promise<void> {
export async function clickDeleteProfile(page: Page, profileId: string): Promise<void> {
await clickElement(page, `delete-profile-${profileId}`);
await waitForElement(page, "delete-profile-confirm-dialog");
await waitForElement(page, 'delete-profile-confirm-dialog');
}
/**
* Confirm profile deletion in the dialog
*/
export async function confirmDeleteProfile(page: Page): Promise<void> {
await clickElement(page, "confirm-delete-profile-button");
await waitForElementHidden(page, "delete-profile-confirm-dialog");
await clickElement(page, 'confirm-delete-profile-button');
await waitForElementHidden(page, 'delete-profile-confirm-dialog');
}
/**
* Cancel profile deletion
*/
export async function cancelDeleteProfile(page: Page): Promise<void> {
await clickElement(page, "cancel-delete-button");
await waitForElementHidden(page, "delete-profile-confirm-dialog");
await clickElement(page, 'cancel-delete-button');
await waitForElementHidden(page, 'delete-profile-confirm-dialog');
}
// ============================================================================
@@ -224,21 +215,15 @@ export async function cancelDeleteProfile(page: Page): Promise<void> {
/**
* Fill the profile name field
*/
export async function fillProfileName(
page: Page,
name: string
): Promise<void> {
await fillInput(page, "profile-name-input", name);
export async function fillProfileName(page: Page, name: string): Promise<void> {
await fillInput(page, 'profile-name-input', name);
}
/**
* Fill the profile description field
*/
export async function fillProfileDescription(
page: Page,
description: string
): Promise<void> {
await fillInput(page, "profile-description-input", description);
export async function fillProfileDescription(page: Page, description: string): Promise<void> {
await fillInput(page, 'profile-description-input', description);
}
/**
@@ -261,10 +246,7 @@ export async function selectModel(page: Page, modelId: string): Promise<void> {
* Select a thinking level for the profile
* @param level - Thinking level: none, low, medium, high, ultrathink
*/
export async function selectThinkingLevel(
page: Page,
level: string
): Promise<void> {
export async function selectThinkingLevel(page: Page, level: string): Promise<void> {
await clickElement(page, `thinking-select-${level}`);
}
@@ -273,11 +255,9 @@ export async function selectThinkingLevel(
*/
export async function getSelectedIcon(page: Page): Promise<string | null> {
// Find the icon button with primary background
const selectedIcon = page.locator(
'[data-testid^="icon-select-"][class*="bg-primary"]'
);
const testId = await selectedIcon.getAttribute("data-testid");
return testId ? testId.replace("icon-select-", "") : null;
const selectedIcon = page.locator('[data-testid^="icon-select-"][class*="bg-primary"]');
const testId = await selectedIcon.getAttribute('data-testid');
return testId ? testId.replace('icon-select-', '') : null;
}
/**
@@ -285,25 +265,19 @@ export async function getSelectedIcon(page: Page): Promise<string | null> {
*/
export async function getSelectedModel(page: Page): Promise<string | null> {
// Find the model button with primary background
const selectedModel = page.locator(
'[data-testid^="model-select-"][class*="bg-primary"]'
);
const testId = await selectedModel.getAttribute("data-testid");
return testId ? testId.replace("model-select-", "") : null;
const selectedModel = page.locator('[data-testid^="model-select-"][class*="bg-primary"]');
const testId = await selectedModel.getAttribute('data-testid');
return testId ? testId.replace('model-select-', '') : null;
}
/**
* Get the currently selected thinking level
*/
export async function getSelectedThinkingLevel(
page: Page
): Promise<string | null> {
export async function getSelectedThinkingLevel(page: Page): Promise<string | null> {
// Find the thinking level button with amber background
const selectedLevel = page.locator(
'[data-testid^="thinking-select-"][class*="bg-amber-500"]'
);
const testId = await selectedLevel.getAttribute("data-testid");
return testId ? testId.replace("thinking-select-", "") : null;
const selectedLevel = page.locator('[data-testid^="thinking-select-"][class*="bg-amber-500"]');
const testId = await selectedLevel.getAttribute('data-testid');
return testId ? testId.replace('thinking-select-', '') : null;
}
// ============================================================================
@@ -314,7 +288,7 @@ export async function getSelectedThinkingLevel(
* Check if the add profile dialog is open
*/
export async function isAddProfileDialogOpen(page: Page): Promise<boolean> {
const dialog = await getByTestId(page, "add-profile-dialog");
const dialog = await getByTestId(page, 'add-profile-dialog');
return await dialog.isVisible().catch(() => false);
}
@@ -322,7 +296,7 @@ export async function isAddProfileDialogOpen(page: Page): Promise<boolean> {
* Check if the edit profile dialog is open
*/
export async function isEditProfileDialogOpen(page: Page): Promise<boolean> {
const dialog = await getByTestId(page, "edit-profile-dialog");
const dialog = await getByTestId(page, 'edit-profile-dialog');
return await dialog.isVisible().catch(() => false);
}
@@ -330,7 +304,7 @@ export async function isEditProfileDialogOpen(page: Page): Promise<boolean> {
* Check if the delete confirmation dialog is open
*/
export async function isDeleteConfirmDialogOpen(page: Page): Promise<boolean> {
const dialog = await getByTestId(page, "delete-profile-confirm-dialog");
const dialog = await getByTestId(page, 'delete-profile-confirm-dialog');
return await dialog.isVisible().catch(() => false);
}
@@ -341,17 +315,15 @@ export async function isDeleteConfirmDialogOpen(page: Page): Promise<boolean> {
export async function waitForDialogClose(page: Page): Promise<void> {
// Wait for all profile dialogs to be hidden
await Promise.all([
waitForElementHidden(page, "add-profile-dialog").catch(() => {}),
waitForElementHidden(page, "edit-profile-dialog").catch(() => {}),
waitForElementHidden(page, "delete-profile-confirm-dialog").catch(
() => {}
),
waitForElementHidden(page, 'add-profile-dialog').catch(() => {}),
waitForElementHidden(page, 'edit-profile-dialog').catch(() => {}),
waitForElementHidden(page, 'delete-profile-confirm-dialog').catch(() => {}),
]);
// Also wait for any Radix dialog overlay to be removed (handles animation)
await page
.locator('[data-radix-dialog-overlay]')
.waitFor({ state: "hidden", timeout: 2000 })
.waitFor({ state: 'hidden', timeout: 2000 })
.catch(() => {
// Overlay may not exist
});
@@ -364,39 +336,30 @@ export async function waitForDialogClose(page: Page): Promise<void> {
/**
* Get the profile name from a card
*/
export async function getProfileName(
page: Page,
profileId: string
): Promise<string> {
export async function getProfileName(page: Page, profileId: string): Promise<string> {
const card = await getProfileCard(page, profileId);
const nameElement = card.locator("h3");
return await nameElement.textContent().then((text) => text?.trim() || "");
const nameElement = card.locator('h3');
return await nameElement.textContent().then((text) => text?.trim() || '');
}
/**
* Get the profile description from a card
*/
export async function getProfileDescription(
page: Page,
profileId: string
): Promise<string> {
export async function getProfileDescription(page: Page, profileId: string): Promise<string> {
const card = await getProfileCard(page, profileId);
const descElement = card.locator("p").first();
return await descElement.textContent().then((text) => text?.trim() || "");
const descElement = card.locator('p').first();
return await descElement.textContent().then((text) => text?.trim() || '');
}
/**
* Get the profile model badge text from a card
*/
export async function getProfileModel(
page: Page,
profileId: string
): Promise<string> {
export async function getProfileModel(page: Page, profileId: string): Promise<string> {
const card = await getProfileCard(page, profileId);
const modelBadge = card.locator(
'span[class*="border-primary"]:has-text("haiku"), span[class*="border-primary"]:has-text("sonnet"), span[class*="border-primary"]:has-text("opus")'
);
return await modelBadge.textContent().then((text) => text?.trim() || "");
return await modelBadge.textContent().then((text) => text?.trim() || '');
}
/**
@@ -410,16 +373,13 @@ export async function getProfileThinkingLevel(
const thinkingBadge = card.locator('span[class*="border-amber-500"]');
const isVisible = await thinkingBadge.isVisible().catch(() => false);
if (!isVisible) return null;
return await thinkingBadge.textContent().then((text) => text?.trim() || "");
return await thinkingBadge.textContent().then((text) => text?.trim() || '');
}
/**
* Check if a profile has the built-in badge
*/
export async function isBuiltInProfile(
page: Page,
profileId: string
): Promise<boolean> {
export async function isBuiltInProfile(page: Page, profileId: string): Promise<boolean> {
const card = await getProfileCard(page, profileId);
const builtInBadge = card.locator('span:has-text("Built-in")');
return await builtInBadge.isVisible().catch(() => false);
@@ -428,17 +388,14 @@ export async function isBuiltInProfile(
/**
* Check if the edit button is visible for a profile
*/
export async function isEditButtonVisible(
page: Page,
profileId: string
): Promise<boolean> {
export async function isEditButtonVisible(page: Page, profileId: string): Promise<boolean> {
const card = await getProfileCard(page, profileId);
// Hover over card to make buttons visible
await card.hover();
const editButton = await getByTestId(page, `edit-profile-${profileId}`);
// Wait for button to become visible after hover (handles CSS transition)
try {
await editButton.waitFor({ state: "visible", timeout: 2000 });
await editButton.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
@@ -448,17 +405,14 @@ export async function isEditButtonVisible(
/**
* Check if the delete button is visible for a profile
*/
export async function isDeleteButtonVisible(
page: Page,
profileId: string
): Promise<boolean> {
export async function isDeleteButtonVisible(page: Page, profileId: string): Promise<boolean> {
const card = await getProfileCard(page, profileId);
// Hover over card to make buttons visible
await card.hover();
const deleteButton = await getByTestId(page, `delete-profile-${profileId}`);
// Wait for button to become visible after hover (handles CSS transition)
try {
await deleteButton.waitFor({ state: "visible", timeout: 2000 });
await deleteButton.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
@@ -480,11 +434,7 @@ export async function isDeleteButtonVisible(
* @param fromIndex - 0-based index of the profile to drag
* @param toIndex - 0-based index of the target position
*/
export async function dragProfile(
page: Page,
fromIndex: number,
toIndex: number
): Promise<void> {
export async function dragProfile(page: Page, fromIndex: number, toIndex: number): Promise<void> {
// Get all profile cards
const cards = await page.locator('[data-testid^="profile-card-"]').all();
@@ -501,14 +451,14 @@ export async function dragProfile(
const dragHandle = fromCard.locator('[data-testid^="profile-drag-handle-"]');
// Ensure drag handle is visible and ready
await dragHandle.waitFor({ state: "visible", timeout: 5000 });
await dragHandle.waitFor({ state: 'visible', timeout: 5000 });
// Get bounding boxes
const handleBox = await dragHandle.boundingBox();
const toBox = await toCard.boundingBox();
if (!handleBox || !toBox) {
throw new Error("Unable to get bounding boxes for drag operation");
throw new Error('Unable to get bounding boxes for drag operation');
}
// Start position (center of drag handle)
@@ -549,10 +499,10 @@ export async function getProfileOrder(page: Page): Promise<string[]> {
const ids: string[] = [];
for (const card of cards) {
const testId = await card.getAttribute("data-testid");
const testId = await card.getAttribute('data-testid');
if (testId) {
// Extract profile ID from data-testid="profile-card-{id}"
const profileId = testId.replace("profile-card-", "");
const profileId = testId.replace('profile-card-', '');
ids.push(profileId);
}
}
@@ -568,5 +518,5 @@ export async function getProfileOrder(page: Page): Promise<string[]> {
* Click the "Refresh Defaults" button
*/
export async function clickRefreshDefaults(page: Page): Promise<void> {
await clickElement(page, "refresh-profiles-button");
await clickElement(page, 'refresh-profiles-button');
}

View File

@@ -1,4 +1,4 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get the settings view scrollable content area