After "Let AI fill this quote" the page re-pulls the quote's lines and merges them with mergeItemsAfterAiRun: rows unchanged since the last load take the server copy (so fills show), rows with unsaved changes stay as typed, with a toast saying how many were kept. Previously onDone ran fetchData, which replaced every row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { mergeItemsAfterAiRun } from "./quote-assistant-merge";
|
|
|
|
type Row = { id: string; description: string; cost_price: number; ai_fill?: unknown };
|
|
|
|
const row = (id: string, over: Partial<Row> = {}): Row => ({
|
|
id,
|
|
description: `item ${id}`,
|
|
cost_price: 0,
|
|
...over,
|
|
});
|
|
|
|
describe("mergeItemsAfterAiRun", () => {
|
|
it("takes the server row for lines the rep has not touched", () => {
|
|
const baseline = [row("a"), row("b")];
|
|
const local = [row("a"), row("b")];
|
|
const fresh = [row("a", { cost_price: 10, ai_fill: { status: "filled" } }), row("b")];
|
|
|
|
const out = mergeItemsAfterAiRun(local, baseline, fresh);
|
|
|
|
expect(out.items).toEqual(fresh);
|
|
expect(out.keptDirty).toEqual([]);
|
|
});
|
|
|
|
it("keeps a row with unsaved typing as the rep left it", () => {
|
|
const baseline = [row("a"), row("b")];
|
|
const local = [row("a", { description: "rep is typing" }), row("b")];
|
|
const fresh = [
|
|
row("a", { cost_price: 10, ai_fill: { status: "filled" } }),
|
|
row("b", { cost_price: 7, ai_fill: { status: "filled" } }),
|
|
];
|
|
|
|
const out = mergeItemsAfterAiRun(local, baseline, fresh);
|
|
|
|
expect(out.items[0]).toEqual(local[0]);
|
|
expect(out.items[1]).toEqual(fresh[1]);
|
|
expect(out.keptDirty).toEqual(["a"]);
|
|
});
|
|
|
|
it("follows the server's row set and order: new rows appear, deleted rows go", () => {
|
|
const baseline = [row("a"), row("b")];
|
|
const local = [row("a"), row("b")];
|
|
const fresh = [row("c"), row("a")];
|
|
|
|
const out = mergeItemsAfterAiRun(local, baseline, fresh);
|
|
|
|
expect(out.items.map((r) => r.id)).toEqual(["c", "a"]);
|
|
});
|
|
|
|
it("treats a local row with no baseline as dirty (added locally, never loaded)", () => {
|
|
const local = [row("a", { description: "x" })];
|
|
const fresh = [row("a")];
|
|
const out = mergeItemsAfterAiRun(local, [], fresh);
|
|
expect(out.items[0]).toEqual(local[0]);
|
|
expect(out.keptDirty).toEqual(["a"]);
|
|
});
|
|
|
|
it("compares values, not object identity or key order", () => {
|
|
const baseline = [{ id: "a", cost_price: 0, description: "d", ai_fill: { status: "no_match" } }];
|
|
const local = [{ description: "d", id: "a", ai_fill: { status: "no_match" }, cost_price: 0 }];
|
|
const fresh = [{ id: "a", cost_price: 5, description: "d", ai_fill: { status: "filled" } }];
|
|
const out = mergeItemsAfterAiRun(local, baseline, fresh);
|
|
expect(out.items).toEqual(fresh);
|
|
});
|
|
});
|