mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
* feat(test): e2e framework Add Playwright-based e2e testing framework for Electron app with: - Custom fixtures for electronApp and mainWindow - Page Object Model (POM) pattern implementation - 15 example test cases covering app launch, navigation, settings, and chat - Comprehensive README for humans and AI assistants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(tests): update imports and improve code readability - Changed imports from 'import { Page, Locator }' to 'import type { Locator, Page }' for better type clarity across multiple page files. - Reformatted waitFor calls in ChatPage and HomePage for improved readability. - Updated index.ts to correct the export order of ChatPage and SidebarPage. - Minor adjustments in electron.fixture.ts and electron-app.ts for consistency in import statements. These changes enhance the maintainability and clarity of the test codebase. * chore: update linting configuration to include tests directory - Added 'tests/**' to the ignore patterns in .oxlintrc.json and eslint.config.mjs to ensure test files are not linted. - Minor adjustment in electron.fixture.ts to improve the fixture definition. These changes streamline the linting process and enhance code organization. * fix(test): select main window by title to fix flaky e2e tests on Mac On Mac, the app may create miniWindow for QuickAssistant alongside mainWindow. Using firstWindow() could randomly select the wrong window, causing test failures. Now we wait for the window with title "Cherry Studio" to ensure we get the main window. Also removed unused electron-app.ts utility file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
160 lines
4.6 KiB
TypeScript
160 lines
4.6 KiB
TypeScript
import type { Locator, Page } from '@playwright/test'
|
|
|
|
import { BasePage } from './base.page'
|
|
|
|
/**
|
|
* Page Object for the Settings page.
|
|
* Handles navigation and interaction with various settings sections.
|
|
*/
|
|
export class SettingsPage extends BasePage {
|
|
readonly settingsContainer: Locator
|
|
readonly providerMenuItem: Locator
|
|
readonly modelMenuItem: Locator
|
|
readonly generalMenuItem: Locator
|
|
readonly displayMenuItem: Locator
|
|
readonly dataMenuItem: Locator
|
|
readonly mcpMenuItem: Locator
|
|
readonly memoryMenuItem: Locator
|
|
readonly aboutMenuItem: Locator
|
|
|
|
constructor(page: Page) {
|
|
super(page)
|
|
this.settingsContainer = page.locator('[id="content-container"], [class*="Settings"]')
|
|
this.providerMenuItem = page.locator('a[href*="/settings/provider"]')
|
|
this.modelMenuItem = page.locator('a[href*="/settings/model"]')
|
|
this.generalMenuItem = page.locator('a[href*="/settings/general"]')
|
|
this.displayMenuItem = page.locator('a[href*="/settings/display"]')
|
|
this.dataMenuItem = page.locator('a[href*="/settings/data"]')
|
|
this.mcpMenuItem = page.locator('a[href*="/settings/mcp"]')
|
|
this.memoryMenuItem = page.locator('a[href*="/settings/memory"]')
|
|
this.aboutMenuItem = page.locator('a[href*="/settings/about"]')
|
|
}
|
|
|
|
/**
|
|
* Navigate to settings page (provider by default).
|
|
*/
|
|
async goto(): Promise<void> {
|
|
await this.navigateTo('/settings/provider')
|
|
await this.waitForElement('[id="content-container"], [class*="Settings"]')
|
|
}
|
|
|
|
/**
|
|
* Check if settings page is loaded.
|
|
*/
|
|
async isLoaded(): Promise<boolean> {
|
|
return this.settingsContainer.first().isVisible()
|
|
}
|
|
|
|
/**
|
|
* Navigate to Provider settings.
|
|
*/
|
|
async goToProvider(): Promise<void> {
|
|
try {
|
|
await this.providerMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/provider')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/provider**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to Model settings.
|
|
*/
|
|
async goToModel(): Promise<void> {
|
|
try {
|
|
await this.modelMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/model')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/model**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to General settings.
|
|
*/
|
|
async goToGeneral(): Promise<void> {
|
|
try {
|
|
await this.generalMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/general')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/general**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to Display settings.
|
|
*/
|
|
async goToDisplay(): Promise<void> {
|
|
try {
|
|
await this.displayMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/display')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/display**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to Data settings.
|
|
*/
|
|
async goToData(): Promise<void> {
|
|
try {
|
|
await this.dataMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/data')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/data**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to MCP settings.
|
|
*/
|
|
async goToMCP(): Promise<void> {
|
|
try {
|
|
await this.mcpMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/mcp')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/mcp**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to Memory settings.
|
|
*/
|
|
async goToMemory(): Promise<void> {
|
|
try {
|
|
await this.memoryMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/memory')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/memory**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Navigate to About page.
|
|
*/
|
|
async goToAbout(): Promise<void> {
|
|
try {
|
|
await this.aboutMenuItem.click({ timeout: 5000 })
|
|
} catch {
|
|
await this.navigateTo('/settings/about')
|
|
}
|
|
await this.page.waitForURL('**/#/settings/about**', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
/**
|
|
* Toggle a switch setting by its label.
|
|
*/
|
|
async toggleSwitch(label: string): Promise<void> {
|
|
const switchElement = this.page.locator(`text=${label}`).locator('..').locator('button[role="switch"], .ant-switch')
|
|
await switchElement.first().click()
|
|
}
|
|
|
|
/**
|
|
* Check if a menu item is active/selected.
|
|
*/
|
|
async isMenuItemActive(menuItem: Locator): Promise<boolean> {
|
|
const className = await menuItem.getAttribute('class')
|
|
return className?.includes('active') || className?.includes('selected') || false
|
|
}
|
|
}
|