diff --git a/.changeset/cruel-feet-grow.md b/.changeset/cruel-feet-grow.md new file mode 100644 index 0000000000..b4d2ea9d57 --- /dev/null +++ b/.changeset/cruel-feet-grow.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +Add dual-thread commutation logs for troubleshooting when `REACT_ALOG=true` or global define `__ALOG__` is set. diff --git a/packages/react/runtime/__test__/alog/elementPAPICall.test.js b/packages/react/runtime/__test__/alog/elementPAPICall.test.js new file mode 100644 index 0000000000..788d3dd260 --- /dev/null +++ b/packages/react/runtime/__test__/alog/elementPAPICall.test.js @@ -0,0 +1,50 @@ +import { describe, it, vi } from 'vitest'; +import { initElementPAPICallAlog } from '../../src/alog/elementPAPICall'; +import { globalEnvManager } from '../utils/envManager'; +import { expect } from 'vitest'; + +describe('ElementPAPICall Alog', () => { + it('should log ElementPAPICall as ALog', () => { + globalEnvManager.switchToMainThread(); + console.alog = vi.fn(); + initElementPAPICallAlog(); + + const page = __CreatePage('0', 0); + __SetCSSId([page], 0); + const text0 = __CreateText(0); + const rawText0 = __CreateRawText('2', text0.$$uiSign); + __AppendElement(text0, rawText0); + __AppendElement(page, text0); + __AddDataset(text0, 'testid', 'count-value'); + __OnLifecycleEvent(['rLynxFirstScreen', { 'root': '{}', 'jsReadyEventIdSwap': {} }]); + + expect(console.alog.mock.calls).toMatchInlineSnapshot(` + [ + [ + "[ReactLynxDebug] FiberElement API call #1: __CreatePage("0", 0) => page#0", + ], + [ + "[ReactLynxDebug] FiberElement API call #2: __SetCSSId([page#0], 0)", + ], + [ + "[ReactLynxDebug] FiberElement API call #3: __CreateText(0) => text#1", + ], + [ + "[ReactLynxDebug] FiberElement API call #4: __CreateRawText("2", 1) => raw-text#2", + ], + [ + "[ReactLynxDebug] FiberElement API call #5: __AppendElement(text#1, raw-text#2)", + ], + [ + "[ReactLynxDebug] FiberElement API call #6: __AppendElement(page#0, text#1)", + ], + [ + "[ReactLynxDebug] FiberElement API call #7: __AddDataset(text#1, "testid", "count-value")", + ], + [ + "[ReactLynxDebug] FiberElement API call #8: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{}","jsReadyEventIdSwap":{}}])", + ], + ] + `); + }); +}); diff --git a/packages/react/runtime/__test__/debug/printSnapshot.test.jsx b/packages/react/runtime/__test__/debug/printSnapshot.test.jsx index 895c62ccdb..5536c93c13 100644 --- a/packages/react/runtime/__test__/debug/printSnapshot.test.jsx +++ b/packages/react/runtime/__test__/debug/printSnapshot.test.jsx @@ -7,7 +7,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { backgroundSnapshotInstanceManager, snapshotInstanceManager } from '../../src/snapshot'; import { elementTree } from '../utils/nativeMethod'; import { BackgroundSnapshotInstance } from '../../src/backgroundSnapshot'; -import { printSnapshotInstance } from '../../src/debug/printSnapshot'; +import { printSerializedSnapshotInstance, printSnapshotInstance } from '../../src/debug/printSnapshot'; +import { SnapshotInstance } from '../../src/snapshot'; const HOLE = null; @@ -72,6 +73,40 @@ describe('printSnapshotInstance', () => { `); }); + it('SnapshotInstance', async function() { + const si1 = new SnapshotInstance(snapshot1); + const si2 = new SnapshotInstance(snapshot2); + const si22 = new SnapshotInstance(snapshot2); + const si3 = new SnapshotInstance(snapshot3); + si1.insertBefore(si2); + si1.insertBefore(si22); + si2.insertBefore(si3); + si3.setAttribute(0, 'attr 1'); + si3.setAttribute(1, 'attr 2'); + si3.setAttribute('__1', 'attr 2'); + printSnapshotInstance(si1, log); + expect(msg).toMatchInlineSnapshot(` + " + | -2(__snapshot_a94a8_test_1): undefined + | -3(__snapshot_a94a8_test_2): undefined + | -5(__snapshot_a94a8_test_3): ["attr 1","attr 2"] + | -4(__snapshot_a94a8_test_2): undefined + " + `); + + const serialized = JSON.stringify(si1); + msg = '\n'; + printSerializedSnapshotInstance(JSON.parse(serialized), log); + expect(msg).toMatchInlineSnapshot(` + " + | -2(__snapshot_a94a8_test_1): undefined + | -3(__snapshot_a94a8_test_2): undefined + | -5(__snapshot_a94a8_test_3): ["attr 1","attr 2"] + | -4(__snapshot_a94a8_test_2): undefined + " + `); + }); + it('printToScreen', async function() { const bsi1 = new BackgroundSnapshotInstance(snapshot1); const bsi2 = new BackgroundSnapshotInstance(snapshot2); @@ -88,4 +123,30 @@ describe('printSnapshotInstance', () => { " `); }); + + it('printToScreen for SnapshotInstance', async function() { + const si1 = new SnapshotInstance(snapshot1); + const si2 = new SnapshotInstance(snapshot2); + const si22 = new SnapshotInstance(snapshot2); + const si3 = new SnapshotInstance(snapshot3); + si1.insertBefore(si2); + si1.insertBefore(si22); + si2.insertBefore(si3); + si3.setAttribute(0, 'attr 1'); + si3.setAttribute(1, 'attr 2'); + si3.setAttribute('__1', 'attr 2'); + printSnapshotInstance(si1); + expect(msg).toMatchInlineSnapshot(` + " + " + `); + + const serialized = JSON.stringify(si1); + msg = '\n'; + printSerializedSnapshotInstance(JSON.parse(serialized)); + expect(msg).toMatchInlineSnapshot(` + " + " + `); + }); }); diff --git a/packages/react/runtime/__test__/snapshotPatch.test.jsx b/packages/react/runtime/__test__/snapshotPatch.test.jsx index a2ce51f0f2..898369a32a 100644 --- a/packages/react/runtime/__test__/snapshotPatch.test.jsx +++ b/packages/react/runtime/__test__/snapshotPatch.test.jsx @@ -289,19 +289,19 @@ describe('insertBefore', () => { expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, ], [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, ], [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: '__snapshot_a94a8_test_3'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: '__snapshot_a94a8_test_3'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, @@ -464,13 +464,13 @@ describe('removeChild', () => { expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, ], [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, @@ -656,7 +656,7 @@ describe('setAttribute', () => { expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, @@ -700,7 +700,7 @@ describe('setAttribute', () => { expect(_ReportError.mock.calls[0]).toMatchInlineSnapshot(` [ - [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.], { "errorCode": 1101, }, diff --git a/packages/react/runtime/src/alog/elementPAPICall.ts b/packages/react/runtime/src/alog/elementPAPICall.ts new file mode 100644 index 0000000000..af3a96e3a1 --- /dev/null +++ b/packages/react/runtime/src/alog/elementPAPICall.ts @@ -0,0 +1,131 @@ +// 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 '../debug/utils.js'; + +const fiberElementPAPINameList = [ + '__CreatePage', + '__CreateElement', + '__CreateWrapperElement', + '__CreateText', + '__CreateImage', + '__CreateView', + '__CreateRawText', + '__CreateList', + '__AppendElement', + '__InsertElementBefore', + '__RemoveElement', + '__ReplaceElement', + '__FirstElement', + '__LastElement', + '__NextElement', + '__GetPageElement', + '__GetTemplateParts', + '__AddDataset', + '__SetDataset', + '__GetDataset', + '__SetAttribute', + '__GetAttributes', + '__GetAttributeByName', + '__GetAttributeNames', + '__SetClasses', + '__SetCSSId', + '__AddInlineStyle', + '__SetInlineStyles', + '__AddEvent', + '__SetID', + '__GetElementUniqueID', + '__GetTag', + '__FlushElementTree', + '__UpdateListCallbacks', + '__OnLifecycleEvent', + '__QueryComponent', + '__SetGestureDetector', +]; + +export function initElementPAPICallAlog(globalWithIndex: Record = globalThis): void { + let count = 0; + const fiberElementMap = new Map(); + function formatFiberElement(fiberElement: FiberElement): string { + const fiberElementInfo = fiberElementMap.get(fiberElement)!; + return `${fiberElementInfo.tag}#${fiberElementInfo.uniqueId}`; + } + const filteredFiberElementPAPINameList = fiberElementPAPINameList.filter(fiberElementPAPIName => + typeof globalWithIndex[fiberElementPAPIName] === 'function' + ); + const originalFiberElementPAPIs = filteredFiberElementPAPINameList.reduce((prev, fiberElementPAPIName) => ({ + ...prev, + [fiberElementPAPIName]: globalWithIndex[fiberElementPAPIName] as (...args: unknown[]) => unknown, + }), {} as Record unknown>); + + filteredFiberElementPAPINameList.forEach(fiberElementPAPIName => { + const oldFiberElementPAPI = globalWithIndex[fiberElementPAPIName]; + if (typeof oldFiberElementPAPI === 'function') { + globalWithIndex[fiberElementPAPIName] = (...args: unknown[]): unknown => { + if (__PROFILE__) { + profileStart(`FiberElementPAPI: ${fiberElementPAPIName}`, { + args: { + args: JSON.stringify(args), + }, + }); + } + const result = (oldFiberElementPAPI as (...args: unknown[]) => unknown)(...args); + if (__PROFILE__) { + profileEnd(); + } + + const formattedArgs = [...args]; + + for (let i = 0; i < formattedArgs.length; i++) { + const arg = formattedArgs[i]; + if (Array.isArray(arg)) { + formattedArgs[i] = '[' + arg.map((item: unknown) => { + if (fiberElementMap.has(item)) { + return formatFiberElement(item as FiberElement); + } + return JSON.stringify(item); + }).join(', ') + ']'; + } else if (fiberElementMap.has(arg)) { + formattedArgs[i] = formatFiberElement(arg as FiberElement); + } else { + formattedArgs[i] = JSON.stringify(arg); + } + } + + if ( + fiberElementPAPIName === '__CreatePage' + || fiberElementPAPIName === '__CreateElement' + || fiberElementPAPIName === '__CreateWrapperElement' + || fiberElementPAPIName === '__CreateText' + || fiberElementPAPIName === '__CreateImage' + || fiberElementPAPIName === '__CreateView' + || fiberElementPAPIName === '__CreateRawText' + || fiberElementPAPIName === '__CreateList' + ) { + fiberElementMap.set(result as FiberElement, { + tag: originalFiberElementPAPIs['__GetTag']!(result as FiberElement) as string, + uniqueId: originalFiberElementPAPIs['__GetElementUniqueID']!(result as FiberElement) as number, + }); + } + + let formattedResult; + if (fiberElementMap.has(result)) { + formattedResult = formatFiberElement(result as FiberElement); + } else if (result !== null) { + formattedResult = JSON.stringify(result); + } + + console.alog?.( + `[ReactLynxDebug] FiberElement API call #${++count}: ${fiberElementPAPIName}(${formattedArgs.join(', ')})${ + formattedResult == null ? '' : ` => ${formattedResult}` + }`, + ); + return result; + }; + } + }); +} diff --git a/packages/react/runtime/src/alog/index.ts b/packages/react/runtime/src/alog/index.ts index 765c8bb129..d0150cbae2 100644 --- a/packages/react/runtime/src/alog/index.ts +++ b/packages/react/runtime/src/alog/index.ts @@ -1,8 +1,10 @@ // 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 { initElementPAPICallAlog } from './elementPAPICall.js'; import { initRenderAlog } from './render.js'; export function initAlog(): void { initRenderAlog(); + initElementPAPICallAlog(); } diff --git a/packages/react/runtime/src/debug/printSnapshot.ts b/packages/react/runtime/src/debug/printSnapshot.ts index e9553fad1c..fcaeecc31e 100644 --- a/packages/react/runtime/src/debug/printSnapshot.ts +++ b/packages/react/runtime/src/debug/printSnapshot.ts @@ -1,8 +1,9 @@ // 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 type { BackgroundSnapshotInstance } from '../backgroundSnapshot.js'; +import { BackgroundSnapshotInstance } from '../backgroundSnapshot.js'; import { SnapshotInstance } from '../snapshot.js'; +import type { SerializedSnapshotInstance } from '../snapshot.js'; import { logDebug } from './debug.js'; export function printSnapshotInstance( @@ -26,3 +27,37 @@ export function printSnapshotInstance( impl(instance, 0); } + +export function printSerializedSnapshotInstance( + instance: SerializedSnapshotInstance, + log?: (...data: any[]) => void, +): void { + const impl = ( + instance: SerializedSnapshotInstance, + level: number, + ) => { + let msg = ''; + for (let i = 0; i < level; ++i) { + msg += ' '; + } + msg += `| ${instance.id}(${instance.type}): ${JSON.stringify(instance.values)}`; + (log ?? logDebug)(msg); + for (const c of instance.children ?? []) { + impl(c, level + 1); + } + }; + + impl(instance, 0); +} + +export function printSnapshotInstanceToString( + instance: SnapshotInstance | BackgroundSnapshotInstance | SerializedSnapshotInstance, +): string { + const logArr: string[] = []; + if (instance instanceof SnapshotInstance || instance instanceof BackgroundSnapshotInstance) { + printSnapshotInstance(instance, logArr.push.bind(logArr)); + } else { + printSerializedSnapshotInstance(instance, logArr.push.bind(logArr)); + } + return logArr.join('\n'); +} diff --git a/packages/react/runtime/src/lifecycle/patch/error.ts b/packages/react/runtime/src/lifecycle/patch/error.ts index ed6e6fb173..cd056789c1 100644 --- a/packages/react/runtime/src/lifecycle/patch/error.ts +++ b/packages/react/runtime/src/lifecycle/patch/error.ts @@ -42,7 +42,11 @@ export function reportCtxNotFound(data: CtxNotFoundData): void { } } - lynx.reportError(new Error(`${errorMsg}, snapshot type: '${snapshotType}'`)); + let message = `${errorMsg}, snapshot type: '${snapshotType}'`; + if (__DEV__) { + message += '. You can set environment variable `REACT_ALOG=true` and restart your dev server for troubleshooting.'; + } + lynx.reportError(new Error(message)); } export function addCtxNotFoundEventListener(): void { diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts index f33d2f2b1a..ce181f77a0 100644 --- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts +++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts @@ -8,6 +8,7 @@ import { runRunOnMainThreadTask, setEomShouldFlushElementTree } from '@lynx-js/r import type { PatchList, PatchOptions } from './commit.js'; import { setMainThreadHydrating } from './isMainThreadHydrating.js'; import { snapshotPatchApply } from './snapshotPatchApply.js'; +import { prettyFormatSnapshotPatch } from '../../debug/formatPatch.js'; import { LifecycleConstant } from '../../lifecycleConstant.js'; import { markTiming, setPipeline } from '../../lynx/performance.js'; import { __pendingListUpdates } from '../../pendingListUpdates.js'; @@ -37,7 +38,27 @@ function updateMainThread( setPipeline(patchOptions.pipelineOptions); markTiming('mtsRenderStart'); markTiming('parseChangesStart'); - const { patchList, flushOptions = {}, delayedRunOnMainThreadData } = JSON.parse(data) as PatchList; + const parsedData = JSON.parse(data) as PatchList; + const { patchList, flushOptions = {}, delayedRunOnMainThreadData } = parsedData; + + if (typeof __ALOG__ !== 'undefined' && __ALOG__) { + console.alog?.( + '[ReactLynxDebug] BTS -> MTS updateMainThread:\n' + JSON.stringify( + { + data: { + ...parsedData, + patchList: parsedData.patchList.map(patch => ({ + ...patch, + snapshotPatch: prettyFormatSnapshotPatch(patch.snapshotPatch), + })), + }, + patchOptions, + }, + null, + 2, + ), + ); + } markTiming('parseChangesEnd'); markTiming('patchChangesStart'); diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 73788535f4..e6febee621 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -8,6 +8,7 @@ import type { FirstScreenData } from '../lifecycleConstant.js'; import { PerformanceTimingFlags, PipelineOrigins, beginPipeline, markTiming } from './performance.js'; import { BackgroundSnapshotInstance, hydrate } from '../backgroundSnapshot.js'; import { runWithForce } from './runWithForce.js'; +import { printSnapshotInstanceToString } from '../debug/printSnapshot.js'; import { profileEnd, profileStart } from '../debug/utils.js'; import { destroyBackground } from '../lifecycle/destroy.js'; import { delayedEvents, delayedPublishEvent } from '../lifecycle/event/delayEvents.js'; @@ -88,12 +89,39 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { beginPipeline(true, PipelineOrigins.reactLynxHydrate, PerformanceTimingFlags.reactLynxHydrate); markTiming('hydrateParseSnapshotStart'); const before = JSON.parse(lepusSide) as SerializedSnapshotInstance; + if (typeof __ALOG__ !== 'undefined' && __ALOG__) { + console.alog?.( + '[ReactLynxDebug] MTS -> BTS OnLifecycleEvent:\n' + JSON.stringify( + { + ...data as object, + // use parsed lepusSide to avoid extra escape characters ('\\') + root: before, + }, + null, + 2, + ), + ); + console.alog?.( + '[ReactLynxDebug] SnapshotInstance tree for first screen hydration:\n' + + printSnapshotInstanceToString(before), + ); + console.alog?.( + '[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration:\n' + + printSnapshotInstanceToString(__root as BackgroundSnapshotInstance), + ); + } markTiming('hydrateParseSnapshotEnd'); markTiming('diffVdomStart'); const snapshotPatch = hydrate( before, __root as BackgroundSnapshotInstance, ); + if (typeof __ALOG__ !== 'undefined' && __ALOG__) { + console.alog?.( + '[ReactLynxDebug] BackgroundSnapshotInstance after hydration:\n' + + printSnapshotInstanceToString(__root as BackgroundSnapshotInstance), + ); + } if (__PROFILE__) { profileEnd(); } diff --git a/packages/react/runtime/src/snapshot.ts b/packages/react/runtime/src/snapshot.ts index b99c517576..52092c16f4 100644 --- a/packages/react/runtime/src/snapshot.ts +++ b/packages/react/runtime/src/snapshot.ts @@ -295,7 +295,12 @@ export class SnapshotInstance { if (snapshotCreatorMap[type]) { snapshotCreatorMap[type](type); } else { - throw new Error('Snapshot not found: ' + type); + let message = 'Snapshot not found: ' + type; + if (__DEV__) { + message += + '. You can set environment variable `REACT_ALOG=true` and restart your dev server for troubleshooting.'; + } + throw new Error(message); } } this.__snapshot_def = snapshotManager.values.get(type)!; diff --git a/packages/react/testing-library/src/__tests__/alog.test.jsx b/packages/react/testing-library/src/__tests__/alog.test.jsx index b8b454285b..3bf729351a 100644 --- a/packages/react/testing-library/src/__tests__/alog.test.jsx +++ b/packages/react/testing-library/src/__tests__/alog.test.jsx @@ -37,6 +37,15 @@ describe('alog', () => { expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ + [ + "[ReactLynxDebug] FiberElement API call #1: __CreatePage("0", 0) => PAGE#0", + ], + [ + "[ReactLynxDebug] FiberElement API call #2: __GetElementUniqueID(PAGE#0) => 0", + ], + [ + "[ReactLynxDebug] FiberElement API call #3: __SetCSSId([PAGE#0], 0)", + ], [ "[MainThread Component Render] name: ClassComponent", ], @@ -46,6 +55,114 @@ describe('alog', () => { [ "[MainThread Component Render] name: App", ], + [ + "[ReactLynxDebug] FiberElement API call #4: __CreateView(0) => VIEW#1", + ], + [ + "[ReactLynxDebug] FiberElement API call #5: __CreateText(0) => TEXT#2", + ], + [ + "[ReactLynxDebug] FiberElement API call #6: __AppendElement(VIEW#1, TEXT#2)", + ], + [ + "[ReactLynxDebug] FiberElement API call #7: __CreateRawText("count: ") => #text#3", + ], + [ + "[ReactLynxDebug] FiberElement API call #8: __AppendElement(TEXT#2, #text#3)", + ], + [ + "[ReactLynxDebug] FiberElement API call #9: __CreateWrapperElement(0) => WRAPPER#4", + ], + [ + "[ReactLynxDebug] FiberElement API call #10: __AppendElement(TEXT#2, WRAPPER#4)", + ], + [ + "[ReactLynxDebug] FiberElement API call #11: __CreateWrapperElement(0) => WRAPPER#5", + ], + [ + "[ReactLynxDebug] FiberElement API call #12: __AppendElement(VIEW#1, WRAPPER#5)", + ], + [ + "[ReactLynxDebug] FiberElement API call #13: __AppendElement(PAGE#0, VIEW#1)", + ], + [ + "[ReactLynxDebug] FiberElement API call #14: __AddEvent(TEXT#2, "bindEvent", "tap", "-2:0:")", + ], + [ + "[ReactLynxDebug] FiberElement API call #15: __CreateWrapperElement(0) => WRAPPER#6", + ], + [ + "[ReactLynxDebug] FiberElement API call #16: __ReplaceElement(WRAPPER#6, WRAPPER#4)", + ], + [ + "[ReactLynxDebug] FiberElement API call #17: __CreateRawText("") => #text#7", + ], + [ + "[ReactLynxDebug] FiberElement API call #18: __SetAttribute(#text#7, "text", 0)", + ], + [ + "[ReactLynxDebug] FiberElement API call #19: __AppendElement(WRAPPER#6, #text#7)", + ], + [ + "[ReactLynxDebug] FiberElement API call #20: __CreateWrapperElement(0) => WRAPPER#8", + ], + [ + "[ReactLynxDebug] FiberElement API call #21: __ReplaceElement(WRAPPER#8, WRAPPER#5)", + ], + [ + "[ReactLynxDebug] FiberElement API call #22: __CreateView(0) => VIEW#9", + ], + [ + "[ReactLynxDebug] FiberElement API call #23: __CreateRawText("Class Component") => #text#10", + ], + [ + "[ReactLynxDebug] FiberElement API call #24: __AppendElement(VIEW#9, #text#10)", + ], + [ + "[ReactLynxDebug] FiberElement API call #25: __AppendElement(WRAPPER#8, VIEW#9)", + ], + [ + "[ReactLynxDebug] FiberElement API call #26: __CreateView(0) => VIEW#11", + ], + [ + "[ReactLynxDebug] FiberElement API call #27: __CreateRawText("Function Component") => #text#12", + ], + [ + "[ReactLynxDebug] FiberElement API call #28: __AppendElement(VIEW#11, #text#12)", + ], + [ + "[ReactLynxDebug] FiberElement API call #29: __AppendElement(WRAPPER#8, VIEW#11)", + ], + [ + "[ReactLynxDebug] FiberElement API call #30: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_426db_test_1\\",\\"values\\":[\\"-2:0:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-7,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-4,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-5,\\"type\\":\\"__snapshot_426db_test_2\\"},{\\"id\\":-6,\\"type\\":\\"__snapshot_426db_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", + ], + [ + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "snapshotPatch": [], + "id": 2 + } + ] + }, + "patchOptions": { + "isHydration": true, + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", + ], + [ + "[ReactLynxDebug] FiberElement API call #31: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", + ], ] `); expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` @@ -62,6 +179,84 @@ describe('alog', () => { [ "[BackgroundThread Component Render] name: Fragment, uniqID: __snapshot_426db_test_1, __id: 2", ], + [ + "[ReactLynxDebug] MTS -> BTS OnLifecycleEvent: + { + "root": { + "id": -1, + "type": "root", + "children": [ + { + "id": -2, + "type": "__snapshot_426db_test_1", + "values": [ + "-2:0:" + ], + "children": [ + { + "id": -3, + "type": "wrapper", + "children": [ + { + "id": -7, + "type": null, + "values": [ + 0 + ] + } + ] + }, + { + "id": -4, + "type": "wrapper", + "children": [ + { + "id": -5, + "type": "__snapshot_426db_test_2" + }, + { + "id": -6, + "type": "__snapshot_426db_test_3" + } + ] + } + ] + } + ] + }, + "jsReadyEventIdSwap": {} + }", + ], + [ + "[ReactLynxDebug] SnapshotInstance tree for first screen hydration: + | -1(root): undefined + | -2(__snapshot_426db_test_1): ["-2:0:"] + | -3(wrapper): undefined + | -7(null): [0] + | -4(wrapper): undefined + | -5(__snapshot_426db_test_2): undefined + | -6(__snapshot_426db_test_3): undefined", + ], + [ + "[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration: + | 1(root): undefined + | 2(__snapshot_426db_test_1): [null] + | 3(wrapper): undefined + | 4(null): [0] + | 5(wrapper): undefined + | 6(__snapshot_426db_test_2): undefined + | 7(__snapshot_426db_test_3): undefined", + ], + [ + "[ReactLynxDebug] BackgroundSnapshotInstance after hydration: + | -1(root): undefined + | -2(__snapshot_426db_test_1): [null] + | -3(wrapper): undefined + | -7(null): [0] + | -4(wrapper): undefined + | -5(__snapshot_426db_test_2): undefined + | -6(__snapshot_426db_test_3): undefined", + ], ] `); @@ -82,7 +277,46 @@ describe('alog', () => { _setCount(1); }); - expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(`[]`); + expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(` + [ + [ + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "id": 3, + "snapshotPatch": [ + { + "op": "SetAttribute", + "id": -7, + "dynamicPartIndex": 0, + "value": 1 + } + ] + } + ] + }, + "patchOptions": { + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", + ], + [ + "[ReactLynxDebug] FiberElement API call #32: __SetAttribute(#text#7, "text", 1)", + ], + [ + "[ReactLynxDebug] FiberElement API call #33: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", + ], + ] + `); expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ diff --git a/packages/react/testing-library/src/__tests__/lynx.test.jsx b/packages/react/testing-library/src/__tests__/lynx.test.jsx index aebe7494e3..6151c8f730 100644 --- a/packages/react/testing-library/src/__tests__/lynx.test.jsx +++ b/packages/react/testing-library/src/__tests__/lynx.test.jsx @@ -52,7 +52,7 @@ describe('lynx global API', () => { const reportErrorCalls = lynxTestingEnv.backgroundThread.lynx.reportError.mock.calls; expect(() => render()).toThrowErrorMatchingInlineSnapshot( - `[Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null']`, + `[Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'. You can set environment variable \`REACT_ALOG=true\` and restart your dev server for troubleshooting.]`, ); const snapshotPatch = JSON.parse(callLepusMethodCalls[0][1]['data']).patchList[0].snapshotPatch; diff --git a/packages/react/testing-library/src/vitest-global-setup.js b/packages/react/testing-library/src/vitest-global-setup.js index ad9836af51..e140a6f617 100644 --- a/packages/react/testing-library/src/vitest-global-setup.js +++ b/packages/react/testing-library/src/vitest-global-setup.js @@ -8,6 +8,7 @@ import { injectUpdateMainThread } from '../../runtime/lib/lifecycle/patch/update import { injectUpdateMTRefInitValue } from '../../runtime/lib/worklet/ref/updateInitValue.js'; import { injectCalledByNative } from '../../runtime/lib/lynx/calledByNative.js'; import { flushDelayedLifecycleEvents, injectTt } from '../../runtime/lib/lynx/tt.js'; +import { initElementPAPICallAlog } from '../../runtime/lib/alog/elementPAPICall.js'; import { addCtxNotFoundEventListener } from '../../runtime/lib/lifecycle/patch/error.js'; import { setRoot } from '../../runtime/lib/root.js'; import { @@ -108,6 +109,8 @@ globalThis.onInjectMainThreadGlobals = (target) => { target._document = setupDocument({}); target.globalPipelineOptions = undefined; + + initElementPAPICallAlog(target); }; globalThis.onInjectBackgroundThreadGlobals = (target) => { if (onInjectBackgroundThreadGlobals) { diff --git a/packages/webpack/react-webpack-plugin/test/cases/compat/component-pkg/index.jsx b/packages/webpack/react-webpack-plugin/test/cases/compat/component-pkg/index.jsx index afdbf2dc9e..6cee6eb37d 100644 --- a/packages/webpack/react-webpack-plugin/test/cases/compat/component-pkg/index.jsx +++ b/packages/webpack/react-webpack-plugin/test/cases/compat/component-pkg/index.jsx @@ -12,7 +12,9 @@ it('should not have Component in output', async () => { const content = await fs.promises.readFile(__filename, 'utf-8'); if (__JS__) { - expect(content).not.toContain(['V', 'i', 'e', 'w'].join('')); + expect(content.replaceAll('__CreateView', '')).not.toContain( + ['V', 'i', 'e', 'w'].join(''), + ); } expect(content).not.toContain(['react', 'components'].join('-')); });