fix(web): keep review text on a failed save; review round 2
- Review draft sync is a pure helper (format.ts): incoming values are ignored while a save is in flight, and a failed save restores the previous synced value, so the typed text is kept whichever of the rollback and the error callback lands first. Tests cover both orders. - A failed background refetch keeps showing the cached entry instead of the error screen (it unmounted the review box mid-outage). - History restore joins the entry-patch mutation scope. - updatePreferences: read-modify-write in a transaction, row locked on Postgres. - Invalidate auth-status after register; out-of-range rating errors speak in the user's scale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fa59310f3
commit
4608c977b7
7 files changed
+140
-30
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { formatDateRange, formatPartialDate, formatRating, friendlyError, parsePartialDate, rollupLabel, shortDate } from "../src/format.ts";
|
||||
import { draftFailedSave, draftIncoming, draftStartSave, formatDateRange, formatPartialDate, formatRating, friendlyError, parsePartialDate, rollupLabel, shortDate } from "../src/format.ts";
|
||||
|
||||
describe("formatRating", () => {
|
||||
test("5 stars with halves", () => {
|
||||
@@ -72,7 +72,9 @@ describe("friendlyError", () => {
|
||||
test("maps validation reasons to plain copy", () => {
|
||||
expect(friendlyError(v("customFields.Website: not_url"))).toBe("Website needs a full web address, starting with https://.");
|
||||
expect(friendlyError(v("finishedAt: not_date"))).toBe("Finished needs a date like 2025, 2025-03 or 2025-03-14.");
|
||||
expect(friendlyError(v("rating: out_of_range"))).toBe("Rating is out of range. Ratings go from 0 to 100.");
|
||||
expect(friendlyError(v("rating: out_of_range"))).toBe("Pick a rating between 0 and 5 stars.");
|
||||
expect(friendlyError(v("rating: out_of_range"), { ratingScale: "10" })).toBe("Pick a rating between 0 and 10.");
|
||||
expect(friendlyError(v("customFields.Mood: out_of_range"), { ratingScale: "5" })).toBe("Pick a rating for Mood between 0 and 5 stars.");
|
||||
expect(friendlyError(v("title: required"))).toBe("Title can't be empty.");
|
||||
expect(friendlyError(v("review: too_long"))).toBe("Review is too long. Shorten it and try again.");
|
||||
expect(friendlyError(v("password: Too small: expected string to have >=8 characters"))).toBe("Passwords need at least 8 characters.");
|
||||
@@ -94,3 +96,31 @@ describe("friendlyError", () => {
|
||||
test("shortDate", () => {
|
||||
expect(shortDate("2026-10-12T10:00:00")).toBe("12 Oct");
|
||||
});
|
||||
|
||||
describe("review draft sync", () => {
|
||||
const typed = { text: "Second thoughts.", synced: "First thoughts.", saving: false };
|
||||
|
||||
test("a rollback after a failed save keeps the local text (rollback lands first)", () => {
|
||||
let d = draftStartSave(typed); // optimistic cache now says "Second thoughts."
|
||||
d = draftIncoming(d, "Second thoughts.");
|
||||
const was = typed.synced;
|
||||
d = draftIncoming(d, "First thoughts."); // onError rolled the cache back
|
||||
d = draftFailedSave(d, was);
|
||||
expect(d.text).toBe("Second thoughts.");
|
||||
expect(d.synced).toBe("First thoughts."); // so the text still counts as unsaved
|
||||
expect(d.saving).toBe(false);
|
||||
});
|
||||
|
||||
test("a rollback after a failed save keeps the local text (error callback first)", () => {
|
||||
let d = draftStartSave(typed);
|
||||
d = draftFailedSave(d, typed.synced);
|
||||
d = draftIncoming(d, "First thoughts.");
|
||||
expect(d.text).toBe("Second thoughts.");
|
||||
});
|
||||
|
||||
test("a change from elsewhere (Restore) replaces untouched text but not edited text", () => {
|
||||
const clean = { text: "Second thoughts.", synced: "Second thoughts.", saving: false };
|
||||
expect(draftIncoming(clean, "First thoughts.").text).toBe("First thoughts.");
|
||||
expect(draftIncoming(typed, "Something else").text).toBe("Second thoughts.");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user