Merge pull request #2382 from automatisch/AUT-1412
test: add import and export flow tests
This commit is contained in:
@@ -12,12 +12,16 @@ export class FlowEditorPage extends AuthenticatedPage {
|
||||
super(page);
|
||||
|
||||
this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete');
|
||||
this.appAutocompleteInput = this.appAutocomplete.locator('input');
|
||||
this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete');
|
||||
this.eventAutocompleteInput = this.eventAutocomplete.locator('input');
|
||||
this.continueButton = this.page.getByTestId('flow-substep-continue-button');
|
||||
this.testAndContinueButton = this.page.getByText('Test & Continue');
|
||||
this.connectionAutocomplete = this.page.getByTestId(
|
||||
'choose-connection-autocomplete'
|
||||
);
|
||||
this.connectionAutocompleteInput =
|
||||
this.connectionAutocomplete.locator('input');
|
||||
this.addNewConnectionItem = this.page.getByText('Add new connection');
|
||||
this.testOutput = this.page.getByTestId('flow-test-substep-output');
|
||||
this.hasNoOutput = this.page.getByTestId('flow-test-substep-no-output');
|
||||
@@ -32,6 +36,9 @@ export class FlowEditorPage extends AuthenticatedPage {
|
||||
.locator('input');
|
||||
|
||||
this.flowStep = this.page.getByTestId('flow-step');
|
||||
this.goBackButton = this.page.getByTestId('editor-go-back-button');
|
||||
this.exportFlowButton = page.getByTestId('export-flow-button');
|
||||
this.stepName = page.getByTestId('step-name');
|
||||
}
|
||||
|
||||
async createWebhookTrigger(workSynchronously) {
|
||||
@@ -76,7 +83,11 @@ export class FlowEditorPage extends AuthenticatedPage {
|
||||
await expect(this.eventAutocomplete).toBeVisible();
|
||||
await this.eventAutocomplete.click();
|
||||
await Promise.all([
|
||||
this.page.waitForResponse(resp => /(apps\/.*\/actions\/.*\/substeps)/.test(resp.url()) && resp.status() === 200),
|
||||
this.page.waitForResponse(
|
||||
(resp) =>
|
||||
/(apps\/.*\/actions\/.*\/substeps)/.test(resp.url()) &&
|
||||
resp.status() === 200
|
||||
),
|
||||
this.page.getByRole('option', { name: eventName }).click(),
|
||||
]);
|
||||
await this.continueButton.click();
|
||||
|
||||
10
packages/e2e-tests/fixtures/flows-page.js
Normal file
10
packages/e2e-tests/fixtures/flows-page.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class FlowsPage extends AuthenticatedPage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.flowRow = this.page.getByTestId('flow-row');
|
||||
this.importFlowButton = page.getByTestId('import-flow-button');
|
||||
}
|
||||
}
|
||||
20
packages/e2e-tests/fixtures/import-flow-dialog.js
Normal file
20
packages/e2e-tests/fixtures/import-flow-dialog.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ImportFlowDialog extends AuthenticatedPage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.fileName = page.getByTestId('file-name');
|
||||
this.fileNameWrapper = page.getByTestId('file-name-wrapper');
|
||||
this.importButton = page.getByTestId('import-flow-dialog-import-button');
|
||||
this.fileInput = page.locator("input[type='file']");
|
||||
this.genericImportError = page.getByTestId(
|
||||
'import-flow-dialog-generic-error-alert'
|
||||
);
|
||||
this.importParsingError = page.getByTestId(
|
||||
'import-flow-dialog-parsing-error-alert'
|
||||
);
|
||||
this.successAlert = page.getByTestId('import-flow-dialog-success-alert');
|
||||
this.successAlertLink = this.successAlert.getByRole('link');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const { AcceptInvitation } = require('./accept-invitation-page');
|
||||
const { adminFixtures } = require('./admin');
|
||||
const { AdminSetupPage } = require('./admin-setup-page');
|
||||
const { AdminCreateUserPage } = require('./admin/create-user-page');
|
||||
const { FlowsPage } = require('./flows-page');
|
||||
|
||||
exports.test = test.extend({
|
||||
page: async ({ page }, use) => {
|
||||
@@ -36,6 +37,9 @@ exports.test = test.extend({
|
||||
flowEditorPage: async ({ page }, use) => {
|
||||
await use(new FlowEditorPage(page));
|
||||
},
|
||||
flowsPage: async ({ page }, use) => {
|
||||
await use(new FlowsPage(page));
|
||||
},
|
||||
userInterfacePage: async ({ page }, use) => {
|
||||
await use(new UserInterfacePage(page));
|
||||
},
|
||||
|
||||
@@ -112,3 +112,28 @@ export const addWebhookFlow = async (request, token) => {
|
||||
|
||||
return flowId;
|
||||
};
|
||||
|
||||
export const createConnection = async (
|
||||
request,
|
||||
token,
|
||||
appName,
|
||||
requestBody
|
||||
) => {
|
||||
const response = await request.post(
|
||||
`${process.env.BACKEND_APP_URL}/api/v1/apps/${appName}/connections`,
|
||||
{ headers: { Authorization: token }, data: requestBody }
|
||||
);
|
||||
await expect(response.status()).toBe(201);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const verifyConnection = async (request, token, connectionId) => {
|
||||
const response = await request.post(
|
||||
`${process.env.BACKEND_APP_URL}/api/v1/connections/${connectionId}/verify`,
|
||||
{ headers: { Authorization: token } }
|
||||
);
|
||||
await expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
const axios = require('axios');
|
||||
const {
|
||||
createFlow,
|
||||
updateFlowName,
|
||||
getFlow,
|
||||
updateFlowStep,
|
||||
testStep,
|
||||
createConnection,
|
||||
verifyConnection,
|
||||
} = require('../../helpers/flow-api-helper');
|
||||
const { getToken } = require('../../helpers/auth-api-helper');
|
||||
const { ImportFlowDialog } = require('../../fixtures/import-flow-dialog');
|
||||
|
||||
test.describe('Import/Export flow', () => {
|
||||
test('export flow from the details with variables, step names', async ({
|
||||
page,
|
||||
request,
|
||||
flowEditorPage,
|
||||
flowsPage,
|
||||
}) => {
|
||||
let flowId;
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await test.step('create flow', async () => {
|
||||
const tokenJsonResponse = await getToken(request);
|
||||
const token = tokenJsonResponse.data.token;
|
||||
let flow = await createFlow(request, token);
|
||||
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',
|
||||
name: 'triggerStep',
|
||||
parameters: {
|
||||
workSynchronously: true,
|
||||
},
|
||||
});
|
||||
await request.get(triggerStep.data.webhookUrl);
|
||||
await testStep(request, token, triggerStepId);
|
||||
|
||||
await updateFlowStep(request, token, actionStepId, {
|
||||
appKey: 'webhook',
|
||||
key: 'respondWith',
|
||||
name: 'actionStep',
|
||||
parameters: {
|
||||
statusCode: '200',
|
||||
body: `{{step.${triggerStepId}.headers.host}}`,
|
||||
headers: [
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await testStep(request, token, actionStepId);
|
||||
});
|
||||
|
||||
await test.step('open added flow', async () => {
|
||||
await page.goto(`/editor/${flowId}`);
|
||||
});
|
||||
|
||||
await test.step('export and import flow', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await flowEditorPage.exportFlowButton.click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
await download.saveAs(download.suggestedFilename());
|
||||
|
||||
await flowEditorPage.goBackButton.click();
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles(
|
||||
download.suggestedFilename()
|
||||
);
|
||||
await expect(importFlowDialog.fileNameWrapper).toHaveText(
|
||||
`Selected file:${flowId}.json`
|
||||
);
|
||||
await importFlowDialog.importButton.click();
|
||||
|
||||
await expect(importFlowDialog.successAlert).toHaveText(
|
||||
'The flow has been successfully imported. You can view it here.'
|
||||
);
|
||||
await importFlowDialog.successAlertLink.click();
|
||||
});
|
||||
|
||||
await test.step('verify imported flow', async () => {
|
||||
await expect(flowEditorPage.stepName.first()).toHaveText(
|
||||
'1. triggerStep'
|
||||
);
|
||||
await flowEditorPage.continueButton.click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('parameters.workSynchronously-autocomplete')
|
||||
.first()
|
||||
.getByRole('combobox')
|
||||
).toHaveValue('Yes');
|
||||
|
||||
await flowEditorPage.continueButton.last().click();
|
||||
|
||||
const webhookUrl = await page.locator('input[name="webhookUrl"]');
|
||||
await axios.get(await webhookUrl.inputValue());
|
||||
|
||||
await flowEditorPage.testAndContinueButton.click();
|
||||
|
||||
await expect(flowEditorPage.stepName.nth(1)).toHaveText('2. actionStep');
|
||||
await flowEditorPage.continueButton.last().click();
|
||||
await expect(flowEditorPage.appAutocompleteInput).toHaveValue('Webhook');
|
||||
|
||||
await flowEditorPage.continueButton.last().click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('parameters.body-power-input')
|
||||
.locator('[contenteditable="true"]')
|
||||
).toContainText('step1.headers.host: localhost:3000');
|
||||
});
|
||||
});
|
||||
|
||||
test('export flow from the Flow List', async ({
|
||||
page,
|
||||
request,
|
||||
flowsPage,
|
||||
}) => {
|
||||
const tokenJsonResponse = await getToken(request);
|
||||
const token = tokenJsonResponse.data.token;
|
||||
let flow = await createFlow(request, token);
|
||||
const flowId = flow.data.id;
|
||||
await updateFlowName(request, token, flowId);
|
||||
await page.goto('/flows');
|
||||
|
||||
await expect(page).toHaveURL('/flows');
|
||||
await expect(
|
||||
flowsPage.flowRow.filter({
|
||||
hasText: flowId,
|
||||
})
|
||||
).toHaveCount(1);
|
||||
await flowsPage.flowRow
|
||||
.filter({
|
||||
hasText: flowId,
|
||||
})
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('menuitem', { name: 'Export' }).click();
|
||||
const download = await downloadPromise;
|
||||
await download.saveAs(download.suggestedFilename());
|
||||
});
|
||||
|
||||
test('import flow and check if flow is not in status to publish', async ({
|
||||
page,
|
||||
flowEditorPage,
|
||||
flowsPage,
|
||||
}) => {
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles({
|
||||
name: 'flow.json',
|
||||
mimeType: 'application/json',
|
||||
buffer: Buffer.from(`
|
||||
{
|
||||
"id": "b85d6ceb-4220-4afa-965a-c43fa01e96e0",
|
||||
"name": "Name your flow",
|
||||
"steps": [
|
||||
{
|
||||
"id": "3987bcb5-b33c-4f1c-964a-eff5a54d1fd4",
|
||||
"key": "catchRawWebhook",
|
||||
"name": "Catch raw webhook",
|
||||
"appKey": "webhook",
|
||||
"type": "trigger",
|
||||
"parameters": {
|
||||
"workSynchronously": false
|
||||
},
|
||||
"position": 1,
|
||||
"webhookPath": "/webhooks/flows/b85d6ceb-4220-4afa-965a-c43fa01e96e0"
|
||||
},
|
||||
{
|
||||
"id": "49ba3181-d7b0-462b-b62f-005d50689f87",
|
||||
"key": "respondWith",
|
||||
"name": "Respond with",
|
||||
"appKey": "webhook",
|
||||
"type": "action",
|
||||
"parameters": {
|
||||
"body": "{{step.3987bcb5-b33c-4f1c-964a-eff5a54d1fd4.headers.host}}",
|
||||
"headers": [
|
||||
{
|
||||
"key": "abc",
|
||||
"__id": "21a0bfa9-7928-4e42-9e56-f4d68b8ff7aa",
|
||||
"value": "{{step.3da3f847-efda-42b7-adbe-f0203a3491b8.headers.host}}"
|
||||
}
|
||||
],
|
||||
"statusCode": "200"
|
||||
},
|
||||
"position": 2
|
||||
}
|
||||
]
|
||||
}`),
|
||||
});
|
||||
await expect(importFlowDialog.fileNameWrapper).toHaveText(
|
||||
'Selected file:flow.json'
|
||||
);
|
||||
await importFlowDialog.importButton.click();
|
||||
|
||||
await expect(importFlowDialog.successAlert).toHaveText(
|
||||
'The flow has been successfully imported. You can view it here.'
|
||||
);
|
||||
await importFlowDialog.successAlertLink.click();
|
||||
|
||||
await flowEditorPage.publishFlowButton.click();
|
||||
const snackbar = await page.getByTestId('snackbar-error');
|
||||
await expect(snackbar).toHaveText(
|
||||
'All steps should be completed before updating flow status!'
|
||||
);
|
||||
});
|
||||
|
||||
test('connection should not be exported', async ({
|
||||
page,
|
||||
request,
|
||||
flowEditorPage,
|
||||
flowsPage,
|
||||
}) => {
|
||||
let flowId;
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await test.step('create flow', async () => {
|
||||
const tokenJsonResponse = await getToken(request);
|
||||
const token = tokenJsonResponse.data.token;
|
||||
let flow = await createFlow(request, token);
|
||||
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',
|
||||
name: 'triggerStep',
|
||||
parameters: {
|
||||
workSynchronously: true,
|
||||
},
|
||||
});
|
||||
await request.get(triggerStep.data.webhookUrl);
|
||||
await testStep(request, token, triggerStepId);
|
||||
|
||||
const connection = await createConnection(request, token, 'postgresql', {
|
||||
formattedData: {
|
||||
version: '14.5',
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: '5432',
|
||||
enableSsl: 'false',
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
await verifyConnection(request, token, connection.data.id);
|
||||
await updateFlowStep(request, token, actionStepId, {
|
||||
appKey: 'postgresql',
|
||||
key: 'SQLQuery',
|
||||
name: 'SQLQuery',
|
||||
connectionId: connection.data.id,
|
||||
parameters: {
|
||||
queryStatement: 'select * from users;',
|
||||
params: [
|
||||
{
|
||||
parameter: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await testStep(request, token, actionStepId);
|
||||
});
|
||||
|
||||
await test.step('open added flow', async () => {
|
||||
await page.goto(`/editor/${flowId}`);
|
||||
});
|
||||
|
||||
await test.step('export and import flow', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await flowEditorPage.exportFlowButton.click();
|
||||
const download = await downloadPromise;
|
||||
await download.saveAs(download.suggestedFilename());
|
||||
|
||||
await expect(page.getByTestId('snackbar')).toHaveText(
|
||||
'The flow export has been successfully generated.'
|
||||
);
|
||||
await page.getByTestId('snackbar').click();
|
||||
|
||||
await flowEditorPage.goBackButton.click();
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles(
|
||||
download.suggestedFilename()
|
||||
);
|
||||
|
||||
await expect(importFlowDialog.fileNameWrapper).toHaveText(
|
||||
`Selected file:${flowId}.json`
|
||||
);
|
||||
await importFlowDialog.importButton.click();
|
||||
await expect(importFlowDialog.successAlert).toHaveText(
|
||||
'The flow has been successfully imported. You can view it here.'
|
||||
);
|
||||
await importFlowDialog.successAlertLink.click();
|
||||
});
|
||||
|
||||
await test.step('verify imported flow', async () => {
|
||||
await flowEditorPage.flowStep.last().click();
|
||||
await expect(flowEditorPage.appAutocompleteInput).toHaveCount(1);
|
||||
await expect(flowEditorPage.appAutocompleteInput).toHaveValue(
|
||||
'PostgreSQL'
|
||||
);
|
||||
await expect(flowEditorPage.eventAutocompleteInput).toHaveValue(
|
||||
'SQL query'
|
||||
);
|
||||
await flowEditorPage.continueButton.click();
|
||||
await expect(flowEditorPage.connectionAutocompleteInput).toHaveValue('');
|
||||
await page.getByText('Set up action').click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('parameters.queryStatement-power-input')
|
||||
.locator('[contenteditable]')
|
||||
).toHaveText('select * from users;');
|
||||
});
|
||||
});
|
||||
|
||||
// ellipsis if not verified properly
|
||||
test.skip('handle long file names', async ({ flowsPage, page }) => {
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles({
|
||||
name: 'very_long_file_name_with_some_additional_remarks_that_should_not_be_visible.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('this is test'),
|
||||
});
|
||||
|
||||
await expect(importFlowDialog.fileName).toHaveText(
|
||||
'very_long_file_name_with_some_additional_remark...'
|
||||
);
|
||||
});
|
||||
|
||||
test('should fail on import different file than json', async ({
|
||||
flowsPage,
|
||||
page,
|
||||
}) => {
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles({
|
||||
name: 'abc.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('this is test'),
|
||||
});
|
||||
|
||||
await expect(importFlowDialog.fileNameWrapper).toHaveText(
|
||||
'Selected file:abc.txt'
|
||||
);
|
||||
await importFlowDialog.importButton.click();
|
||||
await expect(importFlowDialog.importParsingError).toHaveText(
|
||||
'Something has gone wrong with parsing the selected file.'
|
||||
);
|
||||
});
|
||||
|
||||
test('import invalid nonflow json', async ({ flowsPage, page }) => {
|
||||
const importFlowDialog = new ImportFlowDialog(page);
|
||||
|
||||
await flowsPage.importFlowButton.click();
|
||||
await importFlowDialog.fileInput.setInputFiles({
|
||||
name: 'abcd.json',
|
||||
mimeType: 'application/json',
|
||||
buffer: Buffer.from('{"this": "is test"}'),
|
||||
});
|
||||
|
||||
await expect(importFlowDialog.fileNameWrapper).toHaveText(
|
||||
'Selected file:abcd.json'
|
||||
);
|
||||
await importFlowDialog.importButton.click();
|
||||
await expect(importFlowDialog.genericImportError).toHaveText(
|
||||
'Something went wrong. Please try again.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -265,6 +265,7 @@ function FlowStep(props) {
|
||||
</Typography>
|
||||
|
||||
<EditableTypography
|
||||
data-test="step-name"
|
||||
variant="body2"
|
||||
onConfirm={handleStepNameChange}
|
||||
prefixValue={`${step.position}. `}
|
||||
|
||||
@@ -93,12 +93,14 @@ function ImportFlowDialog(props) {
|
||||
</FileUploadInput>
|
||||
|
||||
{selectedFile && (
|
||||
<Box overflow="hidden">
|
||||
<Box data-test="file-name-wrapper" overflow="hidden">
|
||||
<Typography>
|
||||
{formatMessage('importFlowDialog.selectedFileInformation')}
|
||||
</Typography>
|
||||
<Tooltip title={selectedFile.name}>
|
||||
<Typography noWrap>{selectedFile.name}</Typography>
|
||||
<Typography data-test="file-name" noWrap>
|
||||
{selectedFile.name}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user