Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions frontend/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,46 @@ Coverage data is automatically collected during CI/CD runs. The coverage reports
```
e2e/
├── tests/ # Test files
│ ├── admin/ # Admin panel tests
│ ├── api/ # API tests
│ ├── auth/ # Authentication tests
│ ├── settings/ # Settings page tests
│ └── tasks/ # Task management tests
├── pages/ # Page Object Models
│ ├── auth/ # Auth page objects
│ ├── admin/ # Admin page objects
│ └── settings/ # Settings page objects
├── fixtures/ # Test data and builders
├── helpers/ # Test utilities
│ └── coverage.ts # Coverage collection helper
├── utils/ # Shared utilities
└── config/ # Test configuration
│ ├── admin/ # Admin panel tests
│ ├── api/ # API tests
│ ├── auth/ # Authentication tests
│ ├── chat/ # Chat page tests (/chat route)
│ ├── code/ # Code page tests (/code route)
│ ├── integration/ # Integration tests
│ ├── knowledge/ # Knowledge base tests
│ ├── performance/ # Performance tests
│ ├── settings/ # Settings page tests
│ ├── shared/ # Shared/public task tests
│ ├── tasks/ # Task management tests
│ └── visual/ # Visual regression tests
├── pages/ # Page Object Models
│ ├── auth/ # Auth page objects
│ ├── admin/ # Admin page objects
│ ├── settings/ # Settings page objects
│ └── tasks/ # Task page objects
│ ├── base-task.page.ts # Shared Chat/Code functionality
│ ├── chat-task.page.ts # Chat-specific page object
│ └── code-task.page.ts # Code-specific page object
├── fixtures/ # Test data and builders
├── helpers/ # Test utilities
│ └── coverage.ts # Coverage collection helper
├── utils/ # Shared utilities
└── config/ # Test configuration
```

### Chat vs Code Test Separation

The application has two main task execution routes:

- **`/chat`** - Chat-only interface (no workbench/code editor)
- **`/code`** - Code interface with Workbench (includes code editor, file explorer)

Tests are organized to match this structure:

- `tests/chat/` - Tests specific to the Chat interface
- `tests/code/` - Tests specific to the Code interface (Workbench, repo selector, etc.)
- `pages/tasks/base-task.page.ts` - Shared Page Object for common functionality

## Page Object Model

Tests use the Page Object Model pattern for better maintainability:
Expand Down
2 changes: 2 additions & 0 deletions frontend/e2e/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export { ModelsPage } from './settings/models.page'
export { TeamsPage } from './settings/teams.page'

// Tasks Pages
export { BaseTaskPage } from './tasks/base-task.page'
export { ChatTaskPage } from './tasks/chat-task.page'
export { CodeTaskPage } from './tasks/code-task.page'

// Groups Pages
export { GroupsPage } from './groups/group-list.page'
Expand Down
220 changes: 220 additions & 0 deletions frontend/e2e/pages/tasks/base-task.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { Page, Locator } from '@playwright/test'
import { BasePage } from '../base.page'

