Skip to content

Commit 3611670

Browse files
Merge pull request #12 from effector/fix-client-detection
Fix client detection and add tests for that
2 parents 8b06729 + a1e2aaf commit 3611670

File tree

5 files changed

+202
-123
lines changed

5 files changed

+202
-123
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@vitejs/plugin-react": "^3.1.0",
3232
"effector": "^22.8.1",
3333
"effector-react": "^22.5.1",
34+
"happy-dom": "^9.10.9",
3435
"react": "^18.2.0",
3536
"typescript": "^5.0.3",
3637
"vite": "^4.2.1",

pnpm-lock.yaml

Lines changed: 53 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/get-scope.browser.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// @vitest-environment happy-dom
2+
3+
import { describe, test, expect } from "vitest";
4+
import {
5+
createStore,
6+
createEvent,
7+
createEffect,
8+
fork,
9+
serialize,
10+
allSettled,
11+
combine,
12+
sample,
13+
} from "effector";
14+
15+
import { getScope } from "./get-scope";
16+
17+
const up = createEvent();
18+
const longUpFx = createEffect(async () => {
19+
await new Promise((r) => setTimeout(r, 10));
20+
});
21+
const $count = createStore(0).on([up, longUpFx.done], (s) => s + 1);
22+
const $derived = $count.map((s) => ({ ref: s }));
23+
const $combined = combine({ ref: $count });
24+
const $nestedCombined = combine({ ref: $derived });
25+
26+
const $sampled = sample({
27+
source: { ref: $combined },
28+
fn: (ref) => ref.ref.ref,
29+
});
30+
31+
const getFixedDate = () => new Date(0);
32+
const updateDate = createEvent<Date>();
33+
const $specialData = createStore(getFixedDate(), {
34+
serialize: {
35+
write: (_date) => ({ lol: "jsonified view" }),
36+
read: (_json) => getFixedDate(),
37+
},
38+
}).on($count, () => getFixedDate());
39+
40+
describe("getClientScope", () => {
41+
test("should handle server values injection on the fly", async () => {
42+
const serverScope = fork();
43+
44+
await allSettled(up, { scope: serverScope });
45+
await allSettled(up, { scope: serverScope });
46+
await allSettled(up, { scope: serverScope });
47+
48+
const serverValues = serialize(serverScope);
49+
50+
const clientScopeOne = getScope();
51+
52+
expect(clientScopeOne.getState($count)).toEqual(0);
53+
expect(clientScopeOne.getState($derived)).toEqual({ ref: 0 });
54+
expect(clientScopeOne.getState($combined)).toEqual({ ref: 0 });
55+
expect(clientScopeOne.getState($nestedCombined)).toEqual({
56+
ref: { ref: 0 },
57+
});
58+
expect(clientScopeOne.getState($sampled)).toEqual(0);
59+
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(false);
60+
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(0);
61+
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
62+
63+
const promise = allSettled(longUpFx, { scope: clientScopeOne });
64+
65+
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(1);
66+
67+
const clientScopeTwo = getScope(serverValues);
68+
69+
expect(clientScopeTwo.getState($count)).toEqual(3);
70+
expect(clientScopeOne.getState($derived)).toEqual({ ref: 3 });
71+
expect(clientScopeOne.getState($combined)).toEqual({ ref: 3 });
72+
expect(clientScopeOne.getState($nestedCombined)).toEqual({
73+
ref: { ref: 3 },
74+
});
75+
expect(clientScopeOne.getState($sampled)).toEqual(3);
76+
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(true);
77+
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(1);
78+
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
79+
80+
await promise;
81+
82+
expect(clientScopeTwo.getState($count)).toEqual(4);
83+
expect(clientScopeOne.getState($derived)).toEqual({ ref: 4 });
84+
expect(clientScopeOne.getState($combined)).toEqual({ ref: 4 });
85+
expect(clientScopeOne.getState($nestedCombined)).toEqual({
86+
ref: { ref: 4 },
87+
});
88+
expect(clientScopeOne.getState($sampled)).toEqual(4);
89+
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(false);
90+
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(0);
91+
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
92+
});
93+
test("shallow navigation to same page", async () => {
94+
const serverScope = fork();
95+
96+
await allSettled(up, { scope: serverScope });
97+
await allSettled(up, { scope: serverScope });
98+
await allSettled(up, { scope: serverScope });
99+
100+
const values = serialize(serverScope);
101+
102+
const clientScopeOne = getScope(values);
103+
104+
expect(clientScopeOne.getState($count)).toEqual(3);
105+
106+
await allSettled(up, { scope: clientScopeOne });
107+
108+
expect(clientScopeOne.getState($count)).toEqual(4);
109+
110+
// This imitates shallow navigation to same page, e.g. with different query params
111+
//
112+
// Next.js will reuse the same pageProps instance in this case
113+
// which will basically override current page state with initial one
114+
//
115+
// So we need to basically just ignore it, because
116+
// we already have the latest state in the client scope
117+
const clientScopeTwo = getScope(values);
118+
119+
expect(clientScopeTwo.getState($count)).toEqual(4);
120+
});
121+
});
122+
123+
describe("getScope implementation details", () => {
124+
test("should return same scope on client every time", () => {
125+
/**
126+
* Implementation detail that may change in the future
127+
*/
128+
const scopeOne = getScope();
129+
const scopeTwo = getScope();
130+
131+
expect(scopeOne === scopeTwo).toBe(true);
132+
});
133+
});

