diff --git a/.eslintrc.json b/.eslintrc.json index 73a0106..914427b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,10 +53,11 @@ "error", { "allow": [ - "_name", + "__RR__", "_isMounted", "_isMounting", - "__RR__" + "_isRendering", + "_name" ] } ], diff --git a/src/collect.tsx b/src/collect.tsx index da7e31e..e2ea842 100644 --- a/src/collect.tsx +++ b/src/collect.tsx @@ -104,6 +104,10 @@ const collect = >( private _isMounting = true; + // will trigger multiple renders, + // we must disregard these + private _isRendering = false; + _name = componentName; static displayName = `Collected(${componentName})`; @@ -114,11 +118,13 @@ const collect = >( // Stop recording. For first render() stopRecordingGetsForComponent(); + this._isRendering = false; } componentDidUpdate() { // Stop recording. For not-first render() stopRecordingGetsForComponent(); + this._isRendering = false; } componentWillUnmount() { @@ -137,7 +143,10 @@ const collect = >( } render() { - startRecordingGetsForComponent(this); + if (!this._isRendering) { + startRecordingGetsForComponent(this); + this._isRendering = true; + } const props = { ...this.props, diff --git a/tests/integration/TaskListTest/TaskList.test.tsx b/tests/integration/TaskListTest/TaskList.test.tsx index 4c609ee..5ed4591 100644 --- a/tests/integration/TaskListTest/TaskList.test.tsx +++ b/tests/integration/TaskListTest/TaskList.test.tsx @@ -1,12 +1,15 @@ import React from 'react'; -import { render } from '@testing-library/react'; import TaskList from './TaskList'; import { store } from '../../../src'; +import * as testUtils from '../../testUtils'; it('TaskList', async () => { - const { findByText, getByText, queryByText, getByLabelText } = render( - - ); + const { + findByText, + getByText, + queryByText, + getByLabelText, + } = testUtils.renderStrict(); // it should render a loading indicator getByText('Loading...'); diff --git a/tests/integration/TaskListTest/isolation.test.tsx b/tests/integration/TaskListTest/isolation.test.tsx index 1364641..0145d5f 100644 --- a/tests/integration/TaskListTest/isolation.test.tsx +++ b/tests/integration/TaskListTest/isolation.test.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import * as testUtils from '../../testUtils'; import App from './App'; import { store } from '../../../src'; @@ -29,7 +29,7 @@ const props = { }; it('should handle isolation', () => { - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); // should render the title getByText('The task list site'); diff --git a/tests/integration/basicClassComponent.test.tsx b/tests/integration/basicClassComponent.test.tsx index 4ddcaa0..cb06734 100644 --- a/tests/integration/basicClassComponent.test.tsx +++ b/tests/integration/basicClassComponent.test.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import { render } from '@testing-library/react'; import { collect, store, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; // eslint-disable-next-line react/prefer-stateless-function class RawClassComponent extends Component { @@ -27,7 +27,7 @@ store.title = 'The initial title'; store.clickCount = 3; it('should render and update the title', () => { - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); expect(getByText('The initial title')); @@ -38,7 +38,7 @@ it('should render and update the title', () => { }); it('should render and update the click count', () => { - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); expect(getByText('Button was pressed 3 times')); diff --git a/tests/integration/basicFunctionalComponent.test.tsx b/tests/integration/basicFunctionalComponent.test.tsx index cc4dcf7..53ff02f 100644 --- a/tests/integration/basicFunctionalComponent.test.tsx +++ b/tests/integration/basicFunctionalComponent.test.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { store, WithStoreProp } from '../../src'; -import { collectAndRender } from '../testUtils'; +import * as testUtils from '../testUtils'; it('should render the title', () => { store.title = 'The initial title'; - const { getByText } = collectAndRender((props: WithStoreProp) => ( + const { + getByText, + } = testUtils.collectAndRenderStrict((props: WithStoreProp) => (

{props.store.title}

)); diff --git a/tests/integration/componentDidMount.test.tsx b/tests/integration/componentDidMount.test.tsx index 0424cfb..bbd9b94 100644 --- a/tests/integration/componentDidMount.test.tsx +++ b/tests/integration/componentDidMount.test.tsx @@ -1,8 +1,7 @@ /* eslint-disable max-classes-per-file */ import React, { Component } from 'react'; -import { render } from '@testing-library/react'; -import { expectToLogError } from '../testUtils'; import { collect, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; const TestComponentBad = collect( class extends Component { @@ -43,13 +42,13 @@ const TestComponentGood = collect( ); it('should fail if setting the state during mounting', () => { - expectToLogError(() => { - render(); + testUtils.expectToLogError(() => { + testUtils.renderStrict(); }); }); it('should set loading state after mounting', async () => { - const { findByText } = render(); + const { findByText } = testUtils.renderStrict(); await findByText('Loading...'); diff --git a/tests/integration/componentDidUpdate.test.tsx b/tests/integration/componentDidUpdate.test.tsx index 0ce9b1c..bca21f4 100644 --- a/tests/integration/componentDidUpdate.test.tsx +++ b/tests/integration/componentDidUpdate.test.tsx @@ -1,6 +1,6 @@ /* eslint-disable max-classes-per-file */ import React, { Component, useEffect } from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { collect, store as globalStore, WithStoreProp } from '../../src'; import * as testUtils from '../testUtils'; @@ -100,7 +100,7 @@ const reportUserChange = jest.fn(); it('should handle a change in a value', () => { globalStore.userId = 1; - const { getByText } = render( + const { getByText } = testUtils.renderStrict( ); @@ -125,7 +125,7 @@ it('should re-render on a hidden prop read (FAILS)', () => { const sideEffectMock = jest.fn(); globalStore.loaded = false; - testUtils.collectAndRender( + testUtils.collectAndRenderStrict( class extends Component { componentDidUpdate(prevProps: Readonly) { if (!prevProps.store.loaded && this.props.store.loaded) { diff --git a/tests/integration/forwardRefClass.test.tsx b/tests/integration/forwardRefClass.test.tsx index adcaec4..229c31c 100644 --- a/tests/integration/forwardRefClass.test.tsx +++ b/tests/integration/forwardRefClass.test.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ import React, { Component } from 'react'; -import { render } from '@testing-library/react'; import { collect, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; interface Props extends WithStoreProp { defaultValue: string; @@ -44,7 +44,9 @@ class ComponentWithRef extends Component { } } -const { getByText, getByLabelText } = render(); +const { getByText, getByLabelText } = testUtils.renderStrict( + +); it('should empty the input when the button is clicked', () => { const getInputByLabelText = (text: string) => diff --git a/tests/integration/forwardRefFc.test.tsx b/tests/integration/forwardRefFc.test.tsx index e90954f..a57d565 100644 --- a/tests/integration/forwardRefFc.test.tsx +++ b/tests/integration/forwardRefFc.test.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import { render } from '@testing-library/react'; import { collect, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; type Props = WithStoreProp & { defaultValue: string; @@ -34,7 +34,9 @@ class ComponentWithRef extends Component { } } -const { getByText, getByLabelText } = render(); +const { getByText, getByLabelText } = testUtils.renderStrict( + +); const getInputByLabelText = (text: string) => getByLabelText(text) as HTMLInputElement; diff --git a/tests/integration/hooks.test.tsx b/tests/integration/hooks.test.tsx index 7b9deaa..c8034a5 100644 --- a/tests/integration/hooks.test.tsx +++ b/tests/integration/hooks.test.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'; -import { waitFor } from '@testing-library/react'; +import { waitFor, act } from '@testing-library/react'; import { store as globalStore, WithStoreProp } from '../../src'; import * as testUtils from '../testUtils'; it('should work with useState hook', () => { globalStore.counter = 0; - const { getByText } = testUtils.collectAndRender( + const { getByText } = testUtils.collectAndRenderStrict( ({ store }: WithStoreProp) => { const [counter, setCounter] = useState(0); @@ -53,7 +53,7 @@ it('should work with useEffect hook', async () => { const onMountMock = jest.fn(); const onCountChangeMock = jest.fn(); - const { getByText } = testUtils.collectAndRender( + const { getByText } = testUtils.collectAndRenderStrict( ({ store }: WithStoreProp) => { useEffect(() => { onMountMock(); @@ -83,7 +83,9 @@ it('should work with useEffect hook', async () => { expect(onCountChangeMock).toHaveBeenCalledTimes(1); getByText('Store count: 0'); - getByText('Increment store').click(); + act(() => { + getByText('Increment store').click(); + }); getByText('Store count: 1'); await waitFor(() => {}); // useEffect is async @@ -119,7 +121,7 @@ it('changing store in useLayoutEffect should error', async () => { globalStore.loaded = false; testUtils.expectToLogError(() => { - testUtils.collectAndRender(({ store }: WithStoreProp) => { + testUtils.collectAndRenderStrict(({ store }: WithStoreProp) => { useLayoutEffect(() => { // Oh no, useLayoutEffect is synchronous, so will fire during render store.loaded = true; diff --git a/tests/integration/listening.test.tsx b/tests/integration/listening.test.tsx index 4deda50..fce7bb6 100644 --- a/tests/integration/listening.test.tsx +++ b/tests/integration/listening.test.tsx @@ -42,7 +42,7 @@ it('should register the correct listeners', () => { }, }); - testUtils.collectAndRender(({ store }: WithStoreProp) => { + testUtils.collectAndRenderStrict(({ store }: WithStoreProp) => { return (

Object

@@ -86,7 +86,7 @@ it('should register the correct listeners', () => { it('should register a listener on the store object itself', () => { const { getByText, - } = testUtils.collectAndRender(({ store }: WithStoreProp) => ( + } = testUtils.collectAndRenderStrict(({ store }: WithStoreProp) => (
{Object.keys(store).length ? (
The store has stuff in it
@@ -108,7 +108,7 @@ it('should register a listener on the store object itself', () => { it('should register a listener on the store object with values()', () => { const { getByText, - } = testUtils.collectAndRender(({ store }: WithStoreProp) => ( + } = testUtils.collectAndRenderStrict(({ store }: WithStoreProp) => (
{Object.values(store).includes('test') ? (
Has test
@@ -132,7 +132,7 @@ it('should register a listener on the store object with values()', () => { it('should register a listener on the store object with is', () => { const { getByText, - } = testUtils.collectAndRender(({ store }: WithStoreProp) => ( + } = testUtils.collectAndRenderStrict(({ store }: WithStoreProp) => (
{'anything' in store ? (
Has test
diff --git a/tests/integration/newComponentsGetNewStore.test.tsx b/tests/integration/newComponentsGetNewStore.test.tsx index fc2e2af..3d8733f 100644 --- a/tests/integration/newComponentsGetNewStore.test.tsx +++ b/tests/integration/newComponentsGetNewStore.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react'; import { collect, store as globalStore, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; globalStore.hiddenMessage = ''; @@ -21,7 +21,7 @@ const TestParentComponent = collect(({ store }: WithStoreProp) => (
)); -const { getByText } = render(); +const { getByText } = testUtils.renderStrict(); it('should give the new version of the store to a newly mounting component', () => { getByText('Details are hidden'); diff --git a/tests/integration/propTypes.test.tsx b/tests/integration/propTypes.test.tsx index 648d793..691749b 100644 --- a/tests/integration/propTypes.test.tsx +++ b/tests/integration/propTypes.test.tsx @@ -25,7 +25,7 @@ it('should not listen to props read from prop types', () => { }).isRequired, }; - const { getByText } = testUtils.collectAndRender(MyComponent); + const { getByText } = testUtils.collectAndRenderStrict(MyComponent); expect(testUtils.getAllListeners()).toEqual([ 'prop1', @@ -56,7 +56,7 @@ it('should warn for failed prop types', () => { }; const consoleError = testUtils.expectToLogError(() => { - testUtils.collectAndRender(MyComponent); + testUtils.collectAndRenderStrict(MyComponent); }); expect(consoleError).toMatch( diff --git a/tests/integration/propsInheritance.test.tsx b/tests/integration/propsInheritance.test.tsx index f89ae90..0d88c39 100644 --- a/tests/integration/propsInheritance.test.tsx +++ b/tests/integration/propsInheritance.test.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import { render } from '@testing-library/react'; import { collect, store as globalStore, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; type Props = { visibility: string; @@ -38,7 +38,7 @@ const ClassComponent = collect(RawClassComponent); it('should update a child component not wrapped in collect()', () => { globalStore.clickCount = 0; - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); expect(getByText('This component should be hidden')); diff --git a/tests/integration/readFromTwoStores.test.tsx b/tests/integration/readFromTwoStores.test.tsx index cc05174..8432638 100644 --- a/tests/integration/readFromTwoStores.test.tsx +++ b/tests/integration/readFromTwoStores.test.tsx @@ -1,29 +1,31 @@ import React from 'react'; import { store as globalStore, WithStoreProp } from '../../src'; -import { collectAndRender } from '../testUtils'; +import * as testUtils from '../testUtils'; const getTitle = () => globalStore.meta.title; it('should not matter which store I read/write from', () => { globalStore.meta = { title: 'Hello' }; - const { getByText } = collectAndRender(({ store }: WithStoreProp) => ( -
-

{`${store.meta.title} from the store`}

-

{`${globalStore.meta.title} from the globalStore`}

- - -
- )); + const { getByText } = testUtils.collectAndRenderStrict( + ({ store }: WithStoreProp) => ( +
+

{`${store.meta.title} from the store`}

+

{`${globalStore.meta.title} from the globalStore`}

+ + +
+ ) + ); getByText('Change things').click(); @@ -35,7 +37,7 @@ it('should subscribe to changes from the global store', () => { globalStore.meta = { title: 'Hello' }; // This is wrapped in `collect` but doesn't reference props at all - const { getByText } = collectAndRender(() => ( + const { getByText } = testUtils.collectAndRenderStrict(() => (

{`${globalStore.meta.title} from the globalStore`}

diff --git a/tests/integration/readRemovedProperty.test.tsx b/tests/integration/readRemovedProperty.test.ts similarity index 100% rename from tests/integration/readRemovedProperty.test.tsx rename to tests/integration/readRemovedProperty.test.ts diff --git a/tests/integration/setStoreTwiceInOnClick.test.tsx b/tests/integration/setStoreTwiceInOnClick.test.tsx index 123483d..b92b475 100644 --- a/tests/integration/setStoreTwiceInOnClick.test.tsx +++ b/tests/integration/setStoreTwiceInOnClick.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react'; import { collect, store as globalStore, WithStoreProp } from '../../src'; +import * as testUtils from '../testUtils'; const TestComponent = collect(({ store }: WithStoreProp) => (
@@ -52,7 +52,7 @@ const TestComponent = collect(({ store }: WithStoreProp) => ( )); it('should allow the user to set the store twice in one callback without a re-render', () => { - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); getByText('You have no tasks'); // This click will do store.tasks = [], which is added to the store @@ -76,7 +76,7 @@ it('should increment string', () => { status: 'Happy', }; - const { getByText } = render(); + const { getByText } = testUtils.renderStrict(); getByText('Pump the jams').click(); diff --git a/tests/integration/useProps.test.tsx b/tests/integration/useProps.test.tsx index f2a330a..615ea8b 100644 --- a/tests/integration/useProps.test.tsx +++ b/tests/integration/useProps.test.tsx @@ -157,7 +157,7 @@ it('should work with changing state', () => { hiddenMessage: string; }; - const { queryByText, getByText } = testUtils.collectAndRender( + const { queryByText, getByText } = testUtils.collectAndRenderStrict( ({ store }: Props) => { const [showHiddenMessage, setShowHiddenMessage] = useState(false); diff --git a/tests/testUtils.tsx b/tests/testUtils.tsx index 78831dd..9300d28 100644 --- a/tests/testUtils.tsx +++ b/tests/testUtils.tsx @@ -5,12 +5,22 @@ import { collect } from '../src'; import state from '../src/shared/state'; import { PROP_PATH_SEP } from '../src/shared/constants'; +export const renderStrict = (children: React.ReactNode) => { + return render({children}); +}; + export const collectAndRender = (Comp: React.ComponentType) => { const CollectedComp = collect(Comp); return render(); }; +export const collectAndRenderStrict = (Comp: React.ComponentType) => { + const CollectedComp = collect(Comp); + + return renderStrict(); +}; + export const propPathChanges = (handleChangeMock: jest.Mock) => handleChangeMock.mock.calls.map((call) => call[0].changedProps[0]);