- Add test helper library with assertion functions (scripts/lib/test-helpers.sh) - Add gateway integration test script (scripts/tests/gateway-test.sh) - Add configuration validation tool (scripts/validate-config.ts) - Add health-check.ts library with Tauri command wrappers - Add HealthStatusIndicator component to ConnectionStatus.tsx - Add E2E test specs for memory, settings, and team collaboration - Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state Key improvements: - Store architecture now properly documented as migrated - Tauri backend shown as 85-90% complete - Component integration status clarified Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
734 lines
22 KiB
TypeScript
734 lines
22 KiB
TypeScript
/**
|
|
* ZCLAW Settings E2E Tests
|
|
*
|
|
* Tests for settings page functionality with mocked Gateway responses.
|
|
* Covers model configuration, Channel management, and skill management.
|
|
*
|
|
* Test Categories:
|
|
* - Model Configuration: Load, save, switch models
|
|
* - Channel Configuration: Feishu channels, IM configuration
|
|
* - Skill Management: Browse, install, uninstall skills
|
|
* - General Settings: User profile, workspace, preferences
|
|
*/
|
|
|
|
import { test, expect, Page } from '@playwright/test';
|
|
import { setupMockGateway, mockResponses, mockErrorResponse } from '../fixtures/mock-gateway';
|
|
import { storeInspectors, STORE_NAMES } from '../fixtures/store-inspectors';
|
|
import { userActions, waitForAppReady, skipOnboarding, navigateToTab } from '../utils/user-actions';
|
|
|
|
// Test configuration
|
|
test.setTimeout(120000);
|
|
const BASE_URL = 'http://localhost:1420';
|
|
|
|
// ============================================
|
|
// Test Suite 1: Model Configuration Tests
|
|
// ============================================
|
|
test.describe('Settings - Model Configuration Tests', () => {
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('SET-MODEL-01: Models list loads correctly in settings', async ({ page }) => {
|
|
// Navigate to settings
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Navigate to Models/API section
|
|
const modelsTab = page.getByRole('tab', { name: /模型|model|api/i }).or(
|
|
page.locator('button').filter({ hasText: /模型|API/ })
|
|
);
|
|
if (await modelsTab.first().isVisible()) {
|
|
await modelsTab.first().click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Verify models are loaded via API
|
|
const modelsResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/models');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
expect(Array.isArray(modelsResponse)).toBe(true);
|
|
expect(modelsResponse.length).toBeGreaterThan(0);
|
|
|
|
// Verify model structure
|
|
const firstModel = modelsResponse[0];
|
|
expect(firstModel).toHaveProperty('id');
|
|
expect(firstModel).toHaveProperty('name');
|
|
expect(firstModel).toHaveProperty('provider');
|
|
});
|
|
|
|
test('SET-MODEL-02: Switch default model saves to configuration', async ({ page }) => {
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Get current model from store
|
|
const initialModel = await storeInspectors.getChatState<{
|
|
currentModel: string;
|
|
}>(page);
|
|
|
|
// Find model selector in settings
|
|
const modelSelector = page.locator('select').filter({
|
|
has: page.locator('option'),
|
|
}).or(
|
|
page.locator('[role="combobox"]').filter({ hasText: /model|模型/i })
|
|
);
|
|
|
|
if (await modelSelector.first().isVisible()) {
|
|
// Get available options
|
|
const options = await modelSelector.first().locator('option').allInnerTexts();
|
|
expect(options.length).toBeGreaterThan(0);
|
|
|
|
// Select a different model
|
|
const newModel = options.find(o => o !== initialModel?.currentModel) || options[0];
|
|
await modelSelector.first().selectOption({ label: newModel });
|
|
|
|
// Save settings
|
|
await userActions.saveSettings(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify model changed
|
|
const updatedModel = await storeInspectors.getChatState<{
|
|
currentModel: string;
|
|
}>(page);
|
|
|
|
// Model should be updated
|
|
expect(updatedModel?.currentModel).toBeDefined();
|
|
}
|
|
});
|
|
|
|
test('SET-MODEL-03: Model configuration persists across reload', async ({ page }) => {
|
|
// Set a model in chat store
|
|
await storeInspectors.setChatState(page, {
|
|
currentModel: 'claude-sonnet-4-20250514',
|
|
messages: [],
|
|
conversations: [],
|
|
currentConversationId: null,
|
|
currentAgent: null,
|
|
isStreaming: false,
|
|
sessionKey: null,
|
|
agents: [],
|
|
});
|
|
|
|
// Reload page
|
|
await page.reload();
|
|
await waitForAppReady(page);
|
|
|
|
// Verify model persisted
|
|
const state = await storeInspectors.getChatState<{
|
|
currentModel: string;
|
|
}>(page);
|
|
|
|
expect(state?.currentModel).toBe('claude-sonnet-4-20250514');
|
|
});
|
|
|
|
test('SET-MODEL-04: API configuration saves gateway URL', async ({ page }) => {
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Navigate to API/Gateway section
|
|
const apiTab = page.getByRole('tab', { name: /api|gateway|连接/i }).or(
|
|
page.locator('button').filter({ hasText: /API|Gateway/ })
|
|
);
|
|
if (await apiTab.first().isVisible()) {
|
|
await apiTab.first().click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Find gateway URL input
|
|
const gatewayInput = page.locator('input').filter({
|
|
has: page.locator('[placeholder*="gateway"], [placeholder*="url"]'),
|
|
}).or(
|
|
page.locator('input[name="gatewayUrl"]').or(
|
|
page.locator('input').filter({ hasText: /gateway|url/i })
|
|
)
|
|
);
|
|
|
|
if (await gatewayInput.first().isVisible()) {
|
|
const testUrl = 'http://127.0.0.1:50051';
|
|
await gatewayInput.first().fill(testUrl);
|
|
|
|
// Save settings
|
|
await userActions.saveSettings(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify URL saved
|
|
const gatewayConfig = await storeInspectors.getGatewayConfig(page);
|
|
expect(gatewayConfig.url).toBe(testUrl);
|
|
}
|
|
});
|
|
|
|
test('SET-MODEL-05: Invalid model selection shows error', async ({ page }) => {
|
|
// Mock error response for models
|
|
await mockErrorResponse(page, 'models', 500, 'Failed to load models');
|
|
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify error handling - UI should still be functional
|
|
const settingsPanel = page.locator('[role="tabpanel"]').or(
|
|
page.locator('.settings-content').or(page.locator('main'))
|
|
);
|
|
|
|
await expect(settingsPanel.first()).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Test Suite 2: Channel Configuration Tests
|
|
// ============================================
|
|
test.describe('Settings - Channel Configuration Tests', () => {
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('SET-CHAN-01: Channels list loads correctly', async ({ page }) => {
|
|
// Request channels list
|
|
const channelsResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/channels');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Channels should be an array
|
|
if (channelsResponse) {
|
|
expect(channelsResponse).toHaveProperty('channels');
|
|
expect(Array.isArray(channelsResponse.channels)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('SET-CHAN-02: Feishu channel status check', async ({ page }) => {
|
|
// Check Feishu status
|
|
const feishuResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/channels/feishu');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (feishuResponse?.channel) {
|
|
expect(feishuResponse.channel).toHaveProperty('id');
|
|
expect(feishuResponse.channel).toHaveProperty('type');
|
|
expect(feishuResponse.channel.type).toBe('feishu');
|
|
}
|
|
});
|
|
|
|
test('SET-CHAN-03: Create new IM channel', async ({ page }) => {
|
|
const newChannel = {
|
|
type: 'feishu',
|
|
name: 'Test Feishu Channel',
|
|
config: {
|
|
appId: 'test-app-id',
|
|
appSecret: 'test-secret',
|
|
},
|
|
enabled: true,
|
|
};
|
|
|
|
const createResponse = await page.evaluate(async (channel) => {
|
|
try {
|
|
const response = await fetch('/api/channels', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(channel),
|
|
});
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, newChannel);
|
|
|
|
// Should return created channel
|
|
if (createResponse?.channel) {
|
|
expect(createResponse.channel).toHaveProperty('id');
|
|
expect(createResponse.channel.name).toBe(newChannel.name);
|
|
}
|
|
});
|
|
|
|
test('SET-CHAN-04: Update channel configuration', async ({ page }) => {
|
|
const updateData = {
|
|
name: 'Updated Channel Name',
|
|
enabled: false,
|
|
};
|
|
|
|
const updateResponse = await page.evaluate(async (data) => {
|
|
try {
|
|
const response = await fetch('/api/channels/feishu', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, updateData);
|
|
|
|
// Should return updated channel
|
|
if (updateResponse?.channel) {
|
|
expect(updateResponse.channel.name).toBe(updateData.name);
|
|
}
|
|
});
|
|
|
|
test('SET-CHAN-05: Delete channel', async ({ page }) => {
|
|
const deleteResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/channels/test-channel-id', {
|
|
method: 'DELETE',
|
|
});
|
|
return { status: response.status, ok: response.ok };
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Delete should succeed
|
|
if (deleteResponse) {
|
|
expect([200, 204, 404]).toContain(deleteResponse.status);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Test Suite 3: Skill Management Tests
|
|
// ============================================
|
|
test.describe('Settings - Skill Management Tests', () => {
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('SET-SKILL-01: Skills catalog loads correctly', async ({ page }) => {
|
|
// Request skills list
|
|
const skillsResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/skills');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Skills should be an array
|
|
expect(skillsResponse).toHaveProperty('skills');
|
|
expect(Array.isArray(skillsResponse.skills)).toBe(true);
|
|
expect(skillsResponse.skills.length).toBeGreaterThan(0);
|
|
|
|
// Verify skill structure
|
|
const firstSkill = skillsResponse.skills[0];
|
|
expect(firstSkill).toHaveProperty('id');
|
|
expect(firstSkill).toHaveProperty('name');
|
|
});
|
|
|
|
test('SET-SKILL-02: Get skill details', async ({ page }) => {
|
|
const skillResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/skills/skill-code-review');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (skillResponse?.skill) {
|
|
expect(skillResponse.skill).toHaveProperty('id');
|
|
expect(skillResponse.skill).toHaveProperty('name');
|
|
expect(skillResponse.skill).toHaveProperty('description');
|
|
}
|
|
});
|
|
|
|
test('SET-SKILL-03: Create new skill', async ({ page }) => {
|
|
const newSkill = {
|
|
name: 'Test Skill',
|
|
description: 'A test skill for E2E testing',
|
|
triggers: [{ type: 'keyword', pattern: 'test' }],
|
|
actions: [{ type: 'respond', params: { message: 'Test response' } }],
|
|
enabled: true,
|
|
};
|
|
|
|
const createResponse = await page.evaluate(async (skill) => {
|
|
try {
|
|
const response = await fetch('/api/skills', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(skill),
|
|
});
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, newSkill);
|
|
|
|
// Should return created skill
|
|
if (createResponse?.skill) {
|
|
expect(createResponse.skill).toHaveProperty('id');
|
|
expect(createResponse.skill.name).toBe(newSkill.name);
|
|
}
|
|
});
|
|
|
|
test('SET-SKILL-04: Update skill configuration', async ({ page }) => {
|
|
const updateData = {
|
|
name: 'Updated Skill Name',
|
|
enabled: false,
|
|
};
|
|
|
|
const updateResponse = await page.evaluate(async (data) => {
|
|
try {
|
|
const response = await fetch('/api/skills/skill-code-review', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, updateData);
|
|
|
|
// Should return updated skill
|
|
if (updateResponse?.skill) {
|
|
expect(updateResponse.skill.name).toBe(updateData.name);
|
|
}
|
|
});
|
|
|
|
test('SET-SKILL-05: Delete skill', async ({ page }) => {
|
|
const deleteResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/skills/test-skill-id', {
|
|
method: 'DELETE',
|
|
});
|
|
return { status: response.status, ok: response.ok };
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Delete should succeed
|
|
if (deleteResponse) {
|
|
expect([200, 204, 404]).toContain(deleteResponse.status);
|
|
}
|
|
});
|
|
|
|
test('SET-SKILL-06: Skill triggers configuration', async ({ page }) => {
|
|
// Navigate to Skills tab
|
|
await navigateToTab(page, '技能');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Check if skill cards are visible
|
|
const skillCards = page.locator('.border.rounded-lg').filter({
|
|
hasText: /技能|skill/i,
|
|
});
|
|
|
|
// At minimum, the API should respond
|
|
const skillsResponse = await page.evaluate(async () => {
|
|
const response = await fetch('/api/skills');
|
|
return response.json();
|
|
});
|
|
|
|
expect(skillsResponse.skills).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Test Suite 4: General Settings Tests
|
|
// ============================================
|
|
test.describe('Settings - General Settings Tests', () => {
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('SET-GEN-01: Quick config loads correctly', async ({ page }) => {
|
|
const configResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/config');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Config should have expected fields
|
|
expect(configResponse).not.toBeNull();
|
|
expect(configResponse).toHaveProperty('userName');
|
|
expect(configResponse).toHaveProperty('userRole');
|
|
expect(configResponse).toHaveProperty('defaultModel');
|
|
});
|
|
|
|
test('SET-GEN-02: Save user profile settings', async ({ page }) => {
|
|
const newConfig = {
|
|
userName: 'Test User',
|
|
userRole: 'Developer',
|
|
};
|
|
|
|
const saveResponse = await page.evaluate(async (config) => {
|
|
try {
|
|
const response = await fetch('/api/config', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, newConfig);
|
|
|
|
// Should return updated config
|
|
if (saveResponse) {
|
|
expect(saveResponse.userName).toBe(newConfig.userName);
|
|
expect(saveResponse.userRole).toBe(newConfig.userRole);
|
|
}
|
|
});
|
|
|
|
test('SET-GEN-03: Workspace info loads correctly', async ({ page }) => {
|
|
const workspaceResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/workspace');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Workspace should have path info
|
|
expect(workspaceResponse).not.toBeNull();
|
|
expect(workspaceResponse).toHaveProperty('path');
|
|
expect(workspaceResponse).toHaveProperty('exists');
|
|
});
|
|
|
|
test('SET-GEN-04: Theme preference saves correctly', async ({ page }) => {
|
|
// Navigate to settings
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Find theme toggle
|
|
const themeToggle = page.locator('button').filter({
|
|
hasText: /theme|主题|dark|light|深色|浅色/i,
|
|
}).or(
|
|
page.locator('[role="switch"]').filter({ hasText: /dark|light/i })
|
|
);
|
|
|
|
if (await themeToggle.first().isVisible()) {
|
|
await themeToggle.first().click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify theme changed (check for dark class on html/body)
|
|
const isDark = await page.evaluate(() => {
|
|
return document.documentElement.classList.contains('dark');
|
|
});
|
|
|
|
// Theme toggle should work
|
|
expect(typeof isDark).toBe('boolean');
|
|
}
|
|
});
|
|
|
|
test('SET-GEN-05: Plugin status check', async ({ page }) => {
|
|
const pluginResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/plugins/status');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Plugins should be an array
|
|
if (pluginResponse) {
|
|
expect(Array.isArray(pluginResponse)).toBe(true);
|
|
|
|
if (pluginResponse.length > 0) {
|
|
const firstPlugin = pluginResponse[0];
|
|
expect(firstPlugin).toHaveProperty('id');
|
|
expect(firstPlugin).toHaveProperty('name');
|
|
expect(firstPlugin).toHaveProperty('status');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('SET-GEN-06: Scheduled tasks load correctly', async ({ page }) => {
|
|
const tasksResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/scheduler/tasks');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Tasks should be an array
|
|
if (tasksResponse) {
|
|
expect(tasksResponse).toHaveProperty('tasks');
|
|
expect(Array.isArray(tasksResponse.tasks)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('SET-GEN-07: Security status check', async ({ page }) => {
|
|
const securityResponse = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/security/status');
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Security should have status
|
|
if (securityResponse) {
|
|
expect(securityResponse).toHaveProperty('status');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Test Suite 5: Settings Integration Tests
|
|
// ============================================
|
|
test.describe('Settings - Integration Tests', () => {
|
|
|
|
test('SET-INT-01: Full settings save and reload cycle', async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// Open settings
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Get initial config
|
|
const initialConfig = await page.evaluate(async () => {
|
|
const response = await fetch('/api/config');
|
|
return response.json();
|
|
});
|
|
|
|
// Update config
|
|
const updatedConfig = {
|
|
...initialConfig,
|
|
userName: 'E2E Test User',
|
|
userRole: 'Tester',
|
|
};
|
|
|
|
await page.evaluate(async (config) => {
|
|
await fetch('/api/config', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
}, updatedConfig);
|
|
|
|
// Reload page
|
|
await page.reload();
|
|
await waitForAppReady(page);
|
|
|
|
// Verify config persisted
|
|
const reloadedConfig = await page.evaluate(async () => {
|
|
const response = await fetch('/api/config');
|
|
return response.json();
|
|
});
|
|
|
|
expect(reloadedConfig.userName).toBe(updatedConfig.userName);
|
|
expect(reloadedConfig.userRole).toBe(updatedConfig.userRole);
|
|
});
|
|
|
|
test('SET-INT-02: Settings navigation between tabs', async ({ page }) => {
|
|
await setupMockGateway(page);
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
await userActions.openSettings(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Find all tabs
|
|
const tabs = page.locator('[role="tab"]').or(
|
|
page.locator('button').filter({ has: page.locator('span') })
|
|
);
|
|
|
|
const tabCount = await tabs.count();
|
|
expect(tabCount).toBeGreaterThan(0);
|
|
|
|
// Click through each tab
|
|
for (let i = 0; i < Math.min(tabCount, 5); i++) {
|
|
const tab = tabs.nth(i);
|
|
if (await tab.isVisible()) {
|
|
await tab.click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
|
|
// Settings panel should still be visible
|
|
const settingsPanel = page.locator('[role="tabpanel"]').or(
|
|
page.locator('.settings-content')
|
|
);
|
|
await expect(settingsPanel.first()).toBeVisible();
|
|
});
|
|
|
|
test('SET-INT-03: Error handling for failed config save', async ({ page }) => {
|
|
// Mock error response for config
|
|
await mockErrorResponse(page, 'config', 500, 'Failed to save config');
|
|
|
|
await skipOnboarding(page);
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// Try to save config
|
|
const saveResult = await page.evaluate(async () => {
|
|
try {
|
|
const response = await fetch('/api/config', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userName: 'Test' }),
|
|
});
|
|
return { status: response.status, ok: response.ok };
|
|
} catch {
|
|
return { error: true };
|
|
}
|
|
});
|
|
|
|
// Should handle error gracefully
|
|
expect(saveResult.status).toBe(500);
|
|
expect(saveResult.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Test Report
|
|
// ============================================
|
|
test.afterAll(async ({}, testInfo) => {
|
|
console.log('\n========================================');
|
|
console.log('ZCLAW Settings E2E Tests Complete');
|
|
console.log('========================================');
|
|
console.log(`Test Time: ${new Date().toISOString()}`);
|
|
console.log('========================================\n');
|
|
});
|