diff --git a/packages/e2e-tests/fixtures/flow-editor-page.js b/packages/e2e-tests/fixtures/flow-editor-page.js new file mode 100644 index 00000000..4b894407 --- /dev/null +++ b/packages/e2e-tests/fixtures/flow-editor-page.js @@ -0,0 +1,27 @@ +const path = require('node:path'); +const { BasePage } = require('./base-page'); + +export class FlowEditorPage extends BasePage { + 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'); + this.connectionAutocomplete = this.page.getByTestId( + 'choose-connection-autocomplete' + ); + this.testOuput = this.page.getByTestId('flow-test-substep-output'); + this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button'); + 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?'); + } + + async screenshot(options = {}) { + const { path: plainPath, ...restOptions } = options; + + const computedPath = path.join('flow-editor', plainPath); + + return await super.screenshot({ path: computedPath, ...restOptions }); + } +} diff --git a/packages/e2e-tests/fixtures/index.js b/packages/e2e-tests/fixtures/index.js index 07348681..8c7bc91a 100644 --- a/packages/e2e-tests/fixtures/index.js +++ b/packages/e2e-tests/fixtures/index.js @@ -2,6 +2,7 @@ const base = require('@playwright/test'); const { ApplicationsPage } = require('./applications-page'); const { ConnectionsPage } = require('./connections-page'); const { ExecutionsPage } = require('./executions-page'); +const { FlowEditorPage } = require('./flow-editor-page'); exports.test = base.test.extend({ applicationsPage: async ({ page }, use) => { @@ -13,5 +14,8 @@ exports.test = base.test.extend({ executionsPage: async ({ page }, use) => { await use(new ExecutionsPage(page)); }, + flowEditorPage: async ({ page }, use) => { + await use(new FlowEditorPage(page)); + }, }); exports.expect = base.expect; diff --git a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js new file mode 100644 index 00000000..77fcb985 --- /dev/null +++ b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js @@ -0,0 +1,205 @@ +// @ts-check +const { FlowEditorPage } = require('../../fixtures/flow-editor-page'); +const { test, expect } = require('../../fixtures/index'); + +test.describe.configure({ mode: 'serial' }); + +let page; +let flowEditorPage; + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + flowEditorPage = new FlowEditorPage(page); +}); + +test('create flow', async ({}) => { + await flowEditorPage.login(); + + 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}/ + ); +}); + +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 ({}) => { + await flowEditorPage.appAutocomplete.click(); + await flowEditorPage.page + .getByRole('option', { name: 'Scheduler' }) + .click(); + }); + + test('choose an event', async ({}) => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await flowEditorPage.page + .getByRole('option', { name: 'Every hour' }) + .click(); + }); + + test('continue to next step', async ({}) => { + await flowEditorPage.continueButton.click(); + }); + + 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 ({}) => { + await expect(flowEditorPage.trigger).toBeVisible(); + await flowEditorPage.trigger.click(); + await flowEditorPage.page.getByRole('option', { name: 'Yes' }).click(); + }); + + test('continue to next step', async ({}) => { + await flowEditorPage.continueButton.click(); + }); + + test('collapses the substep', async ({}) => { + await expect(flowEditorPage.trigger).not.toBeVisible(); + }); + }); + + test.describe('test trigger', () => { + test('show sample output', async ({}) => { + await expect(flowEditorPage.testOuput).not.toBeVisible(); + await flowEditorPage.continueButton.click(); + await expect(flowEditorPage.testOuput).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Scheduler trigger test output.png', + }); + await flowEditorPage.continueButton.click(); + }); + }); +}); + +test.describe('arrange Ntfy action', () => { + test.describe('choose app and event substep', () => { + test('choose application', async ({}) => { + await flowEditorPage.appAutocomplete.click(); + await flowEditorPage.page.getByRole('option', { name: 'Ntfy' }).click(); + }); + + test('choose an event', async ({}) => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await flowEditorPage.page + .getByRole('option', { name: 'Send message' }) + .click(); + }); + + test('continue to next step', async ({}) => { + await flowEditorPage.continueButton.click(); + }); + + 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 ({}) => { + await flowEditorPage.connectionAutocomplete.click(); + await flowEditorPage.page.getByRole('listitem').first().click(); + }); + + test('continue to next step', async ({}) => { + await flowEditorPage.continueButton.click(); + }); + + test('collapses the substep', async ({}) => { + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + test.describe('set up action', () => { + test('fill topic and message body', async ({}) => { + await flowEditorPage.page + .getByTestId('parameters.topic-power-input') + .locator('[contenteditable]') + .fill('Topic'); + await flowEditorPage.page + .getByTestId('parameters.message-power-input') + .locator('[contenteditable]') + .fill('Message body'); + }); + + test('continue to next step', async ({}) => { + await flowEditorPage.continueButton.click(); + }); + + test('collapses the substep', async ({}) => { + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + test.describe('test trigger', () => { + test('show sample output', async ({}) => { + await expect(flowEditorPage.testOuput).not.toBeVisible(); + await flowEditorPage.page + .getByTestId('flow-substep-continue-button') + .first() + .click(); + await expect(flowEditorPage.testOuput).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Ntfy action test output.png', + }); + await flowEditorPage.continueButton.click(); + }); + }); +}); + +test.describe('publish and unpublish', () => { + 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 ({}) => { + await expect(flowEditorPage.infoSnackbar).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Published flow.png', + }); + }); + + 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 ({}) => { + await expect(flowEditorPage.publishFlowButton).toBeVisible(); + await flowEditorPage.publishFlowButton.click(); + await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + }); + + test('unpublish from layout top bar', async ({}) => { + await expect(flowEditorPage.unpublishFlowButton).toBeVisible(); + await flowEditorPage.unpublishFlowButton.click(); + await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Unpublished flow.png', + }); + }); +}); + +test.describe('in layout', () => { + 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/PowerInput/index.tsx b/packages/web/src/components/PowerInput/index.tsx index 9322abfd..4ca5c80e 100644 --- a/packages/web/src/components/PowerInput/index.tsx +++ b/packages/web/src/components/PowerInput/index.tsx @@ -60,11 +60,13 @@ const PowerInput = (props: PowerInputProps) => { const [showVariableSuggestions, setShowVariableSuggestions] = React.useState(false); - const disappearSuggestionsOnShift = (event: React.KeyboardEvent) => { + const disappearSuggestionsOnShift = ( + event: React.KeyboardEvent + ) => { if (event.code === 'Tab') { setShowVariableSuggestions(false); } - } + }; const stepsWithVariables = React.useMemo(() => { return processStepWithExecutions(priorStepsWithExecutions); @@ -112,7 +114,10 @@ const PowerInput = (props: PowerInputProps) => { }} > {/* ref-able single child for ClickAwayListener */} - + { /> {/* ghost placer for the variables popover */} -
+