mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-04 20:17:03 +00:00
some more fixes
This commit is contained in:
1 parent
7a3c04f170
commit
2b86a407ba
63 files changed
+12172
-44
No files matched your search
@@ -0,0 +1,408 @@
|
||||
# Testing: the dev harness and `@margin/test`
|
||||
|
||||
Scope: the browser fixture harness, the Playwright configs and suites, the spec helpers, the Rust
|
||||
fixture corpora, and what CI runs. The production half of `@margin/ipc` (the `call` wrapper, the
|
||||
phase union, `listen`) belongs to [hooks.md](hooks.md); this document owns the dev-mode half of the
|
||||
same package. Short names: `margin` = `python/margin`, `calendar` = `python/margin-caledar`,
|
||||
`docs` = `rust/margin-editor`, `mail` = `rust/margin-mail`, all under
|
||||
`/Users/pj/Workspace/projects`. Line numbers and counts are as of 2026-09-06.
|
||||
|
||||
## The harness is the most valuable thing in this consolidation
|
||||
|
||||
[guidelines/working-together.md](guidelines/working-together.md) forbids starting a dev server: the
|
||||
user keeps one running, Vite is on `strictPort`, and in Margin Mail a dev instance shares the real
|
||||
app data directory, so a second one is a live-data accident. That leaves one way for an agent to see
|
||||
whether a change works without asking the user to look: a scratch Vite on a spare port with the
|
||||
invoke boundary stubbed, driven by Playwright. The stub is each app's `src/dev`, and it is worth
|
||||
more than any component in `@margin/ui`, because without it every verification ends in "try it
|
||||
yourself", which the same guideline also forbids.
|
||||
|
||||
What each app has today:
|
||||
|
||||
- calendar: a 356 line `src/dev`, a 12 command mock, no faked events at all, and a suite happy to
|
||||
pass against any checkout's dev server.
|
||||
- docs: the best harness in the suite, a hand-rolled Tauri shim so the real `@tauri-apps/api` event
|
||||
plugin runs in a plain tab, plus the only identity check, both locked inside `tests/` where
|
||||
`pnpm dev` cannot reach them.
|
||||
- mail: the largest fixture at 3,341 lines and 105 commands, events faked as window `CustomEvent`s
|
||||
with an app-side bridge, and the only sane screenshot policy.
|
||||
- margin: nothing. No `tests/`, no `src/dev`, no vitest, no `test` script (`package.json:6-13`), and
|
||||
`src/ipc.ts` is 43 lines calling `invoke` directly behind an `isDesktop` early return, so there is
|
||||
no seam a fixture could plug into.
|
||||
|
||||
Margin is not a migration. It builds the seam the other three already have, which is why it is last
|
||||
in the sequence rather than first.
|
||||
|
||||
## The state of play
|
||||
|
||||
| | margin | calendar | docs | mail |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `test` / `test:ui` scripts | neither | both | both | both |
|
||||
| `vitest` / `@playwright/test` | neither | 3.2.4 / 1.62.1 | 3.2.4 / 1.62.1 | 3.2.4 / 1.62.1 |
|
||||
| `src/dev` lines | absent | 356 | 1,040 | 3,341 |
|
||||
| mock `case` labels | none | 12 | 35 | 105 |
|
||||
| spec files / `test(` blocks | 0 / 0 | 10 / 131 | 14 / 110 | 24 / 253 |
|
||||
| spec lines, helpers excluded | 0 | 2,050 | 3,717 | 6,445 |
|
||||
| colocated `*.test.ts` files / tests | 0 / 0 | 12 / 211 | 32 / 699 | 8 / 81 |
|
||||
| Rust `#[test]` | 0 | 79 | 0, all integration | 531 |
|
||||
| `src-tauri/tests/` | no | no | 8 binaries plus support | no |
|
||||
| `src-tauri/fixtures/` | no | no | no | 76 files |
|
||||
|
||||
Margin has no tests of any kind, and its `vite.config.ts` imports from `vite` rather than
|
||||
`vitest/config`, so there is not even a `test` block to add to. It is also the app with the most
|
||||
design drift, 16 hex literals and 18 rgba values outside its token layer against zero in docs and
|
||||
mail ([design-system.md](design-system.md)); the two facts are related. Docs' six `_audit*.spec.ts`
|
||||
files have since been deleted, so its count is 14 specs, not the 19 the audit found.
|
||||
|
||||
## `@margin/test`, the Playwright side
|
||||
|
||||
The three configs are one file with the port swapped. Byte identical across all three: `testDir`,
|
||||
`fullyParallel`, `retries: 0` with the same comment about a test that only passes on the second go,
|
||||
the list reporter, the cache `outputDir`, both timeouts, the 1440x900 viewport, `en-GB`, the trace
|
||||
and screenshot policies, one chromium project with the identical comment on why it is not
|
||||
`devices["Desktop Chrome"]`, and a `webServer` running `pnpm dev` with `reuseExistingServer: true`.
|
||||
So the whole file becomes a call:
|
||||
|
||||
```ts
|
||||
import { marginPlaywrightConfig } from "@margin/test/playwright";
|
||||
|
||||
export default marginPlaywrightConfig({
|
||||
port: 1450,
|
||||
witnesses: ["src/ipc.ts", "src/dev/backend.ts", "src/dev/fixture.ts"],
|
||||
});
|
||||
|
||||
interface MarginPlaywrightOptions {
|
||||
/** 1430 calendar, 1440 docs, 1450 mail, 1460 margin. baseURL and webServer.url come off it. */
|
||||
port: number;
|
||||
/** Source files byte-compared against the served copy before any spec runs. Empty turns the check off. */
|
||||
witnesses: string[];
|
||||
/** Defaults to Asia/Kolkata. Pass null for a suite that must read the runner's own zone. */
|
||||
timezoneId?: string | null;
|
||||
use?: PlaywrightTestConfig["use"]; // merged over the defaults, for the rare real difference
|
||||
}
|
||||
```
|
||||
|
||||
Two genuine differences exist and both become defaults. `timezoneId: "Asia/Kolkata"` is set in
|
||||
calendar (`playwright.config.ts:34`) and mail (`:31`), which anchor their fixtures to the browser's
|
||||
local day, and absent in docs, which has no clock; the factory defaults it on and docs passes
|
||||
`timezoneId: null`, because pinning the zone by accident is harmless and failing in another zone is
|
||||
not. The second is `globalSetup` in docs only (`:14`), and it becomes the default by way of
|
||||
`witnesses`.
|
||||
|
||||
The identity check is the part of docs' setup the other two most need. `tests/identity.ts` is 106
|
||||
lines run once before any spec: for each of five witness files it fetches
|
||||
`${baseURL}/${witness}?raw`, decodes the string literal Vite serves by hand rather than by
|
||||
evaluating it (`:38-80`, since running what an untrusted server sends to decide whether to trust it
|
||||
has the order wrong), and compares it byte for byte against disk. Its header (`:1-15`) says why:
|
||||
`reuseExistingServer` on a fixed port is what makes the suite quick and also what lets it talk to a
|
||||
server another checkout left behind, where a pass proves nothing and a failure sends you hunting
|
||||
through a file you never touched.
|
||||
|
||||
Calendar and mail have no such check, so both suites are currently happy to pass against anybody's
|
||||
copy. With four apps on four adjacent ports and one machine, that is not hypothetical.
|
||||
|
||||
The factory supplies `globalSetup`, so no app wires it, and `witnesses` is the only per-app input:
|
||||
for mail `src/ipc.ts`, the dev backend and the fixture; for docs the schema, the serializer and the
|
||||
save path it already names at `identity.ts:25-31`. Saved: roughly 45 lines per app, and "all three
|
||||
run at 1440x900 with no retries" becomes a fact rather than a coincidence.
|
||||
|
||||
## `@margin/ipc`, the dev harness side
|
||||
|
||||
All three apps switch dev mode on identically in `src/ipc.ts` (calendar `:192-197`, docs `:288-292`,
|
||||
mail `:748-752`): in a `DEV` build with no `__TAURI_INTERNALS__`, `call` imports `./dev/mockIpc` and
|
||||
routes there, otherwise it invokes. Mail's differs in one way that matters and wins: its `catch` arm
|
||||
posts the failure to `log_note` before rethrowing, skipping `log_note` itself so it cannot loop
|
||||
(`ipc.ts:752-759`). That is [guidelines/errors-and-feedback.md](guidelines/errors-and-feedback.md)
|
||||
in code, and the shared `makeCall(loadBackend)` carries it, alongside `devFlag(key)`, the eight-line
|
||||
localStorage reader that exists three times today.
|
||||
|
||||
### The typed command registry
|
||||
|
||||
`mockCall` is a `switch (command)` in all three, returning `as unknown as T` at every arm and
|
||||
throwing on an unknown command (calendar `mockIpc.ts:131-132`). There is no type link between a
|
||||
command name, its arguments and its return: calendar casts args to `Record<string, never>` (`:41`)
|
||||
and then per field. Replace it with a table:
|
||||
|
||||
```ts
|
||||
// src/dev/backend.ts
|
||||
import { defineBackend, devFlag } from "@margin/ipc/dev";
|
||||
|
||||
export const backend = defineBackend({
|
||||
accounts_list: () => (devFlag("marginmail-dev-empty") ? [] : accounts),
|
||||
thread_view: ({ key }: { key: string }): ThreadView => viewOf(byKey(key)),
|
||||
});
|
||||
export type Command = keyof typeof backend;
|
||||
export const { mockCall, has, commands, emit, script } = backend;
|
||||
```
|
||||
|
||||
The arg cast at every arm and the `as unknown as T` at every return both disappear, and a command
|
||||
declared in `dto.rs` with no handler becomes checkable in one place instead of a runtime surprise
|
||||
three screens later. `has` and `commands` are what makes that check writable as a vitest.
|
||||
|
||||
### Faking events, three ways
|
||||
|
||||
Calendar does not fake them: `App.tsx:52` is `if (!isTauri) return;` ahead of every `listen`, so
|
||||
`menu-action`, `auth`, `sync-progress` and `store-changed` never arrive in a browser and everything
|
||||
they drive is unreachable from the suite, including the `auth` listener its own comment calls the
|
||||
only thing that ever learns a mobile sign-in worked.
|
||||
|
||||
Mail dispatches window `CustomEvent`s from inside the mock under the same names, with an optional
|
||||
delay so a state that would otherwise last one frame is observable (`mockIpc.ts:237-248`;
|
||||
`narrateFirstSync` at `:637` walks a five-step sync). The app side is `onAppEvent` (`App.tsx:69-77`),
|
||||
seven lines picking `listen` or `window.addEventListener` off `isTauri`. Cheapest correct answer,
|
||||
and it leaves the Tauri arm of that fork, the arm that ships, never exercised in a browser.
|
||||
|
||||
Docs does the right thing. `installTauriShim` (`tests/disk.ts:37-133`) installs a hand-rolled
|
||||
`__TAURI_INTERNALS__` at document start: `invoke`, `transformCallback`, `unregisterCallback`,
|
||||
`runCallback`, `plugin:event|listen` and `|unlisten`, a listener map, `metadata`, `convertFileSrc`
|
||||
and `__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener`. Its comment (`:27-36`) records that it is
|
||||
deliberately not `@tauri-apps/api/mocks`, which cannot be reached from an init script, and written
|
||||
to the contract rather than to convenience. `emit` returns a delivery count per event (`:66-75`), so
|
||||
a test can tell a working subscription from a payload that fell on the floor.
|
||||
|
||||
Docs is the one to keep: the app runs the real `@tauri-apps/api`, so the branch under test is the
|
||||
branch that ships, and mail's `onAppEvent` stops being necessary. It is not reusable today for two
|
||||
reasons, that it lives in `tests/` so a person running `pnpm dev` by hand gets no events, and that
|
||||
the backend module specifier is hard-coded at `disk.ts:79`.
|
||||
|
||||
### What an app implements
|
||||
|
||||
```ts
|
||||
// src/dev/harness.ts, imported by src/main.tsx behind import.meta.env.DEV and by the spec helper
|
||||
import { installTauriShim } from "@margin/ipc/dev";
|
||||
installTauriShim({ backend: "/src/dev/backend.ts" });
|
||||
```
|
||||
|
||||
It must stay self-contained with no module-scope closure, because Playwright serialises it into the
|
||||
page through `addInitScript`, which is why the specifier is an argument. From the spec side:
|
||||
|
||||
```ts
|
||||
import { emitEvent, driveFixture } from "@margin/test/harness";
|
||||
const delivered = await emitEvent(page, "store-changed", "threads"); // returns the listener count
|
||||
await driveFixture(page, "external.rename", ["notes/a.md", "notes/b.md"]);
|
||||
```
|
||||
|
||||
`driveFixture` generalises docs' `change`/`ask` pair (`disk.ts:135-144`): the app exports an
|
||||
`external` surface of functions that mutate the fixture the way another program would and return the
|
||||
events the backend would have emitted, and the harness puts them on the bus. Docs' `pauseWrites` and
|
||||
`resumeWrites` (`mockIpc.ts:679-695`) stay app-specific; the mechanism does not. Mail's timed
|
||||
narration becomes `script([{ after: 0, event, payload }, ...])`, and dev flags keep their per-app
|
||||
names, `<app>-dev-<thing>` being uniform already.
|
||||
|
||||
## The spec helpers
|
||||
|
||||
Calendar and mail both have `tests/app.ts`, 370 and 401 lines, and 10 of 10 and 23 of 24 specs
|
||||
import from it (the exception, mail's `kit.spec.ts`, is mostly static scans). These six are code
|
||||
identical between the two and differ only in doc comments:
|
||||
|
||||
| helper | calendar | mail | lines |
|
||||
| --- | --- | --- | --- |
|
||||
| `clockAt` + `MIDDAY` | `app.ts:41-53` | `app.ts:46-58` | 13 |
|
||||
| `openApp` | `:61-90` | `:66-90` | 25, seed keys only |
|
||||
| `settle` | `:105-112` | `:99-106` | 8, third copy at docs `caret.ts:46-53` |
|
||||
| `box` | `:114-124` | `:108-118` | 11 |
|
||||
| `openDialog` | `:288-293` | `:188-193` | 6 |
|
||||
| `contrastOf` | `:306-366` | `:219-278` | 60 |
|
||||
|
||||
`contrastOf` is the one that took real work: it composites every translucent background between the
|
||||
element and the page through a 1x1 canvas, because `oklch()` and `color-mix()` otherwise read as
|
||||
transparent. The two apps that have it are the two that independently found the `--ink-faint`
|
||||
defect. Two more mail helpers are generic despite being written for mail: `token(page, name)`
|
||||
(`app.ts:180-185`) and `failCommands(page, commands)` (`app.ts:343-360`), which answers the request
|
||||
for the dev backend module with a shim forwarding to the real one (`?real`) and rejecting the named
|
||||
commands. That is the only way to test a failure path when the backend is in the page and not on the
|
||||
wire, and 18 lines that work unchanged in any of the four.
|
||||
|
||||
`openApp` becomes a factory, since only the seed keys differ:
|
||||
|
||||
```ts
|
||||
export const openApp = makeOpenApp({ theme: "marginmail-theme", pane: "marginmail-pane" });
|
||||
```
|
||||
|
||||
The `__test-seeded` sentinel goes with it: the seed is written once per context and not on every
|
||||
navigation, so a test that reloads to check what survived is not silently reset underneath itself.
|
||||
|
||||
Docs has no shared spec helper at all. Its 14 specs define 16 local opener functions (`openReadme`,
|
||||
`openHandbook`, `openWriting`, `openFolder`, `open`, `openPreview`) and all 14 inline the same
|
||||
`margindocs-recents` seed. Adopting `makeOpenApp` is the single biggest deletion in this document.
|
||||
|
||||
## The Rust fixture story
|
||||
|
||||
Mail's `src-tauri/fixtures/` is 76 files: 36 `.eml` messages as they come off the wire (CRLF
|
||||
throughout, half not UTF-8, ISO-8859-1 and ISO-2022-JP among them), 36 matching `golden/*.txt` and 4
|
||||
`autoconfig/*.xml`. A `corpus!` macro (`fixtures.rs:10-19`) compiles them into the test binary as
|
||||
one `include_bytes!` const per file plus an `all()` returning every pair, and `fixtures.rs:58-102`
|
||||
is itself a test: every fixture has a header/body break, no bare LF, a plausible date, a parseable
|
||||
From. Beside it, `provider/fake.rs` (612 lines, `#![cfg(test)]`) is an in-memory mailbox with
|
||||
paging, a history log and scripted failures, and the only reason the sync engine is testable
|
||||
without credentials.
|
||||
|
||||
There are two hand-maintained lists of that one directory and they have drifted. `fixtures.rs:21-52`
|
||||
names 30 files; a second `corpus!` macro at `mime/parse.rs:627-672` names 36. The six missing from
|
||||
the first are the `surface-*.eml` set (`dark-inline-text`, `newsletter-background-image`,
|
||||
`newsletter-bgcolor`, `plain-html`, `sender-dark-design`, `wrapper-background`), so
|
||||
`every_fixture_is_a_message` checks none of them. That is the exact failure docs wrote a paragraph
|
||||
about at `src/markdown/corpus/load.ts:6-9`, where naming folders explicitly left twenty adversarial
|
||||
files inside the corpus and outside every gate that read it. The fix there was
|
||||
`import.meta.glob("./*/*.md")`; the fix here is the same idea.
|
||||
|
||||
The others have nothing comparable. Docs builds its Rust fixture at runtime:
|
||||
`src-tauri/tests/support/notes_repo.rs` (339 lines) creates a real git repository per test binary,
|
||||
copies 12 documents out of `src/markdown/corpus/real` so there is one corpus and not two, and
|
||||
generates 13,000 files under a vendored `node_modules`, because several tests need a folder large
|
||||
enough that skipping it beats walking it. Calendar has 79 `#[test]`s and no fixtures, margin
|
||||
neither.
|
||||
|
||||
Worth sharing? Not yet, and this document should say so rather than pad the package list. The macro
|
||||
is ten lines with one consumer, a crate for it would be indirection over a single call site, and
|
||||
[risks.md](risks.md) is explicit that sharing is justified by measured duplication. What is worth
|
||||
doing now is fixing the drift inside mail (glob the directory with `include_dir`, delete both lists)
|
||||
and writing the rule into [guidelines/code-style.md](guidelines/code-style.md): a corpus is a
|
||||
directory, never a list, because adding a file must put it inside every sweep. `TempRepo` moves into
|
||||
a shared crate when a second app wants a git-backed temporary directory, and none does today.
|
||||
|
||||
## Two gaps that are not duplication
|
||||
|
||||
### Nothing asserts anything about icons
|
||||
|
||||
Design-system assertions exist in three shapes and none covers icons. Mail's `tests/kit.spec.ts`
|
||||
(132 lines) renders `#/kit` in both palettes, asserts geometry read from custom properties
|
||||
(`--list-w` is `420px`, a row 46px, an avatar 30x30), then runs three static `node:fs` scans: no hex
|
||||
literal in any stylesheet under `src/ui` or `src/screens`, every `<input>` carrying `NO_AUTOFILL` or
|
||||
an explicit `autoComplete`, no file saying "keychain". Calendar's `legibility.spec.ts` is runtime:
|
||||
contrast, no block reading "Untitled", a non-empty accessible name on every block, both themes.
|
||||
Docs' is a vitest: `src/theme.test.ts` reads three stylesheets plus `index.html`'s pre-bundle boot
|
||||
script and asserts all four declare the same variable set, because a missing dark variable falls
|
||||
back silently to the warm light value in `:root`. A grep for icon assertions across all three suites
|
||||
returns nothing, while [risks.md](risks.md) records five independent fixes for the same icon
|
||||
baseline problem and eleven implementations of a square icon button. What `@margin/test` ships:
|
||||
|
||||
```ts
|
||||
expectThemesAgree(sheets, bootScript); // docs src/theme.test.ts, vitest, no browser
|
||||
expectNoColourLiterals(root, ["src/ui", "src/screens"]); // mail kit.spec.ts:80-96, vitest, no browser
|
||||
expectIconsFromSet(root, dirs); // no path data outside @margin/icons, vitest
|
||||
expectIconGrid(icons); // every path inside the 24 unit box, vitest
|
||||
expectTokens(page, { "--list-w": "420px" }); // mail kit.spec.ts:58-79
|
||||
expectContrast(page, selector, { min: 4.5 }); // calendar legibility.spec.ts, built on contrastOf
|
||||
expectAccessibleNames(page, selector); // calendar legibility.spec.ts
|
||||
expectIconOptical(page, ".icon-button"); // painted glyph box centred within 1px
|
||||
```
|
||||
|
||||
The first four are static scans over source and need no browser, so they run under vitest. That is
|
||||
what makes them the first tests Margin ever gets: they land before it has a Playwright config, a
|
||||
fixture or a dev harness, and they fail today on the drift [design-system.md](design-system.md)
|
||||
measured.
|
||||
|
||||
### Known failures are recorded nowhere in any tree
|
||||
|
||||
The only markers anywhere are `guide-shots.spec.ts:19-22`, an intentional `GUIDE_SHOTS` env gate,
|
||||
and a `test.fail` at `external-changes.spec.ts:320` kept so it turns red the day the defect is
|
||||
fixed. The four Margin Mail browser failures are written down only in
|
||||
[guidelines/app-facts.md](guidelines/app-facts.md), which is in this plan directory and not in the
|
||||
repo that fails.
|
||||
|
||||
They are not one thing and should not be recorded as one. Three (`shell.spec.ts` "New for you above
|
||||
Previously seen", `snooze.spec.ts` "Back above New for you", `triage.spec.ts` "Mark all as seen is a
|
||||
link on the heading") assert Inbox group heads the app deliberately no longer draws: stale tests,
|
||||
and the work is rewriting them to the current design. The fourth, `kit.spec.ts` "every text field
|
||||
tells the webview not to fill it in", flags a real input in `Kit.tsx` and one in `Settings.tsx`.
|
||||
That is the test doing its job, and recording it as a known failure would be weakening a test to
|
||||
reach green, which `margin-editor/docs/conventions.md:74` forbids in as many words. Fix it.
|
||||
|
||||
The record goes in `tests/known-failures.md` per app: one heading per failure naming the spec file
|
||||
and the test title (matched on the name, never the line number), what it asserts, why it fails and
|
||||
which kind it is. `@margin/test` ships the checker that reads it, so a suite whose failures match
|
||||
the file exactly exits with a distinct code and one that fails anything else does not. Without the
|
||||
checker the file rots into a list of excuses within a month.
|
||||
|
||||
## CI
|
||||
|
||||
Playwright runs in no app's CI. Calendar's `ci.yml:31` and `:70` run `pnpm test` and `cargo test`;
|
||||
docs' `:34`, `:58` and `:78` the same plus a separate `--test-threads=1` binary; mail's `:49` and
|
||||
`:92` the same plus `pnpm fonts:check` and `node scripts/docs-check.mjs`. The three justfiles agree
|
||||
line for line that `just test-ui` is `pnpm test:ui`, and nothing in CI calls it. What should run
|
||||
where:
|
||||
|
||||
- Every push, every app: `pnpm build`, `pnpm test`, `cargo test` and the new static assertions.
|
||||
Margin joins this list first and gets `pnpm test` for the first time.
|
||||
- Every push, the three with a suite and then all four: `pnpm test:ui`, after
|
||||
`pnpm exec playwright install --with-deps chromium`. Two minutes on a runner already building the
|
||||
Rust side, and the only check that the harness itself still works. The identity check is a no-op
|
||||
there, where Playwright starts its own server, which is fine: it costs five fetches.
|
||||
- Never in CI: `guide-shots.spec.ts`, which writes committed files, and mail's eight `#[ignore]`
|
||||
Rust tests, six of them "hits the network" in `imap/discover.rs`.
|
||||
- The shared repo's own CI, per [risks.md](risks.md): check out all four apps against the candidate
|
||||
tag and run each one's `pnpm test` and `pnpm test:ui`. That is the only place the four are checked
|
||||
together, and what turns a token rename from a silent break in the app nobody was working on into
|
||||
a red build before the tag exists.
|
||||
|
||||
Worth wiring while in here: nothing runs `tsc -p tests/tsconfig.json` anywhere, so every spec file
|
||||
is type checked by an editor and nothing else. That file is the same nine options in all three, it
|
||||
moves into `@margin/config`, and the gate gains a line.
|
||||
|
||||
## Per app, exactly what changes
|
||||
|
||||
**1. Docs, because it owns the two pieces worth extracting.** Move `installTauriShim` out of
|
||||
`tests/disk.ts` into `@margin/ipc/dev`, unchanged except for the backend specifier becoming an
|
||||
argument, and call it from a dev entry point as well as from `addInitScript`, so `pnpm dev` in a
|
||||
browser gets real Tauri events for the first time. Move `identity.ts` into `@margin/test` behind
|
||||
`witnesses`. Convert `mockIpc.ts` to `defineBackend`, keeping `external`. Adopt `makeOpenApp` across
|
||||
all 14 specs and delete the 16 openers. Move `theme.test.ts` to `expectThemesAgree`. Check: the
|
||||
suite green, in particular `external-changes.spec.ts`, which drives the watcher over the shim, and
|
||||
its `test.fail` at `:320` still failing.
|
||||
|
||||
**2. Mail, the biggest fixture and the most to gain.** Take the shim, delete `onAppEvent`
|
||||
(`App.tsx:69-77`) and the `CustomEvent` arm of `emit` (`mockIpc.ts:237-248`), so the app runs one
|
||||
code path in both environments and the `listen` branch that ships is the branch under test. Convert
|
||||
105 `case` labels to `defineBackend`, the largest single piece of work here and the one to do in
|
||||
slices by screen. Contribute `token` and `failCommands`, adopt the shared helpers, keep the
|
||||
measurement ones. Turn `kit.spec.ts`'s scans into `expectNoColourLiterals` and friends, and fix the
|
||||
two inputs it flags rather than recording them. Rewrite the three stale group-head assertions. Fix
|
||||
the corpus drift. Check: 24 specs green, `cargo test` at 531 green, `just install` last.
|
||||
|
||||
**3. Calendar, which gains events it has never had.** Take the shim and delete `App.tsx:52`'s
|
||||
`if (!isTauri) return;`, then write the first specs that drive `menu-action`, `auth` and
|
||||
`sync-progress`, none of which has ever been reachable from a browser. Adopt the shared helpers,
|
||||
contribute `contrastOf` and `legibility.spec.ts`'s runtime assertions upward, add `witnesses`. Check
|
||||
`legibility.spec.ts` and `touch.spec.ts`, the second because its file-scope `test.use` is the one
|
||||
thing the factory must not disturb.
|
||||
|
||||
**4. Margin, which is new work and not a migration.** In this order, each step useful on its own:
|
||||
|
||||
1. `vitest`, a `test` script, a `test` block in `vite.config.ts`, and `expectNoColourLiterals` plus
|
||||
`expectThemesAgree` pointed at its stylesheets. They fail on day one against the 16 hex and 18
|
||||
rgba literals in `app.css`, which is the point: the first test catches the drift that motivated
|
||||
the plan.
|
||||
2. Split `src/ipc.ts` into a `call` seam using `makeCall`, so a fixture has somewhere to plug in.
|
||||
Today the 43 lines call `invoke` directly and each function early-returns on `!isDesktop`, so a
|
||||
browser gets silence rather than data.
|
||||
3. `src/dev/fixture.ts` and `src/dev/backend.ts`: a library of two or three books with images, which
|
||||
is what its data model is, plus the writing-tools and PDF commands stubbed.
|
||||
4. `installTauriShim` at the dev entry point, `@playwright/test`,
|
||||
`marginPlaywrightConfig({ port: 1460 })`, and a smoke spec that opens a book, types and asserts
|
||||
the word count. Then the rest: the export path, the proofing colours, the settings panel.
|
||||
|
||||
Nothing in steps 2 to 4 blocks the shared token migration, and step 1 should land before it, because
|
||||
[design-system.md](design-system.md) notes Margin is the app whose base sheet extraction has no
|
||||
suite to catch a mistake.
|
||||
|
||||
## What stays per app
|
||||
|
||||
Every measurement helper. Calendar's `gridFit`, `axis`, `blocks`, `headerDates`, `hourY`, `columnX`
|
||||
and `drag`; mail's `rows`, `groups`, `paneMessages`, `paletteRows`, `actionBar`, `checkedRows`,
|
||||
`bodyText`, `toast`, `listScroll`, `place` and `openRow`; docs' `putCaret` and `caretIsIn`, whose
|
||||
28-line header documents why nothing in the suite presses End to move a caret, and `watchDirty`, a
|
||||
MutationObserver installed before typing so the 500ms autosave cannot be raced. They read
|
||||
app-specific class names and encode app-specific timing, so sharing them would be indirection over
|
||||
one caller each.
|
||||
|
||||
The fixtures and every spec file: a calendar of 12,067 events, a folder of markdown and a mailbox of
|
||||
threads have nothing in common but the loading mechanism. Mail's `#[cfg(test)] mod tests` beside the code, with fifteen modules large enough for a sibling
|
||||
`<module>/tests.rs`, and docs' eight integration binaries are both right for their shape, and
|
||||
forcing either on the other buys nothing.
|
||||
|
||||
The screenshot policy is the one convention worth copying by hand rather than packaging. Mail writes
|
||||
28 screenshots across 16 specs into a gitignored `screenshots/` (`.gitignore:41`), so an ordinary
|
||||
run leaves the tree clean, while the 10 pictures that ship inside the bundle go to a committed
|
||||
`public/guide/` behind `test.skip(() => !process.env.GUIDE_SHOTS)` and `just guide-shots`. Only
|
||||
mail has that two-tier rule, it is right, and it is four lines rather than a package.
|
||||
Reference in new issue
Block a user