fix(vscode-preview): enable left-click on external links in diagram preview#2908
Conversation
|
@coderabbitai review |
🦋 Changeset detectedLatest commit: 62d7d63 The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 packages
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 |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughFixes VSCode preview external-link handling: left-clicks on external https? links now open via the default browser; clicks originating from interactive elements inside links are not routed to the external opener (but still prevent default navigation); intercepted link clicks call stopPropagation(); the click listener is attached in the capture phase. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Document
participant Handler as interceptExternalLinks
participant Opener as openExternalUrl
participant Browser
User->>Document: left-click on element
Document->>Handler: capture-phase click event
alt target inside interactive control
Handler-->>Document: preventDefault (do not call Opener)
else target not interactive and anchor is external
Handler->>Document: preventDefault & stopPropagation
Handler->>Opener: openExternalUrl(href)
Opener->>Browser: open URL in default browser
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/vscode-preview/src/interceptExternalLinks.spec.ts (1)
91-105: Consider table-driven coverage for all interactive selector cases.This test currently validates only
button. A small parameterized test over[role="button"],input,select, andtextareawould better protect the selector contract.As per coding guidelines, "Aim to cover new features with relevant tests and keep test names descriptive".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vscode-preview/src/interceptExternalLinks.spec.ts` around lines 91 - 105, The test only checks the 'button' case of the interactive selector and should be converted into a table-driven/parameterized test to cover '[role="button"]', 'input', 'select', and 'textarea' as well; update the spec that calls handleExternalLinkClick(mockEvent(target), openUrl) to iterate over an array of selector-tag pairs (or selector values) and for each stub mockEvent().closest to return the appropriate element for the interactive selector and an anchor for 'a[href]', then assert openUrl was not called for every case using the same mock openUrl; reference the existing test helpers mockEvent, openUrl and the function under test handleExternalLinkClick and reuse the selector string 'button, [role="button"], input, select, textarea' to drive the cases.
🤖 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/vscode-preview/src/interceptExternalLinks.spec.ts`:
- Around line 91-105: The test only checks the 'button' case of the interactive
selector and should be converted into a table-driven/parameterized test to cover
'[role="button"]', 'input', 'select', and 'textarea' as well; update the spec
that calls handleExternalLinkClick(mockEvent(target), openUrl) to iterate over
an array of selector-tag pairs (or selector values) and for each stub
mockEvent().closest to return the appropriate element for the interactive
selector and an anchor for 'a[href]', then assert openUrl was not called for
every case using the same mock openUrl; reference the existing test helpers
mockEvent, openUrl and the function under test handleExternalLinkClick and reuse
the selector string 'button, [role="button"], input, select, textarea' to drive
the cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f7c0bf62-290d-4bb8-9da9-4ee21b031db0
📒 Files selected for processing (3)
.changeset/fix-left-click-links-vscode.mdpackages/vscode-preview/src/interceptExternalLinks.spec.tspackages/vscode-preview/src/interceptExternalLinks.ts
|
@coderabbitai review |
204f129 to
b5898e2
Compare
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/vscode-preview/src/interceptExternalLinks.spec.ts (2)
91-111: Make the interactive-element parameterized test validate each selector independently.At Line 100, the mock currently keys off
selector.includes('button') && selector.includes('input'), so eachit.eachcase effectively exercises the same branch. This can miss regressions where one selector is dropped but others remain.Suggested test-mock refinement
it.each([ 'button', '[role="button"]', 'input', 'select', 'textarea', ])('skips interactive element inside link: %s', (tag) => { const target = { closest: vi.fn((selector: string) => { - if (selector.includes('button') && selector.includes('input')) { + const selectors = selector.split(',').map(s => s.trim()) + if (selectors.includes(tag)) { return { tagName: tag.toUpperCase() } } if (selector === 'a[href]') { return { getAttribute: () => 'https://example.com' } } return null }), }As per coding guidelines: "Aim to cover new features with relevant tests and keep test names descriptive."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vscode-preview/src/interceptExternalLinks.spec.ts` around lines 91 - 111, The test's mock for Element.closest always triggers the same branch because it checks for selector.includes('button') && selector.includes('input'), so each it.each case isn't validated independently; update the mock in the 'skips interactive element inside link' test so closest examines the exact selector argument (e.g., compare selector === expectedSelector or check selector.includes(tag) mapped to tag names) and returns an element with tagName: tag.toUpperCase() only when the selector matches that specific interactive selector, otherwise return the anchor for 'a[href]' or null; this ensures handleExternalLinkClick (and the mockEvent/openUrl expectations) are exercised per selector case rather than all hitting one branch.
151-153: Tighten listener wiring assertions to catch extra registrations/removals.Using
toContainEqualverifies presence but still passes if unintended extra add/remove calls are introduced. Prefer exact assertions for add/remove subsets.Suggested assertion tightening
- expect(calls).toContainEqual({ type: 'click', capture: true, action: 'add' }) - expect(calls).toContainEqual({ type: 'auxclick', capture: false, action: 'add' }) + expect(calls.filter(c => c.action === 'add')).toEqual([ + { type: 'click', capture: true, action: 'add' }, + { type: 'auxclick', capture: false, action: 'add' }, + ]) @@ - expect(calls).toContainEqual({ type: 'click', capture: true, action: 'remove' }) - expect(calls).toContainEqual({ type: 'auxclick', capture: false, action: 'remove' }) + expect(calls.filter(c => c.action === 'remove')).toEqual([ + { type: 'click', capture: true, action: 'remove' }, + { type: 'auxclick', capture: false, action: 'remove' }, + ])Also applies to: 166-167
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vscode-preview/src/interceptExternalLinks.spec.ts` around lines 151 - 153, The current tests use toContainEqual on the `calls` array (e.g., the expectations checking { type: 'click', capture: true, action: 'add' } and { type: 'auxclick', capture: false, action: 'add' }) which only asserts presence and misses extra unwanted add/remove registrations; change these to assert exact subsets by filtering `calls` by `action` (e.g., const adds = calls.filter(c => c.action === 'add')) and then assert the exact array equality with toEqual on the expected add objects, and do the same for removes (and update the other occurrences around the second pair of assertions at the similar spot referenced in the comment), ensuring no extra registrations or removals are present.
🤖 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/vscode-preview/src/interceptExternalLinks.spec.ts`:
- Around line 91-111: The test's mock for Element.closest always triggers the
same branch because it checks for selector.includes('button') &&
selector.includes('input'), so each it.each case isn't validated independently;
update the mock in the 'skips interactive element inside link' test so closest
examines the exact selector argument (e.g., compare selector ===
expectedSelector or check selector.includes(tag) mapped to tag names) and
returns an element with tagName: tag.toUpperCase() only when the selector
matches that specific interactive selector, otherwise return the anchor for
'a[href]' or null; this ensures handleExternalLinkClick (and the
mockEvent/openUrl expectations) are exercised per selector case rather than all
hitting one branch.
- Around line 151-153: The current tests use toContainEqual on the `calls` array
(e.g., the expectations checking { type: 'click', capture: true, action: 'add' }
and { type: 'auxclick', capture: false, action: 'add' }) which only asserts
presence and misses extra unwanted add/remove registrations; change these to
assert exact subsets by filtering `calls` by `action` (e.g., const adds =
calls.filter(c => c.action === 'add')) and then assert the exact array equality
with toEqual on the expected add objects, and do the same for removes (and
update the other occurrences around the second pair of assertions at the similar
spot referenced in the comment), ensuring no extra registrations or removals are
present.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 20f71901-74ce-4d4f-8658-38d4247a72ad
📒 Files selected for processing (3)
.changeset/fix-left-click-links-vscode.mdpackages/vscode-preview/src/interceptExternalLinks.spec.tspackages/vscode-preview/src/interceptExternalLinks.ts
✅ Files skipped from review due to trivial changes (1)
- .changeset/fix-left-click-links-vscode.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/vscode-preview/src/interceptExternalLinks.ts
…review Use capture phase for the click event listener so it fires before React's event system can stopPropagation(). Without capture, left-clicks on links inside element details overlays never reach the document-level handler. Also adds an interactive element guard (button, input, etc.) so clicking a copy button inside a link badge doesn't trigger URL navigation. Ref: #2422 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
b5898e2 to
62d7d63
Compare
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/vscode-preview/src/interceptExternalLinks.spec.ts (2)
135-143: RedundantstopPropagationoverride.Since
mockEventnow already providesstopPropagation: vi.fn()(line 11), the spread-and-override here is unnecessary — you can simplify to justconst e = mockEvent(mockAnchor('https://example.com'))and assert one.stopPropagationdirectly.♻️ Proposed simplification
it('also calls stopPropagation on intercepted links', () => { - const e = { - ...mockEvent(mockAnchor('https://example.com')), - stopPropagation: vi.fn(), - } as unknown as MouseEvent + const e = mockEvent(mockAnchor('https://example.com')) handleExternalLinkClick(e, openUrl) expect(e.stopPropagation).toHaveBeenCalled() expect(openUrl).toHaveBeenCalledWith('https://example.com') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vscode-preview/src/interceptExternalLinks.spec.ts` around lines 135 - 143, The test redundantly overrides stopPropagation when mockEvent already supplies it; update the test in interceptExternalLinks.spec.ts by replacing the spread override with a direct call to mockEvent: set const e = mockEvent(mockAnchor('https://example.com')) (cast to MouseEvent if necessary), then call handleExternalLinkClick(e, openUrl) and keep the assertions expect(e.stopPropagation).toHaveBeenCalled() and expect(openUrl).toHaveBeenCalledWith('https://example.com'); remove the {..., stopPropagation: vi.fn()} spread override.
97-116: Interactive-selector matching is overly loose forbutton.The predicate
selectors.some(s => s === tag || s.includes(tag))causes thebuttoncase to also match when iterating over a selector list containing[role="button"](since the substring"button"is present). The test still passes because both branches lead toreturn { tagName: ... }and the assertions only check thatopenUrlwasn't called, but the mock's behavior no longer faithfully represents whatclosest(interactiveSelector)would return for a plain<button>vs. a[role="button"]. Consider matching only whens === tagto keep the mock precise:♻️ Proposed tightening
- const selectors = selector.split(',').map(s => s.trim()) - if (selectors.some(s => s === tag || s.includes(tag))) { - return { tagName: tag.toUpperCase() } - } + const selectors = selector.split(',').map(s => s.trim()) + if (selectors.includes(tag)) { + return { tagName: tag.replace(/\W/g, '').toUpperCase() } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vscode-preview/src/interceptExternalLinks.spec.ts` around lines 97 - 116, The test's mock for Element.closest is too permissive for the 'button' case: inside the closest callback in interceptExternalLinks.spec.ts (the mock tied to mockEvent/target used by handleExternalLinkClick and openUrl) change the selector-match predicate so it only matches when a selector exactly equals the tag (i.e., use strict equality rather than substring includes) so that `[role="button"]` does not incorrectly match 'button'; keep the rest of the mock (returning tagName or the 'a[href]' branch) and leave assertions unchanged.
🤖 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/vscode-preview/src/interceptExternalLinks.spec.ts`:
- Around line 135-143: The test redundantly overrides stopPropagation when
mockEvent already supplies it; update the test in interceptExternalLinks.spec.ts
by replacing the spread override with a direct call to mockEvent: set const e =
mockEvent(mockAnchor('https://example.com')) (cast to MouseEvent if necessary),
then call handleExternalLinkClick(e, openUrl) and keep the assertions
expect(e.stopPropagation).toHaveBeenCalled() and
expect(openUrl).toHaveBeenCalledWith('https://example.com'); remove the {...,
stopPropagation: vi.fn()} spread override.
- Around line 97-116: The test's mock for Element.closest is too permissive for
the 'button' case: inside the closest callback in interceptExternalLinks.spec.ts
(the mock tied to mockEvent/target used by handleExternalLinkClick and openUrl)
change the selector-match predicate so it only matches when a selector exactly
equals the tag (i.e., use strict equality rather than substring includes) so
that `[role="button"]` does not incorrectly match 'button'; keep the rest of the
mock (returning tagName or the 'a[href]' branch) and leave assertions unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fe456b6f-bba8-4d30-be2e-352f5989ef34
📒 Files selected for processing (3)
.changeset/fix-left-click-links-vscode.mdpackages/vscode-preview/src/interceptExternalLinks.spec.tspackages/vscode-preview/src/interceptExternalLinks.ts
✅ Files skipped from review due to trivial changes (1)
- .changeset/fix-left-click-links-vscode.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/vscode-preview/src/interceptExternalLinks.ts
Summary
Fix left-click on external links in the VSCode extension diagram preview. Previously, only right-click opened links in the browser. Now left-click works too.
Ref: #2422 (comment)
Root Cause
The click event listener was registered in the bubble phase (default). React 18+ attaches event listeners to the React root node, and diagram overlay components call
stopPropagation()on click events. This prevents the native DOM event from reaching the document-level link interceptor.Right-click (
auxclick) wasn't affected because React doesn't handle non-primary button clicks.Fix
Capture phase for the
clicklistener (addEventListener('click', handler, true)). This fires before React's event system, ensuring the interceptor always sees clicks on links.Interactive element guard — skips clicks on
<button>,[role="button"],<input>,<select>,<textarea>inside links. Prevents the copy button in link badges from triggering URL navigation.Defense-in-depth
preventDefault()— when an interactive element is inside an external<a href>, the interceptor callspreventDefault()to suppress anchor navigation even though it doesn't open the URL. This prevents the VSCode webview blank-page bug regardless of whether the interactive element's own handler callspreventDefault(). The interceptor owns "no blank-page navigation" — components shouldn't need webview-awareness.stopPropagation()after intercepting — prevents the captured click from also triggering diagram interactions (node selection, panning).Files changed
packages/vscode-preview/src/interceptExternalLinks.tspackages/vscode-preview/src/interceptExternalLinks.spec.tsTest plan
test/vscode-combinedbranch, built viapnpm package-vsix, installed in VSCode, opened monaco_aml_lidar project)Manual testing instructions
🤖 Generated with Claude Code