Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-templates-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

---

No package release is required because this change only adds Element Template runtime test coverage and aligns hydrate nullish slot handling with existing Snapshot behavior, without changing public APIs, package exports, or release-facing defaults.
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { GlobalCommitContext } from '../../../../src/element-template/background/commit-context.js';
import {
markElementTemplateHydrated,
resetElementTemplateCommitState,
} from '../../../../src/element-template/background/commit-hook.js';
import { hydrate } from '../../../../src/element-template/background/hydrate.js';
import {
BackgroundElementTemplateInstance,
BackgroundElementTemplateSlot,
BUILTIN_RAW_TEXT_TEMPLATE_KEY,
} from '../../../../src/element-template/background/instance.js';
import { backgroundElementTemplateInstanceManager } from '../../../../src/element-template/background/manager.js';
import { ElementTemplateUpdateOps } from '../../../../src/element-template/protocol/opcodes.js';
import type { SerializedElementTemplate } from '../../../../src/element-template/protocol/types.js';

function createHydrationTemplate(
Expand Down Expand Up @@ -40,6 +46,7 @@ describe('hydrate', () => {
beforeEach(() => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
resetElementTemplateCommitState();
vi.clearAllMocks();
(globalThis as { __LYNX_REPORT_ERROR_CALLS?: Error[] }).__LYNX_REPORT_ERROR_CALLS = [];
});
Expand All @@ -63,6 +70,291 @@ describe('hydrate', () => {
expect(root.elementSlots[0]).toEqual([child]);
});

it('patches attribute slots while creating and inserting background-only children', () => {
const root = new BackgroundElementTemplateInstance('root', ['background-root']);
const slot = new BackgroundElementTemplateSlot();
slot.setAttribute('id', 0);
root.appendChild(slot);

const existing = new BackgroundElementTemplateInstance('item', ['background-existing']);
slot.appendChild(existing);

const card = new BackgroundElementTemplateInstance('card', ['background-card']);
const cardSlot = new BackgroundElementTemplateSlot();
cardSlot.setAttribute('id', 0);
card.appendChild(cardSlot);
const rawText = new BackgroundElementTemplateInstance(BUILTIN_RAW_TEXT_TEMPLATE_KEY, ['NEW']);
cardSlot.appendChild(rawText);
slot.appendChild(card);

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
attributeSlots: ['main-root'],
elementSlots: [[
createHydrationChild(existing.instanceId, 'item', {
attributeSlots: ['main-existing'],
}),
]],
}),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.setAttribute,
root.instanceId,
0,
'background-root',
ElementTemplateUpdateOps.setAttribute,
existing.instanceId,
0,
'background-existing',
ElementTemplateUpdateOps.createTemplate,
rawText.instanceId,
BUILTIN_RAW_TEXT_TEMPLATE_KEY,
null,
['NEW'],
[],
ElementTemplateUpdateOps.createTemplate,
card.instanceId,
'card',
null,
['background-card'],
[[rawText.instanceId]],
ElementTemplateUpdateOps.insertNode,
root.instanceId,
0,
card.instanceId,
0,
]);
expect(root.elementSlots[0]).toEqual([existing, card]);
expect(card.elementSlots[0]).toEqual([rawText]);
});

it('removes serialized children that are missing from the background slot', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot = new BackgroundElementTemplateSlot();
slot.setAttribute('id', 0);
root.appendChild(slot);

const stale = new BackgroundElementTemplateInstance('stale');

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
elementSlots: [[createHydrationChild(stale.instanceId, 'stale')]],
}),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.removeNode,
root.instanceId,
0,
stale.instanceId,
]);
expect(root.elementSlots[0]).toEqual([]);
});

it('moves serialized children to match the background slot order', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot = new BackgroundElementTemplateSlot();
slot.setAttribute('id', 0);
root.appendChild(slot);

const a = new BackgroundElementTemplateInstance('a');
const b = new BackgroundElementTemplateInstance('b');
const c = new BackgroundElementTemplateInstance('c');
slot.appendChild(b);
slot.appendChild(a);
slot.appendChild(c);

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
elementSlots: [[
createHydrationChild(a.instanceId, 'a'),
createHydrationChild(b.instanceId, 'b'),
createHydrationChild(c.instanceId, 'c'),
]],
}),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.insertNode,
root.instanceId,
0,
a.instanceId,
c.instanceId,
]);
expect(root.elementSlots[0]).toEqual([b, a, c]);
});

