Skip to content

Fix TransientNode not capturing BlockNode as anchor when replacing#13674

Merged
kmcgrady merged 2 commits intodevelopfrom
fix/transient-replacing-block-node
Jan 22, 2026
Merged

Fix TransientNode not capturing BlockNode as anchor when replacing#13674
kmcgrady merged 2 commits intodevelopfrom
fix/transient-replacing-block-node

Conversation

@kmcgrady
Copy link
Copy Markdown
Collaborator

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

  • Added unit tests for the new behavior in SetNodeByDeltaPathVisitor.test.ts
  • Added e2e test test_spinner_with_delayed_container_write that verifies container content written after a spinner renders appears correctly
  • All existing unit tests pass

🤖 Generated with Claude Code

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]>
Copilot AI review requested due to automatic review settings January 22, 2026 17:14
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 22, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 22, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13674/streamlit-1.53.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13674.streamlit.app (☁️ Deploy here if not accessible)

@kmcgrady kmcgrady added security-assessment-completed change:bugfix PR contains bug fix implementation impact:users PR changes affect end users labels Jan 22, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 visitBlockNode to wrap the existing BlockNode as an anchor when replaced by an anchorless TransientNode
  • Mirrors the existing pattern in visitElementNode for 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

Comment on lines +92 to +97
return new TransientNode(
this.nodeToSet.scriptRunId,
node,
this.nodeToSet.transientNodes,
this.nodeToSet.deltaMsgReceivedAt
)
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +253
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)
})
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@kmcgrady kmcgrady added the ai-review If applied to PR or issue will run AI review workflow label Jan 22, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Jan 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR fixes a bug where a TransientNode (e.g., spinner) without an anchor would not properly capture the existing BlockNode when replacing it at an empty delta path. The fix preserves the original block structure so it can be restored when transient elements are cleared.

Changes:

  • SetNodeByDeltaPathVisitor.ts: Added 13 lines to visitBlockNode that mirror the existing behavior in visitElementNode - when replacing with a TransientNode that has no anchor, capture the current BlockNode as the anchor.
  • SetNodeByDeltaPathVisitor.test.ts: Added 2 unit tests covering positive and negative cases.
  • st_spinner.py: Added test scenario for the regression.
  • st_spinner_test.py: Added e2e test test_spinner_with_delayed_container_write.

Code Quality

The 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 visitBlockNode (new fix) and visitElementNode (existing code) in which scriptRunId is used:

  • visitElementNode uses this.scriptRunId (from the visitor)
  • visitBlockNode uses this.nodeToSet.scriptRunId (from the TransientNode)

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 visitElementNode (lines 49-56) uses this.scriptRunId:

    if (this.nodeToSet instanceof TransientNode && !this.nodeToSet.anchor) {
      return new TransientNode(
        this.scriptRunId,
        node,
        this.nodeToSet.transientNodes,
        this.nodeToSet.deltaMsgReceivedAt
      )
    }

Test Coverage

Unit Tests (TypeScript):
The two new unit tests are well-structured and follow best practices:

  1. wraps block node as anchor when setting TransientNode without anchor - Tests the positive case where the BlockNode should be captured as anchor.
  2. does not modify TransientNode when it already has an anchor - Tests the negative case where an existing anchor should be preserved.

Both tests properly verify:

  • The result is a TransientNode instance
  • The anchor is correctly set or preserved
  • Other properties (transientNodes, deltaMsgReceivedAt) are preserved

E2E Test (Playwright):
The e2e test test_spinner_with_delayed_container_write follows best practices:

  • Uses expect for auto-wait assertions
  • Uses get_button helper from shared app_utils
  • Uses stable locators (get_by_test_id, get_by_text)
  • Includes a clear docstring explaining the test purpose
  • Has a negative assertion (to_have_count(0)) verifying the spinner disappears
  • Uses wait_for_app_run for proper synchronization

The coverage is adequate for this bug fix, testing both the unit-level behavior and the real-world scenario end-to-end.

Backwards Compatibility

No breaking changes. This is a pure bug fix that:

  • Only affects the specific edge case where a TransientNode without an anchor replaces a BlockNode
  • Is additive and follows existing patterns
  • Does not change any public APIs

Security & Risk

Security: No security concerns identified. The change is purely internal to the render tree visitor logic.

Risk Assessment: Low risk of regression because:

  • The fix is minimal and well-targeted
  • It mirrors existing, proven behavior in visitElementNode
  • The e2e test covers the specific regression scenario
  • The unit tests validate the boundary conditions

Recommendations

  1. Optional Consistency Fix: Consider aligning the scriptRunId usage between visitBlockNode and visitElementNode for consistency. Both should probably use this.scriptRunId (the visitor's scriptRunId) to match the existing pattern:
// In visitBlockNode, change:
this.nodeToSet.scriptRunId
// To:
this.scriptRunId

This is a minor suggestion and the current implementation should work correctly in practice.

Verdict

APPROVED: 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 scriptRunId inconsistency noted above is unlikely to cause issues in practice.


This is an automated AI review using opus-4.5-thinking. Please verify the feedback and use your judgment.

Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@kmcgrady kmcgrady merged commit 661cf4f into develop Jan 22, 2026
43 checks passed
@kmcgrady kmcgrady deleted the fix/transient-replacing-block-node branch January 22, 2026 18:30
github-actions bot pushed a commit that referenced this pull request Jan 22, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants