@adobe/data-lit 0.10.15 → 0.10.18

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.
@@ -96,6 +96,24 @@ class LifecycleProbeElement extends LitElement {
96
96
  }
97
97
  customElements.define("lifecycle-probe-element", LifecycleProbeElement);
98
98
 
99
+ /**
100
+ * Renders a plain mutable field (no hook, no requestUpdate on change). Used to prove
101
+ * that a reconnect forces a re-render so mutable non-hook state refreshes after a move.
102
+ */
103
+ class MutableProbeElement extends LitElement {
104
+ flag = "INITIAL";
105
+ rendered = "";
106
+ constructor() {
107
+ super();
108
+ attachDecorator(this, "render", withHooks);
109
+ }
110
+ render() {
111
+ this.rendered = this.flag;
112
+ return html`<span>${this.flag}</span>`;
113
+ }
114
+ }
115
+ customElements.define("mutable-probe-element", MutableProbeElement);
116
+
99
117
  function resetProbes(obs: Observe<number>): void {
100
118
  connectEdges = [];
101
119
  disconnectEdges = [];
@@ -164,7 +182,7 @@ describe("two-slot hook lifecycle — real DOM", () => {
164
182
  expect(connectEdges).toEqual(["connect", "connect"]);
165
183
  });
166
184
 
167
- it("value + subscription slots SURVIVE a MOVE (state kept, no re-subscribe, no forced render)", async () => {
185
+ it("value + subscription slots SURVIVE a MOVE (no re-subscribe), and the reconnect re-renders once", async () => {
168
186
  const { observable, subscriberCount, totalSubscribes, totalUnsubscribes } = createTrackedObservable();
169
187
  resetProbes(observable);
170
188
  const parentA = document.createElement("div");
@@ -180,11 +198,37 @@ describe("two-slot hook lifecycle — real DOM", () => {
180
198
  await flushMicrotasks();
181
199
  await el.updateComplete;
182
200
 
183
- // Subscription untouched; useState value preserved; no re-render forced.
201
+ // The subscription is untouched — this is the perf win (no re-subscribe, no
202
+ // producer re-run, no state reset)...
184
203
  expect(subscriberCount()).toBe(1);
185
204
  expect(totalSubscribes()).toBe(1);
186
205
  expect(totalUnsubscribes()).toBe(0);
187
- expect(renderCount).toBe(rendersBeforeMove);
206
+ // ...but the reconnect forces exactly one re-render (a cheap Lit diff over the
207
+ // preserved hook state) so an element reading mutable non-hook state refreshes.
208
+ expect(renderCount).toBe(rendersBeforeMove + 1);
209
+ });
210
+
211
+ it("an element reading MUTABLE non-hook state refreshes across a MOVE (the reconnect re-render)", async () => {
212
+ // Regression: a move fires no DOM re-render, so without the controller's forced
213
+ // reconnect re-render an element whose render reads a plain mutable field shows
214
+ // stale output after a re-parent.
215
+ const { observable } = createTrackedObservable();
216
+ resetProbes(observable);
217
+ const parentA = document.createElement("div");
218
+ const parentB = document.createElement("div");
219
+ document.body.append(parentA, parentB);
220
+ const el = document.createElement("mutable-probe-element") as MutableProbeElement;
221
+ parentA.appendChild(el);
222
+ await el.updateComplete;
223
+ expect(el.rendered).toBe("INITIAL");
224
+
225
+ el.flag = "CHANGED"; // mutate a plain field (no hook, no requestUpdate)
226
+ parentB.appendChild(el); // MOVE
227
+ await flushMicrotasks();
228
+ await el.updateComplete;
229
+
230
+ expect(el.isConnected).toBe(true);
231
+ expect(el.rendered).toBe("CHANGED");
188
232
  });
189
233
 
190
234
  it("finalizes on a genuine unmount — subscription disposed, no leak", async () => {
@@ -276,4 +320,35 @@ describe("two-slot hook lifecycle — real DOM", () => {
276
320
  await flushMicrotasks();
277
321
  expect(effectCleanups).toBe(1);
278
322
  });
323
+
324
+ it("a subscription that has not yet emitted heals on a post-MOVE emit (preserved-subscription data contract)", async () => {
325
+ // Data-layer contract: the move preserves the single mount-time subscription, so
326
+ // a source that emits only LATER still reaches the element. This guards against a
327
+ // regression that tears subscriptions down on move (which would strand a source
328
+ // that does not replay-on-subscribe — the root of the studio "renders nothing").
329
+ let deliver: ((value: number) => void) | undefined;
330
+ const stalled: Observe<number> = observer => {
331
+ deliver = observer; // note: does NOT emit synchronously on subscribe
332
+ return () => {
333
+ deliver = undefined;
334
+ };
335
+ };
336
+ resetProbes(stalled);
337
+ const parentA = document.createElement("div");
338
+ const parentB = document.createElement("div");
339
+ document.body.append(parentA, parentB);
340
+ const el = document.createElement("lifecycle-probe-element") as LifecycleProbeElement;
341
+ parentA.appendChild(el);
342
+ await el.updateComplete;
343
+ expect(el.shadowRoot?.textContent).toContain("41:"); // value undefined, not yet emitted
344
+
345
+ // Move BEFORE the source emits, then emit.
346
+ parentB.appendChild(el);
347
+ await flushMicrotasks();
348
+ deliver?.(99);
349
+ await el.updateComplete;
350
+
351
+ // The preserved subscription delivered the post-move emit → element healed.
352
+ expect(el.shadowRoot?.textContent).toContain("99");
353
+ });
279
354
  });