Skip to content

chore(react): bump @lynx-js/internal-preact + fix SlotV2 cross-wrapper InsertBefore#2512

Merged
upupming merged 12 commits intomainfrom
chore/bump-internal-preact-10.29.1
Apr 24, 2026
Merged

chore(react): bump @lynx-js/internal-preact + fix SlotV2 cross-wrapper InsertBefore#2512
upupming merged 12 commits intomainfrom
chore/bump-internal-preact-10.29.1

Conversation

@upupming
Copy link
Copy Markdown
Collaborator

@upupming upupming commented Apr 23, 2026

Summary

Two fixes that together restore correct cross-slot keyed move behavior in SlotV2 snapshots.

1. Bump @lynx-js/internal-preact for PR #18

When a keyed child moved to a different $N slot across a re-render, preact's insert() slot-branch:

  • (commit 1, 40912aae) used null as the insertBefore reference when the diff cursor wasn't in the moved node's old slot, appending the moved node past stable siblings — wrong DOM order.
  • (commit 2, 995ae4de) when the moved node was exactly at the diff cursor, called insertBefore(node, node) which corrupted BackgroundSnapshotInstance's linked list with a self-cycle (eventually OOM on main thread).

Both are fixed in PR #18 (merged → 10.29.1-20260424024911-12b794f). We pin via the pkg.pr.new URL because the published npm package is named @lynx-js/internal-preact, which can't satisfy @preact/signals's preact peer-dep — that creates a dual-preact graph and breaks hooks (see commit message of d214d521).

2. Fix SnapshotInstance.insertBefore for cross-wrapper case

For SlotV2 each $N has its own wrapper element. The original code only fell back to __AppendElement when newNode.__slotIndex < existingNode.__slotIndex; the > direction fell through to __InsertElementBefore(newWrapper, node, ref) which throws NotFoundError: The child can not be found in the parent. because ref lives in another wrapper.

