Workflow E2E tests – batch 2 (#9747)
- Fix the e2e according to the last changes in the workflows - Create a few more tests regarding the workflow visualizer
This commit is contained in:
committed by
GitHub
parent
e1731bb31e
commit
8e0467e2e4
@ -1,8 +1,9 @@
|
|||||||
import { test as base, expect, Page } from '@playwright/test';
|
import { test as base, expect, Locator, Page } from '@playwright/test';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { createWorkflow } from '../requests/create-workflow';
|
import { createWorkflow } from '../requests/create-workflow';
|
||||||
import { deleteWorkflow } from '../requests/delete-workflow';
|
import { deleteWorkflow } from '../requests/delete-workflow';
|
||||||
import { destroyWorkflow } from '../requests/destroy-workflow';
|
import { destroyWorkflow } from '../requests/destroy-workflow';
|
||||||
|
import { WorkflowActionType, WorkflowTriggerType } from '../types/workflows';
|
||||||
|
|
||||||
export class WorkflowVisualizerPage {
|
export class WorkflowVisualizerPage {
|
||||||
#page: Page;
|
#page: Page;
|
||||||
@ -10,9 +11,65 @@ export class WorkflowVisualizerPage {
|
|||||||
workflowId: string;
|
workflowId: string;
|
||||||
workflowName: string;
|
workflowName: string;
|
||||||
|
|
||||||
|
readonly addStepButton: Locator;
|
||||||
|
readonly workflowStatus: Locator;
|
||||||
|
readonly activateWorkflowButton: Locator;
|
||||||
|
readonly deactivateWorkflowButton: Locator;
|
||||||
|
readonly addTriggerButton: Locator;
|
||||||
|
readonly commandMenu: Locator;
|
||||||
|
readonly workflowNameButton: Locator;
|
||||||
|
readonly triggerNode: Locator;
|
||||||
|
readonly background: Locator;
|
||||||
|
|
||||||
|
#actionNames: Record<WorkflowActionType, string> = {
|
||||||
|
'create-record': 'Create Record',
|
||||||
|
'update-record': 'Update Record',
|
||||||
|
'delete-record': 'Delete Record',
|
||||||
|
code: 'Code',
|
||||||
|
'send-email': 'Send Email',
|
||||||
|
};
|
||||||
|
|
||||||
|
#createdActionNames: Record<WorkflowActionType, string> = {
|
||||||
|
'create-record': 'Create Record',
|
||||||
|
'update-record': 'Update Record',
|
||||||
|
'delete-record': 'Delete Record',
|
||||||
|
code: 'Code - Serverless Function',
|
||||||
|
'send-email': 'Send Email',
|
||||||
|
};
|
||||||
|
|
||||||
|
#triggerNames: Record<WorkflowTriggerType, string> = {
|
||||||
|
'record-created': 'Record is Created',
|
||||||
|
'record-updated': 'Record is Updated',
|
||||||
|
'record-deleted': 'Record is Deleted',
|
||||||
|
manual: 'Launch manually',
|
||||||
|
};
|
||||||
|
|
||||||
|
#createdTriggerNames: Record<WorkflowTriggerType, string> = {
|
||||||
|
'record-created': 'Record is Created',
|
||||||
|
'record-updated': 'Record is Updated',
|
||||||
|
'record-deleted': 'Record is Deleted',
|
||||||
|
manual: 'Manual Trigger',
|
||||||
|
};
|
||||||
|
|
||||||
constructor({ page, workflowName }: { page: Page; workflowName: string }) {
|
constructor({ page, workflowName }: { page: Page; workflowName: string }) {
|
||||||
this.#page = page;
|
this.#page = page;
|
||||||
this.workflowName = workflowName;
|
this.workflowName = workflowName;
|
||||||
|
|
||||||
|
this.addStepButton = page.getByLabel('Add a step');
|
||||||
|
this.workflowStatus = page.getByTestId('workflow-visualizer-status');
|
||||||
|
this.activateWorkflowButton = page.getByLabel('Activate Workflow', {
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
this.deactivateWorkflowButton = page.getByLabel('Deactivate Workflow', {
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
this.addTriggerButton = page.getByText('Add a Trigger');
|
||||||
|
this.commandMenu = page.getByTestId('command-menu');
|
||||||
|
this.workflowNameButton = page.getByRole('button', {
|
||||||
|
name: this.workflowName,
|
||||||
|
});
|
||||||
|
this.triggerNode = this.#page.getByTestId('rf__node-trigger');
|
||||||
|
this.background = page.locator('.react-flow__pane');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneWorkflow() {
|
async createOneWorkflow() {
|
||||||
@ -32,14 +89,137 @@ export class WorkflowVisualizerPage {
|
|||||||
this.workflowId = id;
|
this.workflowId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForWorkflowVisualizerLoad() {
|
||||||
|
await expect(this.workflowNameButton).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
async goToWorkflowVisualizerPage() {
|
async goToWorkflowVisualizerPage() {
|
||||||
await this.#page.goto(`/object/workflow/${this.workflowId}`);
|
await Promise.all([
|
||||||
|
this.#page.goto(`/object/workflow/${this.workflowId}`),
|
||||||
|
|
||||||
const workflowName = this.#page.getByRole('button', {
|
this.waitForWorkflowVisualizerLoad(),
|
||||||
name: this.workflowName,
|
]);
|
||||||
});
|
}
|
||||||
|
|
||||||
await expect(workflowName).toBeVisible();
|
async createInitialTrigger(trigger: WorkflowTriggerType) {
|
||||||
|
await this.addTriggerButton.click();
|
||||||
|
|
||||||
|
const triggerName = this.#triggerNames[trigger];
|
||||||
|
const createdTriggerName = this.#createdTriggerNames[trigger];
|
||||||
|
|
||||||
|
const triggerOption = this.#page.getByText(triggerName);
|
||||||
|
await triggerOption.click();
|
||||||
|
|
||||||
|
await expect(this.triggerNode).toHaveClass(/selected/);
|
||||||
|
await expect(this.triggerNode).toContainText(createdTriggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createStep(action: WorkflowActionType) {
|
||||||
|
await this.addStepButton.click();
|
||||||
|
|
||||||
|
const actionName = this.#actionNames[action];
|
||||||
|
const createdActionName = this.#createdActionNames[action];
|
||||||
|
|
||||||
|
const actionToCreateOption = this.commandMenu.getByText(actionName);
|
||||||
|
|
||||||
|
const [createWorkflowStepResponse] = await Promise.all([
|
||||||
|
this.#page.waitForResponse((response) => {
|
||||||
|
if (!response.url().endsWith('/graphql')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = response.request().postDataJSON();
|
||||||
|
|
||||||
|
return requestBody.operationName === 'CreateWorkflowVersionStep';
|
||||||
|
}),
|
||||||
|
|
||||||
|
actionToCreateOption.click(),
|
||||||
|
]);
|
||||||
|
const createWorkflowStepResponseBody =
|
||||||
|
await createWorkflowStepResponse.json();
|
||||||
|
const createdStepId =
|
||||||
|
createWorkflowStepResponseBody.data.createWorkflowVersionStep.id;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
this.#page.getByTestId('command-menu').getByRole('textbox').first(),
|
||||||
|
).toHaveValue(createdActionName);
|
||||||
|
|
||||||
|
const createdActionNode = this.#page
|
||||||
|
.locator('.react-flow__node.selected')
|
||||||
|
.getByText(createdActionName);
|
||||||
|
|
||||||
|
await expect(createdActionNode).toBeVisible();
|
||||||
|
|
||||||
|
const selectedNodes = this.#page.locator('.react-flow__node.selected');
|
||||||
|
|
||||||
|
await expect(selectedNodes).toHaveCount(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
createdStepId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getStepNode(stepId: string) {
|
||||||
|
return this.#page.getByTestId(`rf__node-${stepId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeleteNodeButton(nodeLocator: Locator) {
|
||||||
|
return nodeLocator.getByRole('button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllStepNodes() {
|
||||||
|
return this.#page
|
||||||
|
.getByTestId(/^rf__node-.+$/)
|
||||||
|
.and(this.#page.getByTestId(/^((?!rf__node-trigger).)*$/))
|
||||||
|
.and(
|
||||||
|
this.#page.getByTestId(/^((?!rf__node-branch-\d+__create-step).)*$/),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStep(stepId: string) {
|
||||||
|
const stepNode = this.getStepNode(stepId);
|
||||||
|
|
||||||
|
await stepNode.click();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(stepNode).not.toBeVisible(),
|
||||||
|
this.#page.waitForResponse((response) => {
|
||||||
|
if (!response.url().endsWith('/graphql')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = response.request().postDataJSON();
|
||||||
|
|
||||||
|
return (
|
||||||
|
requestBody.operationName === 'DeleteWorkflowVersionStep' &&
|
||||||
|
requestBody.variables.input.stepId === stepId
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.getDeleteNodeButton(stepNode).click(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTrigger() {
|
||||||
|
await this.triggerNode.click();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(this.triggerNode).toContainText('Add a Trigger'),
|
||||||
|
this.#page.waitForResponse((response) => {
|
||||||
|
if (!response.url().endsWith('/graphql')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = response.request().postDataJSON();
|
||||||
|
|
||||||
|
return (
|
||||||
|
requestBody.operationName === 'UpdateOneWorkflowVersion' &&
|
||||||
|
requestBody.variables.input.trigger === null
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.getDeleteNodeButton(this.triggerNode).click(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
packages/twenty-e2e-testing/lib/types/workflows.ts
Normal file
12
packages/twenty-e2e-testing/lib/types/workflows.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export type WorkflowTriggerType =
|
||||||
|
| 'record-created'
|
||||||
|
| 'record-updated'
|
||||||
|
| 'record-deleted'
|
||||||
|
| 'manual';
|
||||||
|
|
||||||
|
export type WorkflowActionType =
|
||||||
|
| 'create-record'
|
||||||
|
| 'update-record'
|
||||||
|
| 'delete-record'
|
||||||
|
| 'code'
|
||||||
|
| 'send-email';
|
||||||
@ -1,35 +1,186 @@
|
|||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { test } from '../lib/fixtures/blank-workflow';
|
import { test } from '../lib/fixtures/blank-workflow';
|
||||||
|
|
||||||
test('Create simple workflow', async ({ workflowVisualizer, page }) => {
|
test('Create workflow with every possible step', async ({
|
||||||
const addTriggerButton = page.getByText('Add a Trigger');
|
workflowVisualizer,
|
||||||
await addTriggerButton.click();
|
page,
|
||||||
|
}) => {
|
||||||
|
await workflowVisualizer.createInitialTrigger('record-created');
|
||||||
|
|
||||||
const triggerOption = page.getByText('Database Event');
|
await workflowVisualizer.createStep('create-record');
|
||||||
await triggerOption.click();
|
await workflowVisualizer.createStep('update-record');
|
||||||
|
await workflowVisualizer.createStep('delete-record');
|
||||||
|
await workflowVisualizer.createStep('code');
|
||||||
|
await workflowVisualizer.createStep('send-email');
|
||||||
|
|
||||||
await expect(
|
await workflowVisualizer.background.click();
|
||||||
page.getByTestId('command-menu').getByRole('textbox'),
|
|
||||||
).toHaveValue('When a Company is Created');
|
|
||||||
|
|
||||||
const triggerNode = page.getByTestId('rf__node-trigger');
|
const draftWorkflowStatus =
|
||||||
await expect(triggerNode).toHaveClass(/selected/);
|
workflowVisualizer.workflowStatus.getByText('Draft');
|
||||||
await expect(triggerNode).toHaveText(/Company is Created/);
|
|
||||||
|
|
||||||
const addStepButton = page.getByLabel('Add a step');
|
await expect(draftWorkflowStatus).toBeVisible();
|
||||||
await addStepButton.click();
|
|
||||||
|
|
||||||
const createRecordOption = page.getByText('Create Record');
|
await workflowVisualizer.activateWorkflowButton.click();
|
||||||
|
|
||||||
await createRecordOption.click();
|
const activeWorkflowStatus =
|
||||||
|
workflowVisualizer.workflowStatus.getByText('Active');
|
||||||
|
|
||||||
await expect(
|
await expect(draftWorkflowStatus).not.toBeVisible();
|
||||||
page.getByTestId('command-menu').getByRole('textbox').first(),
|
await expect(activeWorkflowStatus).toBeVisible();
|
||||||
).toHaveValue('Create Record');
|
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
|
||||||
|
await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
|
||||||
const createRecordNode = page
|
});
|
||||||
.locator('.react-flow__node.selected')
|
|
||||||
.getByText('Create Record');
|
test('Delete steps from draft version', async ({
|
||||||
await expect(createRecordNode).toBeVisible();
|
workflowVisualizer,
|
||||||
await expect(triggerNode).not.toHaveClass(/selected/);
|
page,
|
||||||
|
}) => {
|
||||||
|
await workflowVisualizer.createInitialTrigger('record-created');
|
||||||
|
|
||||||
|
const { createdStepId: firstStepId } =
|
||||||
|
await workflowVisualizer.createStep('create-record');
|
||||||
|
const { createdStepId: secondStepId } =
|
||||||
|
await workflowVisualizer.createStep('update-record');
|
||||||
|
const { createdStepId: thirdStepId } =
|
||||||
|
await workflowVisualizer.createStep('delete-record');
|
||||||
|
const { createdStepId: fourthStepId } =
|
||||||
|
await workflowVisualizer.createStep('code');
|
||||||
|
const { createdStepId: fifthStepId } =
|
||||||
|
await workflowVisualizer.createStep('send-email');
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Create Record',
|
||||||
|
'Update Record',
|
||||||
|
'Delete Record',
|
||||||
|
'Code - Serverless Function',
|
||||||
|
'Send Email',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
|
||||||
|
|
||||||
|
await workflowVisualizer.deleteStep(firstStepId);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Update Record',
|
||||||
|
'Delete Record',
|
||||||
|
'Code - Serverless Function',
|
||||||
|
'Send Email',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
|
||||||
|
|
||||||
|
await workflowVisualizer.deleteStep(fifthStepId);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Update Record',
|
||||||
|
'Delete Record',
|
||||||
|
'Code - Serverless Function',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
|
||||||
|
|
||||||
|
await workflowVisualizer.deleteStep(secondStepId);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Delete Record',
|
||||||
|
'Code - Serverless Function',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
|
||||||
|
|
||||||
|
await workflowVisualizer.deleteStep(fourthStepId);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Delete Record',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||||
|
|
||||||
|
await workflowVisualizer.deleteStep(thirdStepId);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.reload(),
|
||||||
|
|
||||||
|
expect(workflowVisualizer.triggerNode).toBeVisible(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add a step to an active version', async ({
|
||||||
|
workflowVisualizer,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await workflowVisualizer.createInitialTrigger('record-created');
|
||||||
|
|
||||||
|
await workflowVisualizer.createStep('create-record');
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
|
||||||
|
|
||||||
|
await workflowVisualizer.background.click();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||||
|
|
||||||
|
workflowVisualizer.activateWorkflowButton.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
|
||||||
|
|
||||||
|
const assertEndState = async () => {
|
||||||
|
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
|
||||||
|
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||||
|
'Record is Created',
|
||||||
|
);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Create Record',
|
||||||
|
]);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
await assertEndState();
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await assertEndState();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Replace the trigger of an active version', async ({
|
||||||
|
workflowVisualizer,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await workflowVisualizer.createInitialTrigger('record-created');
|
||||||
|
|
||||||
|
await workflowVisualizer.createStep('create-record');
|
||||||
|
|
||||||
|
await workflowVisualizer.background.click();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||||
|
|
||||||
|
workflowVisualizer.activateWorkflowButton.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
|
||||||
|
|
||||||
|
workflowVisualizer.deleteTrigger(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await workflowVisualizer.createInitialTrigger('record-deleted');
|
||||||
|
|
||||||
|
await workflowVisualizer.background.click();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||||
|
|
||||||
|
workflowVisualizer.activateWorkflowButton.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||||
|
'Record is Deleted',
|
||||||
|
);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||||
|
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||||
|
'Create Record',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -233,7 +233,7 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
{children}
|
{children}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
||||||
<StyledStatusTagContainer>
|
<StyledStatusTagContainer data-testid="workflow-visualizer-status">
|
||||||
<WorkflowVersionStatusTag versionStatus={status} />
|
<WorkflowVersionStatusTag versionStatus={status} />
|
||||||
</StyledStatusTagContainer>
|
</StyledStatusTagContainer>
|
||||||
</StyledResetReactflowStyles>
|
</StyledResetReactflowStyles>
|
||||||
|
|||||||
Reference in New Issue
Block a user