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
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2026 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { initElementTemplatePAPICallAlog } from '../../../src/element-template/debug/elementPAPICall.js';

describe('ElementTemplate PAPI alog wrapper', () => {
const originalProfile = globalThis.__PROFILE__;

beforeEach(() => {
vi.clearAllMocks();
globalThis.__PROFILE__ = true;
});

afterEach(() => {
globalThis.__PROFILE__ = originalProfile;
});

it('wraps ET PAPI calls and formats native refs', () => {
const templateRef = { id: 1 };
const childRef = { id: 2 };
const circular: Record<string, unknown> = {};
circular['self'] = circular;
const jsonUndefined = { toJSON: () => undefined };
function namedHandler() {}
const anonymousHandler = () => {};
Object.defineProperty(anonymousHandler, 'name', { value: '' });

const target = {
__CreateElementTemplate: vi.fn(() => templateRef),
__SetAttributeOfElementTemplate: vi.fn(),
__InsertNodeToElementTemplate: vi.fn(() => childRef),
__RemoveNodeFromElementTemplate: vi.fn(() => null),
__SerializeElementTemplate: vi.fn(() => Symbol.for('serialized')),
} satisfies Record<string, unknown>;

initElementTemplatePAPICallAlog(target);

expect((target.__CreateElementTemplate as (...args: unknown[]) => unknown)(
'_et_card',
null,
['title'],
[],
17,
)).toBe(templateRef);
(target.__SetAttributeOfElementTemplate as (...args: unknown[]) => unknown)(
templateRef,
0,
[
templateRef,
undefined,
null,
namedHandler,
anonymousHandler,
Symbol.for('slot'),
circular,
jsonUndefined,
],
null,
);
expect((target.__InsertNodeToElementTemplate as (...args: unknown[]) => unknown)(
templateRef,
1,
childRef,
undefined,
)).toBe(childRef);
expect((target.__RemoveNodeFromElementTemplate as (...args: unknown[]) => unknown)(
templateRef,
1,
childRef,
)).toBeNull();
expect((target.__SerializeElementTemplate as (...args: unknown[]) => unknown)(templateRef)).toBe(
Symbol.for('serialized'),
);

const logs = (console.alog as unknown as { mock: { calls: unknown[][] } }).mock.calls
.map(args => String(args[0]))
.join('\n');
expect(logs).toContain(
'__CreateElementTemplate("_et_card", null, ["title"], [], 17) => _et_card#17',
);
expect(logs).toContain(
'__SetAttributeOfElementTemplate(_et_card#17, 0, [_et_card#17, undefined, null, [Function namedHandler], [Function], Symbol(slot), [object Object], [object Object]], null)',
);
expect(logs).toContain(
'__InsertNodeToElementTemplate(_et_card#17, 1, {"id":2}, undefined) => {"id":2}',
);
expect(logs).toContain('__RemoveNodeFromElementTemplate(_et_card#17, 1, {"id":2})');
expect(logs).toContain('__SerializeElementTemplate(_et_card#17) => Symbol(serialized)');
expect(globalThis.lynx.performance.profileStart).toHaveBeenCalledTimes(5);
expect(globalThis.lynx.performance.profileEnd).toHaveBeenCalledTimes(5);
});

it('skips missing APIs and keeps logging optional', () => {
const originalAlog = console.alog;
const createElementTemplate = vi.fn(() => null);
const target = {
__CreateElementTemplate: createElementTemplate,
} satisfies Record<string, unknown>;

globalThis.__PROFILE__ = false;
console.alog = undefined;
try {
initElementTemplatePAPICallAlog(target);

expect((target.__CreateElementTemplate as (...args: unknown[]) => unknown)(
'_et_empty',
null,
[],
[],
3,
)).toBeNull();
expect(createElementTemplate).toHaveBeenCalledTimes(1);
expect(globalThis.lynx.performance.profileStart).not.toHaveBeenCalled();
expect(globalThis.lynx.performance.profileEnd).not.toHaveBeenCalled();
} finally {
console.alog = originalAlog;
}
});

it('ends the profile when a wrapped ET PAPI throws', () => {
const error = new Error('native failed');
const target = {
__CreateElementTemplate: vi.fn(() => {
throw error;
}),
} satisfies Record<string, unknown>;

initElementTemplatePAPICallAlog(target);

expect(() =>
(target.__CreateElementTemplate as (...args: unknown[]) => unknown)(
'_et_error',
null,
[],
[],
5,
)
).toThrow(error);
expect(globalThis.lynx.performance.profileStart).toHaveBeenCalledTimes(1);
expect(globalThis.lynx.performance.profileEnd).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ describe('element-template native index wiring', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
globalThis.__ALOG_ELEMENT_API__ = undefined;
});

afterEach(() => {
process.env['NODE_ENV'] = originalNodeEnv;
globalThis.__ALOG_ELEMENT_API__ = undefined;
vi.resetModules();
vi.doUnmock('../../../src/element-template/native/main-thread-api.js');
vi.doUnmock('../../../src/element-template/native/patch-listener.js');
Expand All @@ -24,6 +26,7 @@ describe('element-template native index wiring', () => {
vi.doUnmock('../../../src/element-template/background/hydration-listener.js');
vi.doUnmock('../../../src/element-template/background/commit-hook.js');
vi.doUnmock('../../../src/element-template/background/instance.js');
vi.doUnmock('../../../src/element-template/debug/elementPAPICall.js');
vi.doUnmock('../../../src/element-template/debug/profile.js');
vi.doUnmock('../../../src/element-template/lynx/env.js');
vi.doUnmock('../../../src/element-template/lynx/performance.js');
Expand All @@ -32,10 +35,12 @@ describe('element-template native index wiring', () => {

it('installs main-thread wiring only on main thread', async () => {
envManager.resetEnv('main');
globalThis.__ALOG_ELEMENT_API__ = true;

const injectCalledByNative = vi.fn();
const installElementTemplatePatchListener = vi.fn();
const installOnMtsDestruction = vi.fn();
const initElementTemplatePAPICallAlog = vi.fn();
const initProfileHook = vi.fn();
const setupLynxEnv = vi.fn();
const installElementTemplateCommitHook = vi.fn();
Expand All @@ -53,6 +58,9 @@ describe('element-template native index wiring', () => {
vi.doMock('../../../src/element-template/native/mts-destroy.js', () => ({
installOnMtsDestruction,
}));
vi.doMock('../../../src/element-template/debug/elementPAPICall.js', () => ({
initElementTemplatePAPICallAlog,
}));
vi.doMock('../../../src/element-template/debug/profile.js', () => ({
initProfileHook,
}));
Expand Down Expand Up @@ -80,6 +88,7 @@ describe('element-template native index wiring', () => {

await import('../../../src/element-template/native/index.js');

expect(initElementTemplatePAPICallAlog).toHaveBeenCalledTimes(1);
expect(injectCalledByNative).toHaveBeenCalledTimes(1);
expect(installElementTemplatePatchListener).toHaveBeenCalledTimes(1);
expect(installOnMtsDestruction).toHaveBeenCalledTimes(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { destroyElementTemplateBackgroundRuntime } from '../../../../src/element-template/background/destroy.js';
import { BackgroundElementTemplateInstance } from '../../../../src/element-template/background/instance.js';
Expand Down Expand Up @@ -31,6 +31,11 @@ describe('ElementTemplate event bridge', () => {
backgroundElementTemplateInstanceManager.nextId = 0;
clearEtAttrPlanMap();
clearEventState();
globalThis.__ALOG__ = true;
});

afterEach(() => {
globalThis.__ALOG__ = true;
});

it('dispatches publishEvent to the current handler', () => {
Expand All @@ -43,6 +48,39 @@ describe('ElementTemplate event bridge', () => {
expect(handler).toHaveBeenCalledWith(eventData);
});

it('logs dispatch metadata when alog is enabled', () => {
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();
const received: unknown[] = [];
function onTap(data: unknown) {
received.push(data);
}
createEventInstance(-8, onTap);

publishEvent('-8:0:', { type: 'tap', detail: { x: 1 } });

const output = alog.mock.calls.map(args => String(args[0])).join('\n');
expect(received).toEqual([{ type: 'tap', detail: { x: 1 } }]);
expect(output).toContain('[ReactLynxDebug] ElementTemplate BTS received event');
expect(output).toContain('"eventValue": "-8:0:"');
expect(output).toContain('"type": "tap"');
expect(output).toContain('"jsFunctionName": "onTap"');
expect(output).toContain('"hasHandler": true');
});

it('skips dispatch alog when alog is disabled', () => {
globalThis.__ALOG__ = false;
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();
const handler = vi.fn();
createEventInstance(-9, handler);

publishEvent('-9:0:', { type: 'tap' });

expect(handler).toHaveBeenCalledWith({ type: 'tap' });
expect(alog.mock.calls).toHaveLength(0);
});

it('dispatches publicComponentEvent through the same handler lookup', () => {
const handler = vi.fn();
const eventData = { type: 'tap' };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2025 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 { profileEnd, profileStart } from '../../shared/profile.js';

const elementTemplatePAPINameList = [
'__CreateElementTemplate',
'__SetAttributeOfElementTemplate',
'__InsertNodeToElementTemplate',
'__RemoveNodeFromElementTemplate',
'__SerializeElementTemplate',
] as const;

export function initElementTemplatePAPICallAlog(globalWithIndex: Record<string, unknown> = globalThis): void {
let count = 0;
const elementTemplateMap = new Map<unknown, string>();

for (const elementTemplatePAPIName of elementTemplatePAPINameList) {
const oldElementTemplatePAPI = globalWithIndex[elementTemplatePAPIName];
if (typeof oldElementTemplatePAPI !== 'function') {
continue;
}
const callElementTemplatePAPI = oldElementTemplatePAPI as (...args: unknown[]) => unknown;

globalWithIndex[elementTemplatePAPIName] = (...args: unknown[]): unknown => {
const shouldProfile = typeof __PROFILE__ !== 'undefined' && __PROFILE__;
if (shouldProfile) {
profileStart(`ElementTemplatePAPI: ${elementTemplatePAPIName}`, {
args: {
args: formatValue(args, elementTemplateMap),
},
});
}

let result: unknown;
try {
result = callElementTemplatePAPI(...args);
} finally {
if (shouldProfile) {
profileEnd();
}
}

if (elementTemplatePAPIName === '__CreateElementTemplate' && result != null) {
elementTemplateMap.set(result, `${String(args[0])}#${String(args[4])}`);
}

const formattedResult = result == null ? undefined : formatValue(result, elementTemplateMap);
console.alog?.(
`[ReactLynxDebug] ElementTemplate API call #${++count}: ${elementTemplatePAPIName}(${
args.map(arg => formatValue(arg, elementTemplateMap)).join(', ')
})${formattedResult == null ? '' : ` => ${formattedResult}`}`,
);
return result;
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function formatValue(value: unknown, elementTemplateMap: Map<unknown, string>): string {
if (elementTemplateMap.has(value)) {
return elementTemplateMap.get(value)!;
}
if (value === undefined) {
return 'undefined';
}
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return '[' + value.map(item => formatValue(item, elementTemplateMap)).join(', ') + ']';
}
if (typeof value === 'function') {
return `[Function${value.name ? ` ${value.name}` : ''}]`;
}
if (typeof value === 'symbol') {
return value.toString();
}
try {
const formatted = JSON.stringify(value);
return formatted ?? Object.prototype.toString.call(value);
} catch {
return Object.prototype.toString.call(value);
}
}
5 changes: 5 additions & 0 deletions packages/react/runtime/src/element-template/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import { installElementTemplateCommitHook } from '../background/commit-hook.js';
import { setupBackgroundElementTemplateDocument } from '../background/document.js';
import { installElementTemplateHydrationListener } from '../background/hydration-listener.js';
import { BackgroundElementTemplateInstance } from '../background/instance.js';
import { initElementTemplatePAPICallAlog } from '../debug/elementPAPICall.js';
import { initProfileHook } from '../debug/profile.js';
import { setupLynxEnv } from '../lynx/env.js';
import { initTimingAPI } from '../lynx/performance.js';
import { publicComponentEvent, publishEvent, resetEventStateForRuntime } from '../prop-adapters/event.js';
import { setRoot } from '../runtime/page/root-instance.js';

function init(): void {
if (typeof __ALOG_ELEMENT_API__ !== 'undefined' && __ALOG_ELEMENT_API__) {
initElementTemplatePAPICallAlog();
}

if (__MAIN_THREAD__) {
installMainThreadHooks();
injectCalledByNative();
Expand Down
Loading
Loading