diff --git a/examples/nextjs/cypress/components/reactflow/basic-props.cy.tsx b/examples/nextjs/cypress/components/reactflow/basic-props.cy.tsx index c9cf2a43..cd9eb7aa 100644 --- a/examples/nextjs/cypress/components/reactflow/basic-props.cy.tsx +++ b/examples/nextjs/cypress/components/reactflow/basic-props.cy.tsx @@ -1,18 +1,6 @@ -import { useCallback, useState } from 'react'; -import { - ReactFlow, - Node, - Edge, - NodeChange, - EdgeChange, - applyNodeChanges, - applyEdgeChanges, - Connection, - addEdge, - ReactFlowProps, - EdgeProps, -} from '@react-flow/bundle'; +import { ReactFlow, EdgeProps } from '@react-flow/bundle'; +import ControlledFlow from '../../support/ControlledFlow'; import * as simpleflow from '../../fixtures/simpleflow'; describe(': Basic Props', () => { @@ -38,7 +26,13 @@ describe(': Basic Props', () => { describe('uses nodes and edges', () => { beforeEach(() => { - cy.mount(); + cy.mount( + + ); }); it('mounts nodes and edges', () => { @@ -58,7 +52,7 @@ describe(': Basic Props', () => { describe('uses onNodesChange handler', () => { beforeEach(() => { - cy.mount(); + cy.mount(); }); it('mounts nodes', () => { @@ -90,7 +84,7 @@ describe(': Basic Props', () => { describe('uses onEdgeChange handler', () => { beforeEach(() => { - cy.mount(); + cy.mount(); }); it('mounts nodes and edges', () => { @@ -105,7 +99,7 @@ describe(': Basic Props', () => { describe('uses onConnect handlers', () => { beforeEach(() => { - cy.mount(); + cy.mount(); }); it('mounts nodes', () => { @@ -137,7 +131,7 @@ describe(': Basic Props', () => { type: 'custom', })); - cy.mount(); + cy.mount(); cy.get('.react-flow__node-custom').should('have.length', simpleflow.nodes.length); cy.get('.react-flow').contains('custom node'); @@ -149,7 +143,7 @@ describe(': Basic Props', () => { type: 'invalid', })); - cy.mount(); + cy.mount(); cy.get('.react-flow__node-invalid').should('have.length', 0); cy.get('.react-flow__node-default').should('have.length', simpleflow.nodes.length); @@ -169,7 +163,7 @@ describe(': Basic Props', () => { type: 'custom', })); - cy.mount(); + cy.mount(); cy.get('.react-flow__edge-custom').should('have.length', simpleflow.edges.length); }); @@ -180,7 +174,7 @@ describe(': Basic Props', () => { type: 'invalid', })); - cy.mount(); + cy.mount(); cy.get('.react-flow__edge-invalid').should('have.length', 0); cy.get('.react-flow__edge-default').should('have.length', simpleflow.edges.length); @@ -192,64 +186,14 @@ describe(': Basic Props', () => { backgroundColor: 'red', }; - cy.mount(); + cy.mount(); cy.get('.react-flow').should('have.css', 'background-color', 'rgb(255, 0, 0)'); }); it('uses classname', () => { - cy.mount(); + cy.mount(); cy.get('.react-flow').should('have.class', 'custom'); }); }); // test specific helper component - -function Comp({ - addOnNodeChangeHandler = false, - addOnEdgeChangeHandler = false, - addOnConnectHandler = false, - initialNodes = [], - initialEdges = [], - ...rest -}: { - initialNodes?: Node[]; - initialEdges?: Edge[]; - addOnNodeChangeHandler?: boolean; - addOnEdgeChangeHandler?: boolean; - addOnConnectHandler?: boolean; -} & Partial) { - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(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 ; -} diff --git a/examples/nextjs/cypress/components/reactflow/view-props.cy.tsx b/examples/nextjs/cypress/components/reactflow/view-props.cy.tsx index 0bfe2e2f..f6958d90 100644 --- a/examples/nextjs/cypress/components/reactflow/view-props.cy.tsx +++ b/examples/nextjs/cypress/components/reactflow/view-props.cy.tsx @@ -1,5 +1,6 @@ -import { ReactFlow, ReactFlowProps, Viewport, useViewport } from '@react-flow/bundle'; +import { ReactFlow, ReactFlowProps, Viewport, useViewport, SnapGrid, CoordinateExtent, Node } from '@react-flow/bundle'; +import ControlledFlow from '../../support/ControlledFlow'; import * as simpleflow from '../../fixtures/simpleflow'; const nodesOutsideOfView = [ @@ -21,7 +22,7 @@ const nodesOutsideOfView = [ describe(': View Props', () => { it('uses fitView', () => { - cy.mount(); + cy.mount().wait(200); cy.get('.react-flow__node:first').isWithinViewport(); }); @@ -37,7 +38,6 @@ describe(': View Props', () => { const onChangeSpy = cy.spy().as('onChangeSpy'); cy.mount(); - cy.zoomPane(10000).then(() => { expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(0.2); }); @@ -47,7 +47,6 @@ describe(': View Props', () => { const onChangeSpy = cy.spy().as('onChangeSpy'); cy.mount(); - cy.zoomPane(10000).then(() => { expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1); }); @@ -58,7 +57,6 @@ describe(': View Props', () => { const defaultViewport = { x: 0, y: 0, zoom: 0.2 }; cy.mount(); - cy.zoomPane(10000).then(() => { expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1); }); @@ -70,7 +68,6 @@ describe(': View Props', () => { const onChangeSpy = cy.spy().as('onChangeSpy'); cy.mount(); - cy.zoomPane(-10000).then(() => { expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1); }); @@ -80,7 +77,6 @@ describe(': View Props', () => { const onChangeSpy = cy.spy().as('onChangeSpy'); cy.mount(); - cy.zoomPane(-10000).then(() => { expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(2); }); @@ -91,12 +87,202 @@ describe(': View Props', () => { const defaultViewport = { x: 0, y: 0, zoom: 2 }; cy.mount(); - 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().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().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().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().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().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().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().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().then(() => { + cy.get('.react-flow__node').should('have.length', 1); + }); + }); + }); + + it('uses translateExtent', () => { + const translateExtent: CoordinateExtent = [ + [0, 0], + [1000, 1000], + ]; + + cy.mount().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() + .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 ( +
+
+ +
+
+ ); + } + + it('lets user scroll', () => { + cy.mount().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().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().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().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 diff --git a/examples/nextjs/cypress/fixtures/simpleflow.ts b/examples/nextjs/cypress/fixtures/simpleflow.ts index 0e5dea49..86cd16b0 100644 --- a/examples/nextjs/cypress/fixtures/simpleflow.ts +++ b/examples/nextjs/cypress/fixtures/simpleflow.ts @@ -4,12 +4,12 @@ export const nodes: Node[] = [ { id: '1', data: { label: 'Node 1' }, - position: { x: 250, y: 5 }, + position: { x: 0, y: 0 }, }, { id: '2', data: { label: 'Node 2' }, - position: { x: 100, y: 100 }, + position: { x: 200, y: 200 }, }, ]; diff --git a/examples/nextjs/cypress/support/ControlledFlow.tsx b/examples/nextjs/cypress/support/ControlledFlow.tsx new file mode 100644 index 00000000..3a190889 --- /dev/null +++ b/examples/nextjs/cypress/support/ControlledFlow.tsx @@ -0,0 +1,65 @@ +import { useCallback, useState } from 'react'; +import { + ReactFlow, + Node, + Edge, + NodeChange, + EdgeChange, + applyNodeChanges, + applyEdgeChanges, + Connection, + addEdge, + ReactFlowProps, +} from '@react-flow/bundle'; + +function ControlledFlow({ + addOnNodeChangeHandler = true, + addOnEdgeChangeHandler = true, + addOnConnectHandler = true, + initialNodes = [], + initialEdges = [], + ...rest +}: { + initialNodes?: Node[]; + initialEdges?: Edge[]; + addOnNodeChangeHandler?: boolean; + addOnEdgeChangeHandler?: boolean; + addOnConnectHandler?: boolean; +} & Partial) { + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(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 ; +} + +export default ControlledFlow; diff --git a/examples/nextjs/cypress/support/commands.ts b/examples/nextjs/cypress/support/commands.ts index ec20be9c..972d5d6a 100644 --- a/examples/nextjs/cypress/support/commands.ts +++ b/examples/nextjs/cypress/support/commands.ts @@ -1,25 +1,34 @@ /// Cypress.Commands.add('drag', (selector, { x, y }) => - cy.window().then((window) => - cy - .get(selector as string) - .trigger('mousedown', { which: 1, view: window }) - .trigger('mousemove', { clientX: x, clientY: y, force: true }) - .wait(50) - .trigger('mouseup', { view: window, force: true }) - ) + 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', { which: 1, 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, { which: 1, view: window }) - - .trigger('mousemove', to.x, to.y) - .trigger('mouseup', { force: true, view: window }) - ) + cy + .window() + .then((window) => + cy + .get('.react-flow__pane') + .trigger('mousedown', from.x, from.y, { which: 1, view: window }) + .trigger('mousemove', to.x, to.y) + .trigger('mouseup', { force: true, view: window }) + ) ); Cypress.Commands.add('zoomPane', (wheelDelta: number) => diff --git a/examples/nextjs/cypress/support/component.ts b/examples/nextjs/cypress/support/component.ts index 0b6cd2c8..fa281e06 100644 --- a/examples/nextjs/cypress/support/component.ts +++ b/examples/nextjs/cypress/support/component.ts @@ -39,6 +39,3 @@ declare global { } Cypress.Commands.add('mount', mount); - -// Example use: -// cy.mount() diff --git a/examples/nextjs/pages/Edges/index.tsx b/examples/nextjs/pages/Edges/index.tsx index f60ee9ab..a359abfa 100644 --- a/examples/nextjs/pages/Edges/index.tsx +++ b/examples/nextjs/pages/Edges/index.tsx @@ -175,12 +175,6 @@ const edgeTypes: EdgeTypes = { custom2: CustomEdge2, }; -const defaultViewport = { - x: 200, - y: 200, - zoom: 0.8, -}; - const EdgesFlow = () => { const [nodes, , onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -202,7 +196,6 @@ const EdgesFlow = () => { onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseLeave={onEdgeMouseLeave} - defaultViewport={defaultViewport} >