The minimum repro is a 3-key cross-slot rearrange ([F,H,E,G] → [H,A,D,F]) — A is new at slot 1, but its existingNode cursor is F (still at slot 0 since F's slot update happens later). Widened the check to !== so any cross-wrapper insert falls back to __AppendElement, relying on DOM appendChild's auto-detach to move the node out of its old wrapper.

Tests added (packages/react/testing-library/src/__tests__/slot-jsx.test.jsx)

  • single-key cross-slot move (E moves $2 → $0) with patch + tree assertions
  • multi-key cross-slot move (H moves $0 → $1, G moves $2 → $3)
  • three-key cross-slot move (the regression that triggered the runtime fix)
  • two-slot keyed swap
  • array slot diff-context isolation — documents that arrays-in-slots break cross-slot keyed reuse, so the unreachable edge case stays unreachable
  • 10000-step / 6-slot fuzz, keyed and unkeyed variants end-to-end through the main-thread renderer, mirroring the property-based fuzz in internal-preact

Each cross-slot test snapshots the exact element-PAPI sequence (create / append / insertBefore / remove) so any regression that adds a redundant DOM op or accidentally re-creates a reused keyed node shows up as a snapshot diff.

Test plan

  • slot-jsx.test.jsx passes (8 tests)
  • full react/runtime + react/testing-library suites pass locally (657 tests)
  • CI green

@upupming upupming requested review from HuJean, Yradex and hzy as code owners April 23, 2026 03:21
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: 4221128

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@lynx-js/react Minor
@lynx-js/react-umd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Upgrades @lynx-js/internal-preact in packages/react and fixes SnapshotInstance.insertBefore slot-V2 handling; removes an old setState JSX test and adds a large slot/keyed-move test suite plus a changeset entry documenting the bump and fix. (47 words)

Changes

Cohort / File(s) Summary
Dependency
packages/react/package.json
Bumps @lynx-js/internal-preact from 10.28.4-dfff9aa to 10.29.1-20260424024911-12b794f.
Runtime change
packages/react/runtime/src/snapshot/snapshot/snapshot.ts
Adjusts SnapshotInstance.insertBefore for isSlotV2: detect differing __slotIndex and append across wrapper mismatch instead of calling cross-wrapper __InsertElementBefore.
Tests
packages/react/testing-library/src/__tests__/setState-jsx.test.jsx, packages/react/testing-library/src/__tests__/slot-jsx.test.jsx
Removes the old setState-jsx test; adds a comprehensive slot-jsx suite validating slot reconciliation, exact patch op sequences, cross-slot keyed moves (including fallback when reference nodes are outside target parent), a spyElementApi() op-trace helper, and a large randomized fuzz test.
Release notes
.changeset/bump-internal-preact-10-29-1-12b794f.md
Adds changeset documenting a minor bump and a fix for incorrect DOM ordering when keyed children move across slots.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • HuJean
  • hzy
  • Yradex

Poem

🐇 I hopped through slots and traced each move,
With tiny ops the DOM I prove,
A bump, a patch, a test that plays—
Now keys find homes in proper ways. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately reflects the main change: bumping @lynx-js/internal-preact and fixing SlotV2 cross-wrapper InsertBefore behavior, which aligns with all significant file changes including dependency update, bug fix commit, and new regression tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/bump-internal-preact-10.29.1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 23, 2026

Merging this PR will improve performance by 9.1%

⚡ 1 improved benchmark
✅ 80 untouched benchmarks
⏩ 26 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
002-hello-reactLynx-destroyBackground 894.3 µs 819.8 µs +9.1%

Comparing chore/bump-internal-preact-10.29.1 (4221128) with main (989f345)

Open in CodSpeed

Footnotes

  1. 26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 23, 2026

React External

#746 Bundle Size — 680.27KiB (+0.01%).

4221128(current) vs 989f345 main#736(baseline)

Bundle metrics  Change 1 change
                 Current
#746
     Baseline
#736
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 39.69% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#746
     Baseline
#736
Regression  Other 680.27KiB (+0.01%) 680.2KiB

Bundle analysis reportBranch chore/bump-internal-preact-10.29...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 23, 2026

Web Explorer

#9202 Bundle Size — 900.02KiB (0%).

4221128(current) vs 989f345 main#9192(baseline)

Bundle metrics  Change 2 changes
                 Current
#9202
     Baseline
#9192
No change  Initial JS 44.46KiB 44.46KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 230(+1.32%) 227
No change  Duplicate Modules 11 11
Change  Duplicate Code 27.28%(-0.04%) 27.29%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9202
     Baseline
#9192
No change  JS 495.88KiB 495.88KiB
No change  Other 401.92KiB 401.92KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch chore/bump-internal-preact-10.29...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 23, 2026

React MTF Example

#761 Bundle Size — 196.54KiB (+0.04%).

4221128(current) vs 989f345 main#751(baseline)

Bundle metrics  Change 2 changes
                 Current
#761
     Baseline
#751
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 43.38% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 173 173
No change  Duplicate Modules 66 66
Change  Duplicate Code 44.08%(+0.02%) 44.07%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#761
     Baseline
#751
No change  IMG 111.23KiB 111.23KiB
Regression  Other 85.31KiB (+0.08%) 85.24KiB

Bundle analysis reportBranch chore/bump-internal-preact-10.29...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 23, 2026

React Example

#7629 Bundle Size — 225.38KiB (+0.03%).

4221128(current) vs 989f345 main#7619(baseline)

Bundle metrics  Change 1 change
                 Current
#7629
     Baseline
#7619
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 35.31% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 179 179
No change  Duplicate Modules 69 69
No change  Duplicate Code 44.57% 44.57%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#7629
     Baseline
#7619
No change  IMG 145.76KiB 145.76KiB
Regression  Other 79.63KiB (+0.09%) 79.55KiB

Bundle analysis reportBranch chore/bump-internal-preact-10.29...Project dashboard


Generated by RelativeCIDocumentationReport issue

…-e604bd5

Includes a diff-time fix that baselines `__slotIndex` on freshly-created
DOM nodes, preventing `insert()`'s slot-branch from firing spuriously on
first placement and avoiding a stale `insertBefore` reference when a
sibling detaches mid-diff.
@upupming upupming force-pushed the chore/bump-internal-preact-10.29.1 branch from 149ec84 to 5854047 Compare April 23, 2026 14:49
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/testing-library/src/__tests__/setState-jsx.test.jsx`:
- Around line 174-185: The test calls lynxTestingEnv.switchToMainThread(), runs
assertions (inspecting __root and calling printSnapshotInstanceToString), then
calls lynxTestingEnv.switchToBackgroundThread() but if an assertion throws the
background switch is skipped; fix by wrapping the main-thread inspection block
(the code between lynxTestingEnv.switchToMainThread() and
switchToBackgroundThread()) in a try/finally and ensure
lynxTestingEnv.switchToBackgroundThread() is invoked in the finally block so the
environment is always restored; apply the same try/finally pattern to the other
occurrence mentioned (the block around lines 314-325).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c804e5d-d5a1-4439-a01e-8f0fed50b2b0

📥 Commits

Reviewing files that changed from the base of the PR and between 149ec84 and 5854047.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • .changeset/bump-internal-preact-10-29-1.md
  • packages/react/package.json
  • packages/react/testing-library/src/__tests__/setState-jsx.test.jsx
✅ Files skipped from review due to trivial changes (2)
  • packages/react/package.json
  • .changeset/bump-internal-preact-10-29-1.md

Comment thread packages/react/testing-library/src/__tests__/setState-jsx.test.jsx Outdated
Rename setState-jsx.test.jsx → slot-jsx.test.jsx and add three tests
exposing the slot-branch null-fallback bug in @lynx-js/internal-preact:
single-key cross-slot move emits wrong beforeId, multi-key move drops
children from the background snapshot tree, and a 200-step fuzz that
catches any layout producing a spurious null beforeId.

Also update changeset: bump to minor, shorten description, add diff link.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb4f48d72b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
@upupming upupming marked this pull request as draft April 23, 2026 17:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/react/testing-library/src/__tests__/slot-jsx.test.jsx (3)

174-185: Thread-switch state can leak on failure.

Lines 174/185 (and 314/325) flip lynxTestingEnv between main and background threads without a try/finally. If any intervening expect throws, subsequent tests inherit the wrong thread and cascade with confusing failures. Not a blocker, but a try { switchToMainThread(); ... } finally { switchToBackgroundThread(); } — or a afterEach that normalizes the thread — would make failures localized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` around lines
174 - 185, The test flips lynxTestingEnv between threads without guaranteeing
restoration, so failing assertions can leave the environment on the main thread;
wrap the section that calls lynxTestingEnv.switchToMainThread() and the
subsequent assertions in a try/finally that calls
lynxTestingEnv.switchToBackgroundThread() in the finally block (or add an
afterEach that always calls lynxTestingEnv.switchToBackgroundThread()), ensuring
the code paths using switchToMainThread()/switchToBackgroundThread() (and the
surrounding assertions like expect(...).toMatchInlineSnapshot and
printSnapshotInstanceToString(__root)) always restore the background thread even
on failure.

10-13: Unused spy reference.

onLifecycleEventCalls is captured on line 11 but never read; same for test 2 which spies on OnLifecycleEvent at line 334 without asserting anything. Either assert against it or drop the spy to reduce noise.

♻️ Proposed cleanup
-  vi.spyOn(lynxTestingEnv.backgroundThread.lynxCoreInject.tt, 'OnLifecycleEvent');
-  const onLifecycleEventCalls = lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent.mock.calls;
   vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
   const callLepusMethodCalls = lynx.getNativeApp().callLepusMethod.mock.calls;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` around lines
10 - 13, The test creates unused spies and captures (remove or assert them):
either delete the vi.spyOn(...) for
lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent and the
unused const onLifecycleEventCalls (and the similar unused spy later that
references OnLifecycleEvent), or replace them with meaningful assertions (e.g.,
expect(onLifecycleEventCalls.length).toBeGreaterThan(0)) where you actually rely
on those calls; also keep or assert callLepusMethodCalls from
lynx.getNativeApp().callLepusMethod if it's used, otherwise remove that
spy/const as well. Ensure references to OnLifecycleEvent, onLifecycleEventCalls,
and callLepusMethodCalls are either asserted against or removed.

484-506: Use structural tree traversal instead of indentation-dependent regex for child counting.

The regex /^ {4}\| \d+\(/gm is tightly coupled to the hardcoded 2-space-per-level indentation in printSnapshotInstanceToString. If that indentation format ever changes (to 1 space, 3 spaces, tabs, etc.), the regex silently returns 0 and the assertions pass vacuously.

Since SnapshotInstance.childNodes is available, replace the regex with direct property access:

const getViewChildCount = () => {
  const viewNode = __root.childNodes[0]; // assuming view is first child
  return viewNode.childNodes.length;
};

This eliminates the indentation dependency and is more explicit about the tree structure being tested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` around lines
484 - 506, The test's getViewChildCount uses an indentation-dependent regex on
printSnapshotInstanceToString which can break if formatting changes; replace
that with a structural traversal using the SnapshotInstance.childNodes tree
(starting from __root and locating the view node, e.g. __root.childNodes[0]) and
return its childNodes.length instead of relying on the regex so
getViewChildCount, printSnapshotInstanceToString and __root use the actual tree
structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx`:
- Around line 396-436: The comments are stale versus the new slot-branch
behavior; update the test text to match the post-fix snapshots (or update
snapshots if you intended to assert the pre-fix behavior). Specifically, either
remove/replace the "Bug: E moves..." and "Background tree: wrong order..." lines
with a one-line explanation that beforeId can be null because SlotV2 uses
slotIndex (referencing callLepusMethodCalls and prettyFormatSnapshotPatch for
the patch expectation), or if you intended to validate the corrected behavior
adjust the inline snapshots and the final tree assertion produced by
printSnapshotInstanceToString(__root) to show [E, A, N, D]. Ensure any comment
mentions SlotV2/slotIndex and reference functions prettyFormatSnapshotPatch and
printSnapshotInstanceToString so readers understand why beforeId is null.
- Around line 600-603: The inline snapshot call toMatchInlineSnapshot() is
empty, so the final-order assertion is a no-op; update the test by replacing the
empty call on the final assertion that uses
printSnapshotInstanceToString(__root) with a populated inline snapshot string
that represents the expected final layout in correct slot order (capture the
output of printSnapshotInstanceToString(__root) after the fuzz run and paste it
into the toMatchInlineSnapshot call). Locate the assertion using
printSnapshotInstanceToString and __root in the slot-jsx.test.jsx test and
insert the concrete snapshot text into toMatchInlineSnapshot to make the
invariant effective.
- Around line 507-510: The inline snapshot assertion is empty and doesn't verify
the expected post-fix slot order; update the test that calls
expect(printSnapshotInstanceToString(__root)).toMatchInlineSnapshot() to include
the actual snapshot output showing the final slot order (the tree string that
reflects [F, H, E, G] order) so the regression fails if the order changes; run
the test locally (or let Jest populate the inline snapshot) and commit the
populated snapshot string for the expect(printSnapshotInstanceToString(__root))
assertion.

---

Nitpick comments:
In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx`:
- Around line 174-185: The test flips lynxTestingEnv between threads without
guaranteeing restoration, so failing assertions can leave the environment on the
main thread; wrap the section that calls lynxTestingEnv.switchToMainThread() and
the subsequent assertions in a try/finally that calls
lynxTestingEnv.switchToBackgroundThread() in the finally block (or add an
afterEach that always calls lynxTestingEnv.switchToBackgroundThread()), ensuring
the code paths using switchToMainThread()/switchToBackgroundThread() (and the
surrounding assertions like expect(...).toMatchInlineSnapshot and
printSnapshotInstanceToString(__root)) always restore the background thread even
on failure.
- Around line 10-13: The test creates unused spies and captures (remove or
assert them): either delete the vi.spyOn(...) for
lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent and the
unused const onLifecycleEventCalls (and the similar unused spy later that
references OnLifecycleEvent), or replace them with meaningful assertions (e.g.,
expect(onLifecycleEventCalls.length).toBeGreaterThan(0)) where you actually rely
on those calls; also keep or assert callLepusMethodCalls from
lynx.getNativeApp().callLepusMethod if it's used, otherwise remove that
spy/const as well. Ensure references to OnLifecycleEvent, onLifecycleEventCalls,
and callLepusMethodCalls are either asserted against or removed.
- Around line 484-506: The test's getViewChildCount uses an
indentation-dependent regex on printSnapshotInstanceToString which can break if
formatting changes; replace that with a structural traversal using the
SnapshotInstance.childNodes tree (starting from __root and locating the view
node, e.g. __root.childNodes[0]) and return its childNodes.length instead of
relying on the regex so getViewChildCount, printSnapshotInstanceToString and
__root use the actual tree structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c695dfca-1376-4e0a-8e77-445510740e22

📥 Commits

Reviewing files that changed from the base of the PR and between 5854047 and fb4f48d.

📒 Files selected for processing (3)
  • .changeset/bump-internal-preact-10-29-1.md
  • packages/react/testing-library/src/__tests__/setState-jsx.test.jsx
  • packages/react/testing-library/src/__tests__/slot-jsx.test.jsx
💤 Files with no reviewable changes (1)
  • packages/react/testing-library/src/tests/setState-jsx.test.jsx
✅ Files skipped from review due to trivial changes (1)
  • .changeset/bump-internal-preact-10-29-1.md

Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx
Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx
…e behavior

All four tests now assert the fixed behavior: E/H/G land in their correct
slots with real beforeId references, background and main-thread trees both
show the right slot order, and the 200-step fuzz passes without child-count
regression.

Remove unused Component import, dead OnLifecycleEvent spy, and stale
comments that described the old buggy state.
@upupming upupming changed the title chore(react): bump @lynx-js/internal-preact to 10.29.1 chore(react): bump @lynx-js/internal-preact to 10.29.1 (PR #18) Apr 24, 2026
@upupming upupming changed the title chore(react): bump @lynx-js/internal-preact to 10.29.1 (PR #18) chore(react): bump @lynx-js/internal-preact to 10.29.1 Apr 24, 2026
Updates the bump target to the merged version of PR #18 in internal-preact,
which includes both the cross-slot keyed move fix and the self-insert
guard. Same code as the published 10.29.1-20260424024911-12b794f release.

Continues using the pkg.pr.new URL form: the published npm package is
named @lynx-js/internal-preact, which can't satisfy @preact/signals's
preact peer dep, leading to dual-preact instances and broken hook state
in tests. The pkg.pr.new tarball is named preact and resolves cleanly.
@upupming upupming force-pushed the chore/bump-internal-preact-10.29.1 branch from 96cd3e9 to 7dbda08 Compare April 24, 2026 03:26
@upupming upupming marked this pull request as ready for review April 24, 2026 06:38
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dbda08ca7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/react/testing-library/src/__tests__/slot-jsx.test.jsx (2)

536-553: Fuzz test collects patches but never asserts against them — test body doesn't match its name.

The test title claims to verify "update patches have no spurious null beforeId when later slot siblings exist", and lines 548–553 replace callLepusMethod with a mock that accumulates every data payload into patches. However, patches is never read afterwards — the only invariants checked are childCount() === SLOT_COUNT (line 611) and the final tree snapshot (line 621). The child-count check catches gross corruption but wouldn't detect, e.g., a correctly-placed element whose InsertBefore still carries a stray beforeId: null — which is the exact scenario the title calls out.

Either drop the unused collection to match what the test actually verifies, or add an explicit per-step scan of the emitted snapshotPatch for InsertBefore ops whose beforeId is null while a later keyed sibling exists in the target slot. The latter would make this test a real regression guard for the underlying internal-preact fix rather than relying on indirect symptoms.

🧪 Sketch of a direct assertion
   let failure = null;
   for (step = 1; step < STEPS && !failure; step++) {
     act(() => {
       setStep(step);
     });
     const count = childCount();
     if (count !== SLOT_COUNT) {
       failure = `step=${step}`
         + `\n  prev=${JSON.stringify(layouts[step - 1])}`
         + `\n  next=${JSON.stringify(layouts[step])}`
         + `\n  want child count=${SLOT_COUNT}, got=${count}`
         + `\n  tree=${printSnapshotInstanceToString(__root)}`;
+      continue;
     }
+    // Walk the most-recent patch and flag any InsertBefore with a null beforeId
+    // where a later keyed sibling already exists in that slot.
+    const last = patches[patches.length - 1];
+    if (last) {
+      const ops = prettyFormatSnapshotPatch(
+        JSON.parse(last).patchList[0].snapshotPatch,
+      );
+      // ... scan ops and set `failure` if a spurious null beforeId is found
+    }
   }

Also applies to: 600-628

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` around lines
536 - 553, The test creates and accumulates emitted patches by spying on
lynx.getNativeApp().callLepusMethod into the patches array but never asserts on
them; update the test (involving patches, callLepusMethod, snapshotPatch,
InsertBefore, beforeId, SLOT_COUNT, childCount) to either remove the unused
collection or (preferred) add a check after each mutation step that scans the
collected patches for snapshotPatch payloads and fails if any InsertBefore
operation has beforeId === null while the target slot contains a later keyed
sibling—i.e., iterate patches, locate snapshotPatch ops, and assert no
InsertBefore with beforeId null when a later sibling key exists in that slot to
directly verify the bug described.

338-338: Remove unused OnLifecycleEvent spy.

The spy installed on lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent is never referenced or asserted on in this test, so it does nothing beyond adding overhead. The PR explicitly states it removes unused spies elsewhere; this one appears to have been missed.

♻️ Proposed fix
-  vi.spyOn(lynxTestingEnv.backgroundThread.lynxCoreInject.tt, 'OnLifecycleEvent');
   vi.spyOn(lynx.getNativeApp(), 'callLepusMethod');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` at line 338,
Remove the unused spy call on
lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent: delete the
vi.spyOn(lynxTestingEnv.backgroundThread.lynxCoreInject.tt, 'OnLifecycleEvent');
line from the test (slot-jsx.test.jsx) since it isn't asserted against or used;
after removal, run the test suite to ensure no other test relies on that spy and
adjust or add any required setup/teardown if failures appear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx`:
- Around line 536-553: The test creates and accumulates emitted patches by
spying on lynx.getNativeApp().callLepusMethod into the patches array but never
asserts on them; update the test (involving patches, callLepusMethod,
snapshotPatch, InsertBefore, beforeId, SLOT_COUNT, childCount) to either remove
the unused collection or (preferred) add a check after each mutation step that
scans the collected patches for snapshotPatch payloads and fails if any
InsertBefore operation has beforeId === null while the target slot contains a
later keyed sibling—i.e., iterate patches, locate snapshotPatch ops, and assert
no InsertBefore with beforeId null when a later sibling key exists in that slot
to directly verify the bug described.
- Line 338: Remove the unused spy call on
lynxTestingEnv.backgroundThread.lynxCoreInject.tt.OnLifecycleEvent: delete the
vi.spyOn(lynxTestingEnv.backgroundThread.lynxCoreInject.tt, 'OnLifecycleEvent');
line from the test (slot-jsx.test.jsx) since it isn't asserted against or used;
after removal, run the test suite to ensure no other test relies on that spy and
adjust or add any required setup/teardown if failures appear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 14ee5bec-25f9-4333-8658-580d66c96bf6

📥 Commits

Reviewing files that changed from the base of the PR and between fb4f48d and 7dbda08.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • .changeset/bump-internal-preact-10-29-1-12b794f.md
  • packages/react/package.json
  • packages/react/testing-library/src/__tests__/slot-jsx.test.jsx
✅ Files skipped from review due to trivial changes (2)
  • .changeset/bump-internal-preact-10-29-1-12b794f.md
  • packages/react/package.json

Yradex
Yradex previously approved these changes Apr 24, 2026
`SnapshotInstance.insertBefore` for SlotV2 was using `parent.insertBefore(node, ref)`
when `newNode.__slotIndex >= existingNode.__slotIndex`, but in SlotV2 each `$N` slot
has its own wrapper element. When `existingNode` lives in a different slot wrapper
than the target, the underlying DOM API throws `NotFoundError: The child can not be
found in the parent.` because `ref` isn't a child of the target wrapper.

Widen the existing append fallback to fire whenever the two nodes are in different
slots (was: only when `newNode.__slotIndex < existingNode.__slotIndex`). DOM /
native auto-detach via `appendChild` still moves the node out of its old wrapper.

Add three slot-jsx regression tests:
- single-key cross-slot move (E moves slot 2 → 0)
- multi-key cross-slot moves (H, G move simultaneously)
- three-key cross-slot move that previously threw (F, H + new A, D)
- 10000-step / 6-slot fuzz mirroring the internal-preact property test

Each cross-slot test snapshots the exact element-PAPI sequence (create / append /
insertBefore / remove with element identifiers) so regressions that add redundant
DOM ops or accidentally re-create reused keyed nodes show up as a snapshot diff.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47a0efeee8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/react/testing-library/src/__tests__/slot-jsx.test.jsx (1)

15-51: Optional: return a restore() from spyElementApi and call it per test.

vi.spyOn is captured but never restored. Each test that invokes spyElementApi() grabs original = g[name], which on the 2nd+ invocation is the previous test's wrapper — so spies accumulate and every PAPI call walks an ever‑growing chain of wrappers. The outer ops array still receives the right trace, so this isn't a correctness bug, but it's brittle and makes trace diffs harder to reason about if anyone widens the helper later.

♻️ Suggested refactor
 function spyElementApi() {
   const g = lynxTestingEnv.mainThread.globalThis;
+  const spies = [];
   ...
   const wrap = (name, formatter) => {
     const original = g[name];
-    vi.spyOn(g, name).mockImplementation((...args) => {
+    const spy = vi.spyOn(g, name).mockImplementation((...args) => {
       ops.push(formatter(args));
       return original.apply(g, args);
     });
+    spies.push(spy);
   };
   ...
   return {
     mark() { mark = ops.length; },
     trace() { return ops.slice(mark).join('\n'); },
+    restore() { for (const s of spies) s.mockRestore(); },
   };
 }

Then const trace = spyElementApi(); at the top of each test gets a matching trace.restore() (or use afterEach(() => vi.restoreAllMocks()) at the file level if restoreMocks isn't already set in the vitest config).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx` around lines
15 - 51, spyElementApi currently spies on global methods via vi.spyOn but never
restores them, causing spies to accumulate across tests; update spyElementApi to
capture each created spy (or the original functions) inside the wrap closure and
return a restore() method that restores each spy (e.g., call spy.mockRestore()
or reassign g[name] = original) so tests can call trace = spyElementApi(); ...;
trace.restore() (or use restore in afterEach). Ensure you reference the existing
symbols spyElementApi, wrap, vi.spyOn, and g[name] when implementing the restore
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx`:
- Line 721: The fuzz test "fuzz: cross-slot keyed moves keep slot order across
random layouts" is too tight for CI; reduce the STEPS constant from 10000 to
~2000, increase the test timeout from 5000 to 20000, and avoid asserting
slotOrder() on every iteration by only calling the equality checks every N
iterations (e.g., every 50) while still invoking setStep/act(setState) each
loop; apply the same changes to the related loop/test uses referenced by
STEPS/slotOrder()/setStep in the nearby tests.

---

Nitpick comments:
In `@packages/react/testing-library/src/__tests__/slot-jsx.test.jsx`:
- Around line 15-51: spyElementApi currently spies on global methods via
vi.spyOn but never restores them, causing spies to accumulate across tests;
update spyElementApi to capture each created spy (or the original functions)
inside the wrap closure and return a restore() method that restores each spy
(e.g., call spy.mockRestore() or reassign g[name] = original) so tests can call
trace = spyElementApi(); ...; trace.restore() (or use restore in afterEach).
Ensure you reference the existing symbols spyElementApi, wrap, vi.spyOn, and
g[name] when implementing the restore logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7dcd6367-7856-4af7-aa0c-2146c17fd89c

📥 Commits

Reviewing files that changed from the base of the PR and between 7dbda08 and 47a0efe.

📒 Files selected for processing (2)
  • packages/react/runtime/src/snapshot/snapshot/snapshot.ts
  • packages/react/testing-library/src/__tests__/slot-jsx.test.jsx

Comment thread packages/react/testing-library/src/__tests__/slot-jsx.test.jsx Outdated
- Replace the misleading "intra-slot ordering with cross-wrapper insert" test
  (the scenario was structurally unreachable because arrays in JSX become
  Fragments with their own diff context) with one that records the actual
  behavior: a keyed VNode moving from a view-level slot into an array slot
  is recreated, not cross-slot reused.

- Add a test for the only structurally reachable cross-slot keyed reuse:
  two view-level single-VNode slots swapping content. Each slot still ends
  up with at most one child, so the cross-wrapper `__AppendElement`
  fallback in `SnapshotInstance.insertBefore` is correct.
@upupming upupming changed the title chore(react): bump @lynx-js/internal-preact to 10.29.1 chore(react): bump @lynx-js/internal-preact + fix SlotV2 cross-wrapper InsertBefore Apr 24, 2026
Yradex
Yradex previously approved these changes Apr 24, 2026
@upupming upupming enabled auto-merge (squash) April 24, 2026 10:03
@upupming upupming merged commit 7e5f54c into main Apr 24, 2026
79 of 81 checks passed
@upupming upupming deleted the chore/bump-internal-preact-10.29.1 branch April 24, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants