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:
Baptiste Devessier
2025-01-21 11:46:27 +01:00
committed by GitHub
parent e1731bb31e
commit 8e0467e2e4
4 changed files with 374 additions and 31 deletions

View File

@ -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(),
]);
}
}

View 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';

View File

@ -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',
]);
});