chore: 清理macOS同步产生的重复文件
详细说明: - 删除了352个带数字后缀的重复文件 - 更新.gitignore防止未来产生此类文件 - 这些文件是由iCloud或其他同步服务冲突产生的 - 不影响项目功能,仅清理冗余文件
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class AIAssistantPage extends BasePage {
|
||||
getAskAssistantFloatingButton() {
|
||||
return this.page.getByTestId('ask-assistant-floating-button');
|
||||
}
|
||||
|
||||
getAskAssistantCanvasActionButton() {
|
||||
return this.page.getByTestId('ask-assistant-canvas-action-button');
|
||||
}
|
||||
|
||||
getAskAssistantChat() {
|
||||
return this.page.getByTestId('ask-assistant-chat');
|
||||
}
|
||||
|
||||
getPlaceholderMessage() {
|
||||
return this.page.getByTestId('placeholder-message');
|
||||
}
|
||||
|
||||
getChatInput() {
|
||||
return this.page.getByTestId('chat-input');
|
||||
}
|
||||
|
||||
getSendMessageButton() {
|
||||
return this.page.getByTestId('send-message-button');
|
||||
}
|
||||
|
||||
getCloseChatButton() {
|
||||
return this.page.getByTestId('close-chat-button');
|
||||
}
|
||||
|
||||
getAskAssistantSidebarResizer() {
|
||||
return this.page
|
||||
.getByTestId('ask-assistant-sidebar')
|
||||
.locator('[class*="_resizer"][data-dir="left"]')
|
||||
.first();
|
||||
}
|
||||
|
||||
getNodeErrorViewAssistantButton() {
|
||||
return this.page.getByTestId('node-error-view-ask-assistant-button').locator('button').first();
|
||||
}
|
||||
|
||||
getChatMessagesAll() {
|
||||
return this.page.locator('[data-test-id^="chat-message"]');
|
||||
}
|
||||
|
||||
getChatMessagesAssistant() {
|
||||
return this.page.getByTestId('chat-message-assistant');
|
||||
}
|
||||
|
||||
getChatMessagesUser() {
|
||||
return this.page.getByTestId('chat-message-user');
|
||||
}
|
||||
|
||||
getChatMessagesSystem() {
|
||||
return this.page.getByTestId('chat-message-system');
|
||||
}
|
||||
|
||||
getQuickReplyButtons() {
|
||||
return this.page.getByTestId('quick-replies').locator('button');
|
||||
}
|
||||
|
||||
getNewAssistantSessionModal() {
|
||||
return this.page.getByTestId('new-assistant-session-modal');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export abstract class BasePage {
|
||||
constructor(protected readonly page: Page) {}
|
||||
|
||||
protected async clickByTestId(testId: string) {
|
||||
await this.page.getByTestId(testId).click();
|
||||
}
|
||||
|
||||
protected async fillByTestId(testId: string, value: string) {
|
||||
await this.page.getByTestId(testId).fill(value);
|
||||
}
|
||||
|
||||
protected async clickByText(text: string) {
|
||||
await this.page.getByText(text).click();
|
||||
}
|
||||
|
||||
protected async clickButtonByName(name: string) {
|
||||
await this.page.getByRole('button', { name }).click();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class BecomeCreatorCTAPage extends BasePage {
|
||||
getBecomeTemplateCreatorCta() {
|
||||
return this.page.getByTestId('become-template-creator-cta');
|
||||
}
|
||||
|
||||
getCloseBecomeTemplateCreatorCtaButton() {
|
||||
return this.page.getByTestId('close-become-template-creator-cta');
|
||||
}
|
||||
|
||||
async closeBecomeTemplateCreatorCta() {
|
||||
await this.getCloseBecomeTemplateCreatorCtaButton().click();
|
||||
}
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
import { resolveFromRoot } from '../utils/path-helper';
|
||||
|
||||
export class CanvasPage extends BasePage {
|
||||
saveWorkflowButton(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
nodeCreatorItemByName(text: string): Locator {
|
||||
return this.page.getByTestId('node-creator-item-name').getByText(text, { exact: true });
|
||||
}
|
||||
|
||||
nodeCreatorSubItem(subItemText: string): Locator {
|
||||
return this.page.getByTestId('node-creator-item-name').getByText(subItemText, { exact: true });
|
||||
}
|
||||
|
||||
nodeByName(nodeName: string): Locator {
|
||||
return this.page.locator(`[data-test-id="canvas-node"][data-node-name="${nodeName}"]`);
|
||||
}
|
||||
|
||||
nodeToolbar(nodeName: string): Locator {
|
||||
return this.nodeByName(nodeName).getByTestId('canvas-node-toolbar');
|
||||
}
|
||||
|
||||
nodeDeleteButton(nodeName: string): Locator {
|
||||
return this.nodeToolbar(nodeName).getByTestId('delete-node-button');
|
||||
}
|
||||
|
||||
nodeDisableButton(nodeName: string): Locator {
|
||||
return this.nodeToolbar(nodeName).getByTestId('disable-node-button');
|
||||
}
|
||||
|
||||
async clickCanvasPlusButton(): Promise<void> {
|
||||
await this.clickByTestId('canvas-plus-button');
|
||||
}
|
||||
|
||||
getCanvasNodes() {
|
||||
return this.page.getByTestId('canvas-node');
|
||||
}
|
||||
|
||||
async clickNodeCreatorPlusButton(): Promise<void> {
|
||||
await this.clickByTestId('node-creator-plus-button');
|
||||
}
|
||||
|
||||
async clickSaveWorkflowButton(): Promise<void> {
|
||||
await this.saveWorkflowButton().click();
|
||||
}
|
||||
|
||||
async fillNodeCreatorSearchBar(text: string): Promise<void> {
|
||||
await this.nodeCreatorSearchBar().fill(text);
|
||||
}
|
||||
|
||||
async clickNodeCreatorItemName(text: string): Promise<void> {
|
||||
await this.nodeCreatorItemByName(text).click();
|
||||
}
|
||||
|
||||
async addNode(text: string): Promise<void> {
|
||||
await this.clickNodeCreatorPlusButton();
|
||||
await this.fillNodeCreatorSearchBar(text);
|
||||
await this.clickNodeCreatorItemName(text);
|
||||
}
|
||||
|
||||
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
|
||||
if (subItemText) {
|
||||
await this.addNodeWithSubItem(text, subItemText);
|
||||
} else {
|
||||
await this.addNode(text);
|
||||
}
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
async addNodeWithSubItem(searchText: string, subItemText: string): Promise<void> {
|
||||
await this.addNode(searchText);
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async addActionNode(searchText: string, subItemText: string): Promise<void> {
|
||||
await this.addNode(searchText);
|
||||
await this.page.getByText('Actions').click();
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async addTriggerNode(searchText: string, subItemText: string): Promise<void> {
|
||||
await this.addNode(searchText);
|
||||
await this.page.getByText('Triggers').click();
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async deleteNodeByName(nodeName: string): Promise<void> {
|
||||
await this.nodeDeleteButton(nodeName).click();
|
||||
}
|
||||
|
||||
async saveWorkflow(): Promise<void> {
|
||||
await this.clickSaveWorkflowButton();
|
||||
}
|
||||
|
||||
getExecuteWorkflowButton(): Locator {
|
||||
return this.page.getByTestId('execute-workflow-button');
|
||||
}
|
||||
|
||||
async clickExecuteWorkflowButton(): Promise<void> {
|
||||
await this.page.getByTestId('execute-workflow-button').click();
|
||||
}
|
||||
|
||||
async clickDebugInEditorButton(): Promise<void> {
|
||||
await this.page.getByRole('button', { name: 'Debug in editor' }).click();
|
||||
}
|
||||
|
||||
async pinNode(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).click({ button: 'right' });
|
||||
await this.page.getByTestId('context-menu').getByText('Pin').click();
|
||||
}
|
||||
|
||||
async unpinNode(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).click({ button: 'right' });
|
||||
await this.page.getByText('Unpin').click();
|
||||
}
|
||||
|
||||
async openNode(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).dblclick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all pinned nodes on the canvas.
|
||||
* @returns An array of node names.
|
||||
*/
|
||||
async getPinnedNodeNames(): Promise<string[]> {
|
||||
const pinnedNodesLocator = this.page
|
||||
.getByTestId('canvas-node')
|
||||
.filter({ has: this.page.getByTestId('canvas-node-status-pinned') });
|
||||
|
||||
const names: string[] = [];
|
||||
const count = await pinnedNodesLocator.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const node = pinnedNodesLocator.nth(i);
|
||||
const name = await node.getAttribute('data-node-name');
|
||||
if (name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
async clickExecutionsTab(): Promise<void> {
|
||||
await this.page.getByRole('radio', { name: 'Executions' }).click();
|
||||
}
|
||||
async setWorkflowName(name: string): Promise<void> {
|
||||
await this.clickByTestId('inline-edit-preview');
|
||||
await this.fillByTestId('inline-edit-input', name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a workflow from a fixture file
|
||||
* @param fixtureKey - The key of the fixture file to import
|
||||
* @param workflowName - The name of the workflow to import
|
||||
* Naming the file causes the workflow to save so we don't need to click save
|
||||
*/
|
||||
async importWorkflow(fixtureKey: string, workflowName: string) {
|
||||
await this.clickByTestId('workflow-menu');
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
this.page.waitForEvent('filechooser'),
|
||||
this.clickByText('Import from File...'),
|
||||
]);
|
||||
await fileChooser.setFiles(resolveFromRoot('workflows', fixtureKey));
|
||||
|
||||
await this.clickByTestId('inline-edit-preview');
|
||||
await this.fillByTestId('inline-edit-input', workflowName);
|
||||
await this.page.getByTestId('inline-edit-input').press('Enter');
|
||||
}
|
||||
|
||||
getWorkflowTags() {
|
||||
return this.page.getByTestId('workflow-tags').locator('.el-tag');
|
||||
}
|
||||
async activateWorkflow() {
|
||||
const responsePromise = this.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/workflows/') && response.request().method() === 'PATCH',
|
||||
);
|
||||
|
||||
await this.page.getByTestId('workflow-activate-switch').click();
|
||||
await responsePromise;
|
||||
|
||||
await this.page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async clickZoomToFitButton(): Promise<void> {
|
||||
await this.clickByTestId('zoom-to-fit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node issues for a specific node
|
||||
*/
|
||||
getNodeIssuesByName(nodeName: string) {
|
||||
return this.nodeByName(nodeName).getByTestId('node-issues');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags to the workflow
|
||||
* @param count - The number of tags to add
|
||||
* @returns An array of tag names
|
||||
*/
|
||||
async addTags(count: number = 1): Promise<string[]> {
|
||||
const tags: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const tag = `tag-${nanoid(8)}-${i}`;
|
||||
tags.push(tag);
|
||||
|
||||
if (i === 0) {
|
||||
await this.clickByText('Add tag');
|
||||
} else {
|
||||
await this.page
|
||||
.getByTestId('tags-dropdown')
|
||||
.getByText(tags[i - 1])
|
||||
.click();
|
||||
}
|
||||
|
||||
await this.page.getByRole('combobox').first().fill(tag);
|
||||
await this.page.getByRole('combobox').first().press('Enter');
|
||||
}
|
||||
|
||||
await this.page.click('body');
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
getWorkflowSaveButton(): Locator {
|
||||
return this.page.getByTestId('workflow-save-button');
|
||||
}
|
||||
|
||||
// Production Checklist methods
|
||||
getProductionChecklistButton(): Locator {
|
||||
return this.page.getByTestId('suggested-action-count');
|
||||
}
|
||||
|
||||
getProductionChecklistPopover(): Locator {
|
||||
return this.page.locator('[data-reka-popper-content-wrapper=""]').filter({ hasText: /./ });
|
||||
}
|
||||
|
||||
getProductionChecklistActionItem(text?: string): Locator {
|
||||
const items = this.page.getByTestId('suggested-action-item');
|
||||
if (text) {
|
||||
return items.getByText(text);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
getProductionChecklistIgnoreAllButton(): Locator {
|
||||
return this.page.getByTestId('suggested-action-ignore-all');
|
||||
}
|
||||
|
||||
getErrorActionItem(): Locator {
|
||||
return this.getProductionChecklistActionItem('Set up error notifications');
|
||||
}
|
||||
|
||||
getTimeSavedActionItem(): Locator {
|
||||
return this.getProductionChecklistActionItem('Track time saved');
|
||||
}
|
||||
|
||||
getEvaluationsActionItem(): Locator {
|
||||
return this.getProductionChecklistActionItem('Test reliability of AI steps');
|
||||
}
|
||||
|
||||
async clickProductionChecklistButton(): Promise<void> {
|
||||
await this.getProductionChecklistButton().click();
|
||||
}
|
||||
|
||||
async clickProductionChecklistIgnoreAll(): Promise<void> {
|
||||
await this.getProductionChecklistIgnoreAllButton().click();
|
||||
}
|
||||
|
||||
async clickProductionChecklistAction(actionText: string): Promise<void> {
|
||||
await this.getProductionChecklistActionItem(actionText).click();
|
||||
}
|
||||
|
||||
async duplicateNode(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).click({ button: 'right' });
|
||||
await this.page.getByTestId('context-menu').getByText('Duplicate').click();
|
||||
}
|
||||
|
||||
nodeConnections(): Locator {
|
||||
return this.page.locator('[data-test-id="edge"]');
|
||||
}
|
||||
|
||||
canvasNodePlusEndpointByName(nodeName: string): Locator {
|
||||
return this.page
|
||||
.locator(
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||
)
|
||||
.first();
|
||||
}
|
||||
|
||||
nodeCreatorSearchBar(): Locator {
|
||||
return this.page.getByTestId('node-creator-search-bar');
|
||||
}
|
||||
|
||||
nodeCreatorNodeItems(): Locator {
|
||||
return this.page.getByTestId('node-creator-node-item');
|
||||
}
|
||||
|
||||
nodeCreatorActionItems(): Locator {
|
||||
return this.page.getByTestId('node-creator-action-item');
|
||||
}
|
||||
|
||||
nodeCreatorCategoryItems(): Locator {
|
||||
return this.page.getByTestId('node-creator-category-item');
|
||||
}
|
||||
|
||||
selectedNodes(): Locator {
|
||||
return this.page
|
||||
.locator('[data-test-id="canvas-node"]')
|
||||
.locator('xpath=..')
|
||||
.locator('.selected');
|
||||
}
|
||||
|
||||
disabledNodes(): Locator {
|
||||
return this.page.locator('[data-canvas-node-render-type][class*="disabled"]');
|
||||
}
|
||||
|
||||
nodeExecuteButton(nodeName: string): Locator {
|
||||
return this.nodeToolbar(nodeName).getByTestId('execute-node-button');
|
||||
}
|
||||
|
||||
canvasPane(): Locator {
|
||||
return this.page.getByTestId('canvas-wrapper');
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
async addInitialNodeToCanvas(nodeName: string): Promise<void> {
|
||||
await this.clickCanvasPlusButton();
|
||||
await this.fillNodeCreatorSearchBar(nodeName);
|
||||
await this.clickNodeCreatorItemName(nodeName);
|
||||
}
|
||||
|
||||
async clickNodePlusEndpoint(nodeName: string): Promise<void> {
|
||||
await this.canvasNodePlusEndpointByName(nodeName).click();
|
||||
}
|
||||
|
||||
async executeNode(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).hover();
|
||||
await this.nodeExecuteButton(nodeName).click();
|
||||
}
|
||||
|
||||
async selectAll(): Promise<void> {
|
||||
await this.page.keyboard.press('ControlOrMeta+a');
|
||||
}
|
||||
|
||||
async copyNodes(): Promise<void> {
|
||||
await this.page.keyboard.press('ControlOrMeta+c');
|
||||
}
|
||||
|
||||
async deselectAll(): Promise<void> {
|
||||
await this.canvasPane().click({ position: { x: 10, y: 10 } });
|
||||
}
|
||||
|
||||
getNodeLeftPosition(nodeLocator: Locator): Promise<number> {
|
||||
return nodeLocator.evaluate((el) => el.getBoundingClientRect().left);
|
||||
}
|
||||
|
||||
// Connection helpers
|
||||
connectionBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
|
||||
return this.page.locator(
|
||||
`[data-test-id="edge"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||
);
|
||||
}
|
||||
|
||||
connectionToolbarBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
|
||||
return this.page.locator(
|
||||
`[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||
);
|
||||
}
|
||||
|
||||
// Canvas action helpers
|
||||
async addNodeBetweenNodes(
|
||||
sourceNodeName: string,
|
||||
targetNodeName: string,
|
||||
newNodeName: string,
|
||||
): Promise<void> {
|
||||
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await specificConnection.hover({ force: true });
|
||||
|
||||
const addNodeButton = this.connectionToolbarBetweenNodes(
|
||||
sourceNodeName,
|
||||
targetNodeName,
|
||||
).getByTestId('add-connection-button');
|
||||
|
||||
await addNodeButton.click();
|
||||
await this.fillNodeCreatorSearchBar(newNodeName);
|
||||
await this.clickNodeCreatorItemName(newNodeName);
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
async deleteConnectionBetweenNodes(
|
||||
sourceNodeName: string,
|
||||
targetNodeName: string,
|
||||
): Promise<void> {
|
||||
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await specificConnection.hover({ force: true });
|
||||
|
||||
const deleteButton = this.connectionToolbarBetweenNodes(
|
||||
sourceNodeName,
|
||||
targetNodeName,
|
||||
).getByTestId('delete-connection-button');
|
||||
|
||||
await deleteButton.click();
|
||||
}
|
||||
|
||||
async navigateNodesWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
|
||||
const keyMap = {
|
||||
left: 'ArrowLeft',
|
||||
right: 'ArrowRight',
|
||||
up: 'ArrowUp',
|
||||
down: 'ArrowDown',
|
||||
};
|
||||
await this.canvasPane().focus();
|
||||
await this.page.keyboard.press(keyMap[direction]);
|
||||
}
|
||||
|
||||
async extendSelectionWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
|
||||
const keyMap = {
|
||||
left: 'Shift+ArrowLeft',
|
||||
right: 'Shift+ArrowRight',
|
||||
up: 'Shift+ArrowUp',
|
||||
down: 'Shift+ArrowDown',
|
||||
};
|
||||
await this.canvasPane().focus();
|
||||
await this.page.keyboard.press(keyMap[direction]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the workflow page with a specific timestamp for NPS survey testing.
|
||||
* Uses Playwright's clock API to set a fixed time.
|
||||
*/
|
||||
async visitWithTimestamp(timestamp: number): Promise<void> {
|
||||
// Set fixed time using Playwright's clock API
|
||||
await this.page.clock.setFixedTime(timestamp);
|
||||
|
||||
await this.page.goto('/workflow/new');
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class CredentialsPage extends BasePage {
|
||||
get emptyListCreateCredentialButton() {
|
||||
return this.page.getByRole('button', { name: 'Add first credential' });
|
||||
}
|
||||
|
||||
get createCredentialButton() {
|
||||
return this.page.getByTestId('create-credential-button');
|
||||
}
|
||||
|
||||
get credentialCards() {
|
||||
return this.page.getByTestId('credential-cards');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new credential of the specified type
|
||||
* @param credentialType - The type of credential to create (e.g. 'Notion API')
|
||||
*/
|
||||
async openNewCredentialDialogFromCredentialList(credentialType: string): Promise<void> {
|
||||
await this.page.getByRole('combobox', { name: 'Search for app...' }).fill(credentialType);
|
||||
await this.page
|
||||
.getByTestId('new-credential-type-select-option')
|
||||
.filter({ hasText: credentialType })
|
||||
.click();
|
||||
await this.page.getByTestId('new-credential-type-button').click();
|
||||
}
|
||||
|
||||
async openCredentialSelector() {
|
||||
await this.page.getByRole('combobox', { name: 'Select Credential' }).click();
|
||||
}
|
||||
|
||||
async createNewCredential() {
|
||||
await this.clickByText('Create new credential');
|
||||
}
|
||||
|
||||
async fillCredentialField(fieldName: string, value: string) {
|
||||
const field = this.page
|
||||
.getByTestId(`parameter-input-${fieldName}`)
|
||||
.getByTestId('parameter-input-field');
|
||||
await field.click();
|
||||
await field.fill(value);
|
||||
}
|
||||
|
||||
async saveCredential() {
|
||||
await this.clickButtonByName('Save');
|
||||
}
|
||||
|
||||
async closeCredentialDialog() {
|
||||
await this.clickButtonByName('Close this dialog');
|
||||
}
|
||||
|
||||
async createAndSaveNewCredential(fieldName: string, value: string) {
|
||||
await this.openCredentialSelector();
|
||||
await this.createNewCredential();
|
||||
await this.filLCredentialSaveClose(fieldName, value);
|
||||
}
|
||||
|
||||
async filLCredentialSaveClose(fieldName: string, value: string) {
|
||||
await this.fillCredentialField(fieldName, value);
|
||||
await this.saveCredential();
|
||||
await this.page.getByText('Connection tested successfully').waitFor({ state: 'visible' });
|
||||
await this.closeCredentialDialog();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class ExecutionsPage extends BasePage {
|
||||
async clickDebugInEditorButton(): Promise<void> {
|
||||
await this.clickButtonByName('Debug in editor');
|
||||
}
|
||||
|
||||
async clickCopyToEditorButton(): Promise<void> {
|
||||
await this.clickButtonByName('Copy to editor');
|
||||
}
|
||||
|
||||
getExecutionItems(): Locator {
|
||||
return this.page.locator('div.execution-card');
|
||||
}
|
||||
|
||||
getLastExecutionItem(): Locator {
|
||||
const executionItems = this.getExecutionItems();
|
||||
return executionItems.nth(0);
|
||||
}
|
||||
|
||||
async clickLastExecutionItem(): Promise<void> {
|
||||
const executionItem = this.getLastExecutionItem();
|
||||
await executionItem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the pinned nodes confirmation dialog.
|
||||
* @param action - The action to take.
|
||||
*/
|
||||
async handlePinnedNodesConfirmation(action: 'Unpin' | 'Cancel'): Promise<void> {
|
||||
const confirmDialog = this.page.locator('.matching-pinned-nodes-confirmation');
|
||||
await this.page.getByRole('button', { name: action }).click();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class IframePage extends BasePage {
|
||||
getIframe() {
|
||||
return this.page.locator('iframe');
|
||||
}
|
||||
|
||||
getIframeBySrc(src: string) {
|
||||
return this.page.locator(`iframe[src="${src}"]`);
|
||||
}
|
||||
|
||||
async waitForIframeRequest(url: string) {
|
||||
await this.page.waitForResponse(url);
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
import { NodeParameterHelper } from '../helpers/NodeParameterHelper';
|
||||
import { EditFieldsNode } from './nodes/EditFieldsNode';
|
||||
|
||||
export class NodeDetailsViewPage extends BasePage {
|
||||
readonly setupHelper: NodeParameterHelper;
|
||||
readonly editFields: EditFieldsNode;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.setupHelper = new NodeParameterHelper(this);
|
||||
this.editFields = new EditFieldsNode(page);
|
||||
}
|
||||
|
||||
async clickBackToCanvasButton() {
|
||||
await this.clickByTestId('back-to-canvas');
|
||||
}
|
||||
|
||||
getParameterByLabel(labelName: string) {
|
||||
return this.page.locator('.parameter-item').filter({ hasText: labelName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a parameter input field
|
||||
* @param labelName - The label of the parameter e.g URL
|
||||
* @param value - The value to fill in the input field e.g https://foo.bar
|
||||
*/
|
||||
async fillParameterInput(labelName: string, value: string) {
|
||||
await this.getParameterByLabel(labelName).getByTestId('parameter-input-field').fill(value);
|
||||
}
|
||||
|
||||
async selectWorkflowResource(createItemText: string, searchText: string = '') {
|
||||
await this.clickByTestId('rlc-input');
|
||||
|
||||
if (searchText) {
|
||||
await this.fillByTestId('rlc-search', searchText);
|
||||
}
|
||||
|
||||
await this.clickByText(createItemText);
|
||||
}
|
||||
|
||||
async togglePinData() {
|
||||
await this.clickByTestId('ndv-pin-data');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async execute() {
|
||||
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');
|
||||
}
|
||||
|
||||
getEditPinnedDataButton() {
|
||||
return this.page.getByTestId('ndv-edit-pinned-data');
|
||||
}
|
||||
|
||||
getPinDataButton() {
|
||||
return this.getOutputPanel().getByTestId('ndv-pin-data');
|
||||
}
|
||||
|
||||
getRunDataPaneHeader() {
|
||||
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');
|
||||
}
|
||||
|
||||
getOutputTableRows() {
|
||||
return this.getOutputTable().locator('tr');
|
||||
}
|
||||
|
||||
getOutputTableHeaders() {
|
||||
return this.getOutputTable().locator('thead th');
|
||||
}
|
||||
|
||||
getOutputTableRow(row: number) {
|
||||
return this.getOutputTableRows().nth(row);
|
||||
}
|
||||
|
||||
getOutputTableCell(row: number, col: number) {
|
||||
return this.getOutputTableRow(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
getOutputTbodyCell(row: number, col: number) {
|
||||
return this.getOutputTableRow(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
// Pin data operations
|
||||
async setPinnedData(data: object | string) {
|
||||
const pinnedData = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
await this.getEditPinnedDataButton().click();
|
||||
|
||||
// Wait for editor to appear and use broader selector
|
||||
const editor = this.getOutputPanel().locator('[contenteditable="true"]');
|
||||
await editor.waitFor();
|
||||
await editor.click();
|
||||
await editor.fill(pinnedData);
|
||||
|
||||
await this.savePinnedData();
|
||||
}
|
||||
|
||||
async pastePinnedData(data: object) {
|
||||
await this.getEditPinnedDataButton().click();
|
||||
|
||||
const editor = this.getOutputPanel().locator('[contenteditable="true"]');
|
||||
await editor.waitFor();
|
||||
await editor.click();
|
||||
await editor.fill('');
|
||||
|
||||
// Set clipboard data and paste
|
||||
await this.page.evaluate(async (jsonData) => {
|
||||
await navigator.clipboard.writeText(JSON.stringify(jsonData));
|
||||
}, data);
|
||||
await this.page.keyboard.press('ControlOrMeta+V');
|
||||
|
||||
await this.savePinnedData();
|
||||
}
|
||||
|
||||
async savePinnedData() {
|
||||
await this.getRunDataPaneHeader().locator('button:visible').filter({ hasText: 'Save' }).click();
|
||||
}
|
||||
|
||||
// Assignment collection methods for advanced tests
|
||||
getAssignmentCollectionAdd(paramName: string) {
|
||||
return this.page
|
||||
.getByTestId(`assignment-collection-${paramName}`)
|
||||
.getByTestId('assignment-collection-drop-area');
|
||||
}
|
||||
|
||||
getAssignmentValue(paramName: string) {
|
||||
return this.page
|
||||
.getByTestId(`assignment-collection-${paramName}`)
|
||||
.getByTestId('assignment-value');
|
||||
}
|
||||
|
||||
getInlineExpressionEditorInput() {
|
||||
return this.page.getByTestId('inline-expression-editor-input');
|
||||
}
|
||||
|
||||
getNodeParameters() {
|
||||
return this.page.getByTestId('node-parameters');
|
||||
}
|
||||
|
||||
getParameterInputHint() {
|
||||
return this.page.getByTestId('parameter-input-hint');
|
||||
}
|
||||
|
||||
async makeWebhookRequest(path: string) {
|
||||
return await this.page.request.get(path);
|
||||
}
|
||||
|
||||
getVisiblePoppers() {
|
||||
return this.page.locator('.el-popper:visible');
|
||||
}
|
||||
|
||||
async clearExpressionEditor() {
|
||||
const editor = this.getInlineExpressionEditorInput();
|
||||
await editor.click();
|
||||
await this.page.keyboard.press('ControlOrMeta+A');
|
||||
await this.page.keyboard.press('Delete');
|
||||
}
|
||||
|
||||
async typeInExpressionEditor(text: string) {
|
||||
const editor = this.getInlineExpressionEditorInput();
|
||||
await editor.click();
|
||||
// We have to use type() instead of fill() because the editor is a CodeMirror editor
|
||||
await editor.type(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter input by name (for Code node and similar)
|
||||
* @param parameterName - The name of the parameter e.g 'jsCode', 'mode'
|
||||
*/
|
||||
getParameterInput(parameterName: string) {
|
||||
return this.page.getByTestId(`parameter-input-${parameterName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter input field
|
||||
* @param parameterName - The name of the parameter
|
||||
*/
|
||||
getParameterInputField(parameterName: string) {
|
||||
return this.getParameterInput(parameterName).getByTestId('parameter-input-field');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option in parameter dropdown (improved with Playwright best practices)
|
||||
* @param parameterName - The parameter name
|
||||
* @param optionText - The text of the option to select
|
||||
*/
|
||||
async selectOptionInParameterDropdown(parameterName: string, optionText: string) {
|
||||
const dropdown = this.getParameterInput(parameterName);
|
||||
await dropdown.click();
|
||||
|
||||
// Wait for dropdown to be visible and select option - following Playwright best practices
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click parameter dropdown by name (test-id based selector)
|
||||
* @param parameterName - The parameter name e.g 'httpMethod', 'authentication'
|
||||
*/
|
||||
async clickParameterDropdown(parameterName: string): Promise<void> {
|
||||
await this.clickByTestId(`parameter-input-${parameterName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option from visible dropdown using Playwright role-based selectors
|
||||
* This follows the pattern used in working n8n tests
|
||||
* @param optionText - The text of the option to select
|
||||
*/
|
||||
async selectFromVisibleDropdown(optionText: string): Promise<void> {
|
||||
// Use Playwright's role-based selector - this is more reliable than CSS selectors
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill parameter input field by parameter name
|
||||
* @param parameterName - The parameter name e.g 'path', 'url'
|
||||
* @param value - The value to fill
|
||||
*/
|
||||
async fillParameterInputByName(parameterName: string, value: string): Promise<void> {
|
||||
const input = this.getParameterInputField(parameterName);
|
||||
await input.click();
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click parameter options expansion (e.g. for Response Code)
|
||||
*/
|
||||
async clickParameterOptions(): Promise<void> {
|
||||
await this.page.locator('.param-options').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible Element UI popper (dropdown/popover)
|
||||
* Ported from Cypress pattern with Playwright selectors
|
||||
*/
|
||||
getVisiblePopper() {
|
||||
return this.page
|
||||
.locator('.el-popper')
|
||||
.filter({ hasNot: this.page.locator('[aria-hidden="true"]') });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for parameter dropdown to be visible and ready for interaction
|
||||
* @param parameterName - The parameter name
|
||||
*/
|
||||
async waitForParameterDropdown(parameterName: string): Promise<void> {
|
||||
const dropdown = this.getParameterInput(parameterName);
|
||||
await dropdown.waitFor({ state: 'visible' });
|
||||
await expect(dropdown).toBeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a floating node in the NDV (for switching between connected nodes)
|
||||
* @param nodeName - The name of the node to click
|
||||
*/
|
||||
async clickFloatingNode(nodeName: string) {
|
||||
await this.page.locator(`[data-test-id="floating-node"][data-node-name="${nodeName}"]`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the previous node (useful for providing input data)
|
||||
*/
|
||||
async executePrevious() {
|
||||
await this.clickByTestId('execute-previous-node');
|
||||
}
|
||||
|
||||
async clickAskAiTab() {
|
||||
await this.page.locator('#tab-ask-ai').click();
|
||||
}
|
||||
|
||||
getAskAiTabPanel() {
|
||||
return this.page.getByTestId('code-node-tab-ai');
|
||||
}
|
||||
|
||||
getAskAiCtaButton() {
|
||||
return this.page.getByTestId('ask-ai-cta');
|
||||
}
|
||||
|
||||
getAskAiPromptInput() {
|
||||
return this.page.getByTestId('ask-ai-prompt-input');
|
||||
}
|
||||
|
||||
getAskAiPromptCounter() {
|
||||
return this.page.getByTestId('ask-ai-prompt-counter');
|
||||
}
|
||||
|
||||
getAskAiCtaTooltipNoInputData() {
|
||||
return this.page.getByTestId('ask-ai-cta-tooltip-no-input-data');
|
||||
}
|
||||
|
||||
getAskAiCtaTooltipNoPrompt() {
|
||||
return this.page.getByTestId('ask-ai-cta-tooltip-no-prompt');
|
||||
}
|
||||
|
||||
getAskAiCtaTooltipPromptTooShort() {
|
||||
return this.page.getByTestId('ask-ai-cta-tooltip-prompt-too-short');
|
||||
}
|
||||
|
||||
getCodeTabPanel() {
|
||||
return this.page.getByTestId('code-node-tab-code');
|
||||
}
|
||||
|
||||
getCodeTab() {
|
||||
return this.page.locator('#tab-code');
|
||||
}
|
||||
|
||||
getCodeEditor() {
|
||||
return this.getParameterInput('jsCode').locator('.cm-content');
|
||||
}
|
||||
|
||||
getLintErrors() {
|
||||
return this.getParameterInput('jsCode').locator('.cm-lintRange-error');
|
||||
}
|
||||
|
||||
getLintTooltip() {
|
||||
return this.page.locator('.cm-tooltip-lint');
|
||||
}
|
||||
|
||||
getPlaceholderText(text: string) {
|
||||
return this.page.getByText(text);
|
||||
}
|
||||
|
||||
getHeyAiText() {
|
||||
return this.page.locator('text=Hey AI, generate JavaScript');
|
||||
}
|
||||
|
||||
getCodeGenerationCompletedText() {
|
||||
return this.page.locator('text=Code generation completed');
|
||||
}
|
||||
|
||||
getErrorMessageText(message: string) {
|
||||
return this.page.locator(`text=${message}`);
|
||||
}
|
||||
|
||||
async setParameterDropdown(parameterName: string, optionText: string): Promise<void> {
|
||||
await this.getParameterInput(parameterName).click();
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
async setParameterInput(parameterName: string, value: string): Promise<void> {
|
||||
await this.fillParameterInputByName(parameterName, value);
|
||||
}
|
||||
|
||||
async setParameterSwitch(parameterName: string, enabled: boolean): Promise<void> {
|
||||
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
|
||||
const isCurrentlyEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
|
||||
if (isCurrentlyEnabled !== enabled) {
|
||||
await switchElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
async setMultipleParameters(
|
||||
parameters: Record<string, string | number | boolean>,
|
||||
): Promise<void> {
|
||||
for (const [parameterName, value] of Object.entries(parameters)) {
|
||||
if (typeof value === 'string') {
|
||||
const parameterType = await this.setupHelper.detectParameterType(parameterName);
|
||||
if (parameterType === 'dropdown') {
|
||||
await this.setParameterDropdown(parameterName, value);
|
||||
} else {
|
||||
await this.setParameterInput(parameterName, value);
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
await this.setParameterSwitch(parameterName, value);
|
||||
} else if (typeof value === 'number') {
|
||||
await this.setParameterInput(parameterName, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getParameterValue(parameterName: string): Promise<string> {
|
||||
const parameterType = await this.setupHelper.detectParameterType(parameterName);
|
||||
|
||||
switch (parameterType) {
|
||||
case 'text':
|
||||
return await this.getTextParameterValue(parameterName);
|
||||
case 'dropdown':
|
||||
return await this.getDropdownParameterValue(parameterName);
|
||||
case 'switch':
|
||||
return await this.getSwitchParameterValue(parameterName);
|
||||
default:
|
||||
// Fallback for unknown types
|
||||
return (await this.getParameterInput(parameterName).textContent()) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a text parameter - simplified approach
|
||||
*/
|
||||
private async getTextParameterValue(parameterName: string): Promise<string> {
|
||||
const parameterContainer = this.getParameterInput(parameterName);
|
||||
const input = parameterContainer.locator('input').first();
|
||||
return await input.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a dropdown parameter
|
||||
*/
|
||||
private async getDropdownParameterValue(parameterName: string): Promise<string> {
|
||||
const selectedOption = this.getParameterInput(parameterName).locator('.el-select__tags-text');
|
||||
return (await selectedOption.textContent()) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a switch parameter
|
||||
*/
|
||||
private async getSwitchParameterValue(parameterName: string): Promise<string> {
|
||||
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
|
||||
const isEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
|
||||
return isEnabled ? 'true' : 'false';
|
||||
}
|
||||
|
||||
async validateParameter(parameterName: string, expectedValue: string): Promise<void> {
|
||||
const actualValue = await this.getParameterValue(parameterName);
|
||||
if (actualValue !== expectedValue) {
|
||||
throw new Error(
|
||||
`Parameter ${parameterName} has value "${actualValue}", expected "${expectedValue}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getAssignmentCollectionContainer(paramName: string) {
|
||||
return this.page.getByTestId(`assignment-collection-${paramName}`);
|
||||
}
|
||||
|
||||
getAssignmentName(paramName: string, index = 0) {
|
||||
return this.getAssignmentCollectionContainer(paramName)
|
||||
.getByTestId('assignment')
|
||||
.nth(index)
|
||||
.getByTestId('assignment-name');
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class NotificationsPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main container locator for a notification by searching in its title text.
|
||||
* @param text The text or a regular expression to find within the notification's title.
|
||||
* @returns A Locator for the notification container element.
|
||||
*/
|
||||
getNotificationByTitle(text: string | RegExp): Locator {
|
||||
return this.page.getByRole('alert').filter({
|
||||
has: this.page.locator('.el-notification__title').filter({ hasText: text }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main container locator for a notification by searching in its content/body text.
|
||||
* This is useful for finding notifications where the detailed message is in the content
|
||||
* rather than the title (e.g., error messages with detailed descriptions).
|
||||
* @param text The text or a regular expression to find within the notification's content.
|
||||
* @returns A Locator for the notification container element.
|
||||
*/
|
||||
getNotificationByContent(text: string | RegExp): Locator {
|
||||
return this.page.getByRole('alert').filter({
|
||||
has: this.page.locator('.el-notification__content').filter({ hasText: text }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main container locator for a notification by searching in both title and content.
|
||||
* This is the most flexible method as it will find notifications regardless of whether
|
||||
* the text appears in the title or content section.
|
||||
* @param text The text or a regular expression to find within the notification's title or content.
|
||||
* @returns A Locator for the notification container element.
|
||||
*/
|
||||
getNotificationByTitleOrContent(text: string | RegExp): Locator {
|
||||
return this.page.getByRole('alert').filter({ hasText: text });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main container locator for a notification by searching in both title and content,
|
||||
* filtered to a specific node name. This is useful when multiple notifications might be present
|
||||
* and you want to ensure you're checking the right one for a specific node.
|
||||
* @param text The text or a regular expression to find within the notification's title or content.
|
||||
* @param nodeName The name of the node to filter notifications for.
|
||||
* @returns A Locator for the notification container element.
|
||||
*/
|
||||
getNotificationByTitleOrContentForNode(text: string | RegExp, nodeName: string): Locator {
|
||||
return this.page.getByRole('alert').filter({ hasText: text }).filter({ hasText: nodeName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the close button on the FIRST notification matching the text.
|
||||
* Fast execution with short timeouts for snappy notifications.
|
||||
* @param text The text of the notification to close.
|
||||
* @param options Optional configuration
|
||||
*/
|
||||
async closeNotificationByText(
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<boolean> {
|
||||
const { timeout = 2000 } = options;
|
||||
|
||||
try {
|
||||
const notification = this.getNotificationByTitle(text).first();
|
||||
await notification.waitFor({ state: 'visible', timeout });
|
||||
|
||||
const closeBtn = notification.locator('.el-notification__closeBtn');
|
||||
await closeBtn.click({ timeout: 500 });
|
||||
|
||||
// Quick check that it's gone - don't wait long
|
||||
await notification.waitFor({ state: 'hidden', timeout: 1000 });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes ALL currently visible notifications that match the given text.
|
||||
* Uses aggressive polling for fast cleanup.
|
||||
* @param text The text of the notifications to close.
|
||||
* @param options Optional configuration
|
||||
*/
|
||||
async closeAllNotificationsWithText(
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number; maxRetries?: number } = {},
|
||||
): Promise<number> {
|
||||
const { timeout = 1500, maxRetries = 15 } = options;
|
||||
let closedCount = 0;
|
||||
let retries = 0;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
const notifications = this.getNotificationByTitle(text);
|
||||
const count = await notifications.count();
|
||||
|
||||
if (count === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Close the first visible notification quickly
|
||||
const firstNotification = notifications.first();
|
||||
if (await firstNotification.isVisible({ timeout: 200 })) {
|
||||
const closeBtn = firstNotification.locator('.el-notification__closeBtn');
|
||||
await closeBtn.click({ timeout: 300 });
|
||||
|
||||
// Brief wait for disappearance, then continue
|
||||
await firstNotification.waitFor({ state: 'hidden', timeout: 500 }).catch(() => {});
|
||||
closedCount++;
|
||||
} else {
|
||||
// If not visible, likely already gone
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue quickly on any error
|
||||
break;
|
||||
}
|
||||
|
||||
retries++;
|
||||
}
|
||||
|
||||
return closedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a notification is visible based on text.
|
||||
* Fast check with short timeout.
|
||||
* @param text The text to search for in notification title.
|
||||
* @param options Optional configuration
|
||||
*/
|
||||
async isNotificationVisible(
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<boolean> {
|
||||
const { timeout = 500 } = options;
|
||||
|
||||
try {
|
||||
const notification = this.getNotificationByTitle(text).first();
|
||||
await notification.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a notification to appear with specific text.
|
||||
* Reasonable timeout for waiting, but still faster than before.
|
||||
* @param text The text to search for in notification title.
|
||||
* @param options Optional configuration
|
||||
*/
|
||||
async waitForNotification(
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<boolean> {
|
||||
const { timeout = 5000 } = options;
|
||||
|
||||
try {
|
||||
const notification = this.getNotificationByTitle(text).first();
|
||||
await notification.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for notification and then close it
|
||||
async waitForNotificationAndClose(
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<boolean> {
|
||||
const { timeout = 3000 } = options;
|
||||
await this.waitForNotification(text, { timeout });
|
||||
await this.closeNotificationByText(text, { timeout });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible notification texts.
|
||||
* @returns Array of notification title texts
|
||||
*/
|
||||
async getAllNotificationTexts(): Promise<string[]> {
|
||||
try {
|
||||
const titles = this.page.getByRole('alert').locator('.el-notification__title');
|
||||
return await titles.allTextContents();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all notifications to disappear.
|
||||
* Fast check with short timeout.
|
||||
* @param options Optional configuration
|
||||
*/
|
||||
async waitForAllNotificationsToDisappear(options: { timeout?: number } = {}): Promise<boolean> {
|
||||
const { timeout = 2000 } = options;
|
||||
|
||||
try {
|
||||
// Wait for no alerts to be visible
|
||||
await this.page.getByRole('alert').first().waitFor({
|
||||
state: 'detached',
|
||||
timeout,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// Check if any are still visible
|
||||
const count = await this.getNotificationCount();
|
||||
return count === 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of visible notifications.
|
||||
* @param text Optional text to filter notifications
|
||||
*/
|
||||
async getNotificationCount(text?: string | RegExp): Promise<number> {
|
||||
try {
|
||||
const notifications = text ? this.getNotificationByTitle(text) : this.page.getByRole('alert');
|
||||
return await notifications.count();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick utility to close any notification and continue.
|
||||
* Uses the most aggressive timeouts for maximum speed.
|
||||
* @param text The text of the notification to close.
|
||||
*/
|
||||
async quickClose(text: string | RegExp): Promise<void> {
|
||||
try {
|
||||
const notification = this.getNotificationByTitle(text).first();
|
||||
if (await notification.isVisible({ timeout: 100 })) {
|
||||
await notification.locator('.el-notification__closeBtn').click({ timeout: 200 });
|
||||
}
|
||||
} catch {
|
||||
// Silent fail for speed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nuclear option: Close everything as fast as possible.
|
||||
* No waiting, no error handling, just close and move on.
|
||||
*/
|
||||
async quickCloseAll(): Promise<void> {
|
||||
try {
|
||||
const closeButtons = this.page.locator('.el-notification__closeBtn');
|
||||
const count = await closeButtons.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
try {
|
||||
await closeButtons.nth(i).click({ timeout: 100 });
|
||||
} catch {
|
||||
// Continue silently
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class NpsSurveyPage extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
getNpsSurveyModal(): Locator {
|
||||
return this.page.getByTestId('nps-survey-modal');
|
||||
}
|
||||
|
||||
getNpsSurveyRatings(): Locator {
|
||||
return this.page.getByTestId('nps-survey-ratings');
|
||||
}
|
||||
|
||||
getNpsSurveyFeedback(): Locator {
|
||||
return this.page.getByTestId('nps-survey-feedback');
|
||||
}
|
||||
|
||||
getNpsSurveySubmitButton(): Locator {
|
||||
return this.page.getByTestId('nps-survey-feedback-button');
|
||||
}
|
||||
|
||||
getNpsSurveyCloseButton(): Locator {
|
||||
return this.getNpsSurveyModal().locator('button.el-drawer__close-btn');
|
||||
}
|
||||
|
||||
getRatingButton(rating: number): Locator {
|
||||
return this.getNpsSurveyRatings().locator('button').nth(rating);
|
||||
}
|
||||
|
||||
getFeedbackTextarea(): Locator {
|
||||
return this.getNpsSurveyFeedback().locator('textarea');
|
||||
}
|
||||
|
||||
async clickRating(rating: number): Promise<void> {
|
||||
await this.getRatingButton(rating).click();
|
||||
}
|
||||
|
||||
async fillFeedback(feedback: string): Promise<void> {
|
||||
await this.getFeedbackTextarea().fill(feedback);
|
||||
}
|
||||
|
||||
async clickSubmitButton(): Promise<void> {
|
||||
await this.getNpsSurveySubmitButton().click();
|
||||
}
|
||||
|
||||
async closeSurvey(): Promise<void> {
|
||||
await this.getNpsSurveyCloseButton().click();
|
||||
}
|
||||
|
||||
async getRatingButtonCount(): Promise<number> {
|
||||
return await this.getNpsSurveyRatings().locator('button').count();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class ProjectSettingsPage extends BasePage {
|
||||
async fillProjectName(name: string) {
|
||||
await this.page.getByTestId('project-settings-name-input').locator('input').fill(name);
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
await this.clickButtonByName('Save');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class SettingsPage extends BasePage {
|
||||
getMenuItems() {
|
||||
return this.page.getByTestId('menu-item');
|
||||
}
|
||||
|
||||
getMenuItem(id: string) {
|
||||
return this.page.getByTestId('menu-item').getByTestId(id);
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
await this.page.goto('/settings');
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class SidebarPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async clickAddProjectButton() {
|
||||
await this.page.getByTestId('project-plus-button').click();
|
||||
}
|
||||
|
||||
async universalAdd() {
|
||||
await this.page.getByTestId('universal-add').click();
|
||||
}
|
||||
|
||||
async addProjectFromUniversalAdd() {
|
||||
await this.universalAdd();
|
||||
await this.page.getByTestId('navigation-menu-item').filter({ hasText: 'Project' }).click();
|
||||
}
|
||||
|
||||
async addWorkflowFromUniversalAdd(projectName: string) {
|
||||
await this.universalAdd();
|
||||
await this.page.getByTestId('universal-add').getByText('Workflow').click();
|
||||
await this.page.getByTestId('universal-add').getByRole('link', { name: projectName }).click();
|
||||
}
|
||||
|
||||
async openNewCredentialDialogForProject(projectName: string) {
|
||||
await this.universalAdd();
|
||||
await this.page.getByTestId('universal-add').getByText('Credential').click();
|
||||
await this.page.getByTestId('universal-add').getByRole('link', { name: projectName }).click();
|
||||
}
|
||||
|
||||
getProjectMenuItems(): Locator {
|
||||
return this.page.getByTestId('project-menu-item');
|
||||
}
|
||||
|
||||
async clickProjectMenuItem(projectName: string) {
|
||||
await this.getProjectMenuItems().filter({ hasText: projectName }).click();
|
||||
}
|
||||
|
||||
getAddFirstProjectButton(): Locator {
|
||||
return this.page.getByTestId('add-first-project-button');
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class VersionsPage extends BasePage {
|
||||
getVersionUpdatesPanelOpenButton() {
|
||||
return this.page.getByTestId('version-update-next-versions-link');
|
||||
}
|
||||
|
||||
getVersionUpdatesPanel() {
|
||||
return this.page.getByTestId('version-updates-panel');
|
||||
}
|
||||
|
||||
getVersionUpdatesPanelCloseButton() {
|
||||
return this.getVersionUpdatesPanel().getByRole('button', { name: 'Close' });
|
||||
}
|
||||
|
||||
getVersionCard() {
|
||||
return this.page.getByTestId('version-card');
|
||||
}
|
||||
|
||||
getWhatsNewMenuItem() {
|
||||
return this.page.getByTestId('menu-item').getByTestId('whats-new');
|
||||
}
|
||||
|
||||
async openWhatsNewMenu() {
|
||||
await this.getWhatsNewMenuItem().click();
|
||||
}
|
||||
|
||||
async openVersionUpdatesPanel() {
|
||||
await this.getVersionUpdatesPanelOpenButton().click();
|
||||
}
|
||||
|
||||
async closeVersionUpdatesPanel() {
|
||||
await this.getVersionUpdatesPanelCloseButton().click();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class WorkflowActivationModal extends BasePage {
|
||||
getModal(): Locator {
|
||||
return this.page.getByTestId('activation-modal');
|
||||
}
|
||||
|
||||
getDontShowAgainCheckbox(): Locator {
|
||||
return this.getModal().getByText("Don't show again");
|
||||
}
|
||||
|
||||
getGotItButton(): Locator {
|
||||
return this.getModal().getByRole('button', { name: 'Got it' });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.getDontShowAgainCheckbox().click();
|
||||
|
||||
await this.getGotItButton().click();
|
||||
}
|
||||
|
||||
async clickDontShowAgain(): Promise<void> {
|
||||
await this.getDontShowAgainCheckbox().click();
|
||||
}
|
||||
|
||||
async clickGotIt(): Promise<void> {
|
||||
await this.getGotItButton().click();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class WorkflowSettingsModal extends BasePage {
|
||||
getModal(): Locator {
|
||||
return this.page.getByTestId('workflow-settings-dialog');
|
||||
}
|
||||
|
||||
getWorkflowMenu(): Locator {
|
||||
return this.page.getByTestId('workflow-menu');
|
||||
}
|
||||
|
||||
getSettingsMenuItem(): Locator {
|
||||
return this.page.getByTestId('workflow-menu-item-settings');
|
||||
}
|
||||
|
||||
getErrorWorkflowField(): Locator {
|
||||
return this.page.getByTestId('workflow-settings-error-workflow');
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
await this.getWorkflowMenu().click();
|
||||
await this.getSettingsMenuItem().click();
|
||||
}
|
||||
|
||||
async clickSave(): Promise<void> {
|
||||
await this.getSaveButton().click();
|
||||
}
|
||||
|
||||
async selectErrorWorkflow(workflowName: string): Promise<void> {
|
||||
await this.getErrorWorkflowField().click();
|
||||
await this.page.getByRole('option', { name: workflowName }).first().click();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class WorkflowSharingModal extends BasePage {
|
||||
getModal() {
|
||||
return this.page.getByTestId('workflowShare-modal');
|
||||
}
|
||||
|
||||
async waitForModal() {
|
||||
await this.getModal().waitFor({ state: 'visible', timeout: 5000 });
|
||||
}
|
||||
|
||||
async addUser(email: string) {
|
||||
await this.clickByTestId('project-sharing-select');
|
||||
await this.page
|
||||
.locator('.el-select-dropdown__item')
|
||||
.filter({ hasText: email.toLowerCase() })
|
||||
.click();
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.clickByTestId('workflow-sharing-modal-save-button');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.getModal().locator('.el-dialog__close').first().click();
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class WorkflowsPage extends BasePage {
|
||||
async clickAddFirstProjectButton() {
|
||||
await this.clickByTestId('add-first-project-button');
|
||||
}
|
||||
|
||||
async clickAddProjectButton() {
|
||||
await this.clickByTestId('project-plus-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the add workflow button on the workflows page, visible when there are already workflows.
|
||||
*/
|
||||
async clickAddWorkflowButton() {
|
||||
await this.clickByTestId('add-resource-workflow');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the new workflow button on the workflows page, visible when there are no workflows.
|
||||
*/
|
||||
async clickNewWorkflowCard() {
|
||||
await this.clickByTestId('new-workflow-card');
|
||||
}
|
||||
|
||||
getNewWorkflowCard() {
|
||||
return this.page.getByTestId('new-workflow-card');
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.clickByTestId('resources-list-search');
|
||||
await this.page.getByTestId('resources-list-search').clear();
|
||||
}
|
||||
|
||||
getProjectName() {
|
||||
return this.page.getByTestId('project-name');
|
||||
}
|
||||
|
||||
getSearchBar() {
|
||||
return this.page.getByTestId('resources-list-search');
|
||||
}
|
||||
|
||||
getWorkflowFilterButton() {
|
||||
return this.page.getByTestId('workflow-filter-button');
|
||||
}
|
||||
|
||||
getWorkflowTagsDropdown() {
|
||||
return this.page.getByTestId('workflow-tags-dropdown');
|
||||
}
|
||||
|
||||
getWorkflowTagItem(tagName: string) {
|
||||
return this.page.getByTestId('workflow-tag-item').filter({ hasText: tagName });
|
||||
}
|
||||
|
||||
getWorkflowArchivedCheckbox() {
|
||||
return this.page.getByTestId('workflow-archived-checkbox');
|
||||
}
|
||||
|
||||
async unarchiveWorkflow(workflowItem: Locator) {
|
||||
await workflowItem.getByTestId('workflow-card-actions').click();
|
||||
await this.page.getByRole('menuitem', { name: 'Unarchive' }).click();
|
||||
}
|
||||
|
||||
async deleteWorkflow(workflowItem: Locator) {
|
||||
await workflowItem.getByTestId('workflow-card-actions').click();
|
||||
await this.page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await this.page.getByRole('button', { name: 'delete' }).click();
|
||||
}
|
||||
|
||||
async searchWorkflows(searchTerm: string) {
|
||||
await this.clickByTestId('resources-list-search');
|
||||
await this.fillByTestId('resources-list-search', searchTerm);
|
||||
}
|
||||
|
||||
getWorkflowItems() {
|
||||
return this.page.getByTestId('resources-list-item-workflow');
|
||||
}
|
||||
|
||||
getWorkflowByName(name: string) {
|
||||
return this.getWorkflowItems().filter({ hasText: name });
|
||||
}
|
||||
|
||||
async shareWorkflow(workflowName: string) {
|
||||
const workflow = this.getWorkflowByName(workflowName);
|
||||
await workflow.getByTestId('workflow-card-actions').click();
|
||||
await this.page.getByRole('menuitem', { name: 'Share...' }).click();
|
||||
}
|
||||
|
||||
getArchiveMenuItem() {
|
||||
return this.page.getByRole('menuitem', { name: 'Archive' });
|
||||
}
|
||||
|
||||
async archiveWorkflow(workflowItem: Locator) {
|
||||
await workflowItem.getByTestId('workflow-card-actions').click();
|
||||
await this.getArchiveMenuItem().click();
|
||||
}
|
||||
|
||||
getFiltersButton() {
|
||||
return this.page.getByTestId('resources-list-filters-trigger');
|
||||
}
|
||||
|
||||
async openFilters() {
|
||||
await this.clickByTestId('resources-list-filters-trigger');
|
||||
}
|
||||
|
||||
async closeFilters() {
|
||||
await this.clickByTestId('resources-list-filters-trigger');
|
||||
}
|
||||
|
||||
getShowArchivedCheckbox() {
|
||||
return this.page.getByTestId('show-archived-checkbox');
|
||||
}
|
||||
|
||||
async toggleShowArchived() {
|
||||
await this.openFilters();
|
||||
await this.getShowArchivedCheckbox().locator('span').nth(1).click();
|
||||
await this.closeFilters();
|
||||
}
|
||||
|
||||
getStatusDropdown() {
|
||||
return this.page.getByTestId('status-dropdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a status filter (for active/deactivated workflows)
|
||||
* @param status - 'All', 'Active', or 'Deactivated'
|
||||
*/
|
||||
async selectStatusFilter(status: 'All' | 'Active' | 'Deactivated') {
|
||||
await this.openFilters();
|
||||
await this.getStatusDropdown().getByRole('combobox', { name: 'Select' }).click();
|
||||
if (status === 'All') {
|
||||
await this.page.getByRole('option', { name: 'All' }).click();
|
||||
} else {
|
||||
await this.page.getByText(status, { exact: true }).click();
|
||||
}
|
||||
await this.closeFilters();
|
||||
}
|
||||
|
||||
getTagsDropdown() {
|
||||
return this.page.getByTestId('tags-dropdown');
|
||||
}
|
||||
|
||||
async filterByTags(tags: string[]) {
|
||||
await this.openFilters();
|
||||
await this.clickByTestId('tags-dropdown');
|
||||
|
||||
for (const tag of tags) {
|
||||
await this.page.getByRole('option', { name: tag }).locator('span').click();
|
||||
}
|
||||
|
||||
await this.closeFilters();
|
||||
}
|
||||
|
||||
async filterByTag(tag: string) {
|
||||
await this.filterByTags([tag]);
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { AIAssistantPage } from './AIAssistantPage';
|
||||
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
|
||||
import { CanvasPage } from './CanvasPage';
|
||||
import { CredentialsPage } from './CredentialsPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
import { IframePage } from './IframePage';
|
||||
import { NodeDetailsViewPage } from './NodeDetailsViewPage';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { SidebarPage } from './SidebarPage';
|
||||
import { VersionsPage } from './VersionsPage';
|
||||
import { WorkflowActivationModal } from './WorkflowActivationModal';
|
||||
import { WorkflowSettingsModal } from './WorkflowSettingsModal';
|
||||
import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||
import { WorkflowsPage } from './WorkflowsPage';
|
||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { ProjectComposer } from '../composables/ProjectComposer';
|
||||
import { TestEntryComposer } from '../composables/TestEntryComposer';
|
||||
import { WorkflowComposer } from '../composables/WorkflowComposer';
|
||||
import type { ApiHelpers } from '../services/api-helper';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export class n8nPage {
|
||||
readonly page: Page;
|
||||
readonly api: ApiHelpers;
|
||||
|
||||
// Pages
|
||||
readonly aiAssistant: AIAssistantPage;
|
||||
readonly becomeCreatorCTA: BecomeCreatorCTAPage;
|
||||
readonly canvas: CanvasPage;
|
||||
|
||||
readonly iframe: IframePage;
|
||||
readonly ndv: NodeDetailsViewPage;
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
readonly settings: SettingsPage;
|
||||
readonly versions: VersionsPage;
|
||||
readonly workflows: WorkflowsPage;
|
||||
readonly notifications: NotificationsPage;
|
||||
readonly credentials: CredentialsPage;
|
||||
readonly executions: ExecutionsPage;
|
||||
readonly sideBar: SidebarPage;
|
||||
|
||||
// Modals
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
readonly workflowSettingsModal: WorkflowSettingsModal;
|
||||
readonly workflowSharingModal: WorkflowSharingModal;
|
||||
|
||||
// Composables
|
||||
readonly workflowComposer: WorkflowComposer;
|
||||
readonly projectComposer: ProjectComposer;
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly start: TestEntryComposer;
|
||||
|
||||
constructor(page: Page, api: ApiHelpers) {
|
||||
this.page = page;
|
||||
this.api = api;
|
||||
|
||||
// Pages
|
||||
this.aiAssistant = new AIAssistantPage(page);
|
||||
this.becomeCreatorCTA = new BecomeCreatorCTAPage(page);
|
||||
this.canvas = new CanvasPage(page);
|
||||
|
||||
this.iframe = new IframePage(page);
|
||||
this.ndv = new NodeDetailsViewPage(page);
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
this.settings = new SettingsPage(page);
|
||||
this.versions = new VersionsPage(page);
|
||||
this.workflows = new WorkflowsPage(page);
|
||||
this.notifications = new NotificationsPage(page);
|
||||
this.credentials = new CredentialsPage(page);
|
||||
this.executions = new ExecutionsPage(page);
|
||||
this.sideBar = new SidebarPage(page);
|
||||
this.workflowSharingModal = new WorkflowSharingModal(page);
|
||||
|
||||
// Modals
|
||||
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||
this.workflowSettingsModal = new WorkflowSettingsModal(page);
|
||||
|
||||
// Composables
|
||||
this.workflowComposer = new WorkflowComposer(this);
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.start = new TestEntryComposer(this);
|
||||
}
|
||||
|
||||
async goHome() {
|
||||
await this.page.goto('/');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user