diff --git a/webviews/components/header.tsx b/webviews/components/header.tsx index 9e04929ead..b532dd6bf4 100644 --- a/webviews/components/header.tsx +++ b/webviews/components/header.tsx @@ -136,7 +136,10 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr title={url} data-vscode-context={JSON.stringify(context)} onClick={event => { + // The webview host opens any anchor with an href and ignores defaultPrevented, + // so the click must not reach it or a second browser tab is opened. event.preventDefault(); + event.stopPropagation(); void openOnGitHub(); }} > diff --git a/webviews/components/stickyHeader.tsx b/webviews/components/stickyHeader.tsx index dc6500bdda..f9ea2c9fa6 100644 --- a/webviews/components/stickyHeader.tsx +++ b/webviews/components/stickyHeader.tsx @@ -65,7 +65,10 @@ export function StickyHeader({ pr, visible }: { pr: PullRequest; visible: boolea 'github:copyMenu': true, })} onClick={event => { + // The webview host opens any anchor with an href and ignores defaultPrevented, + // so the click must not reach it or a second browser tab is opened. event.preventDefault(); + event.stopPropagation(); void openOnGitHub(); }} > diff --git a/webviews/editorWebview/test/overview.test.tsx b/webviews/editorWebview/test/overview.test.tsx index 6a23cae97d..fe8d9a9c22 100644 --- a/webviews/editorWebview/test/overview.test.tsx +++ b/webviews/editorWebview/test/overview.test.tsx @@ -59,6 +59,45 @@ describe('Overview', function () { assert.strictEqual(openOnGitHub.callCount, 2); }); + it('opens a PR number link exactly once', function () { + const pr = new PullRequestBuilder().build(); + const context = new PRContext(pr); + const openOnGitHub = sinon.stub(context, 'openOnGitHub'); + + // Stands in for the webview host, which opens any anchor with an href that a click + // reaches, and does not check defaultPrevented. + const hostOpenedLinks: string[] = []; + const hostLinkHandler = (event: Event) => { + const anchor = (event.target as HTMLElement).closest('a[href]'); + if (anchor) { + hostOpenedLinks.push(anchor.getAttribute('href')!); + } + }; + window.addEventListener('click', hostLinkHandler); + + try { + const out = render( + + + , + ); + + const numberLinks = out.container.querySelectorAll('.overview-title a, .sticky-header-number'); + assert.strictEqual(numberLinks.length, 2); + numberLinks.forEach(link => { + openOnGitHub.resetHistory(); + hostOpenedLinks.length = 0; + + fireEvent.click(link); + + assert.strictEqual(openOnGitHub.callCount, 1); + assert.deepStrictEqual(hostOpenedLinks, []); + }); + } finally { + window.removeEventListener('click', hostLinkHandler); + } + }); + it('shows view changes in both headers', function () { const pr = new PullRequestBuilder().isAgentSessionsWorkspace(true).build(); const context = new PRContext(pr);