first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* AddResource component for creating workflows, credentials, folders, and data tables.
|
||||
* Represents the "add resource" functionality in the project header.
|
||||
*
|
||||
* @example
|
||||
* // Access via workflows page
|
||||
* await n8n.workflows.addResource.workflow();
|
||||
* await n8n.workflows.addResource.credential();
|
||||
* await n8n.workflows.addResource.folder();
|
||||
* await n8n.workflows.addResource.dataTable();
|
||||
*/
|
||||
export class AddResource {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
getWorkflowButton(): Locator {
|
||||
return this.page.getByTestId('add-resource-workflow');
|
||||
}
|
||||
|
||||
async workflow(): Promise<void> {
|
||||
await this.getWorkflowButton().click();
|
||||
}
|
||||
|
||||
async credential(): Promise<void> {
|
||||
await this.page.getByTestId('add-resource-credential').click();
|
||||
}
|
||||
|
||||
async folder(): Promise<void> {
|
||||
await this.page.getByTestId('add-resource').click();
|
||||
await this.page.getByTestId('action-folder').click();
|
||||
}
|
||||
|
||||
async dataTable(fromDataTableTab: boolean = true): Promise<void> {
|
||||
if (fromDataTableTab) {
|
||||
await this.page.getByTestId('add-resource-dataTable').click();
|
||||
} else {
|
||||
await this.page.getByTestId('add-resource').click();
|
||||
await this.page.getByTestId('action-dataTable').click();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { FloatingUiHelper } from './FloatingUiHelper';
|
||||
|
||||
/**
|
||||
* Base modal component for handling modal dialogs.
|
||||
*/
|
||||
export class BaseModal extends FloatingUiHelper {
|
||||
constructor(protected readonly page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this.page.getByRole('dialog');
|
||||
}
|
||||
|
||||
getCloseButton() {
|
||||
return this.container.getByRole('button', { name: /close/i });
|
||||
}
|
||||
|
||||
async waitForModal() {
|
||||
await this.container.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async fillInput(text: string) {
|
||||
await this.container.getByRole('textbox').fill(text);
|
||||
}
|
||||
|
||||
async clickButton(buttonText: string | RegExp) {
|
||||
await this.container.getByRole('button', { name: buttonText }).click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export class Breadcrumbs {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
getBreadcrumbs() {
|
||||
return this.page.getByTestId('breadcrumbs-item');
|
||||
}
|
||||
|
||||
getBreadcrumb(resourceName: string) {
|
||||
return this.getBreadcrumbs().filter({ hasText: resourceName });
|
||||
}
|
||||
getCurrentBreadcrumb() {
|
||||
return this.page.getByTestId('breadcrumbs-item-current');
|
||||
}
|
||||
|
||||
getHiddenBreadcrumbs() {
|
||||
return this.page.getByTestId('hidden-items-menu');
|
||||
}
|
||||
|
||||
getHomeProjectBreadcrumb() {
|
||||
return this.page.getByTestId('home-project');
|
||||
}
|
||||
|
||||
getActionToggleDropdown(resourceName: string) {
|
||||
return this.page.getByTestId('action-toggle-dropdown').getByTestId(`action-${resourceName}`);
|
||||
}
|
||||
|
||||
getFolderBreadcrumbsActionToggle() {
|
||||
return this.page.getByTestId('folder-breadcrumbs-actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the current breadcrumb by activating inline edit mode
|
||||
* @param newName - The new name for the breadcrumb item
|
||||
*/
|
||||
async renameCurrentBreadcrumb(newName: string) {
|
||||
await this.getCurrentBreadcrumb().getByTestId('inline-edit-preview').click();
|
||||
await this.getCurrentBreadcrumb().getByTestId('inline-edit-input').fill(newName);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BaseModal } from './BaseModal';
|
||||
|
||||
export class ChatHubCredentialModal extends BaseModal {
|
||||
constructor(private root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getCredentialSelector(): Locator {
|
||||
return this.root.getByRole('combobox');
|
||||
}
|
||||
|
||||
getCreateButton(): Locator {
|
||||
return this.page.getByTestId('node-credentials-select-item-new');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BaseModal } from './BaseModal';
|
||||
|
||||
export class ChatHubPersonalAgentModal extends BaseModal {
|
||||
constructor(protected readonly root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getNameField() {
|
||||
return this.root.getByPlaceholder(/Enter agent name/);
|
||||
}
|
||||
|
||||
getDescriptionField() {
|
||||
return this.root.getByPlaceholder(/Enter agent description/);
|
||||
}
|
||||
|
||||
getSystemPromptField() {
|
||||
return this.root.getByPlaceholder(/Enter system prompt/);
|
||||
}
|
||||
|
||||
getModelSelectorButton(): Locator {
|
||||
return this.root.getByTestId('chat-model-selector');
|
||||
}
|
||||
|
||||
getSaveButton() {
|
||||
return this.root.getByText('Save');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BaseModal } from './BaseModal';
|
||||
|
||||
export class ChatHubProviderSettingsModal extends BaseModal {
|
||||
constructor(protected readonly root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getRoot(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getEnabledToggle(): Locator {
|
||||
return this.root.getByLabel(/^Enable /).locator('..');
|
||||
}
|
||||
|
||||
getCredentialPicker(): Locator {
|
||||
return this.root.getByLabel('Default credential');
|
||||
}
|
||||
|
||||
getEditCredentialButton(): Locator {
|
||||
return this.root.getByTitle('Update Credential');
|
||||
}
|
||||
|
||||
getLimitModelsToggle(): Locator {
|
||||
return this.root.getByLabel('Limit models').locator('..');
|
||||
}
|
||||
|
||||
getModelSelector(): Locator {
|
||||
return this.root.getByLabel('Models', { exact: true });
|
||||
}
|
||||
|
||||
getConfirmButton(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Confirm' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
export class ChatHubSidebar {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getPersonalAgentButton() {
|
||||
return this.root.getByRole('menuitem', { name: 'Personal agents' });
|
||||
}
|
||||
|
||||
getConversations() {
|
||||
return this.root.getByTestId('chat-conversation-list').getByRole('link');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
export class ChatHubToolsModal {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getRoot(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/** Click "Add" button next to a tool in the available tools list */
|
||||
getAddButton(toolDisplayName: string): Locator {
|
||||
return this.root
|
||||
.locator('[class*="item"]')
|
||||
.filter({ hasText: toolDisplayName })
|
||||
.getByRole('button', { name: /add/i });
|
||||
}
|
||||
|
||||
/** Credential selector rendered by NodeCredentials inside settings view */
|
||||
getCredentialSelect(): Locator {
|
||||
return this.root.getByTestId('node-credentials-select');
|
||||
}
|
||||
|
||||
/** Save button in the settings view header */
|
||||
getSaveButton(): Locator {
|
||||
return this.root.getByRole('button', { name: /save/i });
|
||||
}
|
||||
|
||||
/** Get a parameter input by parameter name (e.g. "operation") */
|
||||
getParameterInput(parameterName: string): Locator {
|
||||
return this.root.getByTestId(`parameter-input-${parameterName}`);
|
||||
}
|
||||
|
||||
/** Get the "from AI" override button scoped to a specific parameter */
|
||||
getFromAiOverrideButton(parameterName: string): Locator {
|
||||
return this.getParameterInput(parameterName).getByTestId('from-ai-override-button');
|
||||
}
|
||||
|
||||
/** Close button (X) shown in list view */
|
||||
getCloseButton(): Locator {
|
||||
return this.root.locator('.el-dialog__close').first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from '../BasePage';
|
||||
|
||||
/**
|
||||
* Convert to Sub-workflow Modal component for converting nodes to sub-workflows.
|
||||
* Used within CanvasPage as `n8n.canvas.convertToSubworkflowModal.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page
|
||||
* await n8n.canvas.rightClickNode('My Node');
|
||||
* await n8n.canvas.clickContextMenuAction('Convert node to sub-workflow');
|
||||
* await n8n.canvas.convertToSubworkflowModal.waitForModal();
|
||||
* await n8n.canvas.convertToSubworkflowModal.clickSubmitButton();
|
||||
* await n8n.canvas.convertToSubworkflowModal.waitForClose();
|
||||
*/
|
||||
export class ConvertToSubworkflowModal extends BasePage {
|
||||
constructor(private root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getSubmitButton(): Locator {
|
||||
return this.root.getByTestId('submit-button');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.root.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async clickSubmitButton(): Promise<void> {
|
||||
await this.getSubmitButton().click();
|
||||
}
|
||||
|
||||
async waitForClose(): Promise<void> {
|
||||
await this.root.waitFor({ state: 'hidden' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BaseModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* Credential modal component for canvas and credentials interactions.
|
||||
* Used within CanvasPage as `n8n.canvas.credentialModal.*`
|
||||
* Used within CredentialsPage as `n8n.credentials.modal.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page or credentials page
|
||||
* await n8n.canvas.credentialModal.addCredential();
|
||||
* await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
*/
|
||||
export class CredentialModal extends BaseModal {
|
||||
constructor(private root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getCredentialName(): Locator {
|
||||
return this.root.getByTestId('credential-name');
|
||||
}
|
||||
|
||||
getNameInput(): Locator {
|
||||
return this.getCredentialName().getByTestId('inline-edit-input');
|
||||
}
|
||||
|
||||
getCredentialInputs(): Locator {
|
||||
return this.root.getByTestId('credential-connection-parameter');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.root.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async fillField(key: string, value: string): Promise<void> {
|
||||
const parameterInput = this.root.getByTestId(`parameter-input-${key}`);
|
||||
const input = parameterInput.locator('input, textarea');
|
||||
// Wait for input to be visible before filling
|
||||
await input.waitFor({ state: 'visible', timeout: 10000 });
|
||||
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.root.getByTestId('credential-save-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for save to fully complete.
|
||||
* After saving (and optional credential testing), the button becomes
|
||||
* disabled (no unsaved changes) and is no longer loading
|
||||
*/
|
||||
async waitForSaveComplete(): Promise<void> {
|
||||
const btn = this.getSaveButton().locator('button');
|
||||
await expect(async () => {
|
||||
await expect(btn).toBeDisabled();
|
||||
await expect(btn).not.toHaveAttribute('aria-busy', 'true');
|
||||
}).toPass({ timeout: 10000 });
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await this.getSaveButton().click();
|
||||
await this.waitForSaveComplete();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const closeBtn = this.root.locator('.el-dialog__close').first();
|
||||
if (await closeBtn.isVisible()) {
|
||||
await closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a credential to the modal
|
||||
* @param fields - The fields to fill in the modal
|
||||
* @param options - The options to pass to the modal
|
||||
* @param options.closeDialog - Whether to close the modal after saving
|
||||
* @param options.name - The name of the credential
|
||||
*/
|
||||
async addCredential(
|
||||
fields: Record<string, string>,
|
||||
options?: { closeDialog?: boolean; skipSave?: boolean; name?: string },
|
||||
): Promise<void> {
|
||||
await this.fillAllFields(fields);
|
||||
if (options?.name) {
|
||||
await this.getCredentialName().click();
|
||||
await this.getNameInput().fill(options.name);
|
||||
}
|
||||
|
||||
if (!options?.skipSave) {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
const shouldClose = options?.closeDialog ?? true;
|
||||
if (shouldClose) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
get oauthConnectButton() {
|
||||
return this.root.getByTestId('oauth-connect-button');
|
||||
}
|
||||
|
||||
get oauthConnectSuccessBanner() {
|
||||
return this.root.getByTestId('oauth-connect-success-banner');
|
||||
}
|
||||
|
||||
async editCredential(): Promise<void> {
|
||||
await this.root.page().getByTestId('credential-edit-button').click();
|
||||
}
|
||||
|
||||
async deleteCredential(): Promise<void> {
|
||||
await this.root.page().getByTestId('credential-delete-button').click();
|
||||
}
|
||||
|
||||
async confirmDelete(): Promise<void> {
|
||||
await this.root.page().getByRole('button', { name: 'Yes' }).click();
|
||||
}
|
||||
|
||||
async renameCredential(newName: string): Promise<void> {
|
||||
await this.getCredentialName().click();
|
||||
await this.getNameInput().fill(newName);
|
||||
await this.getNameInput().press('Enter');
|
||||
}
|
||||
|
||||
getOAuthRedirectUrl() {
|
||||
return this.root.page().getByTestId('oauth-redirect-url');
|
||||
}
|
||||
|
||||
getModeSelector() {
|
||||
return this.root.getByTestId('credential-mode-selector');
|
||||
}
|
||||
|
||||
getModeDropdownTrigger() {
|
||||
return this.root.getByTestId('credential-mode-dropdown-trigger');
|
||||
}
|
||||
|
||||
async selectAuthTypeFromDropdown(optionName: string | RegExp): Promise<void> {
|
||||
await this.getModeDropdownTrigger().click();
|
||||
await this.root.page().getByRole('menuitem', { name: optionName }).click();
|
||||
}
|
||||
|
||||
async changeTab(tabName: 'Sharing'): Promise<void> {
|
||||
await this.root.getByTestId('menu-item').filter({ hasText: tabName }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific credential field input
|
||||
*/
|
||||
getFieldInput(key: string): Locator {
|
||||
return this.root.getByTestId(`parameter-input-${key}`).locator('input, textarea');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the users select dropdown in the Sharing tab
|
||||
*/
|
||||
getUsersSelect(): Locator {
|
||||
return this.root.getByTestId('project-sharing-select').filter({ visible: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible dropdown popper (for sharing dropdown interactions)
|
||||
*/
|
||||
getVisibleDropdown(): Locator {
|
||||
return this.root.page().locator('.el-popper[aria-hidden="false"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to credential sharing
|
||||
* @param emailOrName - User email or name to share with
|
||||
*/
|
||||
async addUserToSharing(emailOrName: string): Promise<void> {
|
||||
await this.getUsersSelect().click();
|
||||
const dropdown = this.getVisibleDropdown();
|
||||
// Wait for dropdown content to load
|
||||
await dropdown.locator('.el-select-dropdown__item').first().waitFor({ state: 'visible' });
|
||||
|
||||
// Try to find by email or name (personal projects now show "Personal space" instead of email)
|
||||
const byEmail = dropdown.getByText(emailOrName.toLowerCase(), { exact: false });
|
||||
if ((await byEmail.count()) > 0) {
|
||||
await byEmail.click();
|
||||
} else {
|
||||
// For personal projects, try matching by name part of email
|
||||
const namePart = emailOrName.split('@')[0].replace(/[.-]/g, ' ');
|
||||
await dropdown.getByText(new RegExp(namePart, 'i')).first().click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save credential sharing (different from regular save - hits /share endpoint)
|
||||
*/
|
||||
async saveSharing(): Promise<void> {
|
||||
const saveBtn = this.getSaveButton();
|
||||
await saveBtn.click();
|
||||
|
||||
// Wait for share API call to complete
|
||||
await this.root
|
||||
.page()
|
||||
.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/credentials/') &&
|
||||
response.url().includes('/share') &&
|
||||
response.request().method() === 'PUT',
|
||||
);
|
||||
|
||||
await this.waitForSaveComplete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
type GetByRoleName = NonNullable<Parameters<Locator['getByRole']>[1]>['name'];
|
||||
type GetByRoleOptionsWithoutName = Omit<Parameters<Locator['getByRole']>[1], 'name'>;
|
||||
|
||||
export class FloatingUiHelper {
|
||||
constructor(protected readonly page: Page) {}
|
||||
|
||||
getVisiblePoppers() {
|
||||
// Match Reka UI popovers (data-side is unique to Reka UI positioned content)
|
||||
return this.page.locator('[data-state="open"][data-side]');
|
||||
}
|
||||
|
||||
getVisiblePopper() {
|
||||
// Match both Element+ poppers (.el-popper:visible) and Reka UI poppers ([data-state="open"])
|
||||
return this.page.locator(
|
||||
'.el-popper:visible, [data-state="open"][role="dialog"], [data-state="open"][role="menu"], [data-state="open"][role="listbox"]',
|
||||
);
|
||||
}
|
||||
|
||||
getVisiblePopoverMenuItem(name?: GetByRoleName, options: GetByRoleOptionsWithoutName = {}) {
|
||||
return this.getVisiblePopper()
|
||||
.getByRole('menuitem', { name, ...options })
|
||||
.filter({ visible: true });
|
||||
}
|
||||
|
||||
getVisiblePopoverOption(name?: GetByRoleName, options: GetByRoleOptionsWithoutName = {}) {
|
||||
return this.getVisiblePopper()
|
||||
.getByRole('option', { name, ...options })
|
||||
.filter({ visible: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
export class FocusPanel {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
/**
|
||||
* Accessors
|
||||
*/
|
||||
|
||||
getHeaderNodeName(): Locator {
|
||||
return this.root.locator('header').getByTestId('inline-edit-preview');
|
||||
}
|
||||
|
||||
getParameterInputField(path: string): Locator {
|
||||
return this.root.locator(
|
||||
`[data-test-id="parameter-input-field"][title="Parameter: \\"${path}\\""]`,
|
||||
);
|
||||
}
|
||||
|
||||
getMapper(): Locator {
|
||||
// find from the entire page because the mapper is rendered as portal
|
||||
return this.root.page().getByRole('dialog').getByTestId('ndv-input-panel');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { RunDataPanel } from './RunDataPanel';
|
||||
import type { ClipboardHelper } from '../../helpers/ClipboardHelper';
|
||||
|
||||
/**
|
||||
* Page object for the log view with configurable root element.
|
||||
*
|
||||
* @example
|
||||
* // Include in a page
|
||||
* class ExamplePage {
|
||||
* readonly logsPanel = new LogsPanel(this.page.getByTestId('logs-panel'));
|
||||
* }
|
||||
*
|
||||
* // Usage in a test
|
||||
* await expect(n8n.example.logsPage.getLogEntries()).toHaveCount(2);
|
||||
*/
|
||||
export class LogsPanel {
|
||||
readonly inputPanel = new RunDataPanel(this.root.getByTestId('log-details-input'));
|
||||
readonly outputPanel = new RunDataPanel(this.root.getByTestId('log-details-output'));
|
||||
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
/**
|
||||
* Accessors
|
||||
*/
|
||||
|
||||
getOverviewStatus(): Locator {
|
||||
return this.root.getByTestId('logs-overview-status');
|
||||
}
|
||||
|
||||
getClearExecutionButton(): Locator {
|
||||
return this.root
|
||||
.getByTestId('logs-overview-header')
|
||||
.locator('button')
|
||||
.filter({ hasText: 'Clear execution' });
|
||||
}
|
||||
|
||||
getLogEntries(): Locator {
|
||||
return this.root.getByTestId('logs-overview-body').getByRole('treeitem');
|
||||
}
|
||||
|
||||
getSelectedLogEntry(): Locator {
|
||||
return this.root.getByTestId('logs-overview-body').getByRole('treeitem', { selected: true });
|
||||
}
|
||||
|
||||
getManualChatModal(): Locator {
|
||||
return this.root.getByTestId('canvas-chat');
|
||||
}
|
||||
|
||||
getManualChatInput(): Locator {
|
||||
return this.getManualChatModal().locator('.chat-inputs textarea');
|
||||
}
|
||||
|
||||
getManualChatMessages(): Locator {
|
||||
return this.getManualChatModal().locator('.chat-messages-list .chat-message');
|
||||
}
|
||||
|
||||
getSessionIdButton(): Locator {
|
||||
return this.getManualChatModal().getByTestId('chat-session-id');
|
||||
}
|
||||
|
||||
getRefreshSessionButton(): Locator {
|
||||
return this.getManualChatModal().getByTestId('refresh-session-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
async open(): Promise<void> {
|
||||
await this.root.getByTestId('logs-overview-header').click();
|
||||
}
|
||||
|
||||
async clickLogEntryAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).click();
|
||||
}
|
||||
|
||||
async toggleInputPanel(): Promise<void> {
|
||||
await this.root.getByTestId('log-details-header').getByText('Input').click();
|
||||
}
|
||||
|
||||
async clickOpenNdvAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).hover();
|
||||
await this.getLogEntries().nth(rowIndex).getByLabel('Open...').click();
|
||||
}
|
||||
|
||||
async clickTriggerPartialExecutionAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).hover();
|
||||
await this.getLogEntries().nth(rowIndex).getByLabel('Execute step').click();
|
||||
}
|
||||
|
||||
async clearExecutionData(): Promise<void> {
|
||||
await this.root.getByTestId('clear-execution-data-button').click();
|
||||
}
|
||||
|
||||
async sendManualChatMessage(message: string): Promise<void> {
|
||||
await this.getManualChatInput().fill(message);
|
||||
await this.getManualChatModal().locator('.chat-input-send-button').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the session ID button to copy the session ID to clipboard and returns it.
|
||||
* @param clipboard - ClipboardHelper instance for reading clipboard
|
||||
* @returns The full session ID string
|
||||
*/
|
||||
async getSessionId(clipboard: ClipboardHelper): Promise<string> {
|
||||
await clipboard.grant();
|
||||
await this.getSessionIdButton().click();
|
||||
return await clipboard.readText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the refresh session button to reset the chat session.
|
||||
*/
|
||||
async refreshSession(): Promise<void> {
|
||||
await this.getRefreshSessionButton().click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Node Creator component for adding nodes to workflows.
|
||||
* Used within CanvasPage as `n8n.canvas.nodeCreator.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page
|
||||
* await n8n.canvas.nodeCreator.open();
|
||||
* await n8n.canvas.nodeCreator.searchFor('Gmail');
|
||||
* await n8n.canvas.nodeCreator.selectItem('Gmail');
|
||||
*/
|
||||
export class NodeCreator {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
// Core locators
|
||||
getRoot(): Locator {
|
||||
return this.page.getByTestId('node-creator');
|
||||
}
|
||||
|
||||
getSearchBar(): Locator {
|
||||
return this.page.getByTestId('node-creator-search-bar');
|
||||
}
|
||||
|
||||
getNodeItems(): Locator {
|
||||
return this.page.getByTestId('item-iterator-item');
|
||||
}
|
||||
|
||||
getCategoryItems(): Locator {
|
||||
return this.page.getByTestId('node-creator-category-item');
|
||||
}
|
||||
|
||||
getActiveSubcategory(): Locator {
|
||||
return this.page.getByTestId('nodes-list-header').first();
|
||||
}
|
||||
|
||||
getNoResults(): Locator {
|
||||
return this.page.getByTestId('node-creator-no-results');
|
||||
}
|
||||
|
||||
getNoTriggersCallout(): Locator {
|
||||
return this.page.getByTestId('actions-panel-no-triggers-callout');
|
||||
}
|
||||
|
||||
getActivationCallout(): Locator {
|
||||
return this.page.getByTestId('actions-panel-activation-callout');
|
||||
}
|
||||
|
||||
getTriggerText(): Locator {
|
||||
return this.page.getByText('What triggers this workflow?');
|
||||
}
|
||||
|
||||
getNextText(): Locator {
|
||||
return this.page.getByText('What happens next?');
|
||||
}
|
||||
|
||||
// Item getters
|
||||
getItem(text: string): Locator {
|
||||
return this.getNodeItems().filter({ hasText: text }).first();
|
||||
}
|
||||
|
||||
getCategoryItem(text: string): Locator {
|
||||
return this.getCategoryItems().filter({ hasText: text });
|
||||
}
|
||||
|
||||
// Actions
|
||||
async open(): Promise<void> {
|
||||
await this.page.getByTestId('node-creator-plus-button').click();
|
||||
await expect(this.getRoot()).toBeVisible();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
async searchFor(text: string): Promise<void> {
|
||||
await this.getSearchBar().fill(text);
|
||||
}
|
||||
|
||||
async clearSearch(): Promise<void> {
|
||||
await this.getSearchBar().clear();
|
||||
}
|
||||
|
||||
async selectItem(text: string): Promise<void> {
|
||||
await this.getItem(text).click();
|
||||
}
|
||||
|
||||
async selectCategoryItem(text: string): Promise<void> {
|
||||
await this.getCategoryItem(text).click();
|
||||
}
|
||||
|
||||
async navigateToSubcategory(category: string): Promise<void> {
|
||||
await this.getItem(category).click();
|
||||
await expect(this.getActiveSubcategory()).toContainText(category);
|
||||
}
|
||||
|
||||
async goBackFromSubcategory(): Promise<void> {
|
||||
await this.getActiveSubcategory().locator('button').click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* ProjectTabs component - navigation tabs within a project view
|
||||
* Mirrors the ProjectTabs.vue component in the frontend
|
||||
*/
|
||||
export class ProjectTabsComponent {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async clickCredentialsTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /credentials/i })
|
||||
.click();
|
||||
}
|
||||
|
||||
async clickWorkflowsTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /workflows/i })
|
||||
.click();
|
||||
}
|
||||
|
||||
async clickDataTablesTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /data tables/i })
|
||||
.click();
|
||||
}
|
||||
|
||||
async clickVariablesTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /variables/i })
|
||||
.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* ResourceCards component for handling folder, workflow, credential, and data store cards.
|
||||
* All cards are contained within a resources-list-wrapper.
|
||||
*/
|
||||
export class ResourceCards {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
getFolders(): Locator {
|
||||
return this.page.getByTestId('folder-card');
|
||||
}
|
||||
|
||||
getWorkflows(): Locator {
|
||||
return this.page.getByTestId('resources-list-item-workflow');
|
||||
}
|
||||
|
||||
getCredentials(): Locator {
|
||||
return this.page.getByTestId('resources-list-item');
|
||||
}
|
||||
|
||||
getFolder(name: string): Locator {
|
||||
return this.page.locator(`[data-test-id="folder-card"][data-resourcename="${name}"]`);
|
||||
}
|
||||
|
||||
getWorkflow(name: string): Locator {
|
||||
return this.getWorkflows().filter({ hasText: name });
|
||||
}
|
||||
|
||||
getCredential(name: string): Locator {
|
||||
return this.getCredentials().filter({
|
||||
has: this.page.getByTestId('card-content').locator('h2').filter({ hasText: name }),
|
||||
});
|
||||
}
|
||||
|
||||
getCardActionToggle(card: Locator): Locator {
|
||||
return card
|
||||
.getByTestId('card-append')
|
||||
.locator('[class*="action-toggle"]')
|
||||
.filter({ visible: true });
|
||||
}
|
||||
|
||||
getCardAction(actionName: string): Locator {
|
||||
return this.page.getByTestId(`action-${actionName}`).filter({ visible: true });
|
||||
}
|
||||
|
||||
async openCardActions(card: Locator): Promise<void> {
|
||||
await this.getCardActionToggle(card).click();
|
||||
}
|
||||
|
||||
async clickCardAction(card: Locator, actionName: string): Promise<void> {
|
||||
await this.openCardActions(card);
|
||||
await this.getCardAction(actionName).click();
|
||||
}
|
||||
|
||||
async openFolder(folderName: string): Promise<void> {
|
||||
const folderCard = this.getFolder(folderName);
|
||||
await this.clickCardAction(folderCard, 'open');
|
||||
}
|
||||
|
||||
async deleteFolder(folderName: string): Promise<void> {
|
||||
const folderCard = this.getFolder(folderName);
|
||||
await this.clickCardAction(folderCard, 'delete');
|
||||
}
|
||||
|
||||
async clickWorkflowCard(workflowName: string): Promise<void> {
|
||||
await this.getWorkflow(workflowName).getByTestId('card-content').click();
|
||||
}
|
||||
|
||||
async clickCredentialCard(credentialName: string): Promise<void> {
|
||||
await this.getCredential(credentialName).getByTestId('card-content').click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Page object for interacting with move resource modals (MoveToFolderModal for workflows, ProjectMoveResourceModal for credentials).
|
||||
*/
|
||||
export class ResourceMoveModal {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
getProjectSelect(): Locator {
|
||||
return this.page.getByTestId('project-sharing-select');
|
||||
}
|
||||
|
||||
getProjectSelectCredential(): Locator {
|
||||
return this.page.getByTestId('project-move-resource-modal-select');
|
||||
}
|
||||
|
||||
getMoveConfirmButton(): Locator {
|
||||
return this.page.getByTestId('confirm-move-folder-button');
|
||||
}
|
||||
|
||||
getMoveCredentialButton(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Move credential' });
|
||||
}
|
||||
|
||||
getFolderSelect(): Locator {
|
||||
return this.page.getByTestId('move-to-folder-dropdown');
|
||||
}
|
||||
|
||||
async selectProjectOption(projectNameOrEmail: string): Promise<void> {
|
||||
const options = this.page.getByRole('option');
|
||||
// Try to find by exact text (project name or email)
|
||||
const byExact = options.filter({ hasText: projectNameOrEmail });
|
||||
if ((await byExact.count()) > 0) {
|
||||
await byExact.click();
|
||||
} else {
|
||||
// For personal projects, the email is not shown, so try matching by name part of email
|
||||
const namePart = projectNameOrEmail.split('@')[0].replace(/[.-]/g, ' ');
|
||||
await options
|
||||
.filter({ hasText: new RegExp(namePart, 'i') })
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
async clickMoveCredentialButton(): Promise<void> {
|
||||
await this.getMoveCredentialButton().click();
|
||||
}
|
||||
|
||||
async clickConfirmMoveButton(): Promise<void> {
|
||||
await this.getMoveConfirmButton().click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Page object for the run data view with configurable root element.
|
||||
*
|
||||
* @example
|
||||
* // Include in a page
|
||||
* class ExamplePage {
|
||||
* readonly runDataPanel = new RunDataPanel(this.page.getByTestId('run-data'));
|
||||
* }
|
||||
*
|
||||
* // Usage in a test
|
||||
* await n8n.example.runDataPanel.getRunSelector().click();
|
||||
*/
|
||||
export class RunDataPanel {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
get() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getRunSelector() {
|
||||
return this.root.getByTestId('run-selector');
|
||||
}
|
||||
|
||||
getRunSelectorInput() {
|
||||
return this.root.locator('[data-test-id="run-selector"] input');
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
return this.root.getByTestId('ndv-items-count');
|
||||
}
|
||||
|
||||
getSearchInput() {
|
||||
return this.root.getByTestId('ndv-search');
|
||||
}
|
||||
|
||||
getSearchContainer() {
|
||||
return this.root.getByTestId('ndv-search-container');
|
||||
}
|
||||
|
||||
getDataContainer() {
|
||||
return this.root.getByTestId('ndv-data-container');
|
||||
}
|
||||
|
||||
getPinDataButton() {
|
||||
return this.root.getByTestId('ndv-pin-data');
|
||||
}
|
||||
|
||||
getTable() {
|
||||
return this.root.locator('table');
|
||||
}
|
||||
|
||||
getTableHeaders() {
|
||||
return this.root.locator('table th');
|
||||
}
|
||||
|
||||
getTableHeader(index: number) {
|
||||
return this.root.locator('table th').nth(index);
|
||||
}
|
||||
|
||||
getTableRows() {
|
||||
return this.root.locator('tr');
|
||||
}
|
||||
|
||||
getTableRow(index: number) {
|
||||
return this.root.locator('tr').nth(index);
|
||||
}
|
||||
|
||||
getTbodyCell(row: number, col: number) {
|
||||
return this.root.locator('table tbody tr').nth(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
getTableCellSpan(row: number, col: number, dataName: string) {
|
||||
return this.getTbodyCell(row, col).locator(`span[data-name="${dataName}"]`).first();
|
||||
}
|
||||
|
||||
getJsonDataContainer() {
|
||||
return this.root.locator('.json-data');
|
||||
}
|
||||
|
||||
getJsonProperty(propertyName: string) {
|
||||
return this.root
|
||||
.locator('.json-data')
|
||||
.locator('span')
|
||||
.filter({ hasText: new RegExp(`^"${propertyName}"$`) })
|
||||
.first();
|
||||
}
|
||||
|
||||
getJsonPropertyContaining(text: string) {
|
||||
return this.root
|
||||
.locator('.json-data')
|
||||
.locator('span')
|
||||
.filter({ hasText: `"${text}"` })
|
||||
.first();
|
||||
}
|
||||
|
||||
getSchemaItems() {
|
||||
return this.root.getByTestId('run-data-schema-item');
|
||||
}
|
||||
|
||||
getSchemaItem(text: string) {
|
||||
return this.getSchemaItems().filter({ hasText: text }).first();
|
||||
}
|
||||
|
||||
getSchemaItemText(text: string) {
|
||||
return this.getSchemaItems().locator('span').filter({ hasText: text }).first();
|
||||
}
|
||||
|
||||
getNodeInputOptions() {
|
||||
return this.root.getByTestId('ndv-input-select');
|
||||
}
|
||||
|
||||
getLinkRun() {
|
||||
return this.root.getByTestId('link-run');
|
||||
}
|
||||
|
||||
getRelatedExecutionLink() {
|
||||
return this.root.getByTestId('related-execution-link');
|
||||
}
|
||||
|
||||
getNodeErrorMessageHeader(): Locator {
|
||||
return this.root.getByTestId('node-error-message');
|
||||
}
|
||||
|
||||
async toggleInputRunLinking() {
|
||||
await this.root.getByTestId('link-run').click();
|
||||
}
|
||||
|
||||
async switchDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema' | 'binary'): Promise<void> {
|
||||
await this.root.getByTestId(`radio-button-${mode}`).click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Save Changes Modal component for handling unsaved changes dialogs.
|
||||
* Appears when navigating away from workflow with unsaved changes.
|
||||
*/
|
||||
export class SaveChangesModal {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.root.filter({ hasText: 'Save changes before leaving?' });
|
||||
}
|
||||
|
||||
getCancelButton(): Locator {
|
||||
return this.root.locator('.btn--cancel');
|
||||
}
|
||||
|
||||
getCloseButton(): Locator {
|
||||
return this.root.locator('.el-message-box__headerbtn');
|
||||
}
|
||||
|
||||
async clickCancel(): Promise<void> {
|
||||
await this.getCancelButton().click();
|
||||
}
|
||||
|
||||
async clickClose(): Promise<void> {
|
||||
await this.getCloseButton().click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { BasePage } from '../BasePage';
|
||||
|
||||
/**
|
||||
* Sticky note component for canvas interactions.
|
||||
* Used within CanvasPage as `n8n.canvas.sticky.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page
|
||||
* await n8n.canvas.sticky.addSticky();
|
||||
* await expect(n8n.canvas.sticky.getStickies()).toHaveCount(1);
|
||||
*/
|
||||
export class StickyComponent extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
getAddButton(): Locator {
|
||||
return this.page.getByTestId('add-sticky-button');
|
||||
}
|
||||
|
||||
getStickies(): Locator {
|
||||
return this.page.getByTestId('sticky');
|
||||
}
|
||||
|
||||
async addSticky(): Promise<void> {
|
||||
await this.getAddButton().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sticky from the context menu, targets top left corner of canvas, so could fail if it's covered
|
||||
* @param canvasPane - The canvas pane locator
|
||||
*/
|
||||
async addFromContextMenu(canvasPane: Locator): Promise<void> {
|
||||
await canvasPane.click({
|
||||
button: 'right',
|
||||
position: { x: 10, y: 10 },
|
||||
});
|
||||
await this.page.getByText('Add sticky note').click();
|
||||
}
|
||||
|
||||
getDefaultStickyGuideLink(): Locator {
|
||||
return this.getStickies().first().getByRole('link', { name: 'Guide' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from '../BasePage';
|
||||
|
||||
/**
|
||||
* Tags Manager Modal component for managing workflow tags.
|
||||
* Used within CanvasPage as `n8n.canvas.tagsManagerModal.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page
|
||||
* await n8n.canvas.openTagManagerModal();
|
||||
* await n8n.canvas.tagsManagerModal.clickAddNewButton();
|
||||
* await expect(n8n.canvas.tagsManagerModal.getTable()).toBeVisible();
|
||||
*/
|
||||
export class TagsManagerModal extends BasePage {
|
||||
constructor(private root: Locator) {
|
||||
super(root.page());
|
||||
}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getTable(): Locator {
|
||||
return this.root.getByTestId('tags-table');
|
||||
}
|
||||
|
||||
getTagInputInModal(): Locator {
|
||||
return this.getTable().locator('input').first();
|
||||
}
|
||||
|
||||
getFirstTagRow(): Locator {
|
||||
return this.getTable().locator('tbody tr').first();
|
||||
}
|
||||
|
||||
getDeleteTagButton(): Locator {
|
||||
return this.root.getByTestId('delete-tag-button');
|
||||
}
|
||||
|
||||
getDeleteTagConfirmButton(): Locator {
|
||||
return this.root.getByText('Delete tag', { exact: true });
|
||||
}
|
||||
|
||||
getDeleteConfirmationMessage(): Locator {
|
||||
return this.root.getByText('Are you sure you want to delete this tag?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start adding a new tag, handling both empty state ("Create a tag") and existing tags ("Add new")
|
||||
*/
|
||||
async addTag(): Promise<void> {
|
||||
const addNewButton = this.root.getByRole('button', { name: 'Add new' });
|
||||
const createTagButton = this.root.getByRole('button', { name: 'Create a tag' });
|
||||
await addNewButton.or(createTagButton).click();
|
||||
}
|
||||
|
||||
async clickDoneButton(): Promise<void> {
|
||||
await this.clickButtonByName('Done');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Variable modal component for canvas and variables interactions.
|
||||
* Used within VariablesPage as `n8n.variables.modal.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page or variables page
|
||||
* await n8n.variables.modal.addVariable();
|
||||
* await expect(n8n.variables.modal.getModal()).toBeVisible();
|
||||
*/
|
||||
export class VariableModal {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getKeyInput(): Locator {
|
||||
return this.root.getByTestId('variable-modal-key-input').getByRole('textbox');
|
||||
}
|
||||
|
||||
getValueInput(): Locator {
|
||||
return this.root.getByTestId('variable-modal-value-input').getByRole('textbox');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.root.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.root.getByTestId('variable-modal-save-button');
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const saveBtn = this.getSaveButton();
|
||||
await saveBtn.click();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const closeBtn = this.root.locator('.el-dialog__close').first();
|
||||
if (await closeBtn.isVisible()) {
|
||||
await closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a variable to the modal
|
||||
* @param key - The variable key
|
||||
* @param value - The variable value
|
||||
* @param options - The options to pass to the modal
|
||||
* @param options.closeDialog - Whether to close the modal after saving
|
||||
*/
|
||||
async addVariable(
|
||||
key: string,
|
||||
value: string,
|
||||
{ shouldSave }: { shouldSave: boolean } = { shouldSave: true },
|
||||
): Promise<void> {
|
||||
await this.getKeyInput().fill(key);
|
||||
await this.getValueInput().fill(value);
|
||||
if (shouldSave) {
|
||||
console.log('Saving variable from modal');
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user