src/get-scope.test.ts

Lines changed: 11 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,15 @@
11
import { describe, test, expect } from "vitest";
2-
import {
3-
createStore,
4-
createEvent,
5-
createEffect,
6-
fork,
7-
serialize,
8-
allSettled,
9-
combine,
10-
sample,
11-
} from "effector";
122

13-
import { internalGetClientScope } from "./get-scope";
3+
import { getScope } from "./get-scope";
144

15-
const up = createEvent();
16-
const longUpFx = createEffect(async () => {
17-
await new Promise((r) => setTimeout(r, 10));
18-
});
19-
const $count = createStore(0).on([up, longUpFx.done], (s) => s + 1);
20-
const $derived = $count.map((s) => ({ ref: s }));
21-
const $combined = combine({ ref: $count });
22-
const $nestedCombined = combine({ ref: $derived });
5+
describe('getScope implementation details', () => {
6+
test('should return new scope on server every time', () => {
7+
/**
8+
* Implementation detail that may change in the future
9+
*/
10+
const scopeOne = getScope();
11+
const scopeTwo = getScope();
2312

24-
const $sampled = sample({
25-
source: { ref: $combined },
26-
fn: (ref) => ref.ref.ref,
27-
});
28-
29-
const getFixedDate = () => new Date(0);
30-
const updateDate = createEvent<Date>();
31-
const $specialData = createStore(getFixedDate(), {
32-
serialize: {
33-
write: (_date) => ({ lol: "jsonified view" }),
34-
read: (_json) => getFixedDate(),
35-
},
36-
}).on($count, () => getFixedDate());
37-
38-
describe("getClientScope", () => {
39-
test("should handle server values injection on the fly", async () => {
40-
const serverScope = fork();
41-
42-
await allSettled(up, { scope: serverScope });
43-
await allSettled(up, { scope: serverScope });
44-
await allSettled(up, { scope: serverScope });
45-
46-
const serverValues = serialize(serverScope);
47-
48-
const clientScopeOne = internalGetClientScope();
49-
50-
expect(clientScopeOne.getState($count)).toEqual(0);
51-
expect(clientScopeOne.getState($derived)).toEqual({ ref: 0 });
52-
expect(clientScopeOne.getState($combined)).toEqual({ ref: 0 });
53-
expect(clientScopeOne.getState($nestedCombined)).toEqual({
54-
ref: { ref: 0 },
55-
});
56-
expect(clientScopeOne.getState($sampled)).toEqual(0);
57-
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(false);
58-
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(0);
59-
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
60-
61-
const promise = allSettled(longUpFx, { scope: clientScopeOne });
62-
63-
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(1);
64-
65-
const clientScopeTwo = internalGetClientScope(serverValues);
66-
67-
expect(clientScopeTwo.getState($count)).toEqual(3);
68-
expect(clientScopeOne.getState($derived)).toEqual({ ref: 3 });
69-
expect(clientScopeOne.getState($combined)).toEqual({ ref: 3 });
70-
expect(clientScopeOne.getState($nestedCombined)).toEqual({
71-
ref: { ref: 3 },
72-
});
73-
expect(clientScopeOne.getState($sampled)).toEqual(3);
74-
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(true);
75-
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(1);
76-
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
77-
78-
await promise;
79-
80-
expect(clientScopeTwo.getState($count)).toEqual(4);
81-
expect(clientScopeOne.getState($derived)).toEqual({ ref: 4 });
82-
expect(clientScopeOne.getState($combined)).toEqual({ ref: 4 });
83-
expect(clientScopeOne.getState($nestedCombined)).toEqual({
84-
ref: { ref: 4 },
85-
});
86-
expect(clientScopeOne.getState($sampled)).toEqual(4);
87-
expect(clientScopeOne.getState(longUpFx.pending)).toEqual(false);
88-
expect(clientScopeOne.getState(longUpFx.inFlight)).toEqual(0);
89-
expect(clientScopeOne.getState($specialData)).toEqual(getFixedDate());
90-
});
91-
test("shallow navigation to same page", async () => {
92-
const serverScope = fork();
93-
94-
await allSettled(up, { scope: serverScope });
95-
await allSettled(up, { scope: serverScope });
96-
await allSettled(up, { scope: serverScope });
97-
98-
const values = serialize(serverScope);
99-
100-
const clientScopeOne = internalGetClientScope(values);
101-
102-
expect(clientScopeOne.getState($count)).toEqual(3);
103-
104-
await allSettled(up, { scope: clientScopeOne });
105-
106-
expect(clientScopeOne.getState($count)).toEqual(4);
107-
108-
// This imitates shallow navigation to same page, e.g. with different query params
109-
//
110-
// Next.js will reuse the same pageProps instance in this case
111-
// which will basically override current page state with initial one
112-
//
113-
// So we need to basically just ignore it, because
114-
// we already have the latest state in the client scope
115-
const clientScopeTwo = internalGetClientScope(values);
116-
117-
expect(clientScopeTwo.getState($count)).toEqual(4);
118-
});
119-
});
13+
expect(scopeOne !== scopeTwo).toBe(true);
14+
})
15+
})

0 commit comments

Comments
 (0)