n8n/packages/testing/playwright/pages/ChatHubChatPage.ts
yehorkardash 64079ad98c
feat(core): Agents as first class entities support (no-changelog) (#28017)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Michael Drury <michael.drury@n8n.io>
Co-authored-by: Arvin A <51036481+DeveloperTheExplorer@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Arvin Ansari <arvin.ansari@n8n.io>
Co-authored-by: bjorger <50590409+bjorger@users.noreply.github.com>
Co-authored-by: Eugene <eugene@n8n.io>
Co-authored-by: Michael Drury <me@michaeldrury.co.uk>
Co-authored-by: Robin Braumann <robin.braumann@n8n.io>
Co-authored-by: Rob Hough <robhough180@gmail.com>
2026-05-06 15:44:44 +00:00

165 lines
5.3 KiB
TypeScript

import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ChatHubCredentialModal } from './components/ChatHubCredentialModal';
import { ChatHubPersonalAgentModal } from './components/ChatHubPersonalAgentModal';
import { ChatHubSidebar } from './components/ChatHubSidebar';
import { ChatHubToolsModal } from './components/ChatHubToolsModal';
export class ChatHubChatPage extends BasePage {
async goto() {
await this.page.goto('/home/chat');
}
readonly sidebar = new ChatHubSidebar(this.page.locator('#sidebar'));
readonly toolsModal = new ChatHubToolsModal(
this.page.getByRole('dialog').filter({ has: this.page.locator('[data-tools-manager-modal]') }),
);
readonly credModal = new ChatHubCredentialModal(
this.page.getByTestId('chatCredentialSelectorModal-modal'),
);
readonly personalAgentModal = new ChatHubPersonalAgentModal(this.page.getByRole('dialog'));
constructor(page: Page) {
super(page);
}
getGreetingMessage(): Locator {
return this.page.getByRole('heading', { level: 2 });
}
getWelcomeStartNewChatButton(): Locator {
return this.page.getByTestId('welcome-start-new-chat');
}
async dismissWelcomeScreen(): Promise<void> {
// Wait for sessions to load - either the welcome screen or the model selector will appear
const welcomeButton = this.getWelcomeStartNewChatButton();
const modelSelector = this.getModelSelectorButton();
// Wait for either element to be visible (indicates sessions are loaded)
await expect(welcomeButton.or(modelSelector)).toBeVisible();
// If welcome screen is shown, click to dismiss it
if (await welcomeButton.isVisible()) {
await welcomeButton.click();
await welcomeButton.waitFor({ state: 'hidden' });
}
}
getModelSelectorButton(): Locator {
return this.page.getByTestId('chat-model-selector');
}
getSelectedCredentialName(): Locator {
return this.page.getByTestId('chat-model-selector-credential');
}
getChatInput(): Locator {
return this.page.locator('form').getByRole('textbox');
}
getSendButton(): Locator {
return this.page.getByTitle('Send');
}
getChatMessages(): Locator {
return this.page.locator('[data-message-id]');
}
getEditButtonAt(index: number): Locator {
return this.getChatMessages().nth(index).getByTestId('chat-message-edit');
}
getEditorAt(index: number): Locator {
return this.getChatMessages().nth(index).getByRole('textbox');
}
getSendButtonAt(index: number): Locator {
return this.getChatMessages().nth(index).getByText('Send');
}
getRegenerateButtonAt(index: number): Locator {
return this.getChatMessages().nth(index).getByTestId('chat-message-regenerate');
}
getPrevAlternativeButtonAt(index: number): Locator {
return this.getChatMessages().nth(index).getByTestId('chat-message-prev-alternative');
}
async clickEditButtonAt(index: number): Promise<void> {
await this.hoverMessageActionsAt(index);
const editButton = this.getEditButtonAt(index);
// Wait for streaming to complete - the button is disabled during streaming
await editButton.waitFor({ state: 'visible' });
await expect(editButton).toBeEnabled();
await editButton.click({ force: true });
}
async clickRegenerateButtonAt(index: number): Promise<void> {
await this.hoverMessageActionsAt(index);
const regenerateButton = this.getRegenerateButtonAt(index);
// Wait for streaming to complete - the button is disabled during streaming
await regenerateButton.waitFor({ state: 'visible' });
await expect(regenerateButton).toBeEnabled();
await regenerateButton.click({ force: true });
}
async clickPrevAlternativeButtonAt(index: number): Promise<void> {
await this.hoverMessageActionsAt(index);
const prevButton = this.getPrevAlternativeButtonAt(index);
// Wait for streaming to complete - the button is disabled during streaming
await prevButton.waitFor({ state: 'visible' });
await expect(prevButton).toBeEnabled();
await prevButton.click({ force: true });
}
/**
* Hovers over the message content area to reveal hidden action buttons.
* The action buttons are hidden by CSS until the content area is hovered.
*/
private async hoverMessageActionsAt(index: number): Promise<void> {
const message = this.getChatMessages().nth(index);
await message.hover();
await message.getByTestId('chat-message-actions').waitFor({ state: 'visible' });
}
getFileInput(): Locator {
return this.page.locator('input[type="file"]');
}
getAttachmentsAt(messageIndex: number): Locator {
return this.getChatMessages().nth(messageIndex).locator('.chat-file');
}
getToolsButton(): Locator {
return this.page.getByTestId('chat-tools-button');
}
getOpenWorkflowButton(): Locator {
return this.page.getByRole('button', { name: /open workflow/i });
}
async clickOpenWorkflowButton(): Promise<Page> {
const newPagePromise = this.page.context().waitForEvent('page');
await this.getOpenWorkflowButton().click();
const workflowPage = await newPagePromise;
await workflowPage.waitForLoadState();
return workflowPage;
}
async openAttachmentAt(messageIndex: number, attachmentIndex: number): Promise<Page> {
const [newPage] = await Promise.all([
this.page.context().waitForEvent('page'),
this.getAttachmentsAt(messageIndex).nth(attachmentIndex).click(),
]);
await newPage.waitForLoadState('load');
return newPage;
}
}