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,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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user