From fcfd66c555677d022a33d6f62c1c25fabef5a1ad Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Sat, 29 Oct 2022 18:05:26 +0200 Subject: [PATCH] test: port tests to ts (#253) --- @types/@schwingbat/relative-angle/index.d.ts | 8 +++++ @types/jest/index.d.ts | 12 +++++++ jest.setup.js => jest.setup.ts | 2 +- package-lock.json | 32 +++++++++++++++++++ package.json | 7 ++-- src/__tests__/Chart.test.tsx | 22 ++++++++----- src/__tests__/Label.test.tsx | 29 ++++++++--------- src/__tests__/Path.test.tsx | 26 ++++++++------- .../{getArcCenter.js => getArcCenter.ts} | 15 ++++++++- .../{getArcInfo.js => getArcInfo.ts} | 22 ++++++++++--- .../testUtils/{index.js => index.ts} | 0 .../testUtils/{render.js => render.tsx} | 22 +++++++++---- tsconfig.test.json | 2 +- 13 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 @types/@schwingbat/relative-angle/index.d.ts create mode 100644 @types/jest/index.d.ts rename jest.setup.js => jest.setup.ts (87%) rename src/__tests__/testUtils/{getArcCenter.js => getArcCenter.ts} (89%) rename src/__tests__/testUtils/{getArcInfo.js => getArcInfo.ts} (71%) rename src/__tests__/testUtils/{index.js => index.ts} (100%) rename src/__tests__/testUtils/{render.js => render.tsx} (53%) diff --git a/@types/@schwingbat/relative-angle/index.d.ts b/@types/@schwingbat/relative-angle/index.d.ts new file mode 100644 index 00000000..da579449 --- /dev/null +++ b/@types/@schwingbat/relative-angle/index.d.ts @@ -0,0 +1,8 @@ +declare module '@schwingbat/relative-angle'; + +type Point = { + x: number; + y: number; +}; + +export function degrees(objectCoords: Point, targetCoords: Point): number; diff --git a/@types/jest/index.d.ts b/@types/jest/index.d.ts new file mode 100644 index 00000000..d96ca36b --- /dev/null +++ b/@types/jest/index.d.ts @@ -0,0 +1,12 @@ +declare global { + namespace jest { + interface Expect { + toEqualWithRoundingError(expected: number): any; + } + interface Matchers { + toEqualWithRoundingError(expected: number): R; + } + } +} + +export {}; diff --git a/jest.setup.js b/jest.setup.ts similarity index 87% rename from jest.setup.js rename to jest.setup.ts index bf2110d3..ee5237b8 100644 --- a/jest.setup.js +++ b/jest.setup.ts @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/extend-expect'; // https://stackoverflow.com/a/53464807/2902821 expect.extend({ - toEqualWithRoundingError(actual, expected, decimals = 12) { + toEqualWithRoundingError(actual: number, expected: number, decimals = 12) { const pass = Math.abs(expected - actual) < Math.pow(10, -decimals) / 2; if (pass) { return { diff --git a/package-lock.json b/package-lock.json index ffafded7..59eb38d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "react-minimal-pie-chart", "version": "8.3.0", "license": "MIT", + "dependencies": { + "@types/svg-path-parser": "^1.1.3" + }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-react-jsx": "^7.12.12", @@ -24,6 +27,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^12.0.0", "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.18", "all-contributors-cli": "^6.19.0", "babel-jest": "^27.1.0", "babel-loader": "^8.2.2", @@ -5165,6 +5169,15 @@ "@types/reactcss": "*" } }, + "node_modules/@types/react-dom": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz", + "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==", + "dev": true, + "dependencies": { + "@types/react": "^17" + } + }, "node_modules/@types/react-syntax-highlighter": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", @@ -5204,6 +5217,11 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/svg-path-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/svg-path-parser/-/svg-path-parser-1.1.3.tgz", + "integrity": "sha512-F1Y6lQIto5b2sKCseVUsFfY5J+8PIhhX4jrDVxpth4m7hwM2OdySh3iTLeR35lEhl/K4ZMEF+GDAwTl7yJcO5Q==" + }, "node_modules/@types/tapable": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", @@ -29281,6 +29299,15 @@ "@types/reactcss": "*" } }, + "@types/react-dom": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz", + "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==", + "dev": true, + "requires": { + "@types/react": "^17" + } + }, "@types/react-syntax-highlighter": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", @@ -29320,6 +29347,11 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/svg-path-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/svg-path-parser/-/svg-path-parser-1.1.3.tgz", + "integrity": "sha512-F1Y6lQIto5b2sKCseVUsFfY5J+8PIhhX4jrDVxpth4m7hwM2OdySh3iTLeR35lEhl/K4ZMEF+GDAwTl7yJcO5Q==" + }, "@types/tapable": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", diff --git a/package.json b/package.json index 6fba719e..67b1d6b5 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "react": "^16.8.0 || ^17.0.0 || ^18", "react-dom": "^16.8.0 || ^17.0.0 || ^18" }, - "dependencies": {}, + "dependencies": { + "@types/svg-path-parser": "^1.1.3" + }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-react-jsx": "^7.12.12", @@ -74,6 +76,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^12.0.0", "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.18", "all-contributors-cli": "^6.19.0", "babel-jest": "^27.1.0", "babel-loader": "^8.2.2", @@ -105,7 +108,7 @@ "!**/__tests__/**" ], "setupFilesAfterEnv": [ - "/jest.setup.js" + "/jest.setup.ts" ] }, "size-limit": [ diff --git a/src/__tests__/Chart.test.tsx b/src/__tests__/Chart.test.tsx index 1cca96c8..8bdccaae 100644 --- a/src/__tests__/Chart.test.tsx +++ b/src/__tests__/Chart.test.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import React from 'react'; import { act, render, dataMock, getArcInfo } from './testUtils'; import { degreesToRadians, extractPercentage } from '../utils'; @@ -7,7 +6,8 @@ import { pieChartDefaultProps } from '../../src'; jest.useFakeTimers(); beforeAll(() => { - global.requestAnimationFrame = (callback) => { + /// @ts-expect-error: this is a partial mock + global.requestAnimationFrame = (callback: () => void) => { callback(); return 'id'; }; @@ -22,6 +22,8 @@ describe('Chart', () => { children: , }); const root = container.firstChild; + + // @ts-expect-error: ChildNode type doesn't have tagName prop expect(root.tagName).toBe('svg'); expect(root).toHaveClass('foo'); expect(root).toHaveStyle({ color: 'green' }); @@ -118,7 +120,7 @@ describe('Chart', () => { lengthAngle: 200, background: 'green', }); - const paths = container.querySelectorAll('path'); + const paths = Array.from(container.querySelectorAll('path')); const [background, segment] = paths; const backgroundInfo = getArcInfo(background); const segmentInfo = getArcInfo(segment); @@ -145,7 +147,7 @@ describe('Chart', () => { background: 'green', rounded: true, }); - const paths = container.querySelectorAll('path'); + const paths = Array.from(container.querySelectorAll('path')); const [background] = paths; expect(paths.length).toBe(dataMock.length + 1); expect(background).toHaveAttribute('stroke-linecap', 'round'); @@ -193,11 +195,11 @@ describe('Chart', () => { const fullPathLength = degreesToRadians(segmentRadius) * lengthAngle; let hiddenPercentage; const initialProps = { - data: [...dataMock[0]], + data: [dataMock[0]], animate: true, reveal, }; - const { container, rerender } = render(initialProps); + const { container, reRender } = render(initialProps); const path = container.querySelector('path'); // Paths are hidden @@ -223,7 +225,7 @@ describe('Chart', () => { // Update reveal prop after initial animation const newReveal = 77; - rerender({ + reRender({ ...initialProps, reveal: newReveal, }); @@ -251,6 +253,7 @@ describe('Chart', () => { jest.runAllTimers(); }); + // @ts-expect-error: This is a Jest mocke console.error.mockRestore(); expect(consoleError).not.toHaveBeenCalled(); }); @@ -258,7 +261,10 @@ describe('Chart', () => { describe('stroke-dashoffset attribute', () => { it("doesn't generate zero rounding issues after animation (GitHub: #133)", () => { const { container } = render({ - data: [{ value: 1 }, { value: 1.6 }], + data: [ + { value: 1, color: 'red' }, + { value: 1.6, color: 'blue' }, + ], animate: true, }); diff --git a/src/__tests__/Label.test.tsx b/src/__tests__/Label.test.tsx index b77349ac..a4405ce2 100644 --- a/src/__tests__/Label.test.tsx +++ b/src/__tests__/Label.test.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import React from 'react'; import { render, dataMock, getArcInfo } from './testUtils'; import { @@ -6,9 +5,9 @@ import { extractPercentage, shiftVectorAlongAngle, } from '../utils'; -import { pieChartDefaultProps } from '../../src'; +import { pieChartDefaultProps, PieChartProps } from '../../src'; -function getExpectedLabelRenderProps(dataEntry) { +function getExpectedLabelRenderProps(dataEntry: PieChartProps['data'][number]) { return { x: expect.any(Number), y: expect.any(Number), @@ -43,13 +42,13 @@ describe('Label', () => { describe('returning a value', () => { const labels = [0, null, 'label']; describe.each` - description | label | expectedLabels - ${'number'} | ${() => -5} | ${[-5, -5, -5]} - ${'number'} | ${({ dataIndex }) => dataIndex} | ${[0, 1, 2]} - ${'string'} | ${() => 'label'} | ${['label', 'label', 'label']} - ${'null'} | ${() => null} | ${[]} - ${'undefined'} | ${() => undefined} | ${[]} - ${'mixed'} | ${({ dataIndex }) => labels[dataIndex]} | ${[0, 'label']} + description | label | expectedLabels + ${'number'} | ${() => -5} | ${[-5, -5, -5]} + ${'number'} | ${({ dataIndex }: { dataIndex: number }) => dataIndex} | ${[0, 1, 2]} + ${'string'} | ${() => 'label'} | ${['label', 'label', 'label']} + ${'null'} | ${() => null} | ${[]} + ${'undefined'} | ${() => undefined} | ${[]} + ${'mixed'} | ${({ dataIndex }: { dataIndex: number }) => labels[dataIndex]} | ${[0, 'label']} `('$description', ({ label, expectedLabels }) => { it('renders expected elements with expected content', () => { const { container } = render({ label }); @@ -72,7 +71,7 @@ describe('Label', () => { }); container.querySelectorAll('text').forEach((label, index) => { - expect(label).toHaveTextContent(index); + expect(label).toHaveTextContent(`${index}`); }); }); }); @@ -83,9 +82,9 @@ describe('Label', () => { const labelPosition = 5; const expectedDistanceFromCenter = extractPercentage(radius, labelPosition); describe.each` - description | segmentsShift | expectedSegmentsShift - ${'as number'} | ${1} | ${[1, 1, 1]} - ${'as function'} | ${(index) => index} | ${[0, 1, 2]} + description | segmentsShift | expectedSegmentsShift + ${'as number'} | ${1} | ${[1, 1, 1]} + ${'as function'} | ${(index: number) => index} | ${[0, 1, 2]} `( '+ segmentShift $description', ({ segmentsShift, expectedSegmentsShift }) => { @@ -183,7 +182,7 @@ describe('Label', () => { 'Label $description', ({ labelPosition, startAngle, expectedAlignment }) => { const { getByText } = render({ - data: [{ value: 1 }], + data: [{ value: 1, color: 'red' }], lineWidth, lengthAngle, startAngle, diff --git a/src/__tests__/Path.test.tsx b/src/__tests__/Path.test.tsx index 716f1ce0..da6db60a 100644 --- a/src/__tests__/Path.test.tsx +++ b/src/__tests__/Path.test.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import { render, dataMock, getArcInfo, fireEvent } from './testUtils'; import { bisectorAngle, @@ -19,7 +18,10 @@ describe('Path', () => { it('get empty "d" attributes when data values sum equals 0', () => { const { container } = render({ - data: [{ value: 0 }, { value: 0 }], + data: [ + { value: 0, color: 'red' }, + { value: 0, color: 'green' }, + ], }); const paths = container.querySelectorAll('path'); expect(paths.length).toEqual(2); @@ -51,11 +53,11 @@ describe('Path', () => { describe('segmentsStyle prop', () => { describe.each` - description | segmentsStyle | expectedStyle - ${'undefined'} | ${undefined} | ${null} - ${'as object'} | ${{ color: 'green' }} | ${{ color: 'green' }} - ${'as function'} | ${(i) => ({ color: 'green' })} | ${{ color: 'green' }} - ${'as function'} | ${jest.fn((i) => undefined)} | ${null} + description | segmentsStyle | expectedStyle + ${'undefined'} | ${undefined} | ${null} + ${'as object'} | ${{ color: 'green' }} | ${{ color: 'green' }} + ${'as function'} | ${(index: number) => ({ color: 'green' })} | ${{ color: 'green' }} + ${'as function'} | ${jest.fn((i) => undefined)} | ${null} `('$description', ({ segmentsStyle, expectedStyle }) => { if (jest.isMockFunction(segmentsStyle)) { it('gets called with expected arguments', () => { @@ -86,10 +88,10 @@ describe('Path', () => { * 3- Compare shifted and non-shifted segments info */ describe.each` - description | segmentsShift | expectedSegmentsShift - ${'as number'} | ${1} | ${[1, 1, 1]} - ${'as function'} | ${(index) => index} | ${[0, 1, 2]} - ${'as function'} | ${jest.fn()} | ${[0, 0, 0]} + description | segmentsShift | expectedSegmentsShift + ${'as number'} | ${1} | ${[1, 1, 1]} + ${'as function'} | ${(index: number) => index} | ${[0, 1, 2]} + ${'as function'} | ${jest.fn()} | ${[0, 0, 0]} `('$description', ({ segmentsShift, expectedSegmentsShift }) => { if (jest.isMockFunction(segmentsShift)) { it('gets called with expected arguments', () => { @@ -158,7 +160,7 @@ describe('Path', () => { describe('reveal prop', () => { const pathLength = degreesToRadians(25) * 360; - const singleEntryDataMock = [...dataMock[0]]; + const singleEntryDataMock = [dataMock[0]]; describe('undefined', () => { it('render a fully revealed path without "strokeDasharray" nor "strokeDashoffset"', () => { diff --git a/src/__tests__/testUtils/getArcCenter.js b/src/__tests__/testUtils/getArcCenter.ts similarity index 89% rename from src/__tests__/testUtils/getArcCenter.js rename to src/__tests__/testUtils/getArcCenter.ts index a97421b6..45232fcf 100644 --- a/src/__tests__/testUtils/getArcCenter.js +++ b/src/__tests__/testUtils/getArcCenter.ts @@ -1,7 +1,20 @@ // https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc // https://github.com/Ghostkeeper/SVGToolpathReader/blob/a2bbe90da64e6cd9d54fec553f61ba941001e85d/Parser.py#L493 // @TODO Find a more reliable solution -export function getArcCenter(x1, y1, rx, ry, phi, fA, fS, x2, y2) { +export function getArcCenter( + x1: number, + y1: number, + rx: number, + ry: number, + phi: number, + fA: number, + fS: number, + x2: number, + y2: number +): { + x: number; + y: number; +} { var cx, cy; if (rx < 0) { diff --git a/src/__tests__/testUtils/getArcInfo.js b/src/__tests__/testUtils/getArcInfo.ts similarity index 71% rename from src/__tests__/testUtils/getArcInfo.js rename to src/__tests__/testUtils/getArcInfo.ts index 08447bd7..ebcedd10 100644 --- a/src/__tests__/testUtils/getArcInfo.js +++ b/src/__tests__/testUtils/getArcInfo.ts @@ -1,9 +1,14 @@ -import parseSVG from 'svg-path-parser'; +import { parseSVG } from 'svg-path-parser'; import { degrees as getDegrees } from '@schwingbat/relative-angle'; import { getArcCenter } from './getArcCenter'; -function getAbsoluteAngle(radius, point) { - const relativeAngle = getDegrees(radius, point); +type Point = { + x: number; + y: number; +}; + +function getAbsoluteAngle(center: Point, point: Point): number { + const relativeAngle = getDegrees(center, point); if (relativeAngle < 0) { return 360 + relativeAngle; } @@ -15,10 +20,19 @@ function getAbsoluteAngle(radius, point) { * - Paths with non-integer center/startAngle/lengthAngle values * generate respective values with rounding issues */ -export function getArcInfo(element) { +export function getArcInfo(element: Element) { const d = element.getAttribute('d'); + + if (!d) { + throw new Error('Provided element must have a "d" attribute'); + } + const [moveto, arc] = parseSVG(d); + if (moveto.command !== 'moveto' || arc.command !== 'elliptical arc') { + throw new Error('Provided path is not the section of a circumference'); + } + if (arc.rx !== arc.ry) { throw new Error('Provided path is not the section of a circumference'); } diff --git a/src/__tests__/testUtils/index.js b/src/__tests__/testUtils/index.ts similarity index 100% rename from src/__tests__/testUtils/index.js rename to src/__tests__/testUtils/index.ts diff --git a/src/__tests__/testUtils/render.js b/src/__tests__/testUtils/render.tsx similarity index 53% rename from src/__tests__/testUtils/render.js rename to src/__tests__/testUtils/render.tsx index b34d283f..59eb3b81 100644 --- a/src/__tests__/testUtils/render.js +++ b/src/__tests__/testUtils/render.tsx @@ -1,8 +1,13 @@ import React from 'react'; -import { render as TLRender, act, fireEvent } from '@testing-library/react'; +import { + render as TLRender, + act, + fireEvent, + RenderResult, +} from '@testing-library/react'; // @NOTE this import must finish with "/src" to allow test runner // to remap it against bundled artefacts (npm run test:bundles:unit) -import { PieChart } from '../../../src'; +import { PieChart, PieChartProps } from '../..'; const dataMock = [ { value: 10, color: 'blue' }, @@ -10,15 +15,18 @@ const dataMock = [ { value: 20, color: 'green' }, ]; -function render(props) { +function render( + props?: Partial +): RenderResult & { reRender: (props?: Partial) => void } { const defaultProps = { data: dataMock }; const instance = TLRender(); // Uniform rerender to render's API - const { rerender } = instance; - instance.rerender = (props) => - rerender(); - return instance; + return { + ...instance, + reRender: (props?: Partial) => + instance.rerender(), + }; } export { PieChart, act, dataMock, render, fireEvent }; diff --git a/tsconfig.test.json b/tsconfig.test.json index ef43e7f3..6b2a5f18 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,5 +4,5 @@ "emitDeclarationOnly": false, "noEmit": true }, - "include": ["./src", "./stories"] + "include": ["./src", "./stories", "@types"] }