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 { await this.root.waitFor({ state: 'visible' }); } async fillField(key: string, value: string): Promise { 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); } /** * Switch a credential field to expression mode and fill it with an expression. * * Expression mode is activated by clicking the "Expression" radio button that * appears when hovering over a parameter input. * * @example * await modal.fillExpressionField('value', "{{ $secrets['myVault']['apikey'] }}"); */ async fillExpressionField(key: string, expression: string): Promise { const parameterInput = this.root .getByTestId('credential-connection-parameter') .getByTestId(key); // Hover to reveal the Fixed / Expression toggle await parameterInput.locator('label').first().hover(); await parameterInput.getByTestId('parameter-options-container').waitFor({ state: 'visible' }); // Click the "Expression" radio option await parameterInput.getByTestId('radio-button-expression').click(); // After switching modes, the field becomes a CodeMirror editor const cmContent = parameterInput.locator('.cm-content'); await cmContent.waitFor({ state: 'visible', timeout: 10_000 }); await cmContent.click(); await cmContent.fill(expression); } async fillAllFields(values: Record): Promise { for (const [key, val] of Object.entries(values)) { await this.fillField(key, val); } } getSaveButton(): Locator { return this.root.getByTestId('credential-save-button'); } getParameterInputHint(): Locator { return this.container.getByTestId('parameter-input-hint'); } /** * Wait for save to fully complete. * After saving (and optional credential testing), the button shows "Saved" label. */ async waitForSaveComplete(): Promise { await expect(this.getSaveButton().getByText('Saved', { exact: true })).toBeVisible({ timeout: 10000, }); } async save(): Promise { await this.getSaveButton().click(); await this.waitForSaveComplete(); } async close(): Promise { 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, options?: { closeDialog?: boolean; skipSave?: boolean; name?: string }, ): Promise { 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('quick-connect-button'); } get oauthConnectSuccessBanner() { return this.root.getByTestId('oauth-connect-success-banner'); } async editCredential(): Promise { await this.root.page().getByTestId('credential-edit-button').click(); } async deleteCredential(): Promise { await this.root.page().getByTestId('credential-delete-button').click(); } async confirmDelete(): Promise { await this.root.page().getByRole('button', { name: 'Yes' }).click(); } async renameCredential(newName: string): Promise { 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 { await this.getModeDropdownTrigger().click(); await this.root.page().getByRole('menuitem', { name: optionName }).click(); } async changeTab(tabName: 'Sharing'): Promise { 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 { 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 { 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(); } }