mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
test: Migrate Langchain e2e tests to Playwright (#19161)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
67
packages/testing/playwright/pages/CredentialsEditModal.ts
Normal file
67
packages/testing/playwright/pages/CredentialsEditModal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user