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,399 @@
|
||||
# Testing, fixtures and the dev harness across the four apps
|
||||
|
||||
Scope: vitest and playwright config, the `tests/` suites and their helpers, `src/dev/` (the
|
||||
browser-against-fake-data harness), Rust fixtures and `#[cfg(test)]` conventions, the tsconfig
|
||||
split. Not build tooling, not CI beyond how it invokes tests.
|
||||
|
||||
Paths: margin `/Users/pj/Workspace/projects/python/margin`, margin-calendar
|
||||
`/Users/pj/Workspace/projects/python/margin-caledar`, margin-docs
|
||||
`/Users/pj/Workspace/projects/rust/margin-editor`, margin-mail
|
||||
`/Users/pj/Workspace/projects/rust/margin-mail`. Cites are relative to those roots.
|
||||
|
||||
## Findings first
|
||||
|
||||
1. **margin has no tests of any kind.** Confirmed plainly: `package.json:5-12` has `dev`, `build`,
|
||||
`preview`, `tauri`, `dmg`, `fonts:sync`, `fonts:check` and nothing else, and neither `vitest` nor
|
||||
`@playwright/test` is a dependency. No `tests/`, no `src/dev/`, zero `*.test.ts` under `src/`,
|
||||
zero `#[cfg(test)]` and zero `#[test]` under `src-tauri/src/`. Its `vite.config.ts` imports from
|
||||
`vite`, not `vitest/config`, so there is no `test` block to add to, and its `src/ipc.ts` (43
|
||||
lines) calls `invoke` directly behind an `isDesktop` early-return, so there is no seam a fixture
|
||||
could plug into. Bringing margin into a shared harness is not a config change, it is building the
|
||||
dev fixture it never had.
|
||||
2. **The three playwright configs are one file with the port swapped.** Diffing them leaves the port
|
||||
(1430/1440/1450), two rewritten comments, and two real differences: margin-docs adds
|
||||
`globalSetup: "./tests/identity.ts"` and drops `timezoneId`. Everything else is byte identical.
|
||||
3. **margin-docs is the only app that checks the dev server is serving its own checkout**, and all
|
||||
three set `reuseExistingServer: true` on a fixed port. `tests/identity.ts` (106 lines) fetches
|
||||
five source files over `?raw` and byte-compares them against disk. Its own header says the four
|
||||
suites beside it "were happy to pass against anybody's copy". margin-calendar and margin-mail
|
||||
still are.
|
||||
4. **The dev-mode invoke stub is the thing worth sharing and the three apps solved it three
|
||||
different ways.** All three branch identically in `src/ipc.ts`, but margin-calendar fakes no
|
||||
events at all, margin-mail fakes them as window `CustomEvent`s with an app-side bridge, and
|
||||
margin-docs hand-rolls 97 lines of `__TAURI_INTERNALS__` inside a test helper so the real
|
||||
`@tauri-apps/api` event plugin works in a plain tab. The third is the correct one and it is the
|
||||
one that is not reusable, because it lives in `tests/disk.ts` rather than in `src/dev/`.
|
||||
5. **margin-docs has no shared spec helper at all.** 19 of 19 spec files define their own
|
||||
`async function open()` and inline the same `margindocs-recents` localStorage seed.
|
||||
margin-calendar and margin-mail both have `tests/app.ts`, and 33 of their 34 spec files import
|
||||
`openApp` from it. Between those two, `contrastOf` (60 lines), `clockAt`, `MIDDAY`, `settle`,
|
||||
`box` and `openDialog` are code identical and differ only in doc comments.
|
||||
|
||||
## What each app has
|
||||
|
||||
| | margin | calendar | docs | mail |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `test` script | none | `vitest run` | `vitest run` | `vitest run` |
|
||||
| `test:ui` script | none | `playwright test` | `playwright test` | `playwright test` |
|
||||
| `vitest` dep | no | 3.2.4 | 3.2.4 | 3.2.4 |
|
||||
| `@playwright/test` dep | no | 1.62.1 | 1.62.1 | 1.62.1 |
|
||||
| `src/dev/` | absent | 356 lines | 1,040 lines | 3,341 lines |
|
||||
| spec files / source-level tests | 0 | 10 / 131 | 19 / 116 | 24 / 252 |
|
||||
| colocated `*.test.ts` / tests | 0 | 12 / 211 | 32 / 655 | 8 / 81 |
|
||||
| Rust `#[test]` | 0 | 79 | 0 (all integration) | 531 |
|
||||
| `src-tauri/tests/` | no | no | 8 files + support | no |
|
||||
| `src-tauri/fixtures/` | no | no | no | 76 files |
|
||||
|
||||
Source-level counts are `test(` and `it(` at file scope; several files wrap tests in a
|
||||
`for (const theme of ["light","dark"])` loop, so the run counts are higher. CI runs `pnpm test` and
|
||||
`cargo test` in all three (calendar `ci.yml:31,70`, docs `ci.yml:34,58,78`, mail `ci.yml:49,92`) and
|
||||
Playwright nowhere. `just test` is `pnpm test` plus `cargo test` and `just test-ui` is
|
||||
`pnpm test:ui`, the three justfiles agreeing line for line on both.
|
||||
|
||||
## playwright.config.ts
|
||||
|
||||
Shared by all three: `testDir: "./tests"`, `fullyParallel: true`, `retries: 0` (with the same
|
||||
comment in each saying a test that only passes on the second go is lying), `reporter: [["list"]]`,
|
||||
`outputDir: "node_modules/.cache/playwright"`, `timeout: 30_000`, `expect.timeout: 5_000`,
|
||||
`baseURL` off the port, `viewport: {1440, 900}`, `deviceScaleFactor: 1`, `locale: "en-GB"`,
|
||||
`trace: "retain-on-failure"`, `screenshot: "only-on-failure"`, a single project
|
||||
`{ name: "chromium", use: { browserName: "chromium" } }` with an identical comment explaining why it
|
||||
is not `devices["Desktop Chrome"]` (that device pins a Windows user agent and the keymap reads the
|
||||
platform off it), and a webServer block of `pnpm dev`, `reuseExistingServer: true`,
|
||||
`timeout: 60_000`, `stdout: "ignore"`, `stderr: "pipe"`.
|
||||
|
||||
Differences, in full: port 1430 / 1440 / 1450; `timezoneId: "Asia/Kolkata"` in calendar
|
||||
(`playwright.config.ts:34`) and mail (`:31`) but not docs, because both anchor their fixture to the
|
||||
browser's local day; `globalSetup` in docs only (`:14`).
|
||||
|
||||
No app configures `toHaveScreenshot`, `snapshotDir` or any visual-regression comparison, and there
|
||||
are zero snapshot baselines in the three. Every screenshot is a named PNG for a person or the docs.
|
||||
|
||||
## The tests directories
|
||||
|
||||
margin-calendar, 10 specs plus `app.ts` (370 lines) and `tsconfig.json`. Geometry and layout
|
||||
(`grid` 26, `compact` 5, `views` 8), input (`keyboard` 14, `interaction` 12, `touch` 22), form
|
||||
factor (`phone` 10), design-system (`legibility` 10), overlays (7), regression (`bugs` 17). The two
|
||||
phone files use `test.use({ viewport, hasTouch, isMobile })` at file scope (`touch.spec.ts:19`,
|
||||
`phone.spec.ts:16`) rather than a Playwright project.
|
||||
|
||||
margin-docs, 19 specs plus `caret.ts` (134), `disk.ts` (144), `saving.ts` (52), `identity.ts` (106).
|
||||
Six of the 19 are `_audit*.spec.ts`, headed "Temporary exploration harness. Deleted once the
|
||||
findings are written down" (`_audit.spec.ts:1`) and still present. The rest are bytes-on-disk claims
|
||||
(`bytes` 11, `color` 7, `export-writes-only-the-pdf` 2, `external-changes` 8), editing (`blocks`,
|
||||
`headings`, `tables`, `clipboard`, `shortcuts`), and smoke (9).
|
||||
|
||||
margin-mail, 24 specs plus `app.ts` (401 lines) and `tsconfig.json`. Roughly one spec per screen,
|
||||
plus `kit.spec.ts` (design system), `keyboard.spec.ts`, `guide.spec.ts` and `guide-shots.spec.ts`
|
||||
(asset generation, not assertion).
|
||||
|
||||
### The helpers each invented
|
||||
|
||||
`tests/app.ts` in calendar and mail is the same file with different measurement functions bolted on.
|
||||
Identical modulo doc comments:
|
||||
|
||||
- `clockAt(hour, minute)` and `MIDDAY` (cal `:41-53`, mail `:46-58`), pinning the clock to the
|
||||
current day at a fixed hour in Asia/Kolkata.
|
||||
- `openApp(page, options)` (cal `:61-90`, mail `:66-90`). Same `addInitScript` body, same
|
||||
`__test-seeded` sentinel so a reload is not silently reset, same `page.clock.setFixedTime`. Only
|
||||
the seed keys differ.
|
||||
- `settle(page)`, two `requestAnimationFrame`s (cal `:105-112`, mail `:99-106`, third copy in docs
|
||||
`tests/caret.ts:46-53`), `box(target)` (cal `:114-124`, mail `:108-118`), and `openDialog(page)`
|
||||
(cal `:288-293`, mail `:188-193`).
|
||||
- `contrastOf(page, selector)` (cal `:306-366`, mail `:219-278`), which composites every translucent
|
||||
background between the element and the page through a 1x1 canvas so `oklch()` and `color-mix()`
|
||||
do not read as transparent.
|
||||
|
||||
App-specific and correctly so: calendar's `gridFit`, `axis`, `blocks`, `headerDates`, `hourY`,
|
||||
`columnX`, `drag`; mail's `rows`, `groups`, `paneMessages`, `paletteRows`, `actionBar`,
|
||||
`checkedRows`, `bodyText`, `toast`, `listScroll`, `place`, `openRow`.
|
||||
|
||||
Two mail helpers are generic and belong in a shared package despite being written for mail:
|
||||
`token(page, name)` (`app.ts:180-185`), reading a CSS custom property off the root, and
|
||||
`failCommands(page, commands)` (`app.ts:343-360`), which uses `page.route` to answer the request for
|
||||
`/src/dev/mockIpc.ts` with a shim that forwards to the real module (`?real`) and rejects the named
|
||||
commands. That is the only way to test a failure path when the backend is in the page rather than on
|
||||
the wire, and it is 18 lines that would work unchanged in any of the four.
|
||||
|
||||
margin-docs' helpers have no sibling: `putCaret`/`caretIsIn` (`caret.ts`), whose 28-line header
|
||||
documents why nothing in the suite presses End to move a caret; `watchDirty`/`dirtyWasShown`
|
||||
(`saving.ts`), a MutationObserver installed before typing so the 500ms autosave cannot be raced; and
|
||||
`installTauriShim`/`change`/`ask` (`disk.ts`), covered below.
|
||||
|
||||
### Screenshot policy
|
||||
|
||||
`page.screenshot` appears 33 times across 16 mail specs, 7 times across 4 docs specs (all in
|
||||
`_audit*`), and never in calendar. Mail writes into `screenshots/`, which is gitignored
|
||||
(`.gitignore:41`), so an ordinary run leaves the tree clean; the 10 pictures that ship inside the
|
||||
bundle go to the committed `public/guide/` and are gated by
|
||||
`test.skip(() => !process.env.GUIDE_SHOTS)` (`guide-shots.spec.ts:19-22`) behind
|
||||
`just guide-shots` (`justfile:28-30`). That two-tier rule is right and only mail has it.
|
||||
|
||||
### Design-system assertions
|
||||
|
||||
`mail/tests/kit.spec.ts` (132 lines) is the only design-system suite. It renders a `#/kit` route in
|
||||
both palettes and shoots it, asserts fixed geometry read from custom properties (`--list-w` is
|
||||
`420px`, a row is 46px, an avatar 30x30), then runs three static source scans with `node:fs`: no hex
|
||||
literal in any stylesheet under `src/ui` or `src/screens`, every `<input>` carries `NO_AUTOFILL` or
|
||||
an explicit `autoComplete`, and no file says "keychain".
|
||||
|
||||
calendar's equivalent is runtime rather than static: `legibility.spec.ts` measures contrast, checks
|
||||
no block reads "Untitled" and checks every block has a non-empty accessible name, over both themes.
|
||||
docs' equivalent is a vitest, not a spec: `src/theme.test.ts` reads three stylesheets (including
|
||||
`node_modules/margin-shared/css/tokens.css`, `:18-22`) plus `index.html`'s pre-bundle boot script
|
||||
and asserts all of them declare the same variable set, because a missing dark variable falls back
|
||||
silently to the warm light value in `:root`. Nothing anywhere asserts anything about icons.
|
||||
|
||||
## src/dev: the invoke stub
|
||||
|
||||
All three apps switch dev mode on the same way, in `src/ipc.ts`:
|
||||
|
||||
```ts
|
||||
export function call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
if (import.meta.env.DEV && !isTauri) {
|
||||
return import("./dev/mockIpc").then((m) => m.mockCall<T>(command, args));
|
||||
}
|
||||
return invoke<T>(command, args);
|
||||
}
|
||||
```
|
||||
|
||||
calendar `ipc.ts:192-197`, docs `ipc.ts:288-292`, mail `ipc.ts:748-752`, byte identical bar the
|
||||
comment. `isTauri` is `"__TAURI_INTERNALS__" in window`, computed once at import time, and all three
|
||||
derive `isDesktop`, `isMacDesktop` and `live()` from it with the same comments verbatim (calendar
|
||||
`:9-40`, docs `:9-40`, mail `:10-34`). There is no environment variable and no dev-only build flag:
|
||||
the switch is "DEV build with no Tauri bridge", so `pnpm dev` in a browser and Playwright get the
|
||||
fixture and the packaged app cannot.
|
||||
|
||||
`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`, "dev mock has no handler for"). There
|
||||
is no type link between a command name, its arguments and its return; calendar casts args to
|
||||
`Record<string, never>` (`:41`) and then to the real type per field. Command counts: calendar 12
|
||||
arms, docs 40, mail roughly 180. State is a mutable copy of the fixture taken at module load, so
|
||||
writes persist for the session and a reload resets (calendar `:11-14`, docs `:48-49`, mail `:72-84`).
|
||||
|
||||
### Fixtures
|
||||
|
||||
`src/dev/fixture.ts` in each. calendar (222 lines) exports `devAccounts`, `devCalendars` and
|
||||
`devInstances(from, to)`, generated from a seed table and anchored to the current week; its header
|
||||
records that it is modelled on a real sync of 12,067 events where 90% came back with no summary.
|
||||
docs (344 lines) exports `devRoots`, `devEntries` (an in-memory folder with real base64 PNG, SVG and
|
||||
PDF bytes at `:208-273`) and path utilities the mock and the app both use. mail (1,572 lines)
|
||||
exports 20 symbols, 13 data tables (`devAccounts` through `devSyncStatus`) and 6 functions
|
||||
(`devDiscover`, `devCert`, `devFiles`, `devContacts`, `groupOf`, `categoryOf`).
|
||||
|
||||
A fourth fixture source in docs has no sibling: `src/markdown/corpus/`, loaded through
|
||||
`import.meta.glob("./*/*.md", { query: "?raw", eager: true })` at `corpus/load.ts:20`. Its header
|
||||
records the bug that motivated the glob, that naming folders explicitly left twenty adversarial
|
||||
files inside the corpus but outside every gate reading it. That is the "add a file, get a test"
|
||||
pattern and it is worth generalising.
|
||||
|
||||
### Dev flags
|
||||
|
||||
localStorage, read defensively inside try/catch. calendar has one, `margincal-dev-empty`
|
||||
(`mockIpc.ts:32-38`); docs has one, `margindocs-dev-no-writing-tools` (`:246-252`); mail has eight,
|
||||
`marginmail-dev-` plus `empty`, `crowd`, `notify`, `imap`, `bridge`, `pending`, `hydrate-fails`,
|
||||
`sync-fails`, read through `firstRun()` (`:220-230`) and a generic `flagged(key)` (`:262-268`). The
|
||||
naming is uniform (`<app>-dev-<thing>`) and the reader is the same eight lines three times over.
|
||||
|
||||
### Faking events, three ways
|
||||
|
||||
This is where the three diverge and where the shared design has to be decided.
|
||||
|
||||
calendar does not fake events at all: `App.tsx:52` is `if (!isTauri) return;` before every `listen`,
|
||||
so in a browser `menu-action`, `auth`, `sync-progress` and `store-changed` never arrive, and
|
||||
anything they drive is unreachable from the Playwright suite.
|
||||
|
||||
mail dispatches window `CustomEvent`s under the same names from inside the mock
|
||||
(`mockIpc.ts:237-241`), with an optional delay so a state that would otherwise last one frame is
|
||||
observable (`narrateFirstSync` at `:637-651` walks a five-step sync over 1,250ms). The app side is
|
||||
`onAppEvent` in `App.tsx:69-77`, seven lines picking `listen` or `window.addEventListener` off
|
||||
`isTauri`. Cheapest correct answer, and confined to mail.
|
||||
|
||||
docs does neither. Its mock exports `external` (`mockIpc.ts:619-696`), a second surface that mutates
|
||||
the fixture the way another program would, behind the app's back, and returns the exact
|
||||
`WatchEvent[]` the Rust watcher would have emitted; putting them on the bus is the caller's job.
|
||||
That caller is `tests/disk.ts:37-133`, which installs a hand-rolled `__TAURI_INTERNALS__` at
|
||||
document start: `invoke`, `transformCallback`, `unregisterCallback`, `runCallback`, the
|
||||
`plugin:event|listen` and `|unlisten` commands, a listener map, `metadata`, `convertFileSrc` and
|
||||
`__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener`. Its comment at `:29-36` says this is
|
||||
deliberately not `@tauri-apps/api/mocks`, which cannot be reached from an init script, and
|
||||
deliberately written to the contract rather than to convenience. `emit` returns a delivery count per
|
||||
event so a test can tell a working subscription from a payload that fell on the floor (`:66-75`).
|
||||
`external` also carries `pauseWrites`/`resumeWrites` (`:679-695`) so a save can be held and a buffer
|
||||
kept dirty instead of racing the 500ms autosave.
|
||||
|
||||
The docs approach is the strictly better one: the app runs the real `@tauri-apps/api`, so the
|
||||
`isTauri` branch that ships is the branch under test, where mail leaves `onAppEvent`'s Tauri arm
|
||||
never exercised in a browser. But it is 97 lines in a test helper, so a person running `pnpm dev` by
|
||||
hand gets no events at all.
|
||||
|
||||
## The Rust side
|
||||
|
||||
margin-mail `src-tauri/fixtures/` is 36 `.eml` files (real messages as they come off the wire, CRLF
|
||||
throughout, half of them not UTF-8), 36 matching `golden/*.txt` and 4 `autoconfig/*.xml`, compiled
|
||||
into the test binary by a `corpus!` macro in `src/fixtures.rs:10-19` that emits one `include_bytes!`
|
||||
const per file plus an `all()` returning every pair, so a sweep over the corpus is one call.
|
||||
`fixtures.rs:58-102` is itself a test: every fixture has a header/body break, no bare LF, a
|
||||
plausible date and a parseable From. Nothing is generated at test time.
|
||||
|
||||
Alongside it, `src/provider/fake.rs` (612 lines, `#![cfg(test)]` at `:15`) is an in-memory mailbox
|
||||
implementing the `Provider` trait: real ids, labels, dates and raw bytes, paging, a history log, and
|
||||
scripted failures via `fail_next` and `withhold_body`. `provider/mod.rs:1-14` records that this is
|
||||
the only reason the sync engine is testable without credentials.
|
||||
|
||||
Nothing comparable exists elsewhere. margin-docs builds its Rust fixture at runtime instead:
|
||||
`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. `git status` is the oracle.
|
||||
|
||||
Conventions differ and both are defensible. calendar and mail put tests in `#[cfg(test)] mod tests`
|
||||
beside the code, 6 files / 79 tests and 54 files / 531 tests; fifteen of mail's are large enough to
|
||||
live in a sibling `<module>/tests.rs` (backup, clips, contacts, decisions, drafts, exports, invites,
|
||||
notify, piles, screener, send, snooze, state, sync, unsubscribe). docs has zero `#[cfg(test)]` and
|
||||
eight integration binaries totalling 4,210 lines, one needing `--test-threads=1` and run as its own
|
||||
CI step (`ci.yml:78`). Eight mail tests carry `#[ignore]`, six of them "hits the network" in
|
||||
`imap/discover.rs`.
|
||||
|
||||
## tsconfig
|
||||
|
||||
The app `tsconfig.json` is `"include": ["src"]` in all four, so `tests/` is excluded by omission
|
||||
rather than by an `exclude` entry, and the colocated `src/**/*.test.ts` files sit inside the app's
|
||||
own type check. `tests/tsconfig.json` exists in the three and is the same nine options each time
|
||||
(ES2022, bundler resolution, strict, noUnusedLocals, noUnusedParameters, skipLibCheck, noEmit) with
|
||||
`"include": [".", "../playwright.config.ts"]`. Mail's adds `"types": ["node"]`. Nothing runs `tsc -p
|
||||
tests/tsconfig.json` in any script or CI job, so the spec files are type checked only by an editor.
|
||||
|
||||
Vitest config lives in `vite.config.ts` under `test:`, all three with
|
||||
`include: ["src/**/*.test.ts"]` and `environment: "node"`. No jsdom, no happy-dom, no
|
||||
`@testing-library/*` anywhere. margin-docs adds `maxWorkers: "50%"` and three 30-second timeouts
|
||||
with a 20-line comment explaining that a CPU-blocking markdown sweep cannot answer vitest's
|
||||
`onTaskUpdate` RPC while it runs.
|
||||
|
||||
## Colocated unit tests: what they are and are not
|
||||
|
||||
They test pure functions in a node environment. No component is rendered, no store is mounted
|
||||
against a DOM, nothing goes near `mockIpc`. Calendar's convention is explicit: a component `X.tsx`
|
||||
gets a sibling `XModel.ts` holding the decisions and `XModel.test.ts` tests that module.
|
||||
`GridModel.test.ts` covers `busyHours`, `heldHours` and `bandAt` over synthetic `Placed` values with
|
||||
`instance: {} as Instance`, because only the times matter; `EventDetailsModel.test.ts` covers
|
||||
popover placement against a fixed `Bounds`; `overlayModel` and `QuickCreateModel` cover date
|
||||
arithmetic and draft construction; `AgendaModel` covers grouping, gap labels and search matching.
|
||||
The odd one out is `EventDetailsHtml.test.ts`, half readability and half hostile input, because the
|
||||
description string comes off the wire from whoever created the event.
|
||||
|
||||
margin-mail's eight follow the same rule. `providers.test.ts` tests the routing decision for an
|
||||
address twice over, by domain and by what discovery found; `useSync.test.ts` the two pure decisions
|
||||
the header and toast make from a `SyncStatus`; `guide.test.ts` link-checks the article library
|
||||
without rendering it; the other five are table checks.
|
||||
|
||||
margin-docs' two named ones are a different genre and the more interesting one. `theme.test.ts` and
|
||||
`width.test.ts` both read source with `node:fs` and assert that two tables not written in the same
|
||||
language agree: theme against three stylesheets and a boot script, width against the command table
|
||||
and the subscription joining them. The other 30 are the markdown bridge and are app-specific.
|
||||
|
||||
None of them snapshot, mock `call()`, or configure coverage, and there is no property-based library
|
||||
(though `typst.test.ts` and the five `adversarial*.test.ts` files are hand-rolled corpus sweeps,
|
||||
which is the same idea).
|
||||
|
||||
## Known-failing specs
|
||||
|
||||
Nothing in any repo records a known failure. The only markers are `guide-shots.spec.ts:19` (an
|
||||
intentional env gate, not a failure) and a `test.fail` at `external-changes.spec.ts:320`, kept so it
|
||||
would turn red the day the defect was fixed. `margin-editor/docs/conventions.md:74` is the standing
|
||||
rule: "Never weaken, skip or delete a test to reach green."
|
||||
|
||||
The four margin-mail browser failures recorded in session memory are written down nowhere in the
|
||||
tree. If they are real that is the gap: either a per-app `tests/known-failures.md` or annotations on
|
||||
the specs, because right now the only record is a chat log.
|
||||
|
||||
## What a shared harness package would contain
|
||||
|
||||
`margin-shared` already exists at `margin/shared` and is consumed by margin and margin-docs by
|
||||
relative `file:` path, and by margin-mail. margin-calendar does not depend on it at all. Adding a
|
||||
`./test` subpath export is the least-friction home; a separate package means a fourth `file:` edge.
|
||||
|
||||
**1. A playwright config factory.** Everything in the three configs bar the port is a default, so
|
||||
the whole file becomes `export default marginPlaywrightConfig({ port: 1450, witnesses: ["src/ipc.ts",
|
||||
"src/dev/mockIpc.ts", "src/dev/fixture.ts"] })`. `witnesses` turns the docs identity check on for
|
||||
every app rather than one, with the factory supplying `globalSetup` so no app wires it. `timezoneId`
|
||||
defaults to `Asia/Kolkata` and docs passes null. Saves roughly 45 lines per app and makes "all three
|
||||
run at 1440x900 with retries 0" a fact rather than a coincidence.
|
||||
|
||||
**2. The dev backend, with a typed command registry.** Replace `switch (command)` with a table keyed
|
||||
by command whose handlers are typed off the DTO types `src/ipc.ts` already declares:
|
||||
|
||||
```ts
|
||||
export const backend = defineBackend({
|
||||
accounts_list: () => (firstRun() ? [] : accounts),
|
||||
thread_view: ({ key }: { key: string }): ThreadView => viewOf(byKey(key)),
|
||||
});
|
||||
export type Command = keyof typeof backend;
|
||||
```
|
||||
|
||||
`defineBackend` returns `{ mockCall, has, commands }`, with `mockCall` throwing the existing "no
|
||||
handler for" error on a miss. The win is that the arg cast at every arm and the `as unknown as T` at
|
||||
every return both disappear, and a command in `dto.rs` with no handler becomes checkable rather than
|
||||
a runtime surprise three screens later. Alongside it: `makeCall(loader)` producing the `call()` in
|
||||
`src/ipc.ts`, already identical in three places, and `devFlag(name)` replacing the three copies of
|
||||
the try/catch localStorage reader.
|
||||
|
||||
**3. The Tauri shim, moved from `tests/` into the shared package and made the default.** Lift
|
||||
`installTauriShim` out of docs' `tests/disk.ts:37-133` unchanged, parameterise the backend module
|
||||
specifier, and expose it both as a Playwright init script (what docs does now) and as a dev entry
|
||||
point so `pnpm dev` in a browser gets real Tauri events too. Then `onAppEvent` (mail
|
||||
`App.tsx:69-77`) is unnecessary and the app runs one code path in both environments. This is the
|
||||
highest-value item in the audit: 97 lines that took real care to get right, written to a contract
|
||||
rather than to convenience, and two of the three apps are the poorer for not having it. With it,
|
||||
`emitEvent(page, name, payload)` and the delivery-count return become shared, and mail's
|
||||
`narrateFirstSync` style of timed multi-step event script becomes a shared `script([...])`.
|
||||
|
||||
**4. Fixture loading.** Two patterns, both generalisable: docs' `import.meta.glob` corpus loader
|
||||
(`corpus/load.ts`) as `loadCorpus(glob)`, and mail's `corpus!` macro (`fixtures.rs:10-19`) as a
|
||||
shared Rust macro crate. Both encode the rule that adding a file puts it inside every sweep. Also
|
||||
shared: `mutableCopy(fixture)` for the clone-at-module-load idiom in all three mocks, and
|
||||
`undoLedger()` for mail's `undoable`/`runUndo` (`mockIpc.ts:292-314`), which any app with an undo
|
||||
toast will want.
|
||||
|
||||
**5. Playwright helpers.** `settle`, `box`, `clockAt`, `MIDDAY`, `openDialog`, `token`,
|
||||
`failCommands`, plus `makeOpenApp({ theme: "marginmail-theme", pane: "marginmail-pane" })` returning
|
||||
an `openApp`, since only the seed keys differ between the two that have one.
|
||||
|
||||
**6. Assertions on tokens and icons.**
|
||||
|
||||
```ts
|
||||
expectNoColourLiterals(root, ["src/ui", "src/screens"]); // mail kit.spec.ts:82-96
|
||||
expectThemesAgree(sheets, bootScript); // docs theme.test.ts
|
||||
expectTokens(page, { "--list-w": "420px" }); // mail kit.spec.ts:62-67
|
||||
expectContrast(page, selector, { min: 4.5 }); // cal legibility.spec.ts
|
||||
expectAccessibleNames(page, selector); // cal legibility.spec.ts
|
||||
expectIconsInSet(root, dirs); // does not exist yet
|
||||
```
|
||||
|
||||
The first two are static scans over source and need no browser, so they can run under vitest in an
|
||||
app with no Playwright, which is how margin gets its first test.
|
||||
|
||||
## What stays per app
|
||||
|
||||
Every measurement helper: calendar's `gridFit`, `axis`, `blocks`, `hourY`, `drag`; mail's `rows`,
|
||||
`groups`, `paneMessages`, `paletteRows`, `actionBar`, `toast`; docs' `putCaret`, `caretIsIn`,
|
||||
`watchDirty`. They read app-specific class names and encode app-specific timing, so sharing them
|
||||
would be indirection over one caller each.
|
||||
|
||||
The fixtures themselves, 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.
|
||||
|
||||
The Rust test convention. Mail's `#[cfg(test)] mod tests` beside the code and docs' integration
|
||||
binaries are both right for their shape, and forcing one on the other buys nothing. The shareable
|
||||
parts there are narrower: the `corpus!` macro and a `TempRepo` builder along the lines of
|
||||
`support/notes_repo.rs`.
|
||||
Reference in new issue
Block a user