Skip to content

Commit

Permalink
fix(useReplicant): add replicant as dependency for useEffect (#7)
Browse files Browse the repository at this point in the history
* Added eslint-plugin-react-hooks. Allowed yarn to install for Node 11.

* Fixed broken tests + bug with the replicant changing being ignored.

* Increased test coverage for useReplicant.

* Copied gitignore to esliintignore and prettierignore. Fixes linter in certain situations.
  • Loading branch information
CarlosFdez authored and Hoishin committed Apr 30, 2019
1 parent 6c47d67 commit dc5521a
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 11 deletions.
65 changes: 65 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/cjs
/esm
.vscode

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
5 changes: 5 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"extends": ["@hoishin/ts", "prettier"],
"plugins": ["react-hooks"],
"parserOptions": {
"project": "tsconfig.json"
},
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
65 changes: 65 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/cjs
/esm
.vscode

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"del-cli": "^1.1.0",
"eslint": "^5.15.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-react-hooks": "^1.6.0",
"husky": "^1.3.1",
"jest": "^24.1.0",
"lint-staged": "^8.1.5",
Expand All @@ -91,7 +92,7 @@
"tslib": "^1.9.3"
},
"engines": {
"node": "^8.9.0 || ^10.13.0"
"node": "^8.9.0 || ^10.13.0 || ^11.2.0"
},
"publishConfig": {
"access": "public"
Expand Down
105 changes: 96 additions & 9 deletions src/__tests__/use-replicant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@

import {EventEmitter} from 'events';
import React from 'react';
import {render, RenderResult} from 'react-testing-library';
import {render, RenderResult, act, fireEvent} from 'react-testing-library';
import {useReplicant} from '..';
import {ReplicantOptions} from 'nodecg/types/browser';

const replicantHandler = jest.fn();
const replicantRemoveListener = jest.fn();

// Intercept mock function
class Replicant extends EventEmitter {
private _value?: any;

constructor(public name: string, initialValues: ReplicantOptions<any>) {
super();

const {defaultValue} = initialValues;
if (typeof defaultValue !== 'undefined') {
this.value = defaultValue;
}
}

on(event: string, payload: any): this {
replicantHandler(event, payload);
return super.on(event, payload);
Expand All @@ -18,44 +30,119 @@ class Replicant extends EventEmitter {
replicantRemoveListener();
return super.removeListener(event, listener);
}
set value(newValue: any) {
this._value = newValue;
this.emit('change', newValue);
}
get value(): any {
return this._value;
}
}

const replicant = new Replicant();
const replicantConstructor = jest.fn(() => replicant);
const allReplicants = new Map<string, Replicant>();
const replicantConstructor = jest.fn(
(name: string, options: ReplicantOptions<any>) => {
if (allReplicants.has(name)) {
return allReplicants.get(name);
}
const replicant = new Replicant(name, options);
allReplicants.set(name, replicant);
return replicant;
},
);

(global as any).nodecg = {
Replicant: replicantConstructor,
};

const RunnerName = (): JSX.Element => {
const [currentRun] = useReplicant('currentRun', {runner: {name: 'foo'}});
interface RunnerNameProps {
prefix?: string;
}

const RunnerName: React.FC<RunnerNameProps> = (props): JSX.Element => {
const {prefix} = props;
const repName = `${prefix || 'default'}:currentRun`;
const [currentRun] = useReplicant(repName, {runner: {name: 'foo'}});
return <div>{currentRun.runner.name}</div>;
};

// Example of a replicant with a mutating value.
const Counter: React.FC = (): JSX.Element => {
const [counter, setCounter] = useReplicant('counter', 0);
return <button onClick={() => setCounter(counter + 1)}>{counter}</button>;
};

let renderResult: RenderResult;
beforeEach(() => {
renderResult = render(<RunnerName />);
allReplicants.clear();
replicantHandler.mockReset();
replicantRemoveListener.mockReset();
});

test('Initializes replicant correctly', () => {
expect(replicantConstructor).toBeCalledWith('currentRun', {
renderResult = render(<RunnerName />);
expect(replicantConstructor).toBeCalledWith('default:currentRun', {
defaultValue: {
runner: {name: 'foo'},
},
});
});

test('Change handler is set correctly', () => {
renderResult = render(<RunnerName />);
expect(replicantHandler).toBeCalledTimes(1);
});

test.skip('Handles replicant changes', () => {
replicant.emit('change', {runner: {name: 'bar'}});
test('Change not triggered on rerender', () => {
renderResult = render(<RunnerName />);
const timesCalled = replicantHandler.mock.calls.length;
renderResult.rerender(<RunnerName />);
expect(replicantHandler).toBeCalledTimes(timesCalled);
});

test('Handles replicant name changes', () => {
renderResult = render(<RunnerName />);
const timesCalled = replicantHandler.mock.calls.length;
renderResult.rerender(<RunnerName prefix='test2' />);
expect(replicantConstructor).toBeCalledWith('test2:currentRun', {
defaultValue: {
runner: {name: 'foo'},
},
});
expect(replicantHandler).toBeCalledTimes(timesCalled + 1);
});

test('Handles replicant changes', () => {
renderResult = render(<RunnerName />);
expect(allReplicants.size).toEqual(1);
const replicant = allReplicants.values().next().value;
act(() => {
replicant.emit('change', {runner: {name: 'bar'}});
});
renderResult.rerender(<RunnerName />);
expect(renderResult.container.textContent).toBe('bar');
});

test('Can change replicant value using hook', () => {
renderResult = render(<Counter />);
expect(allReplicants.has('counter')).toBe(true);
expect(renderResult.container.firstChild).toBeTruthy();
const replicant = allReplicants.get('counter');

// We know its not undefined cause we asserted it, but we have a picky linter.
if (typeof replicant === 'undefined') return;
if (renderResult.container.firstChild === null) return;

const initialValue = replicant.value as number;
expect(replicant.value).toBe(0);

fireEvent.click(renderResult.container.firstChild as Element);
renderResult.rerender(<Counter />);
expect(replicant.value).toBe(initialValue + 1);
});

test('Unlistens when unmounted', () => {
renderResult = render(<RunnerName />);
renderResult.unmount();
expect(replicantRemoveListener).toBeCalledTimes(1);
});
2 changes: 1 addition & 1 deletion src/useReplicant/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const useReplicant = <T>(
return () => {
replicant.removeListener('change', changeHandler);
};
}, []);
}, [replicant]);

// Function to set replicant value
const updateRepValue = (newValue: T): void => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,11 @@ eslint-plugin-prefer-arrow@^1.1.4:
resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.1.5.tgz#ad2b91b126eb208547da7af9e9719b3598a5a063"
integrity sha512-vjwd/I3R0jjMbzjMxaekzHz5/ohTLparvxtW5f/7tAXvM8h4/z/C4HR6l7qv5e0JnYRSTfyzad0OiJxlnI1bBw==

eslint-plugin-react-hooks@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.0.tgz#348efcda8fb426399ac7b8609607c7b4025a6f5f"
integrity sha512-lHBVRIaz5ibnIgNG07JNiAuBUeKhEf8l4etNx5vfAEwqQ5tcuK3jV9yjmopPgQDagQb7HwIuQVsE3IVcGrRnag==

eslint-plugin-unicorn@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-7.1.0.tgz#9efef5c68fde0ebefb0241fbcfa274f1b959c04e"
Expand Down

0 comments on commit dc5521a

Please sign in to comment.