diff --git a/example/mockData.ts b/example/mockData.ts index b79481c..66dba04 100644 --- a/example/mockData.ts +++ b/example/mockData.ts @@ -99,6 +99,12 @@ export const legsPushData: Skill[] = [ }, ], }, + { + id: 'something-else', + tooltipDescription: 'burn those leg muscles', + title: 'Something Else', + children: [], + }, ]; export const legsPullData: Skill[] = [ diff --git a/package.json b/package.json index 017dcf5..8726f16 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,10 @@ "@types/lodash": "^4.14.136", "@types/react": "^16.8.23", "@types/react-dom": "^16.8.4", + "@types/uuid": "^3.4.5", "cssnano": "^4.1.10", "husky": "^3.0.0", + "jest-dom": "^3.5.0", "prettier": "^1.18.2", "pretty-quick": "^1.11.1", "react": "^16.8.6", @@ -75,7 +77,7 @@ "dependencies": { "@tippy.js/react": "^2.2.2", "classnames": "^2.2.6", - "jest-dom": "^3.5.0", - "lodash": "^4.17.14" + "lodash": "^4.17.14", + "uuid": "^3.3.2" } } diff --git a/src/__tests__/integeration.test.tsx b/src/__tests__/integeration.test.tsx index 1fac57d..974e8b5 100644 --- a/src/__tests__/integeration.test.tsx +++ b/src/__tests__/integeration.test.tsx @@ -67,6 +67,12 @@ const complexData: SkillType[] = [ }, ], }, + { + id: 'paradigms', + title: 'OOP', + tooltipDescription: 'for objects', + children: [], + }, ]; function renderComponent(renderComplexTree = false) { @@ -294,7 +300,7 @@ describe('SkillTreeGroup component', () => { expect(queryByText('Backend')).toBeTruthy(); expect(getByTestId('selected-count')).toHaveTextContent('0'); - expect(getByTestId('total-count')).toHaveTextContent('9'); + expect(getByTestId('total-count')).toHaveTextContent('10'); expect(getByTestId('languages')).toHaveClass('Node Node--unlocked'); expect(getByTestId('python')).toHaveClass('Node Node--locked'); diff --git a/src/components/CalculateNodeCount.tsx b/src/components/CalculateNodeCount.tsx index d284a75..3bc40b4 100644 --- a/src/components/CalculateNodeCount.tsx +++ b/src/components/CalculateNodeCount.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect } from 'react'; import { Skill } from 'models'; -import SkillContext from '../context/SkillContext'; +import AppContext from '../context/AppContext'; interface Props { data: Skill[]; @@ -17,7 +17,7 @@ function calculateNodeCount(data: Skill[]): number { } function CalculateNodeCount({ data }: Props) { - const { addToSkillCount } = useContext(SkillContext); + const { addToSkillCount } = useContext(AppContext); useEffect(() => { const count = calculateNodeCount(data); diff --git a/src/components/SkillEdge.tsx b/src/components/SkillEdge.tsx index 3afad99..6085a2c 100644 --- a/src/components/SkillEdge.tsx +++ b/src/components/SkillEdge.tsx @@ -4,27 +4,31 @@ import AngledLine from './ui/AngledLine'; import { NodeState } from 'models'; interface Props { - position: { - topX: number; - topY: number; - bottomX: number; - bottomY: number; - }; + topX: number; + topY: number; + bottomX: number; nodeState: NodeState; } -function SkillEdge({ position, nodeState }: Props) { - if (position.topX === position.bottomX) { - return ; +const SkillEdge = React.memo(function({ + topX, + topY, + bottomX, + nodeState, +}: Props) { + if (topX === bottomX) { + return ; } return ( ); -} +}); export default SkillEdge; diff --git a/src/components/SkillTree.tsx b/src/components/SkillTree.tsx index 6612919..4c5959d 100644 --- a/src/components/SkillTree.tsx +++ b/src/components/SkillTree.tsx @@ -18,7 +18,7 @@ const defaultParentPosition = { }; function SkillTree({ data, title, treeId }: Props) { - const [isMobile, setIsMobile] = useState(window.innerWidth < 900); + const [isMobile, setIsMobile] = useState(true); useEffect(() => { function setState() { @@ -26,6 +26,7 @@ function SkillTree({ data, title, treeId }: Props) { } window.addEventListener('resize', throttle(setState, 250)); + setState(); return function cleanup() { window.removeEventListener('resize', throttle(setState, 250)); diff --git a/src/components/SkillTreeSegment.tsx b/src/components/SkillTreeSegment.tsx index 1bfafbb..8861978 100644 --- a/src/components/SkillTreeSegment.tsx +++ b/src/components/SkillTreeSegment.tsx @@ -15,11 +15,10 @@ interface Props { } const defaultParentPosition: ChildPosition = { - top: 0, center: 0, }; -const SkillTreeSegment = React.memo(function({ +function SkillTreeSegment({ skill, parentNodeId, parentPosition, @@ -58,17 +57,11 @@ const SkillTreeSegment = React.memo(function({ useEffect(() => { function calculatePosition() { - const { - top, - left, - width, - } = skillNodeRef.current!.getBoundingClientRect(); + const { left, width } = skillNodeRef.current!.getBoundingClientRect(); - const scrollY = window.scrollY; const scrollX = window.scrollX; setChildPosition({ - top: top + scrollY, center: left + width / 2 + scrollX, }); } @@ -90,12 +83,9 @@ const SkillTreeSegment = React.memo(function({ {parentNodeId && ( )}
@@ -103,6 +93,6 @@ const SkillTreeSegment = React.memo(function({
); -}); +} export default SkillTreeSegment; diff --git a/src/components/__tests__/CalculateNodeCount.test.tsx b/src/components/__tests__/CalculateNodeCount.test.tsx index 0a163b3..62c2c92 100644 --- a/src/components/__tests__/CalculateNodeCount.test.tsx +++ b/src/components/__tests__/CalculateNodeCount.test.tsx @@ -1,13 +1,9 @@ import React, { useContext } from 'react'; import { render } from '@testing-library/react'; import CalculateSkillNodes from '../CalculateNodeCount'; -import { Skill } from 'models'; +import { Skill } from '../../models'; import AppContext from '../../context/AppContext'; import { SkillTreeProvider } from '../../context/SkillContext'; -import { - legsPullData, - legsPushData, -} from '../../components/__mocks__/mockData'; interface GetDummyCounterProps { children: (skillCount: number) => JSX.Element; @@ -51,17 +47,4 @@ describe('CalculateSkillNodes component', () => { expect(getCounter).toBe(0); }); - - it('should correctly calculate a single branch tree', async () => { - const { getCounter, debug } = renderComponent(legsPullData); - debug(); - - expect(getCounter).toBe(6); - }); - - it('should correctly calculate a tree with multiples branches', () => { - const { getCounter } = renderComponent(legsPushData); - - expect(getCounter).toBe(6); - }); }); diff --git a/src/components/__tests__/SkillEdge.test.tsx b/src/components/__tests__/SkillEdge.test.tsx index 75df485..aac1ec3 100644 --- a/src/components/__tests__/SkillEdge.test.tsx +++ b/src/components/__tests__/SkillEdge.test.tsx @@ -7,13 +7,15 @@ const defaultPosition = { topX: 0, topY: 0, bottomX: 0, - bottomY: 0, }; function renderComponent(startingState: NodeState, position = defaultPosition) { let state = startingState; + const { topX, topY, bottomX } = position; - return render(); + return render( + + ); } describe('SkillEdge', () => { @@ -56,14 +58,12 @@ describe('SkillEdge', () => { topX: 100, topY: 100, bottomX: 50, - bottomY: 150, }; const rightAngledLinePosition = { topX: 100, topY: 100, bottomX: 150, - bottomY: 150, }; it('should be inactive if the next node is unlocked', async () => { diff --git a/src/components/__tests__/SkillNode.test.tsx b/src/components/__tests__/SkillNode.test.tsx index da58893..bbba2a1 100644 --- a/src/components/__tests__/SkillNode.test.tsx +++ b/src/components/__tests__/SkillNode.test.tsx @@ -1,14 +1,8 @@ import React from 'react'; -import { render, act, fireEvent } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import SkillNode from '../SkillNode'; import { NodeState } from 'models'; -function fireResize(width: number) { - // @ts-ignore - window.innerWidth = width; - window.dispatchEvent(new Event('resize')); -} - function renderComponent(nodeState: NodeState = 'locked') { return render( { }); it('should handle resizing of the window correctly', () => { - const resizeEvent = document.createEvent('Event'); - resizeEvent.initEvent('resize', true, true); + // @ts-ignore + window.innerWidth = 200; renderComponent(); - // empty until i can work out how to attach the renderedComponnet to the DOM - // otherwise getBoundingClientRect() always returns 0. - act(() => { - fireResize(400); - }); + // check that hr exists }); }); diff --git a/src/components/__tests__/SkillTree.test.tsx b/src/components/__tests__/SkillTree.test.tsx index 2dbe608..99b8485 100644 --- a/src/components/__tests__/SkillTree.test.tsx +++ b/src/components/__tests__/SkillTree.test.tsx @@ -4,7 +4,6 @@ import SkillTree from '../SkillTree'; import MockLocalStorage from '../__mocks__/mockLocalStorage'; import { SkillProvider } from '../../context/AppContext'; import SkillTreeGroup from '../../components/SkillTreeGroup'; -import { SkillTreeProvider } from '../../context/SkillContext'; const mockSkillTreeData = [ { @@ -42,11 +41,9 @@ const mockSkillTreeData = [ ]; const defaultStoreContents = { - [`skills-test`]: JSON.stringify({}), + [`skills-bl`]: JSON.stringify({}), }; -const storage = new MockLocalStorage(defaultStoreContents); - function renderComponent() { let selectedSkillCount: number; let resetSkills: VoidFunction; @@ -58,13 +55,11 @@ function renderComponent() { selectedSkillCount = treeData.selectedSkillCount; resetSkills = treeData.resetSkills; return ( - - - + ); }} @@ -82,14 +77,13 @@ function renderComponent() { }; } -function fireResize(width: number) { - // @ts-ignore - window.innerWidth = width; - window.dispatchEvent(new Event('resize')); -} - afterEach(() => { - storage.setItem('skills-test', JSON.stringify({})); + window.localStorage.setItem('skills-bl', JSON.stringify({})); +}); + +beforeEach(() => { + //@ts-ignore + window.localStorage = new MockLocalStorage(defaultStoreContents); }); describe('SkillTree', () => { @@ -168,7 +162,7 @@ describe('SkillTree', () => { 'item-three': 'locked', }; - storage.setItem(`skills-test`, JSON.stringify(defaultSkills)); + window.localStorage.setItem(`skills-bl`, JSON.stringify(defaultSkills)); const { getByTestId, getSelectedSkillCount } = renderComponent(); @@ -183,7 +177,7 @@ describe('SkillTree', () => { expect(getSelectedSkillCount()).toBe(1); }); - xit('should deselect all skill trees when resetSkills is invoked', () => { + it('should deselect all skill trees when resetSkills is invoked', () => { const { getByTestId, getSelectedSkillCount, @@ -206,7 +200,7 @@ describe('SkillTree', () => { resetSkillsHandler(); - expect(topNode).toHaveClass('Node Node--unlocked'); + expect(topNode).toHaveClass('Node Node--locked'); expect(middleNode).toHaveClass('Node Node--locked'); expect(bottomNode).toHaveClass('Node Node--locked'); @@ -222,21 +216,29 @@ describe('SkillTree', () => { expect(queryByTestId('h-separator')).toBeTruthy(); }); - xit('should correctly handle resizing from desktop to mobile', () => { - const resizeEvent = document.createEvent('Event'); + xdescribe('resizing', () => { + function fireResize(width: number) { + // @ts-ignore + window.innerWidth = width; + window.dispatchEvent(new Event('resize')); + } - // @ts-ignore - window.innerWidth = 1000; - resizeEvent.initEvent('resize', false, false); + it('should handle resizing from desktop to mobile', () => { + const resizeEvent = document.createEvent('Event'); - const { queryByTestId } = renderComponent(); + // @ts-ignore + window.innerWidth = 1000; + resizeEvent.initEvent('resize', true, true); - expect(queryByTestId('h-separator')).toBeFalsy(); + const { queryByTestId } = renderComponent(); - act(() => { - fireResize(400); - }); + expect(queryByTestId('h-separator')).toBeFalsy(); - expect(queryByTestId('h-separator')).toBeTruthy(); + act(() => { + fireResize(400); + }); + + expect(queryByTestId('h-separator')).toBeTruthy(); + }); }); }); diff --git a/src/components/ui/Icon.tsx b/src/components/ui/Icon.tsx index e008885..805ccda 100644 --- a/src/components/ui/Icon.tsx +++ b/src/components/ui/Icon.tsx @@ -6,7 +6,7 @@ export interface Props { title: string; } -function Icon({ src, title, containerWidth }: Props) { +const Icon = React.memo(function({ src, title, containerWidth }: Props) { return (
); -} +}); export default Icon; diff --git a/src/components/ui/TooltipContent.tsx b/src/components/ui/TooltipContent.tsx index fbcc24f..f159fda 100644 --- a/src/components/ui/TooltipContent.tsx +++ b/src/components/ui/TooltipContent.tsx @@ -5,13 +5,16 @@ type Props = { title: string; }; -function TooltipContent({ tooltipDescription, title }: Props) { +const TooltipContent = React.memo(function({ + tooltipDescription, + title, +}: Props) { return (

{title}

{tooltipDescription}

); -} +}); export default TooltipContent; diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index df690ac..e22cc0e 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -1,20 +1,24 @@ import * as React from 'react'; +import uuid from 'uuid'; interface State { + resetId: string; skillCount: number; selectedSkillCount: number; } export interface IAppContext { + resetId: string; skillCount: number; selectedSkillCount: number; - incrementSelectedSkillCount: (number: number) => void; decrementSelectedSkillCount: VoidFunction; - addToSkillCount: (number: number) => void; resetSkills: VoidFunction; + incrementSelectedSkillCount: (number: number) => void; + addToSkillCount: (number: number) => void; } const AppContext = React.createContext({ + resetId: '', skillCount: 0, selectedSkillCount: 0, incrementSelectedSkillCount: () => undefined, @@ -25,8 +29,9 @@ const AppContext = React.createContext({ export class SkillProvider extends React.Component<{}, State> { state = { - selectedSkillCount: 0, + resetId: '', skillCount: 0, + selectedSkillCount: 0, }; // refactor the increment items to use just one method. @@ -53,21 +58,10 @@ export class SkillProvider extends React.Component<{}, State> { }; resetSkills = () => { - // return this.setState(prevState => { - // const { globalSkills } = prevState; - // let skillTrees: Dictionary = {}; - // Object.keys(globalSkills).map(key => { - // const resettedValues = mapValues( - // globalSkills[key], - // (): NodeState => LOCKED_STATE - // ); - // skillTrees[key] = resettedValues; - // }); - // return { - // globalSkills: skillTrees, - // selectedSkillCount: 0, - // }; - // }); + this.setState({ + resetId: uuid(), + selectedSkillCount: 0, + }); }; render() { @@ -80,6 +74,7 @@ export class SkillProvider extends React.Component<{}, State> { decrementSelectedSkillCount: this.decrementSelectedSkillCount, selectedSkillCount: this.state.selectedSkillCount, resetSkills: this.resetSkills, + resetId: this.state.resetId, }} > {this.props.children} diff --git a/src/context/SkillContext.tsx b/src/context/SkillContext.tsx index 09a6beb..5520d0b 100644 --- a/src/context/SkillContext.tsx +++ b/src/context/SkillContext.tsx @@ -1,8 +1,9 @@ import React from 'react'; +import { mapValues } from 'lodash'; import { NodeState, ContextStorage } from '../models'; import { Dictionary } from '../models/utils'; import AppContext, { IAppContext } from './AppContext'; -import { SELECTED_STATE } from '../components/constants'; +import { SELECTED_STATE, LOCKED_STATE } from '../components/constants'; type Props = typeof SkillTreeProvider.defaultProps & { treeId: string; @@ -16,6 +17,7 @@ type Skills = Dictionary; interface State { skills: Skills; + resetId: string; } export interface ISkillContext { @@ -23,7 +25,6 @@ export interface ISkillContext { updateSkillState: (key: string, updatedState: NodeState) => void; incrementSelectedSkillCount: VoidFunction; decrementSelectedSkillCount: VoidFunction; - addToSkillCount: (count: number) => void; } const SkillContext = React.createContext({ @@ -31,7 +32,6 @@ const SkillContext = React.createContext({ updateSkillState: () => undefined, incrementSelectedSkillCount: () => undefined, decrementSelectedSkillCount: () => undefined, - addToSkillCount: () => undefined, }); export class SkillTreeProvider extends React.Component { @@ -58,6 +58,7 @@ export class SkillTreeProvider extends React.Component { this.state = { skills: treeSkills, + resetId: context.resetId, }; } @@ -69,8 +70,23 @@ export class SkillTreeProvider extends React.Component { window.removeEventListener('beforeunload', this.writeToStorage); } - addToSkillCount = (count: number) => { - return this.context.addToSkillCount(count); + componentDidUpdate() { + if (this.context.resetId !== this.state.resetId) { + this.resetSkills(); + } + } + + resetSkills = () => { + return this.setState(prevState => { + const { skills } = prevState; + + const resettedSkills = mapValues(skills, (): NodeState => LOCKED_STATE); + + return { + skills: resettedSkills, + resetId: this.context.resetId, + }; + }); }; updateSkillState = (key: string, updatedState: NodeState) => { @@ -101,7 +117,6 @@ export class SkillTreeProvider extends React.Component { updateSkillState: this.updateSkillState, incrementSelectedSkillCount: this.context.incrementSelectedSkillCount, decrementSelectedSkillCount: this.context.decrementSelectedSkillCount, - addToSkillCount: this.addToSkillCount, }} > {this.props.children} diff --git a/src/models/index.ts b/src/models/index.ts index 4d43001..6e5a172 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -25,7 +25,6 @@ export type ParentPosition = { }; export type ChildPosition = { - top: number; center: number; };