test: Migrate Langchain e2e tests to Playwright (#19161)

This commit is contained in:
oleg
2025-09-08 18:06:00 +02:00
committed by GitHub
parent 574ec6e895
commit 64f260cb72
19 changed files with 1752 additions and 1469 deletions

View File

@@ -541,6 +541,28 @@ export class CanvasPage extends BasePage {
await this.clickContextMenuAction('execute');
}
async clearExecutionData(): Promise<void> {
await this.page.getByTestId('clear-execution-data-button').click();
}
getManualChatModal(): Locator {
return this.page.getByTestId('canvas-chat');
}
getManualChatInput(): Locator {
return this.getManualChatModal().locator('.chat-inputs textarea');
}
getManualChatMessages(): Locator {
return this.getManualChatModal().locator('.chat-messages-list .chat-message');
}
getManualChatLatestBotMessage(): Locator {
return this.getManualChatModal()
.locator('.chat-messages-list .chat-message.chat-message-from-bot')
.last();
}
getNodesWithSpinner(): Locator {
return this.page.getByTestId('canvas-node').filter({
has: this.page.locator('[data-icon=refresh-cw]'),
@@ -560,6 +582,67 @@ export class CanvasPage extends BasePage {
return this.page.locator('[data-test-id="canvas-node"].selected');
}
// Disable node via context menu
async disableNodeFromContextMenu(nodeName: string): Promise<void> {
await this.rightClickNode(nodeName);
await this.page
.getByTestId('context-menu')
.getByTestId('context-menu-item-toggle_activation')
.click();
}
// Chat open/close buttons (manual chat)
async clickManualChatButton(): Promise<void> {
await this.page.getByTestId('workflow-chat-button').click();
await this.getManualChatModal().waitFor({ state: 'visible' });
}
async closeManualChatModal(): Promise<void> {
// Same toggle button closes the chat
await this.page.getByTestId('workflow-chat-button').click();
}
// Input plus endpoints (to add supplemental nodes to parent inputs)
getInputPlusEndpointByType(nodeName: string, endpointType: string) {
return this.page
.locator(
`[data-test-id="canvas-node-input-handle"][data-connection-type="${endpointType}"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
)
.first();
}
// Generic supplemental node addition, then wrappers for specific types
async addSupplementalNodeToParent(
childNodeName: string,
endpointType:
| 'main'
| 'ai_chain'
| 'ai_document'
| 'ai_embedding'
| 'ai_languageModel'
| 'ai_memory'
| 'ai_outputParser'
| 'ai_tool'
| 'ai_retriever'
| 'ai_textSplitter'
| 'ai_vectorRetriever'
| 'ai_vectorStore',
parentNodeName: string,
{ closeNDV = false, exactMatch = false }: { closeNDV?: boolean; exactMatch?: boolean } = {},
): Promise<void> {
await this.getInputPlusEndpointByType(parentNodeName, endpointType).click();
if (exactMatch) {
await this.nodeCreatorNodeItems().getByText(childNodeName, { exact: true }).click();
} else {
await this.nodeCreatorNodeItems().filter({ hasText: childNodeName }).first().click();
}
if (closeNDV) {
await this.page.keyboard.press('Escape');
}
}
async openExecutions() {
await this.page.getByTestId('radio-button-executions').click();
}

View File

@@ -0,0 +1,67 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class CredentialsEditModal extends BasePage {
constructor(page: Page) {
super(page);
}
getModal(): Locator {
return this.page.getByTestId('editCredential-modal');
}
async waitForModal(): Promise<void> {
await this.getModal().waitFor({ state: 'visible' });
}
async fillField(key: string, value: string): Promise<void> {
const input = this.page.getByTestId(`parameter-input-${key}`).locator('input, textarea');
await input.fill(value);
await expect(input).toHaveValue(value);
}
async fillAllFields(values: Record<string, string>): Promise<void> {
for (const [key, val] of Object.entries(values)) {
await this.fillField(key, val);
}
}
getSaveButton(): Locator {
return this.page.getByTestId('credential-save-button');
}
async save(): Promise<void> {
const saveBtn = this.getSaveButton();
await saveBtn.click();
await saveBtn.waitFor({ state: 'visible' });
// Saved state changes the button text to "Saved"
// Defensive wait for text when UI updates
try {
await saveBtn
.getByText('Saved', { exact: true })
.waitFor({ state: 'visible', timeout: 3000 });
} catch {
// ignore if text assertion is flaky; modal close below will still ensure flow continues
}
}
async close(): Promise<void> {
const closeBtn = this.getModal().locator('.el-dialog__close').first();
if (await closeBtn.isVisible()) {
await closeBtn.click();
}
}
async setValues(values: Record<string, string>, save: boolean = true): Promise<void> {
await this.waitForModal();
await this.fillAllFields(values);
if (save) {
await this.save();
await this.close();
}
}
}

View File

@@ -52,10 +52,18 @@ export class NodeDetailsViewPage extends BasePage {
await this.clickByTestId('node-execute-button');
}
getOutputPanel() {
return this.page.getByTestId('output-panel');
}
getContainer() {
return this.page.getByTestId('ndv');
}
getInputPanel() {
return this.page.getByTestId('ndv-input-panel');
}
getParameterExpressionPreviewValue() {
return this.page.getByTestId('parameter-expression-preview-value');
}
@@ -81,6 +89,14 @@ export class NodeDetailsViewPage extends BasePage {
return this.page.getByTestId('run-data-pane-header');
}
getOutputTable() {
return this.getOutputPanel().getByTestId('ndv-data-container').locator('table');
}
getOutputDataContainer() {
return this.getOutputPanel().getByTestId('ndv-data-container');
}
async setPinnedData(data: object | string) {
const pinnedData = typeof data === 'string' ? data : JSON.stringify(data);
await this.getEditPinnedDataButton().click();
@@ -373,6 +389,14 @@ export class NodeDetailsViewPage extends BasePage {
await this.page.getByRole('option', { name: nodeName }).click();
}
getInputTableHeader(index: number = 0) {
return this.getInputPanel().locator('table th').nth(index);
}
getInputTbodyCell(row: number, col: number) {
return this.getInputPanel().locator('table tbody tr').nth(row).locator('td').nth(col);
}
getAssignmentName(paramName: string, index = 0) {
return this.getAssignmentCollectionContainer(paramName)
.getByTestId('assignment')
@@ -457,6 +481,14 @@ export class NodeDetailsViewPage extends BasePage {
await input.type(content);
}
getInputTable() {
return this.getInputPanel().locator('table');
}
getInputTableCellSpan(row: number, col: number, dataName: string) {
return this.getInputTbodyCell(row, col).locator(`span[data-name="${dataName}"]`).first();
}
getAddFieldToSortByButton() {
return this.getNodeParameters().getByText('Add Field To Sort By');
}
@@ -493,6 +525,46 @@ export class NodeDetailsViewPage extends BasePage {
await input.fill(value);
}
async clickGetBackToCanvas(): Promise<void> {
await this.clickBackToCanvasButton();
}
getRunDataInfoCallout() {
return this.page.getByTestId('run-data-callout');
}
getOutputPanelTable() {
return this.getOutputTable();
}
async checkParameterCheckboxInputByName(name: string): Promise<void> {
const checkbox = this.getParameterInput(name).locator('.el-switch.switch-input');
await checkbox.click();
}
// Credentials modal helpers
async clickCreateNewCredential(eq: number = 0): Promise<void> {
await this.page.getByTestId('node-credentials-select').nth(eq).click();
await this.page.getByTestId('node-credentials-select-item-new').click();
}
// Run selector and linking helpers
getInputRunSelector() {
return this.page.locator('[data-test-id="ndv-input-panel"] [data-test-id="run-selector"]');
}
getOutputRunSelector() {
return this.page.locator('[data-test-id="output-panel"] [data-test-id="run-selector"]');
}
getInputRunSelectorInput() {
return this.getInputRunSelector().locator('input');
}
async toggleInputRunLinking(): Promise<void> {
await this.getInputPanel().getByTestId('link-run').click();
}
getNodeRunErrorMessage() {
return this.page.getByTestId('node-error-message');
}
@@ -725,6 +797,19 @@ export class NodeDetailsViewPage extends BasePage {
getInputSelect() {
return this.page.getByTestId('ndv-input-select').locator('input');
}
getInputTableRows() {
return this.getInputTable().locator('tr');
}
getOutputRunSelectorInput() {
return this.getOutputPanel().locator('[data-test-id="run-selector"] input');
}
getAiOutputModeToggle() {
return this.page.getByTestId('ai-output-mode-select');
}
getCredentialLabel(credentialType: string) {
return this.page.getByText(credentialType);
}

View File

@@ -4,6 +4,7 @@ import { AIAssistantPage } from './AIAssistantPage';
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
import { CanvasPage } from './CanvasPage';
import { CommunityNodesPage } from './CommunityNodesPage';
import { CredentialsEditModal } from './CredentialsEditModal';
import { CredentialsPage } from './CredentialsPage';
import { DemoPage } from './DemoPage';
import { ExecutionsPage } from './ExecutionsPage';
@@ -59,6 +60,7 @@ export class n8nPage {
readonly workflowActivationModal: WorkflowActivationModal;
readonly workflowSettingsModal: WorkflowSettingsModal;
readonly workflowSharingModal: WorkflowSharingModal;
readonly credentialsModal: CredentialsEditModal;
// Composables
readonly workflowComposer: WorkflowComposer;
@@ -98,6 +100,7 @@ export class n8nPage {
// Modals
this.workflowActivationModal = new WorkflowActivationModal(page);
this.workflowSettingsModal = new WorkflowSettingsModal(page);
this.credentialsModal = new CredentialsEditModal(page);
// Composables
this.workflowComposer = new WorkflowComposer(this);