it('recreates background children when serialized root has no child slots', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot = new BackgroundElementTemplateSlot();
slot.setAttribute('id', 0);
root.appendChild(slot);

const child = new BackgroundElementTemplateInstance('child', ['background-child']);
const childSlot = new BackgroundElementTemplateSlot();
childSlot.setAttribute('id', 0);
child.appendChild(childSlot);
const rawText = new BackgroundElementTemplateInstance(BUILTIN_RAW_TEXT_TEMPLATE_KEY, ['NEW']);
childSlot.appendChild(rawText);
slot.appendChild(child);

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root'),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.createTemplate,
rawText.instanceId,
BUILTIN_RAW_TEXT_TEMPLATE_KEY,
null,
['NEW'],
[],
ElementTemplateUpdateOps.createTemplate,
child.instanceId,
'child',
null,
['background-child'],
[[rawText.instanceId]],
ElementTemplateUpdateOps.insertNode,
root.instanceId,
0,
child.instanceId,
0,
]);
expect(root.elementSlots[0]).toEqual([child]);
expect(child.elementSlots[0]).toEqual([rawText]);
});

it('diffs multiple dynamic children slots independently during hydrate', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot0 = new BackgroundElementTemplateSlot();
slot0.setAttribute('id', 0);
root.appendChild(slot0);
const slot1 = new BackgroundElementTemplateSlot();
slot1.setAttribute('id', 1);
root.appendChild(slot1);

const newA = new BackgroundElementTemplateInstance('new-a');
slot0.appendChild(newA);

const b0 = new BackgroundElementTemplateInstance('b0');
const b1 = new BackgroundElementTemplateInstance('b1');
slot1.appendChild(b1);
slot1.appendChild(b0);

const oldAId = -11;
const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
elementSlots: [
[createHydrationChild(oldAId, 'old-a')],
[
createHydrationChild(b0.instanceId, 'b0'),
createHydrationChild(b1.instanceId, 'b1'),
],
],
}),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.removeNode,
root.instanceId,
0,
oldAId,
ElementTemplateUpdateOps.createTemplate,
newA.instanceId,
'new-a',
null,
[],
[],
ElementTemplateUpdateOps.insertNode,
root.instanceId,
0,
newA.instanceId,
0,
ElementTemplateUpdateOps.insertNode,
root.instanceId,
1,
b0.instanceId,
0,
]);
expect(root.elementSlots[0]).toEqual([newA]);
expect(root.elementSlots[1]).toEqual([b1, b0]);
});

it('does not match same-type children across element slot indexes', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot0 = new BackgroundElementTemplateSlot();
slot0.setAttribute('id', 0);
root.appendChild(slot0);
const slot1 = new BackgroundElementTemplateSlot();
slot1.setAttribute('id', 1);
root.appendChild(slot1);

const slot0Item = new BackgroundElementTemplateInstance('item', ['B']);
const slot1Item = new BackgroundElementTemplateInstance('item', ['A']);
slot0.appendChild(slot0Item);
slot1.appendChild(slot1Item);

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
elementSlots: [
[createHydrationChild(slot0Item.instanceId, 'item', { attributeSlots: ['A'] })],
[createHydrationChild(slot1Item.instanceId, 'item', { attributeSlots: ['B'] })],
],
}),
root,
);

expect(stream).toEqual([
ElementTemplateUpdateOps.setAttribute,
slot0Item.instanceId,
0,
'B',
ElementTemplateUpdateOps.setAttribute,
slot1Item.instanceId,
0,
'A',
]);
expect(root.elementSlots[0]).toEqual([slot0Item]);
expect(root.elementSlots[1]).toEqual([slot1Item]);
});

it('keeps hydrated handles for later background attribute updates', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot = new BackgroundElementTemplateSlot();
slot.setAttribute('id', 0);
root.appendChild(slot);
const child = new BackgroundElementTemplateInstance('child', ['before']);
slot.appendChild(child);

const childHandleId = -2;
hydrate(
createHydrationTemplate(root.instanceId, 'root', {
elementSlots: [[createHydrationChild(childHandleId, 'child', { attributeSlots: ['before'] })]],
}),
root,
);

expect(backgroundElementTemplateInstanceManager.get(childHandleId)).toBe(child);

markElementTemplateHydrated();
GlobalCommitContext.ops = [];
child.setAttribute('attributeSlots', ['after']);

expect(GlobalCommitContext.ops).toEqual([
ElementTemplateUpdateOps.setAttribute,
childHandleId,
0,
'after',
]);
});

it('creates missing slots and stringifies raw-text placeholder values', () => {
const root = new BackgroundElementTemplateInstance('root');

Expand Down Expand Up @@ -200,6 +492,20 @@ describe('hydrate', () => {
expect(root.elementSlots).toEqual([]);
});

it('does not patch serialized null when the background attribute slot is missing', () => {
const root = new BackgroundElementTemplateInstance('root');

const stream = hydrate(
createHydrationTemplate(root.instanceId, 'root', {
attributeSlots: [null],
}),
root,
);

expect(stream).toEqual([]);
expect(root.attributeSlots).toEqual([]);
});

it('skips sparse background slot indexes when checking trailing slots', () => {
const root = new BackgroundElementTemplateInstance('root');
const slot = new BackgroundElementTemplateSlot();
Expand Down
Loading
Loading