diff --git a/cypress/e2e/10-settings-log-streaming.cy.ts b/cypress/e2e/10-settings-log-streaming.cy.ts deleted file mode 100644 index e294186b83..0000000000 --- a/cypress/e2e/10-settings-log-streaming.cy.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { SettingsLogStreamingPage } from '../pages'; -import { getVisibleDropdown } from '../utils'; -import { getVisibleModalOverlay } from '../utils/modal'; - -const settingsLogStreamingPage = new SettingsLogStreamingPage(); - -describe('Log Streaming Settings', () => { - it('should show the unlicensed view when the feature is disabled', () => { - cy.visit('/settings/log-streaming'); - settingsLogStreamingPage.getters.getActionBoxUnlicensed().should('be.visible'); - settingsLogStreamingPage.getters.getContactUsButton().should('be.visible'); - settingsLogStreamingPage.getters.getActionBoxLicensed().should('not.exist'); - }); - - it('should show the licensed view when the feature is enabled', () => { - cy.enableFeature('logStreaming'); - cy.visit('/settings/log-streaming'); - settingsLogStreamingPage.getters.getActionBoxLicensed().should('be.visible'); - settingsLogStreamingPage.getters.getAddFirstDestinationButton().should('be.visible'); - settingsLogStreamingPage.getters.getActionBoxUnlicensed().should('not.exist'); - }); - - it('should show the add destination modal', () => { - cy.enableFeature('logStreaming'); - cy.visit('/settings/log-streaming'); - settingsLogStreamingPage.actions.clickAddFirstDestination(); - cy.wait(100); - settingsLogStreamingPage.getters.getDestinationModal().should('be.visible'); - settingsLogStreamingPage.getters.getSelectDestinationType().should('be.visible'); - settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible'); - settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled'); - settingsLogStreamingPage.getters - .getDestinationModal() - .invoke('css', 'width') - .then((widthStr) => parseInt((widthStr as unknown as string).replace('px', ''))) - .should('be.lessThan', 500); - settingsLogStreamingPage.getters.getSelectDestinationType().click(); - settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); - settingsLogStreamingPage.getters - .getSelectDestinationButton() - .should('not.have.attr', 'disabled'); - getVisibleModalOverlay().click(1, 1); - settingsLogStreamingPage.getters.getDestinationModal().should('not.exist'); - }); - - it('should create a destination and delete it', () => { - cy.enableFeature('logStreaming'); - cy.visit('/settings/log-streaming'); - cy.wait(1000); // Race condition with getDestinationDataFromBackend() - settingsLogStreamingPage.actions.clickAddFirstDestination(); - cy.wait(100); - settingsLogStreamingPage.getters.getDestinationModal().should('be.visible'); - settingsLogStreamingPage.getters.getSelectDestinationType().click(); - settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); - settingsLogStreamingPage.getters.getSelectDestinationButton().click(); - settingsLogStreamingPage.getters.getDestinationNameInput().click(); - - settingsLogStreamingPage.getters - .getDestinationNameInput() - .find('span[data-test-id=inline-edit-preview]') - .click({ force: true }); - cy.getByTestId('inline-edit-input').type('Destination 0'); - settingsLogStreamingPage.getters.getDestinationSaveButton().click(); - cy.wait(100); - getVisibleModalOverlay().click(1, 1); - cy.reload(); - settingsLogStreamingPage.getters.getDestinationCards().eq(0).click(); - settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click(); - cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click(); - settingsLogStreamingPage.getters.getDestinationDeleteButton().click(); - cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); - }); - - it('should create a destination and delete it via card actions', () => { - cy.enableFeature('logStreaming'); - cy.visit('/settings/log-streaming'); - cy.wait(1000); // Race condition with getDestinationDataFromBackend() - settingsLogStreamingPage.actions.clickAddFirstDestination(); - cy.wait(100); - settingsLogStreamingPage.getters.getDestinationModal().should('be.visible'); - settingsLogStreamingPage.getters.getSelectDestinationType().click(); - settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); - settingsLogStreamingPage.getters.getSelectDestinationButton().click(); - settingsLogStreamingPage.getters.getDestinationNameInput().click(); - settingsLogStreamingPage.getters - .getDestinationNameInput() - .find('span[data-test-id=inline-edit-preview]') - .click({ force: true }); - cy.getByTestId('inline-edit-input').type('Destination 1'); - settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled'); - settingsLogStreamingPage.getters.getDestinationSaveButton().click(); - cy.wait(100); - getVisibleModalOverlay().click(1, 1); - cy.reload(); - - settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click(); - getVisibleDropdown().find('.el-dropdown-menu__item').eq(0).click(); - settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist'); - getVisibleModalOverlay().click(1, 1); - - settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click(); - getVisibleDropdown().find('.el-dropdown-menu__item').eq(1).click(); - cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); - }); -}); diff --git a/packages/testing/playwright/pages/SettingsLogStreamingPage.ts b/packages/testing/playwright/pages/SettingsLogStreamingPage.ts new file mode 100644 index 0000000000..5a2eac1df2 --- /dev/null +++ b/packages/testing/playwright/pages/SettingsLogStreamingPage.ts @@ -0,0 +1,172 @@ +import type { Locator } from '@playwright/test'; + +import { BasePage } from './BasePage'; + +export class SettingsLogStreamingPage extends BasePage { + getActionBoxUnlicensed(): Locator { + return this.page.getByTestId('action-box-unlicensed'); + } + + getActionBoxLicensed(): Locator { + return this.page.getByTestId('action-box-licensed'); + } + + getContactUsButton(): Locator { + return this.getActionBoxUnlicensed().locator('button'); + } + + getAddFirstDestinationButton(): Locator { + return this.getActionBoxLicensed().locator('button'); + } + + getDestinationModal(): Locator { + return this.page.getByTestId('destination-modal'); + } + + getSelectDestinationType(): Locator { + return this.page.getByTestId('select-destination-type'); + } + + getSelectDestinationTypeItems(): Locator { + return this.page.locator('.el-select-dropdown__item'); + } + + getSelectDestinationButton(): Locator { + return this.page.getByTestId('select-destination-button'); + } + + getDestinationNameInput(): Locator { + return this.page.getByTestId('subtitle-showing-type'); + } + + getDestinationSaveButton(): Locator { + return this.page.getByTestId('destination-save-button').locator('button'); + } + + getDestinationDeleteButton(): Locator { + return this.page.getByTestId('destination-delete-button'); + } + + getDestinationCards(): Locator { + return this.page.getByTestId('destination-card'); + } + + getInlineEditPreview(): Locator { + return this.page.getByTestId('inline-edit-preview'); + } + + getInlineEditInput(): Locator { + return this.page.getByTestId('inline-edit-input'); + } + + getModalOverlay(): Locator { + return this.page.locator('.el-overlay'); + } + + getDropdownMenu(): Locator { + return this.page.locator('.el-dropdown-menu'); + } + + getDropdownMenuItem(index: number): Locator { + return this.page.locator('.el-dropdown-menu__item').nth(index); + } + + getConfirmationDialog(): Locator { + return this.page.locator('.el-message-box'); + } + + getCancelButton(): Locator { + return this.page.locator('.btn--cancel'); + } + + getConfirmButton(): Locator { + return this.page.locator('.btn--confirm'); + } + + async clickAddFirstDestination(): Promise { + await this.getAddFirstDestinationButton().click(); + } + + async clickSelectDestinationType(): Promise { + await this.clickByTestId('select-destination-type'); + } + + async selectDestinationType(index: number): Promise { + await this.getSelectDestinationTypeItems().nth(index).click(); + } + + async clickSelectDestinationButton(): Promise { + await this.clickByTestId('select-destination-button'); + } + + async clickDestinationNameInput(): Promise { + await this.clickByTestId('subtitle-showing-type'); + } + + async clickInlineEditPreview(): Promise { + // First click on the destination name input to activate it + await this.getDestinationNameInput().click(); + const inlineEditPreview = this.getDestinationNameInput().locator( + 'span[data-test-id="inline-edit-preview"]', + ); + // eslint-disable-next-line playwright/no-force-option + await inlineEditPreview.click({ force: true }); + } + + async typeDestinationName(name: string): Promise { + await this.fillByTestId('inline-edit-input', name); + } + + async saveDestination(): Promise { + await this.getDestinationSaveButton().click(); + } + + async deleteDestination(): Promise { + await this.clickByTestId('destination-delete-button'); + } + + async clickDestinationCard(index: number): Promise { + await this.getDestinationCards().nth(index).click(); + } + + async clickDestinationCardDropdown(index: number): Promise { + await this.getDestinationCards().nth(index).locator('.el-dropdown').click(); + } + + async clickDropdownMenuItem(index: number): Promise { + await this.getDropdownMenuItem(index).click(); + } + + async closeModalByClickingOverlay(): Promise { + await this.page + .locator('.el-overlay') + .filter({ has: this.getDestinationModal() }) + .click({ position: { x: 1, y: 1 } }); + } + + async confirmDialog(): Promise { + await this.getConfirmButton().click(); + } + + async cancelDialog(): Promise { + await this.getCancelButton().click(); + } + + /** + * Creates a new log streaming destination with the specified name. + * Handles the full flow: modal opening, type selection, naming, and saving. + * @param destinationName - The name to give the new destination + */ + async createDestination(destinationName: string): Promise { + await this.clickAddFirstDestination(); + await this.getDestinationModal().waitFor({ state: 'visible' }); + await this.clickSelectDestinationType(); + await this.selectDestinationType(0); + await this.clickSelectDestinationButton(); + await this.clickDestinationNameInput(); + await this.clickInlineEditPreview(); + await this.typeDestinationName(destinationName); + await this.saveDestination(); + await this.closeModalByClickingOverlay(); + } +} diff --git a/packages/testing/playwright/pages/n8nPage.ts b/packages/testing/playwright/pages/n8nPage.ts index 8437d8a452..275014aa8f 100644 --- a/packages/testing/playwright/pages/n8nPage.ts +++ b/packages/testing/playwright/pages/n8nPage.ts @@ -13,6 +13,7 @@ import { NodeDetailsViewPage } from './NodeDetailsViewPage'; import { NotificationsPage } from './NotificationsPage'; import { NpsSurveyPage } from './NpsSurveyPage'; import { ProjectSettingsPage } from './ProjectSettingsPage'; +import { SettingsLogStreamingPage } from './SettingsLogStreamingPage'; import { SettingsPage } from './SettingsPage'; import { SidebarPage } from './SidebarPage'; import { VariablesPage } from './VariablesPage'; @@ -47,6 +48,7 @@ export class n8nPage { readonly npsSurvey: NpsSurveyPage; readonly projectSettings: ProjectSettingsPage; readonly settings: SettingsPage; + readonly settingsLogStreaming: SettingsLogStreamingPage; readonly variables: VariablesPage; readonly versions: VersionsPage; readonly workerView: WorkerViewPage; @@ -87,6 +89,7 @@ export class n8nPage { this.npsSurvey = new NpsSurveyPage(page); this.projectSettings = new ProjectSettingsPage(page); this.settings = new SettingsPage(page); + this.settingsLogStreaming = new SettingsLogStreamingPage(page); this.variables = new VariablesPage(page); this.versions = new VersionsPage(page); this.workerView = new WorkerViewPage(page); diff --git a/packages/testing/playwright/tests/ui/10-settings-log-streaming.spec.ts b/packages/testing/playwright/tests/ui/10-settings-log-streaming.spec.ts new file mode 100644 index 0000000000..6a43e1cc82 --- /dev/null +++ b/packages/testing/playwright/tests/ui/10-settings-log-streaming.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '../../fixtures/base'; + +const DESTINATION_NAMES = { + FIRST: 'Destination 0', + SECOND: 'Destination 1', +} as const; + +const MODAL_MAX_WIDTH = 500; + +test.describe('Log Streaming Settings @db:reset', () => { + test.describe('unlicensed', () => { + test.beforeEach(async ({ api }) => { + await api.disableFeature('logStreaming'); + }); + + test('should show the unlicensed view when the feature is disabled', async ({ n8n }) => { + await n8n.navigate.toLogStreaming(); + await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getContactUsButton()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).not.toBeAttached(); + }); + }); + + test.describe('licensed', () => { + test.beforeEach(async ({ api, n8n }) => { + await api.enableFeature('logStreaming'); + await n8n.navigate.toLogStreaming(); + }); + + test('should show the licensed view when the feature is enabled', async ({ n8n }) => { + await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getAddFirstDestinationButton()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).not.toBeAttached(); + }); + + test('should show the add destination modal', async ({ n8n }) => { + await n8n.settingsLogStreaming.clickAddFirstDestination(); + await expect(n8n.settingsLogStreaming.getDestinationModal()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getSelectDestinationType()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeVisible(); + await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeDisabled(); + + const modal = n8n.settingsLogStreaming.getDestinationModal(); + const width = await modal.evaluate((element) => { + return parseInt(window.getComputedStyle(element).width.replace('px', '')); + }); + expect(width).toBeLessThan(MODAL_MAX_WIDTH); + + await n8n.settingsLogStreaming.clickSelectDestinationType(); + await n8n.settingsLogStreaming.selectDestinationType(0); + await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeEnabled(); + await n8n.settingsLogStreaming.closeModalByClickingOverlay(); + await expect(n8n.settingsLogStreaming.getDestinationModal()).not.toBeAttached(); + }); + + test('should create a destination and delete it', async ({ n8n }) => { + await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.FIRST); + await n8n.page.reload(); + await n8n.settingsLogStreaming.clickDestinationCard(0); + await expect(n8n.settingsLogStreaming.getDestinationDeleteButton()).toBeVisible(); + await n8n.settingsLogStreaming.deleteDestination(); + await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible(); + await n8n.settingsLogStreaming.cancelDialog(); + await n8n.settingsLogStreaming.deleteDestination(); + await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible(); + await n8n.settingsLogStreaming.confirmDialog(); + }); + + test('should create a destination and delete it via card actions', async ({ n8n }) => { + await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.SECOND); + await n8n.page.reload(); + + await n8n.settingsLogStreaming.clickDestinationCardDropdown(0); + await n8n.settingsLogStreaming.clickDropdownMenuItem(0); + await expect(n8n.settingsLogStreaming.getDestinationSaveButton()).not.toBeAttached(); + await n8n.settingsLogStreaming.closeModalByClickingOverlay(); + + await n8n.settingsLogStreaming.clickDestinationCardDropdown(0); + await n8n.settingsLogStreaming.clickDropdownMenuItem(1); + await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible(); + await n8n.settingsLogStreaming.confirmDialog(); + }); + }); +});