From ad37317bd1ed09b3c4e7d6c06b7bddfb81c58614 Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 4 May 2024 11:36:27 +0900 Subject: [PATCH] feat(tree): init e2e tests --- Makefile | 2 + cypress/cypress.config.ts | 10 +- cypress/package.json | 7 +- cypress/src/components/tree/Tree.cy.tsx | 174 ++++++++++++++++++++++++ packages/tree/src/Link.tsx | 1 + packages/tree/src/Node.tsx | 1 + packages/tree/src/hooks.ts | 9 +- pnpm-lock.yaml | 99 +++++++++++++- 8 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 cypress/src/components/tree/Tree.cy.tsx diff --git a/Makefile b/Makefile index fa8517ec8..7a323a8cc 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,7 @@ fmt: ##@0 global format code using prettier (js, css, md) "storybook/stories/**/*.{js,ts,tsx}" \ "cypress/src/**/*.{js,ts,tsx}" \ "scripts/*.{js,mjs}" \ + "cypress/src/**/*.{js,tsx}" \ "README.md" fmt-check: ##@0 global check if files were all formatted using prettier @@ -76,6 +77,7 @@ fmt-check: ##@0 global check if files were all formatted using prettier "storybook/stories/**/*.{js,ts,tsx}" \ "cypress/src/**/*.{js,ts,tsx}" \ "scripts/*.{js,mjs}" \ + "cypress/src/**/*.{js,tsx}" \ "README.md" test: ##@0 global run all checks/tests (packages, website) diff --git a/cypress/cypress.config.ts b/cypress/cypress.config.ts index 2a0d40c7e..f90b5622f 100644 --- a/cypress/cypress.config.ts +++ b/cypress/cypress.config.ts @@ -1,11 +1,13 @@ -import { defineConfig } from 'cypress' +import { defineConfig } from "cypress"; export default defineConfig({ + viewportWidth: 600, + viewportHeight: 600, component: { devServer: { - framework: 'create-react-app', - bundler: 'webpack', + framework: "create-react-app", + bundler: "webpack", }, video: false, }, -}) +}); diff --git a/cypress/package.json b/cypress/package.json index 8abc4343d..72fff2fdc 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -33,6 +33,7 @@ "@nivo/stream": "workspace:*", "@nivo/sunburst": "workspace:*", "@nivo/swarmplot": "workspace:*", + "@nivo/tree": "workspace:*", "@nivo/treemap": "workspace:*", "@nivo/voronoi": "workspace:*", "@nivo/waffle": "workspace:*" @@ -45,9 +46,9 @@ "node": ">=18" }, "devDependencies": { - "cypress": "^12.11.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "cypress": "^13.8.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-scripts": "^5.0.1", "typescript": "^4.9.5" }, diff --git a/cypress/src/components/tree/Tree.cy.tsx b/cypress/src/components/tree/Tree.cy.tsx new file mode 100644 index 000000000..cbb58f8ba --- /dev/null +++ b/cypress/src/components/tree/Tree.cy.tsx @@ -0,0 +1,174 @@ +import { Tree, TreeSvgProps } from '@nivo/tree' +import { before } from 'lodash' + +interface Datum { + id: string + children?: Datum[] +} + +const sampleData: Datum = { + id: 'A', + children: [ + { id: '0' }, + { + id: '1', + children: [{ id: 'A' }, { id: 'B' }], + }, + { id: '2' }, + ], +} + +const defaultProps: Pick< + TreeSvgProps, + | 'data' + | 'width' + | 'height' + | 'margin' + | 'nodeSize' + | 'activeNodeSize' + | 'inactiveNodeSize' + | 'linkThickness' + | 'activeLinkThickness' + | 'inactiveLinkThickness' + | 'animate' +> = { + data: sampleData, + width: 640, + height: 640, + margin: { + top: 20, + right: 20, + bottom: 20, + left: 20, + }, + nodeSize: 12, + activeNodeSize: 24, + inactiveNodeSize: 8, + linkThickness: 2, + activeLinkThickness: 12, + inactiveLinkThickness: 1, + animate: false, +} + +describe('', () => { + beforeEach(() => { + cy.viewport( + defaultProps.margin.left + defaultProps.width + defaultProps.margin.right, + defaultProps.margin.top + defaultProps.height + defaultProps.margin.bottom + ) + }) + + it('should render a tree graph', () => { + cy.mount( {...defaultProps} />) + + cy.get('[data-testid="node.A"]').should('exist') + cy.get('[data-testid="node.A.0"]').should('exist') + cy.get('[data-testid="node.A.1"]').should('exist') + cy.get('[data-testid="node.A.1.A"]').should('exist') + cy.get('[data-testid="node.A.1.B"]').should('exist') + cy.get('[data-testid="node.A.2"]').should('exist') + }) + + it('should highlight ancestor nodes and links', () => { + cy.mount( + + {...defaultProps} + useMesh={false} + highlightAncestorNodes={true} + highlightAncestorLinks={true} + /> + ) + + const expectations = [ + { uid: 'node.A', nodes: ['node.A'], links: [] }, + { uid: 'node.A.0', nodes: ['node.A', 'node.A.0'], links: ['link.A:A.0'] }, + { uid: 'node.A.1', nodes: ['node.A', 'node.A.1'], links: ['link.A:A.1'] }, + { + uid: 'node.A.1.A', + nodes: ['node.A', 'node.A.1', 'node.A.1.A'], + links: ['link.A:A.1', 'link.A.1:A.1.A'], + }, + { + uid: 'node.A.1.B', + nodes: ['node.A', 'node.A.1', 'node.A.1.B'], + links: ['link.A:A.1', 'link.A.1:A.1.B'], + }, + { uid: 'node.A.2', nodes: ['node.A', 'node.A.2'], links: ['link.A:A.2'] }, + ] + + for (const expectation of expectations) { + cy.get(`[data-testid="${expectation.uid}"]`).trigger('mouseover') + cy.wait(100) + + cy.get('[data-testid^="node."]').each($node => { + cy.wrap($node) + .invoke('attr', 'data-testid') + .then(testId => { + const size = expectation.nodes.includes(testId!) + ? defaultProps.activeNodeSize + : defaultProps.inactiveNodeSize + cy.wrap($node) + .invoke('attr', 'r') + .should('equal', `${size / 2}`) + }) + }) + + cy.get('[data-testid^="link."]').each($link => { + cy.wrap($link) + .invoke('attr', 'data-testid') + .then(testId => { + const thickness = expectation.links.includes(testId!) + ? defaultProps.activeLinkThickness + : defaultProps.inactiveLinkThickness + cy.wrap($link) + .invoke('attr', 'stroke-width') + .should('equal', `${thickness}`) + }) + }) + } + }) + + it('should highlight descendant nodes and links', () => { + cy.mount( + + {...defaultProps} + useMesh={false} + highlightAncestorNodes={false} + highlightAncestorLinks={false} + highlightDescendantNodes={true} + highlightDescendantLinks={true} + /> + ) + + const expectations = [ + { + uid: 'node.A', + nodes: ['node.A', 'node.A.0', 'node.A.1', 'node.A.1.A', 'node.A.1.B', 'node.A.2'], + links: [], + }, + { uid: 'node.A.0', nodes: ['node.A.0'], links: [] }, + { uid: 'node.A.1', nodes: ['node.A.1', 'node.A.1.A', 'node.A.1.B'], links: [] }, + { uid: 'node.A.1.A', nodes: ['node.A.1.A'], links: [] }, + { uid: 'node.A.1.B', nodes: ['node.A.1.B'], links: [] }, + { uid: 'node.A.2', nodes: ['node.A.2'], links: [] }, + ] + + for (const expectation of expectations) { + cy.get(`[data-testid="${expectation.uid}"]`).trigger('mouseover') + cy.wait(100) + + cy.get('[data-testid^="node."]').each($node => { + cy.wrap($node) + .invoke('attr', 'data-testid') + .then(testId => { + const size = expectation.nodes.includes(testId!) + ? defaultProps.activeNodeSize + : defaultProps.inactiveNodeSize + cy.wrap($node) + .invoke('attr', 'r') + .should('equal', `${size / 2}`) + }) + }) + } + }) +}) diff --git a/packages/tree/src/Link.tsx b/packages/tree/src/Link.tsx index f0acbcbe6..2b0bcfa2f 100644 --- a/packages/tree/src/Link.tsx +++ b/packages/tree/src/Link.tsx @@ -24,6 +24,7 @@ export const Link = ({ return ( ({ return ( size / 2)} fill={animatedProps.color} cx={animatedProps.x} diff --git a/packages/tree/src/hooks.ts b/packages/tree/src/hooks.ts index 57c328230..78d3795af 100644 --- a/packages/tree/src/hooks.ts +++ b/packages/tree/src/hooks.ts @@ -209,7 +209,7 @@ const useNodes = ({ activeNodeUids, ]) - return { ...computed, setActiveNodeUids } + return { ...computed, activeNodeUids, setActiveNodeUids } } const useLinkThicknessModifier = ( @@ -224,6 +224,7 @@ const useLinkThicknessModifier = ( const useLinks = ({ root, nodeByUid, + activeNodeUids, linkThickness, activeLinkThickness, inactiveLinkThickness, @@ -231,6 +232,7 @@ const useLinks = ({ }: { root: HierarchyTreeNode nodeByUid: Record> + activeNodeUids: string[] linkThickness: Exclude['linkThickness'], undefined> activeLinkThickness?: CommonProps['activeLinkThickness'] inactiveLinkThickness?: CommonProps['inactiveLinkThickness'] @@ -268,7 +270,7 @@ const useLinks = ({ isActive: null, } - if (activeLinkIds.length > 0) { + if (activeNodeUids.length > 0) { computedLink.isActive = activeLinkIds.includes(computedLink.id) if (computedLink.isActive) { computedLink.thickness = getActiveLinkThickness(computedLink) @@ -417,7 +419,7 @@ export const useTree = ({ const root = useRoot({ data, mode, getIdentity }) const { xScale, yScale } = useCartesianScales({ width, height, layout }) - const { nodes, nodeByUid, setActiveNodeUids } = useNodes({ + const { nodes, nodeByUid, activeNodeUids, setActiveNodeUids } = useNodes({ root, xScale, yScale, @@ -433,6 +435,7 @@ export const useTree = ({ const { links, setActiveLinkIds } = useLinks({ root, nodeByUid, + activeNodeUids, linkThickness, activeLinkThickness, inactiveLinkThickness, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbbcdb2c5..f571d532f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: '@nivo/swarmplot': specifier: workspace:* version: link:../packages/swarmplot + '@nivo/tree': + specifier: workspace:* + version: link:../packages/tree '@nivo/treemap': specifier: workspace:* version: link:../packages/treemap @@ -330,8 +333,8 @@ importers: version: link:../packages/waffle devDependencies: cypress: - specifier: ^12.11.0 - version: 12.11.0 + specifier: ^13.8.1 + version: 13.8.1 react: specifier: 18.2.0 version: 18.2.0 @@ -3532,6 +3535,30 @@ packages: uuid: 8.3.2 dev: true + /@cypress/request@3.0.1: + resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==} + engines: {node: '>= 6'} + dependencies: + aws-sign2: 0.7.0 + aws4: 1.11.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.2.1 + tough-cookie: 4.1.4 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + dev: true + /@cypress/xvfb@1.2.4(supports-color@8.1.1): resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} dependencies: @@ -11247,6 +11274,56 @@ packages: yauzl: 2.10.0 dev: true + /cypress@13.8.1: + resolution: {integrity: sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + hasBin: true + requiresBuild: true + dependencies: + '@cypress/request': 3.0.1 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.3 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.3.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.3 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.7 + debug: 4.3.4(supports-color@8.1.1) + enquirer: 2.3.6 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.3.6) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.6.0 + supports-color: 8.1.1 + tmp: 0.2.1 + untildify: 4.0.0 + yauzl: 2.10.0 + dev: true + /d3-array@1.2.4: resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} dev: false @@ -22651,6 +22728,14 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /send@0.17.2: resolution: {integrity: sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==} engines: {node: '>= 0.8.0'} @@ -24046,6 +24131,16 @@ packages: url-parse: 1.5.10 dev: true + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.8.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}