diff --git a/apps/desktop/src/lib/codeSnapshot/__tests__/codeSnapshot.spec.ts b/apps/desktop/src/lib/codeSnapshot/__tests__/codeSnapshot.spec.ts index 2c581fc58d..f6ec8c10a0 100644 --- a/apps/desktop/src/lib/codeSnapshot/__tests__/codeSnapshot.spec.ts +++ b/apps/desktop/src/lib/codeSnapshot/__tests__/codeSnapshot.spec.ts @@ -329,4 +329,46 @@ describe("renderCodeSnapshotHtml", () => { await expect(savePngDataUrlToFile("data:image/png;base64,AA==", "snapshot.png")).rejects.toThrow("disk full"); expect(writeFile).toHaveBeenCalledOnce(); }); + + it("encompasses the last row bottom edge and relaxes clone height for grid snapshots", async () => { + toPng.mockResolvedValue("data:image/png;base64,test"); + const container = document.createElement("div"); + container.className = "dbx-grid-snapshot"; + container.innerHTML = "
row1
row2
"; + document.body.appendChild(container); + + // Mock layout measurements + Object.defineProperty(container, "offsetWidth", { value: 400 }); + Object.defineProperty(container, "offsetHeight", { value: 300 }); + Object.defineProperty(container, "scrollWidth", { value: 400 }); + Object.defineProperty(container, "scrollHeight", { value: 298 }); + container.getBoundingClientRect = () => ({ top: 10, bottom: 310, left: 0, right: 400, width: 400, height: 300 }) as DOMRect; + + const lastRow = container.querySelector("tr:last-child") as HTMLTableRowElement; + lastRow.getBoundingClientRect = () => ({ top: 280, bottom: 318, left: 0, right: 400, width: 400, height: 38 }) as DOMRect; + + try { + await snapshotElementToPng(container); + + // Height should encompass lastRow.bottom (318) - container.top (10) = 308 + expect(toPng).toHaveBeenLastCalledWith( + container, + expect.objectContaining({ + width: 400, + height: 308, + }), + ); + + const options = toPng.mock.calls.at(-1)?.[1]; + expect(typeof options?.onclone).toBe("function"); + + const clone = container.cloneNode(true) as HTMLElement; + clone.style.height = "308px"; + options.onclone(clone); + expect(clone.style.height).toBe("auto"); + expect(clone.style.minHeight).toBe("308px"); + } finally { + container.remove(); + } + }); }); diff --git a/apps/desktop/src/lib/codeSnapshot/codeSnapshot.ts b/apps/desktop/src/lib/codeSnapshot/codeSnapshot.ts index d9a0b64475..23034decb3 100644 --- a/apps/desktop/src/lib/codeSnapshot/codeSnapshot.ts +++ b/apps/desktop/src/lib/codeSnapshot/codeSnapshot.ts @@ -343,8 +343,34 @@ export async function renderCodeSnapshotHtml(source: CodeSnapshotSource, options export async function snapshotElementToPng(element: HTMLElement): Promise { const domtoimage = (await import("dom-to-image-more")).default; // 预览区允许滚动,因此导出尺寸必须覆盖完整内容,而不是只取当前可见宽度。 - const width = Math.max(element.offsetWidth, element.scrollWidth); - const height = Math.max(element.offsetHeight, element.scrollHeight); + const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : null; + let borderTop = 0; + let borderBottom = 0; + if (typeof window !== "undefined" && typeof window.getComputedStyle === "function" && element instanceof (window.Element || Object)) { + try { + const computedStyle = window.getComputedStyle(element); + borderTop = parseFloat(computedStyle.borderTopWidth) || 0; + borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0; + } catch { + // Ignored for non-DOM elements in test runners + } + } + + let width = Math.max(element.offsetWidth || 0, element.scrollWidth || 0, rect ? Math.ceil(rect.width) : 0); + let height = Math.max(element.offsetHeight || 0, element.scrollHeight || 0, (element.scrollHeight || 0) + Math.ceil(borderTop + borderBottom), rect ? Math.ceil(rect.height) : 0); + + if (rect && typeof element.querySelector === "function") { + // If element contains table rows, ensure height fully encloses the bottom edge of the last row + const lastRow = element.querySelector("tr:last-child"); + if (lastRow && typeof lastRow.getBoundingClientRect === "function") { + const rowRect = lastRow.getBoundingClientRect(); + const requiredHeight = Math.ceil(rowRect.bottom - rect.top) + Math.ceil(borderBottom); + if (requiredHeight > height) { + height = requiredHeight; + } + } + } + const basePixels = width * height; const targetScale = Math.min(MAX_EXPORT_PIXEL_RATIO, Math.max(1, typeof window === "undefined" ? 1 : window.devicePixelRatio || 1)); // Keep the output canvas within browser limits without rejecting a larger @@ -352,6 +378,8 @@ export async function snapshotElementToPng(element: HTMLElement): Promise { - if (typeof (clone as ParentNode).querySelectorAll === "function") { + if (useLegacyLineNumberFallback && typeof (clone as ParentNode).querySelectorAll === "function") { materializeSnapshotCloneLineNumbers(clone as ParentNode); } + if (isGridSnapshot && clone && (clone as HTMLElement).style) { + // Prevent rigid fixed height from clipping table rows inside overflow:hidden + (clone as HTMLElement).style.height = "auto"; + (clone as HTMLElement).style.minHeight = `${height}px`; + } } : undefined, style: { diff --git a/apps/desktop/src/lib/gridSnapshot/__tests__/gridSnapshot.spec.ts b/apps/desktop/src/lib/gridSnapshot/__tests__/gridSnapshot.spec.ts index f5ddda1919..476c139c2c 100644 --- a/apps/desktop/src/lib/gridSnapshot/__tests__/gridSnapshot.spec.ts +++ b/apps/desktop/src/lib/gridSnapshot/__tests__/gridSnapshot.spec.ts @@ -126,4 +126,20 @@ describe("grid snapshot rendering", () => { const unstripedHtml = renderGridSnapshotHtml(source, { appearance: "light", stripedRows: false }); expect(unstripedHtml).not.toContain("background:rgb(240, 240, 240)"); }); + + it("defines explicit line-height and safe bottom padding on the last row to prevent clipping", () => { + const source = { + columns: ["id", "val"], + rows: [ + [1, "first"], + [2, "second"], + ], + }; + const standardHtml = renderGridSnapshotHtml(source, { appearance: "light" }); + expect(standardHtml).toContain("line-height: 1.4"); + expect(standardHtml).toContain("padding:4px 12px 6px"); + + const compactHtml = renderGridSnapshotHtml(source, { appearance: "light", compact: true }); + expect(compactHtml).toContain("padding:3px 8px 5px"); + }); }); diff --git a/apps/desktop/src/lib/gridSnapshot/gridSnapshot.ts b/apps/desktop/src/lib/gridSnapshot/gridSnapshot.ts index 8ad1cb4801..83863ee737 100644 --- a/apps/desktop/src/lib/gridSnapshot/gridSnapshot.ts +++ b/apps/desktop/src/lib/gridSnapshot/gridSnapshot.ts @@ -90,14 +90,15 @@ export const GRID_SNAPSHOT_CSS = ` border-radius: 8px; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.4; text-align: left; box-shadow: 0 8px 24px rgba(0, 0, 0, .35); } .dbx-grid-snapshot__bar { display: flex; width: 100%; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--dbx-grid-snapshot-border); } .dbx-grid-snapshot__dot { width: 12px; height: 12px; border-radius: 50%; flex: none; } .dbx-grid-snapshot__title { margin-left: 4px; font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.dbx-grid-snapshot__table { border-collapse: separate; border-spacing: 0; table-layout: auto; width: 100%; } -.dbx-grid-snapshot__cell { border-right: 1px solid var(--dbx-grid-snapshot-border); border-bottom: 1px solid var(--dbx-grid-snapshot-border); padding: 7px 10px; white-space: nowrap; vertical-align: top; } +.dbx-grid-snapshot__table { border-collapse: separate; border-spacing: 0; table-layout: auto; width: 100%; line-height: 1.4; } +.dbx-grid-snapshot__cell { border-right: 1px solid var(--dbx-grid-snapshot-border); border-bottom: 1px solid var(--dbx-grid-snapshot-border); padding: 7px 10px; white-space: nowrap; vertical-align: top; line-height: 1.4; } .dbx-grid-snapshot__cell--wrapped { max-width: 420px; white-space: pre-wrap; overflow-wrap: anywhere; } .dbx-grid-snapshot__cell:last-child { border-right: 0; } .dbx-grid-snapshot__table tbody tr:last-child .dbx-grid-snapshot__cell { border-bottom: 0; } @@ -136,6 +137,7 @@ export function renderGridSnapshotHtml(source: GridSnapshotSource, options: Grid const wrapCells = options.wrapCells === true; const compact = options.compact === true; const cellPadding = compact ? "3px 8px" : "4px 12px"; + const lastRowCellPadding = compact ? "3px 8px 5px" : "4px 12px 6px"; const headerPadding = compact ? "4px 8px" : "6px 8px"; const fontSize = compact ? "12px" : "13px"; const rowNumberHeader = showRowNumbers ? `#` : ""; @@ -151,15 +153,16 @@ export function renderGridSnapshotHtml(source: GridSnapshotSource, options: Grid : ""; const displayRows: GridSnapshotCellValue[][] = transpose ? source.columns.map((_, columnIndex) => source.rows.map((row) => row[columnIndex] ?? null)) : source.rows; const body = displayRows - .map( - (row, rowIndex) => - `${showRowNumbers ? `${rowIndex + 1}` : ""}${transpose && showFieldNames ? renderFieldCell(source.columns[rowIndex] ?? "", rowIndex, "td") : ""}${row - .map((value) => { - const nullClass = value === null ? " dbx-grid-snapshot__cell--null" : ""; - return `${escapeHtml(formatGridSnapshotCell(value))}`; - }) - .join("")}`, - ) + .map((row, rowIndex) => { + const isLastRow = rowIndex === displayRows.length - 1; + const rowPadding = isLastRow ? lastRowCellPadding : cellPadding; + return `${showRowNumbers ? `${rowIndex + 1}` : ""}${transpose && showFieldNames ? renderFieldCell(source.columns[rowIndex] ?? "", rowIndex, "td") : ""}${row + .map((value) => { + const nullClass = value === null ? " dbx-grid-snapshot__cell--null" : ""; + return `${escapeHtml(formatGridSnapshotCell(value))}`; + }) + .join("")}`; + }) .join(""); const bar = options.showTrafficLights !== false || source.title ? renderSnapshotBar(source.title, appearance, options.showTrafficLights !== false) : ""; return `
${bar}${header}${body}
`;