The renderer has no tests at all; typecheck is the only gate on UI behaviour #212

Closed
opened 2026-09-12 20:29:38 -04:00 by Salastil · 1 comment
Owner

The daemon has 555 #[test] functions across 109 files. The window has none, and no test dependency of any kind — package.json has dev, build, start, typecheck, pack, daemon, deploy, and nothing that runs a test. typecheck is the entire automated gate on everything the user actually looks at.

This is not theoretical. In #211 I windowed the emoji picker's grid, which made the picker mount short and grow to its 360px cap a frame later, while its placement was computed once against the short measurement. 134 pixels of the picker opened below the bottom of the screen, permanently. It shipped. It was caught by the user opening the picker, and nothing in the project could have caught it earlier.

Two tiers, and they are not the same job

1. Unit tests for the pure logic — cheap, and the obvious gap.

There are ~79 exported functions across fourteen lib/ modules that are plain data-in/data-out and need no DOM at all:

module exported fns module exported fns
format.ts 18 mentions.ts 5
util.ts 17 emotecache.ts 5
groups.ts 17 searchfilters.ts 3
categories.ts 3 buffermenu.ts 2
kickwatch.ts 2 completion.ts 2
sniff.ts 2 presence.ts 2
networks.ts 1

These are exactly the kind of thing the daemon side already tests heavily — token parsing, URL rewriting, buffer-name splitting, filter predicates, delta computation. kickwatch.ts's watchDelta and emotecache.ts's kickEmoteId are pure functions with obvious edge cases and no coverage.

Vitest is the natural fit: the project already builds with Vite via electron-vite, so it needs one devDependency and a test script, with no new toolchain.

2. A real browser for layout invariants — and only this tier would have caught the bug above.

Worth being explicit, because it is the trap: jsdom would not have caught it. jsdom has no layout engine, so getBoundingClientRect() returns zeros, every element is 0x0, and the clamp arithmetic that failed is vacuously satisfied. A component test under jsdom would have passed while the picker hung off the screen.

Catching that needs real layout. The method is already proven — it is how the regression was diagnosed and the fix verified:

moho.AppImage --remote-debugging-port=9222
# then drive CDP: open the picker, read getBoundingClientRect at
# 80ms / 400ms / 1.5s / 3.5s, assert the rect is inside the viewport

That produced, on the broken build and the fixed one:

before:  rect 1514,805  320x359  bottom 1164 / viewport 1030   OUTSIDE
after:   rect 1514,624  320x360  bottom  984 / viewport 1030   INSIDE

Sampling at several delays matters: the whole bug was that the first measurement was not the final one, so a single check straight after open would have passed.

A small number of these is enough — they are slow and need a built app. The invariant worth asserting first is general rather than picker-specific: nothing that opens on top of the UI may extend outside the viewport. That covers the emoji picker, the context menus, the conversation menu, and whatever is added later, and it is the class of bug this just shipped.

Suggested shape

  • add Vitest plus a test script, and start with the pure lib/ modules, weighted toward what has bitten: formatting, emote URL rewriting, buffer naming
  • a test:ui script that packs, launches with a debugging port, and asserts the popup-inside-viewport invariant across every popup
  • npm run typecheck && npm test in .github/workflows/rolling-release.yml, where the Rust tests already gate the release — the same structural property, extended to the half of the product that currently has none
