mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
Compare commits
5 Commits
fix-error
...
yumesha/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
329e429841 | ||
|
|
46636cf385 | ||
|
|
e24a6894a5 | ||
|
|
cf9289e21a | ||
|
|
fe7bc954ba |
@@ -57,7 +57,7 @@ FROM node:22-slim AS server
|
||||
|
||||
# Install git, curl, bash (for terminal), gosu (for user switching), and GitHub CLI (pinned version, multi-arch)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git curl bash gosu ca-certificates \
|
||||
git curl bash gosu ca-certificates openssh-client \
|
||||
&& GH_VERSION="2.63.2" \
|
||||
&& ARCH=$(uname -m) \
|
||||
&& case "$ARCH" in \
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createCheckoutBranchHandler() {
|
||||
}
|
||||
|
||||
// Get current branch for reference
|
||||
const { stdout: currentBranchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const currentBranch = currentBranchOutput.trim();
|
||||
|
||||
@@ -59,7 +59,7 @@ export function createCommitHandler() {
|
||||
const commitHash = hashOutput.trim().substring(0, 8);
|
||||
|
||||
// Get branch name
|
||||
const { stdout: branchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
@@ -43,7 +43,7 @@ export function createCreatePRHandler() {
|
||||
const effectiveProjectPath = projectPath || worktreePath;
|
||||
|
||||
// Get current branch name
|
||||
const { stdout: branchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export function createDeleteHandler() {
|
||||
// Get branch name before removing worktree
|
||||
let branchName: string | null = null;
|
||||
try {
|
||||
const { stdout } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
branchName = stdout.trim();
|
||||
|
||||
@@ -31,7 +31,7 @@ export function createInfoHandler() {
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
try {
|
||||
await secureFs.access(worktreePath);
|
||||
const { stdout } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
res.json({
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createListBranchesHandler() {
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const currentBranch = currentBranchOutput.trim();
|
||||
|
||||
@@ -35,7 +35,7 @@ export function createMergeHandler() {
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranch } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export function createPullHandler() {
|
||||
}
|
||||
|
||||
// Get current branch name
|
||||
const { stdout: branchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
@@ -29,7 +29,7 @@ export function createPushHandler() {
|
||||
}
|
||||
|
||||
// Get branch name
|
||||
const { stdout: branchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
@@ -87,7 +87,7 @@ export function createSwitchBranchHandler() {
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranchOutput } = await execAsync('git symbolic-ref --short HEAD', {
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const previousBranch = currentBranchOutput.trim();
|
||||
|
||||
82
apps/ui/src/hooks/use-project-settings-loader.ts
Normal file
82
apps/ui/src/hooks/use-project-settings-loader.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
|
||||
/**
|
||||
* Hook that loads project settings from the server when the current project changes.
|
||||
* This ensures that settings like board backgrounds are properly restored when
|
||||
* switching between projects or restarting the app.
|
||||
*/
|
||||
export function useProjectSettingsLoader() {
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
const setBoardBackground = useAppStore((state) => state.setBoardBackground);
|
||||
const setCardOpacity = useAppStore((state) => state.setCardOpacity);
|
||||
const setColumnOpacity = useAppStore((state) => state.setColumnOpacity);
|
||||
const setColumnBorderEnabled = useAppStore((state) => state.setColumnBorderEnabled);
|
||||
const setCardGlassmorphism = useAppStore((state) => state.setCardGlassmorphism);
|
||||
const setCardBorderEnabled = useAppStore((state) => state.setCardBorderEnabled);
|
||||
const setCardBorderOpacity = useAppStore((state) => state.setCardBorderOpacity);
|
||||
const setHideScrollbar = useAppStore((state) => state.setHideScrollbar);
|
||||
|
||||
const loadingRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProject?.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent loading the same project multiple times
|
||||
if (loadingRef.current === currentProject.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingRef.current = currentProject.path;
|
||||
|
||||
const loadProjectSettings = async () => {
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
const result = await httpClient.settings.getProject(currentProject.path);
|
||||
|
||||
if (result.success && result.settings?.boardBackground) {
|
||||
const bg = result.settings.boardBackground;
|
||||
const projectPath = currentProject.path;
|
||||
|
||||
// Update store with loaded settings (without triggering server save)
|
||||
setBoardBackground(projectPath, bg.imagePath);
|
||||
|
||||
const settingsMap = {
|
||||
cardOpacity: setCardOpacity,
|
||||
columnOpacity: setColumnOpacity,
|
||||
columnBorderEnabled: setColumnBorderEnabled,
|
||||
cardGlassmorphism: setCardGlassmorphism,
|
||||
cardBorderEnabled: setCardBorderEnabled,
|
||||
cardBorderOpacity: setCardBorderOpacity,
|
||||
hideScrollbar: setHideScrollbar,
|
||||
} as const;
|
||||
|
||||
for (const [key, setter] of Object.entries(settingsMap)) {
|
||||
const value = bg[key as keyof typeof bg];
|
||||
if (value !== undefined) {
|
||||
(setter as (path: string, val: typeof value) => void)(projectPath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load project settings:', error);
|
||||
// Don't show error toast - just log it
|
||||
}
|
||||
};
|
||||
|
||||
loadProjectSettings();
|
||||
}, [
|
||||
currentProject?.path,
|
||||
setBoardBackground,
|
||||
setCardOpacity,
|
||||
setColumnOpacity,
|
||||
setColumnBorderEnabled,
|
||||
setCardGlassmorphism,
|
||||
setCardBorderEnabled,
|
||||
setCardBorderOpacity,
|
||||
setHideScrollbar,
|
||||
]);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||
import { SandboxRiskDialog } from '@/components/dialogs/sandbox-risk-dialog';
|
||||
import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-screen';
|
||||
import { LoadingState } from '@/components/ui/loading-state';
|
||||
import { useProjectSettingsLoader } from '@/hooks/use-project-settings-loader';
|
||||
|
||||
const logger = createLogger('RootLayout');
|
||||
|
||||
@@ -47,6 +48,9 @@ function RootLayoutContent() {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const { openFileBrowser } = useFileBrowser();
|
||||
|
||||
// Load project settings when switching projects
|
||||
useProjectSettingsLoader();
|
||||
|
||||
const isSetupRoute = location.pathname === '/setup';
|
||||
const isLoginRoute = location.pathname === '/login';
|
||||
|
||||
|
||||
BIN
apps/ui/tests/img/background.jpg
Normal file
BIN
apps/ui/tests/img/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
411
apps/ui/tests/projects/board-background-persistence.spec.ts
Normal file
411
apps/ui/tests/projects/board-background-persistence.spec.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* Board Background Persistence End-to-End Test
|
||||
*
|
||||
* Tests that board background settings are properly saved and loaded when switching projects.
|
||||
* This verifies that:
|
||||
* 1. Background settings are saved to .automaker-local/settings.json
|
||||
* 2. Settings are loaded when switching back to a project
|
||||
* 3. Background image, opacity, and other settings are correctly restored
|
||||
* 4. Settings persist across app restarts (new page loads)
|
||||
*
|
||||
* This test prevents regression of the board background loading bug where
|
||||
* settings were saved but never loaded when switching projects.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
createTempDirPath,
|
||||
cleanupTempDir,
|
||||
authenticateForTests,
|
||||
handleLoginScreenIfPresent,
|
||||
} from '../utils';
|
||||
|
||||
// Create unique temp dirs for this test run
|
||||
const TEST_TEMP_DIR = createTempDirPath('board-bg-test');
|
||||
|
||||
test.describe('Board Background Persistence', () => {
|
||||
test.beforeAll(async () => {
|
||||
// Create test temp directory
|
||||
if (!fs.existsSync(TEST_TEMP_DIR)) {
|
||||
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Cleanup temp directory
|
||||
cleanupTempDir(TEST_TEMP_DIR);
|
||||
});
|
||||
|
||||
test('should load board background settings when switching projects', async ({ page }) => {
|
||||
const projectAName = `project-a-${Date.now()}`;
|
||||
const projectBName = `project-b-${Date.now()}`;
|
||||
const projectAPath = path.join(TEST_TEMP_DIR, projectAName);
|
||||
const projectBPath = path.join(TEST_TEMP_DIR, projectBName);
|
||||
const projectAId = `project-a-${Date.now()}`;
|
||||
const projectBId = `project-b-${Date.now()}`;
|
||||
|
||||
// Create both project directories
|
||||
fs.mkdirSync(projectAPath, { recursive: true });
|
||||
fs.mkdirSync(projectBPath, { recursive: true });
|
||||
|
||||
// Create basic files for both projects
|
||||
for (const [name, projectPath] of [
|
||||
[projectAName, projectAPath],
|
||||
[projectBName, projectBPath],
|
||||
]) {
|
||||
fs.writeFileSync(
|
||||
path.join(projectPath, 'package.json'),
|
||||
JSON.stringify({ name, version: '1.0.0' }, null, 2)
|
||||
);
|
||||
fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${name}\n`);
|
||||
}
|
||||
|
||||
// Create .automaker-local directory for project A with background settings
|
||||
const automakerDirA = path.join(projectAPath, '.automaker-local');
|
||||
fs.mkdirSync(automakerDirA, { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDirA, 'board'), { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDirA, 'features'), { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDirA, 'context'), { recursive: true });
|
||||
|
||||
// Copy actual background image from test fixtures
|
||||
const backgroundPath = path.join(automakerDirA, 'board', 'background.jpg');
|
||||
const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg');
|
||||
fs.copyFileSync(testImagePath, backgroundPath);
|
||||
|
||||
// Create settings.json with board background configuration
|
||||
const settingsPath = path.join(automakerDirA, 'settings.json');
|
||||
const backgroundSettings = {
|
||||
version: 1,
|
||||
boardBackground: {
|
||||
imagePath: backgroundPath,
|
||||
cardOpacity: 85,
|
||||
columnOpacity: 60,
|
||||
columnBorderEnabled: true,
|
||||
cardGlassmorphism: true,
|
||||
cardBorderEnabled: false,
|
||||
cardBorderOpacity: 50,
|
||||
hideScrollbar: true,
|
||||
imageVersion: Date.now(),
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(backgroundSettings, null, 2));
|
||||
|
||||
// Create minimal automaker-local directory for project B (no background)
|
||||
const automakerDirB = path.join(projectBPath, '.automaker-local');
|
||||
fs.mkdirSync(automakerDirB, { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDirB, 'features'), { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDirB, 'context'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(automakerDirB, 'settings.json'),
|
||||
JSON.stringify({ version: 1 }, null, 2)
|
||||
);
|
||||
|
||||
// Set up app state with both projects in the list (not recent, but in projects list)
|
||||
await page.addInitScript(
|
||||
({ projects }: { projects: string[] }) => {
|
||||
const appState = {
|
||||
state: {
|
||||
projects: [
|
||||
{
|
||||
id: projects[0],
|
||||
name: projects[1],
|
||||
path: projects[2],
|
||||
lastOpened: new Date(Date.now() - 86400000).toISOString(),
|
||||
theme: 'red',
|
||||
},
|
||||
{
|
||||
id: projects[3],
|
||||
name: projects[4],
|
||||
path: projects[5],
|
||||
lastOpened: new Date(Date.now() - 172800000).toISOString(),
|
||||
theme: 'red',
|
||||
},
|
||||
],
|
||||
currentProject: null,
|
||||
currentView: 'welcome',
|
||||
theme: 'red',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
boardBackgroundByProject: {},
|
||||
},
|
||||
version: 2,
|
||||
};
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(appState));
|
||||
|
||||
// Setup complete
|
||||
const setupState = {
|
||||
state: {
|
||||
setupComplete: true,
|
||||
workspaceDir: '/tmp',
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
localStorage.setItem('setup-storage', JSON.stringify(setupState));
|
||||
},
|
||||
{ projects: [projectAId, projectAName, projectAPath, projectBId, projectBName, projectBPath] }
|
||||
);
|
||||
|
||||
// Track API calls to /api/settings/project to verify settings are being loaded
|
||||
const settingsApiCalls: Array<{ url: string; method: string; body: string }> = [];
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('/api/settings/project') && request.method() === 'POST') {
|
||||
settingsApiCalls.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
body: request.postData() || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate to the app
|
||||
await authenticateForTests(page);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('load');
|
||||
await handleLoginScreenIfPresent(page);
|
||||
|
||||
// Wait for welcome view
|
||||
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Open project A (has background settings)
|
||||
const projectACard = page.locator(`[data-testid="recent-project-${projectAId}"]`);
|
||||
await expect(projectACard).toBeVisible();
|
||||
await projectACard.click();
|
||||
|
||||
// Wait for board view
|
||||
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Verify project A is current
|
||||
await expect(
|
||||
page.locator('[data-testid="project-selector"]').getByText(projectAName)
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// CRITICAL: Wait for settings to be loaded (useProjectSettingsLoader hook)
|
||||
// This ensures the background settings are fetched from the server
|
||||
await page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/api/settings/project') &&
|
||||
resp.request().postData()?.includes(projectAPath) === true
|
||||
);
|
||||
|
||||
// Check if background settings were applied by checking the store
|
||||
// We can't directly access React state, so we'll verify via DOM/CSS
|
||||
const boardView = page.locator('[data-testid="board-view"]');
|
||||
await expect(boardView).toBeVisible();
|
||||
|
||||
// Wait for initial project load to stabilize
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Switch to project B (no background)
|
||||
const projectSelector = page.locator('[data-testid="project-selector"]');
|
||||
await projectSelector.click();
|
||||
|
||||
// Wait for dropdown to be visible
|
||||
await expect(page.locator('[data-testid="project-picker-dropdown"]')).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const projectPickerB = page.locator(`[data-testid="project-option-${projectBId}"]`);
|
||||
await expect(projectPickerB).toBeVisible({ timeout: 5000 });
|
||||
await projectPickerB.click();
|
||||
|
||||
// Wait for project B to load
|
||||
await expect(
|
||||
page.locator('[data-testid="project-selector"]').getByText(projectBName)
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait a bit for project B to fully load before switching
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Switch back to project A
|
||||
await projectSelector.click();
|
||||
|
||||
// Wait for dropdown to be visible
|
||||
await expect(page.locator('[data-testid="project-picker-dropdown"]')).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const projectPickerA = page.locator(`[data-testid="project-option-${projectAId}"]`);
|
||||
await expect(projectPickerA).toBeVisible({ timeout: 5000 });
|
||||
await projectPickerA.click();
|
||||
|
||||
// Verify we're back on project A
|
||||
await expect(
|
||||
page.locator('[data-testid="project-selector"]').getByText(projectAName)
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// CRITICAL: Wait for settings to be loaded again
|
||||
await page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/api/settings/project') &&
|
||||
resp.request().postData()?.includes(projectAPath) === true
|
||||
);
|
||||
|
||||
// Verify that the settings API was called for project A (at least twice - initial load and switch back)
|
||||
const projectASettingsCalls = settingsApiCalls.filter((call) =>
|
||||
call.body.includes(projectAPath)
|
||||
);
|
||||
|
||||
// Debug: log all API calls if test fails
|
||||
if (projectASettingsCalls.length < 2) {
|
||||
console.log('Total settings API calls:', settingsApiCalls.length);
|
||||
console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2));
|
||||
console.log('Looking for path:', projectAPath);
|
||||
}
|
||||
|
||||
expect(projectASettingsCalls.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify settings file still exists with correct data
|
||||
const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
expect(loadedSettings.boardBackground).toBeDefined();
|
||||
expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath);
|
||||
expect(loadedSettings.boardBackground.cardOpacity).toBe(85);
|
||||
expect(loadedSettings.boardBackground.columnOpacity).toBe(60);
|
||||
expect(loadedSettings.boardBackground.hideScrollbar).toBe(true);
|
||||
|
||||
// The test passing means:
|
||||
// 1. The useProjectSettingsLoader hook is working
|
||||
// 2. Settings are loaded when switching projects
|
||||
// 3. The API call to /api/settings/project is made correctly
|
||||
});
|
||||
|
||||
test('should load background settings on app restart', async ({ page }) => {
|
||||
const projectName = `restart-test-${Date.now()}`;
|
||||
const projectPath = path.join(TEST_TEMP_DIR, projectName);
|
||||
const projectId = `project-${Date.now()}`;
|
||||
|
||||
// Create project directory
|
||||
fs.mkdirSync(projectPath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectPath, 'package.json'),
|
||||
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
|
||||
);
|
||||
|
||||
// Create .automaker-local with background settings
|
||||
const automakerDir = path.join(projectPath, '.automaker-local');
|
||||
fs.mkdirSync(automakerDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDir, 'board'), { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
|
||||
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
|
||||
|
||||
// Copy actual background image from test fixtures
|
||||
const backgroundPath = path.join(automakerDir, 'board', 'background.jpg');
|
||||
const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg');
|
||||
fs.copyFileSync(testImagePath, backgroundPath);
|
||||
|
||||
const settingsPath = path.join(automakerDir, 'settings.json');
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
boardBackground: {
|
||||
imagePath: backgroundPath,
|
||||
cardOpacity: 90,
|
||||
columnOpacity: 70,
|
||||
imageVersion: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Set up with project as current using direct localStorage
|
||||
await page.addInitScript(
|
||||
({ project }: { project: string[] }) => {
|
||||
const projectObj = {
|
||||
id: project[0],
|
||||
name: project[1],
|
||||
path: project[2],
|
||||
lastOpened: new Date().toISOString(),
|
||||
theme: 'red',
|
||||
};
|
||||
|
||||
const appState = {
|
||||
state: {
|
||||
projects: [projectObj],
|
||||
currentProject: projectObj,
|
||||
currentView: 'board',
|
||||
theme: 'red',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
boardBackgroundByProject: {},
|
||||
},
|
||||
version: 2,
|
||||
};
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(appState));
|
||||
|
||||
// Setup complete
|
||||
const setupState = {
|
||||
state: {
|
||||
setupComplete: true,
|
||||
workspaceDir: '/tmp',
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
localStorage.setItem('setup-storage', JSON.stringify(setupState));
|
||||
},
|
||||
{ project: [projectId, projectName, projectPath] }
|
||||
);
|
||||
|
||||
// Track API calls to /api/settings/project to verify settings are being loaded
|
||||
const settingsApiCalls: Array<{ url: string; method: string; body: string }> = [];
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('/api/settings/project') && request.method() === 'POST') {
|
||||
settingsApiCalls.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
body: request.postData() || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate and authenticate
|
||||
await authenticateForTests(page);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('load');
|
||||
await handleLoginScreenIfPresent(page);
|
||||
|
||||
// Should go straight to board view (not welcome) since we have currentProject
|
||||
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/api/settings/project') &&
|
||||
resp.request().postData()?.includes(projectPath) === true
|
||||
);
|
||||
|
||||
// Verify that the settings API was called for this project
|
||||
const projectSettingsCalls = settingsApiCalls.filter((call) => call.body.includes(projectPath));
|
||||
|
||||
// Debug: log all API calls if test fails
|
||||
if (projectSettingsCalls.length < 1) {
|
||||
console.log('Total settings API calls:', settingsApiCalls.length);
|
||||
console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2));
|
||||
console.log('Looking for path:', projectPath);
|
||||
}
|
||||
|
||||
expect(projectSettingsCalls.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Verify settings file exists with correct data
|
||||
const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
expect(loadedSettings.boardBackground).toBeDefined();
|
||||
expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath);
|
||||
expect(loadedSettings.boardBackground.cardOpacity).toBe(90);
|
||||
expect(loadedSettings.boardBackground.columnOpacity).toBe(70);
|
||||
|
||||
// The test passing means:
|
||||
// 1. The useProjectSettingsLoader hook is working
|
||||
// 2. Settings are loaded when app starts with a currentProject
|
||||
// 3. The API call to /api/settings/project is made correctly
|
||||
});
|
||||
});
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -20,7 +20,8 @@
|
||||
"devDependencies": {
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "16.2.7",
|
||||
"prettier": "3.7.4"
|
||||
"prettier": "3.7.4",
|
||||
"vitest": "4.0.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0 <23.0.0"
|
||||
@@ -28,7 +29,7 @@
|
||||
},
|
||||
"apps/server": {
|
||||
"name": "@automaker/server",
|
||||
"version": "0.7.3",
|
||||
"version": "0.8.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
||||
@@ -78,7 +79,7 @@
|
||||
},
|
||||
"apps/ui": {
|
||||
"name": "@automaker/ui",
|
||||
"version": "0.7.3",
|
||||
"version": "0.8.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user