@@ -5,6 +5,7 @@ import { act, createRef, useEffect, useState, type ReactNode } from "react";
55import type { AppBootstrap , DiffFile } from "../../core/types" ;
66import { createTestGitAppBootstrap } from "../../../test/helpers/app-bootstrap" ;
77import { createTestDiffFile as buildTestDiffFile , lines } from "../../../test/helpers/diff-helpers" ;
8+ import { hexColorDistance } from "../lib/color" ;
89import { resolveTheme } from "../themes" ;
910import { measureDiffSectionGeometry } from "../lib/diffSectionGeometry" ;
1011import { buildFileSectionLayouts , buildInStreamFileHeaderHeights } from "../lib/fileSectionLayout" ;
@@ -351,6 +352,44 @@ function frameHasHighlightedMarker(
351352 } ) ;
352353}
353354
355+ /** Convert captured RGBA output back into a #rrggbb color string for contrast assertions. */
356+ function capturedColorToHex ( color : { buffer ?: ArrayLike < number > } | undefined ) {
357+ const buffer = color ?. buffer ;
358+ if ( ! buffer || buffer [ 0 ] == null || buffer [ 1 ] == null || buffer [ 2 ] == null ) {
359+ return null ;
360+ }
361+
362+ const componentToHex = ( value : number ) =>
363+ Math . max ( 0 , Math . min ( 255 , Math . round ( value * 255 ) ) )
364+ . toString ( 16 )
365+ . padStart ( 2 , "0" ) ;
366+
367+ return `#${ componentToHex ( buffer [ 0 ] ) } ${ componentToHex ( buffer [ 1 ] ) } ${ componentToHex ( buffer [ 2 ] ) } ` ;
368+ }
369+
370+ /** Measure the rendered background contrast between one word-diff span and its surrounding line. */
371+ function renderedWordDiffBackgroundDistance (
372+ frame : { lines : Array < { spans : Array < { text : string ; bg ?: { buffer ?: ArrayLike < number > } } > } > } ,
373+ marker : string ,
374+ ) {
375+ for ( const line of frame . lines ) {
376+ const spanIndex = line . spans . findIndex ( ( span ) => span . text . includes ( marker ) ) ;
377+ if ( spanIndex <= 0 ) {
378+ continue ;
379+ }
380+
381+ const wordBg = capturedColorToHex ( line . spans [ spanIndex ] ?. bg ) ;
382+ const surroundingBg = capturedColorToHex ( line . spans [ spanIndex - 1 ] ?. bg ) ;
383+ if ( ! wordBg || ! surroundingBg ) {
384+ continue ;
385+ }
386+
387+ return hexColorDistance ( wordBg , surroundingBg ) ;
388+ }
389+
390+ return null ;
391+ }
392+
354393describe ( "UI components" , ( ) => {
355394 test ( "SidebarPane renders grouped file rows with indented filenames and right-aligned stats" , async ( ) => {
356395 const theme = resolveTheme ( "midnight" , null ) ;
@@ -1901,6 +1940,61 @@ describe("UI components", () => {
19011940 expect ( binaryFileFrame ) . toContain ( "Binary file skipped" ) ;
19021941 } ) ;
19031942
1943+ test ( "PierreDiffView renders word-diff spans with a visibly different background in split view" , async ( ) => {
1944+ const file = createTestDiffFile (
1945+ "word-diff" ,
1946+ "word-diff.ts" ,
1947+ "export const answer = 41;\nexport const stable = true;\n" ,
1948+ "export const answer = 42;\nexport const stable = true;\n" ,
1949+ ) ;
1950+ const theme = resolveTheme ( "graphite" , null ) ;
1951+ const setup = await testRender (
1952+ < PierreDiffView
1953+ file = { file }
1954+ layout = "split"
1955+ theme = { theme }
1956+ width = { 120 }
1957+ selectedHunkIndex = { 0 }
1958+ scrollable = { false }
1959+ /> ,
1960+ { width : 124 , height : 10 } ,
1961+ ) ;
1962+
1963+ try {
1964+ let removedBackgroundDistance : number | null = null ;
1965+ let addedBackgroundDistance : number | null = null ;
1966+
1967+ for ( let iteration = 0 ; iteration < 200 ; iteration += 1 ) {
1968+ await act ( async ( ) => {
1969+ await setup . renderOnce ( ) ;
1970+ await Bun . sleep ( 0 ) ;
1971+ await setup . renderOnce ( ) ;
1972+ await Bun . sleep ( 0 ) ;
1973+ } ) ;
1974+
1975+ const frame = setup . captureSpans ( ) ;
1976+ removedBackgroundDistance = renderedWordDiffBackgroundDistance ( frame , "41" ) ;
1977+ addedBackgroundDistance = renderedWordDiffBackgroundDistance ( frame , "42" ) ;
1978+
1979+ if (
1980+ removedBackgroundDistance !== null &&
1981+ addedBackgroundDistance !== null &&
1982+ removedBackgroundDistance > 0 &&
1983+ addedBackgroundDistance > 0
1984+ ) {
1985+ break ;
1986+ }
1987+ }
1988+
1989+ expect ( removedBackgroundDistance ) . toBeGreaterThanOrEqual ( 28 ) ;
1990+ expect ( addedBackgroundDistance ) . toBeGreaterThanOrEqual ( 28 ) ;
1991+ } finally {
1992+ await act ( async ( ) => {
1993+ setup . renderer . destroy ( ) ;
1994+ } ) ;
1995+ }
1996+ } ) ;
1997+
19041998 test ( "PierreDiffView reuses highlighted rows after unmounting and remounting a file section" , async ( ) => {
19051999 const file = createTestDiffFile (
19062000 "cache" ,
0 commit comments