After @bosiraphael's updates on the table, cells are duplicated when they get the hover/focus. Playwright has a hard time finding which element to click on. I dislike my solution because it doesn't mimic how a real user would use the application, but I couldn't find a better solution that wasn't flaky.
279 lines
8.0 KiB
TypeScript
279 lines
8.0 KiB
TypeScript
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;
|
|
|
|
workflowId: string;
|
|
workflowName: string;
|
|
|
|
readonly addStepButton: Locator;
|
|
readonly workflowStatus: Locator;
|
|
readonly activateWorkflowButton: Locator;
|
|
readonly deactivateWorkflowButton: Locator;
|
|
readonly addTriggerButton: Locator;
|
|
readonly commandMenu: Locator;
|
|
readonly stepHeaderInCommandMenu: Locator;
|
|
readonly workflowNameLabel: Locator;
|
|
readonly triggerNode: Locator;
|
|
readonly background: Locator;
|
|
readonly useAsDraftButton: Locator;
|
|
readonly overrideDraftButton: Locator;
|
|
readonly discardDraftButton: 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: 'Launch manually',
|
|
};
|
|
|
|
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.stepHeaderInCommandMenu = this.commandMenu.getByTestId(
|
|
'workflow-step-header',
|
|
);
|
|
this.workflowNameLabel = page
|
|
.getByTestId('top-bar-title')
|
|
.getByText(this.workflowName);
|
|
this.triggerNode = this.#page.getByTestId('rf__node-trigger');
|
|
this.background = page.locator('.react-flow__pane');
|
|
this.useAsDraftButton = page.getByRole('button', { name: 'Use as draft' });
|
|
this.overrideDraftButton = page.getByRole('button', {
|
|
name: 'Override Draft',
|
|
});
|
|
this.discardDraftButton = page.getByRole('button', {
|
|
name: 'Discard Draft',
|
|
});
|
|
}
|
|
|
|
async createOneWorkflow() {
|
|
const id = randomUUID();
|
|
|
|
const response = await createWorkflow({
|
|
page: this.#page,
|
|
workflowId: id,
|
|
workflowName: this.workflowName,
|
|
});
|
|
|
|
expect(response.status()).toBe(200);
|
|
|
|
const responseBody = await response.json();
|
|
expect(responseBody.data.createWorkflow.id).toBe(id);
|
|
|
|
this.workflowId = id;
|
|
}
|
|
|
|
async waitForWorkflowVisualizerLoad() {
|
|
await expect(this.workflowNameLabel).toBeVisible();
|
|
}
|
|
|
|
async goToWorkflowVisualizerPage() {
|
|
await this.#page.goto(`/`);
|
|
|
|
const workflowsLink = this.#page.getByRole('link', { name: 'Workflows' });
|
|
|
|
await workflowsLink.click();
|
|
|
|
const workflowLink = this.#page
|
|
.getByTestId(`row-id-${this.workflowId}`)
|
|
.getByRole('link', { name: this.workflowName });
|
|
|
|
await workflowLink.click({ force: true });
|
|
|
|
await this.waitForWorkflowVisualizerLoad();
|
|
}
|
|
|
|
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);
|
|
|
|
await actionToCreateOption.click();
|
|
|
|
const createWorkflowStepResponse = await this.#page.waitForResponse(
|
|
(response) => {
|
|
if (!response.url().endsWith('/graphql')) {
|
|
return false;
|
|
}
|
|
|
|
const requestBody = response.request().postDataJSON();
|
|
|
|
return requestBody.operationName === 'CreateWorkflowVersionStep';
|
|
},
|
|
);
|
|
|
|
const createWorkflowStepResponseBody =
|
|
await createWorkflowStepResponse.json();
|
|
const createdStepId =
|
|
createWorkflowStepResponseBody.data.createWorkflowVersionStep.id;
|
|
|
|
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(),
|
|
]);
|
|
}
|
|
|
|
async closeSidePanel() {
|
|
const closeButton = this.#page.getByRole('button', {
|
|
name: 'Close command menu',
|
|
});
|
|
|
|
await closeButton.click();
|
|
|
|
await expect(this.#page.getByTestId('command-menu')).not.toBeVisible();
|
|
}
|
|
}
|
|
|
|
export const test = base.extend<{ workflowVisualizer: WorkflowVisualizerPage }>(
|
|
{
|
|
workflowVisualizer: async ({ page }, use) => {
|
|
const workflowVisualizer = new WorkflowVisualizerPage({
|
|
page,
|
|
workflowName: 'Test Workflow',
|
|
});
|
|
|
|
await workflowVisualizer.createOneWorkflow();
|
|
await workflowVisualizer.goToWorkflowVisualizerPage();
|
|
|
|
await use(workflowVisualizer);
|
|
|
|
await deleteWorkflow({
|
|
page,
|
|
workflowId: workflowVisualizer.workflowId,
|
|
});
|
|
await destroyWorkflow({
|
|
page,
|
|
workflowId: workflowVisualizer.workflowId,
|
|
});
|
|
},
|
|
},
|
|
);
|