mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 22:53:08 +00:00
Add orphaned features management routes and UI integration (#819)
* test(copilot): add edge case test for error with code field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Changes from fix/bug-fixes-1-0 * refactor(auto-mode): enhance orphaned feature detection and improve project initialization - Updated detectOrphanedFeatures method to accept preloaded features, reducing redundant disk reads. - Improved project initialization by creating required directories and files in parallel for better performance. - Adjusted planning mode handling in UI components to clarify approval requirements for different modes. - Added refresh functionality for file editor tabs to ensure content consistency with disk state. These changes enhance performance, maintainability, and user experience across the application. * feat(orphaned-features): add orphaned features management routes and UI integration - Introduced new routes for managing orphaned features, including listing, resolving, and bulk resolving. - Updated the UI to include an Orphaned Features section in project settings and navigation. - Enhanced the execution service to support new orphaned feature functionalities. These changes improve the application's capability to handle orphaned features effectively, enhancing user experience and project management. * fix: Normalize line endings and resolve stale dirty states in file editor * chore: Update .gitignore and enhance orphaned feature handling - Added a blank line in .gitignore for better readability. - Introduced a hash to worktree paths in orphaned feature resolution to prevent conflicts. - Added validation for target branch existence during orphaned feature resolution. - Improved prompt formatting in execution service for clarity. - Enhanced error handling in project selector for project initialization failures. - Refactored orphaned features section to improve state management and UI responsiveness. These changes improve code maintainability and user experience when managing orphaned features. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Cleanup leftover E2E test artifact directories.
|
||||
* Used by globalSetup (start of run) and globalTeardown (end of run) to ensure
|
||||
* test/board-bg-test-*, test/edit-feature-test-*, etc. are removed.
|
||||
* Used by globalSetup (start of run) and globalTeardown (end of run) to ensure:
|
||||
* - test/board-bg-test-*, test/edit-feature-test-*, etc. are removed
|
||||
* - test/fixtures/.worker-* (worker-isolated fixture copies) are removed
|
||||
*
|
||||
* Per-spec afterAll hooks clean up their own dirs, but when workers crash,
|
||||
* runs are aborted, or afterAll fails, dirs can be left behind.
|
||||
@@ -25,9 +26,33 @@ const TEST_DIR_PREFIXES = [
|
||||
'skip-tests-toggle-test',
|
||||
'manual-review-test',
|
||||
'feature-backlog-test',
|
||||
'agent-output-modal-responsive',
|
||||
'responsive-modal-test',
|
||||
'success-log-contrast',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Remove worker-isolated fixture copies (test/fixtures/.worker-*).
|
||||
* These are created during test runs for parallel workers and should be
|
||||
* cleaned up after tests complete (or at start of next run).
|
||||
*/
|
||||
export function cleanupLeftoverFixtureWorkerDirs(): void {
|
||||
const fixturesBase = path.join(getWorkspaceRoot(), 'test', 'fixtures');
|
||||
if (!fs.existsSync(fixturesBase)) return;
|
||||
|
||||
const entries = fs.readdirSync(fixturesBase, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.worker-')) {
|
||||
const dirPath = path.join(fixturesBase, entry.name);
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
console.log('[Cleanup] Removed fixture worker dir', entry.name);
|
||||
} catch (err) {
|
||||
console.warn('[Cleanup] Failed to remove', dirPath, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupLeftoverTestDirs(): void {
|
||||
const testBase = path.join(getWorkspaceRoot(), 'test');
|
||||
if (!fs.existsSync(testBase)) return;
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
/**
|
||||
* Responsive testing utilities for modal components
|
||||
* These utilities help test responsive behavior across different screen sizes
|
||||
*/
|
||||
|
||||
import { Page, expect } from '@playwright/test';
|
||||
import { waitForElement } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Wait for viewport resize to stabilize by polling element dimensions
|
||||
* until they stop changing. Much more reliable than a fixed timeout.
|
||||
*/
|
||||
async function waitForLayoutStable(page: Page, testId: string, timeout = 2000): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
({ testId: tid, timeout: t }) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const el = document.querySelector(`[data-testid="${tid}"]`);
|
||||
if (!el) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
let lastWidth = el.clientWidth;
|
||||
let lastHeight = el.clientHeight;
|
||||
let stableCount = 0;
|
||||
const interval = setInterval(() => {
|
||||
const w = el.clientWidth;
|
||||
const h = el.clientHeight;
|
||||
if (w === lastWidth && h === lastHeight) {
|
||||
stableCount++;
|
||||
if (stableCount >= 3) {
|
||||
clearInterval(interval);
|
||||
resolve(true);
|
||||
}
|
||||
} else {
|
||||
stableCount = 0;
|
||||
lastWidth = w;
|
||||
lastHeight = h;
|
||||
}
|
||||
}, 50);
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve(true);
|
||||
}, t);
|
||||
});
|
||||
},
|
||||
{ testId, timeout },
|
||||
{ timeout: timeout + 500 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport sizes for different device types
|
||||
*/
|
||||
export const VIEWPORTS = {
|
||||
mobile: { width: 375, height: 667 },
|
||||
mobileLarge: { width: 414, height: 896 },
|
||||
tablet: { width: 768, height: 1024 },
|
||||
tabletLarge: { width: 1024, height: 1366 },
|
||||
desktop: { width: 1280, height: 720 },
|
||||
desktopLarge: { width: 1920, height: 1080 },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Expected responsive classes for AgentOutputModal
|
||||
*/
|
||||
export const EXPECTED_CLASSES = {
|
||||
mobile: {
|
||||
width: ['w-full', 'max-w-[calc(100%-2rem)]'],
|
||||
height: ['max-h-[85dvh]'],
|
||||
},
|
||||
small: {
|
||||
width: ['sm:w-[60vw]', 'sm:max-w-[60vw]'],
|
||||
height: ['sm:max-h-[80vh]'],
|
||||
},
|
||||
tablet: {
|
||||
width: ['md:w-[90vw]', 'md:max-w-[1200px]'],
|
||||
height: ['md:max-h-[85vh]'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the computed width of the modal in pixels
|
||||
*/
|
||||
export async function getModalWidth(page: Page): Promise<number> {
|
||||
const modal = page.locator('[data-testid="agent-output-modal"]');
|
||||
return await modal.evaluate((el) => el.offsetWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed height of the modal in pixels
|
||||
*/
|
||||
export async function getModalHeight(page: Page): Promise<number> {
|
||||
const modal = page.locator('[data-testid="agent-output-modal"]');
|
||||
return await modal.evaluate((el) => el.offsetHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed style properties of the modal
|
||||
*/
|
||||
export async function getModalComputedStyle(page: Page): Promise<{
|
||||
width: string;
|
||||
height: string;
|
||||
maxWidth: string;
|
||||
maxHeight: string;
|
||||
}> {
|
||||
const modal = page.locator('[data-testid="agent-output-modal"]');
|
||||
return await modal.evaluate((el) => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return {
|
||||
width: style.width,
|
||||
height: style.height,
|
||||
maxWidth: style.maxWidth,
|
||||
maxHeight: style.maxHeight,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if modal has expected classes for a specific viewport
|
||||
*/
|
||||
export async function expectModalResponsiveClasses(
|
||||
page: Page,
|
||||
viewport: keyof typeof VIEWPORTS,
|
||||
expectedClasses: string[]
|
||||
): Promise<void> {
|
||||
const modal = page.locator('[data-testid="agent-output-modal"]');
|
||||
|
||||
for (const className of expectedClasses) {
|
||||
await expect(modal).toContainClass(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test modal width across different viewports
|
||||
*/
|
||||
export async function testModalWidthAcrossViewports(
|
||||
page: Page,
|
||||
viewports: Array<keyof typeof VIEWPORTS>
|
||||
): Promise<void> {
|
||||
for (const viewport of viewports) {
|
||||
const size = VIEWPORTS[viewport];
|
||||
|
||||
// Set viewport
|
||||
await page.setViewportSize(size);
|
||||
|
||||
// Wait for any responsive transitions
|
||||
await waitForLayoutStable(page, 'agent-output-modal');
|
||||
|
||||
// Get modal width
|
||||
const modalWidth = await getModalWidth(page);
|
||||
const viewportWidth = size.width;
|
||||
|
||||
// Check constraints based on viewport
|
||||
if (viewport === 'mobile' || viewport === 'mobileLarge') {
|
||||
// Mobile: should be close to full width with 2rem margins
|
||||
expect(modalWidth).toBeGreaterThan(viewportWidth - 40);
|
||||
expect(modalWidth).toBeLessThan(viewportWidth - 20);
|
||||
} else if (viewport === 'tablet' || viewport === 'tabletLarge') {
|
||||
// Tablet: should be around 90vw but not exceed max-w-[1200px]
|
||||
const expected90vw = Math.floor(viewportWidth * 0.9);
|
||||
expect(modalWidth).toBeLessThanOrEqual(expected90vw);
|
||||
expect(modalWidth).toBeLessThanOrEqual(1200);
|
||||
} else if (viewport === 'desktop' || viewport === 'desktopLarge') {
|
||||
// Desktop: should be bounded by viewport and max-width constraints
|
||||
const expectedMaxWidth = Math.floor(viewportWidth * 0.9);
|
||||
const modalHeight = await getModalHeight(page);
|
||||
const viewportHeight = size.height;
|
||||
const expectedMaxHeight = Math.floor(viewportHeight * 0.9);
|
||||
expect(modalWidth).toBeLessThanOrEqual(expectedMaxWidth);
|
||||
expect(modalWidth).toBeLessThanOrEqual(1200);
|
||||
expect(modalWidth).toBeGreaterThan(0);
|
||||
expect(modalHeight).toBeLessThanOrEqual(expectedMaxHeight);
|
||||
expect(modalHeight).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test modal height across different viewports
|
||||
*/
|
||||
export async function testModalHeightAcrossViewports(
|
||||
page: Page,
|
||||
viewports: Array<keyof typeof VIEWPORTS>
|
||||
): Promise<void> {
|
||||
for (const viewport of viewports) {
|
||||
const size = VIEWPORTS[viewport];
|
||||
|
||||
// Set viewport
|
||||
await page.setViewportSize(size);
|
||||
|
||||
// Wait for any responsive transitions
|
||||
await waitForLayoutStable(page, 'agent-output-modal');
|
||||
|
||||
// Get modal height
|
||||
const modalHeight = await getModalHeight(page);
|
||||
const viewportHeight = size.height;
|
||||
|
||||
// Check constraints based on viewport
|
||||
if (viewport === 'mobile' || viewport === 'mobileLarge') {
|
||||
// Mobile: should be max-h-[85dvh]
|
||||
const expected85dvh = Math.floor(viewportHeight * 0.85);
|
||||
expect(modalHeight).toBeLessThanOrEqual(expected85dvh);
|
||||
} else if (viewport === 'tablet' || viewport === 'tabletLarge') {
|
||||
// Tablet: should be max-h-[85vh]
|
||||
const expected85vh = Math.floor(viewportHeight * 0.85);
|
||||
expect(modalHeight).toBeLessThanOrEqual(expected85vh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test modal responsiveness during resize
|
||||
*/
|
||||
export async function testModalResponsiveResize(
|
||||
page: Page,
|
||||
fromViewport: keyof typeof VIEWPORTS,
|
||||
toViewport: keyof typeof VIEWPORTS
|
||||
): Promise<void> {
|
||||
// Set initial viewport
|
||||
await page.setViewportSize(VIEWPORTS[fromViewport]);
|
||||
await waitForLayoutStable(page, 'agent-output-modal');
|
||||
|
||||
// Get initial modal dimensions (used for comparison context)
|
||||
await getModalComputedStyle(page);
|
||||
|
||||
// Resize to new viewport
|
||||
await page.setViewportSize(VIEWPORTS[toViewport]);
|
||||
await waitForLayoutStable(page, 'agent-output-modal');
|
||||
|
||||
// Get new modal dimensions
|
||||
const newDimensions = await getModalComputedStyle(page);
|
||||
|
||||
// Verify dimensions changed appropriately using resolved pixel values
|
||||
const toSize = VIEWPORTS[toViewport];
|
||||
if (fromViewport === 'mobile' && toViewport === 'tablet') {
|
||||
const widthPx = parseFloat(newDimensions.width);
|
||||
const maxWidthPx = parseFloat(newDimensions.maxWidth);
|
||||
const expected90vw = toSize.width * 0.9;
|
||||
expect(widthPx).toBeLessThanOrEqual(expected90vw + 2);
|
||||
expect(maxWidthPx).toBeGreaterThanOrEqual(1200);
|
||||
} else if (fromViewport === 'tablet' && toViewport === 'mobile') {
|
||||
const widthPx = parseFloat(newDimensions.width);
|
||||
const maxWidthPx = parseFloat(newDimensions.maxWidth);
|
||||
expect(widthPx).toBeGreaterThan(toSize.width - 60);
|
||||
expect(maxWidthPx).toBeLessThan(1200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify modal maintains functionality across viewports
|
||||
*/
|
||||
export async function verifyModalFunctionalityAcrossViewports(
|
||||
page: Page,
|
||||
viewports: Array<keyof typeof VIEWPORTS>
|
||||
): Promise<void> {
|
||||
for (const viewport of viewports) {
|
||||
const size = VIEWPORTS[viewport];
|
||||
|
||||
// Set viewport
|
||||
await page.setViewportSize(size);
|
||||
await waitForLayoutStable(page, 'agent-output-modal');
|
||||
|
||||
// Verify modal is visible
|
||||
const modal = await waitForElement(page, 'agent-output-modal');
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Verify modal content is visible
|
||||
const description = page.locator('[data-testid="agent-output-description"]');
|
||||
await expect(description).toBeVisible();
|
||||
|
||||
// Verify view mode buttons are visible
|
||||
if (
|
||||
viewport === 'tablet' ||
|
||||
viewport === 'tabletLarge' ||
|
||||
viewport === 'desktop' ||
|
||||
viewport === 'desktopLarge'
|
||||
) {
|
||||
const logsButton = page.getByTestId('view-mode-parsed');
|
||||
await expect(logsButton).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* Create a deterministic temp directory path for a test suite.
|
||||
* The directory is NOT created on disk — call fs.mkdirSync in beforeAll.
|
||||
*/
|
||||
export function createTempDirPath(prefix: string): string {
|
||||
return path.join(os.tmpdir(), `automaker-test-${prefix}-${process.pid}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a temp directory and all its contents.
|
||||
* Silently ignores errors (e.g. directory already removed).
|
||||
*/
|
||||
export function cleanupTempDir(dirPath: string): void {
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user