UNPKG

@itwin/core-backend

Version:
832 lines (826 loc) • 43.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; import { Code, FieldRun, SubCategoryAppearance, TextAnnotation, TextBlock, TextRun } from "@itwin/core-common"; import { StandaloneDb } from "../../IModelDb"; import { IModelTestUtils } from "../IModelTestUtils"; import { createUpdateContext, updateField, updateFields } from "../../internal/annotations/fields"; import { DbResult, Id64, ProcessDetector } from "@itwin/core-bentley"; import { SpatialCategory } from "../../Category"; import { Point3d, YawPitchRollAngles } from "@itwin/core-geometry"; import { Schema, Schemas } from "../../Schema"; import { ClassRegistry } from "../../ClassRegistry"; import { PhysicalElement } from "../../Element"; import { ElementOwnsUniqueAspect, ElementUniqueAspect, FontFile, IModelElementCloneContext, TextAnnotation3d } from "../../core-backend"; import { ElementDrivesTextAnnotation, TextAnnotationUsesTextStyleByDefault } from "../../annotations/ElementDrivesTextAnnotation"; function isIntlSupported() { // Node in the mobile add-on does not include Intl, so this test fails. Right now, mobile // users are not expected to do any editing, but long term we will attempt to find a better // solution. return !ProcessDetector.isMobileAppBackend; } function createTestElement(imodel, model, category, overrides, aspectProp = 999) { const props = { classFullName: "Fields:TestElement", model, category, code: Code.createEmpty(), intProp: 100, point: { x: 1, y: 2, z: 3 }, strings: ["a", "b", `"name": "c"`], datetime: new Date("2025-08-28T13:45:30.123Z"), intEnum: 1, outerStruct: { innerStruct: { bool: false, doubles: [1, 2, 3] }, innerStructs: [{ bool: true, doubles: [] }, { bool: false, doubles: [5, 4, 3, 2, 1] }], }, outerStructs: [{ innerStruct: { bool: true, doubles: [10, 9] }, innerStructs: [{ bool: false, doubles: [5] }], }], placement: { origin: new Point3d(1, 2, 0), angles: new YawPitchRollAngles(), }, jsonProperties: { stringProp: "abc", ints: [10, 11, 12, 13], bool: true, zoo: { address: { zipcode: 12345, }, birds: [ { name: "duck", sound: "quack" }, { name: "hawk", sound: "scree!" }, ], }, }, ...overrides, }; const id = imodel.elements.insertElement(props); const aspectProps = { classFullName: TestAspect.classFullName, aspectProp, element: new ElementOwnsUniqueAspect(id), }; imodel.elements.insertAspect(aspectProps); imodel.saveChanges(); return id; } describe("updateField", () => { const mockElementId = "0x1"; const mockPath = { propertyName: "mockProperty", accessors: [0, "nestedProperty"], }; const mockCachedContent = "cachedContent"; const mockUpdatedContent = "updatedContent"; const createMockContext = (elementId, propertyValue) => ({ hostElementId: elementId, getProperty: (field) => { const propertyPath = field.propertyPath; if (propertyPath.propertyName === "mockProperty" && propertyPath.accessors?.[0] === 0 && propertyPath.accessors?.[1] === "nestedProperty" && propertyValue !== undefined) { return { value: propertyValue, type: "string" }; } return undefined; }, }); it("does nothing if hostElementId does not match", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: mockElementId, schemaName: "TestSchema", className: "TestClass" }, propertyPath: mockPath, cachedContent: mockCachedContent, }); const context = createMockContext("0x2", mockUpdatedContent); const result = updateField(fieldRun, context); expect(result).to.be.false; expect(fieldRun.cachedContent).to.equal(mockCachedContent); }); it("produces invalid content indicator if property value is undefined", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: mockElementId, schemaName: "TestSchema", className: "TestClass" }, propertyPath: mockPath, cachedContent: mockCachedContent, }); const context = createMockContext(mockElementId); const result = updateField(fieldRun, context); expect(result).to.be.true; expect(fieldRun.cachedContent).to.equal(FieldRun.invalidContentIndicator); }); it("returns false if cached content matches new content", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: mockElementId, schemaName: "TestSchema", className: "TestClass" }, propertyPath: mockPath, cachedContent: mockCachedContent, }); const context = createMockContext(mockElementId, mockCachedContent); const result = updateField(fieldRun, context); expect(result).to.be.false; expect(fieldRun.cachedContent).to.equal(mockCachedContent); }); it("returns true and updates cached content if new content is different", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: mockElementId, schemaName: "TestSchema", className: "TestClass" }, propertyPath: mockPath, cachedContent: mockCachedContent, }); const context = createMockContext(mockElementId, mockUpdatedContent); const result = updateField(fieldRun, context); expect(result).to.be.true; expect(fieldRun.cachedContent).to.equal(mockUpdatedContent); }); it("resolves to invalid content indicator if an exception occurs", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: mockElementId, schemaName: "TestSchema", className: "TestClass" }, propertyPath: mockPath, cachedContent: mockCachedContent, }); const context = { hostElementId: mockElementId, getProperty: () => { throw new Error("Test exception"); }, }; const result = updateField(fieldRun, context); expect(result).to.be.true; expect(fieldRun.cachedContent).to.equal(FieldRun.invalidContentIndicator); }); }); const fieldsSchemaXml = ` <?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="Fields" alias="ts" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.04" alias="bis"/> <ECSchemaReference name='ECDbMap' version='02.00.04' alias='ecdbmap' /> <ECEnumeration typeName="IntEnum" backingTypeName="int"> <ECEnumerator name="one" displayLabel="One" value="1" /> <ECEnumerator name="two" displayLabel="Two" value="2"/> </ECEnumeration> <ECStructClass typeName="InnerStruct" modifier="None"> <ECProperty propertyName="bool" typeName="boolean"/> <ECArrayProperty propertyName="doubles" typeName="double" minOccurs="0" maxOccurs="unbounded"/> </ECStructClass> <ECStructClass typeName="OuterStruct" modifier="None"> <ECStructProperty propertyName="innerStruct" typeName="InnerStruct"/> <ECStructArrayProperty propertyName="innerStructs" typeName="InnerStruct" minOccurs="0" maxOccurs="unbounded"/> </ECStructClass> <ECEntityClass typeName="TestElement" modifier="None"> <BaseClass>bis:PhysicalElement</BaseClass> <ECProperty propertyName="intProp" typeName="int"/> <ECProperty propertyName="point" typeName="point3d"/> <ECProperty propertyName="maybeNull" typeName="int"/> <ECProperty propertyName="datetime" typeName="dateTime"/> <ECArrayProperty propertyName="strings" typeName="string" minOccurs="0" maxOccurs="unbounded"/> <ECStructProperty propertyName="outerStruct" typeName="OuterStruct"/> <ECStructArrayProperty propertyName="outerStructs" typeName="OuterStruct" minOccurs="0" maxOccurs="unbounded"/> <ECProperty propertyName="intEnum" typeName="IntEnum"/> </ECEntityClass> <ECEntityClass typeName="TestAspect" modifier="None"> <BaseClass>bis:ElementUniqueAspect</BaseClass> <ECProperty propertyName="aspectProp" typeName="int"/> </ECEntityClass> <ECEntityClass typeName="TestElementStringProp" modifier="Abstract"> <ECCustomAttributes> <QueryView xmlns="ECDbMap.02.00.04"> <Query> SELECT jo.ECInstanceId, ec_classid('Fields', 'TestElementStringProp') [ECClassId], json_extract(jo.jsonProperties, '$.stringProp') [StringProp] FROM Fields.TestElement jo </Query> </QueryView> </ECCustomAttributes> <ECProperty propertyName="StringProp" typeName="string" /> </ECEntityClass> </ECSchema> `; class TestElement extends PhysicalElement { static get className() { return "TestElement"; } } class TestAspect extends ElementUniqueAspect { static get className() { return "TestAspect"; } } class FieldsSchema extends Schema { static get schemaName() { return "Fields"; } } async function registerTestSchema(iModel) { if (!Schemas.getRegisteredSchema("Fields")) { Schemas.registerSchema(FieldsSchema); ClassRegistry.register(TestElement, FieldsSchema); ClassRegistry.register(TestAspect, FieldsSchema); } await iModel.importSchemaStrings([fieldsSchemaXml]); iModel.saveChanges(); } describe("Field evaluation", () => { let imodel; let model; let category; let sourceElementId; before(async () => { const iModelPath = IModelTestUtils.prepareOutputFile("UpdateFieldsContext", "test.bim"); imodel = StandaloneDb.createEmpty(iModelPath, { rootSubject: { name: "UpdateFieldsContext" }, enableTransactions: true }); await registerTestSchema(imodel); model = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(imodel, Code.createEmpty(), true)[1]; category = SpatialCategory.insert(imodel, StandaloneDb.dictionaryId, "UpdateFieldsContextCategory", new SubCategoryAppearance()); sourceElementId = insertTestElement(); await imodel.fonts.embedFontFile({ file: FontFile.createFromTrueTypeFileName(IModelTestUtils.resolveFontFile("Karla-Regular.ttf")) }); }); after(() => { imodel.close(); }); function insertTestElement(overrides, aspectProp) { return createTestElement(imodel, model, category, overrides, aspectProp); } function evaluateField(propertyPath, propertyHost, deletedDependency = false) { if (typeof propertyHost === "string") { propertyHost = { schemaName: "Fields", className: "TestElement", elementId: propertyHost }; } const field = FieldRun.create({ propertyPath, propertyHost, }); const context = createUpdateContext(propertyHost.elementId, imodel, deletedDependency); return context.getProperty(field); } describe("getProperty", () => { function expectValue(expected, propertyPath, propertyHost, deletedDependency = false) { expect(evaluateField(propertyPath, propertyHost, deletedDependency)?.value).to.deep.equal(expected); } it("returns a primitive property value", () => { expectValue(100, { propertyName: "intProp" }, sourceElementId); }); it("returns an integer enum property value", () => { expectValue(1, { propertyName: "intEnum" }, sourceElementId); }); it("treats points as primitive values", () => { expectValue({ x: 1, y: 2, z: 3 }, { propertyName: "point" }, sourceElementId); expectValue(undefined, { propertyName: "point", accessors: ["x"] }, sourceElementId); }); it("returns a primitive array value", () => { expectValue("a", { propertyName: "strings", accessors: [0] }, sourceElementId); expectValue("b", { propertyName: "strings", accessors: [1] }, sourceElementId); expectValue(`"name": "c"`, { propertyName: "strings", accessors: [2] }, sourceElementId); }); it("supports negative array indices", () => { expectValue("a", { propertyName: "strings", accessors: [-3] }, sourceElementId); expectValue("b", { propertyName: "strings", accessors: [-2] }, sourceElementId); expectValue(`"name": "c"`, { propertyName: "strings", accessors: [-1] }, sourceElementId); }); it("supports properties of EC views", () => { expectValue("abc", { propertyName: "stringProp" }, { schemaName: "Fields", className: "TestElementStringProp", elementId: sourceElementId }); }); it("returns undefined if the dependency was deleted", () => { expectValue(undefined, { propertyName: "intProp" }, sourceElementId, true); }); it("returns undefined if the host element does not exist", () => { expectValue(undefined, { propertyName: "intProp" }, "0xbaadf00d"); }); it("returns undefined if the host element is not of the specified class or a subclass thereof", () => { expectValue(undefined, { propertyName: "origin" }, { schemaName: "BisCore", className: "GeometricElement2d", elementId: sourceElementId }); }); it("returns undefined if an access string is specified for a non-object property", () => { expectValue(undefined, { propertyName: "intProp", accessors: ["property"] }, sourceElementId); }); it("returns undefined if the specified property does not exist", () => { expectValue(undefined, { propertyName: "nonExistentProperty" }, sourceElementId); }); it("returns undefined if the specified property is null", () => { expectValue(undefined, { propertyName: "maybeNull" }, sourceElementId); }); it("returns undefined if an array index is specified for a non-array property", () => { expectValue(undefined, { propertyName: "intProp", accessors: [0] }, sourceElementId); }); it("returns undefined if an array index is out of bounds", () => { for (const index of [3, 4, -4, -5]) { expectValue(undefined, { propertyName: "strings", accessors: [index] }, sourceElementId); } }); it("returns undefined for a non-primitive value", () => { expectValue(undefined, { propertyName: "strings" }, sourceElementId); expectValue(undefined, { propertyName: "outerStruct" }, sourceElementId); expectValue(undefined, { propertyName: "outerStruct", accessors: ["innerStruct"] }, sourceElementId); expectValue(undefined, { propertyName: "outerStructs" }, sourceElementId); expectValue(undefined, { propertyName: "outerStructs", accessors: [0] }, sourceElementId); expectValue(undefined, { propertyName: "outerStructs", accessors: [0, "innerStruct"] }, sourceElementId); }); it("returns arbitrarily-nested properties of structs and struct arrays", () => { expectValue(false, { propertyName: "outerStruct", accessors: ["innerStruct", "bool"] }, sourceElementId); for (const index of [0, 1, 2]) { expectValue(index + 1, { propertyName: "outerStruct", accessors: ["innerStruct", "doubles", index] }, sourceElementId); expectValue(3 - index, { propertyName: "outerStruct", accessors: ["innerStruct", "doubles", -1 - index] }, sourceElementId); } expectValue(9, { propertyName: "outerStructs", accessors: [0, "innerStruct", "doubles", 1] }, sourceElementId); expectValue(false, { propertyName: "outerStructs", accessors: [0, "innerStructs", -1, "bool"] }, sourceElementId); expectValue(5, { propertyName: "outerStructs", accessors: [0, "innerStructs", 0, "doubles", 0] }, sourceElementId); }); it("returns the value of a property of an aspect", () => { expect(imodel.elements.getAspects(sourceElementId, "Fields:TestAspect").length).to.equal(1); expectValue(999, { propertyName: "aspectProp" }, { elementId: sourceElementId, schemaName: "Fields", className: "TestAspect" }); }); it("should fail to evaluate if prop type does not match", () => { const fieldRun = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "string", accessors: [0] }, cachedContent: "oldValue", formatOptions: { case: "upper", prefix: "Value: ", suffix: "!" } }); const context = createUpdateContext(sourceElementId, imodel, false); const updated = updateField(fieldRun, context); expect(updated).to.be.true; expect(fieldRun.cachedContent).to.equal(FieldRun.invalidContentIndicator); }); function getPropertyType(propertyHost, propertyPath) { if (typeof propertyPath === "string") { propertyPath = { propertyName: propertyPath }; } return evaluateField(propertyPath, propertyHost)?.type; } it("deduces type for primitive properties", () => { const propertyHost = { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }; expect(getPropertyType(propertyHost, "intProp")).to.equal("string"); expect(getPropertyType(propertyHost, "point")).to.equal("coordinate"); expect(getPropertyType(propertyHost, { propertyName: "strings", accessors: [0] })).to.equal("string"); expect(getPropertyType(propertyHost, "intEnum")).to.equal("int-enum"); expect(getPropertyType(propertyHost, { propertyName: "outerStruct", accessors: ["innerStruct", "doubles", 0] })).to.equal("quantity"); expect(getPropertyType(propertyHost, { propertyName: "outerStruct", accessors: ["innerStruct", "bool"] })).to.equal("boolean"); propertyHost.schemaName = "BisCore"; propertyHost.className = "GeometricElement3d"; expect(getPropertyType(propertyHost, "LastMod")).to.equal("datetime"); expect(getPropertyType(propertyHost, "FederationGuid")).to.equal("string"); }); it("returns undefined for non-primitive properties", () => { const propertyHost = { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }; expect(getPropertyType(propertyHost, "outerStruct")).to.equal(undefined); expect(getPropertyType(propertyHost, "outerStructs")).to.equal(undefined); }); it("returns undefined for invalid property paths", () => { const propertyHost = { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }; expect(getPropertyType(propertyHost, "unknownPropertyName")).to.be.undefined; }); it("should return undefined for unsupported primitive types", () => { const host = { elementId: sourceElementId, schemaName: "BisCore", className: "GeometricElement3d" }; expect(getPropertyType(host, "GeometryStream")).to.be.undefined; }); }); describe("updateFields", () => { it("recomputes cached content", () => { const textBlock = TextBlock.create(); const fieldRun = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "intProp" }, cachedContent: "oldValue", }); textBlock.appendRun(fieldRun); const context = createUpdateContext(sourceElementId, imodel, false); const updatedCount = updateFields(textBlock, context); expect(updatedCount).to.equal(1); expect(fieldRun.cachedContent).to.equal("100"); // `intProp` value from the test element }); it("does not update a field if recomputed content matches cached content", () => { const textBlock = TextBlock.create(); const fieldRun = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "intProp" }, cachedContent: "100", }); textBlock.appendRun(fieldRun); const context = createUpdateContext(sourceElementId, imodel, false); const updatedCount = updateFields(textBlock, context); expect(updatedCount).to.equal(0); expect(fieldRun.cachedContent).to.equal("100"); }); it("returns the number of fields updated", () => { const textBlock = TextBlock.create(); const fieldRun1 = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "intProp" }, cachedContent: "100", }); const fieldRun2 = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "strings", accessors: [0] }, cachedContent: "oldValue", }); textBlock.appendRun(fieldRun1); textBlock.appendRun(fieldRun2); const context = createUpdateContext(sourceElementId, imodel, false); const updatedCount = updateFields(textBlock, context); expect(updatedCount).to.equal(1); expect(fieldRun1.cachedContent).to.equal("100"); expect(fieldRun2.cachedContent).to.equal("a"); }); }); function createAnnotationElement(textBlock) { const elem = TextAnnotation3d.fromJSON({ model, category, code: Code.createEmpty(), placement: { origin: { x: 0, y: 0, z: 0 }, angles: YawPitchRollAngles.createDegrees(0, 0, 0).toJSON(), }, classFullName: TextAnnotation3d.classFullName, defaultTextStyle: new TextAnnotationUsesTextStyleByDefault("0x123").toJSON(), }, imodel); if (textBlock) { const annotation = TextAnnotation.fromJSON({ textBlock: textBlock.toJSON() }); elem.setAnnotation(annotation); } return elem; } function insertAnnotationElement(textBlock) { const elem = createAnnotationElement(textBlock); return elem.insert(); } describe("ElementDrivesTextAnnotation", () => { function expectNumRelationships(expected, targetId) { const where = targetId ? ` WHERE TargetECInstanceId=${targetId}` : ""; const ecsql = `SELECT COUNT(*) FROM BisCore.ElementDrivesTextAnnotation ${where}`; // eslint-disable-next-line @typescript-eslint/no-deprecated imodel.withPreparedStatement(ecsql, (stmt) => { expect(stmt.step()).to.equal(DbResult.BE_SQLITE_ROW); expect(stmt.getValue(0).getInteger()).to.equal(expected); }); } it("can be inserted", () => { expectNumRelationships(0); const targetId = insertAnnotationElement(undefined); expect(targetId).not.to.equal(Id64.invalid); const target = imodel.elements.getElement(targetId); expect(target.classFullName).to.equal("BisCore:TextAnnotation3d"); expect(target).instanceof(TextAnnotation3d); const targetAnno = imodel.elements.getElement(targetId); expect(targetAnno).instanceof(TextAnnotation3d); const rel = ElementDrivesTextAnnotation.create(imodel, sourceElementId, targetId); const relId = rel.insert(); expect(relId).not.to.equal(Id64.invalid); expectNumRelationships(1); const relationship = imodel.relationships.getInstance("BisCore:ElementDrivesTextAnnotation", relId); expect(relationship.sourceId).to.equal(sourceElementId); expect(relationship.targetId).to.equal(targetId); }); function createField(propertyHost, cachedContent, propertyName = "intProp", accessors) { if (typeof propertyHost === "string") { propertyHost = { schemaName: "Fields", className: "TestElement", elementId: propertyHost }; } return FieldRun.create({ styleOverrides: { font: { name: "Karla" } }, propertyHost, cachedContent, propertyPath: { propertyName, accessors }, }); } describe("updateFieldDependencies", () => { it("creates exactly one relationship for each unique source element on insert and update", () => { const source1 = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(source1, "1")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectNumRelationships(1, targetId); const source2 = insertTestElement(); const target = imodel.elements.getElement(targetId); const anno = target.getAnnotation(); anno.textBlock.appendRun(createField(source2, "2a")); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(2, targetId); anno.textBlock.appendRun(createField(source2, "2b")); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(2, targetId); const source3 = insertTestElement(); anno.textBlock.appendRun(createField(source3, "3")); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(3, targetId); }); it("deletes stale relationships", () => { const sourceA = insertTestElement(); const sourceB = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(sourceA, "A")); block.appendRun(createField(sourceB, "B")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectNumRelationships(2, targetId); expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceA })).not.to.be.undefined; expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceB })).not.to.be.undefined; const target = imodel.elements.getElement(targetId); const anno = target.getAnnotation(); // Remove the sourceA FieldRun from the first paragraph. const p1 = anno.textBlock.children[0]; p1.children.shift(); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(1, targetId); expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceA })).to.be.undefined; expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceB })).not.to.be.undefined; anno.textBlock.children.length = 0; anno.textBlock.appendRun(createField(sourceA, "A2")); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(1, targetId); expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceA })).not.to.be.undefined; expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceB })).to.be.undefined; anno.textBlock.children.length = 0; anno.textBlock.appendRun(TextRun.create({ styleOverrides: { font: { name: "Karla" } }, content: "not a field", })); target.setAnnotation(anno); target.update(); imodel.saveChanges(); expectNumRelationships(0, targetId); expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceA })).to.be.undefined; expect(imodel.relationships.tryGetInstance(ElementDrivesTextAnnotation.classFullName, { targetId, sourceId: sourceB })).to.be.undefined; }); it("ignores invalid source element Ids", () => { const source = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(Id64.invalid, "invalid")); block.appendRun(createField("0xbaadf00d", "non-existent")); block.appendRun(createField(source, "valid")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectNumRelationships(1, targetId); }); }); function expectText(expected, elemId, db) { db = db ?? imodel; const elem = db.elements.getElement(elemId); const anno = elem.getAnnotation(); const actual = anno.textBlock.stringify(); expect(actual).to.equal(expected); } it("evaluates cachedContent when annotation element is inserted", () => { const sourceId = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(sourceId, "initial cached content")); expect(block.stringify()).to.equal("initial cached content"); const targetId = insertAnnotationElement(block); imodel.saveChanges(); const target = imodel.elements.getElement(targetId); expect(target.getAnnotation().textBlock.stringify()).to.equal("100"); }); it("updates fields when source element is modified or deleted", () => { const sourceId = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(sourceId, "old value")); ; const targetId = insertAnnotationElement(block); imodel.saveChanges(); const target = imodel.elements.getElement(targetId); expect(target.getAnnotation()).not.to.be.undefined; expectText("100", targetId); let source = imodel.elements.getElement(sourceId); source.intProp = 50; source.update(); expectText("100", targetId); imodel.saveChanges(); source = imodel.elements.getElement(sourceId); expect(source.intProp).to.equal(50); expectText("50", targetId); imodel.elements.deleteElement(sourceId); expectText("50", targetId); imodel.saveChanges(); expectText(FieldRun.invalidContentIndicator, targetId); }); it("updates fields when source element aspect is modified, deleted, or recreated", () => { const sourceId = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField({ elementId: sourceId, schemaName: "Fields", className: "TestAspect" }, "", "aspectProp")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectText("999", targetId); const aspects = imodel.elements.getAspects(sourceId, "Fields:TestAspect"); expect(aspects.length).to.equal(1); const aspect = aspects[0]; expect(aspect.aspectProp).to.equal(999); aspect.aspectProp = 12345; imodel.elements.updateAspect(aspect.toJSON()); imodel.saveChanges(); expectText("12345", targetId); imodel.elements.deleteAspect([aspect.id]); imodel.saveChanges(); expectText(FieldRun.invalidContentIndicator, targetId); const newAspect = { element: new ElementOwnsUniqueAspect(sourceId), classFullName: TestAspect.classFullName, aspectProp: 42, }; imodel.elements.insertAspect(newAspect); imodel.saveChanges(); expectText("42", targetId); }); it("updates only fields for specific modified element", () => { const sourceA = insertTestElement(); const sourceB = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(sourceA, "A")); block.appendRun(createField(sourceB, "B")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectText("100100", targetId); const sourceElem = imodel.elements.getElement(sourceB); sourceElem.intProp = 123; sourceElem.update(); imodel.saveChanges(); expectText("100123", targetId); }); it("supports complex property paths", () => { const sourceId = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField(sourceId, "", "outerStruct", ["innerStructs", 1, "doubles", -2])); const targetId = insertAnnotationElement(block); imodel.saveChanges(); expectText("2", targetId); const source = imodel.elements.getElement(sourceId); source.outerStruct.innerStructs[1].doubles[3] = 12.5; source.update(); imodel.saveChanges(); expectText("12.5", targetId); }); it("updates EC view fields when the element changes if the EC view queries the element directly", () => { const sourceId = insertTestElement(); const block = TextBlock.create(); block.appendRun(createField({ elementId: sourceId, schemaName: "Fields", className: "TestElementStringProp", }, "cached-content", "StringProp")); const targetId = insertAnnotationElement(block); imodel.saveChanges(); const target = imodel.elements.getElement(targetId); expect(target.getAnnotation()).not.to.be.undefined; expectText("abc", targetId); let source = imodel.elements.getElement(sourceId); source.jsonProperties.stringProp = "zyx"; source.update(); expectText("abc", targetId); imodel.saveChanges(); expectText("zyx", targetId); source = imodel.elements.getElement(sourceId); expect(source.jsonProperties.stringProp).to.equal("zyx"); expectText("zyx", targetId); imodel.elements.deleteElement(sourceId); expectText("zyx", targetId); imodel.saveChanges(); expectText(FieldRun.invalidContentIndicator, targetId); }); describe("remapFields", () => { let dstIModel; let dstModel; let dstCategory; let dstSourceElementId; before(async () => { const path = IModelTestUtils.prepareOutputFile("RemapFields", `dst.bim`); dstIModel = StandaloneDb.createEmpty(path, { rootSubject: { name: `RemapFields-dst` }, enableTransactions: true }); await registerTestSchema(dstIModel); // Insert additional unused elements to ensure element Ids differ between src and dst iModels for (let i = 0; i < 3; i++) { IModelTestUtils.createAndInsertPhysicalPartitionAndModel(dstIModel, Code.createEmpty(), true); } const modelAndElement = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(dstIModel, Code.createEmpty(), true); expect(modelAndElement[0]).to.equal(modelAndElement[1]); dstModel = modelAndElement[1]; dstCategory = SpatialCategory.insert(dstIModel, StandaloneDb.dictionaryId, `dstCat`, new SubCategoryAppearance()); dstSourceElementId = createTestElement(dstIModel, dstModel, dstCategory, { intProp: 200, point: { x: -1, y: -2, z: -3 }, strings: ["x", "y", "z"], intEnum: 2, }, 1234); await dstIModel.fonts.embedFontFile({ file: FontFile.createFromTrueTypeFileName(IModelTestUtils.resolveFontFile("Karla-Regular.ttf")) }); expect(dstCategory).not.to.equal(category); expect(dstModel).not.to.equal(model); expect(dstSourceElementId).not.to.equal(sourceElementId); }); after(() => { dstIModel.close(); }); function getTextBlockJson() { return { children: [{ children: [{ type: "field", propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement", }, propertyPath: { propertyName: "intProp" }, cachedContent: "intProp", }, { type: "field", propertyHost: { elementId: category, schemaName: "BisCore", className: "Element", }, propertyPath: { propertyName: "CodeValue" }, cachedContent: "CodeValue" }], }], }; } function expectHostIds(elem, host1, host2) { const anno = elem.getAnnotation(); expect(anno.textBlock.children.length).to.equal(1); const para = anno.textBlock.children[0]; expect(para.children.length).to.equal(2); expect(para.children.every((x) => x.type === "field")); const field1 = para.children[0]; expect(field1.propertyHost.elementId).to.equal(host1); const field2 = para.children[1]; expect(field2.propertyHost.elementId).to.equal(host2); } it("remaps field hosts", () => { const elem = createAnnotationElement(TextBlock.create(getTextBlockJson())); expectHostIds(elem, sourceElementId, category); const context = new IModelElementCloneContext(imodel, dstIModel); context.remapElement(sourceElementId, dstSourceElementId); context.remapElement(category, dstCategory); ElementDrivesTextAnnotation.remapFields(elem, context); expectHostIds(elem, dstSourceElementId, dstCategory); }); it("invalidates field host if source element not remapped", () => { const elem = createAnnotationElement(TextBlock.create(getTextBlockJson())); expectHostIds(elem, sourceElementId, category); const context = new IModelElementCloneContext(imodel, dstIModel); ElementDrivesTextAnnotation.remapFields(elem, context); expectHostIds(elem, Id64.invalid, Id64.invalid); }); }); }); describe("Format Validation", () => { it("validates formatting options for string property type", () => { // Create a FieldRun with string property type and some format options const fieldRun = FieldRun.create({ propertyHost: { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }, propertyPath: { propertyName: "strings", accessors: [0] }, cachedContent: "oldValue", formatOptions: { case: "upper", prefix: "Value: ", suffix: "!" } }); // Context returns a string value for the property const context = { hostElementId: sourceElementId, getProperty: () => { return { value: "abc", type: "string" }; }, }; // Update the field and check the result const updated = updateField(fieldRun, context); // The formatted value should be uppercased and have prefix/suffix applied expect(updated).to.be.true; expect(fieldRun.cachedContent).to.equal("Value: ABC!"); }); it("validates formatting options for datetime objects", function () { if (!isIntlSupported()) { this.skip(); } const propertyHost = { elementId: sourceElementId, schemaName: "Fields", className: "TestElement" }; const fieldRun = FieldRun.create({ propertyHost, propertyPath: { propertyName: "datetime" }, cachedContent: "oldval", formatOptions: { dateTime: { formatOptions: { month: "short", day: "2-digit", year: "numeric", timeZone: "UTC" }, locale: "en-US", }, }, }); const context = createUpdateContext(sourceElementId, imodel, false); const updated = updateField(fieldRun, context); expect(updated).to.be.true; expect(fieldRun.cachedContent).to.equal("Aug 28, 2025"); }); }); }); //# sourceMappingURL=Fields.test.js.map