From 9fa96f8303ba86139d06f97e14127a0d2530cd67 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 25 Feb 2019 01:18:23 +0100 Subject: [PATCH] Add useWebId hook. --- src/components/LoggedIn.jsx | 7 ++-- src/components/LoggedOut.jsx | 7 ++-- src/components/withWebId.jsx | 36 +++----------------- src/hooks/useWebId.js | 29 ++++++++++++++++ src/index.js | 4 +++ src/util.js | 11 ++++++ test/.eslintrc | 1 + test/components/LoggedIn-test.jsx | 24 +++++++++---- test/components/LoggedOut-test.jsx | 28 ++++++++++----- test/components/Value-test.jsx | 9 +++-- test/components/evaluateExpressions-test.jsx | 5 ++- test/components/withWebId-test.jsx | 13 +++++-- test/hooks/useWebId-test.js | 26 ++++++++++++++ test/index-test.js | 1 + 14 files changed, 141 insertions(+), 60 deletions(-) create mode 100644 src/hooks/useWebId.js create mode 100644 test/hooks/useWebId-test.js diff --git a/src/components/LoggedIn.jsx b/src/components/LoggedIn.jsx index d102378..9c5a0f4 100644 --- a/src/components/LoggedIn.jsx +++ b/src/components/LoggedIn.jsx @@ -1,6 +1,7 @@ -import withWebId from './withWebId'; +import useWebId from '../hooks/useWebId'; /** Pane that only shows its contents when the user is logged in. */ -export default withWebId(function LoggedIn({ webId, children }) { +export default function LoggedIn({ children }) { + const webId = useWebId(); return webId && children || null; -}); +} diff --git a/src/components/LoggedOut.jsx b/src/components/LoggedOut.jsx index 46f7ecd..3cce57d 100644 --- a/src/components/LoggedOut.jsx +++ b/src/components/LoggedOut.jsx @@ -1,6 +1,7 @@ -import withWebId from './withWebId'; +import useWebId from '../hooks/useWebId'; /** Pane that only shows its contents when the user is logged out. */ -export default withWebId(function LoggedOut({ webId, children }) { +export default function LoggedOut({ children }) { + const webId = useWebId(); return !webId && children || null; -}); +} diff --git a/src/components/withWebId.jsx b/src/components/withWebId.jsx index 04fdbb2..b0c754b 100644 --- a/src/components/withWebId.jsx +++ b/src/components/withWebId.jsx @@ -1,38 +1,10 @@ import React from 'react'; -import auth from 'solid-auth-client'; -import { getDisplayName } from '../util'; - -// Track all instances to inform them of WebID changes -const instances = new Set(); -let authState = { webId: undefined }; +import useWebId from '../hooks/useWebId'; +import { higherOrderComponent } from '../util'; /** * Higher-order component that passes the WebID of the logged-in user * to the webId property of the wrapped component. */ -export default function withWebId(Component) { - return class WithWebID extends React.Component { - static displayName = `WithWebId(${getDisplayName(Component)})`; - - state = authState; - - componentDidMount() { - instances.add(this); - } - - componentWillUnmount() { - instances.delete(this); - } - - render() { - return ; - } - }; -} - -// Inform all instances when the WebID changes -auth.trackSession(session => { - authState = { webId: session && session.webId }; - for (const instance of instances) - instance.setState(authState); -}); +export default higherOrderComponent('WithWebId', Component => + props => ); diff --git a/src/hooks/useWebId.js b/src/hooks/useWebId.js new file mode 100644 index 0000000..87a2afd --- /dev/null +++ b/src/hooks/useWebId.js @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; +import auth from 'solid-auth-client'; + +// Keep track of the WebID and the state setters following it +let webId; +const setters = new Set(); + +/** + * Returns the WebID (string) of the active user, + * `null` if there is no user, + * or `undefined` if the user state is pending. + */ +export default function useWebId() { + const [, setWebId] = useState(webId); + + useEffect(() => { + setters.add(setWebId); + return () => setters.delete(setWebId); + }, []); + + return webId; +} + +// Inform all setters when the WebID changes +auth.trackSession(session => { + webId = session && session.webId; + for (const setter of setters) + setter(webId); +}); diff --git a/src/index.js b/src/index.js index bab9758..a0b7023 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import useWebId from './hooks/useWebId'; + import withWebId from './components/withWebId'; import evaluateExpressions from './components/evaluateExpressions'; import evaluateList from './components/evaluateList'; @@ -20,6 +22,8 @@ import List from './components/List'; import './prop-types'; export { + useWebId, + withWebId, evaluateExpressions, evaluateList, diff --git a/src/util.js b/src/util.js index 42c566f..2951ef7 100644 --- a/src/util.js +++ b/src/util.js @@ -25,6 +25,17 @@ export function getDisplayName(Component) { return Component.displayName || Component.name || 'Component'; } +/** + * Creates a higher-order component with the given name. + */ +export function higherOrderComponent(name, createWrapper) { + return Component => { + const Wrapper = createWrapper(Component); + Wrapper.displayName = `${name}(${getDisplayName(Component)})`; + return Wrapper; + }; +} + /** * Creates a task queue that enforces a minimum time between tasks. * Optionally, new tasks can cause any old ones to be dropped. diff --git a/test/.eslintrc b/test/.eslintrc index 98426fa..8f5c2b3 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -5,5 +5,6 @@ func-style: off, new-cap: off, no-empty-function: off, + no-void: off, }, } diff --git a/test/components/LoggedIn-test.jsx b/test/components/LoggedIn-test.jsx index 428c10f..b7b5e2b 100644 --- a/test/components/LoggedIn-test.jsx +++ b/test/components/LoggedIn-test.jsx @@ -1,10 +1,12 @@ import React from 'react'; import { LoggedIn } from '../../src/'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import auth from 'solid-auth-client'; describe('A LoggedIn pane', () => { let pane; + beforeEach(() => pane.update()); describe('with children', () => { beforeAll(() => { @@ -13,31 +15,39 @@ describe('A LoggedIn pane', () => { afterAll(() => pane.unmount()); describe('when the user is not logged in', () => { - beforeAll(() => auth.mockWebId(null)); + beforeAll(() => !act(() => { + auth.mockWebId(null); + })); it('is empty', () => { - expect(pane.text()).toBe(null); + expect(pane.debug()).toBe(''); }); }); describe('when the user is logged in', () => { - beforeAll(() => auth.mockWebId('https://example.org/#me')); + beforeAll(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('renders the content', () => { - expect(pane.text()).toBe('Logged in'); + expect(pane.debug()).toMatch(/Logged in/); }); }); }); describe('without children', () => { - beforeAll(() => (pane = mount())); + beforeAll(() => !act(() => { + pane = mount(); + })); afterAll(() => pane.unmount()); describe('when the user is logged in', () => { - beforeAll(() => auth.mockWebId('https://example.org/#me')); + beforeAll(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('is empty', () => { - expect(pane.text()).toBe(null); + expect(pane.debug()).toBe(''); }); }); }); diff --git a/test/components/LoggedOut-test.jsx b/test/components/LoggedOut-test.jsx index b60608a..56b8514 100644 --- a/test/components/LoggedOut-test.jsx +++ b/test/components/LoggedOut-test.jsx @@ -1,43 +1,53 @@ import React from 'react'; import { LoggedOut } from '../../src/'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import auth from 'solid-auth-client'; describe('A LoggedOut pane', () => { let pane; + beforeEach(() => pane.update()); describe('with children', () => { - beforeAll(() => { + beforeAll(() => !act(() => { pane = mount(Logged out); - }); + })); afterAll(() => pane.unmount()); describe('when the user is not logged in', () => { - beforeAll(() => auth.mockWebId(null)); + beforeAll(() => !act(() => { + auth.mockWebId(null); + })); it('renders the content', () => { - expect(pane.text()).toBe('Logged out'); + expect(pane.debug()).toMatch(/Logged out/); }); }); describe('when the user is logged in', () => { - beforeAll(() => auth.mockWebId('https://example.org/#me')); + beforeAll(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('is empty', () => { - expect(pane.text()).toBe(null); + expect(pane.debug()).toBe(''); }); }); }); describe('without children', () => { - beforeAll(() => (pane = mount())); + beforeAll(() => !act(() => { + pane = mount(); + })); afterAll(() => pane.unmount()); describe('when the user is not logged in', () => { - beforeAll(() => auth.mockWebId(null)); + beforeAll(() => !act(() => { + auth.mockWebId(null); + })); it('is empty', () => { - expect(pane.text()).toBe(null); + expect(pane.debug()).toBe(''); }); }); }); diff --git a/test/components/Value-test.jsx b/test/components/Value-test.jsx index fafc50a..2c9f64f 100644 --- a/test/components/Value-test.jsx +++ b/test/components/Value-test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Value } from '../../src/'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { mockPromise, update, setProps, timers } from '../util'; import data from '@solid/query-ldflex'; import auth from 'solid-auth-client'; @@ -146,7 +147,9 @@ describe('A Value', () => { }); describe('after the user changes', () => { - beforeEach(() => auth.mockWebId('https://example.org/#me')); + beforeEach(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('re-evaluates the expression', () => { expect(data.resolve).toBeCalledTimes(2); @@ -164,7 +167,9 @@ describe('A Value', () => { afterEach(() => field.unmount()); describe('after the user changes', () => { - beforeEach(() => auth.mockWebId('https://example.org/#me')); + beforeEach(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('does not re-evaluate the expression', () => { expect(expression.then).toBeCalledTimes(1); diff --git a/test/components/evaluateExpressions-test.jsx b/test/components/evaluateExpressions-test.jsx index 22982b5..dc04ffd 100644 --- a/test/components/evaluateExpressions-test.jsx +++ b/test/components/evaluateExpressions-test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { evaluateExpressions } from '../../src/'; import { mount, render } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { mockPromise, setProps, timers } from '../util'; import data from '@solid/query-ldflex'; import auth from 'solid-auth-client'; @@ -333,7 +334,9 @@ describe('An evaluateExpressions wrapper', () => { bar = mockPromise(); bar.resolve('second change'); data.resolve.mockReturnValue(bar); - auth.mockWebId('https://example.org/#me'); + act(() => { + auth.mockWebId('https://example.org/#me'); + }); wrapper.update(); }); diff --git a/test/components/withWebId-test.jsx b/test/components/withWebId-test.jsx index ad9ead2..5937310 100644 --- a/test/components/withWebId-test.jsx +++ b/test/components/withWebId-test.jsx @@ -1,13 +1,16 @@ import React from 'react'; import { withWebId } from '../../src/'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import auth from 'solid-auth-client'; describe('A withWebId wrapper', () => { const Wrapper = withWebId(() => contents); let wrapper; - beforeAll(() => (wrapper = mount())); + beforeAll(() => !act(() => { + wrapper = mount(); + })); beforeEach(() => wrapper.update()); afterAll(() => wrapper.unmount()); @@ -26,7 +29,9 @@ describe('A withWebId wrapper', () => { }); describe('when the user is not logged in', () => { - beforeAll(() => auth.mockWebId(null)); + beforeAll(() => !act(() => { + auth.mockWebId(null); + })); it('renders the wrapped component', () => { expect(wrapper.html()).toBe('contents'); @@ -42,7 +47,9 @@ describe('A withWebId wrapper', () => { }); describe('when the user is logged in', () => { - beforeAll(() => auth.mockWebId('https://example.org/#me')); + beforeAll(() => !act(() => { + auth.mockWebId('https://example.org/#me'); + })); it('renders the wrapped component', () => { expect(wrapper.html()).toBe('contents'); diff --git a/test/hooks/useWebId-test.js b/test/hooks/useWebId-test.js new file mode 100644 index 0000000..b752b20 --- /dev/null +++ b/test/hooks/useWebId-test.js @@ -0,0 +1,26 @@ +import { useWebId } from '../../src/'; +import { renderHook, cleanup, act } from 'react-hooks-testing-library'; +import auth from 'solid-auth-client'; + +describe('useWebId', () => { + let result, unmount; + beforeAll(() => { + ({ result, unmount } = renderHook(() => useWebId())); + }); + afterAll(() => unmount()); + afterAll(cleanup); + + it('returns undefined when the login status is unknown', () => { + expect(result.current).toBeUndefined(); + }); + + it('returns null when the user is logged out', () => { + act(() => void auth.mockWebId(null)); + expect(result.current).toBeNull(); + }); + + it('returns the WebID when the user is logged in', () => { + act(() => void auth.mockWebId('https://example.org/#me')); + expect(result.current).toBe('https://example.org/#me'); + }); +}); diff --git a/test/index-test.js b/test/index-test.js index 2f37145..17403be 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -15,6 +15,7 @@ describe('The SolidReactComponents module', () => { 'Label', 'Name', 'List', + 'useWebId', ]; exports.forEach(name => {