Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/report/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export function mountReport(
const initialRender = new Promise<void>((resolve) => (finishInitialRender = resolve))
wireSectionFolds()
wireFragments(initialRender)
const mountedDiffs = mountDiffs(data, layout, createFileDiff)
const overflow = initialOverflow()
const mountedDiffs = mountDiffs(data, layout, overflow, createFileDiff)
void mountedDiffs.initialRender.then(finishInitialRender)
const { mounted } = mountedDiffs
wireLayout(mounted)
wireWrap(mounted)
prepareForPrint()
}

Expand All @@ -57,6 +59,12 @@ function initialLayout(): 'split' | 'unified' {
return checked?.value === 'unified' ? 'unified' : 'split'
}

function initialOverflow(): 'scroll' | 'wrap' {
const form = document.querySelector<HTMLFormElement>('[data-wrap-form]')
const checked = form?.querySelector<HTMLInputElement>('input[name="wrap"]')
return checked?.checked ? 'wrap' : 'scroll'
}

function isNarrowViewport(): boolean {
return typeof matchMedia === 'function' && matchMedia('(max-width: 900px)').matches
}
Expand All @@ -81,10 +89,12 @@ function showStepError(mount: ReportDiffMount, error: unknown) {

function baseOptions(
diffStyle: 'split' | 'unified',
overflow: 'scroll' | 'wrap',
onPostRender?: FileDiffOptions<undefined>['onPostRender'],
) {
return {
diffStyle,
overflow,
themeType: 'light',
disableFileHeader: true,
onPostRender,
Expand All @@ -94,6 +104,7 @@ function baseOptions(
function mountDiffs(
data: ReportData,
layout: 'split' | 'unified',
overflow: 'scroll' | 'wrap',
createFileDiff: FileDiffFactory,
): { mounted: MountedDiff[]; initialRender: Promise<void> } {
const mounted: MountedDiff[] = []
Expand Down Expand Up @@ -130,7 +141,7 @@ function mountDiffs(
)
try {
const instance = createFileDiff(
baseOptions(layout, (_node, _instance, phase) => {
baseOptions(layout, overflow, (_node, _instance, phase) => {
if (phase === 'mount') finishRender()
}),
)
Expand Down Expand Up @@ -323,6 +334,18 @@ function wireFragments(initialRender: Promise<void>) {
})
}

function wireWrap(mounted: MountedDiff[]) {
const form = document.querySelector<HTMLFormElement>('[data-wrap-form]')
form?.addEventListener('change', (event) => {
if (!(event.target as Element).matches('input[name="wrap"]')) return
const overflow = (event.target as HTMLInputElement).checked ? 'wrap' : 'scroll'
for (const { fileDiff, instance } of mounted) {
instance.setOptions({ ...instance.options, overflow })
instance.render({ fileDiff, forceRender: true })
}
})
}

function wireLayout(mounted: MountedDiff[]) {
const form = document.querySelector<HTMLFormElement>('[data-layout-form]')
form?.addEventListener('change', (event) => {
Expand Down
20 changes: 13 additions & 7 deletions src/report/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type ReportLayout = 'split' | 'unified'
export interface ReportOptions {
title?: string
layout?: ReportLayout
wrap?: boolean
}

export interface HostedAssets {
Expand Down Expand Up @@ -66,6 +67,7 @@ function renderReportBody(
): ReportBody {
const title = options.title ?? document.title
const layout = options.layout ?? 'split'
const wrap = options.wrap ?? false
const targets = reportTargets(document)
const sections = document.sections.map((section, index) =>
renderSection(section, index, targets[index]!),
Expand All @@ -76,7 +78,10 @@ function renderReportBody(
<label><input type="radio" name="layout" value="unified" ${layout === 'unified' ? 'checked' : ''}> Unified</label>
<button type="submit" hidden aria-hidden="true" tabindex="-1"></button>
</form>`
const readingControls = `<div class="review-controls">${layoutForm}</div>`
const wrapForm = `<form class="wrap-form" data-wrap-form aria-label="Long line handling">
<label><input type="checkbox" name="wrap" ${wrap ? 'checked' : ''}> Wrap long lines</label>
</form>`
const readingControls = `<div class="review-controls">${layoutForm}${wrapForm}</div>`
const reviewMap = renderReviewMap(
document.sections.map((section, index) => ({
title: section.title,
Expand Down Expand Up @@ -391,17 +396,17 @@ body {
gap: 8px;
margin: 0 10px 20px;
}
.layout-form {
.layout-form, .wrap-form {
display: flex;
margin: 0;
border: 1px solid #bdcbbf;
border-radius: 7px;
overflow: hidden;
background: #f3f7f3;
}
.layout-form label { flex: 1; padding: 5px 10px; color: #607066; font-size: 13px; text-align: center; cursor: pointer; }
.layout-form input { display: none; }
.layout-form label:has(input:checked) { color: #ffffff; background: var(--accent); font-weight: 600; }
.layout-form label, .wrap-form label { flex: 1; padding: 5px 10px; color: #607066; font-size: 13px; text-align: center; cursor: pointer; }
.layout-form input, .wrap-form input { display: none; }
.layout-form label:has(input:checked), .wrap-form label:has(input:checked) { color: #ffffff; background: var(--accent); font-weight: 600; }
.fold-all {
padding: 6px 10px;
border: 1px solid #bdcbbf;
Expand Down Expand Up @@ -613,6 +618,7 @@ main { max-width: none; min-width: 0; margin: 0; padding: 22px 28px 72px; }
.review-map-label, .review-map-list, .review-map-counts { display: none; }
.review-controls { display: flex; gap: 6px; justify-content: flex-end; margin: 0; }
.layout-form { flex: 1 1 auto; max-width: 220px; margin: 0 0 0 auto; }
.wrap-form { flex: none; }
.fold-all { flex: none; padding: 5px 9px; font-size: 12px; }
/* The aligned fragment target must clear the sticky strip, so push its
scroll-margin past the strip plus breathing room. */
Expand All @@ -622,12 +628,12 @@ main { max-width: none; min-width: 0; margin: 0; padding: 22px 28px 72px; }
@media (max-width: 520px) {
.report-cover { padding: 18px 16px 8px; }
.report-cover h1 { font-size: 21px; }
.layout-form label { padding: 4px 7px; font-size: 11px; }
.layout-form label, .wrap-form label { padding: 4px 7px; font-size: 11px; }
.fold-all { padding: 4px 7px; font-size: 11px; }
.section-fold > summary { font-size: 17px; }
}
@media print {
.layout-form, .permalink { display: none; }
.layout-form, .wrap-form, .permalink { display: none; }
.review-map { display: none; }
.review-workspace { display: block; }
.report-cover { box-shadow: none; break-inside: avoid; }
Expand Down
40 changes: 40 additions & 0 deletions test/report-dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ function submitLayout(dom: Window, value: 'split' | 'unified') {
)
}

function overflowMode(dom: Window, index: number) {
const shadow = fileDiffShadow(dom, index)
return shadow?.querySelector('pre')?.getAttribute('data-overflow') ?? null
}

function toggleWrap(dom: Window, on: boolean) {
const doc = dom.document as unknown as Document
const input = doc.querySelector<HTMLInputElement>('input[name="wrap"]')
input!.checked = on
input!.dispatchEvent(
new dom.window.Event('change', { bubbles: true, cancelable: true }) as unknown as Event,
)
}

function diffContainers(dom: Window) {
const doc = dom.document as unknown as Document
return [...doc.querySelectorAll('.file-diff diffs-container')]
Expand Down Expand Up @@ -822,6 +836,32 @@ describe('report browser client', () => {
expect(button.getAttribute('aria-expanded')).toBe('true')
})

test(
'wrap toggle flips overflow on the same instances',
async () => {
const html = renderReport(
document([section(simplePatch('one', 'one!'), 'Wrap section')]),
clientBundle,
)
const dom = loadReport(html)

runReportClient()

await waitFor(() => mountedCount(dom) === 1, 90000)
await waitForQuiescent(dom)
expect(overflowMode(dom, 0)).toBe('scroll')

const before = diffContainers(dom)
toggleWrap(dom, true)
await waitFor(() => overflowMode(dom, 0) === 'wrap', 30000)
expectSameContainers(before, diffContainers(dom))

toggleWrap(dom, false)
await waitFor(() => overflowMode(dom, 0) === 'scroll', 30000)
},
120000,
)

test(
'narrow viewport defaults split reports to unified but preserves explicit switching',
async () => {
Expand Down
16 changes: 16 additions & 0 deletions test/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,22 @@ describe('renderReport shell', () => {
expect(html).toContain('value="unified" checked')
})

test('wrap checkbox rides alongside the layout radio and defaults to off', () => {
const html = renderReport(document([section(simplePatch(), 'Wrap')]), stubClient)

expect(html).toContain('data-wrap-form')
expect(html).toContain('name="wrap"')
expect(html).not.toContain('name="wrap" checked')
})

test('wrapped lines are preselected when requested', () => {
const html = renderReport(document([section(simplePatch(), 'Wrap')]), stubClient, {
wrap: true,
})

expect(html).toContain('name="wrap" checked')
})

test('each open section has local fold controls and files start collapsed', () => {
const html = renderReport(document([section(simplePatch(), 'Plain')]), stubClient)

Expand Down