From 346e59c879adfa7c55fd22485a221a42116de1d2 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:10:47 -0700 Subject: [PATCH] fix(renderer): keep commit tooltip lines intact (#14000) --- .../right-sidebar/GitHistoryRow.tsx | 7 +- tests/e2e/git-history-tooltip-wrap.spec.ts | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/git-history-tooltip-wrap.spec.ts diff --git a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx index b236d15533c..f41ea2f92b4 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx @@ -94,7 +94,12 @@ export const GitHistoryRow = React.forwardRef( {item.subject} - + {rowTooltip} diff --git a/tests/e2e/git-history-tooltip-wrap.spec.ts b/tests/e2e/git-history-tooltip-wrap.spec.ts new file mode 100644 index 00000000000..6a8c4f5895b --- /dev/null +++ b/tests/e2e/git-history-tooltip-wrap.spec.ts @@ -0,0 +1,144 @@ +import { execFileSync } from 'node:child_process' +import { rmSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { expect, test } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' +import { openSourceControlForWorktree } from './helpers/worktree-registration' + +const subject = 'brew-install: source shared brew context before resolving prefixes' +const reportedLine = 'Sources bin/lib/brew-context.sh, mirroring the brew-install update:' +const conventionalLine = 'Keep this conventional commit-message body line intact through column 72' +const unbrokenLine = `https://example.com/${'commit-message-segment-'.repeat(8)}` +const commitMessage = `${subject} + +${reportedLine} +${conventionalLine} +${unbrokenLine} +the resolved prefix list now feeds both the cask audit and the formula +path checks, so a missing context aborts before any network call runs. + +Verified by running the full tap audit locally with both prefixes set.` + +function createCommitWorktree(repoPath: string): { branchName: string; worktreePath: string } { + const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}` + const branchName = `e2e-tooltip-wrap-${suffix}` + const worktreePath = path.join(os.tmpdir(), branchName) + execFileSync('git', ['worktree', 'add', worktreePath, '-b', branchName], { + cwd: repoPath, + stdio: 'pipe' + }) + return { branchName, worktreePath } +} + +async function cleanupCommitWorktree( + repoPath: string, + worktreePath: string, + branchName: string +): Promise { + try { + execFileSync('git', ['worktree', 'remove', '--force', worktreePath], { + cwd: repoPath, + stdio: 'pipe' + }) + } catch { + rmSync(worktreePath, { recursive: true, force: true }) + execFileSync('git', ['worktree', 'prune'], { cwd: repoPath, stdio: 'pipe' }) + } + execFileSync('git', ['branch', '-D', branchName], { cwd: repoPath, stdio: 'pipe' }) +} + +test('keeps conventional commit-message lines intact in the history tooltip', async ({ + orcaPage, + testRepoPath, + registerPostElectronShutdownCleanup +}) => { + const fixture = createCommitWorktree(testRepoPath) + registerPostElectronShutdownCleanup(() => + cleanupCommitWorktree(testRepoPath, fixture.worktreePath, fixture.branchName) + ) + execFileSync('git', ['commit', '--allow-empty', '--file', '-'], { + cwd: fixture.worktreePath, + input: commitMessage, + stdio: ['pipe', 'pipe', 'pipe'] + }) + + await orcaPage.setViewportSize({ width: 1440, height: 900 }) + await waitForSessionReady(orcaPage) + await openSourceControlForWorktree(orcaPage, testRepoPath, fixture.worktreePath) + + const commitsToggle = orcaPage.getByRole('button', { name: /Commits/ }) + await expect(commitsToggle).toBeVisible() + await commitsToggle.click() + + const row = orcaPage.getByTestId('git-history-row').filter({ hasText: subject }) + await expect(row).toBeVisible({ timeout: 10_000 }) + const trigger = row.locator('[data-slot="tooltip-trigger"]').filter({ hasText: subject }) + await trigger.hover({ position: { x: 20, y: 10 } }) + await trigger.hover({ position: { x: 40, y: 10 } }) + + const tooltip = orcaPage + .locator('[data-slot="tooltip-content"]') + .filter({ hasText: reportedLine }) + await expect(tooltip).toBeVisible() + await expect(tooltip).toContainText(commitMessage) + await orcaPage.evaluate(async () => { + await document.fonts.ready + }) + + const layout = await tooltip.evaluate( + (element, targetLines) => { + const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT) + let textNode: Text | null = null + while (walker.nextNode()) { + const candidate = walker.currentNode as Text + if (targetLines.every((line) => candidate.data.includes(line))) { + textNode = candidate + break + } + } + if (!textNode) { + throw new Error('Commit message text node was not found in the tooltip') + } + + const visualLineCounts = targetLines.map((targetLine) => { + const start = textNode.data.indexOf(targetLine) + const range = document.createRange() + range.setStart(textNode, start) + range.setEnd(textNode, start + targetLine.length) + const rectTops = Array.from(range.getClientRects()) + .filter((rect) => rect.width > 0.5) + .map((rect) => rect.top) + const visualLineTops = rectTops.reduce((tops, top) => { + if (!tops.some((existing) => Math.abs(existing - top) < 2)) { + tops.push(top) + } + return tops + }, []) + return visualLineTops.length + }) + + const style = getComputedStyle(element) + const tooltipRect = element.getBoundingClientRect() + return { + visualLineCounts, + tooltipLeft: tooltipRect.left, + tooltipRight: tooltipRect.right, + tooltipWidth: tooltipRect.width, + viewportWidth: window.innerWidth, + overflowContained: element.scrollWidth <= element.clientWidth, + textWrap: style.textWrap, + whiteSpace: style.whiteSpace + } + }, + [reportedLine, conventionalLine] + ) + + expect(layout.whiteSpace).toBe('pre-wrap') + expect(layout.tooltipWidth).toBeGreaterThan(0) + expect(layout.tooltipLeft).toBeGreaterThanOrEqual(0) + expect(layout.tooltipRight).toBeLessThanOrEqual(layout.viewportWidth) + expect(layout.visualLineCounts).toEqual([1, 1]) + expect(layout.overflowContained).toBe(true) + expect(layout.textWrap).toBe('wrap') +})