refactor(conversation-assistant): migrate settings to webapp
- Delete SettingsView.swift - settings now in webapp modal - Add updateBaseURL method to APIClient for dynamic URL changes - Update ConversationAssistantApp to use EmptyView for Settings scene - Add comprehensive E2E tests for settings modal functionality - Open/close settings modal - Save settings - Reset data confirmation flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5339875f80
commit
d70dbd9920
5 changed files with 222 additions and 79 deletions
|
|
@ -104,6 +104,46 @@ export const mockApi = {
|
|||
needsFullDiskAccess: true,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Mock settings endpoint
|
||||
*/
|
||||
async settings(page: any, settings: Partial<SettingsResponse> = {}) {
|
||||
await page.route('**/api/settings', async (route: any) => {
|
||||
const method = route.request().method();
|
||||
if (method === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
apiBaseURL: 'http://localhost:3100',
|
||||
version: '0.0.1',
|
||||
fullVersion: 'Version 0.0.1 (abc123)',
|
||||
...settings,
|
||||
}),
|
||||
});
|
||||
} else if (method === 'POST') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Mock reset-data endpoint
|
||||
*/
|
||||
async resetData(page: any) {
|
||||
await page.route('**/api/reset-data', async (route: any) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
interface StatusResponse {
|
||||
|
|
@ -119,3 +159,9 @@ interface StatusResponse {
|
|||
isResetting: boolean;
|
||||
activityLog: Array<{ message: string; level: string; timestamp: string }>;
|
||||
}
|
||||
|
||||
interface SettingsResponse {
|
||||
apiBaseURL: string;
|
||||
version: string;
|
||||
fullVersion: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,3 +226,170 @@ test.describe('macOS Client - Footer', () => {
|
|||
expect(quitCalled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('macOS Client - Settings Modal', () => {
|
||||
test('opens settings modal and displays current settings', async ({ page }) => {
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page, {
|
||||
apiBaseURL: 'https://api.example.com',
|
||||
fullVersion: 'Version 1.2.3 (abc123)',
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Click settings button
|
||||
await page.locator('#btn-settings').click();
|
||||
|
||||
// Should show settings modal
|
||||
await expect(page.locator('#settings-modal')).toBeVisible();
|
||||
|
||||
// Should display current API URL
|
||||
await expect(page.locator('#input-api-url')).toHaveValue('https://api.example.com');
|
||||
|
||||
// Should display version in about section
|
||||
await expect(page.locator('#about-version')).toHaveText('Version 1.2.3 (abc123)');
|
||||
});
|
||||
|
||||
test('closes settings modal with close button', async ({ page }) => {
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings
|
||||
await page.locator('#btn-settings').click();
|
||||
await expect(page.locator('#settings-modal')).toBeVisible();
|
||||
|
||||
// Close with X button
|
||||
await page.locator('#btn-close-settings').click();
|
||||
|
||||
// Should be hidden
|
||||
await expect(page.locator('#settings-modal')).toBeHidden();
|
||||
});
|
||||
|
||||
test('closes settings modal with Escape key', async ({ page }) => {
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings
|
||||
await page.locator('#btn-settings').click();
|
||||
await expect(page.locator('#settings-modal')).toBeVisible();
|
||||
|
||||
// Press Escape
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Should be hidden
|
||||
await expect(page.locator('#settings-modal')).toBeHidden();
|
||||
});
|
||||
|
||||
test('saves settings when Save Changes is clicked', async ({ page }) => {
|
||||
let savedSettings: { apiBaseURL?: string } = {};
|
||||
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
|
||||
await page.route('**/api/settings', async (route) => {
|
||||
const method = route.request().method();
|
||||
if (method === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
apiBaseURL: 'http://localhost:3100',
|
||||
version: '0.0.1',
|
||||
fullVersion: 'Version 0.0.1',
|
||||
}),
|
||||
});
|
||||
} else if (method === 'POST') {
|
||||
const body = route.request().postDataJSON();
|
||||
savedSettings = body;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings
|
||||
await page.locator('#btn-settings').click();
|
||||
|
||||
// Change API URL
|
||||
await page.locator('#input-api-url').fill('https://new-api.example.com');
|
||||
|
||||
// Save
|
||||
await page.locator('#btn-save-settings').click();
|
||||
|
||||
// Should have saved new URL
|
||||
expect(savedSettings.apiBaseURL).toBe('https://new-api.example.com');
|
||||
|
||||
// Modal should close
|
||||
await expect(page.locator('#settings-modal')).toBeHidden();
|
||||
});
|
||||
|
||||
test('shows reset confirmation modal', async ({ page }) => {
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings
|
||||
await page.locator('#btn-settings').click();
|
||||
|
||||
// Click Reset All Data
|
||||
await page.locator('#btn-reset-data').click();
|
||||
|
||||
// Should show reset confirmation modal
|
||||
await expect(page.locator('#reset-modal')).toBeVisible();
|
||||
await expect(page.locator('#reset-modal h2')).toHaveText('Reset All Data?');
|
||||
});
|
||||
|
||||
test('cancels reset when Cancel is clicked', async ({ page }) => {
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings and click reset
|
||||
await page.locator('#btn-settings').click();
|
||||
await page.locator('#btn-reset-data').click();
|
||||
|
||||
// Cancel
|
||||
await page.locator('#btn-cancel-reset').click();
|
||||
|
||||
// Reset modal should close, settings modal should still be open
|
||||
await expect(page.locator('#reset-modal')).toBeHidden();
|
||||
await expect(page.locator('#settings-modal')).toBeVisible();
|
||||
});
|
||||
|
||||
test('calls reset API when confirmed', async ({ page }) => {
|
||||
let resetCalled = false;
|
||||
|
||||
await mockApi.status(page, { isAuthenticated: true });
|
||||
await mockApi.settings(page);
|
||||
|
||||
await page.route('**/api/reset-data', async (route) => {
|
||||
resetCalled = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Open settings and click reset
|
||||
await page.locator('#btn-settings').click();
|
||||
await page.locator('#btn-reset-data').click();
|
||||
|
||||
// Confirm reset
|
||||
await page.locator('#btn-confirm-reset').click();
|
||||
|
||||
// Should have called reset API
|
||||
expect(resetCalled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ struct ConversationAssistantApp: App {
|
|||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
// No SwiftUI scenes - app is menu bar only with webapp UI
|
||||
Settings {
|
||||
SettingsView()
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import SwiftyJSON
|
|||
class APIClient {
|
||||
static let shared = APIClient()
|
||||
|
||||
private let baseURL: String
|
||||
private var baseURL: String
|
||||
private var authToken: String?
|
||||
private var deviceId: String?
|
||||
|
||||
|
|
@ -24,6 +24,12 @@ class APIClient {
|
|||
NSLog("APIClient: init - baseURL: \(baseURL), deviceId: \(deviceId ?? "nil"), authToken: \(authToken != nil ? "present" : "nil")")
|
||||
}
|
||||
|
||||
/// Update the API base URL (used when settings change)
|
||||
func updateBaseURL(_ newURL: String) {
|
||||
baseURL = newURL
|
||||
NSLog("APIClient: baseURL updated to \(newURL)")
|
||||
}
|
||||
|
||||
func registerDevice() async throws -> (deviceId: String, code: String) {
|
||||
let hardwareId = getHardwareId()
|
||||
let deviceName = Host.current().localizedName ?? "Mac"
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@AppStorage("apiBaseURL") private var apiBaseURL = "http://localhost:3100"
|
||||
@State private var showingResetAlert = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
TextField("API Server URL", text: $apiBaseURL)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
} header: {
|
||||
Text("Server Configuration")
|
||||
} footer: {
|
||||
Text("The URL of the Conversation Assistant server")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Reset All Data", role: .destructive) {
|
||||
showingResetAlert = true
|
||||
}
|
||||
} header: {
|
||||
Text("Danger Zone")
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "bubble.left.and.bubble.right.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityHidden(true)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Conversation Assistant")
|
||||
.font(.headline)
|
||||
Text(AppVersion.fullVersion)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
} header: {
|
||||
Text("About")
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 400, height: 280)
|
||||
.alert("Reset All Data?", isPresented: $showingResetAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Reset", role: .destructive) {
|
||||
resetAllData()
|
||||
}
|
||||
} message: {
|
||||
Text("This will remove all saved authentication and sync data. You will need to re-authenticate.")
|
||||
}
|
||||
}
|
||||
|
||||
private func resetAllData() {
|
||||
// Clear keychain
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
|
||||
// Clear user defaults
|
||||
UserDefaults.standard.removeObject(forKey: "deviceId")
|
||||
UserDefaults.standard.removeObject(forKey: "lastSync")
|
||||
|
||||
// Restart app
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue