Files
software-mansion__argent/packages/tool-server/test/preview-ui-dotfile.test.ts
Ignacy Łątka 1e8e5fa309 fix(argent-lens): preview-UI serving, variant matcher, and off-screen reveal (#394)
Three independent reliability fixes for **Argent Lens** (the
`propose_variant` / `await_user_selection` variant-review flow and its
preview window). They touch disjoint files and were validated
separately; combined here into one PR.

---

### 1. Preview window 404s on dot-path installs (nvm/fnm/volta/asdf)

The Lens window showed a raw `NotFoundError` instead of the variant UI
whenever argent was installed under a path containing a dot-segment
(most commonly nvm, `~/.nvm/...`). `GET /preview/` and
`/preview/theme.css` returned 404 while JSON routes worked.

Cause: the express 4 → 5 bump (#354, shipped in v0.12.1). Express 5's
`send@1` defaults to `dotfiles: "ignore"`, which 404s any file path
containing a dot-segment, and the preview routes pass the absolute
install path to `res.sendFile`. Proven in isolation: express 5.2.1
`sendFile` of the file → 404; express 4.22.1 → 200; `sendFile(basename,
{ root: dir })` → 200.

Fix: serve the preview UI via `sendFile(basename, { root: dir })` (a
`serveUiFile` helper) so the request path carries no dot-segment.
Regression test serves the UI from a temp dir whose name starts with
`.`.

> Note: the underlying `send@1` dot-path behaviour is a property of the
express-5 migration, not of Lens — these two routes are simply the only
`sendFile` calls in the tool-server today.

### 2. Variant fails to anchor right after navigation

A `propose_variant` made immediately after navigating to a screen
(exactly the navigate → screenshot → propose workflow) often failed to
anchor: the card floated/hid ("didn't register"), or latched onto a
same-text distractor (e.g. a bottom-bar tab instead of a header).

Cause: `captureElementFrame` describes the device **once** at propose
time, but iOS' accessibility tree is empty for ~1.25 s after a screen
appears and then fills incrementally (nav chrome before screen content)
— measured on a real device.

Fix: `captureElementFrame` now retries across the warm-up window and
**holds out for an exact label/identifier hit** (the intended element),
falling back to a substring match only after the budget.
`findElementMatch` / `matchFrameInTree` rank exact matches above
substring ones. Adds `match-element-frame-cold-describe.test.ts`; two
existing tests updated for the new retry signature.

### 3. Off-screen choices were unreachable

In the preview window, the off-screen reveal prompt (the "Show them"
card and the "N hidden — show" pill) was hidden unless **every on-screen
choice was already staged** (`onScreenStaged`). So a proposal whose
element was off-screen showed a hidden-choice count with no affordance
to surface it.

Fix: show the prompt whenever any element is off-screen (`offScreenCount
> 0 && !revealOffscreen && !submitting`), dropping the `onScreenStaged`
gate.

---

Supersedes #390, #392, #393 (same commits, combined).
2026-06-24 11:22:57 +02:00

51 lines
2.0 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import express from "express";
import supertest from "supertest";
import { serveUiFile } from "../src/preview";
// Regression guard for the Express 5 `send` dotfiles default (`dotfiles: "ignore"`),
// which 404s any file path containing a dot-segment. argent is routinely installed
// under such a path — nvm (`~/.nvm/...`), fnm, volta, asdf — so a bare
// `res.sendFile(absolutePath)` made the Lens preview window fail to load its UI
// (`/preview/` and `/preview/theme.css` returned NotFoundError). serveUiFile must
// serve the file regardless of where on disk it lives.
describe("serveUiFile under a dot-segment install path", () => {
let dir: string;
beforeAll(() => {
// Directory name starts with "." — the exact shape that trips send's dotfile
// filter, mirroring an install at ~/.nvm/.../preview-ui.
dir = fs.mkdtempSync(path.join(os.tmpdir(), ".argent-dotfix-"));
fs.writeFileSync(path.join(dir, "index.html"), "<!doctype html><title>lens</title>");
fs.writeFileSync(path.join(dir, "theme.css"), ":root{--accent:#0a7ea4}");
});
afterAll(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
function appServing(file: string, contentType: string) {
const app = express();
app.get("/x", (_req, res) => serveUiFile(res, path.join(dir, file), contentType));
return app;
}
it("serves index.html from a dot-path (200, not 404)", async () => {
const res = await supertest(appServing("index.html", "text/html")).get("/x");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toMatch(/text\/html/);
expect(res.text).toContain("lens");
});
it("serves theme.css from a dot-path (200, not 404)", async () => {
const res = await supertest(appServing("theme.css", "text/css")).get("/x");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toMatch(/text\/css/);
expect(res.text).toContain("--accent");
});
});