Files
pad/web/e2e
xarmian 7f640a9c40 feat(attachments): render markdown and plain-text attachments in the viewer
The arm itself, plus the render seam and the browser proof.

`renderMarkdownDocument` is a third thin wrapper in FRONT of the shared
`marked` pipeline, following `renderMarkedWithAttachments`'s precedent
rather than standing up a second renderer. It omits two things
deliberately. No wiki-link resolution: an attached `.md` was authored
elsewhere, so resolving its `[[brackets]]` against whatever workspace is
showing it would silently retarget a foreign document's links at local
items — BUG-2830's hazard entered through the front door. No attachment
context, and it CLEARS the module-level context for the duration rather
than merely leaving it alone: a nested call from a resolver or a
`missing` hook that itself renders markdown would otherwise inherit the
outer document's workspace and resolver. Save, clear, restore, mirroring
the sibling wrapper. Sanitization is inherited, not re-derived, so an
attached document is governed by the same allowlist as item content.

Plain text does NOT go through the pipeline. A `.txt` renders in a
`<pre>` as text, because interpreting a plain-text file's asterisks as
formatting would misrepresent its content; Svelte escapes it, so that
path emits no HTML at all.

THE FALLBACK ARM'S CONDITION CHANGED, and this is the part to check
hardest. It read `shownRenderer !== 'raster-image'`, which was correct
while the union had one member and would have drawn "No preview
available" over every text document the moment it had two. It is now
`=== null` — the registry's actual "no renderer claims this" answer —
which says what it means and stays correct when 'pdf' lands.

INTERACTION, all of it found by review or re-reading rather than by the
unit suite:

- The wheel handler consumed every wheel before its exclusions, so the
  card could never scroll. The text exclusion returns BEFORE
  `preventDefault`, the opposite of every other exclusion there: the
  others want the wheel swallowed, this one wants it delivered. Scroll
  chaining to the inert page is stopped by `overscroll-behavior:
  contain`, so the guarantee the `preventDefault` provided is kept.
- The full-bleed layer took `pointer-events: auto`, so a click on the
  empty area targeted it and backdrop-close was broken for this arm
  alone. The layer is inert; the CARD is the interactive surface.
- `touch-action` INTERSECTS down the ancestor chain, so the card's
  `pan-y` could never override the stage's `none` and a phone could not
  scroll at all. The stage gives up its pan claim on this arm only.
- The card had no tab stop. The viewer's arrow keys are its own
  next/previous navigation, so a keyboard-only user could open a
  document and never reach past its first screen. `tabindex="0"` plus
  `role="document"` and the filename label; the linter warning is
  suppressed narrowly with its reason at the site.
- The focus-handoff effect tracked `loader.phase` — the IMAGE loader,
  which this arm disposes, so it never changes there. This is the only
  arm whose CONTENT is focusable, so a reload unmounted a focused link
  and stranded focus outside the modal.
- `resolvedSize` and `revalidateToken` ride the load key SCOPED to the
  text arm. The key is shared, so an unconditional append re-ran the
  effect for the raster arm too and restarted image loads. The token is
  there because a parent RESTORE drives the image loader through the
  metadata probe's answer but cannot see a failed text GET's `error`
  phase — without it, a preview that 404'd while archived stayed
  permanently errored after the restore that fixed it.

E2E, because three of these guarantees are CSS mechanisms and the jsdom
suite injects no component styles — `getComputedStyle` there returns the
engine default for every element, so two assertions written for them
could not fail and were deleted rather than banked as coverage.
`web/e2e/attachment-text-preview.spec.ts` covers the render, backdrop
close, scrolling with no leak to the surface behind, and selection. Its
FIRST RUN is what caught the feature rendering raw source, which the
whole green unit suite could not see.

CONVE-23 sweep on the prose this falsified: the ADMISSION vs THE ARM
block enumerated 'raster-image loads bytes / null is no-bytes' as a
two-way split, and the fallback arm described itself as "an entry the
viewer cannot draw as an image". Both rewritten, plus a paragraph on why
two byte-loading arms leave the no-bytes invariant unchanged in kind.

`.pad-e2e-*/` is gitignored: a shared checkout runs concurrent suites, so
each seat points `PAD_E2E_DATA_DIR` at its own, and those hold a
generated encryption key and a multi-MB WAL.

`item-attachment-strip.spec.ts` changes here because it is a CONSEQUENCE
of this arm, not a separate concern. Two of its tests uploaded a
`text/plain` file and asserted the viewer showed "No preview available"
over it — true when written, false by design once text previews. CI
caught them; my sweep had not, because I swept the unit tests and stated
that boundary nowhere, which reads identically to a complete sweep
(CONVE-18's amended half). The fix keeps each test's SUBJECT — both are
producer→host wiring tests whose named subject is the fallback arm — and
moves the vehicle to PDF, which keeps every property the fixture was
chosen for while remaining unclaimed by any renderer. It carries a note
saying it will go red again when PLAN-2393 builds the `'pdf'` slot, and
that the red is the design: pick the next unclaimed type, never weaken
the assertion.

Closes #1169
2026-09-01 03:44:02 +00:00
..