cherry-studio/tests/e2e/pages/base.page.ts
fullex d0bd10190d
feat(test): e2e framework (#11494)
* 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>
2025-11-27 19:52:31 +08:00

111 lines
2.8 KiB
TypeScript

import type { Locator, Page } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
/**
* Base Page Object class.
* All page objects should extend this class.
*/
export abstract class BasePage {
constructor(protected page: Page) {}
/**
* Navigate to a path using HashRouter.
* The app uses HashRouter, so we need to change window.location.hash.
*/
async navigateTo(routePath: string): Promise<void> {
await this.page.evaluate((p) => {
window.location.hash = p
}, routePath)
await this.page.waitForLoadState('domcontentloaded')
}
/**
* Wait for an element to be visible.
*/
async waitForElement(selector: string, timeout: number = 10000): Promise<Locator> {
const locator = this.page.locator(selector)
await locator.waitFor({ state: 'visible', timeout })
return locator
}
/**
* Wait for an element to be hidden.
*/
async waitForElementHidden(selector: string, timeout: number = 10000): Promise<void> {
const locator = this.page.locator(selector)
await locator.waitFor({ state: 'hidden', timeout })
}
/**
* Take a screenshot for debugging.
*/
async takeScreenshot(name: string): Promise<void> {
const screenshotsDir = path.join(process.cwd(), 'test-results', 'screenshots')
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true })
}
await this.page.screenshot({
path: path.join(screenshotsDir, `${name}.png`),
fullPage: true
})
}
/**
* Get the current route from the hash.
*/
async getCurrentRoute(): Promise<string> {
const url = this.page.url()
const hash = new URL(url).hash
return hash.replace('#', '') || '/'
}
/**
* Click an element with retry.
*/
async clickWithRetry(selector: string, maxRetries: number = 3): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await this.page.click(selector, { timeout: 5000 })
return
} catch (error) {
if (i === maxRetries - 1) throw error
await this.page.waitForTimeout(500)
}
}
}
/**
* Fill an input field.
*/
async fillInput(selector: string, value: string): Promise<void> {
const input = this.page.locator(selector)
await input.fill(value)
}
/**
* Get text content of an element.
*/
async getTextContent(selector: string): Promise<string | null> {
const locator = this.page.locator(selector)
return locator.textContent()
}
/**
* Check if an element is visible.
*/
async isElementVisible(selector: string): Promise<boolean> {
const locator = this.page.locator(selector)
return locator.isVisible()
}
/**
* Count elements matching a selector.
*/
async countElements(selector: string): Promise<number> {
const locator = this.page.locator(selector)
return locator.count()
}
}