diff --git a/.changeset/terminal-inline-image-unavailable-dedupe.md b/.changeset/terminal-inline-image-unavailable-dedupe.md new file mode 100644 index 00000000..bef687f3 --- /dev/null +++ b/.changeset/terminal-inline-image-unavailable-dedupe.md @@ -0,0 +1,14 @@ +--- +"@prover-coder-ai/docker-git-terminal": patch +--- + +Stop rendering "unavailable" placeholder boxes for inline image paths that +cannot be fetched, and render each image preview at most once per terminal +session. + +Failed image fetches previously produced an "unavailable" placeholder plus +four spacer rows, and every re-mention or redraw of the same path rendered +another preview. Unavailable paths are now skipped entirely (the clickable +path link remains), rendered paths are tracked per session so duplicates are +suppressed, and the leading line break before previews is written lazily so +fully skipped segments leave the terminal output untouched. diff --git a/packages/terminal/src/web/terminal-inline-images-core.ts b/packages/terminal/src/web/terminal-inline-images-core.ts index 39b76f05..56cf9139 100644 --- a/packages/terminal/src/web/terminal-inline-images-core.ts +++ b/packages/terminal/src/web/terminal-inline-images-core.ts @@ -1,4 +1,5 @@ import { detectTerminalImagePaths } from "./terminal-image-paths.js" +import type { TerminalInlineImageEntry } from "./terminal-inline-images.js" export type TerminalInlineImageOutputSegment = { readonly endedWithLineBreak: boolean @@ -9,8 +10,7 @@ export type TerminalInlineImageOutputSegment = { export type TerminalInlineImagePreviewsEnabledRef = { readonly current: boolean } export type TerminalOutputSegmentWriter = { - readonly writePreviewLineBreak: (segment: TerminalInlineImageOutputSegment, onComplete: () => void) => void - readonly writePreviews: (paths: ReadonlyArray, onComplete: () => void) => void + readonly writePreviews: (segment: TerminalInlineImageOutputSegment, onComplete: () => void) => void readonly writeText: (text: string, onComplete: () => void) => void } @@ -61,12 +61,11 @@ export const splitTerminalInlineImageOutput = ( * effects belong to the writer implementation. * * @pure false - invokes effectful writer callbacks. - * @effect writer callbacks: writeText, writePreviewLineBreak, writePreviews. + * @effect writer callbacks: writeText, writePreviews. * @precondition segment is the next queued terminal output segment and * onComplete belongs to the caller's active output queue drain. * @postcondition writeText is requested exactly once; when previews are enabled - * and imagePaths is non-empty, the preview line break and preview writes are - * requested in order before onComplete. + * and imagePaths is non-empty, the preview write is requested before onComplete. * @invariant segment.text is emitted before any preview callback, and preview * callbacks never run when imagePaths is empty or previews are disabled. * @complexity O(1) plus writer callback complexity; image paths are forwarded @@ -83,8 +82,92 @@ export const writeTerminalOutputSegment = ( onComplete() return } - writer.writePreviewLineBreak(segment, () => { - writer.writePreviews(segment.imagePaths, onComplete) - }) + writer.writePreviews(segment, onComplete) }) } + +export type TerminalInlineImagePreviewWriter = { + readonly loadEntry: (path: string, onComplete: (entry: TerminalInlineImageEntry | null) => void) => void + readonly renderPreview: (entry: TerminalInlineImageEntry, onComplete: () => void) => void + readonly writeLineBreak: (onComplete: () => void) => void +} + +export type TerminalInlineImagePreviewWriteArgs = { + readonly needsLeadingLineBreak: boolean + readonly paths: ReadonlyArray + readonly renderedPaths: Set + readonly writer: TerminalInlineImagePreviewWriter +} + +/** + * Sequences inline image preview writes for the image paths of one segment. + * + * Skips paths whose preview was already rendered in this terminal session and + * paths whose entry loads as `null` (unavailable image), so unavailable links + * never produce a placeholder and the same image is rendered at most once. + * The leading line break is written lazily before the first rendered preview + * only, so segments whose previews are all skipped leave the output untouched. + * + * @param args - Preview paths, session-rendered path set, and effectful writer callbacks. + * @param onComplete - Invoked exactly once after every path is processed. + * @returns Nothing; results are delivered through the writer callbacks. + * @pure false - invokes effectful writer callbacks and mutates renderedPaths. + * @effect writer callbacks: loadEntry, renderPreview, writeLineBreak. + * @precondition paths contains no duplicates (segment paths are deduplicated at detection). + * @postcondition ∀ path ∈ paths: rendered(path) → path ∈ renderedPaths, and + * renderPreview runs only for freshly loaded, previously unrendered entries. + * @invariant renderPreview is never invoked for a null entry or a path already + * in renderedPaths; writeLineBreak runs at most once and only before the first render. + * @complexity O(n) where n = |paths|. + * @throws Through writer callbacks or onComplete only. + */ +// CHANGE: skip unavailable inline images and deduplicate previews per terminal session +// WHY: rendering placeholders for unresolvable paths and repeating previews for the +// same image adds noise to terminal output without conveying information +// QUOTE(ТЗ): "не пытаться рендерить unavailable ссылки + сделать что бы одна и таже +// картинка не рендерилась несколько раз" +// REF: issue-445 +// SOURCE: n/a +// FORMAT THEOREM: ∀ path: renders(path) ≤ 1 ∧ (entry(path) = null → renders(path) = 0) +// PURITY: CORE sequencing over injected SHELL callbacks +// EFFECT: mutates renderedPaths and drives writer callbacks +// INVARIANT: onComplete fires exactly once after all paths are processed in input order +// COMPLEXITY: O(n) where n = |paths| +export const writeTerminalInlineImagePreviews = ( + { needsLeadingLineBreak, paths, renderedPaths, writer }: TerminalInlineImagePreviewWriteArgs, + onComplete: () => void +): void => { + let lineBreakPending = needsLeadingLineBreak + let index = 0 + const renderNext = (entry: TerminalInlineImageEntry, onRendered: () => void): void => { + if (!lineBreakPending) { + writer.renderPreview(entry, onRendered) + return + } + lineBreakPending = false + writer.writeLineBreak(() => { + writer.renderPreview(entry, onRendered) + }) + } + const writeNext = (): void => { + const path = paths[index] + if (path === undefined) { + onComplete() + return + } + index += 1 + if (renderedPaths.has(path)) { + writeNext() + return + } + writer.loadEntry(path, (entry) => { + if (entry === null) { + writeNext() + return + } + renderedPaths.add(path) + renderNext(entry, writeNext) + }) + } + writeNext() +} diff --git a/packages/terminal/src/web/terminal-inline-images.ts b/packages/terminal/src/web/terminal-inline-images.ts index 48c5d245..777e3a34 100644 --- a/packages/terminal/src/web/terminal-inline-images.ts +++ b/packages/terminal/src/web/terminal-inline-images.ts @@ -14,18 +14,12 @@ const terminalInlineImagePreviewColumns = 16 const terminalInlineImagePreviewHeightPx = 56 const terminalInlineImagePreviewWidthPx = 96 -export type TerminalInlineImageEntry = - | { - readonly _tag: "AvailableTerminalInlineImage" - readonly displayUrl: string - readonly fetchUrl: string - readonly path: string - } - | { - readonly _tag: "UnavailableTerminalInlineImage" - readonly fetchUrl: string - readonly path: string - } +export type TerminalInlineImageEntry = { + readonly _tag: "AvailableTerminalInlineImage" + readonly displayUrl: string + readonly fetchUrl: string + readonly path: string +} type TerminalInlineImageObjectUrlCache = Map @@ -40,15 +34,6 @@ const availableTerminalInlineImageEntry = ( path }) -export const unavailableTerminalInlineImageEntry = ( - path: string, - fetchUrl: string -): TerminalInlineImageEntry => ({ - _tag: "UnavailableTerminalInlineImage", - fetchUrl, - path -}) - export const cachedTerminalInlineImageEntry = ( cache: TerminalInlineImageObjectUrlCache, path: string, @@ -101,18 +86,12 @@ export const revokeTerminalInlineImageObjectUrlCache = ( cache.clear() } -const terminalInlineImageLinkUrl = (entry: TerminalInlineImageEntry): string => - entry._tag === "AvailableTerminalInlineImage" ? entry.displayUrl : entry.fetchUrl - -const terminalInlineImageTitle = (entry: TerminalInlineImageEntry): string => - entry._tag === "AvailableTerminalInlineImage" ? entry.path : `${entry.path} unavailable` - const createTerminalInlineImageLink = (entry: TerminalInlineImageEntry): HTMLAnchorElement => { const link = document.createElement("a") - link.href = terminalInlineImageLinkUrl(entry) + link.href = entry.displayUrl link.rel = "noreferrer" link.target = "_blank" - link.title = terminalInlineImageTitle(entry) + link.title = entry.path link.style.alignItems = "center" link.style.background = "#0d1218" link.style.border = "1px solid #3a4652" @@ -129,9 +108,9 @@ const createTerminalInlineImageLink = (entry: TerminalInlineImageEntry): HTMLAnc return link } -const appendAvailableTerminalInlineImage = ( +const appendTerminalInlineImageContent = ( link: HTMLAnchorElement, - entry: Extract + entry: TerminalInlineImageEntry ): void => { const image = document.createElement("img") image.alt = entry.path @@ -144,30 +123,6 @@ const appendAvailableTerminalInlineImage = ( link.append(image) } -const appendUnavailableTerminalInlineImage = (link: HTMLAnchorElement): void => { - const label = document.createElement("span") - label.textContent = "unavailable" - label.style.color = "#9aa8b6" - label.style.fontFamily = "'IBM Plex Mono', ui-monospace, monospace" - label.style.fontSize = "11px" - label.style.lineHeight = "1" - label.style.overflow = "hidden" - label.style.textOverflow = "ellipsis" - label.style.whiteSpace = "nowrap" - link.append(label) -} - -const appendTerminalInlineImageContent = ( - link: HTMLAnchorElement, - entry: TerminalInlineImageEntry -): void => { - if (entry._tag === "AvailableTerminalInlineImage") { - appendAvailableTerminalInlineImage(link, entry) - return - } - appendUnavailableTerminalInlineImage(link) -} - const openImage = (fetchUrl: string): void => { const imageWindow = window.open(fetchUrl, "_blank", "noopener,noreferrer") if (imageWindow === null) { @@ -191,7 +146,7 @@ const renderInlineImageElement = ( element: HTMLElement, entry: TerminalInlineImageEntry ): void => { - if (element.dataset["path"] === entry.path && element.dataset["tag"] === entry._tag) { + if (element.dataset["path"] === entry.path) { return } @@ -199,7 +154,6 @@ const renderInlineImageElement = ( appendTerminalInlineImageContent(link, entry) element.dataset["path"] = entry.path - element.dataset["tag"] = entry._tag element.style.pointerEvents = "none" element.replaceChildren(link) } diff --git a/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts b/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts index 8677d49e..1c4c9e9b 100644 --- a/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts +++ b/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts @@ -32,6 +32,7 @@ export const cleanupTerminalResources = ( } args.lifecycle.inlineImageDisposables = [] revokeTerminalInlineImageObjectUrlCache(args.lifecycle.inlineImageObjectUrls) + args.lifecycle.inlineImageRenderedPaths.clear() args.lifecycle.outputQueue = [] args.lifecycle.outputWriting = false args.removeImageLinks() diff --git a/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts b/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts index 7a464bb7..a8a3b6ae 100644 --- a/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts +++ b/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts @@ -6,14 +6,14 @@ import { resolveTerminalImageFetchUrl } from "./terminal-image-url.js" import { splitTerminalInlineImageOutput, type TerminalInlineImageOutputSegment, + writeTerminalInlineImagePreviews, writeTerminalOutputSegment } from "./terminal-inline-images-core.js" import { cachedTerminalInlineImageEntry, cacheTerminalInlineImageBlob, didAppendTerminalInlineImagePreview, - terminalInlineImageSpacer, - unavailableTerminalInlineImageEntry + terminalInlineImageSpacer } from "./terminal-inline-images.js" import type { TerminalInlineImageEntry } from "./terminal-inline-images.js" import type { TerminalMessageHandlers } from "./terminal-panel-runtime-types.js" @@ -36,14 +36,6 @@ const emptyTerminalInlineImageBufferState: TerminalInlineImageBufferState = { size: 0 } -const terminalImageEntry = ( - handlers: TerminalMessageHandlers, - path: string -): TerminalInlineImageEntry | null => { - const fetchUrl = resolveTerminalImageFetchUrl(handlers.session.websocketPath, path) - return cachedTerminalInlineImageEntry(handlers.lifecycle.inlineImageObjectUrls, path, fetchUrl) -} - const terminalInlineImageFetchError = (message: string): TerminalInlineImageFetchError => ({ _tag: "TerminalInlineImageFetchError", message @@ -136,7 +128,7 @@ const fetchTerminalInlineImageBlob = ( const loadTerminalImageEntry = ( handlers: TerminalMessageHandlers, path: string, - onComplete: (entry: TerminalInlineImageEntry) => void + onComplete: (entry: TerminalInlineImageEntry | null) => void ): void => { const fetchUrl = resolveTerminalImageFetchUrl(handlers.session.websocketPath, path) const cached = cachedTerminalInlineImageEntry(handlers.lifecycle.inlineImageObjectUrls, path, fetchUrl) @@ -147,7 +139,7 @@ const loadTerminalImageEntry = ( Effect.runFork( fetchTerminalInlineImageBlob(fetchUrl).pipe( Effect.match({ - onFailure: () => unavailableTerminalInlineImageEntry(path, fetchUrl), + onFailure: () => null, onSuccess: (blob) => handlers.lifecycle.disposed ? null @@ -155,7 +147,7 @@ const loadTerminalImageEntry = ( }), Effect.flatMap((entry) => Effect.sync(() => { - if (entry === null || handlers.lifecycle.disposed) { + if (handlers.lifecycle.disposed) { return } onComplete(entry) @@ -172,21 +164,6 @@ const writePreviewSpacer = ( handlers.terminal.write(terminalInlineImageSpacer, onComplete) } -const writeInlineImagePreview = ( - handlers: TerminalMessageHandlers, - path: string, - onComplete: () => void -): void => { - const cached = terminalImageEntry(handlers, path) - if (cached !== null) { - writeInlineImagePreviewEntry(handlers, cached, onComplete) - return - } - loadTerminalImageEntry(handlers, path, (entry) => { - writeInlineImagePreviewEntry(handlers, entry, onComplete) - }) -} - const writeInlineImagePreviewEntry = ( handlers: TerminalMessageHandlers, entry: TerminalInlineImageEntry, @@ -205,33 +182,26 @@ const writeInlineImagePreviewEntry = ( } const writeInlineImagePreviews = ( - handlers: TerminalMessageHandlers, - paths: ReadonlyArray, - onComplete: () => void -): void => { - let index = 0 - const writeNext = (): void => { - const path = paths[index] - if (path === undefined) { - onComplete() - return - } - index += 1 - writeInlineImagePreview(handlers, path, writeNext) - } - writeNext() -} - -const writeLineBreakBeforePreview = ( handlers: TerminalMessageHandlers, segment: TerminalInlineImageOutputSegment, onComplete: () => void ): void => { - if (segment.endedWithLineBreak) { - onComplete() - return - } - handlers.terminal.write("\r\n", onComplete) + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: !segment.endedWithLineBreak, + paths: segment.imagePaths, + renderedPaths: handlers.lifecycle.inlineImageRenderedPaths, + writer: { + loadEntry: (path, onEntry) => { + loadTerminalImageEntry(handlers, path, onEntry) + }, + renderPreview: (entry, onRendered) => { + writeInlineImagePreviewEntry(handlers, entry, onRendered) + }, + writeLineBreak: (onWritten) => { + handlers.terminal.write("\r\n", onWritten) + } + } + }, onComplete) } const flushTerminalOutputQueue = (handlers: TerminalMessageHandlers): void => { @@ -248,11 +218,8 @@ const flushTerminalOutputQueue = (handlers: TerminalMessageHandlers): void => { inlineImagePreviewsEnabledRef: handlers.inlineImagePreviewsEnabledRef, segment, writer: { - writePreviewLineBreak: (outputSegment, onComplete) => { - writeLineBreakBeforePreview(handlers, outputSegment, onComplete) - }, - writePreviews: (paths, onComplete) => { - writeInlineImagePreviews(handlers, paths, onComplete) + writePreviews: (outputSegment, onComplete) => { + writeInlineImagePreviews(handlers, outputSegment, onComplete) }, writeText: (text, onComplete) => { handlers.terminal.write(text, onComplete) diff --git a/packages/terminal/src/web/terminal-panel-runtime-core.ts b/packages/terminal/src/web/terminal-panel-runtime-core.ts index ab5a6685..0c35258c 100644 --- a/packages/terminal/src/web/terminal-panel-runtime-core.ts +++ b/packages/terminal/src/web/terminal-panel-runtime-core.ts @@ -31,6 +31,7 @@ export const createLifecycleState = (): TerminalLifecycleState => ({ disposed: false, inlineImageDisposables: [], inlineImageObjectUrls: new Map(), + inlineImageRenderedPaths: new Set(), outputQueue: [], outputWriting: false, readyNotified: false, diff --git a/packages/terminal/src/web/terminal-panel-runtime-types.ts b/packages/terminal/src/web/terminal-panel-runtime-types.ts index b36ffd58..1484ba4d 100644 --- a/packages/terminal/src/web/terminal-panel-runtime-types.ts +++ b/packages/terminal/src/web/terminal-panel-runtime-types.ts @@ -28,6 +28,7 @@ export type TerminalLifecycleState = { disposed: boolean inlineImageDisposables: Array inlineImageObjectUrls: Map + inlineImageRenderedPaths: Set outputQueue: Array outputWriting: boolean readyNotified: boolean diff --git a/packages/terminal/tests/web/terminal-inline-images-core.test.ts b/packages/terminal/tests/web/terminal-inline-images-core.test.ts index 8260b197..dea8fde4 100644 --- a/packages/terminal/tests/web/terminal-inline-images-core.test.ts +++ b/packages/terminal/tests/web/terminal-inline-images-core.test.ts @@ -3,15 +3,17 @@ import { vi } from "vitest" import { splitTerminalInlineImageOutput, + type TerminalInlineImageOutputSegment, + writeTerminalInlineImagePreviews, writeTerminalOutputSegment } from "../../src/web/terminal-inline-images-core.js" +import type { TerminalInlineImageEntry } from "../../src/web/terminal-inline-images.js" import { cachedTerminalInlineImageEntry, cacheTerminalInlineImageBlob, revokeTerminalInlineImageObjectUrlCache, terminalInlineImagePreviewRows, - terminalInlineImageSpacer, - unavailableTerminalInlineImageEntry + terminalInlineImageSpacer } from "../../src/web/terminal-inline-images.js" const issue250ImagePath = `/${["t", "mp"].join("")}/phantom-e2e.tuhl98/wallet-step-after-password.png` @@ -71,8 +73,7 @@ describe("terminal inline image output", () => { it("writes detected image paths as plain terminal text when automatic previews are disabled", () => { const textWrites: Array = [] - const previewWrites: Array> = [] - const previewLineBreaks = { count: 0 } + const previewWrites: Array = [] const completions = { count: 0 } writeTerminalOutputSegment({ @@ -83,12 +84,8 @@ describe("terminal inline image output", () => { text: issue250DeleteCommand }, writer: { - writePreviewLineBreak: (_segment, onComplete) => { - previewLineBreaks.count += 1 - onComplete() - }, - writePreviews: (paths, onComplete) => { - previewWrites.push(paths) + writePreviews: (segment, onComplete) => { + previewWrites.push(segment) onComplete() }, writeText: (text, onComplete) => { @@ -101,11 +98,42 @@ describe("terminal inline image output", () => { }) expect(textWrites).toEqual([issue250DeleteCommand]) - expect(previewLineBreaks.count).toBe(0) expect(previewWrites).toEqual([]) expect(completions.count).toBe(1) }) + it("forwards the segment to the preview writer after text when previews are enabled", () => { + const writes: Array = [] + const segment: TerminalInlineImageOutputSegment = { + endedWithLineBreak: true, + imagePaths: [issue250ImagePath], + text: `${issue250ImagePath}\r\n` + } + + writeTerminalOutputSegment({ + inlineImagePreviewsEnabledRef: { current: true }, + segment, + writer: { + writePreviews: (previewSegment, onComplete) => { + writes.push(`previews:${previewSegment.imagePaths.join(",")}`) + onComplete() + }, + writeText: (text, onComplete) => { + writes.push(`text:${text}`) + onComplete() + } + } + }, () => { + writes.push("complete") + }) + + expect(writes).toEqual([ + `text:${issue250ImagePath}\r\n`, + `previews:${issue250ImagePath}`, + "complete" + ]) + }) + it("caches successful image fetch blobs as reusable object urls", () => { const createObjectUrl = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:terminal-image") const revokeObjectUrl = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => void 0) @@ -130,11 +158,132 @@ describe("terminal inline image output", () => { expect(cache.size).toBe(0) }) - it("represents failed image fetches without using a broken image url", () => { - expect(unavailableTerminalInlineImageEntry("/var/data/missing.png", "https://api/image")).toEqual({ - _tag: "UnavailableTerminalInlineImage", - fetchUrl: "https://api/image", - path: "/var/data/missing.png" +}) + +const availableEntry = (path: string): TerminalInlineImageEntry => ({ + _tag: "AvailableTerminalInlineImage", + displayUrl: `blob:${path}`, + fetchUrl: `https://api${path}`, + path +}) + +type PreviewWriterLog = { + readonly events: Array + readonly loadedEntries: Map +} + +const previewWriter = ({ events, loadedEntries }: PreviewWriterLog) => ({ + loadEntry: (path: string, onComplete: (entry: TerminalInlineImageEntry | null) => void) => { + events.push(`load:${path}`) + onComplete(loadedEntries.get(path) ?? null) + }, + renderPreview: (entry: TerminalInlineImageEntry, onComplete: () => void) => { + events.push(`render:${entry.path}`) + onComplete() + }, + writeLineBreak: (onComplete: () => void) => { + events.push("line-break") + onComplete() + } +}) + +describe("terminal inline image previews (issue 445)", () => { + const missingPath = "/var/data/missing.png" + const presentPath = "/var/data/present.png" + + it("does not render a preview for unavailable images", () => { + const events: Array = [] + const renderedPaths = new Set() + const completions = { count: 0 } + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [missingPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries: new Map([[missingPath, null]]) }) + }, () => { + completions.count += 1 + }) + + expect(events).toEqual([`load:${missingPath}`]) + expect(renderedPaths.size).toBe(0) + expect(completions.count).toBe(1) + }) + + it("renders the same image path only once across segments", () => { + const events: Array = [] + const renderedPaths = new Set() + const loadedEntries = new Map([[presentPath, availableEntry(presentPath)]]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: false, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete:first") + }) + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: false, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete:second") + }) + + expect(events).toEqual([ + `load:${presentPath}`, + `render:${presentPath}`, + "complete:first", + "complete:second" + ]) + expect(renderedPaths).toEqual(new Set([presentPath])) + }) + + it("writes the leading line break lazily and only before the first rendered preview", () => { + const events: Array = [] + const renderedPaths = new Set() + const otherPath = "/var/data/other.png" + const loadedEntries = new Map([ + [missingPath, null], + [otherPath, availableEntry(otherPath)], + [presentPath, availableEntry(presentPath)] + ]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [missingPath, presentPath, otherPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete") + }) + + expect(events).toEqual([ + `load:${missingPath}`, + `load:${presentPath}`, + "line-break", + `render:${presentPath}`, + `load:${otherPath}`, + `render:${otherPath}`, + "complete" + ]) + }) + + it("leaves output untouched when every preview is skipped", () => { + const events: Array = [] + const renderedPaths = new Set([presentPath]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries: new Map() }) + }, () => { + events.push("complete") }) + + expect(events).toEqual(["complete"]) }) })