Fix TransientNode not capturing BlockNode as anchor when replacing#13674
Fix TransientNode not capturing BlockNode as anchor when replacing#13674
Conversation
When a TransientNode without an anchor is set directly on a BlockNode position (empty delta path), the BlockNode should be captured as the anchor. This preserves the original block structure so it can be restored when transient elements are cleared. This fix mirrors the existing behavior in visitElementNode and resolves issues where container content written after a spinner renders would not appear correctly. Co-Authored-By: Claude <[email protected]>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where TransientNode instances without an anchor fail to capture the existing BlockNode when replacing it at an empty delta path. The fix ensures the original block structure is preserved for restoration when transient elements (like spinners) are cleared.
Changes:
- Added logic in
visitBlockNodeto wrap the existingBlockNodeas an anchor when replaced by an anchorlessTransientNode - Mirrors the existing pattern in
visitElementNodefor consistency - Includes comprehensive unit and e2e tests
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
frontend/lib/src/render-tree/visitors/SetNodeByDeltaPathVisitor.ts |
Adds anchor-wrapping logic to visitBlockNode when replacing with anchorless TransientNode |
frontend/lib/src/render-tree/visitors/SetNodeByDeltaPathVisitor.test.ts |
Adds unit tests for anchor wrapping and non-wrapping scenarios |
e2e_playwright/st_spinner.py |
Adds regression test app for delayed container write in spinner |
e2e_playwright/st_spinner_test.py |
Adds e2e test verifying container content appears correctly after spinner |
| return new TransientNode( | ||
| this.nodeToSet.scriptRunId, | ||
| node, | ||
| this.nodeToSet.transientNodes, | ||
| this.nodeToSet.deltaMsgReceivedAt | ||
| ) |
There was a problem hiding this comment.
The scriptRunId should be this.scriptRunId instead of this.nodeToSet.scriptRunId to match the pattern in visitElementNode (line 51). The visitor's scriptRunId is passed in and should be used when creating the new TransientNode wrapper, not the scriptRunId from the nodeToSet.
| it("wraps block node as anchor when setting TransientNode without anchor", () => { | ||
| const originalBlock = block([text("child1"), text("child2")]) | ||
| const transientNode = new TransientNode( | ||
| "run_id", | ||
| undefined, // No anchor provided | ||
| [text("spinner")], | ||
| 123 | ||
| ) | ||
| const visitor = new SetNodeByDeltaPathVisitor( | ||
| [], | ||
| transientNode, | ||
| "run_id" | ||
| ) | ||
|
|
||
| const result = visitor.visitBlockNode(originalBlock) as TransientNode | ||
|
|
||
| // Should wrap originalBlock as the anchor | ||
| expect(result).toBeInstanceOf(TransientNode) | ||
| expect(result.anchor).toBe(originalBlock) | ||
| expect(result.transientNodes).toEqual(transientNode.transientNodes) | ||
| expect(result.deltaMsgReceivedAt).toBe(transientNode.deltaMsgReceivedAt) | ||
| }) |
There was a problem hiding this comment.
The test should verify that result.scriptRunId matches the visitor's scriptRunId to ensure the correct scriptRunId is being used when wrapping the anchor. This would catch the bug where this.nodeToSet.scriptRunId is used instead of this.scriptRunId.
SummaryThis PR fixes a bug where a Changes:
Code QualityThe code change is minimal, targeted, and follows existing patterns in the codebase: visitBlockNode(node: BlockNode): AppNode {
if (this.deltaPath.length === 0) {
// When replacing a BlockNode with a TransientNode that has no anchor,
// capture the current BlockNode as the anchor. This preserves the original
// block structure so it can be restored when transient elements are cleared.
// This mirrors the analogous behavior in visitElementNode.
if (this.nodeToSet instanceof TransientNode && !this.nodeToSet.anchor) {
return new TransientNode(
this.nodeToSet.scriptRunId,
node,
this.nodeToSet.transientNodes,
this.nodeToSet.deltaMsgReceivedAt
)
}
return this.nodeToSet
}Minor Observation: There's a subtle inconsistency between
This is unlikely to cause issues in practice since these values should typically be the same when setting a TransientNode, but for consistency, consider using the same approach in both methods. The existing code in if (this.nodeToSet instanceof TransientNode && !this.nodeToSet.anchor) {
return new TransientNode(
this.scriptRunId,
node,
this.nodeToSet.transientNodes,
this.nodeToSet.deltaMsgReceivedAt
)
}Test CoverageUnit Tests (TypeScript):
Both tests properly verify:
E2E Test (Playwright):
The coverage is adequate for this bug fix, testing both the unit-level behavior and the real-world scenario end-to-end. Backwards CompatibilityNo breaking changes. This is a pure bug fix that:
Security & RiskSecurity: No security concerns identified. The change is purely internal to the render tree visitor logic. Risk Assessment: Low risk of regression because:
Recommendations
// In visitBlockNode, change:
this.nodeToSet.scriptRunId
// To:
this.scriptRunIdThis is a minor suggestion and the current implementation should work correctly in practice. VerdictAPPROVED: This is a well-crafted bug fix with appropriate test coverage. The code follows existing patterns, the tests are comprehensive, and there are no backwards compatibility or security concerns. The minor This is an automated AI review using |
…13674) ## Summary - Fixes an issue where a `TransientNode` without an anchor would not properly capture the existing `BlockNode` when replacing it at an empty delta path - This preserves the original block structure so it can be restored when transient elements (like spinners) are cleared - Mirrors the existing behavior in `visitElementNode` ## Test plan - [x] Added unit tests for the new behavior in `SetNodeByDeltaPathVisitor.test.ts` - [x] Added e2e test `test_spinner_with_delayed_container_write` that verifies container content written after a spinner renders appears correctly - [x] All existing unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <[email protected]>
Summary
TransientNodewithout an anchor would not properly capture the existingBlockNodewhen replacing it at an empty delta pathvisitElementNodeTest plan
SetNodeByDeltaPathVisitor.test.tstest_spinner_with_delayed_container_writethat verifies container content written after a spinner renders appears correctly🤖 Generated with Claude Code