feat: enhance ESLint configuration and improve component error handling

- Updated ESLint configuration to include support for `.mjs` and `.cjs` file types, adding necessary global variables for Node.js and browser environments.
- Introduced a new `vite-env.d.ts` file to define environment variables for Vite, improving type safety.
- Refactored error handling in `file-browser-dialog.tsx`, `description-image-dropzone.tsx`, and `feature-image-upload.tsx` to omit error parameters, simplifying the catch blocks.
- Removed unused bug report button functionality from the sidebar, streamlining the component structure.
- Adjusted various components to improve code readability and maintainability, including updates to type imports and component props.

These changes aim to enhance the development experience by improving linting support and simplifying error handling across components.
This commit is contained in:
Kacper
2025-12-21 23:08:08 +01:00
parent 43c93fe19a
commit 26236d3d5b
40 changed files with 2013 additions and 2587 deletions

View File

@@ -3,12 +3,12 @@
* Provides helpers for creating test git repos and managing worktrees
*/
import * as fs from "fs";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { Page } from "@playwright/test";
import { sanitizeBranchName, TIMEOUTS } from "../core/constants";
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { Page } from '@playwright/test';
import { sanitizeBranchName, TIMEOUTS } from '../core/constants';
const execAsync = promisify(exec);
@@ -40,8 +40,8 @@ export interface FeatureData {
*/
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;
}
@@ -49,9 +49,9 @@ function getWorkspaceRoot(): string {
/**
* Create a unique temp directory path for tests
*/
export function createTempDirPath(prefix: string = "temp-worktree-tests"): string {
export function createTempDirPath(prefix: string = 'temp-worktree-tests'): string {
const uniqueId = `${process.pid}-${Math.random().toString(36).substring(2, 9)}`;
return path.join(getWorkspaceRoot(), "test", `${prefix}-${uniqueId}`);
return path.join(getWorkspaceRoot(), 'test', `${prefix}-${uniqueId}`);
}
/**
@@ -59,7 +59,7 @@ export function createTempDirPath(prefix: string = "temp-worktree-tests"): strin
*/
export function getWorktreePath(projectPath: string, branchName: string): string {
const sanitizedName = sanitizeBranchName(branchName);
return path.join(projectPath, ".worktrees", sanitizedName);
return path.join(projectPath, '.worktrees', sanitizedName);
}
// ============================================================================
@@ -79,25 +79,25 @@ export async function createTestGitRepo(tempDir: string): Promise<TestRepo> {
fs.mkdirSync(tmpDir, { recursive: true });
// Initialize git repo
await execAsync("git init", { cwd: tmpDir });
await execAsync('git init', { cwd: tmpDir });
await execAsync('git config user.email "test@example.com"', { cwd: tmpDir });
await execAsync('git config user.name "Test User"', { cwd: tmpDir });
// Create initial commit
fs.writeFileSync(path.join(tmpDir, "README.md"), "# Test Project\n");
await execAsync("git add .", { cwd: tmpDir });
fs.writeFileSync(path.join(tmpDir, 'README.md'), '# Test Project\n');
await execAsync('git add .', { cwd: tmpDir });
await execAsync('git commit -m "Initial commit"', { cwd: tmpDir });
// Create main branch explicitly
await execAsync("git branch -M main", { cwd: tmpDir });
await execAsync('git branch -M main', { cwd: tmpDir });
// Create .automaker directories
const automakerDir = path.join(tmpDir, ".automaker");
const featuresDir = path.join(automakerDir, "features");
const automakerDir = path.join(tmpDir, '.automaker');
const featuresDir = path.join(automakerDir, 'features');
fs.mkdirSync(featuresDir, { recursive: true });
// Create empty categories.json to avoid ENOENT errors in tests
fs.writeFileSync(path.join(automakerDir, "categories.json"), "[]");
fs.writeFileSync(path.join(automakerDir, 'categories.json'), '[]');
return {
path: tmpDir,
@@ -113,16 +113,16 @@ export async function createTestGitRepo(tempDir: string): Promise<TestRepo> {
export async function cleanupTestRepo(repoPath: string): Promise<void> {
try {
// Remove all worktrees first
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
}).catch(() => ({ stdout: "" }));
}).catch(() => ({ stdout: '' }));
const worktrees = stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
return pathLine ? pathLine.replace("worktree ", "") : null;
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean);
@@ -139,7 +139,7 @@ export async function cleanupTestRepo(repoPath: string): Promise<void> {
// Remove the repository
fs.rmSync(repoPath, { recursive: true, force: true });
} catch (error) {
console.error("Failed to cleanup test repo:", error);
console.error('Failed to cleanup test repo:', error);
}
}
@@ -171,18 +171,18 @@ export async function gitExec(
*/
export async function listWorktrees(repoPath: string): Promise<string[]> {
try {
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
});
return stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
if (!pathLine) return null;
// Normalize path separators to OS native (git on Windows returns forward slashes)
const worktreePath = pathLine.replace("worktree ", "");
const worktreePath = pathLine.replace('worktree ', '');
return path.normalize(worktreePath);
})
.filter(Boolean) as string[];
@@ -195,10 +195,10 @@ export async function listWorktrees(repoPath: string): Promise<string[]> {
* Get list of git branches
*/
export async function listBranches(repoPath: string): Promise<string[]> {
const { stdout } = await execAsync("git branch --list", { cwd: repoPath });
const { stdout } = await execAsync('git branch --list', { cwd: repoPath });
return stdout
.split("\n")
.map((line) => line.trim().replace(/^[*+]\s*/, ""))
.split('\n')
.map((line) => line.trim().replace(/^[*+]\s*/, ''))
.filter(Boolean);
}
@@ -206,7 +206,7 @@ export async function listBranches(repoPath: string): Promise<string[]> {
* Get the current branch name
*/
export async function getCurrentBranch(repoPath: string): Promise<string> {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: repoPath });
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: repoPath });
return stdout.trim();
}
@@ -233,7 +233,7 @@ export async function createWorktreeDirectly(
worktreePath?: string
): Promise<string> {
const sanitizedName = sanitizeBranchName(branchName);
const targetPath = worktreePath || path.join(repoPath, ".worktrees", sanitizedName);
const targetPath = worktreePath || path.join(repoPath, '.worktrees', sanitizedName);
await execAsync(`git worktree add "${targetPath}" -b ${branchName}`, { cwd: repoPath });
return targetPath;
@@ -257,7 +257,7 @@ export async function commitFile(
* Get the latest commit message
*/
export async function getLatestCommitMessage(repoPath: string): Promise<string> {
const { stdout } = await execAsync("git log --oneline -1", { cwd: repoPath });
const { stdout } = await execAsync('git log --oneline -1', { cwd: repoPath });
return stdout.trim();
}
@@ -268,32 +268,36 @@ export async function getLatestCommitMessage(repoPath: string): Promise<string>
/**
* Create a feature file in the test repo
*/
export function createTestFeature(repoPath: string, featureId: string, featureData: FeatureData): void {
const featuresDir = path.join(repoPath, ".automaker", "features");
export function createTestFeature(
repoPath: string,
featureId: string,
featureData: FeatureData
): void {
const featuresDir = path.join(repoPath, '.automaker', 'features');
const featureDir = path.join(featuresDir, featureId);
fs.mkdirSync(featureDir, { recursive: true });
fs.writeFileSync(path.join(featureDir, "feature.json"), JSON.stringify(featureData, null, 2));
fs.writeFileSync(path.join(featureDir, 'feature.json'), JSON.stringify(featureData, null, 2));
}
/**
* Read a feature file from the test repo
*/
export function readTestFeature(repoPath: string, featureId: string): FeatureData | null {
const featureFilePath = path.join(repoPath, ".automaker", "features", featureId, "feature.json");
const featureFilePath = path.join(repoPath, '.automaker', 'features', featureId, 'feature.json');
if (!fs.existsSync(featureFilePath)) {
return null;
}
return JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
}
/**
* List all feature directories in the test repo
*/
export function listTestFeatures(repoPath: string): string[] {
const featuresDir = path.join(repoPath, ".automaker", "features");
const featuresDir = path.join(repoPath, '.automaker', 'features');
if (!fs.existsSync(featuresDir)) {
return [];
@@ -312,8 +316,8 @@ export function listTestFeatures(repoPath: string): string[] {
export async function setupProjectWithPath(page: Page, projectPath: string): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-worktree",
name: "Worktree Test Project",
id: 'test-project-worktree',
name: 'Worktree Test Project',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -322,36 +326,36 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
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,
aiProfiles: [],
useWorktrees: true, // Enable worktree feature for tests
currentWorktreeByProject: {
[pathArg]: { path: null, branch: "main" }, // Initialize to main branch
[pathArg]: { path: null, branch: 'main' }, // Initialize to main branch
},
worktreesByProject: {},
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// 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));
}, projectPath);
}
@@ -359,11 +363,14 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
* Set up localStorage with a project pointing to a test repo with worktrees DISABLED
* Use this to test scenarios where the worktree feature flag is off
*/
export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: string): Promise<void> {
export async function setupProjectWithPathNoWorktrees(
page: Page,
projectPath: string
): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-no-worktree",
name: "Test Project (No Worktrees)",
id: 'test-project-no-worktree',
name: 'Test Project (No Worktrees)',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -372,10 +379,10 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
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,
@@ -387,19 +394,19 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// 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));
}, projectPath);
}
@@ -408,11 +415,14 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
* The currentWorktreeByProject points to a worktree path that no longer exists
* This simulates the scenario where a user previously selected a worktree that was later deleted
*/
export async function setupProjectWithStaleWorktree(page: Page, projectPath: string): Promise<void> {
export async function setupProjectWithStaleWorktree(
page: Page,
projectPath: string
): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-stale-worktree",
name: "Stale Worktree Test Project",
id: 'test-project-stale-worktree',
name: 'Stale Worktree Test Project',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -421,10 +431,10 @@ export async function setupProjectWithStaleWorktree(page: Page, projectPath: str
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,
@@ -432,26 +442,26 @@ export async function setupProjectWithStaleWorktree(page: Page, projectPath: str
useWorktrees: true, // Enable worktree feature for tests
currentWorktreeByProject: {
// This is STALE data - pointing to a worktree path that doesn't exist
[pathArg]: { path: "/non/existent/worktree/path", branch: "feature/deleted-branch" },
[pathArg]: { path: '/non/existent/worktree/path', branch: 'feature/deleted-branch' },
},
worktreesByProject: {},
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// 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));
}, projectPath);
}
@@ -477,8 +487,6 @@ export async function waitForBoardView(page: Page): Promise<void> {
await page.waitForFunction(
() => {
const boardView = document.querySelector('[data-testid="board-view"]');
const noProject = document.querySelector('[data-testid="board-view-no-project"]');
const loading = document.querySelector('[data-testid="board-view-loading"]');
// Return true only when board-view is visible (store hydrated with project)
return boardView !== null;
},
@@ -490,8 +498,10 @@ export async function waitForBoardView(page: Page): Promise<void> {
* Wait for the worktree selector to be visible
*/
export async function waitForWorktreeSelector(page: Page): Promise<void> {
await page.waitForSelector('[data-testid="worktree-selector"]', { timeout: TIMEOUTS.medium }).catch(() => {
// Fallback: wait for "Branch:" text
return page.getByText("Branch:").waitFor({ timeout: TIMEOUTS.medium });
});
await page
.waitForSelector('[data-testid="worktree-selector"]', { timeout: TIMEOUTS.medium })
.catch(() => {
// Fallback: wait for "Branch:" text
return page.getByText('Branch:').waitFor({ timeout: TIMEOUTS.medium });
});
}