import { vi } from 'vitest';
import {
globalCommitContext,
resetGlobalCommitContext,
} from '../../../../../src/element-template/background/commit-context.js';
import {
installElementTemplateHydrationListener,
resetElementTemplateHydrationListener,
} from '../../../../../src/element-template/background/hydration-listener.js';
import '../../../../../src/element-template/native/index.js';
import {
BackgroundElementTemplateInstance,
BUILTIN_RAW_TEXT_TEMPLATE_KEY,
} from '../../../../../src/element-template/background/instance.js';
import { backgroundElementTemplateInstanceManager } from '../../../../../src/element-template/background/manager.js';
import { root } from '../../../../../src/element-template/index.js';
import {
installElementTemplatePatchListener,
resetElementTemplatePatchListener,
} from '../../../../../src/element-template/native/patch-listener.js';
import { ElementTemplateLifecycleConstant } from '../../../../../src/element-template/protocol/lifecycle-constant.js';
import { ElementTemplateUpdateOps } from '../../../../../src/element-template/protocol/opcodes.js';
import type {
ElementTemplateHydrateCommitContext,
SerializedCompiledNode,
SerializedEtNode,
} from '../../../../../src/element-template/protocol/types.js';
import { __page } from '../../../../../src/element-template/runtime/page/page.js';
import { __root } from '../../../../../src/element-template/runtime/page/root-instance.js';
import { ElementTemplateEnvManager } from '../../../test-utils/debug/envManager.js';
import { hydrateBackground } from '../../../test-utils/debug/hydrate.js';
import { installMockNativePapi } from '../../../test-utils/mock/mockNativePapi.js';
import { serializeToJSX } from '../../../test-utils/debug/serializer.js';
declare const renderPage: () => void;
declare module '@lynx-js/types' {
interface IntrinsicElements {
child: any;
}
}
type HydrateEvent = { data: ElementTemplateHydrateCommitContext };
type HydrateInstances = SerializedEtNode[];
interface CaseContext {
hydrationData: HydrateInstances;
onHydrate: (event: HydrateEvent) => void;
}
const envManager = new ElementTemplateEnvManager();
function createTextNode(text: string): BackgroundElementTemplateInstance {
return new BackgroundElementTemplateInstance(BUILTIN_RAW_TEXT_TEMPLATE_KEY, [text]);
}
function createHydrationTemplate(
handleId: number,
templateKey: string,
options: {
attributeSlots?: unknown[];
childSlots?: SerializedCompiledNode[][];
} = {},
): SerializedCompiledNode {
return {
templateKey,
attributeSlots: (options.attributeSlots ?? []) as SerializedCompiledNode['attributeSlots'],
childSlots: options.childSlots ?? [],
uid: handleId,
};
}
function createHydrationChild(
handleId: number,
templateKey: string,
options: {
attributeSlots?: unknown[];
childSlots?: SerializedCompiledNode[][];
} = {},
): SerializedCompiledNode {
return createHydrationTemplate(handleId, templateKey, options);
}
function createHydrationRawTextRoot(handleId: number, text: unknown): SerializedCompiledNode {
return createHydrationTemplate(handleId, BUILTIN_RAW_TEXT_TEMPLATE_KEY, {
attributeSlots: [typeof text === 'string' ? text : String(text ?? '')],
childSlots: [],
});
}
function createHydrationRawTextChild(handleId: number, text: unknown): SerializedCompiledNode {
return createHydrationChild(handleId, BUILTIN_RAW_TEXT_TEMPLATE_KEY, {
attributeSlots: [typeof text === 'string' ? text : String(text ?? '')],
childSlots: [],
});
}
function setup(): CaseContext {
vi.clearAllMocks();
installMockNativePapi({ clearTemplatesOnCleanup: false });
const hydrationData: HydrateInstances = [];
envManager.resetEnv('background');
envManager.setUseElementTemplate(true);
const onHydrate = vi.fn().mockImplementation((event: HydrateEvent) => {
hydrationData.push(...(event.data.page.childSlots?.[0] ?? []));
});
lynx.getCoreContext().addEventListener(ElementTemplateLifecycleConstant.hydrate, onHydrate);
return { hydrationData, onHydrate };
}
function teardown(context: CaseContext): void {
// cleanup is automatic
envManager.switchToBackground();
lynx.getCoreContext().removeEventListener(ElementTemplateLifecycleConstant.hydrate, context.onHydrate);
envManager.setUseElementTemplate(false);
(globalThis as { __LYNX_REPORT_ERROR_CALLS?: Error[] }).__LYNX_REPORT_ERROR_CALLS = [];
}
function renderAndCollect(App: () => JSX.Element, context: CaseContext) {
root.render();
envManager.switchToMainThread();
root.render();
renderPage();
envManager.switchToBackground();
const before = context.hydrationData[0]!;
const backgroundRoot = __root as BackgroundElementTemplateInstance;
const after = backgroundRoot.firstChild!;
return { before, after };
}
type CaseRunner = (context: CaseContext) => unknown;
const cases: Record = {};
function defineCase(name: string, runner: CaseRunner): void {
cases[name] = runner;
}
export function runCaseByName(name: string): unknown {
const runner = cases[name];
if (!runner) {
throw new Error(`Unknown background-hydrate case: ${name}`);
}
const context = setup();
try {
return runner(context);
} finally {
teardown(context);
}
}
{
defineCase('root-key-mismatch-reconciles', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const after = new BackgroundElementTemplateInstance('after');
const before = createHydrationTemplate(-1, 'before');
const stream = hydrateBackground(before, after);
return {
stream,
afterInstanceId: after.instanceId,
};
});
}
{
defineCase('updates-raw-text-instance-id', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const after = createTextNode('hi');
const before = createHydrationRawTextRoot(-11, 'hi');
const stream = hydrateBackground(before, after);
return {
stream,
afterInstanceId: after.instanceId,
managerHasAfter: backgroundElementTemplateInstanceManager.get(-11) === after,
};
});
}
{
defineCase('attrs.aligns-ids-and-patches', (context) => {
function App() {
const src = __BACKGROUND__ ? 'background.png' : 'main.png';
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return {
stream,
beforeInstanceId: before.uid,
afterInstanceId: after.instanceId,
};
});
}
{
defineCase('attrs.removes-missing', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 'same' }
: { id: 'same', title: 'main' };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.adds-background-only', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 'same', title: 'background' }
: { id: 'same' };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.skips-identical', (context) => {
function App() {
const props = { id: 'same' };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.object-value-updates', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 'same', data: { foo: 'bar' } }
: { id: 'same', data: { foo: 'baz' } };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.patches-nested-component', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 'same', info: { key: 'new' } }
: { id: 'same', info: { key: 'old' } };
return (
);
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.nullish-values', (context) => {
function App() {
const props = __BACKGROUND__
? { id: null, title: undefined }
: { id: 'same', title: 'main' };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.array-diff', (context) => {
function App() {
const props = __BACKGROUND__
? { ids: [1, 2] }
: { ids: [1] };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.style-object-updates', (context) => {
function App() {
const props = __BACKGROUND__
? { style: { color: 'red', fontSize: 12 } }
: { style: { color: 'blue', fontSize: 12 } };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.type-diff', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 123 }
: { id: '123' };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('attrs.batch-multiple-patches', (context) => {
function App() {
const props = __BACKGROUND__
? { id: 'same', title: 'bg', index: 1 }
: { id: 'same', title: 'main', index: 0 };
return ;
}
const { before, after } = renderAndCollect(App, context);
const stream = hydrateBackground(before, after);
return { stream };
});
}
{
defineCase('children.missing-slot-record-on-main', (context) => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const child = new BackgroundElementTemplateInstance('child');
rootInstance.appendChild(child);
const before = createHydrationTemplate(-1, 'root');
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('children.missing-slot-record-on-background', (context) => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const beforeChild = createHydrationChild(-2, 'child');
const before = createHydrationTemplate(-1, 'root', { childSlots: [[beforeChild]] });
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('children.creates-missing-nodes-recursively', (context) => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const existing = new BackgroundElementTemplateInstance('existing');
rootInstance.appendChild(existing);
const card = new BackgroundElementTemplateInstance('card');
card.setAttribute('attributeSlots', [{ id: 'card' }]);
const text = createTextNode('NEW');
card.appendChild(text);
rootInstance.insertBefore(card, existing);
const beforeExisting = createHydrationChild(-2, 'existing');
const beforeRemoved = createHydrationChild(-3, 'removed');
const before = createHydrationTemplate(-1, 'root', {
childSlots: [[beforeExisting, beforeRemoved]],
});
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('children.raw-text-instance-empty-text', (context) => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const rawText = createTextNode('');
rootInstance.appendChild(rawText);
const before = createHydrationTemplate(-1, 'root', {
childSlots: [[createHydrationRawTextChild(3, '')]],
});
const stream = hydrateBackground(before, rootInstance);
return {
stream,
rawTextInstanceId: rawText.instanceId,
};
});
}
{
defineCase('coverage.raw-text-key-branches', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const rawTextText = createTextNode('bg');
const rawTextInstance = new BackgroundElementTemplateInstance(BUILTIN_RAW_TEXT_TEMPLATE_KEY);
rootInstance.appendChild(rawTextText);
rootInstance.appendChild(rawTextInstance);
const beforeExistingString = createHydrationRawTextChild(rawTextText.instanceId, 'bg');
const beforeExistingNonString = createHydrationRawTextChild(rawTextInstance.instanceId, 123);
const beforeMissingString = createHydrationRawTextChild(-2, 'missing');
const beforeMissingNonString = createHydrationRawTextChild(-3, 456);
const before = createHydrationTemplate(rootInstance.instanceId, 'root', {
childSlots: [[
beforeExistingString,
beforeExistingNonString,
beforeMissingString,
beforeMissingNonString,
]],
});
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('children.iterates-existing-slots', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
// Background side has a child living at slot 1 that the serialized payload
// never mentions. The hydrate loop in `hydrate.ts` extends its slot count
// via `Math.max(serializedSlots.length, instance.childSlots.length)` and
// synthesizes a create + insert for the background-only child at slot 1.
const rootInstance = new BackgroundElementTemplateInstance('root');
const slot1Child = new BackgroundElementTemplateInstance('child');
slot1Child.__slotIndex = 1;
rootInstance.appendChild(slot1Child);
const before = createHydrationTemplate(-1, 'root', { childSlots: [[]] });
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('children.missing-attrs-element', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const before = createHydrationTemplate(-1, 'root', { childSlots: [[]] });
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('coverage.move-before-child', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
const rootInstance = new BackgroundElementTemplateInstance('root');
const childA = new BackgroundElementTemplateInstance('a');
const childB = new BackgroundElementTemplateInstance('b');
const childC = new BackgroundElementTemplateInstance('c');
rootInstance.appendChild(childB);
rootInstance.appendChild(childA);
rootInstance.appendChild(childC);
const beforeChildA = createHydrationChild(childA.instanceId, 'a');
const beforeChildB = createHydrationChild(childB.instanceId, 'b');
const beforeChildC = createHydrationChild(childC.instanceId, 'c');
const before = createHydrationTemplate(rootInstance.instanceId, 'root', {
childSlots: [[beforeChildA, beforeChildB, beforeChildC]],
});
const stream = hydrateBackground(before, rootInstance);
return { stream };
});
}
{
defineCase('coverage.emit-create-raw-text', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
resetGlobalCommitContext();
const rawText = createTextNode('raw');
rawText.emitCreate();
const ops = [...globalCommitContext.ops];
resetGlobalCommitContext();
return { ops };
});
}
{
defineCase('coverage.emit-create-raw-text-non-text', () => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
resetGlobalCommitContext();
const rawText = new BackgroundElementTemplateInstance(BUILTIN_RAW_TEXT_TEMPLATE_KEY);
rawText.emitCreate();
const ops = [...globalCommitContext.ops];
resetGlobalCommitContext();
return { ops };
});
}
{
defineCase('full-flow.dispatches-update-event', (context) => {
installElementTemplateHydrationListener();
envManager.switchToMainThread();
installElementTemplatePatchListener();
envManager.switchToBackground();
function App() {
const id = __BACKGROUND__ ? 'bg' : 'main';
return ;
}
renderAndCollect(App, context);
envManager.switchToMainThread();
const pageJsx = serializeToJSX(__page);
resetElementTemplatePatchListener();
envManager.switchToBackground();
resetElementTemplateHydrationListener();
return { pageJsx };
});
}