diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7b60bd99..d725673e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,10 +18,11 @@ jobs: - name: Install Playwright Browsers run: yarn playwright install --with-deps - name: Run Playwright tests + working-directory: ./packages/e2e-tests env: LOGIN_EMAIL: ${{ secrets.LOGIN_EMAIL }} LOGIN_PASSWORD: ${{ secrets.LOGIN_PASSWORD }} - run: yarn playwright test + run: yarn test - uses: actions/upload-artifact@v3 if: always() with: diff --git a/packages/e2e-tests/fixtures/applications-page.js b/packages/e2e-tests/fixtures/applications-page.js index 19934035..1456de19 100644 --- a/packages/e2e-tests/fixtures/applications-page.js +++ b/packages/e2e-tests/fixtures/applications-page.js @@ -2,6 +2,16 @@ const path = require('node:path'); const { BasePage } = require('./base-page'); export class ApplicationsPage extends BasePage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.drawerLink = this.page.getByTestId('apps-page-drawer-link'); + this.addConnectionButton = this.page.getByTestId('add-connection-button'); + } + async screenshot(options = {}) { const { path: plainPath, ...restOptions } = options; diff --git a/packages/e2e-tests/fixtures/flow-editor-page.js b/packages/e2e-tests/fixtures/flow-editor-page.js index 4b894407..270a8347 100644 --- a/packages/e2e-tests/fixtures/flow-editor-page.js +++ b/packages/e2e-tests/fixtures/flow-editor-page.js @@ -2,8 +2,12 @@ const path = require('node:path'); const { BasePage } = require('./base-page'); export class FlowEditorPage extends BasePage { + /** + * @param {import('@playwright/test').Page} page + */ constructor(page) { super(page); + this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete'); this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete'); this.continueButton = this.page.getByTestId('flow-substep-continue-button'); @@ -15,6 +19,7 @@ export class FlowEditorPage extends BasePage { this.publishFlowButton = this.page.getByTestId('publish-flow-button'); this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar'); this.trigger = this.page.getByLabel('Trigger on weekends?'); + this.stepCircularLoader = this.page.getByTestId('step-circular-loader'); } async screenshot(options = {}) { diff --git a/packages/e2e-tests/fixtures/index.js b/packages/e2e-tests/fixtures/index.js index 7116534d..c4388aa4 100644 --- a/packages/e2e-tests/fixtures/index.js +++ b/packages/e2e-tests/fixtures/index.js @@ -1,11 +1,11 @@ -const base = require('@playwright/test'); +const { test, expect} = require('@playwright/test'); const { ApplicationsPage } = require('./applications-page'); const { ConnectionsPage } = require('./connections-page'); const { ExecutionsPage } = require('./executions-page'); const { FlowEditorPage } = require('./flow-editor-page'); const { LoginPage } = require('./login-page'); -exports.test = base.test.extend({ +exports.test = test.extend({ page: async ({ page }, use) => { await new LoginPage(page).login(); @@ -25,4 +25,12 @@ exports.test = base.test.extend({ }, }); -exports.expect = base.expect; +expect.extend({ + toBeClickableLink: async (locator) => { + await expect(locator).not.toHaveAttribute('aria-disabled', 'true'); + + return { pass: true }; + } +}); + +exports.expect = expect; diff --git a/packages/e2e-tests/fixtures/login-page.js b/packages/e2e-tests/fixtures/login-page.js index b3ee67c1..78647fc5 100644 --- a/packages/e2e-tests/fixtures/login-page.js +++ b/packages/e2e-tests/fixtures/login-page.js @@ -1,4 +1,5 @@ const path = require('node:path'); +const { expect } = require('@playwright/test'); const { BasePage } = require('./base-page'); export class LoginPage extends BasePage { @@ -9,7 +10,6 @@ export class LoginPage extends BasePage { super(page); this.page = page; - this.emailTextField = this.page.getByTestId('email-text-field'); this.passwordTextField = this.page.getByTestId('password-text-field'); this.loginButton = this.page.getByTestId('login-button'); @@ -23,5 +23,7 @@ export class LoginPage extends BasePage { await this.passwordTextField.fill(process.env.LOGIN_PASSWORD); await this.loginButton.click(); + + await expect(this.loginButton).not.toBeVisible(); } } diff --git a/packages/e2e-tests/playwright.config.js b/packages/e2e-tests/playwright.config.js index 4b7ef3e6..21160ce6 100644 --- a/packages/e2e-tests/playwright.config.js +++ b/packages/e2e-tests/playwright.config.js @@ -20,6 +20,8 @@ module.exports = defineConfig({ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, + /* Timeout threshold for each test */ + timeout: 120000, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'github' : 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -35,6 +37,11 @@ module.exports = defineConfig({ viewport: { width: 1280, height: 720 }, }, + expect: { + /* Timeout threshold for each assertion */ + timeout: 30000, + }, + /* Configure projects for major browsers */ projects: [ { diff --git a/packages/e2e-tests/tests/apps/list-apps.spec.js b/packages/e2e-tests/tests/apps/list-apps.spec.js index 38583ef3..e321efcc 100644 --- a/packages/e2e-tests/tests/apps/list-apps.spec.js +++ b/packages/e2e-tests/tests/apps/list-apps.spec.js @@ -2,15 +2,15 @@ const { test, expect } = require('../../fixtures/index'); test.describe('Apps page', () => { - test.beforeEach(async ({ page, applicationsPage }) => { - await page.getByTestId('apps-page-drawer-link').click(); + test.beforeEach(async ({ applicationsPage }) => { + await applicationsPage.drawerLink.click(); }); - test('displays applications', async ({ page, applicationsPage }) => { - await page.getByTestId('apps-loader').waitFor({ + test('displays applications', async ({ applicationsPage }) => { + await applicationsPage.page.getByTestId('apps-loader').waitFor({ state: 'detached', }); - await expect(page.getByTestId('app-row')).not.toHaveCount(0); + await expect(applicationsPage.page.getByTestId('app-row')).not.toHaveCount(0); await applicationsPage.screenshot({ path: 'Applications.png', @@ -18,49 +18,56 @@ test.describe('Apps page', () => { }); test.describe('can add connection', () => { - test.beforeEach(async ({ page }) => { - await expect(page.getByTestId('add-connection-button')).toBeVisible(); - await page.getByTestId('add-connection-button').click(); - await page + test.beforeEach(async ({ applicationsPage }) => { + await expect(applicationsPage.addConnectionButton).toBeClickableLink(); + await applicationsPage.addConnectionButton.click(); + await applicationsPage + .page .getByTestId('search-for-app-loader') .waitFor({ state: 'detached' }); }); - test('lists applications', async ({ page, applicationsPage }) => { - const appListItemCount = await page.getByTestId('app-list-item').count(); + test('lists applications', async ({ applicationsPage }) => { + const appListItemCount = await applicationsPage.page.getByTestId('app-list-item').count(); expect(appListItemCount).toBeGreaterThan(10); await applicationsPage.clickAway(); }); - test('searches an application', async ({ page, applicationsPage }) => { - await page.getByTestId('search-for-app-text-field').fill('DeepL'); - await expect(page.getByTestId('app-list-item')).toHaveCount(1); + test('searches an application', async ({ applicationsPage }) => { + await applicationsPage.page.getByTestId('search-for-app-text-field').fill('DeepL'); + await applicationsPage + .page + .getByTestId('search-for-app-loader') + .waitFor({ state: 'detached' }); + + await expect(applicationsPage.page.getByTestId('app-list-item')).toHaveCount(1); await applicationsPage.clickAway(); }); test('goes to app page to create a connection', async ({ - page, applicationsPage, }) => { - await page.getByTestId('app-list-item').first().click(); - await expect(page).toHaveURL('/app/deepl/connections/add?shared=false'); - await expect(page.getByTestId('add-app-connection-dialog')).toBeVisible(); + // loading app, app config, app auth clients take time + test.setTimeout(60000); + + await applicationsPage.page.getByTestId('app-list-item').first().click(); + await expect(applicationsPage.page).toHaveURL('/app/deepl/connections/add?shared=false'); + await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeVisible(); await applicationsPage.clickAway(); }); test('closes the dialog on backdrop click', async ({ - page, applicationsPage, }) => { - await page.getByTestId('app-list-item').first().click(); - await expect(page).toHaveURL('/app/deepl/connections/add?shared=false'); - await expect(page.getByTestId('add-app-connection-dialog')).toBeVisible(); + await applicationsPage.page.getByTestId('app-list-item').first().click(); + await expect(applicationsPage.page).toHaveURL('/app/deepl/connections/add?shared=false'); + await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeVisible(); await applicationsPage.clickAway(); - await expect(page).toHaveURL('/app/deepl/connections'); - await expect(page.getByTestId('add-app-connection-dialog')).toBeHidden(); + await expect(applicationsPage.page).toHaveURL('/app/deepl/connections'); + await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeHidden(); }); }); }); diff --git a/packages/e2e-tests/tests/connections/create-connection.spec.js b/packages/e2e-tests/tests/connections/create-connection.spec.js index 2415edc0..f9f4669c 100644 --- a/packages/e2e-tests/tests/connections/create-connection.spec.js +++ b/packages/e2e-tests/tests/connections/create-connection.spec.js @@ -19,7 +19,7 @@ test.describe('Connections page', () => { test.describe('can add connection', () => { test('has a button to open add connection dialog', async ({ page }) => { - await expect(page.getByTestId('add-connection-button')).toBeVisible(); + await expect(page.getByTestId('add-connection-button')).toBeClickableLink(); }); test('add connection button takes user to add connection page', async ({ diff --git a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js index 00e30627..70070278 100644 --- a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js +++ b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js @@ -13,28 +13,30 @@ test.beforeAll(async ({ browser }) => { flowEditorPage = new FlowEditorPage(page); }); -test('create flow', async ({}) => { +test('create flow', async () => { await flowEditorPage.page.getByTestId('create-flow-button').click(); await expect(flowEditorPage.page).toHaveURL(/\/editor\/create/); await expect(flowEditorPage.page).toHaveURL( /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ ); + + await expect(flowEditorPage.stepCircularLoader).not.toBeVisible(); }); -test('has two steps by default', async ({}) => { +test('has two steps by default', async () => { await expect(flowEditorPage.page.getByTestId('flow-step')).toHaveCount(2); }); test.describe('arrange Scheduler trigger', () => { test.describe('choose app and event substep', () => { - test('choose application', async ({}) => { + test('choose application', async () => { await flowEditorPage.appAutocomplete.click(); await flowEditorPage.page .getByRole('option', { name: 'Scheduler' }) .click(); }); - test('choose an event', async ({}) => { + test('choose an event', async () => { await expect(flowEditorPage.eventAutocomplete).toBeVisible(); await flowEditorPage.eventAutocomplete.click(); await flowEditorPage.page @@ -42,34 +44,34 @@ test.describe('arrange Scheduler trigger', () => { .click(); }); - test('continue to next step', async ({}) => { + test('continue to next step', async () => { await flowEditorPage.continueButton.click(); }); - test('collapses the substep', async ({}) => { + test('collapses the substep', async () => { await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); }); }); test.describe('set up a trigger', () => { - test('choose "yes" in "trigger on weekends?"', async ({}) => { + test('choose "yes" in "trigger on weekends?"', async () => { await expect(flowEditorPage.trigger).toBeVisible(); await flowEditorPage.trigger.click(); await flowEditorPage.page.getByRole('option', { name: 'Yes' }).click(); }); - test('continue to next step', async ({}) => { + test('continue to next step', async () => { await flowEditorPage.continueButton.click(); }); - test('collapses the substep', async ({}) => { + test('collapses the substep', async () => { await expect(flowEditorPage.trigger).not.toBeVisible(); }); }); test.describe('test trigger', () => { - test('show sample output', async ({}) => { + test('show sample output', async () => { await expect(flowEditorPage.testOuput).not.toBeVisible(); await flowEditorPage.continueButton.click(); await expect(flowEditorPage.testOuput).toBeVisible(); @@ -83,12 +85,12 @@ test.describe('arrange Scheduler trigger', () => { test.describe('arrange Ntfy action', () => { test.describe('choose app and event substep', () => { - test('choose application', async ({}) => { + test('choose application', async () => { await flowEditorPage.appAutocomplete.click(); await flowEditorPage.page.getByRole('option', { name: 'Ntfy' }).click(); }); - test('choose an event', async ({}) => { + test('choose an event', async () => { await expect(flowEditorPage.eventAutocomplete).toBeVisible(); await flowEditorPage.eventAutocomplete.click(); await flowEditorPage.page @@ -96,33 +98,33 @@ test.describe('arrange Ntfy action', () => { .click(); }); - test('continue to next step', async ({}) => { + test('continue to next step', async () => { await flowEditorPage.continueButton.click(); }); - test('collapses the substep', async ({}) => { + test('collapses the substep', async () => { await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); }); }); test.describe('choose connection', () => { - test('choose connection list item', async ({}) => { + test('choose connection list item', async () => { await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.page.getByRole('option').first().click(); }); - test('continue to next step', async ({}) => { + test('continue to next step', async () => { await flowEditorPage.continueButton.click(); }); - test('collapses the substep', async ({}) => { + test('collapses the substep', async () => { await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); }); }); test.describe('set up action', () => { - test('fill topic and message body', async ({}) => { + test('fill topic and message body', async () => { await flowEditorPage.page .getByTestId('parameters.topic-power-input') .locator('[contenteditable]') @@ -133,17 +135,17 @@ test.describe('arrange Ntfy action', () => { .fill('Message body'); }); - test('continue to next step', async ({}) => { + test('continue to next step', async () => { await flowEditorPage.continueButton.click(); }); - test('collapses the substep', async ({}) => { + test('collapses the substep', async () => { await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); }); }); test.describe('test trigger', () => { - test('show sample output', async ({}) => { + test('show sample output', async () => { await expect(flowEditorPage.testOuput).not.toBeVisible(); await flowEditorPage.page .getByTestId('flow-substep-continue-button') @@ -159,34 +161,34 @@ test.describe('arrange Ntfy action', () => { }); test.describe('publish and unpublish', () => { - test('publish flow', async ({}) => { + test('publish flow', async () => { await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); await expect(flowEditorPage.publishFlowButton).toBeVisible(); await flowEditorPage.publishFlowButton.click(); await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); }); - test('shows read-only sticky snackbar', async ({}) => { + test('shows read-only sticky snackbar', async () => { await expect(flowEditorPage.infoSnackbar).toBeVisible(); await flowEditorPage.screenshot({ path: 'Published flow.png', }); }); - test('unpublish from snackbar', async ({}) => { + test('unpublish from snackbar', async () => { await flowEditorPage.page .getByTestId('unpublish-flow-from-snackbar') .click(); await expect(flowEditorPage.infoSnackbar).not.toBeVisible(); }); - test('publish once again', async ({}) => { + test('publish once again', async () => { await expect(flowEditorPage.publishFlowButton).toBeVisible(); await flowEditorPage.publishFlowButton.click(); await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); }); - test('unpublish from layout top bar', async ({}) => { + test('unpublish from layout top bar', async () => { await expect(flowEditorPage.unpublishFlowButton).toBeVisible(); await flowEditorPage.unpublishFlowButton.click(); await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); @@ -197,7 +199,7 @@ test.describe('publish and unpublish', () => { }); test.describe('in layout', () => { - test('can go back to flows page', async ({}) => { + test('can go back to flows page', async () => { await flowEditorPage.page.getByTestId('editor-go-back-button').click(); await expect(flowEditorPage.page).toHaveURL('/flows'); }); diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index d971b042..99821b2b 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -71,17 +71,18 @@ function generateValidationSchema(substeps: ISubstep[]) { substepArgumentValidations[key] = yup.mixed(); } - if (typeof substepArgumentValidations[key] === 'object' && (arg.type === 'string' || arg.type === 'dropdown')) { + if ( + typeof substepArgumentValidations[key] === 'object' && + (arg.type === 'string' || arg.type === 'dropdown') + ) { // if the field is required, add the required validation if (required) { - substepArgumentValidations[key] = substepArgumentValidations[ - key - ] + substepArgumentValidations[key] = substepArgumentValidations[key] .required(`${key} is required.`) .test( 'empty-check', `${key} must be not empty`, - (value: any) => !isEmpty(value), + (value: any) => !isEmpty(value) ); } @@ -166,7 +167,9 @@ export default function FlowStep( const actionsOrTriggers: Array = (isTrigger ? app?.triggers : app?.actions) || []; - const actionOrTrigger = actionsOrTriggers?.find(({ key }) => key === step.key); + const actionOrTrigger = actionsOrTriggers?.find( + ({ key }) => key === step.key + ); const substeps = actionOrTrigger?.substeps || []; const handleChange = React.useCallback(({ step }: { step: IStep }) => { @@ -187,7 +190,12 @@ export default function FlowStep( ); if (!apps) { - return ; + return ( + + ); } const onContextMenuClose = (event: React.SyntheticEvent) => { @@ -279,7 +287,8 @@ export default function FlowStep( step={step} /> - {actionOrTrigger && substeps?.length > 0 && + {actionOrTrigger && + substeps?.length > 0 && substeps.map((substep: ISubstep, index: number) => ( {substep.key === 'chooseConnection' && app && ( @@ -304,7 +313,11 @@ export default function FlowStep( onSubmit={expandNextStep} onChange={handleChange} onContinue={onContinue} - showWebhookUrl={'showWebhookUrl' in actionOrTrigger ? actionOrTrigger.showWebhookUrl : false} + showWebhookUrl={ + 'showWebhookUrl' in actionOrTrigger + ? actionOrTrigger.showWebhookUrl + : false + } step={step} /> )}