diff --git a/src/export/json.test.ts b/src/export/json.test.ts
index c496d91..5bc497b 100644
--- a/src/export/json.test.ts
+++ b/src/export/json.test.ts
@@ -6,6 +6,7 @@ import {
ImportValidationError,
exportFilename,
} from "./json.ts";
+import { addRecurringCost, updatePost, updateRecurringCost } from "../store/actions.ts";
import { createSeedDataset } from "../domain/seed.ts";
import { monthView, yearView } from "../domain/views.ts";
import type { Dataset } from "../domain/types.ts";
@@ -164,21 +165,33 @@ describe("import rejects invalid folder graph", () => {
expect(() => parse(data)).toThrow(ImportValidationError);
});
- test("rejects a blank post name", () => {
+ test.each([
+ ["", "Rent"], [" ", "Rent"], ["Games", ""], ["Games", " "],
+ ])("blank item names (%j, %j) survive saved dataset round trips and v7 import", (postName, billName) => {
const data = populated();
- data.posts[0]!.name = " ";
- expect(() => parse(data)).toThrow(ImportValidationError);
- });
-
- test("rejects a blank recurring-cost name", () => {
- const data = populated();
- data.recurring.push({
- id: "rent", name: " ", order: 3, folderId: null, archived: false,
+ const bill = addRecurringCost(data, {
+ name: "Rent", folderId: null, archived: false,
amount: { amount: 100, currency: "DKK" }, startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar",
splitMode: "percent", splits: [{ postId: data.posts[0]!.id, value: 100, absorbsRemainder: true }],
});
- expect(() => parse(data)).toThrow(ImportValidationError);
+ updatePost(data, data.posts[0]!.id, { name: postName });
+ updateRecurringCost(data, bill.id, { name: billName });
+ expect(parseDatasetJson(exportDatasetJson(data))).toEqual(data);
+ const legacy = JSON.parse(exportDatasetJson(data));
+ legacy.settings.schemaVersion = 7;
+ delete legacy.folders;
+ for (const item of [...legacy.posts, ...legacy.recurring]) delete item.folderId;
+ const imported = parseDatasetJson(JSON.stringify(legacy));
+ expect(imported.posts[0]!.name).toBe(postName);
+ expect(imported.recurring[0]!.name).toBe(billName);
+ expect(imported.folders).toEqual([]);
+ });
+
+ test.each(["", " "])("still rejects blank folder names %j", name => {
+ const data = populated();
+ data.folders = [{ ...folder("home", null, data.posts.length), name }];
+ expect(() => parse(data)).toThrow(/empty name/);
});
});
diff --git a/src/export/json.ts b/src/export/json.ts
index ee68998..6d92bc1 100644
--- a/src/export/json.ts
+++ b/src/export/json.ts
@@ -190,14 +190,14 @@ function requireFolderTree(data: Dataset): void {
requireLocation(folder.parentId, folder.order, `Folder "${folder.id}"`);
}
for (const post of data.posts) {
- if (typeof post?.name !== "string" || !post.name.trim()) {
- throw new ImportValidationError("Post has an empty name");
+ if (typeof post?.name !== "string") {
+ throw new ImportValidationError("Post name must be a string");
}
requireLocation(post?.folderId, post?.order, `Post "${post?.name}"`);
}
for (const cost of data.recurring) {
- if (typeof cost?.name !== "string" || !cost.name.trim()) {
- throw new ImportValidationError("Recurring cost has an empty name");
+ if (typeof cost?.name !== "string") {
+ throw new ImportValidationError("Recurring cost name must be a string");
}
requireLocation(cost?.folderId, cost?.order, `Recurring cost "${cost?.name}"`);
}
diff --git a/src/ui/routes/SettingsRoute.test.tsx b/src/ui/routes/SettingsRoute.test.tsx
new file mode 100644
index 0000000..82dc167
--- /dev/null
+++ b/src/ui/routes/SettingsRoute.test.tsx
@@ -0,0 +1,51 @@
+import { expect, test } from "bun:test";
+import { GlobalRegistrator } from "@happy-dom/global-registrator";
+if (!GlobalRegistrator.isRegistered) GlobalRegistrator.register();
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { Link, MemoryRouter, Route, Routes } from "react-router";
+import { createSeedDataset } from "../../domain/seed.ts";
+import { addRecurringCost } from "../../store/actions.ts";
+import { store } from "../../store/index.ts";
+import { monthView } from "../../domain/views.ts";
+import { BudgetTree } from "../components/BudgetTree.tsx";
+import { SettingsRoute } from "./SettingsRoute.tsx";
+
+test("month bill Edit navigates to Settings, opens and focuses its editor; fragment changes retarget it", () => {
+ const data = createSeedDataset("2026-09");
+ const bill = addRecurringCost(data, {
+ name: "Phone", folderId: null, archived: false,
+ amount: { amount: 30, currency: "DKK" }, startDate: "2026-09",
+ recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar",
+ splitMode: "percent", splits: [{ postId: data.posts[0]!.id, value: 100, absorbsRemainder: true }],
+ });
+ const second = addRecurringCost(data, { ...bill, name: "Internet" });
+ const original = { get: store.get, subscribe: store.subscribe };
+ store.get = () => data;
+ store.subscribe = () => () => {};
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ try {
+ act(() => root.render(