The daemon has **555 `#[test]` functions across 109 files**. The window has **none**, and no test dependency of any kind — `package.json` has `dev`, `build`, `start`, `typecheck`, `pack`, `daemon`, `deploy`, and nothing that runs a test. `typecheck` is the entire automated gate on everything the user actually looks at. This is not theoretical. In #211 I windowed the emoji picker's grid, which made the picker mount short and grow to its 360px cap a frame later, while its placement was computed once against the short measurement. **134 pixels of the picker opened below the bottom of the screen, permanently.** It shipped. It was caught by the user opening the picker, and nothing in the project could have caught it earlier. ## Two tiers, and they are not the same job **1. Unit tests for the pure logic — cheap, and the obvious gap.** There are ~79 exported functions across fourteen `lib/` modules that are plain data-in/data-out and need no DOM at all: | module | exported fns | | module | exported fns | | --- | --- | --- | --- | --- | | `format.ts` | 18 | | `mentions.ts` | 5 | | `util.ts` | 17 | | `emotecache.ts` | 5 | | `groups.ts` | 17 | | `searchfilters.ts` | 3 | | `categories.ts` | 3 | | `buffermenu.ts` | 2 | | `kickwatch.ts` | 2 | | `completion.ts` | 2 | | `sniff.ts` | 2 | | `presence.ts` | 2 | | `networks.ts` | 1 | | | | These are exactly the kind of thing the daemon side already tests heavily — token parsing, URL rewriting, buffer-name splitting, filter predicates, delta computation. `kickwatch.ts`'s `watchDelta` and `emotecache.ts`'s `kickEmoteId` are pure functions with obvious edge cases and no coverage. Vitest is the natural fit: the project already builds with Vite via electron-vite, so it needs one devDependency and a `test` script, with no new toolchain. **2. A real browser for layout invariants — and only this tier would have caught the bug above.** Worth being explicit, because it is the trap: **jsdom would not have caught it.** jsdom has no layout engine, so `getBoundingClientRect()` returns zeros, every element is 0x0, and the clamp arithmetic that failed is vacuously satisfied. A component test under jsdom would have passed while the picker hung off the screen. Catching that needs real layout. The method is already proven — it is how the regression was diagnosed and the fix verified: ``` moho.AppImage --remote-debugging-port=9222 # then drive CDP: open the picker, read getBoundingClientRect at # 80ms / 400ms / 1.5s / 3.5s, assert the rect is inside the viewport ``` That produced, on the broken build and the fixed one: ``` before: rect 1514,805 320x359 bottom 1164 / viewport 1030 OUTSIDE after: rect 1514,624 320x360 bottom 984 / viewport 1030 INSIDE ``` Sampling at several delays matters: the whole bug was that the first measurement was not the final one, so a single check straight after open would have passed. A small number of these is enough — they are slow and need a built app. The invariant worth asserting first is general rather than picker-specific: **nothing that opens on top of the UI may extend outside the viewport.** That covers the emoji picker, the context menus, the conversation menu, and whatever is added later, and it is the class of bug this just shipped. ## Suggested shape - add Vitest plus a `test` script, and start with the pure `lib/` modules, weighted toward what has bitten: formatting, emote URL rewriting, buffer naming - a `test:ui` script that packs, launches with a debugging port, and asserts the popup-inside-viewport invariant across every popup - `npm run typecheck && npm test` in `.github/workflows/rolling-release.yml`, where the Rust tests already gate the release — the same structural property, extended to the half of the product that currently has none
Salastil added the feature label 2026-09-12 20:29:38 -04:00
Salastil added this to the v1.0 milestone 2026-10-02 00:00:02 -04:00
Author
Owner

Finished. Master's rolling release passed: https://github.com/Moho-Chat/moho/actions/runs/37167188355

The renderer's unit tests and the layout check run in CI before anything is packaged. The build published moho-117bbc0 (AppImage, deb, exe).

The run before it failed on clippy (two warnings that -D warnings turns into errors), and that was fixed first.

Finished. Master's rolling release passed: https://github.com/Moho-Chat/moho/actions/runs/37167188355 The renderer's unit tests and the layout check run in CI before anything is packaged. The build published moho-117bbc0 (AppImage, deb, exe). The run before it failed on clippy (two warnings that `-D warnings` turns into errors), and that was fixed first. - tests: https://git.salastil.com/Salastil/moho/commit/53a6f92 - clippy fix: https://git.salastil.com/Salastil/nobilis/commit/2999f30
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#212