diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index de9d4e5e..b7e979c5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -114,6 +114,7 @@ jobs: - name: Run Playwright tests working-directory: ./packages/e2e-tests env: + PORT: 3000 LOGIN_EMAIL: user@automatisch.io LOGIN_PASSWORD: sample BASE_URL: http://localhost:3000 diff --git a/packages/e2e-tests/fixtures/execution-details-page.js b/packages/e2e-tests/fixtures/execution-details-page.js new file mode 100644 index 00000000..2a361a9d --- /dev/null +++ b/packages/e2e-tests/fixtures/execution-details-page.js @@ -0,0 +1,21 @@ +const { AuthenticatedPage } = require('./authenticated-page'); +const { expect } = require('@playwright/test'); + +export class ExecutionDetailsPage extends AuthenticatedPage { + constructor(page) { + super(page); + + this.executionCreatedAt = page.getByTestId('execution-created-at'); + this.executionId = page.getByTestId('execution-id'); + this.executionName = page.getByTestId('execution-name'); + this.executionStep = page.getByTestId('execution-step'); + } + + async verifyExecutionData(flowId) { + await expect(this.executionCreatedAt).toContainText(/\d+ seconds? ago/); + await expect(this.executionId).toHaveText( + /Execution ID: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ); + await expect(this.executionName).toHaveText(flowId); + } +} diff --git a/packages/e2e-tests/fixtures/execution-step-details.js b/packages/e2e-tests/fixtures/execution-step-details.js new file mode 100644 index 00000000..d21e1c1e --- /dev/null +++ b/packages/e2e-tests/fixtures/execution-step-details.js @@ -0,0 +1,93 @@ +const { ExecutionDetailsPage } = require('./execution-details-page'); +const { expect } = require('@playwright/test'); + +export class ExecutionStepDetails extends ExecutionDetailsPage { + constructor(page, executionStep) { + super(page); + + this.executionStep = executionStep; + this.stepType = executionStep.getByTestId('step-type'); + this.stepPositionAndName = executionStep.getByTestId( + 'step-position-and-name' + ); + this.executionStepId = executionStep.getByTestId('execution-step-id'); + this.executionStepExecutedAt = executionStep.getByTestId( + 'execution-step-executed-at' + ); + this.dataInTab = executionStep.getByTestId('data-in-tab'); + this.dataInPanel = executionStep.getByTestId('data-in-panel'); + this.dataOutTab = executionStep.getByTestId('data-out-tab'); + this.dataOutPanel = executionStep.getByTestId('data-out-panel'); + } + + async expectDataInTabToBeSelectedByDefault() { + await expect(this.dataInTab).toHaveClass(/Mui-selected/); + } + + async expectDataInToContainText(searchText, desiredText) { + await expect(this.dataInPanel).toContainText(desiredText); + await this.dataInPanel.locator('#search-input').fill(searchText); + await expect(this.dataInPanel).toContainText(desiredText); + } + + async expectDataOutToContainText(searchText, desiredText) { + await expect(this.dataOutPanel).toContainText(desiredText); + await this.dataOutPanel.locator('#search-input').fill(searchText); + await expect(this.dataOutPanel).toContainText(desiredText); + } + + async verifyTriggerExecutionStep({ + stepPositionAndName, + stepDataInKey, + stepDataInValue, + stepDataOutKey, + stepDataOutValue, + }) { + await expect(this.stepType).toHaveText('Trigger'); + await this.verifyExecutionStep({ + stepPositionAndName, + stepDataInKey, + stepDataInValue, + stepDataOutKey, + stepDataOutValue, + }); + } + + async verifyActionExecutionStep({ + stepPositionAndName, + stepDataInKey, + stepDataInValue, + stepDataOutKey, + stepDataOutValue, + }) { + await expect(this.stepType).toHaveText('Action'); + await this.verifyExecutionStep({ + stepPositionAndName, + stepDataInKey, + stepDataInValue, + stepDataOutKey, + stepDataOutValue, + }); + } + + async verifyExecutionStep({ + stepPositionAndName, + stepDataInKey, + stepDataInValue, + stepDataOutKey, + stepDataOutValue, + }) { + await expect(this.stepPositionAndName).toHaveText(stepPositionAndName); + await expect(this.executionStepId).toHaveText( + /ID: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ); + await expect(this.executionStepExecutedAt).toContainText( + /executed \d+ seconds? ago/ + ); + await this.expectDataInTabToBeSelectedByDefault(); + await this.expectDataInToContainText(stepDataInKey, stepDataInValue); + await this.dataOutTab.click(); + await expect(this.dataOutPanel).toContainText(stepDataOutValue); + await this.expectDataOutToContainText(stepDataOutKey, stepDataOutValue); + } +} diff --git a/packages/e2e-tests/fixtures/executions-page.js b/packages/e2e-tests/fixtures/executions-page.js index 41b673e3..4888e89d 100644 --- a/packages/e2e-tests/fixtures/executions-page.js +++ b/packages/e2e-tests/fixtures/executions-page.js @@ -2,4 +2,11 @@ const { AuthenticatedPage } = require('./authenticated-page'); export class ExecutionsPage extends AuthenticatedPage { screenshotPath = '/executions'; + + constructor(page) { + super(page); + + this.executionRow = this.page.getByTestId('execution-row'); + this.executionsPageLoader = this.page.getByTestId('executions-loader'); + } } diff --git a/packages/e2e-tests/fixtures/index.js b/packages/e2e-tests/fixtures/index.js index 069c2a7e..6a7b3557 100644 --- a/packages/e2e-tests/fixtures/index.js +++ b/packages/e2e-tests/fixtures/index.js @@ -2,6 +2,7 @@ const { test, expect } = require('@playwright/test'); const { ApplicationsPage } = require('./applications-page'); const { ConnectionsPage } = require('./connections-page'); const { ExecutionsPage } = require('./executions-page'); +const { ExecutionDetailsPage } = require('./execution-details-page'); const { FlowEditorPage } = require('./flow-editor-page'); const { UserInterfacePage } = require('./user-interface-page'); const { LoginPage } = require('./login-page'); @@ -29,6 +30,9 @@ exports.test = test.extend({ executionsPage: async ({ page }, use) => { await use(new ExecutionsPage(page)); }, + executionDetailsPage: async ({ page }, use) => { + await use(new ExecutionDetailsPage(page)); + }, flowEditorPage: async ({ page }, use) => { await use(new FlowEditorPage(page)); }, diff --git a/packages/e2e-tests/helpers/auth-api-helper.js b/packages/e2e-tests/helpers/auth-api-helper.js new file mode 100644 index 00000000..214f7d0c --- /dev/null +++ b/packages/e2e-tests/helpers/auth-api-helper.js @@ -0,0 +1,15 @@ +const { expect } = require('../fixtures/index'); + +export const getToken = async (apiRequest) => { + const tokenResponse = await apiRequest.post( + `http://localhost:${process.env.PORT}/api/v1/access-tokens`, + { + data: { + email: process.env.LOGIN_EMAIL, + password: process.env.LOGIN_PASSWORD, + }, + } + ); + await expect(tokenResponse.status()).toBe(200); + return await tokenResponse.json(); +}; diff --git a/packages/e2e-tests/helpers/flow-api-helper.js b/packages/e2e-tests/helpers/flow-api-helper.js new file mode 100644 index 00000000..24d6fd5e --- /dev/null +++ b/packages/e2e-tests/helpers/flow-api-helper.js @@ -0,0 +1,108 @@ +const { expect } = require('../fixtures/index'); + +export const createFlow = async (request, token) => { + const response = await request.post( + `http://localhost:${process.env.PORT}/api/v1/flows`, + { headers: { Authorization: token } } + ); + await expect(response.status()).toBe(201); + return await response.json(); +}; + +export const getFlow = async (request, token, flowId) => { + const response = await request.get( + `http://localhost:${process.env.PORT}/api/v1/flows/${flowId}`, + { headers: { Authorization: token } } + ); + await expect(response.status()).toBe(200); + return await response.json(); +}; + +export const updateFlowName = async (request, token, flowId) => { + const updateFlowNameResponse = await request.patch( + `http://localhost:${process.env.PORT}/api/v1/flows/${flowId}`, + { + headers: { Authorization: token }, + data: { name: flowId }, + } + ); + await expect(updateFlowNameResponse.status()).toBe(200); +}; + +export const updateFlowStep = async (request, token, stepId, requestBody) => { + const updateTriggerStepResponse = await request.patch( + `http://localhost:${process.env.PORT}/api/v1/steps/${stepId}`, + { + headers: { Authorization: token }, + data: requestBody, + } + ); + await expect(updateTriggerStepResponse.status()).toBe(200); + return await updateTriggerStepResponse.json(); +}; + +export const testStep = async (request, token, stepId) => { + const testTriggerStepResponse = await request.post( + `http://localhost:${process.env.PORT}/api/v1/steps/${stepId}/test`, + { + headers: { Authorization: token }, + } + ); + await expect(testTriggerStepResponse.status()).toBe(200); +}; + +export const publishFlow = async (request, token, flowId) => { + const publishFlowResponse = await request.patch( + `http://localhost:${process.env.PORT}/api/v1/flows/${flowId}/status`, + { + headers: { Authorization: token }, + data: { active: true }, + } + ); + await expect(publishFlowResponse.status()).toBe(200); + return publishFlowResponse.json(); +}; + +export const triggerFlow = async (request, url) => { + const triggerFlowResponse = await request.get(url); + await expect(triggerFlowResponse.status()).toBe(204); +}; + +export const addWebhookFlow = async (request, token) => { + let flow = await createFlow(request, token); + const flowId = flow.data.id; + await updateFlowName(request, token, flowId); + flow = await getFlow(request, token, flowId); + const flowSteps = flow.data.steps; + + const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id; + const actionStepId = flowSteps.find((step) => step.type === 'action').id; + + const triggerStep = await updateFlowStep(request, token, triggerStepId, { + appKey: 'webhook', + key: 'catchRawWebhook', + parameters: { + workSynchronously: false, + }, + }); + await request.get(triggerStep.data.webhookUrl); + await testStep(request, token, triggerStepId); + + await updateFlowStep(request, token, actionStepId, { + appKey: 'webhook', + key: 'respondWith', + parameters: { + statusCode: '200', + body: 'ok', + headers: [ + { + key: '', + value: '', + }, + ], + }, + }); + await testStep(request, token, actionStepId); + + return flowId; +}; diff --git a/packages/e2e-tests/tests/admin-setup/admin.setup.js b/packages/e2e-tests/tests/admin-setup/admin.setup.js index 43bcc7b1..20393013 100644 --- a/packages/e2e-tests/tests/admin-setup/admin.setup.js +++ b/packages/e2e-tests/tests/admin-setup/admin.setup.js @@ -2,11 +2,14 @@ const { publicTest: setup, expect } = require('../../fixtures/index'); setup.describe.serial('Admin setup page', () => { // eslint-disable-next-line no-unused-vars - setup('should not be able to login if admin is not created', async ({ page, adminSetupPage, loginPage }) => { - await expect(async () => { - await expect(await page.url()).toContain(adminSetupPage.path); - }).toPass(); - }); + setup( + 'should not be able to login if admin is not created', + async ({ page, adminSetupPage }) => { + await expect(async () => { + await expect(await page.url()).toContain(adminSetupPage.path); + }).toPass(); + } + ); setup('should validate the inputs', async ({ adminSetupPage }) => { await adminSetupPage.open(); diff --git a/packages/e2e-tests/tests/executions/display-execution.spec.js b/packages/e2e-tests/tests/executions/display-execution.spec.js index 8de79dd7..bc260747 100644 --- a/packages/e2e-tests/tests/executions/display-execution.spec.js +++ b/packages/e2e-tests/tests/executions/display-execution.spec.js @@ -1,37 +1,138 @@ +const { request } = require('@playwright/test'); const { test, expect } = require('../../fixtures/index'); +const { + triggerFlow, + publishFlow, + addWebhookFlow, +} = require('../../helpers/flow-api-helper'); +const { + ExecutionStepDetails, +} = require('../../fixtures/execution-step-details'); +const { getToken } = require('../../helpers/auth-api-helper'); + +test.describe('Executions page', () => { + let flowId; + + test.beforeAll(async () => { + const apiRequest = await request.newContext(); + const tokenJsonResponse = await getToken(apiRequest); + + flowId = await addWebhookFlow(apiRequest, tokenJsonResponse.data.token); + + const { data } = await publishFlow( + apiRequest, + tokenJsonResponse.data.token, + flowId + ); + + const triggerStepWebhookUrl = data.steps.find( + (step) => step.type === 'trigger' + ).webhookUrl; + + await triggerFlow(apiRequest, triggerStepWebhookUrl); + }); -// no execution data exists in an empty account -test.describe.skip('Executions page', () => { test.beforeEach(async ({ page }) => { await page.getByTestId('executions-page-drawer-link').click(); - await page.getByTestId('execution-row').first().click(); - - await expect(page).toHaveURL(/\/executions\//); }); - test('displays data in by default', async ({ page, executionsPage }) => { - await expect(page.getByTestId('execution-step').last()).toBeVisible(); - await expect(page.getByTestId('execution-step')).toHaveCount(2); + test('show correct step data for trigger and actions on test and non-test execution', async ({ + page, + executionsPage, + executionDetailsPage, + }) => { + await executionsPage.executionsPageLoader.waitFor({ + state: 'detached', + }); + const flowExecutions = await executionsPage.executionRow.filter({ + hasText: flowId, + }); + await test.step('show only trigger step on test execution', async () => { + await expect(flowExecutions.last()).toContainText('Test run'); + await flowExecutions.last().click(); - await executionsPage.screenshot({ - path: 'Execution - data in.png', + await executionDetailsPage.verifyExecutionData(flowId); + await expect(executionDetailsPage.executionStep).toHaveCount(1); + + const executionStepDetails = new ExecutionStepDetails( + page, + executionDetailsPage.executionStep.last() + ); + await executionStepDetails.verifyTriggerExecutionStep({ + stepPositionAndName: '1. Webhook', + stepDataInKey: 'workSynchronously', + stepDataInValue: 'workSynchronously', + stepDataOutKey: 'host', + stepDataOutValue: 'localhost', + }); + + await page.goBack(); + }); + + await test.step('show trigger and action step on action test execution', async () => { + await expect(flowExecutions.nth(1)).toContainText('Test run'); + await flowExecutions.nth(1).click(); + + await expect(executionDetailsPage.executionStep).toHaveCount(2); + await executionDetailsPage.verifyExecutionData(flowId); + + const firstExecutionStepDetails = new ExecutionStepDetails( + page, + executionDetailsPage.executionStep.first() + ); + await firstExecutionStepDetails.verifyTriggerExecutionStep({ + stepPositionAndName: '1. Webhook', + stepDataInKey: 'workSynchronously', + stepDataInValue: 'workSynchronously', + stepDataOutKey: 'host', + stepDataOutValue: 'localhost', + }); + + const lastExecutionStepDetails = new ExecutionStepDetails( + page, + executionDetailsPage.executionStep.last() + ); + await lastExecutionStepDetails.verifyActionExecutionStep({ + stepPositionAndName: '2. Webhook', + stepDataInKey: 'body', + stepDataInValue: 'body:"ok"', + stepDataOutKey: 'body', + stepDataOutValue: 'body:"ok"', + }); + + await page.goBack(); + }); + + await test.step('show trigger and action step on flow execution', async () => { + await expect(flowExecutions.first()).not.toContainText('Test run'); + await flowExecutions.first().click(); + + await expect(executionDetailsPage.executionStep).toHaveCount(2); + await executionDetailsPage.verifyExecutionData(flowId); + + const firstExecutionStepDetails = new ExecutionStepDetails( + page, + executionDetailsPage.executionStep.first() + ); + await firstExecutionStepDetails.verifyTriggerExecutionStep({ + stepPositionAndName: '1. Webhook', + stepDataInKey: 'workSynchronously', + stepDataInValue: 'workSynchronously', + stepDataOutKey: 'host', + stepDataOutValue: 'localhost', + }); + + const lastExecutionStepDetails = new ExecutionStepDetails( + page, + executionDetailsPage.executionStep.last() + ); + await lastExecutionStepDetails.verifyActionExecutionStep({ + stepPositionAndName: '2. Webhook', + stepDataInKey: 'body', + stepDataInValue: 'body:"ok"', + stepDataOutKey: 'body', + stepDataOutValue: 'body:"ok"', + }); }); }); - - test('displays data out', async ({ page, executionsPage }) => { - const executionStepCount = await page.getByTestId('execution-step').count(); - for (let i = 0; i < executionStepCount; i++) { - await page.getByTestId('data-out-tab').nth(i).click(); - await expect(page.getByTestId('data-out-panel').nth(i)).toBeVisible(); - - await executionsPage.screenshot({ - path: `Execution - data out - ${i}.png`, - animations: 'disabled', - }); - } - }); - - test('does not display error', async ({ page }) => { - await expect(page.getByTestId('error-tab')).toBeHidden(); - }); }); diff --git a/packages/e2e-tests/tests/executions/list-executions.spec.js b/packages/e2e-tests/tests/executions/list-executions.spec.js index 48527229..f0182dc0 100644 --- a/packages/e2e-tests/tests/executions/list-executions.spec.js +++ b/packages/e2e-tests/tests/executions/list-executions.spec.js @@ -1,17 +1,54 @@ +const { request } = require('@playwright/test'); const { test, expect } = require('../../fixtures/index'); +const { + triggerFlow, + publishFlow, + addWebhookFlow, +} = require('../../helpers/flow-api-helper'); +const { getToken } = require('../../helpers/auth-api-helper'); test.describe('Executions page', () => { + let flowId; + + test.beforeAll(async () => { + const apiRequest = await request.newContext(); + const tokenJsonResponse = await getToken(apiRequest); + + flowId = await addWebhookFlow(apiRequest, tokenJsonResponse.data.token); + + const { data } = await publishFlow( + apiRequest, + tokenJsonResponse.data.token, + flowId + ); + + const triggerStepWebhookUrl = data.steps.find( + (step) => step.type === 'trigger' + ).webhookUrl; + + await triggerFlow(apiRequest, triggerStepWebhookUrl); + }); + test.beforeEach(async ({ page }) => { await page.getByTestId('executions-page-drawer-link').click(); }); - // no executions exist in an empty account - test.skip('displays executions', async ({ page, executionsPage }) => { - await page.getByTestId('executions-loader').waitFor({ + test('should be able to see normal and test executions', async ({ + executionsPage, + }) => { + await executionsPage.executionsPageLoader.waitFor({ state: 'detached', }); - await expect(page.getByTestId('execution-row').first()).toBeVisible(); + const flowExecutions = await executionsPage.executionRow.filter({ + hasText: flowId, + }); - await executionsPage.screenshot({ path: 'Executions.png' }); + await expect(flowExecutions).toHaveCount(4); + await expect(flowExecutions.first()).toContainText('Success'); + await expect(flowExecutions.first()).not.toContainText('Test run'); + for (let testFlow = 1; testFlow < 4; testFlow++) { + await expect(flowExecutions.nth(testFlow)).toContainText('Test run'); + await expect(flowExecutions.nth(testFlow)).toContainText('Success'); + } }); }); diff --git a/packages/web/src/components/ExecutionHeader/index.jsx b/packages/web/src/components/ExecutionHeader/index.jsx index 8cc42b15..6962181f 100644 --- a/packages/web/src/components/ExecutionHeader/index.jsx +++ b/packages/web/src/components/ExecutionHeader/index.jsx @@ -10,7 +10,7 @@ import { ExecutionPropType } from 'propTypes/propTypes'; function ExecutionName(props) { return ( - + {props.name} ); @@ -29,7 +29,7 @@ function ExecutionId(props) { ); return ( - + {formatMessage('execution.id', { id })} @@ -47,7 +47,7 @@ function ExecutionDate(props) { - + {relativeCreatedAt} diff --git a/packages/web/src/components/ExecutionStep/index.jsx b/packages/web/src/components/ExecutionStep/index.jsx index 91f61a77..5aa3e40c 100644 --- a/packages/web/src/components/ExecutionStep/index.jsx +++ b/packages/web/src/components/ExecutionStep/index.jsx @@ -36,7 +36,11 @@ function ExecutionStepId(props) { return ( - + {formatMessage('executionStep.id', { id })} @@ -56,7 +60,11 @@ function ExecutionStepDate(props) { - + {formatMessage('executionStep.executedAt', { datetime: relativeCreatedAt, })} @@ -119,12 +127,12 @@ function ExecutionStep(props) { - + {isTrigger && formatMessage('flowStep.triggerType')} {isAction && formatMessage('flowStep.actionType')} - + {step.position}. {app?.name} @@ -152,7 +160,7 @@ function ExecutionStep(props) { - +