added tests for new bpmn and dmn diagrams and added edit functionality to groups and models w/ burnettk

This commit is contained in:
jasquat 2022-06-20 14:45:15 -04:00
parent 0819d44a33
commit 8f1ad42d6d
13 changed files with 374 additions and 70 deletions

View File

@ -3,7 +3,7 @@ describe('process-groups', () => {
cy.visit('/');
});
it('can create a new group and navigate there', () => {
it('can perform crud operations', () => {
const uuid = () => Cypress._.random(0, 1e6)
const id = uuid()
const groupDisplayName = `Test Group 1 ${id}`;
@ -15,19 +15,13 @@ describe('process-groups', () => {
cy.contains(groupId).click()
cy.url().should('include', `process-groups/${groupId}`);
cy.contains(`Process Group: ${groupId}`);
cy.contains('Delete Process Group').click();
cy.url().should('include', `process-groups`);
cy.contains(groupId).should('not.exist');
})
it('can paginate items', () => {
cy.get("#pagination-page-dropdown")
.type("typing_to_open_dropdown_box....FIXME")
.find('.dropdown-item')
.contains(/^2$/)
.click();
cy.contains(/^1-2 of \d+$/);
cy.getBySel("pagination-next-button").click();
cy.contains(/^3-4 of \d+$/);
cy.getBySel("pagination-previous-button").click();
cy.contains(/^1-2 of \d+$/);
cy.basicPaginationTest();
})
})

View File

@ -1,6 +1,8 @@
describe('process-instances', () => {
beforeEach(() => {
cy.visit('/');
cy.contains('acceptance-tests-group-one').click();
cy.contains('acceptance-tests-model-1').click();
});
it('can create a new instance and can modify', () => {
@ -14,12 +16,8 @@ describe('process-instances', () => {
const dmnFile = "awesome_decision.dmn";
const bpmnFile = "process_model_one.bpmn";
cy.contains('acceptance-tests-group-one').click();
cy.contains('acceptance-tests-model-1').click();
cy.contains(originalDmnOutputForKevin).should('not.exist');;
cy.contains('Run Primary').click();
cy.contains(originalDmnOutputForKevin);
runPrimaryBpmnFile(originalDmnOutputForKevin);
// Change dmn
cy.contains(dmnFile).click();
@ -27,46 +25,60 @@ describe('process-instances', () => {
updateDmnText(originalDmnOutputForKevin, newDmnOutputForKevin);
cy.contains('acceptance-tests-model-1').click();
cy.contains('Run Primary').click();
cy.contains(newDmnOutputForKevin);
runPrimaryBpmnFile(newDmnOutputForKevin);
cy.contains(dmnFile).click();
cy.contains(`Process Model File: ${dmnFile}`);
updateDmnText(newDmnOutputForKevin, originalDmnOutputForKevin);
cy.contains('acceptance-tests-model-1').click();
cy.contains('Run Primary').click();
cy.contains(originalDmnOutputForKevin);
runPrimaryBpmnFile(originalDmnOutputForKevin);
// Change bpmn
cy.contains(bpmnFile).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScript(newPythonScript, bpmnFile);
cy.contains('acceptance-tests-model-1').click();
cy.contains('Run Primary').click();
cy.contains(dmnOutputForDan);
runPrimaryBpmnFile(dmnOutputForDan);
cy.contains(bpmnFile).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScript(originalPythonScript, bpmnFile);
cy.contains('acceptance-tests-model-1').click();
cy.contains('Run Primary').click();
cy.contains(originalDmnOutputForKevin);
runPrimaryBpmnFile(originalDmnOutputForKevin);
});
// it('can paginate items', () => {
// cy.contains('acceptance-tests-group-one').click();
// cy.get("#pagination-page-dropdown")
// .type("typing_to_open_dropdown_box....FIXME")
// .find('.dropdown-item')
// .contains(/^2$/)
// .click();
//
// cy.contains(/^1-2 of \d+$/);
// cy.getBySel("pagination-next-button").click();
// cy.contains(/^3-4 of \d+$/);
// cy.getBySel("pagination-previous-button").click();
// cy.contains(/^1-2 of \d+$/);
// })
it('can create a new instance and can modify with monaco text editor', () => {
const dmnOutputForKevin = "Very wonderful";
const dmnOutputForMike = "Powerful wonderful";
const originalPythonScript = 'person = "Kevin"';
const newPythonScript = 'person = "Mike"';
const bpmnFile = "process_model_one.bpmn";
// Change bpmn
cy.contains(bpmnFile).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScriptWithMonaco(newPythonScript, bpmnFile);
cy.contains('acceptance-tests-model-1').click();
runPrimaryBpmnFile(dmnOutputForMike);
cy.contains(bpmnFile).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScriptWithMonaco(originalPythonScript, bpmnFile);
cy.contains('acceptance-tests-model-1').click();
runPrimaryBpmnFile(dmnOutputForKevin);
});
it('can paginate items', () => {
// make sure we have some process instances
runPrimaryBpmnFile('Very wonderful');
runPrimaryBpmnFile('Very wonderful');
runPrimaryBpmnFile('Very wonderful');
runPrimaryBpmnFile('Very wonderful');
runPrimaryBpmnFile('Very wonderful');
cy.contains('Process Instances').click();
cy.basicPaginationTest();
})
})
function updateDmnText(oldText, newText, elementId="wonderful_process", dmnFile="awesome_decision.dmn") {
@ -90,3 +102,25 @@ function updateBpmnPythonScript(pythonScript, bpmnFile, elementId="process_scrip
cy.wait(500);
cy.contains('Save').click();
}
function updateBpmnPythonScriptWithMonaco(pythonScript, bpmnFile, elementId="process_script") {
cy.get(`g[data-element-id=${elementId}]`).click().should('exist');
cy.contains('SpiffWorkflow Properties').click();
cy.contains('Launch Editor').click();
cy.contains("Loading...").should('not.exist');
cy.get('.monaco-editor textarea:first')
.click()
.focused() // change subject to currently focused element
.type('{ctrl}a')
.type(pythonScript)
cy.contains('Close').click();
// wait for a little bit for the xml to get set before saving
cy.wait(500);
cy.contains('Save').click();
}
function runPrimaryBpmnFile(expectedText) {
cy.contains('Run Primary').click();
cy.contains(expectedText);
}

View File

@ -3,14 +3,14 @@ describe('process-models', () => {
cy.visit('/');
});
it('can create a new model and navigate there', () => {
it('can perform crud operations', () => {
const uuid = () => Cypress._.random(0, 1e6)
const id = uuid()
const groupDisplayName = `Test Group 2 ${id}`;
const groupId = `test-group-2-${id}`;
const groupId = 'acceptance-tests-group-one';
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
cy.createModel(groupId, groupDisplayName, modelId, modelDisplayName);
cy.contains(groupId).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(`Process Group: ${groupId}`).click();
cy.contains(modelId);
@ -18,24 +18,68 @@ describe('process-models', () => {
cy.url().should('include', `process-models/${groupId}/${modelId}`);
cy.contains(`Process Model: ${modelId}`);
cy.contains('Delete Process Model').click();
cy.contains('Delete process model').click();
cy.url().should('include', `process-groups/${groupId}`);
cy.contains(modelId).should('not.exist');
});
// it('can paginate items', () => {
// cy.contains('acceptance-tests-group-one').click();
// cy.get("#pagination-page-dropdown")
// .type("typing_to_open_dropdown_box....FIXME")
// .find('.dropdown-item')
// .contains(/^2$/)
// .click();
//
// cy.contains(/^1-2 of \d+$/);
// cy.getBySel("pagination-next-button").click();
// cy.contains(/^3-4 of \d+$/);
// cy.getBySel("pagination-previous-button").click();
// cy.contains(/^1-2 of \d+$/);
// })
it('can create new bpmn and dmn files', () => {
const uuid = () => Cypress._.random(0, 1e6)
const id = uuid()
const groupId = 'acceptance-tests-group-one';
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
const bpmnFileName = `bpmn_test_file_${id}`;
const dmnFileName = `dmn_test_file_${id}`;
cy.contains(groupId).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(`Process Group: ${groupId}`).click();
cy.contains(modelId);
cy.contains(modelId).click()
cy.url().should('include', `process-models/${groupId}/${modelId}`);
cy.contains(`Process Model: ${modelId}`);
cy.contains(bpmnFileName + '.bpmn').should('not.exist');
cy.contains(dmnFileName + '.dmn').should('not.exist');
// add new bpmn file
cy.contains('Add New BPMN File').click();
cy.contains(/^Process Model File$/);
cy.get('g[data-element-id=StartEvent_1]').click().should('exist');
cy.contains('General').click();
cy.get('#bio-properties-panel-name').clear().type("Start Event Name");
cy.wait(500);
cy.contains('Save').click();
cy.contains('Start Event Name');
cy.get('input[name=file_name]').type(bpmnFileName);
cy.contains('Save Changes').click();
cy.contains(`Process Model File: ${bpmnFileName}`);
cy.contains(modelId).click();
cy.contains(`Process Model: ${modelId}`);
cy.contains(bpmnFileName + '.bpmn').should('exist');
// add new dmn file
cy.contains('Add New DMN File').click();
cy.contains(/^Process Model File$/);
cy.get('g[data-element-id=decision_1]').click().should('exist');
cy.contains('General').click();
cy.contains('Save').click();
cy.get('input[name=file_name]').type(dmnFileName);
cy.contains('Save Changes').click();
cy.contains(`Process Model File: ${dmnFileName}`);
cy.contains(modelId).click();
cy.contains(`Process Model: ${modelId}`);
cy.contains(dmnFileName + '.dmn').should('exist');
cy.contains('Delete process model').click();
cy.url().should('include', `process-groups/${groupId}`);
cy.contains(modelId).should('not.exist');
});
it('can paginate items', () => {
cy.contains('acceptance-tests-group-one').click();
cy.basicPaginationTest();
})
})

View File

@ -40,8 +40,7 @@ Cypress.Commands.add('createGroup', (groupId, groupDisplayName) => {
cy.contains(`Process Group: ${groupId}`);
});
Cypress.Commands.add('createModel', (groupId, groupDisplayName, modelId, modelDisplayName) => {
cy.createGroup(groupId, groupDisplayName);
Cypress.Commands.add('createModel', (groupId, modelId, modelDisplayName) => {
cy.contains(modelId).should('not.exist');
cy.contains('Add a process model').click();
@ -53,3 +52,19 @@ Cypress.Commands.add('createModel', (groupId, groupDisplayName, modelId, modelDi
cy.url().should('include', `process-models/${groupId}/${modelId}`);
cy.contains(`Process Model: ${modelId}`);
});
Cypress.Commands.add('basicPaginationTest', () => {
cy.get("#pagination-page-dropdown")
.type("typing_to_open_dropdown_box....FIXME")
.find('.dropdown-item')
.contains(/^2$/)
.click();
cy.contains(/^1-2 of \d+$/);
cy.getBySel("pagination-previous-button-inactive");
cy.getBySel("pagination-next-button").click();
cy.contains(/^3-4 of \d+$/);
cy.getBySel("pagination-previous-button").click();
cy.contains(/^1-2 of \d+$/);
});

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_76910d7" name="DRD" namespace="http://camunda.org/schema/1.0/dmn">
<decision id="Decision_6981fef" name="Decision 1">
<decision id="decision_1" name="Decision 1">
<decisionTable id="decisionTable_1">
<input id="input_1">
<inputExpression id="inputExpression_1" typeRef="string">
@ -12,7 +12,7 @@
</decision>
<dmndi:DMNDI>
<dmndi:DMNDiagram id="DMNDiagram_1cykosu">
<dmndi:DMNShape id="DMNShape_1dhfq2s" dmnElementRef="Decision_6981fef">
<dmndi:DMNShape id="DMNShape_1dhfq2s" dmnElementRef="decision_1">
<dc:Bounds height="80" width="180" x="157" y="151" />
</dmndi:DMNShape>
</dmndi:DMNDiagram>

View File

@ -36,7 +36,7 @@ export default function PaginationForTable(props) {
let previousPageTag = "";
if (props.page === 1) {
previousPageTag = (
<li className="page-item disabled" key="previous"><span style={{fontSize:"1.5em"}} className="page-link">&laquo;</span></li>
<li data-qa="pagination-previous-button-inactive" className="page-item disabled" key="previous"><span style={{fontSize:"1.5em"}} className="page-link">&laquo;</span></li>
)
} else {
previousPageTag = (
@ -49,7 +49,7 @@ export default function PaginationForTable(props) {
let nextPageTag = "";
if (props.page >= props.pagination.pages) {
nextPageTag = (
<li className="page-item disabled" key="next"><span style={{fontSize:"1.5em"}} className="page-link">&raquo;</span></li>
<li data-qa="pagination-next-button-inactive" className="page-item disabled" key="next"><span style={{fontSize:"1.5em"}} className="page-link">&raquo;</span></li>
)
} else {
nextPageTag = (

View File

@ -3,3 +3,9 @@ const hostAndPort = `${host}:7000`;
export const BACKEND_BASE_URL = `http://${hostAndPort}/v1.0`
export const HOT_AUTH_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOm51bGx9.krsOjlSilPMu_3r7WkkUfKyr-h3HprXr6R4_FXRXz6Y"
export const STANDARD_HEADERS = {
headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
})
}

View File

@ -16,11 +16,13 @@ import {
import ProcessGroups from "./routes/ProcessGroups"
import ProcessGroupShow from "./routes/ProcessGroupShow"
import ProcessGroupNew from "./routes/ProcessGroupNew"
import ProcessGroupEdit from "./routes/ProcessGroupEdit"
import ProcessModelShow from "./routes/ProcessModelShow"
import ProcessModelEditDiagram from "./routes/ProcessModelEditDiagram"
import ProcessInstanceList from "./routes/ProcessInstanceList"
import ProcessInstanceReport from "./routes/ProcessInstanceReport"
import ProcessModelNew from "./routes/ProcessModelNew"
import ProcessModelEdit from "./routes/ProcessModelEdit"
import ErrorBoundary from "./components/ErrorBoundary"
import { Container } from 'react-bootstrap'
@ -38,6 +40,7 @@ root.render(
<Route path="process-groups" element={<ProcessGroups />} />
<Route path="process-groups/:process_group_id" element={<ProcessGroupShow />} />
<Route path="process-groups/new" element={<ProcessGroupNew />} />
<Route path="process-groups/:process_group_id/edit" element={<ProcessGroupEdit />} />
<Route path="process-models/:process_group_id/new" element={<ProcessModelNew />} />
<Route path="process-models/:process_group_id/:process_model_id" element={<ProcessModelShow />} />
@ -45,6 +48,7 @@ root.render(
<Route path="process-models/:process_group_id/:process_model_id/file/:file_name" element={<ProcessModelEditDiagram />} />
<Route path="process-models/:process_group_id/:process_model_id/process-instances" element={<ProcessInstanceList />} />
<Route path="process-models/:process_group_id/:process_model_id/process-instances/report" element={<ProcessInstanceReport />} />
<Route path="process-models/:process_group_id/:process_model_id/edit" element={<ProcessModelEdit />} />
</Routes>
</BrowserRouter>
</ErrorBoundary>

View File

@ -0,0 +1,87 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { BACKEND_BASE_URL } from '../config';
import { HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'
import { Button, Stack } from 'react-bootstrap'
export default function ProcessGroupEdit() {
const [displayName, setDisplayName] = useState("");
const params = useParams();
const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState(null);
useEffect(() => {
fetch(`${BACKEND_BASE_URL}/process-groups/${params.process_group_id}`, STANDARD_HEADERS)
.then(res => res.json())
.then(
(result) => {
setProcessGroup(result);
setDisplayName(result.display_name);
},
(error) => {
console.log(error);
}
)
}, [params]);
const updateProcessGroup = ((event) => {
event.preventDefault()
fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, {
headers: new Headers({
'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
}),
method: 'PUT',
body: JSON.stringify({
display_name: displayName,
id: processGroup.id,
}),
})
.then(res => res.json())
.then(
(result) => {
navigate(`/process-groups/${processGroup.id}`)
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(newError) => {
console.log(newError);
}
)
});
const onDisplayNameChanged = ((newDisplayName) => {
setDisplayName(newDisplayName);
});
if (processGroup) {
return (
<main style={{ padding: "1rem 0" }}>
<ProcessBreadcrumb processGroupId={processGroup.id} />
<h2>Edit Process Group: {processGroup.id}</h2>
<form onSubmit={updateProcessGroup}>
<label>Display Name:</label>
<input
name='display_name'
type='text'
value={displayName}
onChange={e => onDisplayNameChanged(e.target.value)}
/>
<br />
<br />
<Stack direction="horizontal" gap={3}>
<Button type="submit">Submit</Button>
<Button variant="secondary" href={`/process-groups/${processGroup.id}`}>Cancel</Button>
</Stack>
</form>
</main>
);
} else {
return (<></>)
}
}

View File

@ -1,15 +1,16 @@
import React, { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { Link, useSearchParams, useNavigate } from "react-router-dom";
import { BACKEND_BASE_URL } from '../config';
import { HOT_AUTH_TOKEN } from '../config';
import { useParams } from "react-router-dom";
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'
import { Button, Table } from 'react-bootstrap'
import { Button, Table, Stack } from 'react-bootstrap'
import PaginationForTable, { DEFAULT_PER_PAGE, DEFAULT_PAGE } from '../components/PaginationForTable'
export default function ProcessGroupShow() {
let params = useParams();
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState(null);
const [processModels, setProcessModels] = useState([]);
@ -48,6 +49,24 @@ export default function ProcessGroupShow() {
)
}, [params, searchParams]);
const deleteProcessGroup = (() => {
fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, {
headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
}),
method: 'DELETE',
})
.then(res => res.json())
.then(
(result) => {
navigate(`/process-groups`);
},
(error) => {
console.log(error);
}
)
});
const buildTable = (() => {
const rows = processModels.map((row,index) => {
return (
@ -82,7 +101,11 @@ export default function ProcessGroupShow() {
<ProcessBreadcrumb processGroupId={processGroup.id} />
<h2>Process Group: {processGroup.id}</h2>
<ul>
<Button href={`/process-models/${processGroup.id}/new`}>Add a process model</Button>
<Stack direction="horizontal" gap={3}>
<Button href={`/process-models/${processGroup.id}/new`}>Add a process model</Button>
<Button href={`/process-groups/${processGroup.id}/edit`} variant="secondary">Edit process group</Button>
<Button onClick={deleteProcessGroup} variant="danger">Delete Process Group</Button>
</Stack>
<br />
<br />
<PaginationForTable

View File

@ -0,0 +1,95 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { BACKEND_BASE_URL } from '../config';
import { HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'
import { slugifyString } from '../helpers'
import { Button, Stack } from 'react-bootstrap'
export default function ProcessModelEdit() {
const [displayName, setDisplayName] = useState("");
const params = useParams();
const navigate = useNavigate();
const [processModel, setProcessModel] = useState(null);
const processModelPath = `process-models/${params.process_group_id}/${params.process_model_id}`
useEffect(() => {
fetch(`${BACKEND_BASE_URL}/${processModelPath}`, STANDARD_HEADERS)
.then(res => res.json())
.then(
(result) => {
setProcessModel(result);
setDisplayName(result.display_name);
},
(error) => {
console.log(error);
}
)
}, [processModelPath]);
const updateProcessModel = ((event) => {
event.preventDefault()
fetch(`${BACKEND_BASE_URL}/${processModelPath}`, {
headers: new Headers({
'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
}),
method: 'PUT',
body: JSON.stringify({
id: processModel.id,
display_name: displayName,
description: processModel.description,
process_group_id: processModel.process_group_id,
is_master_spec: processModel.is_master_spec,
standalone: processModel.standalone,
library: processModel.library,
}),
})
.then(res => res.json())
.then(
(result) => {
navigate(`/${processModelPath}`)
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(newError) => {
console.log(newError);
}
)
});
const onDisplayNameChanged = ((newDisplayName) => {
setDisplayName(newDisplayName);
});
if (processModel) {
return (
<main style={{ padding: "1rem 0" }}>
<ProcessBreadcrumb processGroupId={processModel.id} />
<h2>Edit Process Group: {processModel.id}</h2>
<form onSubmit={updateProcessModel}>
<label>Display Name:</label>
<input
name='display_name'
type='text'
value={displayName}
onChange={e => onDisplayNameChanged(e.target.value)}
/>
<br />
<br />
<Stack direction="horizontal" gap={3}>
<Button type="submit">Submit</Button>
<Button variant="secondary" href={`/${processModelPath}`}>Cancel</Button>
</Stack>
</form>
</main>
);
} else {
return (<></>)
}
}

View File

@ -215,7 +215,8 @@ export default function ProcessModelEditDiagram() {
)
});
if (bpmnXmlForDiagramRendering) {
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
if (bpmnXmlForDiagramRendering || !params.file_name) {
return (
<main style={{ padding: "1rem 0" }}>
<ProcessBreadcrumb

View File

@ -102,7 +102,8 @@ export default function ProcessModelShow() {
<br />
<Stack direction="horizontal" gap={3}>
<Button onClick={processModelRun} variant="primary">Run Primary</Button>
<Button onClick={deleteProcessModel} variant="danger">Delete Process Model</Button>
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/edit`} variant="secondary">Edit process model</Button>
<Button onClick={deleteProcessModel} variant="danger">Delete process model</Button>
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=bpmn`} variant="warning">Add New BPMN File</Button>
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=dmn`} variant="success">Add New DMN File</Button>
</Stack>