tests: one fixture for the note grammar, run by the core, the server and the web
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m19s
Desktop (Tauri) / Update manifest (push) Successful in 9s
Android / Kotlin + Rust (APK) (push) Canceled after 11m21s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m19s
Desktop (Tauri) / Update manifest (push) Successful in 9s
Android / Kotlin + Rust (APK) (push) Canceled after 11m21s
The checklist grammar and the #tag rule are implemented three times (derive.rs, checklist.py/tags.py, markdown.ts), and the tag colour twice (colors.ts, DerivedTint.kt). Only Rust and Kotlin had tests. core/testdata/grammar.json now holds one set of cases (task lines, rendered items, tags, standalone-tag lifts and the tint hashes), and every suite reads it. - web: vitest, a dev dependency approved for #5166, with `npm test`. grammar.test.ts runs the fixture, and titles.test.ts pins #5165's palette fix. - ci.yml runs the web tests in the job the image build needs. desktop.yml's verify job runs them too, because the installers embed this frontend and can't see ci.yml's verdict (rule 177). - core: derive.rs reads the fixture. server: tests/test_grammar_fixture.py. - Android keeps its hand-written tint values; its doc now points at the fixture. The server is expected red here, on purpose. tags.py only takes a tag after whitespace and lets it start with a digit or `_`, while the core (the definition) takes any non-tag boundary and needs a letter. So `(#todo)` is a label on the phone and plain text on the server. The fix follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,9 +169,9 @@ export const NOTE_COLOR_LABELS: Record<NoteColor, string> = {
|
||||
// three implementations, and the same reason: a value that disagrees across surfaces
|
||||
// is a bug you cannot unsee and cannot explain.
|
||||
//
|
||||
// The Kotlin side has a unit test pinning the fixture below. THIS SIDE HAS NO
|
||||
// MECHANICAL GUARD — the frontend has no test runner, only `vue-tsc --noEmit`.
|
||||
// If you change anything here, check it against the fixture by hand.
|
||||
// Both sides are pinned to the same values: `core/testdata/grammar.json` (`tint`),
|
||||
// which `notes/grammar.test.ts` reads, and DerivedTintTest.kt, which writes the same
|
||||
// numbers out by hand. Change the hash or the key order and both go red.
|
||||
export const DERIVED_TINT_KEYS: readonly NoteColor[] = NOTE_COLOR_KEYS.filter(
|
||||
(key) => key !== "default",
|
||||
);
|
||||
@@ -235,22 +235,8 @@ export function resolveLabelColor(label: { name: string; color?: string | null }
|
||||
return derivedTint(label.name.toLowerCase());
|
||||
}
|
||||
|
||||
// Fixture — the same names and expected keys the Kotlin test asserts. Kept here as
|
||||
// prose because there is nowhere on this side to assert it. If you change the hash or
|
||||
// the key order, these must still hold on BOTH surfaces.
|
||||
//
|
||||
// The raw hash, over four UUIDs — ids no longer pick a colour, but they are what the
|
||||
// hash itself is pinned by and the Kotlin test still asserts them:
|
||||
//
|
||||
// 00000000-0000-0000-0000-000000000000 0xbe478ed1 purple
|
||||
// 11111111-1111-1111-1111-111111111111 0x3d75cc01 blue
|
||||
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 0xf108e530 orange
|
||||
// f47ac10b-58cc-4372-a567-0e02b2c3d479 0x5b651540 orange
|
||||
//
|
||||
// And the live path — a label, hashing its lowercased NAME:
|
||||
//
|
||||
// todo -> pink grocery -> blue work -> green home -> gray
|
||||
// ideas -> green reading -> gray urgent -> red
|
||||
// The fixture (four UUID hashes and seven tag names) lives in
|
||||
// core/testdata/grammar.json under `tint`.
|
||||
//
|
||||
// Note `work`/`ideas` and `home`/`reading` collide. Nine keys makes that unavoidable
|
||||
// and it is not a bug: colour hints that two tags are distinct, it never claims two
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// The web's half of the shared grammar fixture (core/testdata/grammar.json).
|
||||
//
|
||||
// The same cases run against the core (derive.rs) and the server (checklist.py,
|
||||
// tags.py). Until this file the web copy had no mechanical guard at all — the
|
||||
// frontend's only CI lane was `vue-tsc --noEmit` — so a drift here showed up as a
|
||||
// checklist or a tag colour that differed in the browser and nowhere else.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fixture from "../../../core/testdata/grammar.json";
|
||||
import { DERIVED_TINT_KEYS, derivedTint, resolveLabelColor, tintHash } from "./colors";
|
||||
import { parseInline, parseTaskLine, renderTaskLine } from "./markdown";
|
||||
|
||||
describe("checklist lines", () => {
|
||||
it.each(fixture.task_lines)("$line", ({ line, item }) => {
|
||||
expect(parseTaskLine(line)).toEqual(item);
|
||||
});
|
||||
|
||||
it.each(fixture.rendered_items)("renders $line", ({ text, checked, line }) => {
|
||||
expect(renderTaskLine(text, checked)).toBe(line);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tags", () => {
|
||||
// The renderer finds tags; it does not collect them. Collected here line by line
|
||||
// and deduped the way the derivers dedupe — first spelling wins, case-insensitively.
|
||||
function tagsOf(body: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const line of body.split("\n")) {
|
||||
for (const token of parseInline(line)) {
|
||||
if (token.type !== "tag") continue;
|
||||
if (!out.some((t) => t.toLowerCase() === token.value.toLowerCase())) out.push(token.value);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
it.each(fixture.tags)("$body", ({ body, tags }) => {
|
||||
expect(tagsOf(body)).toEqual(tags);
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived tag colour", () => {
|
||||
it("uses the same key order as Android", () => {
|
||||
expect([...DERIVED_TINT_KEYS]).toEqual(fixture.tint.keys);
|
||||
});
|
||||
|
||||
it.each(fixture.tint.hashes)("hashes $id", ({ id, hash, color }) => {
|
||||
expect(tintHash(id)).toBe(hash);
|
||||
expect(derivedTint(id)).toBe(color);
|
||||
});
|
||||
|
||||
it.each(fixture.tint.names)("colours #$name", ({ name, color }) => {
|
||||
expect(resolveLabelColor({ name })).toBe(color);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
vi.mock("../adapters", () => ({ repo: { notes: { titles: vi.fn() } } }));
|
||||
|
||||
import { repo } from "../adapters";
|
||||
import { useTitlesStore } from "./titles";
|
||||
|
||||
describe("the palette's note titles", () => {
|
||||
beforeEach(() => setActivePinia(createPinia()));
|
||||
|
||||
// #5165: the list loaded once per session, so a note written after the palette
|
||||
// first opened could not be found by name until the page reloaded.
|
||||
it("sees a note written since the last open", async () => {
|
||||
vi.mocked(repo.notes.titles)
|
||||
.mockResolvedValueOnce([{ id: "a", title: "Groceries" }])
|
||||
.mockResolvedValueOnce([
|
||||
{ id: "a", title: "Groceries" },
|
||||
{ id: "b", title: "Trip" },
|
||||
]);
|
||||
const titles = useTitlesStore();
|
||||
|
||||
await titles.refresh();
|
||||
expect(titles.items.map((t) => t.title)).toEqual(["Groceries"]);
|
||||
|
||||
await titles.refresh();
|
||||
expect(titles.items.map((t) => t.title)).toEqual(["Groceries", "Trip"]);
|
||||
});
|
||||
|
||||
it("keeps the last list when a refresh fails", async () => {
|
||||
vi.mocked(repo.notes.titles)
|
||||
.mockResolvedValueOnce([{ id: "a", title: "Groceries" }])
|
||||
.mockRejectedValueOnce(new Error("offline"));
|
||||
const titles = useTitlesStore();
|
||||
|
||||
await titles.refresh();
|
||||
await expect(titles.refresh()).rejects.toThrow("offline");
|
||||
expect(titles.items.map((t) => t.title)).toEqual(["Groceries"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user