// Copyright 2024 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import { createElement } from 'preact'; import { vi } from 'vitest'; import { BackgroundElementTemplateInstance } from '../../../../src/element-template/background/instance.js'; import { hydrateBackground } from '../../test-utils/debug/hydrate.js'; import { installMockNativePapi } from '../../test-utils/mock/mockNativePapi.js'; import { installElementTemplatePatchListener, resetElementTemplatePatchListener, } from '../../../../src/element-template/native/patch-listener.js'; import { installElementTemplateHydrationListener, resetElementTemplateHydrationListener, } from '../../../../src/element-template/background/hydration-listener.js'; import { installElementTemplateCommitHook } from '../../../../src/element-template/background/commit-hook.js'; import '../../../../src/element-template/native/index.js'; import { ElementTemplateLifecycleConstant } from '../../../../src/element-template/protocol/lifecycle-constant.js'; import type { ElementTemplateHydrateCommitContext, ElementTemplateUpdateCommitContext, SerializedEtNode, } from '../../../../src/element-template/protocol/types.js'; import { parseElementTemplateUpdateEventPayload } from '../../../../src/element-template/protocol/update-event.js'; import { root } from '../../../../src/element-template/client/root.js'; import { __page } from '../../../../src/element-template/runtime/page/page.js'; import { __root as internalRoot } from '../../../../src/element-template/runtime/page/root-instance.js'; import { resetTemplateId } from '../../../../src/element-template/runtime/template/handle.js'; import { elementTemplateRegistry } from '../../../../src/element-template/runtime/template/registry.js'; import { registerBuiltinRawTextTemplate } from '../../test-utils/debug/registry.js'; import { ElementTemplateEnvManager } from '../../test-utils/debug/envManager.js'; import { compileFixtureSource } from '../../test-utils/debug/compiledFixtureCompiler.js'; import { loadCompiledFixtureModule, type CompiledFixtureModuleExports, } from '../../test-utils/debug/compiledFixtureModule.js'; import { primeCompiledFixtureTemplates } from '../../test-utils/debug/compiledFixtureRegistry.js'; import { serializeToJSX } from '../../test-utils/debug/serializer.js'; interface RootWithFirstChild { firstChild: BackgroundElementTemplateInstance | null; } interface RootNode { type: 'root'; } type HydrateEvent = { data: ElementTemplateHydrateCommitContext }; type HydrateInstance = SerializedEtNode; type HydrateInstances = SerializedEtNode[]; export interface PatchContext { envManager: ElementTemplateEnvManager; hydrationData: HydrateInstances; onHydrate: (event: HydrateEvent) => void; root: RootNode; nativeLog: unknown[]; cleanupNative: () => void; } export interface UpdateFixtureContext { envManager: ElementTemplateEnvManager; hydrationData: HydrateInstances; updateEvents: ElementTemplateUpdateCommitContext[]; onHydrate: (event: HydrateEvent) => void; onUpdate: (event: { data: unknown }) => void; cleanupNative: () => void; } export function setupPatchContext(): PatchContext { vi.clearAllMocks(); elementTemplateRegistry.clear(); resetTemplateId(); registerBuiltinRawTextTemplate(); const installed = installMockNativePapi({ clearTemplatesOnCleanup: false }); const envManager = new ElementTemplateEnvManager(); const hydrationData: HydrateInstances = []; envManager.resetEnv('background'); envManager.setUseElementTemplate(true); // Core context lives on background thread installElementTemplateHydrationListener(); // JS context lives on main thread envManager.switchToMainThread(); installElementTemplatePatchListener(); envManager.switchToBackground(); const onHydrate = vi.fn().mockImplementation((event: HydrateEvent) => { hydrationData.push(...(event.data.page.childSlots?.[0] ?? [])); }); lynx.getCoreContext().addEventListener(ElementTemplateLifecycleConstant.hydrate, onHydrate); return { envManager, hydrationData, onHydrate, root: { type: 'root' }, nativeLog: installed.nativeLog, cleanupNative: installed.cleanup, }; } export function setupUpdateFixtureContext(): UpdateFixtureContext { vi.clearAllMocks(); elementTemplateRegistry.clear(); resetTemplateId(); registerBuiltinRawTextTemplate(); const installed = installMockNativePapi({ clearTemplatesOnCleanup: false }); const envManager = new ElementTemplateEnvManager(); const hydrationData: HydrateInstances = []; const updateEvents: ElementTemplateUpdateCommitContext[] = []; envManager.resetEnv('background'); envManager.setUseElementTemplate(true); envManager.switchToBackground(); installElementTemplateHydrationListener(); installElementTemplateCommitHook(); const onHydrate = (event: HydrateEvent) => { hydrationData.push(...(event.data.page.childSlots?.[0] ?? [])); }; lynx.getCoreContext().addEventListener(ElementTemplateLifecycleConstant.hydrate, onHydrate); envManager.switchToMainThread(); installElementTemplatePatchListener(); const onUpdate = (event: { data: unknown }) => { updateEvents.push(parseElementTemplateUpdateEventPayload(event.data)); }; lynx.getJSContext().addEventListener(ElementTemplateLifecycleConstant.update, onUpdate); envManager.switchToBackground(); return { envManager, hydrationData, updateEvents, onHydrate, onUpdate, cleanupNative: installed.cleanup, }; } export function teardownPatchContext(context: PatchContext): void { try { context.cleanupNative(); } finally { context.envManager.switchToBackground(); resetElementTemplateHydrationListener(); lynx.getCoreContext().removeEventListener(ElementTemplateLifecycleConstant.hydrate, context.onHydrate); context.envManager.switchToMainThread(); resetElementTemplatePatchListener(); context.envManager.setUseElementTemplate(false); (globalThis as { __LYNX_REPORT_ERROR_CALLS?: Error[] }).__LYNX_REPORT_ERROR_CALLS = []; } } export function teardownUpdateFixtureContext(context: UpdateFixtureContext): void { try { context.cleanupNative(); } finally { context.envManager.switchToBackground(); resetElementTemplateHydrationListener(); lynx.getCoreContext().removeEventListener(ElementTemplateLifecycleConstant.hydrate, context.onHydrate); context.envManager.switchToMainThread(); lynx.getJSContext().removeEventListener(ElementTemplateLifecycleConstant.update, context.onUpdate); resetElementTemplatePatchListener(); context.envManager.setUseElementTemplate(false); (globalThis as { __LYNX_REPORT_ERROR_CALLS?: Error[] }).__LYNX_REPORT_ERROR_CALLS = []; } } export function renderAndCollect(App: () => JSX.Element, context: PatchContext): { before: HydrateInstance; after: BackgroundElementTemplateInstance; } { root.render(); context.envManager.switchToMainThread(); (globalThis as { renderPage: () => void }).renderPage(); context.envManager.switchToBackground(); const before = context.hydrationData[0]; if (!before) { throw new Error('Missing hydration data.'); } const backgroundRoot = internalRoot as unknown as RootWithFirstChild; const after = backgroundRoot.firstChild; if (!after) { throw new Error('Missing background root child.'); } return { before, after }; } function resolveCompiledFixtureProps( moduleExports: CompiledFixtureModuleExports, thread: 'main' | 'background', ): Record { return thread === 'main' ? (moduleExports.mainProps ?? {}) : (moduleExports.backgroundProps ?? {}); } export async function runCompiledPatchScenario(sourcePath: string): Promise<{ files: { 'after-jsx.txt': string; 'before-jsx.txt': string; 'ops.txt': ElementTemplateUpdateCommandStream; }; }> { const context = setupUpdateFixtureContext(); try { const mainArtifact = await compileFixtureSource(sourcePath, { target: 'LEPUS' }); primeCompiledFixtureTemplates(mainArtifact); const mainModule = await loadCompiledFixtureModule(mainArtifact); const backgroundArtifact = await compileFixtureSource(sourcePath, { target: 'JS' }); const backgroundModule = await loadCompiledFixtureModule(backgroundArtifact); context.envManager.switchToBackground(); root.render(createElement( backgroundModule.App, resolveCompiledFixtureProps(backgroundModule, 'background'), )); context.envManager.switchToMainThread(); root.render(createElement( mainModule.App, resolveCompiledFixtureProps(mainModule, 'main'), )); (globalThis as { renderPage: () => void }).renderPage(); const beforeJSX = serializeToJSX(__page); context.envManager.switchToBackground(); if (!context.hydrationData[0]) { throw new Error('Missing compiled patch hydration data.'); } const backgroundRoot = internalRoot as unknown as RootWithFirstChild; const afterData = backgroundRoot.firstChild; if (!afterData) { throw new Error('Missing compiled patch background root child.'); } context.envManager.switchToMainThread(); const ops = context.updateEvents[0]?.ops ?? []; const afterJSX = serializeToJSX(__page); return { files: { 'before-jsx.txt': beforeJSX, 'after-jsx.txt': afterJSX, 'ops.txt': ops, }, }; } finally { teardownUpdateFixtureContext(context); } }