From 7adceb3b05ff35e04e96c919bee01112906c8086 Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Thu, 27 Jun 2019 13:35:35 -0400 Subject: [PATCH 1/6] :construction: working on adding projection and tests for state from share doc --- package.json | 1 + src/useSharedState.js | 54 ++++++++++++++++++++--- src/useSharedState.test.js | 87 +++++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index b681ae5..5195c07 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "prebuild": "rimraf lib", "build": "babel ./src --ignore '**/*.test.js' --out-dir ./lib", "test": "tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", + "test:debug": "node --inspect-brk=127.0.0.1:9228 ./node_modules/.bin/tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", "prepublish": "npm run build", "lint": "eslint src", "pretty": "pretty-quick --staged" diff --git a/src/useSharedState.js b/src/useSharedState.js index f4a854b..1cae451 100644 --- a/src/useSharedState.js +++ b/src/useSharedState.js @@ -1,6 +1,6 @@ // @flow -import { useContext, useEffect, useState, useCallback } from 'react'; +import { useContext, useEffect, useState, useCallback, useRef } from 'react'; import { ShareContext } from './SharedStateProvider'; @@ -10,7 +10,9 @@ import { ShareContext } from './SharedStateProvider'; export function useSharedState( collection: string, - doc_id: string + doc_id: string, + projector?: any => any, + deps?: Array ): [any, (mixed) => Promise] { const connection = useContext(ShareContext); @@ -30,8 +32,18 @@ export function useSharedState( throw maybe_doc._error; default: { // 'resolved' + const has_projector = !!projector; + + const memo_projector = useCallback(projector, [has_projector]); + + const [{ state }, setState] = useState({ + state: has_projector + ? memo_projector(maybe_doc.data) + : maybe_doc.data, + }); + + const state_ref = useRef(state); - const [{ state }, setState] = useState({ state: maybe_doc.data }); const dispatch = useCallback( action => { // todo: allow action to be a function that takes unprojected/unmapped state @@ -50,10 +62,43 @@ export function useSharedState( useEffect(() => { const handle_op = () => { - setState({ state: maybe_doc.data }); + if (!has_projector) { + setState({ state: maybe_doc.data }); + state_ref.current = maybe_doc.data; + return; + } + + const new_state = memo_projector(maybe_doc.data); + const { current: current_state } = state_ref; + + if (typeof new_state !== typeof current_state) { + // rerender + setState({ state: maybe_doc.data }); + state_ref.current = maybe_doc.data; + return; + } + + if (Array.isArray(new_state)) { + // compare + return; + } + + // if isPlainObject(new_state) { /* shallow compare */ } + + if (new_state !== current_state) { + // rerender + setState({ state: memo_projector(maybe_doc.data) }); + state_ref.current = new_state; + } }; maybe_doc.addListener('op', handle_op); + return () => { + maybe_doc.removeListener('op', handle_op); + }; + }, [maybe_doc, has_projector, memo_projector]); + + useEffect(() => { // increment the subscription ref count to indicate that this component is now subscribed to the doc maybe_doc._subscription_ref_count++; @@ -62,7 +107,6 @@ export function useSharedState( // return; // } // todo: delay destroying the doc from ram for a given timeout. - maybe_doc.removeListener('op', handle_op); // decrement the subscription ref count to indicate that this component is no longer subscribed to the doc maybe_doc._subscription_ref_count--; diff --git a/src/useSharedState.test.js b/src/useSharedState.test.js index 527585c..7977989 100644 --- a/src/useSharedState.test.js +++ b/src/useSharedState.test.js @@ -3,11 +3,11 @@ import test from 'tape'; import sharedb from 'sharedb'; -import React, { Suspense } from 'react'; +import React, { Suspense, useEffect } from 'react'; import type { Node } from 'react'; import TestRenderer from 'react-test-renderer'; -import { SharedState, SharedStateProvider } from './index'; +import { SharedState, SharedStateProvider, useSharedState } from './index'; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -565,3 +565,86 @@ test.skip('react-sharedb: useSharedReducer suspends rendering if a document is d assert.end(e); } }); + +test.only('react-sharedb: useSharedState projector correctly filters the state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + count2: 0, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [ count1, submit ] = useSharedState( + collection, + doc_id, + ({ count1 }) => ( count1 ) + ); + + return ( + + ) + } + + // initial render of the react tree + const { root, unmount } = TestRenderer.create( + + }> + + + + ); + + // allow the sharedb server to flush updates + await sleep(0); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 0, + "The 's count1 prop is 0." + ); + + // submit an operation to increment the count property by one + mock_instance.props.onSubmit({ + p: ['count1'], + na: 1, + }); + } + + // allow the sharedb server to flush updates + await sleep(0); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 1, + "The 's count prop is 1," + ); + + assert.equals( + remote_doc.data.count1, + 1, + "And the remote doc's count is also 1" + ); + } + + assert.end(); +}); From 4b6a87c8baf1ab1c291cc7c5cb9f51ef1b4eaced Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Fri, 28 Jun 2019 09:15:12 -0400 Subject: [PATCH 2/6] :white_check_mark: added act from react-test-renderer to get updates to components to work correctly --- package.json | 1 + src/broken.test.js | 38 +++++++++++++++++++++ src/useSharedState.test.js | 69 +++++++++++++++++++++++++------------- 3 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 src/broken.test.js diff --git a/package.json b/package.json index b681ae5..5195c07 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "prebuild": "rimraf lib", "build": "babel ./src --ignore '**/*.test.js' --out-dir ./lib", "test": "tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", + "test:debug": "node --inspect-brk=127.0.0.1:9228 ./node_modules/.bin/tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", "prepublish": "npm run build", "lint": "eslint src", "pretty": "pretty-quick --staged" diff --git a/src/broken.test.js b/src/broken.test.js new file mode 100644 index 0000000..f556a22 --- /dev/null +++ b/src/broken.test.js @@ -0,0 +1,38 @@ + +import test from 'tape'; + +import React, { useState, useEffect, useCallback } from 'react'; +import TestRenderer from 'react-test-renderer'; + +const SubscriptionThing = () => { + +} + +const Child = () => { + return
; +}; + +const App = ({ children }) => { + const [count, setCount] = useState(0); + const submit = useCallback(() => { + setCount(count => count + 1); + }, []); + + return children(count, submit); +}; + +test.skip("foo", assert => { + const renderer = TestRenderer.create( + {(count, submit) => } + ); + + const [child] = renderer.root.findAllByType(Child); + + assert.ok(child.props.count === 0, "count is 0"); + + child.props.submit(); + + assert.ok(child.props.count === 1, "count is 1"); + + assert.end(); +}); diff --git a/src/useSharedState.test.js b/src/useSharedState.test.js index 527585c..e927578 100644 --- a/src/useSharedState.test.js +++ b/src/useSharedState.test.js @@ -5,7 +5,7 @@ import sharedb from 'sharedb'; import React, { Suspense } from 'react'; import type { Node } from 'react'; -import TestRenderer from 'react-test-renderer'; +import TestRenderer, { act } from 'react-test-renderer'; import { SharedState, SharedStateProvider } from './index'; @@ -288,22 +288,30 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch const remote_doc = remote.get(collection, doc_id); remote_doc.create(doc_state); + let renderer, mock_instance; + + act(() => { + renderer = TestRenderer.create( + + }> + + {(state, submit) => } + + + + ); + }) + // initial render of the react tree - const { root, unmount } = TestRenderer.create( - - }> - - {(state, submit) => } - - - - ); + const { root, unmount } = renderer; // allow the sharedb server to flush updates await sleep(0); { - const [mock_instance] = root.findAllByType(Mock); + act(() => { + mock_instance = root.findAllByType(Mock)[0]; + }) assert.equals( mock_instance.props.count, @@ -322,7 +330,9 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch await sleep(0); { - const [mock_instance] = root.findAllByType(Mock); + act(() => { + mock_instance = root.findAllByType(Mock)[0]; + }) assert.equals( mock_instance.props.count, @@ -354,23 +364,32 @@ test('react-sharedb: when submitting operations via the submit function returned remote_doc.subscribe(); remote_doc.create(doc_state); + let renderer, mock_instance; + + act(() => { + renderer = TestRenderer.create( + + }> + + {(state, submit) => } + + + + ); + }) + + // initial render of the react tree - const { root, unmount } = TestRenderer.create( - - }> - - {(state, submit) => } - - - - ); + // const { root, unmount } = renderer; // allow the sharedb server to flush updates await sleep(0); { //const remote_doc = remote.get(collection, doc_id); - const [mock_instance] = root.findAllByType(Mock); + act(() => { + mock_instance = renderer.root.findAllByType(Mock)[0]; + }); assert.equals( mock_instance.props.count, @@ -395,7 +414,9 @@ test('react-sharedb: when submitting operations via the submit function returned await sleep(0); { - const [mock_instance] = root.findAllByType(Mock); + act(() => { + mock_instance = renderer.root.findAllByType(Mock)[0]; + }); assert.equals( mock_instance.props.count, @@ -413,7 +434,7 @@ test('react-sharedb: when submitting operations via the submit function returned remote_doc.unsubscribe(); remote_doc.destroy(); - unmount(); + renderer.unmount(); assert.end(); }); From a1c8620930759d2a5fb2ac1afaf57ffcacbd53f9 Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Tue, 2 Jul 2019 13:36:22 -0400 Subject: [PATCH 3/6] :construction: Updated useSharedState to use the mout library, it now will clone output from the projector function (will be optional later, and required for json0) to get the state to update correctly when the output of the projector is a reference to an object. --- package-lock.json | 371 ++++++++++++++++++++++++++++++++++--- package.json | 14 +- src/broken.test.js | 38 ---- src/useSharedState.js | 77 ++++++-- src/useSharedState.test.js | 265 ++++++++++++++++++++------ 5 files changed, 627 insertions(+), 138 deletions(-) delete mode 100644 src/broken.test.js diff --git a/package-lock.json b/package-lock.json index caf395f..b42db86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -940,6 +940,12 @@ "color-convert": "1.9.3" } }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "dev": true + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -1279,6 +1285,16 @@ "integrity": "sha512-/F3t/Yo8LEdRSEPCmI15fLu5vepVh9UCg/9inJXF5AAfW7xRRJkbaM2ut52iRMQMnGCLQouLbFdbOA+VEFOIsg==", "dev": true }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "dev": true, + "requires": { + "ansicolors": "0.3.2", + "redeyed": "2.1.1" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1355,6 +1371,25 @@ "restore-cursor": "2.0.0" } }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + } + }, + "cli-usage": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.9.tgz", + "integrity": "sha512-MIJJnLu89KTRoGN1ix9dwvKYUPUP7tUL+YGKNH/7mFmy8n3aWNznQKK8FU7PsFVQxePW5rxBp0lupzeSjRiXTA==", + "dev": true, + "requires": { + "marked": "0.6.2", + "marked-terminal": "3.2.0" + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -1386,6 +1421,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -2094,6 +2135,15 @@ "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", "dev": true }, + "exec-sh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", + "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", + "dev": true, + "requires": { + "merge": "1.2.1" + } + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -2176,6 +2226,17 @@ "dev": true, "requires": { "is-plain-object": "2.0.4" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -3015,6 +3076,12 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3425,15 +3492,6 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "3.0.1" - } - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -3612,6 +3670,105 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=", + "dev": true + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "dev": true, + "requires": { + "lodash._arraycopy": "3.0.0", + "lodash._arrayeach": "3.0.0", + "lodash._baseassign": "3.2.0", + "lodash._basefor": "3.0.3", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "dev": true, + "requires": { + "lodash._baseclone": "3.3.0", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3660,6 +3817,32 @@ "object-visit": "1.0.1" } }, + "marked": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.2.tgz", + "integrity": "sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==", + "dev": true + }, + "marked-terminal": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.2.0.tgz", + "integrity": "sha512-Yr1yVS0BbDG55vx7be1D0mdv+jGs9AW563o/Tt/7FTsId2J0yqhrTeXAqq/Q0DyyXltIn6CSxzesQuFqXgafjQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.2.0", + "cardinal": "2.1.1", + "chalk": "2.4.2", + "cli-table": "0.3.1", + "node-emoji": "1.10.0", + "supports-hyperlinks": "1.0.1" + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -3728,6 +3911,17 @@ "dev": true, "requires": { "is-plain-object": "2.0.4" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -3749,6 +3943,11 @@ } } }, + "mout": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", + "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==" + }, "mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -3817,12 +4016,36 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dev": true, + "requires": { + "lodash.toarray": "4.4.0" + } + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", "dev": true }, + "node-notifier": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-4.6.1.tgz", + "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=", + "dev": true, + "requires": { + "cli-usage": "0.1.9", + "growly": "1.3.0", + "lodash.clonedeep": "3.0.2", + "minimist": "1.2.0", + "semver": "5.7.0", + "shellwords": "0.1.1", + "which": "1.3.1" + } + }, "node-releases": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.22.tgz", @@ -4398,14 +4621,25 @@ "dev": true }, "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.9.0-alpha.0.tgz", + "integrity": "sha512-y4bu7rJvtnPPsIwOj7sp5Y2SqlOb0jFupfkdjWxxn8ZeqzUARgpR9wJBUVwW1/QosVdOblmApjo/j6iiAXnebA==", "requires": { "loose-envify": "1.4.0", "object-assign": "4.1.1", "prop-types": "15.7.2", - "scheduler": "0.13.6" + "scheduler": "0.14.0" + }, + "dependencies": { + "scheduler": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.14.0.tgz", + "integrity": "sha512-9CgbS06Kki2f4R9FjLSITjZo5BZxPsryiRNyL3LpvrM9WxcVmhlqAOc9E+KQbeI2nqej4JIIbOsfdL51cNb4Iw==", + "requires": { + "loose-envify": "1.4.0", + "object-assign": "4.1.1" + } + } } }, "react-is": { @@ -4414,15 +4648,33 @@ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "react-test-renderer": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", - "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.9.0-alpha.0.tgz", + "integrity": "sha512-eDl0oVFo6PGY1wpYFs0ezBpZhOgVce5TSta9UPLanshTi4z8NhlM6IgO8KBdioQ5H5/pmyGxOVtpUxJOt19NAQ==", "dev": true, "requires": { "object-assign": "4.1.1", "prop-types": "15.7.2", - "react-is": "16.8.6", - "scheduler": "0.13.6" + "react-is": "16.9.0-alpha.0", + "scheduler": "0.14.0" + }, + "dependencies": { + "react-is": { + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0-alpha.0.tgz", + "integrity": "sha512-psl0ePLTFliYfwcbwvimLgTNN156ZdeWB4zvP7dV/6lTAqWMHFfidg/mSZ2fFgE1LMNN8ZJOLl2DfZ8yg+3ETA==", + "dev": true + }, + "scheduler": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.14.0.tgz", + "integrity": "sha512-9CgbS06Kki2f4R9FjLSITjZo5BZxPsryiRNyL3LpvrM9WxcVmhlqAOc9E+KQbeI2nqej4JIIbOsfdL51cNb4Iw==", + "dev": true, + "requires": { + "loose-envify": "1.4.0", + "object-assign": "4.1.1" + } + } } }, "read-pkg": { @@ -4530,6 +4782,15 @@ "readable-stream": "2.3.6" } }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "dev": true, + "requires": { + "esprima": "4.0.1" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -4732,15 +4993,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "requires": { - "loose-envify": "1.4.0", - "object-assign": "4.1.1" - } - }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -4773,6 +5025,15 @@ "requires": { "is-extendable": "0.1.1" } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } } } }, @@ -4804,6 +5065,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -5126,6 +5393,24 @@ "has-flag": "3.0.0" } }, + "supports-hyperlinks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", + "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", + "dev": true, + "requires": { + "has-flag": "2.0.0", + "supports-color": "5.5.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + } + } + }, "table": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/table/-/table-5.4.0.tgz", @@ -5233,6 +5518,17 @@ } } }, + "tap-notify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tap-notify/-/tap-notify-1.0.0.tgz", + "integrity": "sha1-2VZKV9xVv8VY0/hkActH8B77cBg=", + "dev": true, + "requires": { + "node-notifier": "4.6.1", + "tap-parser": "1.3.2", + "through2": "2.0.5" + } + }, "tap-parser": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-1.3.2.tgz", @@ -5451,6 +5747,17 @@ "is-extendable": "0.1.1", "is-plain-object": "2.0.4", "to-object-path": "0.3.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -5539,6 +5846,16 @@ "spdx-expression-parse": "3.0.0" } }, + "watch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", + "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=", + "dev": true, + "requires": { + "exec-sh": "0.2.2", + "minimist": "1.2.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 5195c07..1259e9f 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "scripts": { "prebuild": "rimraf lib", "build": "babel ./src --ignore '**/*.test.js' --out-dir ./lib", - "test": "tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", - "test:debug": "node --inspect-brk=127.0.0.1:9228 ./node_modules/.bin/tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", + "test": "tape -r @babel/register -r tape-catch './src/**/*.test.js*' | tap-notify | tap-diff", + "test:watch": "watch 'clear && npm run test' ./src", + "test:debug": "node --inspect-brk -r @babel/register -r tape-catch ./node_modules/.bin/tape './src/**/*.test.js*'", "prepublish": "npm run build", "lint": "eslint src", "pretty": "pretty-quick --staged" @@ -41,14 +42,17 @@ "husky": "^2.3.0", "prettier": "^1.17.1", "pretty-quick": "^1.11.0", - "react-test-renderer": "^16.8.6", + "react-test-renderer": "^16.9.0-alpha.0", "rimraf": "^2.6.3", "tap-diff": "^0.1.1", + "tap-notify": "^1.0.0", "tape": "^4.10.2", - "tape-catch": "^1.0.6" + "tape-catch": "^1.0.6", + "watch": "^1.0.2" }, "dependencies": { - "react": "^16.8.6", + "mout": "^1.1.0", + "react": "^16.9.0-alpha.0", "sharedb": "^1.0.0-beta.23" } } diff --git a/src/broken.test.js b/src/broken.test.js deleted file mode 100644 index f556a22..0000000 --- a/src/broken.test.js +++ /dev/null @@ -1,38 +0,0 @@ - -import test from 'tape'; - -import React, { useState, useEffect, useCallback } from 'react'; -import TestRenderer from 'react-test-renderer'; - -const SubscriptionThing = () => { - -} - -const Child = () => { - return
; -}; - -const App = ({ children }) => { - const [count, setCount] = useState(0); - const submit = useCallback(() => { - setCount(count => count + 1); - }, []); - - return children(count, submit); -}; - -test.skip("foo", assert => { - const renderer = TestRenderer.create( - {(count, submit) => } - ); - - const [child] = renderer.root.findAllByType(Child); - - assert.ok(child.props.count === 0, "count is 0"); - - child.props.submit(); - - assert.ok(child.props.count === 1, "count is 1"); - - assert.end(); -}); diff --git a/src/useSharedState.js b/src/useSharedState.js index 1cae451..6fbb228 100644 --- a/src/useSharedState.js +++ b/src/useSharedState.js @@ -1,7 +1,9 @@ // @flow import { useContext, useEffect, useState, useCallback, useRef } from 'react'; - +import { isPlainObject, deepClone } from 'mout/lang'; +import { equals as arrayShallowEquals } from 'mout/array'; +import { equals as objectShallowEquals } from 'mout/object'; import { ShareContext } from './SharedStateProvider'; /** questions @@ -12,7 +14,7 @@ export function useSharedState( collection: string, doc_id: string, projector?: any => any, - deps?: Array + deps?: Array ): [any, (mixed) => Promise] { const connection = useContext(ShareContext); @@ -33,12 +35,17 @@ export function useSharedState( default: { // 'resolved' const has_projector = !!projector; + const should_clone = true; - const memo_projector = useCallback(projector, [has_projector]); + const memo_projector = useCallback(projector || (state => state), [ + has_projector, + ]); const [{ state }, setState] = useState({ state: has_projector - ? memo_projector(maybe_doc.data) + ? should_clone + ? deepClone(memo_projector(maybe_doc.data)) + : memo_projector(maybe_doc.data) : maybe_doc.data, }); @@ -63,32 +70,70 @@ export function useSharedState( useEffect(() => { const handle_op = () => { if (!has_projector) { - setState({ state: maybe_doc.data }); state_ref.current = maybe_doc.data; + setState({ state: maybe_doc.data }); return; } - const new_state = memo_projector(maybe_doc.data); - const { current: current_state } = state_ref; + const projected_state = memo_projector(maybe_doc.data); + const { current: current_projected_state } = state_ref; - if (typeof new_state !== typeof current_state) { + if (typeof projected_state !== typeof current_projected_state) { // rerender - setState({ state: maybe_doc.data }); - state_ref.current = maybe_doc.data; + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); return; } - if (Array.isArray(new_state)) { - // compare + if ( + Array.isArray(projected_state) && + Array.isArray(current_projected_state) + ) { + // if lengths are not the same then rerender + if ( + !arrayShallowEquals(projected_state, current_projected_state) + ) { + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); + return; + } + return; } - // if isPlainObject(new_state) { /* shallow compare */ } + if ( + isPlainObject(projected_state) && + isPlainObject(current_projected_state) + ) { + // if they have a different amount of keys, rerender: + if ( + objectShallowEquals(projected_state, current_projected_state) + ) { + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); + return; + } + + return; + } - if (new_state !== current_state) { + // if we're here it's likely a number, bool, string, etc. so do an equality check + if (projected_state !== current_projected_state) { // rerender - setState({ state: memo_projector(maybe_doc.data) }); - state_ref.current = new_state; + setState({ state: projected_state }); + state_ref.current = projected_state; } }; maybe_doc.addListener('op', handle_op); diff --git a/src/useSharedState.test.js b/src/useSharedState.test.js index a221995..b91f634 100644 --- a/src/useSharedState.test.js +++ b/src/useSharedState.test.js @@ -3,7 +3,7 @@ import test from 'tape'; import sharedb from 'sharedb'; -import React, { Suspense, useEffect } from 'react'; +import React, { Suspense, useRef } from 'react'; import type { Node } from 'react'; import TestRenderer, { act } from 'react-test-renderer'; @@ -288,7 +288,7 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch const remote_doc = remote.get(collection, doc_id); remote_doc.create(doc_state); - let renderer, mock_instance; + let renderer = {}; act(() => { renderer = TestRenderer.create( @@ -300,18 +300,16 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch ); - }) + }); // initial render of the react tree const { root, unmount } = renderer; // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { - act(() => { - mock_instance = root.findAllByType(Mock)[0]; - }) + const [mock_instance] = root.findAllByType(Mock); assert.equals( mock_instance.props.count, @@ -321,18 +319,18 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch } // submit an operation to increment the count property by one - remote_doc.submitOp({ - p: ['count'], - na: 1, + act(() => { + remote_doc.submitOp({ + p: ['count'], + na: 1, + }); }); // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { - act(() => { - mock_instance = root.findAllByType(Mock)[0]; - }) + const [mock_instance] = root.findAllByType(Mock); assert.equals( mock_instance.props.count, @@ -341,6 +339,8 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch ); } + remote_doc.unsubscribe(); + remote_doc.destroy(); unmount(); assert.end(); }); @@ -364,7 +364,7 @@ test('react-sharedb: when submitting operations via the submit function returned remote_doc.subscribe(); remote_doc.create(doc_state); - let renderer, mock_instance; + let renderer = {}; act(() => { renderer = TestRenderer.create( @@ -376,20 +376,17 @@ test('react-sharedb: when submitting operations via the submit function returned ); - }) - + }); // initial render of the react tree - // const { root, unmount } = renderer; + const { root, unmount } = renderer; // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { //const remote_doc = remote.get(collection, doc_id); - act(() => { - mock_instance = renderer.root.findAllByType(Mock)[0]; - }); + const [mock_instance] = root.findAllByType(Mock); assert.equals( mock_instance.props.count, @@ -404,19 +401,19 @@ test('react-sharedb: when submitting operations via the submit function returned ); // submit an operation to increment the count property by one - mock_instance.props.onSubmit({ - p: ['count'], - na: 1, + act(() => { + mock_instance.props.onSubmit({ + p: ['count'], + na: 1, + }); }); } // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { - act(() => { - mock_instance = renderer.root.findAllByType(Mock)[0]; - }); + const [mock_instance] = root.findAllByType(Mock); assert.equals( mock_instance.props.count, @@ -434,7 +431,7 @@ test('react-sharedb: when submitting operations via the submit function returned remote_doc.unsubscribe(); remote_doc.destroy(); - renderer.unmount(); + unmount(); assert.end(); }); @@ -587,7 +584,7 @@ test.skip('react-sharedb: useSharedReducer suspends rendering if a document is d } }); -test.only('react-sharedb: useSharedState projector correctly filters the state', async assert => { +test('react-sharedb: useSharedState projector correctly filters the state', async assert => { // Create a client connection to an in memory sharedb database to simulate // a frontend connection to sharedb running on a server. const server = new sharedb.Backend(); @@ -599,7 +596,6 @@ test.only('react-sharedb: useSharedState projector correctly filters the state', const doc_state = { doc_id: doc_id, count1: 0, - count2: 0, }; // create the doc via the secondary connection @@ -608,28 +604,32 @@ test.only('react-sharedb: useSharedState projector correctly filters the state', remote_doc.create(doc_state); const MockWrapper = () => { - const [ count1, submit ] = useSharedState( + const [count1, submit] = useSharedState( collection, doc_id, - ({ count1 }) => ( count1 ) + ({ count1 }) => count1 ); - return ( - - ) - } + return ; + }; - // initial render of the react tree - const { root, unmount } = TestRenderer.create( - - }> - - - - ); + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { //const remote_doc = remote.get(collection, doc_id); @@ -641,15 +641,16 @@ test.only('react-sharedb: useSharedState projector correctly filters the state', "The 's count1 prop is 0." ); - // submit an operation to increment the count property by one - mock_instance.props.onSubmit({ - p: ['count1'], - na: 1, + act(() => { + mock_instance.props.onSubmit({ + p: ['count1'], + oi: 1, + }); }); } // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { const [mock_instance] = root.findAllByType(Mock); @@ -667,5 +668,165 @@ test.only('react-sharedb: useSharedState projector correctly filters the state', ); } + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test.only('react-sharedb: updating projected array', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + list: [], + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [list, submit] = useSharedState( + collection, + doc_id, + ({ list }) => list + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 0], + li: 0, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + assert.equals( + mock_instance.props.list[0], + 0, + 'Adding an item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + li: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + // assert.comment(JSON.stringify(remote_doc.data.list)); + // assert.comment(JSON.stringify(mock_instance.props)); + + assert.equals( + mock_instance.props.getRenderCount(), + 3, + 'Rendered exactly 3 times' + ); + + assert.equals( + mock_instance.props.list[1], + 1, + 'Adding a second item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + ld: 1, + }); + }); + } + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 4, + 'Rendered exactly 4 times' + ); + + assert.equals( + mock_instance.props.list[1], + undefined, + 'Removing an item from the list.' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); assert.end(); }); From 714cebad25f0fad2410168e53e6fcd54aba6c063 Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Wed, 3 Jul 2019 08:32:17 -0400 Subject: [PATCH 4/6] :construction: some small fixes to useSharedState, a reasonable amount of tests written --- src/useSharedState.js | 2 +- src/useSharedState.projector.test.js | 661 +++++++++++++++++++++++++++ src/useSharedState.test.js | 249 +--------- 3 files changed, 663 insertions(+), 249 deletions(-) create mode 100644 src/useSharedState.projector.test.js diff --git a/src/useSharedState.js b/src/useSharedState.js index 6fbb228..938c33b 100644 --- a/src/useSharedState.js +++ b/src/useSharedState.js @@ -115,7 +115,7 @@ export function useSharedState( ) { // if they have a different amount of keys, rerender: if ( - objectShallowEquals(projected_state, current_projected_state) + !objectShallowEquals(projected_state, current_projected_state) ) { const state = should_clone ? deepClone(projected_state) diff --git a/src/useSharedState.projector.test.js b/src/useSharedState.projector.test.js new file mode 100644 index 0000000..e4f4d6b --- /dev/null +++ b/src/useSharedState.projector.test.js @@ -0,0 +1,661 @@ +// @flow +import test from 'tape'; +import sharedb from 'sharedb'; +import React, { Suspense, useRef } from 'react'; +import { useSharedState } from './useSharedState'; +import TestRenderer, { act } from 'react-test-renderer'; +import { SharedStateProvider } from './SharedStateProvider'; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function Loading() { + return
; +} + +function Mock() { + return
; +} + +test('react-sharedb: useSharedState projector correctly filters the state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [count1, submit] = useSharedState( + collection, + doc_id, + ({ count1 }) => count1 + ); + + return ; + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 0, + "The 's count1 prop is 0." + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['count1'], + oi: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 1, + "The 's count prop is 1," + ); + + assert.equals( + remote_doc.data.count1, + 1, + "And the remote doc's count is also 1" + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating projected array tuple', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + prop1: 0, + prop2: '', + prop3: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [items, submit] = useSharedState( + collection, + doc_id, + ({ prop1, prop2 }) => [prop1, prop2] + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + assert.equals( + mock_instance.props.items[0], + 1, + 'Updated the value correctly' + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop3'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times after updating non-projected value.' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating directly projected array in doc state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + list: [], + prop1: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [list, submit] = useSharedState( + collection, + doc_id, + ({ list }) => list + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 0], + li: 0, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + assert.equals( + mock_instance.props.list[0], + 0, + 'Adding an item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + li: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + // assert.comment(JSON.stringify(remote_doc.data.list)); + // assert.comment(JSON.stringify(mock_instance.props)); + + assert.equals( + mock_instance.props.getRenderCount(), + 3, + 'Rendered exactly 3 times' + ); + + assert.equals( + mock_instance.props.list[1], + 1, + 'Adding a second item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + ld: 1, + }); + }); + } + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 4, + 'Rendered exactly 4 times' + ); + + assert.equals( + mock_instance.props.list[1], + undefined, + 'Removing an item from the list.' + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 4, + 'Rendered exactly 4 times after updating non-projected value.' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating projected plain object tuple', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + prop1: 0, + prop2: '', + prop3: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [{ prop1, prop2 }, submit] = useSharedState( + collection, + doc_id, + ({ prop1, prop2 }) => ({ prop1, prop2 }) + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 time' + ); + + assert.equals( + mock_instance.props.prop1, + 1, + 'The property was properly updated', + ) + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop3'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [ mock_instance ] = root.findAllByType( Mock ); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times after updating non-projected value' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating directly projected object in doc state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + thing: { + prop1: 'one', + prop2: 0, + prop3: '', + } + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [{ prop1, prop2 }, submit] = useSharedState( + collection, + doc_id, + ({ thing }) => thing, + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['thing', 'prop1'], + oi: 'two', + }); + }); + } + + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.prop1, + 'two', + 'The prop was updated.' + ) + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + // submit op but don't change prop - make sure there's no re-render + act(() => { + mock_instance.props.onSubmit({ + p: ['thing', 'prop2'], + oi: 0, + }); + }); + } + + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); diff --git a/src/useSharedState.test.js b/src/useSharedState.test.js index b91f634..4ccdd3f 100644 --- a/src/useSharedState.test.js +++ b/src/useSharedState.test.js @@ -3,7 +3,7 @@ import test from 'tape'; import sharedb from 'sharedb'; -import React, { Suspense, useRef } from 'react'; +import React, { Suspense } from 'react'; import type { Node } from 'react'; import TestRenderer, { act } from 'react-test-renderer'; @@ -583,250 +583,3 @@ test.skip('react-sharedb: useSharedReducer suspends rendering if a document is d assert.end(e); } }); - -test('react-sharedb: useSharedState projector correctly filters the state', async assert => { - // Create a client connection to an in memory sharedb database to simulate - // a frontend connection to sharedb running on a server. - const server = new sharedb.Backend(); - const local = server.connect(); - const remote = server.connect(); - - const collection = 'collection'; - const doc_id = 'id:5456563'; - const doc_state = { - doc_id: doc_id, - count1: 0, - }; - - // create the doc via the secondary connection - const remote_doc = remote.get(collection, doc_id); - remote_doc.subscribe(); - remote_doc.create(doc_state); - - const MockWrapper = () => { - const [count1, submit] = useSharedState( - collection, - doc_id, - ({ count1 }) => count1 - ); - - return ; - }; - - let renderer = {}; - - act(() => { - // initial render of the react tree - renderer = TestRenderer.create( - - }> - - - - ); - }); - - const { root, unmount } = renderer; - - // allow the sharedb server to flush updates - await act(() => sleep(0)); - - { - //const remote_doc = remote.get(collection, doc_id); - const [mock_instance] = root.findAllByType(Mock); - - assert.equals( - mock_instance.props.count1, - 0, - "The 's count1 prop is 0." - ); - - act(() => { - mock_instance.props.onSubmit({ - p: ['count1'], - oi: 1, - }); - }); - } - - // allow the sharedb server to flush updates - await act(() => sleep(0)); - - { - const [mock_instance] = root.findAllByType(Mock); - - assert.equals( - mock_instance.props.count1, - 1, - "The 's count prop is 1," - ); - - assert.equals( - remote_doc.data.count1, - 1, - "And the remote doc's count is also 1" - ); - } - - remote_doc.unsubscribe(); - remote_doc.destroy(); - - unmount(); - assert.end(); -}); - -test.only('react-sharedb: updating projected array', async assert => { - // Create a client connection to an in memory sharedb database to simulate - // a frontend connection to sharedb running on a server. - const server = new sharedb.Backend(); - const local = server.connect(); - const remote = server.connect(); - - const collection = 'collection'; - const doc_id = 'id:5456563'; - const doc_state = { - doc_id: doc_id, - list: [], - }; - - // create the doc via the secondary connection - const remote_doc = remote.get(collection, doc_id); - remote_doc.subscribe(); - remote_doc.create(doc_state); - - const MockWrapper = () => { - const [list, submit] = useSharedState( - collection, - doc_id, - ({ list }) => list - ); - - const render_count_ref = useRef(0); - - render_count_ref.current++; - - return ( - render_count_ref.current} - onSubmit={submit} - /> - ); - }; - - let renderer = {}; - - act(() => { - // initial render of the react tree - renderer = TestRenderer.create( - - }> - - - - ); - }); - - const { root, unmount } = renderer; - - // allow the sharedb server to flush updates - await act(() => sleep(0)); - - { - //const remote_doc = remote.get(collection, doc_id); - const [mock_instance] = root.findAllByType(Mock); - - assert.equals( - mock_instance.props.getRenderCount(), - 1, - 'Rendered exactly 1 time' - ); - - // submit an operation to add item to list - act(() => { - mock_instance.props.onSubmit({ - p: ['list', 0], - li: 0, - }); - }); - } - - // allow the sharedb server to flush updates - await act(() => sleep(0)); - - { - const [mock_instance] = root.findAllByType(Mock); - - assert.equals( - mock_instance.props.getRenderCount(), - 2, - 'Rendered exactly 2 times' - ); - - assert.equals( - mock_instance.props.list[0], - 0, - 'Adding an item to the list.' - ); - - // submit an operation to add item to list - act(() => { - mock_instance.props.onSubmit({ - p: ['list', 1], - li: 1, - }); - }); - } - - // allow the sharedb server to flush updates - await act(() => sleep(0)); - - { - const [mock_instance] = root.findAllByType(Mock); - - // assert.comment(JSON.stringify(remote_doc.data.list)); - // assert.comment(JSON.stringify(mock_instance.props)); - - assert.equals( - mock_instance.props.getRenderCount(), - 3, - 'Rendered exactly 3 times' - ); - - assert.equals( - mock_instance.props.list[1], - 1, - 'Adding a second item to the list.' - ); - - // submit an operation to add item to list - act(() => { - mock_instance.props.onSubmit({ - p: ['list', 1], - ld: 1, - }); - }); - } - - { - const [mock_instance] = root.findAllByType(Mock); - - assert.equals( - mock_instance.props.getRenderCount(), - 4, - 'Rendered exactly 4 times' - ); - - assert.equals( - mock_instance.props.list[1], - undefined, - 'Removing an item from the list.' - ); - } - - remote_doc.unsubscribe(); - remote_doc.destroy(); - - unmount(); - assert.end(); -}); From 63920c1d432863150f8e531c613a259e4f866929 Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Wed, 3 Jul 2019 16:03:35 -0400 Subject: [PATCH 5/6] :construction: publishing --- package.json | 2 +- src/useSharedState.js | 36 ++++++++----- src/useSharedState.projector.test.js | 79 ++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 1259e9f..7b55cca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@retrium/react-sharedb", - "version": "0.1.0-alpha.3", + "version": "0.1.0-alpha.5", "description": "", "main": "./lib/index.js", "scripts": { diff --git a/src/useSharedState.js b/src/useSharedState.js index 938c33b..88d7b5f 100644 --- a/src/useSharedState.js +++ b/src/useSharedState.js @@ -4,6 +4,7 @@ import { useContext, useEffect, useState, useCallback, useRef } from 'react'; import { isPlainObject, deepClone } from 'mout/lang'; import { equals as arrayShallowEquals } from 'mout/array'; import { equals as objectShallowEquals } from 'mout/object'; +import { identity } from 'mout/function' import { ShareContext } from './SharedStateProvider'; /** questions @@ -15,7 +16,7 @@ export function useSharedState( doc_id: string, projector?: any => any, deps?: Array -): [any, (mixed) => Promise] { +): [any, ((any) => any | any) => Promise] { const connection = useContext(ShareContext); if (!connection) { @@ -37,9 +38,7 @@ export function useSharedState( const has_projector = !!projector; const should_clone = true; - const memo_projector = useCallback(projector || (state => state), [ - has_projector, - ]); + const memo_projector = useCallback(projector || identity, deps); const [{ state }, setState] = useState({ state: has_projector @@ -52,16 +51,25 @@ export function useSharedState( const state_ref = useRef(state); const dispatch = useCallback( - action => { - // todo: allow action to be a function that takes unprojected/unmapped state - // and returns an action + (action: any => any | any): Promise => { // todo: add validation that prevents submitting ops when the doc has been destroyed - - return new Promise(resolve => { - maybe_doc.submitOp(action, () => { - // todo: figure out how errors are to be handled - resolve(); - }); + return new Promise((resolve, reject) => { + try { + maybe_doc.submitOp( + typeof action === 'function' + ? action(maybe_doc.data) + : action, + err => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + } catch (err) { + reject(err); + } }); }, [maybe_doc] @@ -141,7 +149,7 @@ export function useSharedState( return () => { maybe_doc.removeListener('op', handle_op); }; - }, [maybe_doc, has_projector, memo_projector]); + }, [should_clone, maybe_doc, has_projector, memo_projector]); useEffect(() => { // increment the subscription ref count to indicate that this component is now subscribed to the doc diff --git a/src/useSharedState.projector.test.js b/src/useSharedState.projector.test.js index e4f4d6b..e8225f1 100644 --- a/src/useSharedState.projector.test.js +++ b/src/useSharedState.projector.test.js @@ -659,3 +659,82 @@ test('react-sharedb: useSharedState updating directly projected object in doc st unmount(); assert.end(); }); + +test('react-sharedb: passing a function in to dispatch', async assert => { + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + count2: 2, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + let renderer = {}; + + const MockWrapper = () => { + const [count1, submit] = useSharedState( + collection, + doc_id, + ({ count1 }) => count1 + ); + + return ; + }; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + act(() => { + mock_instance.props.onSubmit(state => { + return { + p: ['count1'], + oi: state.count2, + }; + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 2, + "The 's count1 prop is now equal to its count2 prop." + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); From 68238af44fd46d4ce946c5f7acdc3f9597ac3075 Mon Sep 17 00:00:00 2001 From: Jason Crider Date: Tue, 30 Jul 2019 13:31:08 -0400 Subject: [PATCH 6/6] :white_check_mark: updated a test --- src/useSharedState.projector.test.js | 102 +++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/src/useSharedState.projector.test.js b/src/useSharedState.projector.test.js index e8225f1..97c574f 100644 --- a/src/useSharedState.projector.test.js +++ b/src/useSharedState.projector.test.js @@ -1,7 +1,7 @@ // @flow import test from 'tape'; import sharedb from 'sharedb'; -import React, { Suspense, useRef } from 'react'; +import React, { Suspense, useRef, useState } from 'react'; import { useSharedState } from './useSharedState'; import TestRenderer, { act } from 'react-test-renderer'; import { SharedStateProvider } from './SharedStateProvider'; @@ -504,8 +504,8 @@ test('react-sharedb: useSharedState updating projected plain object tuple', asyn assert.equals( mock_instance.props.prop1, 1, - 'The property was properly updated', - ) + 'The property was properly updated' + ); act(() => { mock_instance.props.onSubmit({ @@ -519,7 +519,7 @@ test('react-sharedb: useSharedState updating projected plain object tuple', asyn { //const remote_doc = remote.get(collection, doc_id); - const [ mock_instance ] = root.findAllByType( Mock ); + const [mock_instance] = root.findAllByType(Mock); assert.equals( mock_instance.props.getRenderCount(), @@ -550,7 +550,7 @@ test('react-sharedb: useSharedState updating directly projected object in doc st prop1: 'one', prop2: 0, prop3: '', - } + }, }; // create the doc via the secondary connection @@ -562,7 +562,7 @@ test('react-sharedb: useSharedState updating directly projected object in doc st const [{ prop1, prop2 }, submit] = useSharedState( collection, doc_id, - ({ thing }) => thing, + ({ thing }) => thing ); const render_count_ref = useRef(0); @@ -620,11 +620,7 @@ test('react-sharedb: useSharedState updating directly projected object in doc st { const [mock_instance] = root.findAllByType(Mock); - assert.equals( - mock_instance.props.prop1, - 'two', - 'The prop was updated.' - ) + assert.equals(mock_instance.props.prop1, 'two', 'The prop was updated.'); assert.equals( mock_instance.props.getRenderCount(), @@ -649,7 +645,7 @@ test('react-sharedb: useSharedState updating directly projected object in doc st assert.equals( mock_instance.props.getRenderCount(), 2, - 'Rendered exactly 2 times' + 'Rendered exactly 2 times after updating doc but not changing value' ); } @@ -657,7 +653,7 @@ test('react-sharedb: useSharedState updating directly projected object in doc st remote_doc.destroy(); unmount(); - assert.end(); + assert.end(); }); test('react-sharedb: passing a function in to dispatch', async assert => { @@ -738,3 +734,83 @@ test('react-sharedb: passing a function in to dispatch', async assert => { unmount(); assert.end(); }); + +test('react-sharedb: passing in deps', async assert => { + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + let renderer = {}; + + const MockWrapper = () => { + const [num, setNum] = useState(0); + const [projection, onSubmit] = useSharedState( + collection, + doc_id, + () => num, + [] + ); + + return ; + }; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals(mock_instance.props.projection, 0, 'Projected value is 0'); + + act(() => { + mock_instance.props.setNum(num => num + 1); + }); + + act(() => { + mock_instance.props.onSubmit({ + p: ['count1'], + oi: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals(mock_instance.props.projection, 0, 'Projected value is still 0'); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +});