- Global Carbon toast stack replaces scattered inline notices; errors persist until closed. - Settings is split into eight tabs with one Save and a remembered last tab; Numbering embeds the series settings. - New Clients page with a DataTable, shared add/edit modal, structured addresses, GST category (informational), per-client notes and payment terms. The composed address string is kept for back-compat. Deleting a client warns about invoice count; issued invoices keep their own snapshot. - Migration M7 adds the client columns and item_presets; existing clients with a GSTIN are backfilled to registered_regular. - New invoice form: client ComboBox with type-ahead and inline create, client defaults applied on selection, and an Add from preset control. - Rust tests for M7 (fresh and v6 upgrade), client validation and CRUD, presets CRUD; vitest for the toast store and new helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { AUTO_DISMISS_MS, MAX_TOASTS, createToastStore } from "./toastStore";
|
|
|
|
function fakeTimers() {
|
|
let next = 1;
|
|
const pending = new Map<number, { fn: () => void; ms: number }>();
|
|
return {
|
|
setTimer: (fn: () => void, ms: number) => {
|
|
const id = next++;
|
|
pending.set(id, { fn, ms });
|
|
return id;
|
|
},
|
|
clearTimer: (id: unknown) => void pending.delete(id as number),
|
|
fireAll: () => [...pending.entries()].forEach(([id, t]) => (pending.delete(id), t.fn())),
|
|
pending,
|
|
};
|
|
}
|
|
|
|
describe("toast store", () => {
|
|
it("auto-dismisses non-error toasts after the delay", () => {
|
|
const timers = fakeTimers();
|
|
const store = createToastStore(timers);
|
|
store.add("success", "Saved");
|
|
store.add("info", "FYI");
|
|
expect([...timers.pending.values()].map((t) => t.ms)).toEqual([AUTO_DISMISS_MS, AUTO_DISMISS_MS]);
|
|
timers.fireAll();
|
|
expect(store.getSnapshot()).toHaveLength(0);
|
|
});
|
|
|
|
it("keeps errors until they are closed", () => {
|
|
const timers = fakeTimers();
|
|
const store = createToastStore(timers);
|
|
const id = store.add("error", "Failed", "details");
|
|
expect(timers.pending.size).toBe(0);
|
|
timers.fireAll();
|
|
expect(store.getSnapshot()).toEqual([{ id, kind: "error", title: "Failed", subtitle: "details" }]);
|
|
store.dismiss(id);
|
|
expect(store.getSnapshot()).toHaveLength(0);
|
|
});
|
|
|
|
it("dismiss cancels the timer and is idempotent", () => {
|
|
const timers = fakeTimers();
|
|
const store = createToastStore(timers);
|
|
const id = store.add("success", "x");
|
|
store.dismiss(id);
|
|
expect(timers.pending.size).toBe(0);
|
|
store.dismiss(id);
|
|
expect(store.getSnapshot()).toHaveLength(0);
|
|
});
|
|
|
|
it("notifies subscribers with a new snapshot reference per change", () => {
|
|
const store = createToastStore(fakeTimers());
|
|
let calls = 0;
|
|
const off = store.subscribe(() => calls++);
|
|
const before = store.getSnapshot();
|
|
store.add("info", "a");
|
|
expect(calls).toBe(1);
|
|
expect(store.getSnapshot()).not.toBe(before);
|
|
const same = store.getSnapshot();
|
|
store.dismiss(999);
|
|
expect(calls).toBe(1);
|
|
expect(store.getSnapshot()).toBe(same);
|
|
off();
|
|
store.add("info", "b");
|
|
expect(calls).toBe(1);
|
|
});
|
|
|
|
it("caps the stack and drops the oldest first, cancelling its timer", () => {
|
|
const timers = fakeTimers();
|
|
const store = createToastStore(timers);
|
|
for (let i = 0; i < MAX_TOASTS + 2; i++) store.add("success", `t${i}`);
|
|
const titles = store.getSnapshot().map((t) => t.title);
|
|
expect(titles).toHaveLength(MAX_TOASTS);
|
|
expect(titles[0]).toBe("t2");
|
|
expect(timers.pending.size).toBe(MAX_TOASTS);
|
|
});
|
|
|
|
it("clear empties the stack and cancels timers", () => {
|
|
const timers = fakeTimers();
|
|
const store = createToastStore(timers);
|
|
store.add("success", "a");
|
|
store.add("error", "b");
|
|
store.clear();
|
|
expect(store.getSnapshot()).toHaveLength(0);
|
|
expect(timers.pending.size).toBe(0);
|
|
});
|
|
});
|