Persist project device synchronization
This commit is contained in:
@@ -18,6 +18,7 @@ function projectDevice() {
|
||||
totalPower: 0.192,
|
||||
cosPhi: 0.95,
|
||||
remark: "DALI",
|
||||
voltageV: 230,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,8 +56,6 @@ function linkedRow() {
|
||||
|
||||
function createService() {
|
||||
const rows = [linkedRow()];
|
||||
const updates: Array<Record<string, unknown>> = [];
|
||||
let transactionCalls = 0;
|
||||
const service = new ProjectDeviceSyncService({
|
||||
projectDeviceRepository: {
|
||||
async findById() {
|
||||
@@ -65,99 +64,104 @@ function createService() {
|
||||
},
|
||||
deviceRowRepository: {
|
||||
async listLinkedByProjectDevice() {
|
||||
return rows.filter((row) => row.linkedProjectDeviceId === "pd1") as never;
|
||||
return rows as never;
|
||||
},
|
||||
async listLinkStatesByIds() {
|
||||
return rows.map((row) => ({ id: row.id, linkedProjectDeviceId: row.linkedProjectDeviceId })) as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowSyncStore: {
|
||||
updateLinkedRows(_projectDeviceId, changes) {
|
||||
transactionCalls += 1;
|
||||
for (const change of changes) {
|
||||
updates.push({ rowId: change.rowId, ...change.input });
|
||||
Object.assign(rows.find((row) => row.id === change.rowId)!, change.input);
|
||||
}
|
||||
},
|
||||
disconnectLinkedRows(_projectDeviceId, rowIds) {
|
||||
transactionCalls += 1;
|
||||
for (const rowId of rowIds) {
|
||||
const row = rows.find((entry) => entry.id === rowId)!;
|
||||
updates.push({ rowId, linkedProjectDeviceId: undefined, displayName: row.displayName, quantity: row.quantity });
|
||||
Object.assign(row, { linkedProjectDeviceId: null });
|
||||
}
|
||||
},
|
||||
reconnectRows(projectDeviceId, rowIds) {
|
||||
transactionCalls += 1;
|
||||
for (const rowId of rowIds) {
|
||||
Object.assign(rows.find((row) => row.id === rowId)!, { linkedProjectDeviceId: projectDeviceId });
|
||||
}
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
return { service, rows, updates, transactionCalls: () => transactionCalls };
|
||||
return { service, rows };
|
||||
}
|
||||
|
||||
describe("project device synchronization", () => {
|
||||
it("shows field differences and local overrides without changing rows", async () => {
|
||||
const { service, updates } = createService();
|
||||
const { service, rows } = createService();
|
||||
const before = structuredClone(rows);
|
||||
const preview = await service.getPreview("p1", "pd1");
|
||||
|
||||
assert.equal(updates.length, 0);
|
||||
assert.deepEqual(rows, before);
|
||||
assert.equal(preview.rows[0].equipmentIdentifier, "-1F1");
|
||||
assert.equal(preview.rows[0].differences.some((difference) => difference.field === "quantity"), true);
|
||||
assert.equal(
|
||||
preview.rows[0].differences.find((difference) => difference.field === "displayName")?.isOverridden,
|
||||
preview.rows[0].differences.some(
|
||||
(difference) => difference.field === "quantity"
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
preview.rows[0].differences.find(
|
||||
(difference) => difference.field === "displayName"
|
||||
)?.isOverridden,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("synchronizes only explicitly selected fields", async () => {
|
||||
const { service, rows, transactionCalls } = createService();
|
||||
await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
|
||||
it("creates a deterministic command for selected synchronization fields", async () => {
|
||||
const { service, rows } = createService();
|
||||
const before = structuredClone(rows);
|
||||
const command = await service.createSynchronizeCommand(
|
||||
"p1",
|
||||
"pd1",
|
||||
["row1"],
|
||||
["quantity", "powerPerUnit"]
|
||||
);
|
||||
|
||||
assert.equal(rows[0].quantity, 6);
|
||||
assert.equal(rows[0].powerPerUnit, 0.04);
|
||||
assert.equal(rows[0].displayName, "Local display name");
|
||||
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName"]);
|
||||
assert.equal(transactionCalls(), 1);
|
||||
assert.deepEqual(rows, before);
|
||||
assert.equal(command.payload.operation, "synchronize");
|
||||
assert.equal(command.payload.rows[0].expected.quantity, 2);
|
||||
assert.equal(command.payload.rows[0].target.quantity, 6);
|
||||
assert.equal(command.payload.rows[0].target.powerPerUnit, 0.04);
|
||||
assert.equal(
|
||||
command.payload.rows[0].target.displayName,
|
||||
"Local display name"
|
||||
);
|
||||
assert.deepEqual(
|
||||
JSON.parse(command.payload.rows[0].target.overriddenFields ?? "[]"),
|
||||
["displayName"]
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects rows that are not linked to the selected project device", async () => {
|
||||
const { service } = createService();
|
||||
await assert.rejects(
|
||||
() => service.synchronize("p1", "pd1", ["other-row"], ["quantity"]),
|
||||
() =>
|
||||
service.createSynchronizeCommand(
|
||||
"p1",
|
||||
"pd1",
|
||||
["other-row"],
|
||||
["quantity"]
|
||||
),
|
||||
/not linked/
|
||||
);
|
||||
});
|
||||
|
||||
it("disconnects selected rows without changing local values", async () => {
|
||||
const { service, updates } = createService();
|
||||
await service.disconnect("p1", "pd1", ["row1"]);
|
||||
it("creates a link-only disconnect command", async () => {
|
||||
const { service } = createService();
|
||||
const command = await service.createDisconnectCommand(
|
||||
"p1",
|
||||
"pd1",
|
||||
["row1"]
|
||||
);
|
||||
|
||||
assert.equal(updates[0].linkedProjectDeviceId, undefined);
|
||||
assert.equal(updates[0].displayName, "Local display name");
|
||||
assert.equal(updates[0].quantity, 2);
|
||||
const assignment = command.payload.rows[0];
|
||||
assert.equal(command.payload.operation, "disconnect");
|
||||
assert.equal(assignment.expected.linkedProjectDeviceId, "pd1");
|
||||
assert.equal(assignment.target.linkedProjectDeviceId, null);
|
||||
assert.equal(
|
||||
assignment.target.displayName,
|
||||
assignment.expected.displayName
|
||||
);
|
||||
assert.equal(assignment.target.quantity, assignment.expected.quantity);
|
||||
});
|
||||
|
||||
it("restores synchronized values and override markers", async () => {
|
||||
const { service, rows } = createService();
|
||||
const result = await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
|
||||
|
||||
await service.restore("p1", "pd1", result.undo.rows);
|
||||
|
||||
assert.equal(rows[0].quantity, 2);
|
||||
assert.equal(rows[0].powerPerUnit, 0.03);
|
||||
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName", "quantity"]);
|
||||
it("does not create no-op synchronization commands", async () => {
|
||||
const { service } = createService();
|
||||
await assert.rejects(
|
||||
() =>
|
||||
service.createSynchronizeCommand(
|
||||
"p1",
|
||||
"pd1",
|
||||
["row1"],
|
||||
["category"]
|
||||
),
|
||||
/at least one row/
|
||||
);
|
||||
});
|
||||
|
||||
it("reconnects rows only while they remain disconnected", async () => {
|
||||
const { service, rows } = createService();
|
||||
const result = await service.disconnect("p1", "pd1", ["row1"]);
|
||||
|
||||
await service.reconnect("p1", "pd1", result.undo.rowIds);
|
||||
|
||||
assert.equal(rows[0].linkedProjectDeviceId, "pd1");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user