chore(svelte-examples): pull out of lib package (#3363)

* chore(svelte-examples): pull out of lib package

* chore(svelte-examples): cleanup

* chore(examples): add readme files
This commit is contained in:
Moritz Klack
2023-08-31 16:44:27 +02:00
committed by GitHub
parent 3da34c739e
commit dcc38794a6
197 changed files with 2003 additions and 1575 deletions
@@ -0,0 +1,42 @@
import React, { useEffect } from 'react';
import { ReactFlow, Edge, useEdges } from '@xyflow/react';
import { nodes as initialNodes, edges as initialEdges } from '../../fixtures/simpleflow';
describe('useEdges.cy.tsx', () => {
it('handles edges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes} edges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
it('handles defaultEdges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (edges: Edge[]) => void }) => {
const edges = useEdges();
useEffect(() => {
onChange(edges);
}, [edges]);
return null;
};
@@ -0,0 +1,58 @@
import { useEffect } from 'react';
import { ReactFlow, Node, useNodes } from '@xyflow/react';
import { nodes } from '../../fixtures/simpleflow';
const nodeDimensions = {
width: 100,
height: 50,
};
const initialNodes: Node[] = nodes.map((n) => ({
...n,
style: nodeDimensions,
}));
const expectedNodes: Node[] = initialNodes.map((n) => ({
...n,
positionAbsolute: n.position,
...nodeDimensions,
}));
describe('useNodes.cy.tsx', () => {
it('handles nodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
it('handles defaultNodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (nodes: Node[]) => void }) => {
const nodes = useNodes();
useEffect(() => {
onChange(nodes);
}, [nodes]);
return null;
};
@@ -0,0 +1,68 @@
import { ReactFlow, useNodesInitialized } from '@xyflow/react';
import { nodes } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('useNodesInitialized.cy.tsx', () => {
it('returns false for no nodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={[]}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
// cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.been.calledWith', false);
});
it('returns false without onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
// cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.be.calledWith', false);
});
it('returns true for defaultNodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow defaultNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
it('returns true for nodes + onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ControlledFlow initialNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ControlledFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (init: boolean) => void }) {
const initialized = useNodesInitialized();
onChange(initialized);
return null;
}
@@ -0,0 +1,96 @@
import { ReactFlow, useOnViewportChange, Viewport } from '@xyflow/react';
describe('useOnViewportChange.cy.tsx', () => {
it('listen to viewport drag', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
cy.get('@onEndSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
});
});
it('handles zoom in', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onStartSpy = cy.spy().as('onStartSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onStart={onStartSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', defaultViewport);
});
});
});
// test specific helpers
function HookHelperComponent({
onStart,
onChange,
onEnd,
}: {
onStart?: (viewport: Viewport) => void;
onChange?: (viewport: Viewport) => void;
onEnd?: (viewport: Viewport) => void;
}) {
useOnViewportChange({ onStart, onChange, onEnd });
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
@@ -0,0 +1,79 @@
import { ReactFlow, useViewport, Viewport } from '@xyflow/react';
describe('useViewport.cy.tsx', () => {
it('handles drag', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.dragPane({ from: { x: 50, y: 0 }, to: { x: 50, y: 400 } }).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
expect(getLatestViewport(onChangeSpy)).to.eql({ x: 0, y: 400, zoom: 1 });
});
});
it('handles zoom in', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', defaultViewport);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (vp: Viewport) => void }) {
const viewport = useViewport();
onChange(viewport);
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
@@ -0,0 +1,197 @@
import { ReactFlow, EdgeProps } from '@xyflow/react';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
describe('uses nodes and edges', () => {
beforeEach(() => {
cy.mount(
<ControlledFlow
addOnNodeChangeHandler={false}
initialNodes={simpleflow.nodes}
initialEdges={simpleflow.edges}
/>
);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('can not drag a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.equal(styleAfterDrag);
});
});
});
describe('uses onNodesChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow addOnConnectHandler={false} initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('can not connect nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 0);
});
});
describe('uses onEdgeChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={simpleflow.edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
});
});
describe('uses onConnect handlers', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 1);
});
});
describe('uses nodeTypes', () => {
it('renders custom nodes', () => {
const nodeTypes = {
custom: () => <div className="customnode">custom node</div>,
};
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'custom',
}));
cy.mount(<ControlledFlow nodeTypes={nodeTypes} initialNodes={initialNodes} />);
cy.get('.react-flow__node-custom').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow').contains('custom node');
});
it('tries invalid node type - should fallback to default', () => {
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={initialNodes} />);
cy.get('.react-flow__node-invalid').should('have.length', 0);
cy.get('.react-flow__node-default').should('have.length', simpleflow.nodes.length);
});
});
describe('uses edgeTypes', () => {
it('renders custom edge', () => {
const edgeTypes = {
custom: ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => (
<path d={`M${sourceX} ${sourceY} L${targetX} ${targetY}`} stroke="black" />
),
};
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'custom',
}));
cy.mount(<ControlledFlow edgeTypes={edgeTypes} initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-custom').should('have.length', simpleflow.edges.length);
});
it('tries invalid edge type - should fallback to default', () => {
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-invalid').should('have.length', 0);
cy.get('.react-flow__edge-default').should('have.length', simpleflow.edges.length);
});
});
it('uses style', () => {
const styles = {
backgroundColor: 'red',
};
cy.mount(<ControlledFlow style={styles} />);
cy.get('.react-flow').should('have.css', 'background-color', 'rgb(255, 0, 0)');
});
it('uses classname', () => {
cy.mount(<ControlledFlow className="custom" />);
cy.get('.react-flow').should('have.class', 'custom');
});
});
@@ -0,0 +1,210 @@
import { skipOn } from '@cypress/skip-test';
import { nodes, edges } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('<ReactFlow />: Event handlers', () => {
describe('Node event handlers', () => {
it('handles onInit', () => {
const onInitSpy = cy.spy().as('onInitSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onInit={onInitSpy} />)
.wait(100)
.then(() => {
expect(onInitSpy.callCount).to.be.eq(1);
});
});
it('handles onNodeClick', () => {
const onNodeClickSpy = cy.spy().as('onNodeClickSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onNodeClick={onNodeClickSpy} />).then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(0);
cy.get('.react-flow__node:first')
.click()
.then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(1);
});
});
});
it('handles onNodeDrag handlers', () => {
const onNodeDragStart = cy.spy().as('onNodeDragStart');
const onNodeDrag = cy.spy().as('onNodeDrag');
const onNodeDragStop = cy.spy().as('onNodeDragStop');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
/>
).then(() => {
expect(onNodeDragStart.callCount).to.be.eq(0);
expect(onNodeDrag.callCount).to.be.eq(0);
expect(onNodeDragStop.callCount).to.be.eq(0);
cy.drag('.react-flow__node:first', { x: 200, y: 0 }).then(() => {
expect(onNodeDragStart.callCount).to.be.gt(0);
expect(onNodeDrag.callCount).to.be.gt(0);
expect(onNodeDragStop.callCount).to.be.gt(0);
});
});
});
it('handles onNodeMouse handlers', () => {
const onNodeMouseEnter = cy.spy().as('onNodeMouseEnter');
const onNodeMouseMove = cy.spy().as('onNodeMouseMove');
const onNodeMouseLeave = cy.spy().as('onNodeMouseLeave');
const onNodeContextMenu = cy.spy().as('onNodeContextMenu');
const onNodeDoubleClick = cy.spy().as('onNodeDoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.eq(0);
expect(onNodeMouseMove.callCount).to.be.eq(0);
expect(onNodeMouseLeave.callCount).to.be.eq(0);
expect(onNodeContextMenu.callCount).to.be.eq(0);
expect(onNodeDoubleClick.callCount).to.be.eq(0);
const node = cy.get('.react-flow__node').contains('Node 1');
node
.rightclick()
.dblclick()
.then(() => {
expect(onNodeContextMenu.callCount).to.be.eq(1);
expect(onNodeDoubleClick.callCount).to.be.eq(1);
});
skipOn('firefox');
node
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.gt(0);
expect(onNodeMouseMove.callCount).to.be.gt(0);
expect(onNodeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Edge event handlers', () => {
it('handles onEdgeClick', () => {
const onEdgeClick = cy.spy().as('onEdgeClick');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onEdgeClick={onEdgeClick} />).then(() => {
expect(onEdgeClick.callCount).to.be.eq(0);
cy.get('.react-flow__edge:first')
.click()
.then(() => {
expect(onEdgeClick.callCount).to.be.eq(1);
});
});
});
it('handles onEdgeMouse handlers', () => {
const onEdgeMouseEnter = cy.spy().as('onEdgeMouseEnter');
const onEdgeMouseMove = cy.spy().as('onEdgeMouseMove');
const onEdgeMouseLeave = cy.spy().as('onEdgeMouseLeave');
const onEdgeContextMenu = cy.spy().as('onEdgeContextMenu');
const onEdgeDoubleClick = cy.spy().as('onEdgedoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.eq(0);
expect(onEdgeMouseMove.callCount).to.be.eq(0);
expect(onEdgeMouseLeave.callCount).to.be.eq(0);
expect(onEdgeContextMenu.callCount).to.be.eq(0);
expect(onEdgeDoubleClick.callCount).to.be.eq(0);
const edge = cy.get('.react-flow__edge:first');
edge
.rightclick()
.dblclick()
.then(() => {
expect(onEdgeContextMenu.callCount).to.be.eq(1);
});
skipOn('firefox');
edge
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.gt(0);
expect(onEdgeMouseMove.callCount).to.be.gt(0);
expect(onEdgeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Pane event handlers', () => {
it('handles onMove handlers', () => {
const onMoveStart = cy.spy().as('onMoveStart');
const onMove = cy.spy().as('onMove');
const onMoveEnd = cy.spy().as('onMoveEnd');
cy.mount(<ControlledFlow onMoveStart={onMoveStart} onMove={onMove} onMoveEnd={onMoveEnd} />).then(() => {
expect(onMoveStart.callCount).to.be.eq(0);
expect(onMove.callCount).to.be.eq(0);
expect(onMoveEnd.callCount).to.be.eq(0);
cy.dragPane({ from: { x: 10, y: 200 }, to: { x: 100, y: 200 } })
.wait(100)
.then(() => {
expect(onMoveStart.callCount).to.be.eq(1);
expect(onMove.callCount).to.be.gt(0);
expect(onMoveEnd.callCount).to.be.eq(1);
});
});
});
it('handles click handlers', () => {
const onPaneClick = cy.spy().as('onPaneClick');
const onPaneContextMenu = cy.spy().as('onPaneContextMenu');
cy.mount(<ControlledFlow onPaneClick={onPaneClick} onPaneContextMenu={onPaneContextMenu} />).then(() => {
expect(onPaneClick.callCount).to.be.eq(0);
expect(onPaneContextMenu.callCount).to.be.eq(0);
cy.get('.react-flow__pane')
.rightclick()
.click()
.wait(100)
.then(() => {
expect(onPaneClick.callCount).to.be.eq(1);
expect(onPaneContextMenu.callCount).to.be.eq(1);
});
});
});
});
});
@@ -0,0 +1,63 @@
import { ReactFlow, BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, ReactFlowProvider } from '@xyflow/react';
import * as simpleflow from '../../fixtures/simpleflow';
function CustomEdge(props: EdgeProps) {
const [path, labelX, labelY] = getSmoothStepPath(props);
return (
<>
<BaseEdge path={path} labelX={labelX} labelY={labelY} />
<EdgeLabelRenderer>
<div className="label">{props.id}</div>
</EdgeLabelRenderer>
</>
);
}
const simpleflow1 = { ...simpleflow };
simpleflow1.edges = [...simpleflow1.edges];
simpleflow1.edges[0] = { ...simpleflow1.edges[0], id: 'edge1' };
const simpleflow2 = { ...simpleflow };
simpleflow2.edges = [...simpleflow2.edges];
simpleflow2.edges[0] = { ...simpleflow2.edges[0], id: 'edge2' };
describe('<ReactFlow />: Multiple Instances', () => {
describe('render EdgeLabelRenderer', () => {
beforeEach(() => {
cy.mount(
<>
<ReactFlowProvider>
<ReactFlow
defaultNodes={simpleflow1.nodes}
edgeTypes={{ default: CustomEdge }}
defaultEdges={simpleflow1.edges}
/>
</ReactFlowProvider>
<ReactFlowProvider>
<ReactFlow
defaultNodes={simpleflow2.nodes}
edgeTypes={{ default: CustomEdge }}
defaultEdges={simpleflow2.edges}
/>
</ReactFlowProvider>
</>
);
});
it('Each ReactFlow instance has one edge label in EdgeLabelRenderer', () => {
cy.get('.react-flow__edgelabel-renderer').should('have.length', 2);
cy.get('.react-flow__edgelabel-renderer')
.eq(0)
.within(() => {
cy.get('.label').should('have.length', 1).should('contain.text', 'edge1');
});
cy.get('.react-flow__edgelabel-renderer')
.eq(1)
.within(() => {
cy.get('.label').should('have.length', 1).should('contain.text', 'edge2');
});
});
});
});
@@ -0,0 +1,35 @@
import { ReactFlow } from '@xyflow/react';
import { nodes, edges } from '../../fixtures/simpleflow';
describe('<ReactFlow />: Uncontrolled Flow', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={nodes} defaultEdges={edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', nodes.length);
cy.get('.react-flow__edge').should('have.length', edges.length);
});
it('selects a node', () => {
cy.get('.react-flow__node').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__node').first().should('not.have.class', 'selected');
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__edge').first().should('not.have.class', 'selected');
});
});
@@ -0,0 +1,312 @@
import { ReactFlow, ReactFlowProps, Viewport, useViewport, SnapGrid, CoordinateExtent, Node } from '@xyflow/react';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
const nodesOutsideOfView = [
{
...simpleflow.nodes[0],
position: {
x: -500,
y: 0,
},
},
{
...simpleflow.nodes[1],
position: {
x: 500,
y: 0,
},
},
];
describe('<ReactFlow />: View Props', () => {
it('uses fitView', () => {
cy.mount(<Comp nodes={nodesOutsideOfView} fitView minZoom={0.2} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
it('uses fitViewOptions', () => {
const fitViewOptions = { padding: 0.5 };
cy.mount(<ReactFlow nodes={[nodesOutsideOfView[0]]} fitView fitViewOptions={fitViewOptions} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
describe('uses minZoom', () => {
it('minZoom: 0.2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={0.2} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(0.2);
});
});
it('minZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 0.2 };
cy.mount(<Comp minZoom={1} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
});
describe('uses maxZoom', () => {
it('maxZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('maxZoom: 2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={2} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(2);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 2 };
cy.mount(<Comp maxZoom={1.5} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1.5);
});
});
});
describe('uses defaultViewport', () => {
it('defaultViewport: { x: 0, y: 0, zoom: 1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 0, y: 0, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 100, y: 100, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 100, y: 100, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: -1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: -1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be min zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(0.5);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: 100 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 100 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be max zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(2);
});
});
});
it('uses snapGrid and snapToGrid', () => {
const snapGrid: SnapGrid = [100, 100];
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} snapGrid={snapGrid} snapToGrid />).then(() => {
cy.drag('.react-flow__node:first', { x: 125, y: 125 }).then(($el: any) => {
const transformAfterDrag = $el.css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transformAfterDrag);
expect([nodeX, nodeY]).to.eql(snapGrid);
});
});
});
describe('uses onlyRenderVisibleElements', () => {
it('displays no nodes', () => {
cy.mount(<ControlledFlow initialNodes={nodesOutsideOfView} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 0);
});
});
it('displays one node', () => {
const nodes = nodesOutsideOfView.map((n) => {
if (n.id === '1') {
return { ...n, position: { x: 0, y: 0 } };
}
return n;
});
cy.mount(<ControlledFlow initialNodes={nodes} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 1);
});
});
});
it('uses translateExtent', () => {
const translateExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
cy.mount(<ControlledFlow translateExtent={translateExtent} />).then(() => {
const transformBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
cy.dragPane({ from: { x: 0, y: 0 }, to: { x: 100, y: 100 } }).then(() => {
const transformAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(transformBeforeDrag).to.eql(transformAfterDrag);
});
});
});
it('uses nodeExtent', () => {
const nodes: Node[] = [
{
id: '1',
position: { x: 200, y: 200 },
data: { label: '1' },
},
];
const nodeExtent: CoordinateExtent = [
[0, 0],
[50, 50],
];
cy.mount(<ControlledFlow nodes={nodes} nodeExtent={nodeExtent} />)
.wait(500)
.then(() => {
const transform = Cypress.$('.react-flow__node:first').css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transform);
expect(nodeX).to.equal(50);
expect(nodeY).to.equal(50);
});
});
describe('uses preventScrolling', () => {
function ScrollFlow({ preventScrolling }: { preventScrolling: boolean }) {
return (
<div className="wrapper" style={{ height: 2000 }}>
<div className="inner" style={{ height: 500 }}>
<ControlledFlow preventScrolling={preventScrolling} />
</div>
</div>
);
}
it('lets user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={false} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
// @TODO: why is this not working?
// it seems that the event is somehow broken. This works fine when using the mouse.
// cy.window().its('scrollY').should('be.gt', 0);
});
});
});
it('does not user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={true} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
cy.window().its('scrollY').should('be.equal', 0);
});
});
});
});
describe('uses attributionPosition', () => {
it('displays it on the bottom right', () => {
cy.mount(<ControlledFlow />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
expect(left).equal(window.innerWidth - width);
expect(top).equal(window.innerHeight - height);
});
});
});
it('displays it on the top left', () => {
cy.mount(<ControlledFlow attributionPosition="top-left" />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).equal(0);
expect(top).equal(0);
});
});
});
});
});
// test specific helpers
type CompProps = {
onChange?: (vp: Viewport) => void;
};
function InnerComp({ onChange }: CompProps) {
const viewport = useViewport();
onChange?.(viewport);
return null;
}
function Comp({ onChange, ...rest }: CompProps & ReactFlowProps) {
return (
<ReactFlow {...rest}>
<InnerComp onChange={onChange} />
</ReactFlow>
);
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
@@ -0,0 +1,79 @@
import { Node, Edge, isNode, isEdge, getOutgoers, getIncomers, addEdge } from '@xyflow/react';
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-3', source: '2', target: '3' },
];
describe('Graph Utils Testing', () => {
it('tests isNode function', () => {
expect(isNode(nodes[0])).to.be.true;
expect(isNode(edges[0])).to.be.false;
});
it('tests isEdge function', () => {
expect(isEdge(edges[0])).to.be.true;
expect(isEdge(nodes[0])).to.be.false;
});
it('tests getOutgoers function', () => {
const outgoers = getOutgoers(nodes[0], nodes, edges);
expect(outgoers.length).to.be.equal(2);
const noOutgoers = getOutgoers(nodes[2], nodes, edges);
expect(noOutgoers.length).to.be.equal(0);
});
it('tests getIncomers function', () => {
const incomers = getIncomers(nodes[2], nodes, edges);
expect(incomers.length).to.be.equal(2);
const noIncomers = getIncomers(nodes[0], nodes, edges);
expect(noIncomers.length).to.be.equal(0);
});
describe('tests addEdge function', () => {
it('adds edge', () => {
const newEdge: Edge = { source: '1', target: '4', id: 'new-edge-1-4' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length + 1);
});
it('tries to add existing edge', () => {
const newEdge: Edge = { source: '2', target: '3', id: 'new-edge-2-3' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length);
});
it('tries to add invalid edge', () => {
// @ts-ignore
const newEdge: Edge = { nosource: '1', notarget: '3' };
try {
addEdge(newEdge, edges);
} catch (e: any) {
console.log(e.message);
expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
}
});
});
});
export {};
+170
View File
@@ -0,0 +1,170 @@
describe('Basic Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders a flow with three nodes', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow-basic-example'); // check if className prop works
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('renders a grid', () => {
cy.get('.react-flow__background');
});
it('selects two nodes by clicks', () => {
cy.get('body').type('{cmd}', { release: false });
cy.get('.react-flow__node:first')
.click()
.should('have.class', 'selected')
.get('.react-flow__node:last')
.click()
.should('have.class', 'selected')
.get('.react-flow__node:first')
.should('have.class', 'selected');
cy.get('body').type('{cmd}', { release: true });
});
it('selects a node by click', () => {
cy.get('.react-flow__node:first').as('node').click({ force: true }).should('have.class', 'selected');
});
it('deselects node', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
});
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').as('edge').click({ force: true });
cy.get('.react-flow__edge:first').should('have.class', 'selected');
});
it('deselects edge', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
});
it('selects one node with a selection', () => {
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 })
.wait(50)
.trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(200);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
it('selects all nodes', () => {
cy.get('body')
.type('{shift}', { release: false })
.get('.react-flow__pane')
.trigger('mousedown', 'topRight', { button: 0, force: true })
.trigger('mousemove', 'bottomLeft', { button: 0 })
.wait(50)
.trigger('mouseup', 'bottomLeft', { button: 0, force: true })
.wait(400)
.get('.react-flow__node')
.should('have.class', 'selected');
cy.wait(200);
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
it('removes selection', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__nodesselection-rect').should('not.exist');
});
it('selects an edge', () => {
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
// @TODO: why does this fail since react18?
// it('removes a node', () => {
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1);
// });
it('connects nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { force: true, button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove', { force: true, button: 0 })
.wait(200)
.trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').as('edge');
cy.get('@edge').should('have.length', 3);
});
// @TODO: why does this fail since react18?
// it('removes an edge', () => {
// cy.get('.react-flow__edge:first').click();
// cy.get('body').type('{backspace}');
// cy.get('.react-flow__edge').should('have.length', 1);
// });
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('zooms the pane', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
});
export {};
+83
View File
@@ -0,0 +1,83 @@
describe('Controls Testing', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders the control panel', () => {
cy.get('.react-flow__controls');
});
it('zooms in', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomin')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
it('zooms out', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomout')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
// view is already fitted so we drag the pane to un-fit it
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 10, 400)
.wait(50)
.trigger('mouseup', 10, 400, { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('fits view', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-fitview')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
it('uses interactive control - not interactive', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
cy.get('.react-flow__pane').click('topLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
cy.get('.react-flow__controls-interactive')
.click()
.then(() => {
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
});
});
it('uses interactive control - interactive', () => {
cy.get('.react-flow__controls-interactive')
.click()
.then(() => {
cy.get('.react-flow__node:first').click({ force: true }).should('have.class', 'selected');
});
});
});
export {};
@@ -0,0 +1,32 @@
describe('DragHandle Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/DragHandle');
});
it('renders a flow with a node', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 1);
});
it('tries to drag a node', () => {
const $nodeElement = Cypress.$('.react-flow__node:first');
const styleBeforeDrag = $nodeElement.css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 500 }).then(() => {
const styleAfterDrag = $nodeElement.css('transform');
expect(styleBeforeDrag).to.be.equal(styleAfterDrag);
});
});
it('drags a node', () => {
const $nodeElement = Cypress.$('.react-flow__node:first');
const styleBeforeDrag = $nodeElement.css('transform');
cy.drag('.custom-drag-handle:first', { x: 500, y: 500 }).then(() => {
const styleAfterDrag = $nodeElement.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
export {};
+44
View File
@@ -0,0 +1,44 @@
describe('Empty Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Empty');
});
it('renders an empty flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
});
it('renders empty selection', () => {
cy.get('.react-flow__renderer').click();
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 400, 50, { button: 0, force: true })
.trigger('mousemove', 200, 200, { button: 0 })
.wait(50)
.trigger('mouseup', 200, 200, { force: true });
cy.get('body').type('{shift}', { release: true });
});
it('adds two nodes', () => {
cy.contains('add node').click();
cy.contains('add node').click();
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 1);
});
});
export {};
+51
View File
@@ -0,0 +1,51 @@
describe('Figma Flow UI', { testIsolation: false }, () => {
before(() => {
cy.visit('/figma');
});
it('renders a flow with three nodes', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('renders a grid', () => {
cy.get('.react-flow__background');
});
it('selects all nodes by drag', () => {
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomRight', { force: true })
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
cy.get('.react-flow__node').should('have.class', 'selected');
});
});
});
it('removes selection', () => {
cy.get('.react-flow__pane').click('topLeft');
cy.get('.react-flow__node').should('not.have.class', 'selected');
});
it('drags using right click', () => {
cy.window().then((win) => {
cy.get('.react-flow__node:last').isWithinViewport();
cy.get('.react-flow__pane')
.trigger('mousedown', 'center', { button: 2, view: win })
.trigger('mousemove', 'bottom', { force: true })
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
cy.get('.react-flow__node').should('not.have.class', 'selected');
cy.get('.react-flow__node:last').isOutsideViewport();
});
});
});
});
export {};
+34
View File
@@ -0,0 +1,34 @@
describe('Hidden Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Hidden');
});
it('renders empty flow', () => {
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
it('toggles isHidden mode', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders initial flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 3);
cy.get('.react-flow__minimap-node').should('have.length', 4);
});
it('toggles isHidden mode again', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders empty flow', () => {
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
});
export {};
@@ -0,0 +1,164 @@
describe('Interaction Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Interaction');
});
it('renders initial flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('tries to select a node by click', () => {
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
expect(pointerEvents).to.equal('none');
});
it('tries to select an edge by click', () => {
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
expect(pointerEvents).to.equal('none');
});
it('toggles on capture element click', () => {
cy.get('.react-flow__captureelementclick').click();
});
it('allows node clicks when enabled', () => {
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
expect(pointerEvents).to.equal('all');
});
it('allows edge clicks when enabled', () => {
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
expect(pointerEvents.toLowerCase()).to.equal('visiblestroke');
});
it('tries to do a selection', () => {
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 1000, 50, { button: 0, force: true })
.trigger('mousemove', 1, 400, { button: 0 })
.wait(50)
.get('.react-flow__selection')
.should('not.exist');
cy.get('.react-flow__pane').trigger('mouseup', 1, 200, { force: true });
cy.get('body').type('{shift}', { release: true });
});
it('tries to connect to nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.then(($el) => {
const pointerEvents = $el.css('pointer-events');
expect(pointerEvents).to.equal('none');
});
});
it('tries to zoom by double click', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__renderer')
.dblclick()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
expect(styleBeforeZoom).to.equal(styleAfterZoom);
});
});
it('tries to zoom by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
expect(styleBeforeZoom).to.equal(styleAfterZoom);
});
});
it('toggles draggable mode', () => {
cy.get('.react-flow__draggable').click();
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 325, y: 100 }).then(($el) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('toggles selectable mode', () => {
cy.get('.react-flow__selectable').click();
});
it('selects a node by click', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
});
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').click({ force: true });
cy.get('.react-flow__edge:first').should('have.class', 'selected');
});
it('toggles connectable mode', () => {
cy.get('.react-flow__connectable').click();
});
it('connects two nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 3);
});
it('toggles zoom on scroll', () => {
cy.get('.react-flow__zoomonscroll').click();
});
it('zooms by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: 200 })
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});
it('toggles zoom on double click', () => {
cy.get('.react-flow__zoomondbl').click();
});
it('zooms by double click', () => {
cy.get('.react-flow__controls-zoomout').click();
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.dblclick()
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});
});
export {};
+77
View File
@@ -0,0 +1,77 @@
describe('Minimap Testing', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders the mini map', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-mask');
});
it('has same number of nodes as the pane', () => {
cy.get('.react-flow__minimap-node').then(() => {
const paneNodes = Cypress.$('.react-flow__node').length;
const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
expect(paneNodes).equal(minimapNodes);
});
});
it('changes zoom level', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeZoom).to.not.equal(viewBoxAfterZoom);
expect(maskPathBeforeZoom).to.not.equal(maskPathAfterZoom);
});
});
it('changes node position', () => {
const minimapNode = Cypress.$('.react-flow__minimap-node:first');
const xPosBeforeDrag = Number(minimapNode.attr('x'));
const yPosBeforeDrag = Number(minimapNode.attr('y'));
cy.drag('.react-flow__node:first', { x: 500, y: 25 })
.wait(100)
.then(() => {
const xPosAfterDrag = Number(minimapNode.attr('x'));
const yPosAfterDrag = Number(minimapNode.attr('y'));
expect(xPosAfterDrag).not.to.equal(xPosBeforeDrag);
expect(yPosAfterDrag).not.to.equal(yPosBeforeDrag);
expect(xPosAfterDrag - xPosBeforeDrag).to.be.greaterThan(yPosAfterDrag - yPosBeforeDrag);
});
});
it('changes node positions via pane drag', () => {
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
});
});
});
});
export {};
@@ -0,0 +1,22 @@
import { Node, Edge } from '@xyflow/react';
export const nodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
},
];
export const edges: Edge[] = [
{
id: 'e1',
source: '1',
target: '2',
},
];
@@ -0,0 +1,65 @@
import { useCallback, useState } from 'react';
import {
ReactFlow,
Node,
Edge,
NodeChange,
EdgeChange,
applyNodeChanges,
applyEdgeChanges,
Connection,
addEdge,
ReactFlowProps,
} from '@xyflow/react';
function ControlledFlow({
addOnNodeChangeHandler = true,
addOnEdgeChangeHandler = true,
addOnConnectHandler = true,
initialNodes = [],
initialEdges = [],
...rest
}: {
initialNodes?: Node[];
initialEdges?: Edge[];
addOnNodeChangeHandler?: boolean;
addOnEdgeChangeHandler?: boolean;
addOnConnectHandler?: boolean;
} & Partial<ReactFlowProps>) {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange = useCallback(
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const handlers: {
onNodesChange?: (changes: NodeChange[]) => void;
onEdgesChange?: (changes: EdgeChange[]) => void;
onConnect?: (params: Connection | Edge) => void;
} = {};
if (addOnNodeChangeHandler) {
handlers.onNodesChange = onNodesChange;
}
if (addOnEdgeChangeHandler) {
handlers.onEdgesChange = onEdgesChange;
}
if (addOnConnectHandler) {
handlers.onConnect = onConnect;
}
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
}
export default ControlledFlow;
@@ -0,0 +1,60 @@
Cypress.Commands.add('drag', (selector, { x, y }) =>
cy.window().then((window) => {
const elementToDrag = cy.get(selector as string);
return elementToDrag.then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const nextX: number = centerX + x;
const nextY: number = centerY + y;
return elementToDrag
.trigger('mousedown', { view: window })
.trigger('mousemove', nextX, nextY, { force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true });
});
})
);
Cypress.Commands.add('dragPane', ({ from, to }) =>
cy
.window()
.then((window) =>
cy
.get('.react-flow__pane')
.trigger('mousedown', from.x, from.y, { view: window })
.trigger('mousemove', to.x, to.y)
.trigger('mouseup', { force: true, view: window })
)
);
Cypress.Commands.add('zoomPane', (wheelDelta: number) =>
cy.get('.react-flow__pane').trigger('wheel', 'center', { deltaY: wheelDelta }).wait(250)
);
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
return cy.window().then((window) => {
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
return cy.window().then((window) => {
expect(window.innerHeight < rect.top || rect.bottom < 0 || window.innerWidth < rect.left || rect.right < 0).to.be
.true;
return subject;
});
});
export {};
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
<style>
html,
body,
#root {
margin: 0;
height: 100%;
}
</style>
</head>
<body>
<div data-cy-root id="root"></div>
</body>
</html>
@@ -0,0 +1,27 @@
///<reference types="cypress" />
///<reference types="@cypress/skip-test" />
///<reference types="cypress-real-events" />
import './commands';
import 'cypress-real-events/support';
import { mount } from 'cypress/react18';
import { XYPosition } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
drag: (selector: string, toPosition: XYPosition) => Cypress.Chainable<JQuery<HTMLElement>>;
dragPane: ({ from, to }: { from: XYPosition; to: XYPosition }) => Cypress.Chainable<JQuery<HTMLElement>>;
zoomPane: (wheelDelta: number) => Cypress.Chainable<JQuery<HTMLElement>>;
isWithinViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
isOutsideViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add('mount', mount);
+25
View File
@@ -0,0 +1,25 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
Cypress.on('uncaught:exception', (err) => {
if (resizeObserverLoopErrRe.test(err.message)) {
return false;
}
});