/**
* Base Task Page - Shared functionality between Chat and Code pages
* Both /chat and /code routes share common UI elements like:
* - Team selector
* - Message input
- Send button
* - Task sidebar
* - Message list
*/
export abstract class BaseTaskPage extends BasePage {
// Common locators shared between Chat and Code pages
protected readonly messageInput: Locator
protected readonly sendButton: Locator
protected readonly teamSelector: Locator
protected readonly taskSidebar: Locator
protected readonly messageList: Locator
protected readonly newTaskButton: Locator

constructor(page: Page) {
super(page)
this.messageInput = page
.locator(
'[data-testid="message-input"], textarea[placeholder*="message" i], textarea[placeholder*="type" i], textarea'
)
.first()
this.sendButton = page
.locator(
'[data-testid="send-button"], button[type="submit"]:has-text("Send"), button[type="submit"]:has-text("发送")'
)
.first()
this.teamSelector = page
.locator(
'[data-testid="team-selector"], [data-tour="team-selector"] [role="combobox"], [role="combobox"]'
)
.first()
this.taskSidebar = page
.locator('[data-testid="task-sidebar"], [data-testid="conversation-list"], aside')
.first()
this.messageList = page
.locator('[data-testid="message-list"], [data-testid="messages"], .message-list')
.first()
this.newTaskButton = page
.locator(
'button:has-text("New"), button:has-text("新建"), [data-testid="new-task"], [data-testid="new-chat"]'
)
.first()
}

/**
* Check if message input is visible and enabled
*/
async isMessageInputReady(): Promise<boolean> {
try {
await this.messageInput.waitFor({ state: 'visible', timeout: 5000 })
return await this.messageInput.isEnabled()
} catch {
return false
}
}

/**
* Type a message in the input field
*/
async typeMessage(message: string): Promise<void> {
await this.messageInput.fill(message)
}

/**
* Send the current message
*/
async sendMessage(message?: string): Promise<void> {
if (message) {
await this.typeMessage(message)
}
await this.sendButton.click()
}

/**
* Check if team selector is available
*/
async hasTeamSelector(): Promise<boolean> {
const count = await this.teamSelector.count()
if (count === 0) return false
return await this.teamSelector.isVisible().catch(() => false)
}

/**
* Select a team by name
*/
async selectTeam(teamName: string): Promise<void> {
await this.teamSelector.click({ force: true })
await this.page.waitForTimeout(300)
const option = this.page.locator(`[role="option"]:has-text("${teamName}")`)
await option.click()
await this.page.waitForTimeout(500)
}

/**
* Get the currently selected team name
*/
async getSelectedTeam(): Promise<string | null> {
try {
return await this.teamSelector.textContent()
} catch {
return null
}
}

/**
* Click new task button to create a new task
*/
async createNewTask(): Promise<void> {
await this.newTaskButton.click()
await this.waitForLoading()
}

/**
* Check if new task button is visible
*/
async hasNewTaskButton(): Promise<boolean> {
return await this.newTaskButton.isVisible().catch(() => false)
}

/**
* Wait for a response message to appear
*/
async waitForResponse(timeout: number = 30000): Promise<void> {
await this.page.waitForSelector('[data-testid="message"], [data-role="assistant"], .message', {
timeout,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/**
* Get all message contents
*/
async getMessages(): Promise<string[]> {
const messages = this.page.locator(
'[data-testid="message-content"], .message-content, [data-testid="message"]'
)
return await messages.allTextContents()
}

/**
* Get the count of messages
*/
async getMessageCount(): Promise<number> {
return await this.page.locator('[data-testid="message"], .message').count()
}

/**
* Check if task sidebar is visible
*/
async isSidebarVisible(): Promise<boolean> {
return await this.taskSidebar.isVisible().catch(() => false)
}

/**
* Click on a task in the sidebar by index
*/
async selectTaskByIndex(index: number = 0): Promise<void> {
const taskItems = this.page.locator('[data-testid="task-item"], .task-item')
await taskItems.nth(index).click()
await this.waitForLoading()
}

/**
* Get the number of tasks in the sidebar
*/
async getTaskCount(): Promise<number> {
return await this.page.locator('[data-testid="task-item"], .task-item').count()
}

/**
* Cancel current running task
*/
async cancelTask(): Promise<void> {
const cancelButton = this.page.locator(
'button:has-text("Cancel"), button:has-text("Stop"), button:has-text("取消"), [data-testid="cancel-task"]'
)
if (await cancelButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await cancelButton.click()
await this.waitForLoading()
}
}

/**
* Check if there's a visible cancel button
*/
async hasCancelButton(): Promise<boolean> {
return await this.page
.locator('button:has-text("Cancel"), button:has-text("取消"), [data-testid="cancel-task"]')
.isVisible()
.catch(() => false)
}

/**
* Wait for streaming/loading to complete
*/
async waitForStreamingComplete(timeout: number = 60000): Promise<void> {
await this.page
.waitForSelector('[data-streaming="true"], .streaming', { state: 'detached', timeout })
.catch(() => {})
await this.page
.waitForSelector('[data-testid="send-button"]:not([disabled])', { timeout })
.catch(() => {})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/**
* Check if streaming is in progress
*/
async isStreaming(): Promise<boolean> {
const streamingIndicator = this.page.locator(
'[data-streaming="true"], .streaming, [class*="loading"]'
)
return await streamingIndicator.isVisible().catch(() => false)
}
}
Loading
Loading