@itwin/core-backend
Version:
iTwin.js backend components
832 lines (826 loc) • 43.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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