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 { createWorkflow } from '../requests/create-workflow';
|
||||
import { deleteWorkflow } from '../requests/delete-workflow';
|
||||
import { destroyWorkflow } from '../requests/destroy-workflow';
|
||||
import { WorkflowActionType, WorkflowTriggerType } from '../types/workflows';
|
||||
|
||||
export class WorkflowVisualizerPage {
|
||||
#page: Page;
|
||||
@ -10,9 +11,65 @@ export class WorkflowVisualizerPage {
|
||||
workflowId: 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 }) {
|
||||
this.#page = page;
|
||||
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() {
|
||||
@ -32,14 +89,137 @@ export class WorkflowVisualizerPage {
|
||||
this.workflowId = id;
|
||||
}
|
||||
|
||||
async waitForWorkflowVisualizerLoad() {
|
||||
await expect(this.workflowNameButton).toBeVisible();
|
||||
}
|
||||
|
||||
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', {
|
||||
name: this.workflowName,
|
||||
});
|
||||
this.waitForWorkflowVisualizerLoad(),
|
||||
]);
|
||||
}
|
||||
|
||||
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 { test } from '../lib/fixtures/blank-workflow';
|
||||
|
||||
test('Create simple workflow', async ({ workflowVisualizer, page }) => {
|
||||
const addTriggerButton = page.getByText('Add a Trigger');
|
||||
await addTriggerButton.click();
|
||||
test('Create workflow with every possible step', async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
const triggerOption = page.getByText('Database Event');
|
||||
await triggerOption.click();
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
await workflowVisualizer.createStep('update-record');
|
||||
await workflowVisualizer.createStep('delete-record');
|
||||
await workflowVisualizer.createStep('code');
|
||||
await workflowVisualizer.createStep('send-email');
|
||||
|
||||
await expect(
|
||||
page.getByTestId('command-menu').getByRole('textbox'),
|
||||
).toHaveValue('When a Company is Created');
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
const triggerNode = page.getByTestId('rf__node-trigger');
|
||||
await expect(triggerNode).toHaveClass(/selected/);
|
||||
await expect(triggerNode).toHaveText(/Company is Created/);
|
||||
const draftWorkflowStatus =
|
||||
workflowVisualizer.workflowStatus.getByText('Draft');
|
||||
|
||||
const addStepButton = page.getByLabel('Add a step');
|
||||
await addStepButton.click();
|
||||
await expect(draftWorkflowStatus).toBeVisible();
|
||||
|
||||
const createRecordOption = page.getByText('Create Record');
|
||||
await workflowVisualizer.activateWorkflowButton.click();
|
||||
|
||||
await createRecordOption.click();
|
||||
const activeWorkflowStatus =
|
||||
workflowVisualizer.workflowStatus.getByText('Active');
|
||||
|
||||
await expect(
|
||||
page.getByTestId('command-menu').getByRole('textbox').first(),
|
||||
).toHaveValue('Create Record');
|
||||
|
||||
const createRecordNode = page
|
||||
.locator('.react-flow__node.selected')
|
||||
.getByText('Create Record');
|
||||
await expect(createRecordNode).toBeVisible();
|
||||
await expect(triggerNode).not.toHaveClass(/selected/);
|
||||
await expect(draftWorkflowStatus).not.toBeVisible();
|
||||
await expect(activeWorkflowStatus).toBeVisible();
|
||||
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
|
||||
await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('Delete steps from draft version', async ({
|
||||
workflowVisualizer,
|
||||
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',
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user