PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / unit / QuickEdit / lib / block-source-cache.test.js

block-source-cache.test.js in Extendify 3.1.0, at tests/unit/QuickEdit/lib/block-source-cache.test.js

182 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Net-new helper in the rebuild. Pins the contracts the cache guarantees for
2 // both source kinds (post + template-part):
3 // 1. In-flight Promises are shared across concurrent consumers (one
4 // fetch for hover-prefetch + click).
5 // 2. post and template-part blocks number in separate spaces, so post #N
6 // and header #N must not collide on the same cache key.
7 // 3. Failed Promises are evicted so the next consumer retries fresh.
8 // 4. invalidateBlockSource clears a specific entry post-save so a
9 // re-edit mounts against the freshly rendered markup.
10
11 jest.mock('@wordpress/api-fetch', () => ({
12 __esModule: true,
13 default: jest.fn(),
14 }));
15
16 jest.mock('@wordpress/url', () => ({
17 addQueryArgs: (path, args) => {
18 const params = new URLSearchParams(args).toString();
19 return `${path}?${params}`;
20 },
21 }));
22
23 const POST = { kind: 'post', id: 7 };
24 const PART = { kind: 'template-part', partSlug: 'header' };
25
26 let apiFetch;
27
28 beforeEach(() => {
29 jest.resetModules();
30 jest.clearAllMocks();
31 apiFetch = require('@wordpress/api-fetch').default;
32 apiFetch.mockReset();
33 });
34
35 describe('prefetchBlockSource — short-circuits', () => {
36 it('returns null and skips the fetch when the source is missing', async () => {
37 const { prefetchBlockSource } = await import(
38 '@quick-edit/lib/block-source-cache'
39 );
40 expect(prefetchBlockSource(null, 12)).toBeNull();
41 expect(apiFetch).not.toHaveBeenCalled();
42 });
43
44 it('returns null and skips the fetch when blockId is missing', async () => {
45 const { prefetchBlockSource } = await import(
46 '@quick-edit/lib/block-source-cache'
47 );
48 expect(prefetchBlockSource(POST, null)).toBeNull();
49 expect(apiFetch).not.toHaveBeenCalled();
50 });
51
52 it('returns null for sources loaded through other endpoints (product/wpforms/nav)', async () => {
53 const { prefetchBlockSource } = await import(
54 '@quick-edit/lib/block-source-cache'
55 );
56 expect(prefetchBlockSource({ kind: 'product', id: 9 }, 12)).toBeNull();
57 expect(prefetchBlockSource({ kind: 'template-part' }, 12)).toBeNull();
58 expect(apiFetch).not.toHaveBeenCalled();
59 });
60 });
61
62 describe('prefetchBlockSource — request shape', () => {
63 it('passes postId + blockId for a post source', async () => {
64 apiFetch.mockResolvedValue({ block: '<p>hi</p>' });
65 const { prefetchBlockSource } = await import(
66 '@quick-edit/lib/block-source-cache'
67 );
68 await prefetchBlockSource(POST, 12);
69 expect(apiFetch).toHaveBeenCalledWith({
70 path: '/extendify/v1/agent/get-block-code?postId=7&blockId=12',
71 });
72 });
73
74 it('passes partSlug + blockId for a template-part source', async () => {
75 apiFetch.mockResolvedValue({ block: '<p>hi</p>' });
76 const { prefetchBlockSource } = await import(
77 '@quick-edit/lib/block-source-cache'
78 );
79 await prefetchBlockSource(PART, 32);
80 expect(apiFetch).toHaveBeenCalledWith({
81 path: '/extendify/v1/agent/get-block-code?partSlug=header&blockId=32',
82 });
83 });
84 });
85
86 describe('prefetchBlockSource — cache semantics', () => {
87 it('returns the same in-flight Promise for repeated calls with the same key', async () => {
88 apiFetch.mockReturnValue(new Promise(() => {}));
89 const { prefetchBlockSource } = await import(
90 '@quick-edit/lib/block-source-cache'
91 );
92 const a = prefetchBlockSource(POST, 12);
93 const b = prefetchBlockSource(POST, 12);
94 expect(a).toBe(b);
95 expect(apiFetch).toHaveBeenCalledTimes(1);
96 });
97
98 it('caches by (kind, discriminator, blockId) — different keys fetch independently', async () => {
99 apiFetch.mockResolvedValue({});
100 const { prefetchBlockSource } = await import(
101 '@quick-edit/lib/block-source-cache'
102 );
103 await prefetchBlockSource(POST, 12);
104 await prefetchBlockSource(POST, 13);
105 await prefetchBlockSource({ kind: 'post', id: 8 }, 12);
106 expect(apiFetch).toHaveBeenCalledTimes(3);
107 });
108
109 it('does not collide post #N with template-part #N (separate numbering spaces)', async () => {
110 apiFetch.mockResolvedValue({});
111 const { prefetchBlockSource } = await import(
112 '@quick-edit/lib/block-source-cache'
113 );
114 await prefetchBlockSource(POST, 5);
115 await prefetchBlockSource(PART, 5);
116 expect(apiFetch).toHaveBeenCalledTimes(2);
117 });
118 });
119
120 describe('prefetchBlockSource — failure eviction', () => {
121 it('evicts the cache entry on fetch rejection so the next call retries', async () => {
122 apiFetch
123 .mockRejectedValueOnce(new Error('boom'))
124 .mockResolvedValueOnce({ block: '<p>ok</p>' });
125 const { prefetchBlockSource } = await import(
126 '@quick-edit/lib/block-source-cache'
127 );
128
129 await expect(prefetchBlockSource(POST, 12)).rejects.toThrow('boom');
130 const second = prefetchBlockSource(POST, 12);
131 await expect(second).resolves.toEqual({ block: '<p>ok</p>' });
132 expect(apiFetch).toHaveBeenCalledTimes(2);
133 });
134 });
135
136 describe('getBlockSource — alias of prefetchBlockSource', () => {
137 it('is the exact same function reference', async () => {
138 const mod = await import('@quick-edit/lib/block-source-cache');
139 expect(mod.getBlockSource).toBe(mod.prefetchBlockSource);
140 });
141 });
142
143 describe('invalidateBlockSource', () => {
144 it('drops only the specified (source, blockId) entry', async () => {
145 apiFetch.mockResolvedValue({});
146 const { prefetchBlockSource, invalidateBlockSource } = await import(
147 '@quick-edit/lib/block-source-cache'
148 );
149 await prefetchBlockSource(POST, 12);
150 await prefetchBlockSource(POST, 13);
151
152 invalidateBlockSource(POST, 12);
153 await prefetchBlockSource(POST, 12);
154 await prefetchBlockSource(POST, 13);
155
156 expect(apiFetch).toHaveBeenCalledTimes(3);
157 });
158
159 it('invalidates a template-part entry independently of the post entry', async () => {
160 apiFetch.mockResolvedValue({});
161 const { prefetchBlockSource, invalidateBlockSource } = await import(
162 '@quick-edit/lib/block-source-cache'
163 );
164 await prefetchBlockSource(POST, 5);
165 await prefetchBlockSource(PART, 5);
166
167 invalidateBlockSource(PART, 5);
168 await prefetchBlockSource(POST, 5); // still cached → no refetch
169 await prefetchBlockSource(PART, 5); // evicted → refetch
170
171 expect(apiFetch).toHaveBeenCalledTimes(3);
172 });
173
174 it('is a no-op for missing source / blockId', async () => {
175 const { invalidateBlockSource } = await import(
176 '@quick-edit/lib/block-source-cache'
177 );
178 expect(() => invalidateBlockSource(null, 12)).not.toThrow();
179 expect(() => invalidateBlockSource(POST, null)).not.toThrow();
180 });
181 });
182