test(transaction): add deferred flush and conflict-scope coverage
This commit is contained in:
@@ -122,4 +122,58 @@ describe("TransactionLedger", () => {
|
||||
expect(logicalBefore.get(live.id)?.strokeColor).toBe("#000000");
|
||||
expect(logicalAfter.get(live.id)?.strokeColor).toBe("#ff006e");
|
||||
});
|
||||
|
||||
it("applies per-prop conflict handling and supports element-scope skip", () => {
|
||||
const ledger = new TransactionLedger();
|
||||
const baseline = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
});
|
||||
const target = {
|
||||
...baseline,
|
||||
strokeColor: "#ff006e",
|
||||
backgroundColor: "#ffd8a8",
|
||||
version: baseline.version + 1,
|
||||
};
|
||||
const live = {
|
||||
...target,
|
||||
strokeColor: "#3a86ff",
|
||||
backgroundColor: "#ffd8a8",
|
||||
version: target.version + 1,
|
||||
};
|
||||
|
||||
ledger.recordStep(arrayToMap([baseline]), arrayToMap([target]));
|
||||
|
||||
const propScope = ledger.buildSyntheticSnapshots(
|
||||
arrayToMap([live]),
|
||||
DEFAULT_TRANSACTION_MERGE_POLICY,
|
||||
);
|
||||
expect(propScope.logicalBefore.get(live.id)?.strokeColor).toBe("#3a86ff");
|
||||
expect(propScope.logicalAfter.get(live.id)?.strokeColor).toBe("#3a86ff");
|
||||
expect(propScope.logicalBefore.get(live.id)?.backgroundColor).toBe(
|
||||
"#ffffff",
|
||||
);
|
||||
expect(propScope.logicalAfter.get(live.id)?.backgroundColor).toBe(
|
||||
"#ffd8a8",
|
||||
);
|
||||
|
||||
const elementScope = ledger.buildSyntheticSnapshots(arrayToMap([live]), {
|
||||
...DEFAULT_TRANSACTION_MERGE_POLICY,
|
||||
conflictScope: "element",
|
||||
});
|
||||
expect(elementScope.logicalBefore.get(live.id)?.strokeColor).toBe(
|
||||
"#3a86ff",
|
||||
);
|
||||
expect(elementScope.logicalAfter.get(live.id)?.strokeColor).toBe(
|
||||
"#3a86ff",
|
||||
);
|
||||
expect(elementScope.logicalBefore.get(live.id)?.backgroundColor).toBe(
|
||||
"#ffd8a8",
|
||||
);
|
||||
expect(elementScope.logicalAfter.get(live.id)?.backgroundColor).toBe(
|
||||
"#ffd8a8",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,6 +291,75 @@ describe("TransactionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defers isolated transaction commit until store is idle", async () => {
|
||||
const transactionElement = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "tx-idle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const userElement = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "user-idle",
|
||||
x: 300,
|
||||
y: 0,
|
||||
});
|
||||
const store = h.store as {
|
||||
scheduleAction: (
|
||||
action: (typeof CaptureUpdateAction)[keyof typeof CaptureUpdateAction],
|
||||
) => void;
|
||||
hasPendingIsolatedIncrements: () => boolean;
|
||||
};
|
||||
|
||||
setSceneBaseline([transactionElement, userElement]);
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
|
||||
const session = h.app.transactionManager.open();
|
||||
await session.apply(async () => {
|
||||
applyElementUpdate(transactionElement.id, { x: 180 }, "NEVER");
|
||||
});
|
||||
|
||||
// Keep store non-idle so isolated increment cannot flush immediately.
|
||||
act(() => {
|
||||
store.scheduleAction(CaptureUpdateAction.EVENTUALLY);
|
||||
});
|
||||
|
||||
const summary = commitSession(session);
|
||||
expect(summary.historyCommitted).toBe(true);
|
||||
expect(store.hasPendingIsolatedIncrements()).toBe(true);
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
|
||||
applyElementUpdate(userElement.id, { y: 220 }, "IMMEDIATELY");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.hasPendingIsolatedIncrements()).toBe(false);
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
});
|
||||
|
||||
let liveTxElement = getElement(transactionElement.id)!;
|
||||
let liveUserElement = getElement(userElement.id)!;
|
||||
expect(liveTxElement.x).toBe(180);
|
||||
expect(liveUserElement.y).toBe(220);
|
||||
|
||||
// Latest entry should still be the isolated transaction increment.
|
||||
act(() => {
|
||||
Keyboard.undo();
|
||||
});
|
||||
await waitFor(() => {
|
||||
liveTxElement = getElement(transactionElement.id)!;
|
||||
expect(liveTxElement.x).toBe(transactionElement.x);
|
||||
expect(getElement(userElement.id)?.y).toBe(220);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
Keyboard.undo();
|
||||
});
|
||||
await waitFor(() => {
|
||||
liveUserElement = getElement(userElement.id)!;
|
||||
expect(liveUserElement.y).toBe(userElement.y);
|
||||
});
|
||||
});
|
||||
|
||||
it("undoes transaction-created elements without rolling back user history entries", async () => {
|
||||
const base = API.createElement({
|
||||
type: "rectangle",
|
||||
|
||||
Reference in New Issue
Block a user