pict-section-recordset 1.0.63 → 1.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ /*
2
+ Unit tests for Pict-Template-FilterInstanceViews.renderAsync.
3
+
4
+ Exercises the parallel filter fan-out, per-filter transaction isolation,
5
+ deferred post-render drain, render-epoch guard, and transaction-map cleanup
6
+ added for the filter render performance refactor.
7
+ */
8
+
9
+ const libBrowserEnv = require('browser-env');
10
+ libBrowserEnv({ url: 'http://localhost/' });
11
+
12
+ const Chai = require('chai');
13
+ const Expect = Chai.expect;
14
+
15
+ const libPict = require('pict');
16
+ const libPictView = require('pict-view');
17
+ const libPictTemplateFilterInstanceViews = require('../source/templates/Pict-Template-FilterInstanceViews.js');
18
+
19
+ /**
20
+ * Minimal stub filter view that records every interaction and lets each test
21
+ * control when the render and drain callbacks fire. The stub never touches
22
+ * the DOM and never spawns sub-renders - everything is recorded so the tests
23
+ * can inspect ordering.
24
+ */
25
+ class StubFilterView extends libPictView
26
+ {
27
+ constructor(pFable, pOptions, pServiceHash)
28
+ {
29
+ super(pFable, pOptions, pServiceHash);
30
+ this.recordedCalls = [];
31
+ this.pendingRenderCallbacks = [];
32
+ this.pendingDrainCallbacks = [];
33
+ this._fakeOutput = pOptions && pOptions.fakeOutput ? pOptions.fakeOutput : '<stub/>';
34
+ }
35
+
36
+ prepareRecord(pRecord)
37
+ {
38
+ this.recordedCalls.push({ type: 'prepareRecord', hash: pRecord && pRecord.Hash });
39
+ }
40
+
41
+ renderWithScopeAsync(pScope, pRenderableHash, pDestinationAddress, pRecord, pRootRenderable, fCallback)
42
+ {
43
+ this.recordedCalls.push({
44
+ type: 'renderWithScopeAsync',
45
+ rootRenderable: pRootRenderable,
46
+ destinationAddress: pDestinationAddress,
47
+ clauseHash: pRecord && pRecord.Hash,
48
+ });
49
+ // Simulate a real render writing into the pict template output cache.
50
+ const tmpCacheKey = pDestinationAddress.split('.')[1];
51
+ const tmpIndex = this.pendingRenderCallbacks.length;
52
+ this.pict.__TemplateOutputCache[tmpCacheKey] = `${this._fakeOutput}-${tmpIndex}`;
53
+ // Defer the callback until the test fires it manually.
54
+ this.pendingRenderCallbacks.push(() => fCallback());
55
+ }
56
+
57
+ onAfterRenderAsync(fCallback, pRenderable)
58
+ {
59
+ this.recordedCalls.push({
60
+ type: 'onAfterRenderAsync',
61
+ rootRenderable: pRenderable,
62
+ });
63
+ // Defer the drain callback until the test fires it manually.
64
+ this.pendingDrainCallbacks.push(() => fCallback());
65
+ }
66
+
67
+ fireAllRenderCallbacks()
68
+ {
69
+ while (this.pendingRenderCallbacks.length > 0)
70
+ {
71
+ this.pendingRenderCallbacks.shift()();
72
+ }
73
+ }
74
+
75
+ fireAllDrainCallbacks()
76
+ {
77
+ while (this.pendingDrainCallbacks.length > 0)
78
+ {
79
+ this.pendingDrainCallbacks.shift()();
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Stand up a pict instance with just enough wiring for the FilterInstanceViews
86
+ * template to run. Returns the pict instance, the template instance, and the
87
+ * stub filter view.
88
+ *
89
+ * @param {Array<object>} pFilterClauses - Filter clauses to populate into Bundle._ActiveFilterState.
90
+ */
91
+ function buildHarness(pFilterClauses)
92
+ {
93
+ const _Pict = new libPict();
94
+ _Pict.LogNoisiness = 0;
95
+
96
+ // FilterInstanceViews.renderAsync expects pict.PictSectionRecordSet.recordSetProviderConfigurations
97
+ // to resolve the record set. Stub the minimum shape.
98
+ _Pict.PictSectionRecordSet = {
99
+ recordSetProviderConfigurations: {
100
+ TestRS: { RecordSet: 'TestRS' },
101
+ },
102
+ };
103
+
104
+ // Register a stub filter view so _getViewForFilterClause returns it for
105
+ // clauses of type 'StubFilterType'. Doing this BEFORE adding the
106
+ // 'PRSP-Filters' fake entry below, because addView creates a real view
107
+ // in the pict.views map and we need the hash to match what
108
+ // _getViewForFilterClause looks up.
109
+ _Pict.addView('PRSP-FilterType-StubFilterType', {}, StubFilterView);
110
+
111
+ // FilterInstanceViews expects pict.views['PRSP-Filters'] to have a
112
+ // _renderEpoch field so the deferred drain can snapshot + compare it.
113
+ // Direct assignment on the views map is fine for a stub.
114
+ _Pict.views['PRSP-Filters'] = { _renderEpoch: 0 };
115
+
116
+ // Populate the active filter state that renderAsync reads.
117
+ _Pict.Bundle._ActiveFilterState = {
118
+ TestRS: { FilterClauses: pFilterClauses },
119
+ };
120
+
121
+ // Register the template instance we want to test and capture it.
122
+ const tmpInstruction = _Pict.addTemplate(libPictTemplateFilterInstanceViews);
123
+
124
+ return {
125
+ pict: _Pict,
126
+ instruction: tmpInstruction,
127
+ stubView: _Pict.views['PRSP-FilterType-StubFilterType'],
128
+ };
129
+ }
130
+
131
+ suite('PictSectionRecordSet FilterInstanceViews Render', () =>
132
+ {
133
+ suite('renderAsync', () =>
134
+ {
135
+ test('is a no-op when there are zero clauses', (fDone) =>
136
+ {
137
+ const tmpHarness = buildHarness([]);
138
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, (pError, pResult) =>
139
+ {
140
+ Expect(pError).to.be.null;
141
+ Expect(pResult).to.equal('');
142
+ Expect(tmpHarness.stubView.recordedCalls).to.deep.equal([]);
143
+ return fDone();
144
+ });
145
+ });
146
+
147
+ test('fans out all filter renders in parallel before any callback fires', (fDone) =>
148
+ {
149
+ const tmpClauses = [
150
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
151
+ { Type: 'StubFilterType', Hash: 'clauseB', FilterByColumn: 'B' },
152
+ { Type: 'StubFilterType', Hash: 'clauseC', FilterByColumn: 'C' },
153
+ ];
154
+ const tmpHarness = buildHarness(tmpClauses);
155
+
156
+ let tmpFinalCallbackFired = false;
157
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, () =>
158
+ {
159
+ tmpFinalCallbackFired = true;
160
+ });
161
+
162
+ // At this point every filter should have had renderWithScopeAsync
163
+ // called, even though zero render callbacks have fired yet. A
164
+ // sequential version would have only called renderWithScopeAsync
165
+ // on the first filter at this point.
166
+ const tmpRenderStarts = tmpHarness.stubView.recordedCalls.filter((c) => c.type === 'renderWithScopeAsync');
167
+ Expect(tmpRenderStarts.length).to.equal(3, 'all three filter renders should have started in parallel');
168
+ Expect(tmpFinalCallbackFired).to.equal(false, 'final callback must not fire until every render completes');
169
+
170
+ // Fire all render callbacks, then finalize runs synchronously.
171
+ tmpHarness.stubView.fireAllRenderCallbacks();
172
+ Expect(tmpFinalCallbackFired).to.equal(true, 'final callback should fire once every render completes');
173
+ return fDone();
174
+ });
175
+
176
+ test('concatenates per-filter output in clause order', (fDone) =>
177
+ {
178
+ const tmpClauses = [
179
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
180
+ { Type: 'StubFilterType', Hash: 'clauseB', FilterByColumn: 'B' },
181
+ { Type: 'StubFilterType', Hash: 'clauseC', FilterByColumn: 'C' },
182
+ ];
183
+ const tmpHarness = buildHarness(tmpClauses);
184
+
185
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, (pError, pResult) =>
186
+ {
187
+ Expect(pError).to.be.null;
188
+ // Stub output is `<stub/>-N` where N is the render start index.
189
+ // Since all three are started in order, the outputs are 0, 1, 2.
190
+ Expect(pResult).to.equal('<stub/>-0<stub/>-1<stub/>-2');
191
+ return fDone();
192
+ });
193
+ tmpHarness.stubView.fireAllRenderCallbacks();
194
+ });
195
+
196
+ test('each filter render gets its own transaction hash (not the dashboard root)', (fDone) =>
197
+ {
198
+ const tmpClauses = [
199
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
200
+ { Type: 'StubFilterType', Hash: 'clauseB', FilterByColumn: 'B' },
201
+ ];
202
+ const tmpHarness = buildHarness(tmpClauses);
203
+
204
+ // Pass a fake dashboard root as pState.RootRenderable. The template
205
+ // must NOT propagate this transaction hash into the per-filter
206
+ // renders - each should have its own.
207
+ const tmpFakeDashboardRoot =
208
+ {
209
+ TransactionHash: 'DashboardTx-shouldNotLeak',
210
+ RootRenderableViewHash: 'PRSP-Dashboard-Stub',
211
+ };
212
+
213
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, () =>
214
+ {
215
+ const tmpRenderStarts = tmpHarness.stubView.recordedCalls.filter((c) => c.type === 'renderWithScopeAsync');
216
+ const tmpTransactionHashes = tmpRenderStarts.map((c) => c.rootRenderable.TransactionHash);
217
+
218
+ Expect(tmpTransactionHashes).to.have.lengthOf(2);
219
+ Expect(tmpTransactionHashes[0]).to.not.equal('DashboardTx-shouldNotLeak');
220
+ Expect(tmpTransactionHashes[1]).to.not.equal('DashboardTx-shouldNotLeak');
221
+ Expect(tmpTransactionHashes[0]).to.not.equal(tmpTransactionHashes[1]);
222
+ Expect(tmpTransactionHashes[0]).to.match(/^FilterInstance-/);
223
+ Expect(tmpTransactionHashes[1]).to.match(/^FilterInstance-/);
224
+ return fDone();
225
+ }, null, null, { RootRenderable: tmpFakeDashboardRoot });
226
+
227
+ tmpHarness.stubView.fireAllRenderCallbacks();
228
+ });
229
+
230
+ test('fires fCallback BEFORE draining deferred post-render work', (fDone) =>
231
+ {
232
+ const tmpClauses = [
233
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
234
+ { Type: 'StubFilterType', Hash: 'clauseB', FilterByColumn: 'B' },
235
+ ];
236
+ const tmpHarness = buildHarness(tmpClauses);
237
+
238
+ let tmpFinalCallbackFired = false;
239
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, () =>
240
+ {
241
+ tmpFinalCallbackFired = true;
242
+ });
243
+
244
+ // Complete all filter renders synchronously.
245
+ tmpHarness.stubView.fireAllRenderCallbacks();
246
+ Expect(tmpFinalCallbackFired).to.equal(true, 'fCallback should have fired once every render completed');
247
+
248
+ // At this exact moment no drain has run yet: the drain is queued
249
+ // behind a setTimeout(0) macrotask.
250
+ const tmpDrainsBeforeTick = tmpHarness.stubView.recordedCalls.filter((c) => c.type === 'onAfterRenderAsync').length;
251
+ Expect(tmpDrainsBeforeTick).to.equal(0, 'no drain should have run yet - it is scheduled on the next macrotask');
252
+
253
+ // Let the setTimeout(0) fire.
254
+ setTimeout(() =>
255
+ {
256
+ const tmpDrainsAfterTick = tmpHarness.stubView.recordedCalls.filter((c) => c.type === 'onAfterRenderAsync').length;
257
+ Expect(tmpDrainsAfterTick).to.equal(2, 'both filter drains should have run on the next macrotask');
258
+ return fDone();
259
+ }, 5);
260
+ });
261
+
262
+ test('epoch guard: a drain scheduled before a filter re-render is skipped', (fDone) =>
263
+ {
264
+ const tmpClauses = [
265
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
266
+ ];
267
+ const tmpHarness = buildHarness(tmpClauses);
268
+ const tmpFiltersView = tmpHarness.pict.views['PRSP-Filters'];
269
+
270
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, () => {});
271
+ tmpHarness.stubView.fireAllRenderCallbacks();
272
+
273
+ // Simulate a filter mutation that bumps the epoch between the
274
+ // dashboard callback firing and the setTimeout drain running.
275
+ tmpFiltersView._renderEpoch = tmpFiltersView._renderEpoch + 1;
276
+
277
+ setTimeout(() =>
278
+ {
279
+ const tmpDrainCalls = tmpHarness.stubView.recordedCalls.filter((c) => c.type === 'onAfterRenderAsync').length;
280
+ Expect(tmpDrainCalls).to.equal(0, 'drain should have been skipped because the epoch changed');
281
+ return fDone();
282
+ }, 5);
283
+ });
284
+
285
+ test('transaction map entries for filter renders are cleaned up after the drain', (fDone) =>
286
+ {
287
+ const tmpClauses = [
288
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
289
+ { Type: 'StubFilterType', Hash: 'clauseB', FilterByColumn: 'B' },
290
+ ];
291
+ const tmpHarness = buildHarness(tmpClauses);
292
+
293
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'TestRS' }, () => {});
294
+ tmpHarness.stubView.fireAllRenderCallbacks();
295
+
296
+ // Right after fCallback fires, both filter transactions are
297
+ // registered in the map (the drain has not run yet).
298
+ const tmpFilterKeysBefore = Object.keys(tmpHarness.pict.TransactionTracking.transactionMap)
299
+ .filter((k) => k.startsWith('FilterInstance-'));
300
+ Expect(tmpFilterKeysBefore.length).to.equal(2, 'both filter transactions should be registered before the drain runs');
301
+
302
+ setTimeout(() =>
303
+ {
304
+ // The drain kicks off both filters' onAfterRenderAsync calls.
305
+ // Fire their drain completion callbacks so the cleanup runs.
306
+ tmpHarness.stubView.fireAllDrainCallbacks();
307
+
308
+ const tmpFilterKeysAfter = Object.keys(tmpHarness.pict.TransactionTracking.transactionMap)
309
+ .filter((k) => k.startsWith('FilterInstance-'));
310
+ Expect(tmpFilterKeysAfter.length).to.equal(0, 'transaction map should be cleaned up after the drain completes');
311
+ return fDone();
312
+ }, 5);
313
+ });
314
+
315
+ test('bails early with empty result when the record set is not configured', (fDone) =>
316
+ {
317
+ const tmpHarness = buildHarness([
318
+ { Type: 'StubFilterType', Hash: 'clauseA', FilterByColumn: 'A' },
319
+ ]);
320
+ tmpHarness.instruction.renderAsync('', { RecordSet: 'NotConfigured' }, (pError, pResult) =>
321
+ {
322
+ Expect(pError).to.be.null;
323
+ Expect(pResult).to.equal('');
324
+ return fDone();
325
+ });
326
+ });
327
+ });
328
+ });
File without changes