UNPKG

@itwin/core-backend

Version:
431 lines 24 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { Code, CodeScopeSpec, IModel, SubCategoryAppearance } from "@itwin/core-common"; import { HubWrappers, IModelTestUtils, KnownTestLocations } from ".."; import { ChannelControl, DrawingCategory } from "../../core-backend"; import { HubMock } from "../../internal/HubMock"; import { withEditTxn } from "../../EditTxn"; chai.use(chaiAsPromised); describe("Code value management: null, swap, undo/redo, and cross-briefcase pull", function () { this.timeout(60000); it("swap codeValues between two elements sharing the same codeSpec and codeScope, verify undo/redo in a single transaction", async () => { HubMock.startup("CodeSwapTest", KnownTestLocations.outputDir); let b1; try { // --- Setup iModel --- const iModelId = await HubMock.createNewIModel({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelName: "CodeSwapTest", description: "CodeSwapTest", }); b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelId }); b1.channels.addAllowedChannel(ChannelControl.sharedChannelName); // --- Create a drawing model + category and a CodeSpec --- let drawingModelId; let drawingCategoryId; let codeSpecId; let codeScopeId; // will be the drawing model await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); withEditTxn(b1, "setup model, category, codeSpec", (txn) => { const modelCode = IModelTestUtils.getUniqueModelCode(b1, "DrawingModel"); const [, modelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode); drawingModelId = modelId; let catId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === catId) catId = DrawingCategory.insert(txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); drawingCategoryId = catId; codeSpecId = b1.codeSpecs.insert(txn, "MyCodeSpec", CodeScopeSpec.Type.Model); codeScopeId = drawingModelId; }); await b1.pushChanges({ description: "setup" }); // --- Insert two elements with same codeSpecId/codeScopeId but different codeValues --- let elem1Id; let elem2Id; await b1.locks.acquireLocks({ shared: drawingModelId }); withEditTxn(b1, "insert two coded elements", (txn) => { const elem1Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }), }; const elem2Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }), }; elem1Id = txn.insertElement(elem1Props); elem2Id = txn.insertElement(elem2Props); }); await b1.pushChanges({ description: "insert two elements with CODE_A and CODE_B" }); // Verify initial state let e1 = b1.elements.getElementProps(elem1Id); let e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.spec).to.equal(codeSpecId); chai.expect(e1.code.scope).to.equal(codeScopeId); chai.expect(e1.code.value).to.equal("CODE_A"); chai.expect(e2.code.spec).to.equal(codeSpecId); chai.expect(e2.code.scope).to.equal(codeScopeId); chai.expect(e2.code.value).to.equal("CODE_B"); await b1.locks.acquireLocks({ exclusive: [elem2Id, elem1Id] }); withEditTxn(b1, "swap code values", (txn) => { const props = b1.elements.getElementProps(elem1Id); props.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "" }); txn.updateElement(props); const props1 = b1.elements.getElementProps(elem1Id); const props2 = b1.elements.getElementProps(elem2Id); props2.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }); props1.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }); txn.updateElement(props2); txn.updateElement(props1); }); b1.clearCaches(); // Verify after swap2 e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.equal("CODE_B"); chai.expect(e2.code.value).to.equal("CODE_A"); // --- Undo (reverse txn2: the swap) --- chai.expect(b1.txns.isUndoPossible).to.be.true; b1.txns.reverseTxns(1); e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.equal("CODE_A"); chai.expect(e2.code.value).to.equal("CODE_B"); // --- Redo (reinstate txn2: the swap) --- chai.expect(b1.txns.isRedoPossible).to.be.true; b1.txns.reinstateTxn(); e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.equal("CODE_B"); chai.expect(e2.code.value).to.equal("CODE_A"); } finally { b1?.close(); HubMock.shutdown(); } }); it("insert two elements with same codeSpecId and codeScopeId, null first code, swap codes, undo/redo, push", async () => { HubMock.startup("CodeSwapTest", KnownTestLocations.outputDir); let b1; let b2; try { // --- Setup iModel --- const iModelId = await HubMock.createNewIModel({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelName: "CodeSwapTest", description: "CodeSwapTest", }); b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelId }); b1.channels.addAllowedChannel(ChannelControl.sharedChannelName); b2 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user2", iTwinId: HubMock.iTwinId, iModelId }); b2.channels.addAllowedChannel(ChannelControl.sharedChannelName); // --- Create a drawing model + category and a CodeSpec --- let drawingModelId; let drawingCategoryId; let codeSpecId; let codeScopeId; // will be the drawing model await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); withEditTxn(b1, "setup model, category, codeSpec", (txn) => { const modelCode = IModelTestUtils.getUniqueModelCode(b1, "DrawingModel"); const [, modelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode); drawingModelId = modelId; let catId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === catId) catId = DrawingCategory.insert(txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); drawingCategoryId = catId; codeSpecId = b1.codeSpecs.insert(txn, "MyCodeSpec", CodeScopeSpec.Type.Model); codeScopeId = drawingModelId; }); await b1.pushChanges({ description: "setup" }); await b2.pullChanges(); // --- Insert two elements with same codeSpecId/codeScopeId but different codeValues --- let elem1Id; let elem2Id; await b1.locks.acquireLocks({ shared: drawingModelId }); withEditTxn(b1, "insert two coded elements", (txn) => { const elem1Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }), }; const elem2Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }), }; elem1Id = txn.insertElement(elem1Props); elem2Id = txn.insertElement(elem2Props); }); await b1.pushChanges({ description: "insert two elements with CODE_A and CODE_B" }); await b2.pullChanges(); // Verify initial state let e1 = b1.elements.getElementProps(elem1Id); let e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.spec).to.equal(codeSpecId); chai.expect(e1.code.scope).to.equal(codeScopeId); chai.expect(e1.code.value).to.equal("CODE_A"); chai.expect(e2.code.spec).to.equal(codeSpecId); chai.expect(e2.code.scope).to.equal(codeScopeId); chai.expect(e2.code.value).to.equal("CODE_B"); // --- Step 1 (txn1): Set codeValue of elem1 to empty (null-equivalent) --- await b1.locks.acquireLocks({ exclusive: [elem1Id] }); withEditTxn(b1, "set elem1 code to null", (txn) => { const props = b1.elements.getElementProps(elem1Id); props.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "" }); txn.updateElement(props); }); e1 = b1.elements.getElementProps(elem1Id); chai.expect(e1.code.value).to.satisfy((v) => v === undefined || v === ""); // --- Step 2 (txn2): Swap codeValues — elem1 gets CODE_B, elem2 gets CODE_A --- // elem1's intermediate empty code value avoids a uniqueness conflict while assigning CODE_A to elem2. await b1.locks.acquireLocks({ exclusive: [elem2Id] }); withEditTxn(b1, "swap code values", (txn) => { const props1 = b1.elements.getElementProps(elem1Id); const props2 = b1.elements.getElementProps(elem2Id); props2.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }); props1.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }); txn.updateElement(props2); txn.updateElement(props1); }); b1.clearCaches(); // Verify after swap e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.equal("CODE_B"); chai.expect(e2.code.value).to.equal("CODE_A"); // --- Undo (reverse txn2: the swap) --- chai.expect(b1.txns.isUndoPossible).to.be.true; b1.txns.reverseTxns(1); e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.satisfy((v) => v === undefined || v === ""); chai.expect(e2.code.value).to.equal("CODE_B"); // --- Redo (reinstate txn2: the swap) --- chai.expect(b1.txns.isRedoPossible).to.be.true; b1.txns.reinstateTxn(); e1 = b1.elements.getElementProps(elem1Id); e2 = b1.elements.getElementProps(elem2Id); chai.expect(e1.code.value).to.equal("CODE_B"); chai.expect(e2.code.value).to.equal("CODE_A"); // --- Push changes --- await b1.pushChanges({ description: "null elem1 code and swap code values" }); await b2.pullChanges(); // No local txns remain after push chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; // Verify b2 sees the final swapped code values after pulling const b2e1 = b2.elements.getElementProps(elem1Id); const b2e2 = b2.elements.getElementProps(elem2Id); chai.expect(b2e1.code.value).to.equal("CODE_B"); chai.expect(b2e2.code.value).to.equal("CODE_A"); } finally { b1?.close(); b2?.close(); HubMock.shutdown(); } }); it("delete element with existing code and insert new element with same code, verify undo/redo, push, and b2 sees final state after pull", async () => { HubMock.startup("CodeDeleteInsertTest", KnownTestLocations.outputDir); let b1; let b2; try { // --- Setup iModel --- const iModelId = await HubMock.createNewIModel({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelName: "CodeDeleteInsertTest", description: "CodeDeleteInsertTest", }); b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelId }); b1.channels.addAllowedChannel(ChannelControl.sharedChannelName); b2 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user2", iTwinId: HubMock.iTwinId, iModelId }); b2.channels.addAllowedChannel(ChannelControl.sharedChannelName); // --- Create a drawing model + category and a CodeSpec --- let drawingModelId; let drawingCategoryId; let codeSpecId; let codeScopeId; await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); withEditTxn(b1, "setup model, category, codeSpec", (txn) => { const modelCode = IModelTestUtils.getUniqueModelCode(b1, "DrawingModel"); const [, modelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode); drawingModelId = modelId; let catId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === catId) catId = DrawingCategory.insert(txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); drawingCategoryId = catId; codeSpecId = b1.codeSpecs.insert(txn, "MyCodeSpec", CodeScopeSpec.Type.Model); codeScopeId = drawingModelId; }); await b1.pushChanges({ description: "setup" }); await b2.pullChanges(); // --- Insert an element with a code --- let elem1Id; await b1.locks.acquireLocks({ shared: drawingModelId }); withEditTxn(b1, "insert element with CODE_A", (txn) => { const elem1Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }), }; elem1Id = txn.insertElement(elem1Props); }); await b1.pushChanges({ description: "insert element with CODE_A" }); await b2.pullChanges(); // Verify initial state let e1 = b1.elements.getElementProps(elem1Id); chai.expect(e1.code.spec).to.equal(codeSpecId); chai.expect(e1.code.scope).to.equal(codeScopeId); chai.expect(e1.code.value).to.equal("CODE_A"); // --- Delete elem1 and insert a new element with the same code --- let newElemId; await b1.locks.acquireLocks({ exclusive: [elem1Id] }); withEditTxn(b1, "delete elem1 and insert new element with same code", (txn) => { txn.deleteElement(elem1Id); const newElemProps = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }), }; newElemId = txn.insertElement(newElemProps); }); b1.clearCaches(); // Verify after delete + insert chai.expect(() => b1.elements.getElementProps(elem1Id)).to.throw(); const newElem = b1.elements.getElementProps(newElemId); chai.expect(newElem.code.value).to.equal("CODE_A"); // --- Undo (reverse the delete + insert) --- chai.expect(b1.txns.isUndoPossible).to.be.true; b1.txns.reverseTxns(1); e1 = b1.elements.getElementProps(elem1Id); chai.expect(e1.code.value).to.equal("CODE_A"); chai.expect(() => b1.elements.getElementProps(newElemId)).to.throw(); // --- Redo (reinstate the delete + insert) --- chai.expect(b1.txns.isRedoPossible).to.be.true; b1.txns.reinstateTxn(); chai.expect(() => b1.elements.getElementProps(elem1Id)).to.throw(); const redoneElem = b1.elements.getElementProps(newElemId); chai.expect(redoneElem.code.value).to.equal("CODE_A"); // --- Push changes --- await b1.pushChanges({ description: "delete elem1 and insert new element with same code" }); await b2.pullChanges(); // No local txns remain after push chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; // Verify b2 sees the final state after pulling chai.expect(() => b2.elements.getElementProps(elem1Id)).to.throw(); const b2NewElem = b2.elements.getElementProps(newElemId); chai.expect(b2NewElem.code.value).to.equal("CODE_A"); } finally { b1?.close(); b2?.close(); HubMock.shutdown(); } }); it("single-transaction code swap reverts to old behaviour (swap not applied) when noUpdateLoop is true", async () => { HubMock.startup("CodeSwapNoUpdateLoopTest", KnownTestLocations.outputDir); let b1; let b2; try { // --- Setup iModel --- const iModelId = await HubMock.createNewIModel({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelName: "CodeSwapNoUpdateLoopTest", description: "CodeSwapNoUpdateLoopTest", }); b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user1", iTwinId: HubMock.iTwinId, iModelId }); b1.channels.addAllowedChannel(ChannelControl.sharedChannelName); b2 = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "user2", iTwinId: HubMock.iTwinId, iModelId }); b2.channels.addAllowedChannel(ChannelControl.sharedChannelName); let drawingModelId; let drawingCategoryId; let codeSpecId; let codeScopeId; await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); withEditTxn(b1, "setup model, category, codeSpec", (txn) => { const modelCode = IModelTestUtils.getUniqueModelCode(b1, "DrawingModel"); const [, modelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode); drawingModelId = modelId; let catId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === catId) catId = DrawingCategory.insert(txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); drawingCategoryId = catId; codeSpecId = b1.codeSpecs.insert(txn, "MyCodeSpec", CodeScopeSpec.Type.Model); codeScopeId = drawingModelId; }); await b1.pushChanges({ description: "setup" }); await b2.pullChanges(); let elem1Id; let elem2Id; await b1.locks.acquireLocks({ shared: drawingModelId }); withEditTxn(b1, "insert two coded elements", (txn) => { const elem1Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }), }; const elem2Props = { classFullName: "BisCore:DrawingGraphic", model: drawingModelId, category: drawingCategoryId, code: new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }), }; elem1Id = txn.insertElement(elem1Props); elem2Id = txn.insertElement(elem2Props); }); await b1.pushChanges({ description: "insert two elements with CODE_A and CODE_B" }); await b2.pullChanges(); // --- Single-transaction swap: null elem1, then swap codes --- await b1.locks.acquireLocks({ exclusive: [elem2Id, elem1Id] }); withEditTxn(b1, "single-txn swap", (txn) => { // Step 1: null out elem1's code to free the uniqueness slot const props = b1.elements.getElementProps(elem1Id); props.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "" }); txn.updateElement(props); // Step 2: assign CODE_A to elem2, CODE_B to elem1 const props1 = b1.elements.getElementProps(elem1Id); const props2 = b1.elements.getElementProps(elem2Id); props2.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_A" }); props1.code = new Code({ spec: codeSpecId, scope: codeScopeId, value: "CODE_B" }); txn.updateElement(props2); txn.updateElement(props1); }); // Verify b1 has the swapped state b1.clearCaches(); const b1e1 = b1.elements.getElementProps(elem1Id); const b1e2 = b1.elements.getElementProps(elem2Id); chai.expect(b1e1.code.value).to.equal("CODE_B"); chai.expect(b1e2.code.value).to.equal("CODE_A"); await b1.pushChanges({ description: "single-txn code swap" }); await b2.pullChanges({ noUpdateLoop: true }); b2.clearCaches(); const b2e1 = b2.elements.getElementProps(elem1Id); const b2e2 = b2.elements.getElementProps(elem2Id); // Old behaviour: swap did NOT land — b2 still sees the original codes chai.expect(b2e1.code.value).to.equal("CODE_A"); chai.expect(b2e2.code.value).to.equal("CODE_B"); } finally { b1?.close(); b2?.close(); HubMock.shutdown(); } }); }); //# sourceMappingURL=CodeSwap.test.js.map