744 lines
20 KiB
TypeScript
744 lines
20 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
deserializeProjectCommand,
|
|
serializeProjectCommand,
|
|
type SerializedProjectCommand,
|
|
} from "../src/domain/models/project-command.model.js";
|
|
import {
|
|
assertCircuitUpdateProjectCommand,
|
|
createCircuitUpdateProjectCommand,
|
|
} from "../src/domain/models/circuit-project-command.model.js";
|
|
import {
|
|
assertCircuitDeviceRowUpdateProjectCommand,
|
|
createCircuitDeviceRowUpdateProjectCommand,
|
|
} from "../src/domain/models/circuit-device-row-project-command.model.js";
|
|
import {
|
|
assertCircuitDeviceRowInsertProjectCommand,
|
|
createCircuitDeviceRowDeleteProjectCommand,
|
|
createCircuitDeviceRowInsertProjectCommand,
|
|
} from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
|
import {
|
|
assertCircuitDeviceRowMoveProjectCommand,
|
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
|
createCircuitDeviceRowMoveProjectCommand,
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
|
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
|
import {
|
|
assertCircuitInsertProjectCommand,
|
|
createCircuitDeleteProjectCommand,
|
|
createCircuitInsertProjectCommand,
|
|
} from "../src/domain/models/circuit-structure-project-command.model.js";
|
|
import {
|
|
assertCircuitSectionReorderProjectCommand,
|
|
createCircuitSectionReorderProjectCommand,
|
|
} from "../src/domain/models/circuit-section-reorder-project-command.model.js";
|
|
import {
|
|
assertCircuitSectionRenumberProjectCommand,
|
|
createCircuitSectionRenumberProjectCommand,
|
|
} from "../src/domain/models/circuit-section-renumber-project-command.model.js";
|
|
import {
|
|
assertProjectDeviceRowSyncProjectCommand,
|
|
createProjectDeviceRowSyncProjectCommand,
|
|
type ProjectDeviceSyncRowSnapshot,
|
|
} from "../src/domain/models/project-device-row-sync-project-command.model.js";
|
|
|
|
describe("serialized project commands", () => {
|
|
it("round-trips a versioned command envelope", () => {
|
|
const command: SerializedProjectCommand = {
|
|
schemaVersion: 1,
|
|
type: "circuit.update",
|
|
payload: {
|
|
circuitId: "circuit-1",
|
|
changes: {
|
|
displayName: "Werkstatt",
|
|
protectionRatedCurrent: 16,
|
|
isReserve: false,
|
|
remark: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.deepEqual(
|
|
deserializeProjectCommand(serializeProjectCommand(command)),
|
|
command
|
|
);
|
|
});
|
|
|
|
it("rejects values that JSON would lose or silently change", () => {
|
|
const invalidPayloads: unknown[] = [
|
|
{ value: undefined },
|
|
{ value: Number.NaN },
|
|
{ value: Number.POSITIVE_INFINITY },
|
|
{ value: new Date() },
|
|
];
|
|
|
|
for (const payload of invalidPayloads) {
|
|
assert.throws(() =>
|
|
serializeProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "test.invalid",
|
|
payload: payload as never,
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects cyclic payloads before persistence", () => {
|
|
const payload: Record<string, unknown> = {};
|
|
payload.self = payload;
|
|
|
|
assert.throws(
|
|
() =>
|
|
serializeProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "test.cyclic",
|
|
payload: payload as never,
|
|
}),
|
|
/must not contain cycles/
|
|
);
|
|
});
|
|
|
|
it("rejects malformed serialized envelopes", () => {
|
|
assert.throws(
|
|
() =>
|
|
deserializeProjectCommand(
|
|
JSON.stringify({ schemaVersion: 0, type: "", payload: null })
|
|
),
|
|
/schema version/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit update project commands", () => {
|
|
it("creates a typed command with explicit null clearing semantics", () => {
|
|
const command = createCircuitUpdateProjectCommand("circuit-1", {
|
|
displayName: null,
|
|
cableLength: 12.5,
|
|
isReserve: false,
|
|
});
|
|
|
|
assert.doesNotThrow(() => assertCircuitUpdateProjectCommand(command));
|
|
assert.deepEqual(command.payload.changes, [
|
|
{ field: "displayName", value: null },
|
|
{ field: "cableLength", value: 12.5 },
|
|
{ field: "isReserve", value: false },
|
|
]);
|
|
});
|
|
|
|
it("rejects empty, duplicate and invalid field changes", () => {
|
|
assert.throws(
|
|
() => createCircuitUpdateProjectCommand("circuit-1", {}),
|
|
/at least one change/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertCircuitUpdateProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "circuit.update",
|
|
payload: {
|
|
circuitId: "circuit-1",
|
|
changes: [
|
|
{ field: "displayName", value: "A" },
|
|
{ field: "displayName", value: "B" },
|
|
],
|
|
},
|
|
}),
|
|
/duplicate field/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitUpdateProjectCommand("circuit-1", {
|
|
voltage: 0,
|
|
}),
|
|
/outside its allowed range/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit device-row update project commands", () => {
|
|
it("creates typed nullable device-row changes", () => {
|
|
const command = createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
|
roomId: null,
|
|
quantity: 2,
|
|
cosPhi: 0.9,
|
|
});
|
|
|
|
assert.doesNotThrow(() =>
|
|
assertCircuitDeviceRowUpdateProjectCommand(command)
|
|
);
|
|
assert.deepEqual(command.payload.changes, [
|
|
{ field: "roomId", value: null },
|
|
{ field: "quantity", value: 2 },
|
|
{ field: "cosPhi", value: 0.9 },
|
|
]);
|
|
});
|
|
|
|
it("rejects invalid device-row values", () => {
|
|
assert.throws(
|
|
() => createCircuitDeviceRowUpdateProjectCommand("row-1", {}),
|
|
/at least one change/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
|
displayName: "",
|
|
}),
|
|
/non-empty string/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
|
quantity: -1,
|
|
}),
|
|
/non-negative/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("project-device row sync project commands", () => {
|
|
const linkedSnapshot: ProjectDeviceSyncRowSnapshot = {
|
|
linkedProjectDeviceId: "project-device-1",
|
|
name: "Leuchte",
|
|
displayName: "Lokaler Name",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: "lighting",
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
cosPhi: 0.9,
|
|
remark: null,
|
|
overriddenFields: '["displayName"]',
|
|
};
|
|
|
|
it("captures complete expected and target snapshots for synchronization", () => {
|
|
const command = createProjectDeviceRowSyncProjectCommand(
|
|
"project-device-1",
|
|
"synchronize",
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expected: linkedSnapshot,
|
|
target: {
|
|
...linkedSnapshot,
|
|
displayName: "Projektgerät",
|
|
overriddenFields: null,
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
assert.doesNotThrow(() =>
|
|
assertProjectDeviceRowSyncProjectCommand(command)
|
|
);
|
|
assert.equal(
|
|
command.payload.rows[0]?.target.displayName,
|
|
"Projektgerät"
|
|
);
|
|
});
|
|
|
|
it("permits link-only disconnects and rejects invalid sync transitions", () => {
|
|
assert.doesNotThrow(() =>
|
|
createProjectDeviceRowSyncProjectCommand(
|
|
"project-device-1",
|
|
"disconnect",
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expected: linkedSnapshot,
|
|
target: {
|
|
...linkedSnapshot,
|
|
linkedProjectDeviceId: null,
|
|
},
|
|
},
|
|
]
|
|
)
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createProjectDeviceRowSyncProjectCommand(
|
|
"project-device-1",
|
|
"disconnect",
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expected: linkedSnapshot,
|
|
target: {
|
|
...linkedSnapshot,
|
|
linkedProjectDeviceId: null,
|
|
quantity: 2,
|
|
},
|
|
},
|
|
]
|
|
),
|
|
/must not change local row values/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createProjectDeviceRowSyncProjectCommand(
|
|
"project-device-1",
|
|
"synchronize",
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expected: linkedSnapshot,
|
|
target: linkedSnapshot,
|
|
},
|
|
]
|
|
),
|
|
/no-op row/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit device-row structure project commands", () => {
|
|
const row = {
|
|
id: "row-1",
|
|
circuitId: "circuit-1",
|
|
linkedProjectDeviceId: null,
|
|
legacyConsumerId: null,
|
|
sortOrder: 10,
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: "lighting",
|
|
level: null,
|
|
roomId: null,
|
|
roomNumberSnapshot: null,
|
|
roomNameSnapshot: null,
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
cosPhi: 0.9,
|
|
remark: null,
|
|
overriddenFields: null,
|
|
};
|
|
|
|
it("captures complete insert and delete identities", () => {
|
|
const insert = createCircuitDeviceRowInsertProjectCommand(row);
|
|
const remove = createCircuitDeviceRowDeleteProjectCommand(
|
|
row.id,
|
|
row.circuitId
|
|
);
|
|
|
|
assert.deepEqual(insert.payload.row, row);
|
|
assert.deepEqual(remove.payload, {
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
});
|
|
});
|
|
|
|
it("rejects incomplete snapshots and invalid values", () => {
|
|
assert.throws(
|
|
() =>
|
|
assertCircuitDeviceRowInsertProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "circuit-device-row.insert",
|
|
payload: {
|
|
row: {
|
|
...row,
|
|
linkedProjectDeviceId: undefined,
|
|
},
|
|
},
|
|
}),
|
|
/linkedProjectDeviceId/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowInsertProjectCommand({
|
|
...row,
|
|
quantity: -1,
|
|
}),
|
|
/must not be negative/
|
|
);
|
|
assert.throws(
|
|
() => createCircuitDeviceRowDeleteProjectCommand("", "circuit-1"),
|
|
/rowId/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit device-row move project commands", () => {
|
|
const targetCircuit = {
|
|
id: "circuit-new",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1F3",
|
|
displayName: "Neuer Stromkreis",
|
|
sortOrder: 30,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: null,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
};
|
|
|
|
it("captures deterministic source and target positions", () => {
|
|
const command = createCircuitDeviceRowMoveProjectCommand([
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "circuit-2",
|
|
targetSortOrder: 30,
|
|
},
|
|
{
|
|
rowId: "row-2",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 20,
|
|
targetCircuitId: "circuit-2",
|
|
targetSortOrder: 40,
|
|
},
|
|
]);
|
|
assert.equal(command.payload.moves.length, 2);
|
|
assert.doesNotThrow(() =>
|
|
assertCircuitDeviceRowMoveProjectCommand(command)
|
|
);
|
|
});
|
|
|
|
it("rejects empty, duplicate and no-op moves", () => {
|
|
assert.throws(
|
|
() => createCircuitDeviceRowMoveProjectCommand([]),
|
|
/at least one move/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowMoveProjectCommand([
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "circuit-2",
|
|
targetSortOrder: 20,
|
|
},
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 30,
|
|
targetCircuitId: "circuit-2",
|
|
targetSortOrder: 40,
|
|
},
|
|
]),
|
|
/duplicate row ids/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowMoveProjectCommand([
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "circuit-1",
|
|
targetSortOrder: 10,
|
|
},
|
|
]),
|
|
/no-op/
|
|
);
|
|
});
|
|
|
|
it("captures creation and deletion of a deterministic move target", () => {
|
|
const create =
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
"create",
|
|
targetCircuit,
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: targetCircuit.id,
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
);
|
|
const remove =
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
"delete",
|
|
targetCircuit,
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: targetCircuit.id,
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "circuit-1",
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
create
|
|
)
|
|
);
|
|
assert.equal(remove.payload.targetCircuitAction, "delete");
|
|
});
|
|
|
|
it("rejects inconsistent move targets and non-empty snapshots", () => {
|
|
assert.throws(
|
|
() =>
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
"create",
|
|
targetCircuit,
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "other-circuit",
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
),
|
|
/move rows into the new circuit/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "circuit-device-row.move-with-new-circuit",
|
|
payload: {
|
|
targetCircuitAction: "create",
|
|
targetCircuit: {
|
|
...targetCircuit,
|
|
isReserve: false,
|
|
},
|
|
moves: [
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: targetCircuit.id,
|
|
targetSortOrder: 10,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
/reserve state/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit structure project commands", () => {
|
|
const row = {
|
|
id: "row-1",
|
|
circuitId: "circuit-1",
|
|
linkedProjectDeviceId: null,
|
|
legacyConsumerId: null,
|
|
sortOrder: 10,
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: "lighting",
|
|
level: null,
|
|
roomId: null,
|
|
roomNumberSnapshot: null,
|
|
roomNameSnapshot: null,
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
cosPhi: null,
|
|
remark: null,
|
|
overriddenFields: null,
|
|
};
|
|
const circuit = {
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1F1",
|
|
displayName: "Beleuchtung",
|
|
sortOrder: 10,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: 230,
|
|
controlRequirement: "DALI",
|
|
status: null,
|
|
isReserve: false,
|
|
remark: null,
|
|
deviceRows: [row],
|
|
};
|
|
|
|
it("captures complete circuit blocks and delete identities", () => {
|
|
const insert = createCircuitInsertProjectCommand(circuit);
|
|
const remove = createCircuitDeleteProjectCommand(
|
|
circuit.id,
|
|
circuit.circuitListId
|
|
);
|
|
assert.deepEqual(insert.payload.circuit, circuit);
|
|
assert.deepEqual(remove.payload, {
|
|
circuitId: "circuit-1",
|
|
expectedCircuitListId: "list-1",
|
|
});
|
|
});
|
|
|
|
it("rejects inconsistent reserve state, row ownership and duplicate ids", () => {
|
|
assert.throws(
|
|
() =>
|
|
createCircuitInsertProjectCommand({
|
|
...circuit,
|
|
isReserve: true,
|
|
}),
|
|
/reserve state/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitInsertProjectCommand({
|
|
...circuit,
|
|
deviceRows: [{ ...row, circuitId: "other-circuit" }],
|
|
}),
|
|
/different circuit/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitInsertProjectCommand({
|
|
...circuit,
|
|
deviceRows: [row, { ...row }],
|
|
}),
|
|
/duplicate row ids/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertCircuitInsertProjectCommand({
|
|
schemaVersion: 1,
|
|
type: "circuit.insert",
|
|
payload: {
|
|
circuit: { ...circuit, equipmentIdentifier: "" },
|
|
},
|
|
}),
|
|
/equipmentIdentifier/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit section reorder project commands", () => {
|
|
it("captures every expected and target sort position", () => {
|
|
const command = createCircuitSectionReorderProjectCommand(
|
|
"section-1",
|
|
[
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 20,
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
);
|
|
assert.equal(command.payload.assignments.length, 2);
|
|
assert.doesNotThrow(() =>
|
|
assertCircuitSectionReorderProjectCommand(command)
|
|
);
|
|
});
|
|
|
|
it("rejects empty, duplicate and complete no-op assignments", () => {
|
|
assert.throws(
|
|
() => createCircuitSectionReorderProjectCommand("", []),
|
|
/sectionId/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitSectionReorderProjectCommand("section-1", [
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 20,
|
|
},
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 10,
|
|
},
|
|
]),
|
|
/duplicate circuit ids/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitSectionReorderProjectCommand("section-1", [
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 10,
|
|
},
|
|
]),
|
|
/change at least one position/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit section renumber project commands", () => {
|
|
it("captures every expected and target equipment identifier", () => {
|
|
const command = createCircuitSectionRenumberProjectCommand(
|
|
"section-1",
|
|
[
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedEquipmentIdentifier: "-1F1",
|
|
targetEquipmentIdentifier: "-1F2",
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedEquipmentIdentifier: "-1F2",
|
|
targetEquipmentIdentifier: "-1F1",
|
|
},
|
|
]
|
|
);
|
|
assert.equal(command.payload.assignments.length, 2);
|
|
assert.doesNotThrow(() =>
|
|
assertCircuitSectionRenumberProjectCommand(command)
|
|
);
|
|
});
|
|
|
|
it("rejects duplicate and complete no-op assignments", () => {
|
|
assert.throws(
|
|
() =>
|
|
createCircuitSectionRenumberProjectCommand("section-1", [
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedEquipmentIdentifier: "-1F1",
|
|
targetEquipmentIdentifier: "-1F2",
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedEquipmentIdentifier: "-1F2",
|
|
targetEquipmentIdentifier: "-1F2",
|
|
},
|
|
]),
|
|
/duplicate target identifiers/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitSectionRenumberProjectCommand("section-1", [
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedEquipmentIdentifier: "-1F1",
|
|
targetEquipmentIdentifier: "-1F1",
|
|
},
|
|
]),
|
|
/change at least one identifier/
|
|
);
|
|
});
|
|
});
|