From d1b9f656b79c1fb23285f2f0730a221a3d666117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dmitrij=20Kuzmi=C4=8Diov?= <29233962+Yato333@users.noreply.github.com> Date: Mon, 4 Mar 2024 22:39:16 +0200 Subject: [PATCH] Presentation: Add functions that return async iterators for paged responses (#6472) Co-authored-by: yato333 Co-authored-by: Grigas <35135765+grigasp@users.noreply.github.com> --- common/api/presentation-common.api.md | 2 + common/api/presentation-frontend.api.md | 60 ++- .../summary/presentation-frontend.exports.csv | 4 +- ...ation-page-streaming_2024-02-27-14-22.json | 10 + ...ation-page-streaming_2024-02-27-14-22.json | 10 + ...ation-page-streaming_2024-02-29-09-30.json | 10 + ...ation-page-streaming_2024-03-04-12-33.json | 10 + docs/changehistory/NextVersion.md | 35 ++ full-stack-tests/presentation/src/Utils.ts | 11 + .../presentation/src/frontend/Content.test.ts | 251 +++++++----- .../src/frontend/Hierarchies.test.ts | 78 ++-- .../src/frontend/Localization.test.ts | 178 ++++---- .../src/frontend/RulesetVariables.test.ts | 13 +- .../src/frontend/Rulesets.test.ts | 17 +- .../ContentCustomization.test.ts | 87 ++-- .../RulesetVariables.test.ts | 19 +- .../common/MultiSchemaClasses.test.ts | 21 +- .../RelatedInstanceSpecification.test.ts | 72 ++-- .../RelationshipPathSpecification.test.ts | 10 +- ...tableRelationshipPathSpecification.test.ts | 63 ++- .../CalculatedPropertiesSpecification.test.ts | 40 +- .../DefaultPropertyCategoryOverride.test.ts | 20 +- .../PropertyCategorySpecification.test.ts | 14 +- .../PropertySpecification.test.ts | 24 +- .../RelatedPropertiesSpecification.test.ts | 18 +- .../content/rules/ContentModifier.test.ts | 37 +- .../content/rules/ContentRule.test.ts | 35 +- .../ContentInstancesOfSpecificClasses.test.ts | 29 +- .../ContentRelatedInstances.test.ts | 18 +- .../SelectedNodeInstances.test.ts | 24 +- .../content/specifications/Shared.test.ts | 42 +- .../customization/DisabledSortingRule.test.ts | 50 +-- .../customization/ExtendedDataRule.test.ts | 28 +- .../InstanceLabelOverride.test.ts | 107 ++--- .../customization/PropertySortingRule.test.ts | 63 +-- .../customization/SortingShared.test.ts | 11 +- .../hierarchies/grouping/ClassGroup.test.ts | 3 +- .../grouping/PropertyGroup.test.ts | 7 +- .../grouping/SameLabelInstanceGroup.test.ts | 5 +- .../hierarchies/grouping/Shared.test.ts | 17 +- .../hierarchies/rules/NodeArtifacts.test.ts | 5 +- .../hierarchies/rules/NodeRules.test.ts | 27 +- .../CustomNodeSpecification.test.ts | 15 +- ...tomQueryInstanceNodesSpecification.test.ts | 9 +- ...odesOfSpecificClassesSpecification.test.ts | 15 +- .../RelatedInstanceNodesSpecification.test.ts | 11 +- .../hierarchies/specifications/Shared.test.ts | 79 ++-- .../src/performance/DataViz.test.ts | 78 ++-- .../src/frontend/PresentationRpc.test.ts | 14 +- .../presentation-common/LocalizationHelper.ts | 2 +- .../common/src/test/_helpers/Promises.ts | 14 +- .../PresentationManager.ts | 386 +++++++++++------- .../StreamedResponseGenerator.ts | 144 +++++++ .../selection/HiliteSetProvider.ts | 12 +- .../src/test/PresentationManager.test.ts | 84 +--- .../test/StreamedResponseGenerator.test.ts | 235 +++++++++++ .../test/selection/HiliteSetProvider.test.ts | 104 +++-- 57 files changed, 1748 insertions(+), 1039 deletions(-) create mode 100644 common/changes/@itwin/presentation-common/presentation-page-streaming_2024-02-27-14-22.json create mode 100644 common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-27-14-22.json create mode 100644 common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-29-09-30.json create mode 100644 common/changes/@itwin/rpcinterface-full-stack-tests/presentation-page-streaming_2024-03-04-12-33.json create mode 100644 presentation/frontend/src/presentation-frontend/StreamedResponseGenerator.ts create mode 100644 presentation/frontend/src/test/StreamedResponseGenerator.test.ts diff --git a/common/api/presentation-common.api.md b/common/api/presentation-common.api.md index 0c343adc2881..8f391effc1e1 100644 --- a/common/api/presentation-common.api.md +++ b/common/api/presentation-common.api.md @@ -1683,6 +1683,8 @@ export class LocalizationHelper { // (undocumented) getLocalizedLabelDefinitions(labelDefinitions: LabelDefinition[]): LabelDefinition[]; // (undocumented) + getLocalizedNode(node: Node_2): Node_2; + // (undocumented) getLocalizedNodePathElement(npe: NodePathElement): NodePathElement; // (undocumented) getLocalizedNodes(nodes: Node_2[]): Node_2[]; diff --git a/common/api/presentation-frontend.api.md b/common/api/presentation-frontend.api.md index 68c9fdde2d74..4307e01c7716 100644 --- a/common/api/presentation-frontend.api.md +++ b/common/api/presentation-frontend.api.md @@ -34,6 +34,7 @@ import { IDisposable } from '@itwin/core-bentley'; import { IModelConnection } from '@itwin/core-frontend'; import { InstanceKey } from '@itwin/presentation-common'; import { InternetConnectivityStatus } from '@itwin/core-common'; +import { Item } from '@itwin/presentation-common'; import { Key } from '@itwin/presentation-common'; import { Keys } from '@itwin/presentation-common'; import { KeySet } from '@itwin/presentation-common'; @@ -44,7 +45,6 @@ import { NodeKey } from '@itwin/presentation-common'; import { NodePathElement } from '@itwin/presentation-common'; import { Paged } from '@itwin/presentation-common'; import { PagedResponse } from '@itwin/presentation-common'; -import { PageOptions } from '@itwin/presentation-common'; import { RegisteredRuleset } from '@itwin/presentation-common'; import { RpcRequestsHandler } from '@itwin/presentation-common'; import { Ruleset } from '@itwin/presentation-common'; @@ -78,9 +78,6 @@ export class BrowserLocalFavoritePropertiesStorage implements IFavoritePropertie savePropertiesOrder(orderInfos: FavoritePropertiesOrderInfo[], iTwinId: string | undefined, imodelId: string): Promise; } -// @internal (undocumented) -export const buildPagedArrayResponse: (requestedPage: PageOptions | undefined, getter: (page: Required, requestIndex: number) => Promise>) => Promise>; - // @beta export function consoleDiagnosticsHandler(diagnostics: ClientDiagnostics): void; @@ -165,9 +162,18 @@ export enum FavoritePropertiesScope { ITwin = 1 } +// @public +export type GetContentRequestOptions = ContentRequestOptions & ClientDiagnosticsAttribute; + +// @public +export type GetDistinctValuesRequestOptions = DistinctValuesRequestOptions & ClientDiagnosticsAttribute; + // @internal (undocumented) export const getFieldInfos: (field: Field) => Set; +// @public +export type GetNodesRequestOptions = HierarchyRequestOptions & ClientDiagnosticsAttribute; + // @public @deprecated export function getScopeId(scope: SelectionScope | string | undefined): string; @@ -240,6 +246,11 @@ export interface ISelectionProvider { selectionChange: SelectionChangeEvent; } +// @public +export type MultipleValuesRequestOptions = Paged<{ + maxParallelRequests?: number; +}>; + // @internal (undocumented) export class NoopFavoritePropertiesStorage implements IFavoritePropertiesStorage { // (undocumented) @@ -314,32 +325,55 @@ export class PresentationManager implements IDisposable { dispose(): void; // @internal ensureIModelInitialized(_: IModelConnection): Promise; - getContent(requestOptions: Paged> & ClientDiagnosticsAttribute): Promise; - getContentAndSize(requestOptions: Paged> & ClientDiagnosticsAttribute): Promise<{ + // @deprecated + getContent(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise; + // @deprecated + getContentAndSize(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise<{ content: Content; size: number; } | undefined>; getContentDescriptor(requestOptions: ContentDescriptorRequestOptions & ClientDiagnosticsAttribute): Promise; - getContentInstanceKeys(requestOptions: ContentInstanceKeysRequestOptions & ClientDiagnosticsAttribute): Promise<{ + getContentInstanceKeys(requestOptions: ContentInstanceKeysRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise<{ total: number; items: () => AsyncGenerator; }>; - getContentSetSize(requestOptions: ContentRequestOptions & ClientDiagnosticsAttribute): Promise; + getContentIterator(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise<{ + descriptor: Descriptor; + total: number; + items: AsyncIterableIterator; + } | undefined>; + getContentSetSize(requestOptions: GetContentRequestOptions): Promise; getContentSources(requestOptions: ContentSourcesRequestOptions & ClientDiagnosticsAttribute): Promise; getDisplayLabelDefinition(requestOptions: DisplayLabelRequestOptions & ClientDiagnosticsAttribute): Promise; - getDisplayLabelDefinitions(requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute): Promise; + // @deprecated + getDisplayLabelDefinitions(requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise; + getDisplayLabelDefinitionsIterator(requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise<{ + total: number; + items: AsyncIterableIterator; + }>; + getDistinctValuesIterator(requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions): Promise<{ + total: number; + items: AsyncIterableIterator; + }>; getElementProperties(requestOptions: SingleElementPropertiesRequestOptions & ClientDiagnosticsAttribute): Promise; getFilteredNodePaths(requestOptions: FilterByTextHierarchyRequestOptions & ClientDiagnosticsAttribute): Promise; getNodePaths(requestOptions: FilterByInstancePathsHierarchyRequestOptions & ClientDiagnosticsAttribute): Promise; - getNodes(requestOptions: Paged> & ClientDiagnosticsAttribute): Promise; - getNodesAndCount(requestOptions: Paged> & ClientDiagnosticsAttribute): Promise<{ + // @deprecated + getNodes(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise; + // @deprecated + getNodesAndCount(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<{ count: number; nodes: Node_2[]; }>; - getNodesCount(requestOptions: HierarchyRequestOptions & ClientDiagnosticsAttribute): Promise; + getNodesCount(requestOptions: GetNodesRequestOptions): Promise; // @beta getNodesDescriptor(requestOptions: HierarchyLevelDescriptorRequestOptions & ClientDiagnosticsAttribute): Promise; - getPagedDistinctValues(requestOptions: DistinctValuesRequestOptions & ClientDiagnosticsAttribute): Promise>; + getNodesIterator(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<{ + total: number; + items: AsyncIterableIterator; + }>; + // @deprecated + getPagedDistinctValues(requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions): Promise>; // @internal (undocumented) get ipcRequestsHandler(): IpcRequestsHandler | undefined; // @alpha diff --git a/common/api/summary/presentation-frontend.exports.csv b/common/api/summary/presentation-frontend.exports.csv index ff010b580b91..5cfdbab7e6e7 100644 --- a/common/api/summary/presentation-frontend.exports.csv +++ b/common/api/summary/presentation-frontend.exports.csv @@ -1,7 +1,6 @@ sep=; Release Tag;API Item internal;BrowserLocalFavoritePropertiesStorage -internal;buildPagedArrayResponse: beta;consoleDiagnosticsHandler(diagnostics: ClientDiagnostics): void beta;createCombinedDiagnosticsHandler(handlers: ClientDiagnosticsHandler[]): (diagnostics: ClientDiagnostics) => void public;createFavoritePropertiesStorage(type: DefaultFavoritePropertiesStorageTypes): IFavoritePropertiesStorage @@ -15,7 +14,10 @@ public;FavoritePropertiesManager public;FavoritePropertiesManagerProps public;FavoritePropertiesOrderInfo public;FavoritePropertiesScope +public;GetContentRequestOptions = ContentRequestOptions +public;GetDistinctValuesRequestOptions = DistinctValuesRequestOptions internal;getFieldInfos: (field: Field) => Set +public;GetNodesRequestOptions = HierarchyRequestOptions public;getScopeId(scope: SelectionScope | string | undefined): string deprecated;getScopeId(scope: SelectionScope | string | undefined): string internal;HILITE_RULESET: Ruleset diff --git a/common/changes/@itwin/presentation-common/presentation-page-streaming_2024-02-27-14-22.json b/common/changes/@itwin/presentation-common/presentation-page-streaming_2024-02-27-14-22.json new file mode 100644 index 000000000000..832da0bf5908 --- /dev/null +++ b/common/changes/@itwin/presentation-common/presentation-page-streaming_2024-02-27-14-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-common", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/presentation-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-27-14-22.json b/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-27-14-22.json new file mode 100644 index 000000000000..0e8c5b6da22b --- /dev/null +++ b/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-27-14-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-frontend", + "comment": "Add `getNodesIterator`, `getDistinctValuesIterator`, `getContentIterator` and `getDisplayLabelDefinitionIterator` methods to `PresentationManager` which return async iterators and load data using multiple parallel requests.", + "type": "none" + } + ], + "packageName": "@itwin/presentation-frontend" +} diff --git a/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-29-09-30.json b/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-29-09-30.json new file mode 100644 index 000000000000..723293ba33f8 --- /dev/null +++ b/common/changes/@itwin/presentation-frontend/presentation-page-streaming_2024-02-29-09-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-frontend", + "comment": "Deprecate `getNodes`, `getNodesAndCount`, `getContent`, `getContentAndSize`, `getPagedDistinctValues` and `getDisplayLabelDefinitions` in favor of the alternatives which return iterators.", + "type": "none" + } + ], + "packageName": "@itwin/presentation-frontend" +} \ No newline at end of file diff --git a/common/changes/@itwin/rpcinterface-full-stack-tests/presentation-page-streaming_2024-03-04-12-33.json b/common/changes/@itwin/rpcinterface-full-stack-tests/presentation-page-streaming_2024-03-04-12-33.json new file mode 100644 index 000000000000..f4ac0f33aed4 --- /dev/null +++ b/common/changes/@itwin/rpcinterface-full-stack-tests/presentation-page-streaming_2024-03-04-12-33.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/rpcinterface-full-stack-tests", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/rpcinterface-full-stack-tests" +} \ No newline at end of file diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index c6ce0ec5ddca..3ea379c69f02 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -9,6 +9,8 @@ Table of contents: - [Seafloor terrain](#seafloor-terrain) - [Electron 29 support](#electron-29-support) - [Editor](#editor) +- [Presentation](#presentation) + - [Deprecation of async array results in favor of async iterators](#deprecation-of-async-array-results-in-favor-of-async-iterators) ## Display @@ -47,3 +49,36 @@ Changes to @beta [CreateElementWithDynamicsTool]($editor-frontend) class: - [CreateElementWithDynamicsTool.doCreateElement]($editor-frontend) no longer takes an optional [ElementGeometryBuilderParams]($common) as it will be set on the supplied [GeometricElementProps]($common). +## Presentation + +### Deprecation of async array results in favor of async iterators + +`PresentationManager` contains a number of methods to retrieve sets of results like nodes, content, etc. All of these methods have been deprecated in favor of new ones that return an async iterator instead of an array: + +- Use `getContentIterator` instead of `getContent` and `getContentAndSize`. +- Use `getDisplayLabelDefinitionsIterator` instead of `getDisplayLabelDefinitions`. +- Use `getDistinctValuesIterator` instead of `getPagedDistinctValues`. +- Use `getNodesIterator` instead of `getNodes` and `getNodesAndCount`. + +All of the above methods, including the deprecated ones, received ability to load large sets of results concurrently. Previously, when requesting a large set (page size > 1000), the result was created by sending a number of requests sequentially by requesting the first page, then the second, and so on, until the whole requested set was retrieved. Now, we send a request for the first page to determine total number of items and backend's page size limit, together with the first page of results, and then other requests are made all at once. At attribute `maxParallelRequests` was added to these methods to control how many parallel requests should be sent at a time. + +While performance-wise deprecated methods should be in line with the newly added async iterator ones, the latter have two advantages: + +1. Caller can start iterating over results as soon as we receive the first page, instead of having to wait for the whole set. +2. The iterator version stops sending requests as soon as the caller stops iterating over the async iterator. + +> Example: Showing display labels for a large set of elements in a React component. +> +> The deprecated approach would be to use `PresentationManager.getDisplayLabelDefinitions` to retrieve labels of all elements. Because the API has to load all the data before returning it to the widget, user has to wait a long time to start seeing the results. Moreover, if he decides to close the widget (the component is unmounted), the `getDisplayLabelDefinitions` keeps building the array by sending requests to the backend - there's no way to cancel that. +> +> The new approach is to use `PresentationManager.getDisplayLabelDefinitionsIterator` to get the labels. The labels get streamed to the called as soon as the first results' page is retrieved, so user gets to see initial results quickly, while additional pages keep loading in the background. In addition, if the component is unmounted, iteration can be stopped, thus cancelling all further requests to the backend: +> +> ```ts +> const { items } = await manager.getDisplayLabelDefinitionsIterator(requestProps); +> for await (const label of items) { +> if (isComponentUnmounted) { +> break; +> } +> // update component's model to render the loaded label +> } +> ``` diff --git a/full-stack-tests/presentation/src/Utils.ts b/full-stack-tests/presentation/src/Utils.ts index 288543581305..5288834f4b81 100644 --- a/full-stack-tests/presentation/src/Utils.ts +++ b/full-stack-tests/presentation/src/Utils.ts @@ -102,3 +102,14 @@ export async function waitFor(check: () => Promise | T, timeout?: number): } while (timer.current.milliseconds < timeout); throw lastError; } + +/** + * Collects items of an async iterable to an array. + */ +export async function collect(iter: AsyncIterable): Promise { + const result = new Array(); + for await (const item of iter) { + result.push(item); + } + return result; +} diff --git a/full-stack-tests/presentation/src/frontend/Content.test.ts b/full-stack-tests/presentation/src/frontend/Content.test.ts index 980bc6e87b9c..fb7d148a491e 100644 --- a/full-stack-tests/presentation/src/frontend/Content.test.ts +++ b/full-stack-tests/presentation/src/frontend/Content.test.ts @@ -9,6 +9,7 @@ import { assert, BeDuration, BeTimePoint, Guid, Id64, using } from "@itwin/core- import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; import { ChildNodeSpecificationTypes, + Content, ContentFlags, ContentSpecificationTypes, DefaultContentDisplayTypes, @@ -32,7 +33,7 @@ import { import { Presentation, PresentationManager, PresentationManagerProps } from "@itwin/presentation-frontend"; import { ECClassHierarchy, ECClassHierarchyInfo } from "../ECClasHierarchy"; import { initialize, terminate } from "../IntegrationTests"; -import { getFieldByLabel } from "../Utils"; +import { collect, getFieldByLabel } from "../Utils"; import { buildTestIModelConnection, insertDocumentPartition, insertPhysicalElement, insertPhysicalModel, insertSpatialCategory } from "../IModelSetupUtils"; import { UnitSystemKey } from "@itwin/core-quantity"; import { SchemaContext } from "@itwin/ecschema-metadata"; @@ -85,24 +86,26 @@ describe("Content", () => { }, ], }; - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: { - contentFlags: ContentFlags.IncludeInputKeys, - }, - - keys: new KeySet([ - { - className: "BisCore:Element", - id: "0x1", - }, - { - className: "BisCore:Element", - id: "0x12", + const content = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: { + contentFlags: ContentFlags.IncludeInputKeys, }, - ]), - }); + + keys: new KeySet([ + { + className: "BisCore:Element", + id: "0x1", + }, + { + className: "BisCore:Element", + id: "0x12", + }, + ]), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content?.contentSet.length).to.eq(9); expect(content!.contentSet.map((item) => ({ itemId: item.primaryKeys[0].id, inputIds: item.inputKeys!.map((ik) => ik.id) }))).to.containSubset([ { @@ -155,7 +158,9 @@ describe("Content", () => { expectedResult: DisplayValueGroup[], ) { // first request all pages and confirm the result is valid - const allDistinctValues = await Presentation.presentation.getPagedDistinctValues({ imodel: db, rulesetOrId: ruleset, keys, descriptor, fieldDescriptor }); + const allDistinctValues = await Presentation.presentation + .getDistinctValuesIterator({ imodel: db, rulesetOrId: ruleset, keys, descriptor, fieldDescriptor }) + .then(async (x) => ({ ...x, items: await collect(x.items) })); expect(allDistinctValues).to.be.deep.equal({ total: expectedResult.length, items: expectedResult, @@ -165,14 +170,16 @@ describe("Content", () => { const pageSize = 2; const pagesCount = Math.ceil(expectedResult.length / pageSize); for (let i = 0; i < pagesCount; ++i) { - const pagedDistinctValues = await Presentation.presentation.getPagedDistinctValues({ - imodel: db, - rulesetOrId: ruleset, - keys, - descriptor, - fieldDescriptor, - paging: { size: pageSize, start: i * pageSize }, - }); + const pagedDistinctValues = await Presentation.presentation + .getDistinctValuesIterator({ + imodel: db, + rulesetOrId: ruleset, + keys, + descriptor, + fieldDescriptor, + paging: { size: pageSize, start: i * pageSize }, + }) + .then(async (x) => ({ ...x, items: await collect(x.items) })); expect(pagedDistinctValues).to.be.deep.equal({ total: expectedResult.length, items: expectedResult.slice(i * pageSize, (i + 1) * pageSize), @@ -440,7 +447,7 @@ describe("Content", () => { }, ], }; - const rootNodes = await Presentation.presentation.getNodes({ imodel: testIModel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel: testIModel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.eq(2); const descriptor = await Presentation.presentation.getNodesDescriptor({ imodel: testIModel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); assert(!!descriptor); @@ -595,26 +602,30 @@ describe("Content", () => { ], }; - const content1 = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: {}, - keys: new KeySet(), - }); + const content1 = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: {}, + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content1?.contentSet.length).to.eq(1); const fieldsCount = content1!.descriptor.fields.length; - const content2 = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: { - fieldsSelector: { - type: "exclude", - fields: [content1!.descriptor.fields[0].getFieldDescriptor()], + const content2 = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: { + fieldsSelector: { + type: "exclude", + fields: [content1!.descriptor.fields[0].getFieldDescriptor()], + }, }, - }, - keys: new KeySet(), - }); + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content2?.contentSet.length).to.eq(1); expect(content2!.descriptor.fields.length).to.eq(fieldsCount - 1); }); @@ -636,26 +647,30 @@ describe("Content", () => { ], }; - const content1 = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: {}, - keys: new KeySet(), - }); + const content1 = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: {}, + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content1?.contentSet.length).to.eq(1); expect(content1!.descriptor.fields.length).to.be.greaterThan(1); - const content2 = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: { - fieldsSelector: { - type: "include", - fields: [content1!.descriptor.fields[0].getFieldDescriptor()], + const content2 = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: { + fieldsSelector: { + type: "include", + fields: [content1!.descriptor.fields[0].getFieldDescriptor()], + }, }, - }, - keys: new KeySet(), - }); + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content2?.contentSet.length).to.eq(1); expect(content2!.descriptor.fields.length).to.eq(1); }); @@ -689,12 +704,14 @@ describe("Content", () => { ], }; - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - descriptor: {}, - keys: new KeySet(), - }); + const content = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: {}, + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); const field = getFieldByLabel(content!.descriptor.fields, "Test"); expect(content?.contentSet.length).to.eq(1); @@ -730,12 +747,14 @@ describe("Content", () => { }, ], }; - const content = await Presentation.presentation.getContent({ - imodel: imodelConnection, - rulesetOrId: ruleset, - keys: new KeySet([instanceKey!]), - descriptor: {}, - }); + const content = await Presentation.presentation + .getContentIterator({ + imodel: imodelConnection, + rulesetOrId: ruleset, + keys: new KeySet([instanceKey!]), + descriptor: {}, + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); const field = getFieldByLabel(content!.descriptor.fields, "Federation GUID"); expect(content?.contentSet.length).to.eq(1); @@ -806,12 +825,14 @@ describe("Content", () => { }, ], }; - const content = await Presentation.presentation.getContent({ - imodel: imodelConnection, - rulesetOrId: ruleset, - keys: new KeySet([instanceKey!]), - descriptor: {}, - }); + const content = await Presentation.presentation + .getContentIterator({ + imodel: imodelConnection, + rulesetOrId: ruleset, + keys: new KeySet([instanceKey!]), + descriptor: {}, + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content!.descriptor.categories).to.containSubset([{ label: "Document Partition" }, { label: "Custom Category" }]); @@ -845,17 +866,19 @@ describe("Content", () => { ], }; - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - keys: new KeySet(), - descriptor: { - instanceFilter: { - selectClassName: "PCJ_TestSchema:TestClass", - expression: 'this.String_Property_4 = "Yoda"', + const content = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + keys: new KeySet(), + descriptor: { + instanceFilter: { + selectClassName: "PCJ_TestSchema:TestClass", + expression: 'this.String_Property_4 = "Yoda"', + }, }, - }, - }); + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content?.contentSet.length).to.be.eq(6); }); @@ -876,30 +899,32 @@ describe("Content", () => { ], }; - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - keys: new KeySet(), - descriptor: { - instanceFilter: { - selectClassName: "Generic:PhysicalObject", - expression: "related.Btu__x002F__lb__x0020____x005B__Btu__x0020__per__x0020__pound__x0020__mass__x005D__ = 1475.699", - relatedInstances: [ - { - pathFromSelectToPropertyClass: [ - { - sourceClassName: "Generic:PhysicalObject", - targetClassName: "DgnCustomItemTypes_MyProp:area__x0020__per__x0020__time__x0020__squaredElementAspect", - relationshipName: "BisCore:ElementOwnsMultiAspects", - isForwardRelationship: true, - }, - ], - alias: "related", - }, - ], + const content = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + keys: new KeySet(), + descriptor: { + instanceFilter: { + selectClassName: "Generic:PhysicalObject", + expression: "related.Btu__x002F__lb__x0020____x005B__Btu__x0020__per__x0020__pound__x0020__mass__x005D__ = 1475.699", + relatedInstances: [ + { + pathFromSelectToPropertyClass: [ + { + sourceClassName: "Generic:PhysicalObject", + targetClassName: "DgnCustomItemTypes_MyProp:area__x0020__per__x0020__time__x0020__squaredElementAspect", + relationshipName: "BisCore:ElementOwnsMultiAspects", + isForwardRelationship: true, + }, + ], + alias: "related", + }, + ], + }, }, - }, - }); + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content?.contentSet.length).to.be.eq(1); }); @@ -1146,7 +1171,9 @@ describe("Content", () => { }); expect(descriptor).to.not.be.undefined; const field = getFieldByLabel(descriptor!.fields, "cm2"); - const content = await manager.getContent({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, unitSystem }); + const content = await manager + .getContentIterator({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, unitSystem }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); const displayValues = content!.contentSet[0].values.rc_generic_PhysicalObject_ncc_MyProp_areaElementAspect as DisplayValuesArray; expect(displayValues.length).is.eq(1); return ((displayValues[0] as DisplayValuesMap).displayValues as DisplayValuesMap)[field.name]!; diff --git a/full-stack-tests/presentation/src/frontend/Hierarchies.test.ts b/full-stack-tests/presentation/src/frontend/Hierarchies.test.ts index a5b63a66fa30..5ab6b5b5eb24 100644 --- a/full-stack-tests/presentation/src/frontend/Hierarchies.test.ts +++ b/full-stack-tests/presentation/src/frontend/Hierarchies.test.ts @@ -30,6 +30,7 @@ import { import { Presentation, PresentationManager } from "@itwin/presentation-frontend"; import { initialize, resetBackend, terminate } from "../IntegrationTests"; import { buildTestIModelConnection, insertDocumentPartition } from "../IModelSetupUtils"; +import { collect } from "../Utils"; describe("Hierarchies", () => { before(async () => { @@ -689,7 +690,8 @@ describe("Hierarchies", () => { expression: `TRUE`, }, }; - await expect(Presentation.presentation.getNodes(requestParams)).to.eventually.be.rejectedWith(PresentationError); + const iteratorPromise = Presentation.presentation.getNodesIterator(requestParams); + await expect(iteratorPromise).to.eventually.be.rejectedWith(PresentationError); }); }); @@ -751,7 +753,8 @@ describe("Hierarchies", () => { }); it("throws when result set size exceeds given limit", async () => { - await expect(Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, sizeLimit: 1 })).to.eventually.be.rejectedWith(PresentationError); + const iteratorPromise = Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, sizeLimit: 1 }); + await expect(iteratorPromise).to.eventually.be.rejectedWith(PresentationError); }); }); @@ -839,11 +842,10 @@ describe("Hierarchies", () => { }); it("throws when result set size exceeds given limit", async () => { - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); const rootSubject = rootNodes[0]; - await expect( - Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootSubject.key, sizeLimit: 1 }), - ).to.eventually.be.rejectedWith(PresentationError); + const iteratorPromise = Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootSubject.key, sizeLimit: 1 }); + await expect(iteratorPromise).to.eventually.be.rejectedWith(PresentationError); }); }); @@ -955,22 +957,32 @@ describe("Hierarchies", () => { }); it("throws when result set size exceeds given limit", async () => { - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); const classGroupingNode = classGroupingNodes[0]; await expect( - Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key, sizeLimit: 2 }), + Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key, sizeLimit: 2 }) + .then(async ({ items }) => collect(items)), ).to.eventually.be.rejectedWith(PresentationError); - const propertyGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key }); + const propertyGroupingNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key }) + .then(async (x) => collect(x.items)); const propertyGroupingNode = propertyGroupingNodes[0]; await expect( - Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: propertyGroupingNode.key, sizeLimit: 2 }), + Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: propertyGroupingNode.key, sizeLimit: 2 }) + .then(async ({ items }) => collect(items)), ).to.eventually.be.rejectedWith(PresentationError); - const labelGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: propertyGroupingNode.key }); + const labelGroupingNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: propertyGroupingNode.key }) + .then(async (x) => collect(x.items)); const labelGroupingNode = labelGroupingNodes[0]; await expect( - Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: labelGroupingNode.key, sizeLimit: 1 }), + Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: labelGroupingNode.key, sizeLimit: 1 }) + .then(async ({ items }) => collect(items)), ).to.eventually.be.rejectedWith(PresentationError); }); }); @@ -1082,7 +1094,7 @@ describe("Hierarchies", () => { ], }; - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.eq(1); const result = await Presentation.presentation.getNodesDescriptor({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); @@ -1475,7 +1487,7 @@ describe("Hierarchies", () => { ], }; await using>(await Presentation.presentation.rulesets().add(ruleset), async () => { - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset.id }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset.id }).then(async (x) => collect(x.items)); expect(rootNodes).to.matchSnapshot(); /* The result should look like this (all grouping nodes): @@ -1495,16 +1507,20 @@ describe("Hierarchies", () => { the result should be 1 + 1 + 2 = 4 */ - const definitionModelNodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset.id, - parentKey: rootNodes[0].key, - }); - const dictionaryModelNodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset.id, - parentKey: rootNodes[1].key, - }); + const definitionModelNodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset.id, + parentKey: rootNodes[0].key, + }) + .then(async (x) => collect(x.items)); + const dictionaryModelNodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset.id, + parentKey: rootNodes[1].key, + }) + .then(async (x) => collect(x.items)); const keys = new KeySet([ definitionModelNodes[0].key, @@ -1568,16 +1584,18 @@ describe("Hierarchies", () => { }; const props = { imodel, rulesetOrId: ruleset }; - const rootNodes = await frontend.getNodes(props); + const rootNodes = await frontend.getNodesIterator(props).then(async (x) => collect(x.items)); expect(rootNodes.length).to.eq(1); expect(rootNodes[0].key.type).to.eq("root"); resetBackend(); - const childNodes = await frontend.getNodes({ - ...props, - parentKey: rootNodes[0].key, - }); + const childNodes = await frontend + .getNodesIterator({ + ...props, + parentKey: rootNodes[0].key, + }) + .then(async (x) => collect(x.items)); expect(childNodes.length).to.eq(1); expect(childNodes[0].key.type).to.eq("child"); }); @@ -1737,7 +1755,7 @@ async function validateHierarchy(props: { props.configureParams(requestParams); } - const nodes = await manager.getNodes(requestParams); + const nodes = await manager.getNodesIterator(requestParams).then(async (x) => collect(x.items)); if (nodes.length !== props.expectedHierarchy.length) { throw new Error(`Expected ${props.expectedHierarchy.length} nodes, got ${nodes.length}`); diff --git a/full-stack-tests/presentation/src/frontend/Localization.test.ts b/full-stack-tests/presentation/src/frontend/Localization.test.ts index 88f5d4396b45..2bd50b1e53f5 100644 --- a/full-stack-tests/presentation/src/frontend/Localization.test.ts +++ b/full-stack-tests/presentation/src/frontend/Localization.test.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; import { IModelApp, IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; -import { ChildNodeSpecificationTypes, DefaultContentDisplayTypes, KeySet, Ruleset, RuleTypes } from "@itwin/presentation-common"; +import { ChildNodeSpecificationTypes, Content, DefaultContentDisplayTypes, KeySet, Ruleset, RuleTypes } from "@itwin/presentation-common"; import { Presentation, PresentationManager } from "@itwin/presentation-frontend"; import { initialize, terminate, testLocalization } from "../IntegrationTests"; -import { getFieldByLabel } from "../Utils"; +import { collect, getFieldByLabel } from "../Utils"; describe("Localization", async () => { let imodel: IModelConnection; @@ -27,14 +27,15 @@ describe("Localization", async () => { }); it("localizes nodes", async () => { - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }).then(async (x) => collect(x.items)); expect(nodes.length).to.eq(1); expect(nodes[0].label.displayValue).to.eq("_test_ string"); expect(nodes[0].description).to.eq("_test_ nested string"); }); it("localizes nodes when requesting with count", async () => { - const { nodes } = await Presentation.presentation.getNodesAndCount({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }); + const { items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }); + const nodes = await collect(items); expect(nodes.length).to.eq(1); expect(nodes[0].label.displayValue).to.eq("_test_ string"); expect(nodes[0].description).to.eq("_test_ nested string"); @@ -162,41 +163,43 @@ describe("Localization", async () => { }); it("localizes content", async () => { - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: { - id: "content", - rules: [ - { - ruleType: "Content", - specifications: [ - { - specType: "ContentInstancesOfSpecificClasses", - classes: { schemaName: "BisCore", classNames: ["Subject"] }, - calculatedProperties: [ - { - label: "@Test:string@", - value: `"@Test:nested.string@"`, - }, - ], + const content = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: { + id: "content", + rules: [ + { + ruleType: "Content", + specifications: [ + { + specType: "ContentInstancesOfSpecificClasses", + classes: { schemaName: "BisCore", classNames: ["Subject"] }, + calculatedProperties: [ + { + label: "@Test:string@", + value: `"@Test:nested.string@"`, + }, + ], + }, + ], + }, + { + ruleType: "DefaultPropertyCategoryOverride", + specification: { + id: "default", + label: "@Test:string@", + description: "@Test:nested.string@", }, - ], - }, - { - ruleType: "DefaultPropertyCategoryOverride", - specification: { - id: "default", - label: "@Test:string@", - description: "@Test:nested.string@", }, - }, - ], - }, - descriptor: { - displayType: DefaultContentDisplayTypes.PropertyPane, - }, - keys: new KeySet(), - }); + ], + }, + descriptor: { + displayType: DefaultContentDisplayTypes.PropertyPane, + }, + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content!.descriptor.categories[0].label).to.eq("_test_ string"); expect(content!.descriptor.categories[0].description).to.eq("_test_ nested string"); @@ -206,47 +209,49 @@ describe("Localization", async () => { }); it("localizes content when requesting with size", async () => { - const { content } = (await Presentation.presentation.getContentAndSize({ - imodel, - rulesetOrId: { - id: "content", - rules: [ - { - ruleType: "Content", - specifications: [ - { - specType: "ContentInstancesOfSpecificClasses", - classes: { schemaName: "BisCore", classNames: ["Subject"] }, - calculatedProperties: [ - { - label: "@Test:string@", - value: `"@Test:nested.string@"`, - }, - ], + const { descriptor, items } = (await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: { + id: "content", + rules: [ + { + ruleType: "Content", + specifications: [ + { + specType: "ContentInstancesOfSpecificClasses", + classes: { schemaName: "BisCore", classNames: ["Subject"] }, + calculatedProperties: [ + { + label: "@Test:string@", + value: `"@Test:nested.string@"`, + }, + ], + }, + ], + }, + { + ruleType: "DefaultPropertyCategoryOverride", + specification: { + id: "default", + label: "@Test:string@", + description: "@Test:nested.string@", }, - ], - }, - { - ruleType: "DefaultPropertyCategoryOverride", - specification: { - id: "default", - label: "@Test:string@", - description: "@Test:nested.string@", }, - }, - ], - }, - descriptor: { - displayType: DefaultContentDisplayTypes.PropertyPane, - }, - keys: new KeySet(), - }))!; + ], + }, + descriptor: { + displayType: DefaultContentDisplayTypes.PropertyPane, + }, + keys: new KeySet(), + }) + .then(async (x) => x && { ...x, items: await collect(x.items) }))!; - expect(content.descriptor.categories[0].label).to.eq("_test_ string"); - expect(content.descriptor.categories[0].description).to.eq("_test_ nested string"); + expect(descriptor.categories[0].label).to.eq("_test_ string"); + expect(descriptor.categories[0].description).to.eq("_test_ nested string"); - const field = getFieldByLabel(content.descriptor.fields, "_test_ string"); - expect(content.contentSet[0].displayValues[field.name]).to.eq("_test_ nested string"); + const field = getFieldByLabel(descriptor.fields, "_test_ string"); + expect(items[0].displayValues[field.name]).to.eq("_test_ nested string"); }); it("localizes distinct values", async () => { @@ -277,14 +282,16 @@ describe("Localization", async () => { keys: new KeySet(), }); const field = getFieldByLabel(descriptor!.fields, "_test_ string"); - const distinctValues = await Presentation.presentation.getPagedDistinctValues({ - imodel, - rulesetOrId: ruleset, - descriptor: descriptor!, - keys: new KeySet(), - fieldDescriptor: field.getFieldDescriptor(), - }); - expect(distinctValues.items[0].displayValue).to.eq("_test_ nested string"); + const distinctValues = await Presentation.presentation + .getDistinctValuesIterator({ + imodel, + rulesetOrId: ruleset, + descriptor: descriptor!, + keys: new KeySet(), + fieldDescriptor: field.getFieldDescriptor(), + }) + .then(async (x) => collect(x.items)); + expect(distinctValues[0].displayValue).to.eq("_test_ nested string"); }); describe("Multiple frontends for one backend", async () => { @@ -301,10 +308,9 @@ describe("Localization", async () => { it("handles multiple simultaneous requests from different frontends with different locales", async () => { await Promise.all( Array.from({ length: 100 }).map(async () => { - const [en, test] = await Promise.all([ - await frontends[0].getNodes({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }), - await frontends[1].getNodes({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }), - ]); + const [en, test] = await Promise.all( + frontends.map(async (frontend) => frontend.getNodesIterator({ imodel, rulesetOrId: CUSTOM_NODES_RULESET }).then(async (x) => collect(x.items))), + ); expect(en[0].label.displayValue).to.eq("test value"); expect(en[0].description).to.eq("test nested value"); diff --git a/full-stack-tests/presentation/src/frontend/RulesetVariables.test.ts b/full-stack-tests/presentation/src/frontend/RulesetVariables.test.ts index 09df8cbb9fea..8e4e70019eb8 100644 --- a/full-stack-tests/presentation/src/frontend/RulesetVariables.test.ts +++ b/full-stack-tests/presentation/src/frontend/RulesetVariables.test.ts @@ -6,10 +6,11 @@ import { expect } from "chai"; import * as faker from "faker"; import { Guid, Id64 } from "@itwin/core-bentley"; import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; -import { ChildNodeSpecificationTypes, ContentSpecificationTypes, KeySet, Ruleset, RuleTypes } from "@itwin/presentation-common"; +import { ChildNodeSpecificationTypes, Content, ContentSpecificationTypes, KeySet, Ruleset, RuleTypes } from "@itwin/presentation-common"; import { createRandomId } from "@itwin/presentation-common/lib/cjs/test"; import { Presentation, PresentationManager, RulesetVariablesManager } from "@itwin/presentation-frontend"; import { initialize, resetBackend, terminate } from "../IntegrationTests"; +import { collect } from "../Utils"; const RULESET: Ruleset = { id: "ruleset vars test", @@ -262,7 +263,7 @@ describe("Ruleset Variables", async () => { it("handles multiple simultaneous requests from different frontends with ruleset variables", async () => { for (let i = 0; i < 100; ++i) { frontends.forEach(async (f, fi) => f.vars(RULESET.id).setString("variable_id", `${i}_${fi}`)); - const nodes = await Promise.all(frontends.map(async (f) => f.getNodes({ imodel, rulesetOrId: RULESET }))); + const nodes = await Promise.all(frontends.map(async (f) => f.getNodesIterator({ imodel, rulesetOrId: RULESET }).then(async (x) => collect(x.items)))); frontends.forEach((_f, fi) => { expect(nodes[fi][0].label.displayValue).to.eq(`${i}_${fi}`); }); @@ -335,7 +336,9 @@ describe("Ruleset Variables", async () => { ], }; - let content = await Presentation.presentation.getContent({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {} }); + let content = await Presentation.presentation + .getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {} }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content!.contentSet.length).to.eq(0); // https://www.sqlite.org/limits.html#max_variable_number @@ -345,7 +348,9 @@ describe("Ruleset Variables", async () => { ids.push("0x61"); await Presentation.presentation.vars(ruleset.id).setId64s("ids", ids); - content = await Presentation.presentation.getContent({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {} }); + content = await Presentation.presentation + .getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {} }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); expect(content!.contentSet.length).to.eq(1); expect(content!.contentSet[0].primaryKeys[0]).to.deep.eq({ className: "PCJ_TestSchema:TestClass", id: "0x61" }); }); diff --git a/full-stack-tests/presentation/src/frontend/Rulesets.test.ts b/full-stack-tests/presentation/src/frontend/Rulesets.test.ts index 6236e9c73179..de96ab0d2fa5 100644 --- a/full-stack-tests/presentation/src/frontend/Rulesets.test.ts +++ b/full-stack-tests/presentation/src/frontend/Rulesets.test.ts @@ -8,6 +8,7 @@ import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; import { ChildNodeSpecificationTypes, RegisteredRuleset, Ruleset, RuleTypes } from "@itwin/presentation-common"; import { Presentation, PresentationManager } from "@itwin/presentation-frontend"; import { initialize, resetBackend, terminate } from "../IntegrationTests"; +import { collect } from "../Utils"; const RULESET_1: Ruleset = { id: "ruleset_1", @@ -57,7 +58,7 @@ describe("Rulesets", async () => { it("creates ruleset from json and gets root node using it", async () => { await using>(await Presentation.presentation.rulesets().add(RULESET_1), async () => { - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: RULESET_1.id }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: RULESET_1.id }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.be.equal(1); expect(rootNodes[0].label.displayValue).to.equal("label 1"); }); @@ -66,11 +67,11 @@ describe("Rulesets", async () => { it("removes ruleset", async () => { const registeredRuleset = await Presentation.presentation.rulesets().add(RULESET_1); - let rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: RULESET_1.id }); + let rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: RULESET_1.id }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.be.equal(1); expect(await Presentation.presentation.rulesets().remove(registeredRuleset)).to.be.true; - rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: RULESET_1.id }); + rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: RULESET_1.id }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.be.equal(0); expect(await Presentation.presentation.rulesets().remove(registeredRuleset)).to.be.false; @@ -86,11 +87,11 @@ describe("Rulesets", async () => { it("clears rulesets from frontend", async () => { await Presentation.presentation.rulesets().add(RULESET_1); - let rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: RULESET_1.id }); + let rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: RULESET_1.id }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.be.equal(1); await Presentation.presentation.rulesets().clear(); - rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: RULESET_1.id }); + rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: RULESET_1.id }).then(async (x) => collect(x.items)); expect(rootNodes.length).to.be.equal(0); }); @@ -119,7 +120,7 @@ describe("Rulesets", async () => { const registeredRulesets = await Promise.all(frontends.map(async (f, i) => f.rulesets().add(rulesets[i]))); - const nodes = await Promise.all(frontends.map(async (f) => f.getNodes({ imodel, rulesetOrId: "test" }))); + const nodes = await Promise.all(frontends.map(async (f) => f.getNodesIterator({ imodel, rulesetOrId: "test" }).then(async (x) => collect(x.items)))); frontends.forEach((_f, i) => { expect(nodes[i][0].label.displayValue).to.eq(`label ${i + 1}`); }); @@ -142,13 +143,13 @@ describe("Rulesets", async () => { it("can use the same frontend-registered ruleset after backend is reset", async () => { const props = { imodel, rulesetOrId: RULESET_1.id }; await using>(await frontend.rulesets().add(RULESET_1), async () => { - const rootNodes1 = await frontend.getNodes(props); + const rootNodes1 = await frontend.getNodesIterator(props).then(async (x) => collect(x.items)); expect(rootNodes1.length).to.be.equal(1); expect(rootNodes1[0].label.displayValue).to.be.equal("label 1"); resetBackend(); - const rootNodes2 = await frontend.getNodes(props); + const rootNodes2 = await frontend.getNodesIterator(props).then(async (x) => collect(x.items)); expect(rootNodes2.length).to.be.equal(1); expect(rootNodes2[0].label.displayValue).to.be.equal("label 1"); expect(rootNodes2).to.deep.eq(rootNodes1); diff --git a/full-stack-tests/presentation/src/learning-snippets/ContentCustomization.test.ts b/full-stack-tests/presentation/src/learning-snippets/ContentCustomization.test.ts index 143f61287f94..8fa23645560a 100644 --- a/full-stack-tests/presentation/src/learning-snippets/ContentCustomization.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/ContentCustomization.test.ts @@ -64,13 +64,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - we should use the "OLD" default category - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - const defaultCategory = content.descriptor.categories.find((category) => category.name === "default"); + }); + const defaultCategory = content!.descriptor.categories.find((category) => category.name === "default"); expect(defaultCategory).to.containSubset({ label: "Custom Category OLD", }); @@ -113,7 +113,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - we should use the "OLD" default category - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -153,7 +153,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the default property category is correctly set up - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -204,7 +204,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field is assigned a category with correct label - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -251,7 +251,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field is assigned a category with correct label - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -300,13 +300,13 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertyCategorySpecification.Description.Result // Ensure category description is assigned - const content = (await Presentation.presentation.getContent({ + const descriptor = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - expect(content.descriptor.categories).to.containSubset([ + }))!.descriptor; + expect(descriptor.categories).to.containSubset([ { label: "Custom Category", description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", @@ -353,7 +353,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure categories' hierarchy was set up correctly - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -415,7 +415,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertyCategorySpecification.Priority.Result // Ensure that correct category priorities are assigned - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -477,7 +477,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertyCategorySpecification.AutoExpand.Result // Ensure that categories have the `expand` flag - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -530,7 +530,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertyCategorySpecification.Renderer.Result // Ensure that categories have the `expand` flag - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -589,7 +589,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field is assigned attributes from both specifications - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -635,7 +635,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field is assigned attributes from both specifications - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -681,7 +681,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field has the correct category - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -725,7 +725,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `LastMod` is there - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -767,7 +767,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` property is not the only property in content - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -812,7 +812,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertySpecification.Renderer.Result // Ensure the `UserLabel` field is assigned the "my-renderer" renderer - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -859,7 +859,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertySpecification.Editor.Result // Ensure the `UserLabel` field is assigned the "my-editor" editor - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -904,7 +904,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Content.Customization.PropertySpecification.IsReadOnly.Result // Ensure the `UserLabel` field is read-only. - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -946,7 +946,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field's priority is 9999, which makes it appear higher in the property grid. - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -989,7 +989,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -1030,22 +1030,19 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created and has a value - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "generic.PhysicalObject", id: "0x74" }]), descriptor: {}, }))!; const field = getFieldByLabel(content.descriptor.fields, "Element Volume"); - expect(content.contentSet) - .to.have.lengthOf(1) - .and.to.containSubset([ - { - values: { - [field.name]: "3.449493952966681", - }, - }, - ]); + expect(content.total).to.eq(1); + expect((await content.items.next()).value).to.containSubset({ + values: { + [field.name]: "3.449493952966681", + }, + }); }); it("uses `priority` attribute", async () => { @@ -1077,7 +1074,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property has correct priority - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -1125,7 +1122,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x12" }]), @@ -1182,7 +1179,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -1190,7 +1187,9 @@ describe("Learning Snippets", () => { }))!; const childElementField = getFieldByLabel(content.descriptor.fields, "Element") as NestedContentField; const childElementCodeField = getFieldByLabel(childElementField.nestedFields, "Code"); - expect(content.contentSet[0].values[childElementField.name]) + + expect(content.total).to.be.greaterThan(0); + expect((await content.items.next()).value.values[childElementField.name]) .to.have.lengthOf(2) .and.to.containSubset([ { @@ -1242,7 +1241,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x12" }]), @@ -1304,7 +1303,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that all related properties are placed into a category nested under the default category - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -1379,7 +1378,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the two related properties are picked up - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -1433,7 +1432,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field has `autoExpand` attribute set to `true` - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -1514,7 +1513,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure only one related property is loaded - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -1573,7 +1572,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure properties of physical partition and repository link are loaded - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), diff --git a/full-stack-tests/presentation/src/learning-snippets/RulesetVariables.test.ts b/full-stack-tests/presentation/src/learning-snippets/RulesetVariables.test.ts index f29ed497f78a..2974063a7bf9 100644 --- a/full-stack-tests/presentation/src/learning-snippets/RulesetVariables.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/RulesetVariables.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../IntegrationTests"; import { printRuleset } from "./Utils"; +import { collect } from "../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -58,14 +59,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // No variable set - the request should return 0 nodes - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes.length).to.eq(0); // Set variable to "models" and ensure we get model grouping nodes // __PUBLISH_EXTRACT_START__ Presentation.RulesetVariables.InRuleCondition.SetToModels await Presentation.presentation.vars(ruleset.id).setString("TREE_TYPE", "models"); // __PUBLISH_EXTRACT_END__ - const modelNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const modelNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(modelNodes).to.containSubset([ { label: { displayValue: "Definition Model" }, @@ -94,7 +95,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.RulesetVariables.InRuleCondition.SetToElements await Presentation.presentation.vars(ruleset.id).setString("TREE_TYPE", "elements"); // __PUBLISH_EXTRACT_END__ - const elementNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const elementNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(elementNodes).to.containSubset([ { label: { displayValue: "3D Display Style" }, @@ -177,7 +178,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // No variable set - the request should return grouping nodes of all elements - let nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + let nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "3D Display Style" }, @@ -239,7 +240,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.RulesetVariables.InInstanceFilter.SetIds await Presentation.presentation.vars(ruleset.id).setId64s("ELEMENT_IDS", ["0x1", "0x74", "0x40"]); // __PUBLISH_EXTRACT_END__ - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "Physical Object" }, @@ -254,7 +255,7 @@ describe("Learning Snippets", () => { // Set the value to different element IDs and ensure we get their class grouping nodes await Presentation.presentation.vars(ruleset.id).setId64s("ELEMENT_IDS", ["0x17", "0x16"]); - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "Definition Partition" }, @@ -268,7 +269,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.RulesetVariables.InInstanceFilter.Unset await Presentation.presentation.vars(ruleset.id).unset("ELEMENT_IDS"); // __PUBLISH_EXTRACT_END__ - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "3D Display Style" }, @@ -363,7 +364,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // No variable set - the request should return nodes without any prefix - let nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + let nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "Default - View 1" }, @@ -383,7 +384,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.RulesetVariables.InCustomizationRuleValueExpression.SetValue await Presentation.presentation.vars(ruleset.id).setString("PREFIX", "test"); // __PUBLISH_EXTRACT_END__ - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.containSubset([ { label: { displayValue: "test Default - View 1" }, diff --git a/full-stack-tests/presentation/src/learning-snippets/common/MultiSchemaClasses.test.ts b/full-stack-tests/presentation/src/learning-snippets/common/MultiSchemaClasses.test.ts index a00679cfe3b8..a0b935f09ea7 100644 --- a/full-stack-tests/presentation/src/learning-snippets/common/MultiSchemaClasses.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/common/MultiSchemaClasses.test.ts @@ -8,6 +8,7 @@ import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -50,20 +51,22 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that `bis.PhysicalModel` and `bis.SpatialCategory` instances are selected. - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - keys: new KeySet(), - descriptor: {}, - }); + const contentSet = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + keys: new KeySet(), + descriptor: {}, + }) + .then(async (x) => collect(x!.items)); - expect(content!.contentSet).to.have.lengthOf(2); - expect(content!.contentSet).to.containSubset([ + expect(contentSet).to.have.lengthOf(2); + expect(contentSet).to.containSubset([ { primaryKeys: [{ className: "BisCore:PhysicalModel" }], }, ]); - expect(content!.contentSet).to.containSubset([ + expect(contentSet).to.containSubset([ { primaryKeys: [{ className: "BisCore:SpatialCategory" }], }, diff --git a/full-stack-tests/presentation/src/learning-snippets/common/RelatedInstanceSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/common/RelatedInstanceSpecification.test.ts index 2fe3b3da51f1..0a0eb3942eb5 100644 --- a/full-stack-tests/presentation/src/learning-snippets/common/RelatedInstanceSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/common/RelatedInstanceSpecification.test.ts @@ -5,7 +5,7 @@ import { expect } from "chai"; import { IModel } from "@itwin/core-common"; import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; -import { KeySet, Ruleset, StandardNodeTypes } from "@itwin/presentation-common"; +import { KeySet, Node, Ruleset, StandardNodeTypes } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { getFieldByLabel } from "../../Utils"; @@ -59,18 +59,18 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only `bis.ViewDefinition` instances are selected. - const content = await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, - }); + }))!; - expect(content!.contentSet.length).to.eq(3); - const field = getFieldByLabel(content!.descriptor.fields, "Display Style"); - content!.contentSet.forEach((record) => { + expect(content.total).to.eq(3); + const field = getFieldByLabel(content.descriptor.fields, "Display Style"); + for await (const record of content.items) { expect(record.displayValues[field.name]).to.contain("View"); - }); + } }); it("using in instance filter with target instance ids", async () => { @@ -106,14 +106,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that 4 `bis.ViewDefinition` instances are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(4); + expect(content?.total).to.eq(4); }); it("using for customization", async () => { @@ -162,17 +162,18 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.RelatedInstanceSpecification.UsingForCustomization.Result // Every node should have its full class name in extended data - const nodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes.length).to.eq(417); - nodes.forEach((node) => { + + expect(total).to.eq(417); + for await (const node of items) { const fullClassName = node.extendedData!.fullClassName; const [schemaName, className] = fullClassName.split("."); expect(schemaName).to.not.be.empty; expect(className).to.not.be.empty; - }); + } // __PUBLISH_EXTRACT_END__ }); @@ -226,28 +227,35 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Every node should have its full class name in extended data - const schemaNodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(schemaNodes.length).to.eq(18); - await Promise.all( - schemaNodes.map(async (schemaNode) => { - expect(schemaNode).to.containSubset({ - key: { - type: StandardNodeTypes.ECPropertyGroupingNode, - className: "ECDbMeta:ECSchemaDef", - propertyName: "Name", - }, - }); - const classNodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - parentKey: schemaNode.key, - }); - expect(classNodes).to.not.be.empty; - }), - ); + expect(total).to.eq(18); + + async function testSchemaNode(schemaNode: Node) { + expect(schemaNode).to.containSubset({ + key: { + type: StandardNodeTypes.ECPropertyGroupingNode, + className: "ECDbMeta:ECSchemaDef", + propertyName: "Name", + }, + }); + + const { total: count } = await Presentation.presentation.getNodesIterator({ + imodel, + rulesetOrId: ruleset, + parentKey: schemaNode.key, + }); + + expect(count).not.to.eq(0); + } + + const promises: Promise[] = []; + for await (const node of items) { + promises.push(testSchemaNode(node)); + } + await Promise.all(promises); }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/common/RelationshipPathSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/common/RelationshipPathSpecification.test.ts index eb61807edf5e..2c7399277351 100644 --- a/full-stack-tests/presentation/src/learning-snippets/common/RelationshipPathSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/common/RelationshipPathSpecification.test.ts @@ -53,16 +53,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that all model elements are selected - const physicalModelContent = await Presentation.presentation.getContent({ + const physicalModelContent = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), descriptor: {}, }); - expect(physicalModelContent!.contentSet.length).to.eq(62); + expect(physicalModelContent?.total).to.eq(62); // Ensure that non-physical model elements are not selected - const definitionModelContent = await Presentation.presentation.getContent({ + const definitionModelContent = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:DefinitionModel", id: "0x16" }]), @@ -106,13 +106,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that all model elements are selected - const physicalModelContent = await Presentation.presentation.getContent({ + const physicalModelContent = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), descriptor: {}, }); - expect(physicalModelContent!.contentSet.length).to.eq(1); + expect(physicalModelContent?.total).to.eq(1); }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/common/RepeatableRelationshipPathSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/common/RepeatableRelationshipPathSpecification.test.ts index b10293b82dff..008b408b8377 100644 --- a/full-stack-tests/presentation/src/learning-snippets/common/RepeatableRelationshipPathSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/common/RepeatableRelationshipPathSpecification.test.ts @@ -8,6 +8,7 @@ import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -52,19 +53,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that content of grandparent element is returned - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x1b" }]), descriptor: {}, }); - expect(content!.contentSet) - .to.have.lengthOf(1) - .and.to.containSubset([ - { - primaryKeys: [{ id: "0x1" }], - }, - ]); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value).to.containSubset({ + primaryKeys: [{ id: "0x1" }], + }); }); it("using recursive specification", async () => { @@ -95,13 +93,15 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that content of the root subject's children is returned - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - keys: new KeySet([{ className: "BisCore:Element", id: "0x1" }]), - descriptor: {}, - }); - expect(content!.contentSet) + const contentSet = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + keys: new KeySet([{ className: "BisCore:Element", id: "0x1" }]), + descriptor: {}, + }) + .then(async (x) => collect(x!.items)); + expect(contentSet) .to.have.lengthOf(9) .and.to.containSubset([ { @@ -175,19 +175,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that elements' category is returned when requesting content for those elements' model - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), descriptor: {}, }); - expect(content!.contentSet) - .to.have.lengthOf(1) - .and.to.containSubset([ - { - primaryKeys: [{ id: "0x17" }], - }, - ]); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value).to.containSubset({ + primaryKeys: [{ id: "0x17" }], + }); }); it("combining multiple recursive specifications", async () => { @@ -237,19 +234,21 @@ describe("Learning Snippets", () => { // Ensure that the count is correct (62 elements + 1 category + 1 sub-category) and both // categories are included. Not checking the elements... - const content = await Presentation.presentation.getContent({ - imodel, - rulesetOrId: ruleset, - keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), - descriptor: {}, - }); - expect(content!.contentSet).to.have.lengthOf(62 + 1 + 1); - expect(content!.contentSet).to.containSubset([ + const contentSet = await Presentation.presentation + .getContentIterator({ + imodel, + rulesetOrId: ruleset, + keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), + descriptor: {}, + }) + .then(async (x) => collect(x!.items)); + expect(contentSet).to.have.lengthOf(62 + 1 + 1); + expect(contentSet).to.containSubset([ { primaryKeys: [{ className: "BisCore:SpatialCategory", id: "0x17" }], }, ]); - expect(content!.contentSet).to.containSubset([ + expect(contentSet).to.containSubset([ { primaryKeys: [{ className: "BisCore:SubCategory", id: "0x18" }], }, diff --git a/full-stack-tests/presentation/src/learning-snippets/content/customization/CalculatedPropertiesSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/customization/CalculatedPropertiesSpecification.test.ts index 3a7dae173850..c6c2578f83d1 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/customization/CalculatedPropertiesSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/customization/CalculatedPropertiesSpecification.test.ts @@ -52,7 +52,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -93,22 +93,19 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created and has a value - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "generic.PhysicalObject", id: "0x74" }]), descriptor: {}, }))!; const field = getFieldByLabel(content.descriptor.fields, "Element Volume"); - expect(content.contentSet) - .to.have.lengthOf(1) - .and.to.containSubset([ - { - values: { - [field.name]: "3.449493952966681", - }, - }, - ]); + expect(content.total).to.eq(1); + expect((await content.items.next()).value).to.containSubset({ + values: { + [field.name]: "3.449493952966681", + }, + }); }); it("uses `categoryId` attribute", async () => { @@ -146,13 +143,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the calculated property is assigned a custom category - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, }); - content; expect(content!.descriptor.fields).to.containSubset([ { label: "My Calculated Property", @@ -194,13 +190,13 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.CalculatedPropertiesSpecification.Renderer.Result // Ensure the calculated property field is assigned the "my-renderer" renderer - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - expect(content.descriptor.fields).to.containSubset([ + }); + expect(content!.descriptor.fields).to.containSubset([ { label: "My Calculated property", renderer: { @@ -242,13 +238,13 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.CalculatedPropertiesSpecification.Editor.Result // Ensure the calculated property field is assigned the "my-editor" editor - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - expect(content.descriptor.fields).to.containSubset([ + }); + expect(content!.descriptor.fields).to.containSubset([ { label: "My Calculated property", editor: { @@ -288,13 +284,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property has correct priority - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - expect(content.descriptor.fields).to.containSubset([ + }); + expect(content!.descriptor.fields).to.containSubset([ { label: "My Calculated Property", priority: 9999, diff --git a/full-stack-tests/presentation/src/learning-snippets/content/customization/DefaultPropertyCategoryOverride.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/customization/DefaultPropertyCategoryOverride.test.ts index a5b5842824e9..8a99549bdf74 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/customization/DefaultPropertyCategoryOverride.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/customization/DefaultPropertyCategoryOverride.test.ts @@ -63,13 +63,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - we should use the "OLD" default category - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - const defaultCategory = content.descriptor.categories.find((category) => category.name === "default"); + }); + const defaultCategory = content!.descriptor.categories.find((category) => category.name === "default"); expect(defaultCategory).to.containSubset({ label: "Custom Category OLD", }); @@ -112,13 +112,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - we should use the "OLD" default category - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - const defaultCategory = content.descriptor.categories.find((category) => category.name === "default"); + }); + const defaultCategory = content!.descriptor.categories.find((category) => category.name === "default"); expect(defaultCategory).to.containSubset({ label: "High Priority", }); @@ -152,17 +152,17 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the default property category is correctly set up - const content = (await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), descriptor: {}, - }))!; - const defaultCategory = content.descriptor.categories.find((category) => category.name === "default"); + }); + const defaultCategory = content!.descriptor.categories.find((category) => category.name === "default"); expect(defaultCategory).to.containSubset({ label: "Test Category", }); - content.descriptor.fields.forEach((field) => { + content!.descriptor.fields.forEach((field) => { expect(field.category).to.eq(defaultCategory); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertyCategorySpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertyCategorySpecification.test.ts index 3ac8c3d73adf..bdb6d4f4c803 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertyCategorySpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertyCategorySpecification.test.ts @@ -58,7 +58,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field is assigned a category with correct label - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -105,7 +105,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field is assigned a category with correct label - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -154,7 +154,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertyCategorySpecification.Description.Result // Ensure category description is assigned - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -207,7 +207,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure categories' hierarchy was set up correctly - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -269,7 +269,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertyCategorySpecification.Priority.Result // Ensure that correct category priorities are assigned - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -331,7 +331,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertyCategorySpecification.AutoExpand.Result // Ensure that categories have the `expand` flag - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -384,7 +384,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertyCategorySpecification.Renderer.Result // Ensure that categories have the `expand` flag - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), diff --git a/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertySpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertySpecification.test.ts index bfd9243b4aee..28481159d4f0 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertySpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/customization/PropertySpecification.test.ts @@ -64,7 +64,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field is assigned attributes from both specifications - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -110,7 +110,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field is assigned attributes from both specifications - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -156,7 +156,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field has the correct category - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -200,7 +200,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `LastMod` is there - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -240,7 +240,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the property is not displayed when value is not set - let content = (await Presentation.presentation.getContent({ + let content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -250,7 +250,7 @@ describe("Learning Snippets", () => { // Ensure the property is displayed when value is set to `true` await Presentation.presentation.vars(ruleset.id).setBool("SHOW_LABEL", true); - content = (await Presentation.presentation.getContent({ + content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -264,7 +264,7 @@ describe("Learning Snippets", () => { // Ensure the property is not displayed when value is set to `false` await Presentation.presentation.vars(ruleset.id).setBool("SHOW_LABEL", false); - content = (await Presentation.presentation.getContent({ + content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -302,7 +302,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` property is not the only property in content - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -347,7 +347,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertySpecification.Renderer.Result // Ensure the `CodeValue` field is assigned the "my-renderer" renderer - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -394,7 +394,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertySpecification.Editor.Result // Ensure the `UserLabel` field is assigned the "my-editor" editor - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -439,7 +439,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Content.Customization.PropertySpecification.IsReadOnly.Result // Ensure the `UserLabel` field is read-only. - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -481,7 +481,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the `UserLabel` field's priority is 9999, which makes it appear higher in the property grid. - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), diff --git a/full-stack-tests/presentation/src/learning-snippets/content/customization/RelatedPropertiesSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/customization/RelatedPropertiesSpecification.test.ts index ad16642f59e9..6b5f69b06b30 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/customization/RelatedPropertiesSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/customization/RelatedPropertiesSpecification.test.ts @@ -56,7 +56,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x12" }]), @@ -114,7 +114,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the custom property was created - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x12" }]), @@ -176,7 +176,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that all related properties are placed into a category nested under the default category - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -251,7 +251,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the two related properties are picked up - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -305,7 +305,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure the field has `autoExpand` attribute set to `true` - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Subject", id: "0x1" }]), @@ -386,7 +386,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure only one related property is loaded - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -445,7 +445,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure properties of physical partition and repository link are loaded - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), @@ -501,7 +501,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the relationship property is picked up - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "ECDbMeta:ECClassDef", id: "0x3b" }]), @@ -556,7 +556,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the related property is categorized under relationship category. - const content = (await Presentation.presentation.getContent({ + const content = (await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), diff --git a/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentModifier.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentModifier.test.ts index b5dae0b21068..395c80b42214 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentModifier.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentModifier.test.ts @@ -7,7 +7,7 @@ import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; import { KeySet, NestedContentValue, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; -import { getFieldByLabel, getFieldsByLabel, tryGetFieldByLabel } from "../../../Utils"; +import { collect, getFieldByLabel, getFieldsByLabel, tryGetFieldByLabel } from "../../../Utils"; import { printRuleset } from "../../Utils"; describe("Learning Snippets", () => { @@ -58,12 +58,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure only the `bis.Category` instance has the calculated property - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); + expect(content!.descriptor.fields) .to.containSubset([ { @@ -87,8 +88,10 @@ describe("Learning Snippets", () => { ]) .and.to.have.lengthOf(6); const calculatedField = tryGetFieldByLabel(content!.descriptor.fields, "Calculated"); - expect(content!.contentSet[0].displayValues[calculatedField!.name]).to.be.undefined; - expect(content!.contentSet[1].displayValues[calculatedField!.name]).to.eq("PREFIX_Uncategorized"); + + const items = await collect(content!.items); + expect(items[0].displayValues[calculatedField!.name]).to.be.undefined; + expect(items[1].displayValues[calculatedField!.name]).to.eq("PREFIX_Uncategorized"); }); it("uses `requiredSchemas` attribute", async () => { @@ -131,7 +134,7 @@ describe("Learning Snippets", () => { // The iModel uses BisCore older than 1.0.2 - the returned content should not // include ExternalSourceAspect properties - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), @@ -196,13 +199,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect to get one `bis.SpatialCategory` field and one related content field. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); + expect(content!.total).to.eq(1); expect(content!.descriptor.fields) .to.containSubset([ { @@ -270,14 +273,14 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ printRuleset(ruleset); - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:ModelSelector", id: "0x35" }]), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); + expect(content!.total).to.eq(1); expect(content!.descriptor.fields).to.containSubset([ { label: "Model" }, { label: "Is Private" }, @@ -300,7 +303,7 @@ describe("Learning Snippets", () => { ], }, ]); - const spatialViewDefinition = content!.contentSet[0].values[ + const spatialViewDefinition = (await content!.items.next()).value.values[ getFieldByLabel(content!.descriptor.fields, "Spatial View Definition").name ] as NestedContentValue[]; expect(spatialViewDefinition.length).to.eq(1); @@ -347,13 +350,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure content contains Category's properties - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); + expect(content!.total).to.eq(1); expect(content!.descriptor.fields).to.containSubset([ { label: "Spatial Category", @@ -394,7 +397,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure content contains the calculated property and correct value - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), @@ -405,8 +408,8 @@ describe("Learning Snippets", () => { label: "Yaw & Pitch & Roll", }, ]); - expect(content!.contentSet.length).to.eq(1); - expect(content!.contentSet[0].displayValues[getFieldByLabel(content!.descriptor.fields, "Yaw & Pitch & Roll").name]).to.eq( + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value.displayValues[getFieldByLabel(content!.descriptor.fields, "Yaw & Pitch & Roll").name]).to.eq( "0.000000 & 0.000000 & 90.000000", ); }); @@ -449,7 +452,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure all `bis.GeometricElement3d` properties are in the custom category - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), @@ -519,7 +522,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure customizations have been made - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), diff --git a/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentRule.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentRule.test.ts index b4af47627e83..8492b7895dd1 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentRule.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/rules/ContentRule.test.ts @@ -8,6 +8,7 @@ import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -62,22 +63,22 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect element content when providing `bis.Element` input - const elementContent = await Presentation.presentation.getContent({ + const elementContent = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "Generic:PhysicalObject", id: "0x74" }]), descriptor: {}, }); - expect(elementContent!.contentSet.length).to.eq(1); - expect(elementContent!.contentSet[0].primaryKeys).to.deep.eq([{ className: "Generic:PhysicalObject", id: "0x74" }]); + expect(elementContent!.total).to.eq(1); + expect((await elementContent!.items.next()).value.primaryKeys).to.deep.eq([{ className: "Generic:PhysicalObject", id: "0x74" }]); - const modelContent = await Presentation.presentation.getContent({ + const modelContent = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), descriptor: {}, }); - expect(modelContent!.contentSet.length).to.eq(62); + expect(modelContent!.total).to.eq(62); }); it("uses ruleset variables in rule condition", async () => { @@ -113,7 +114,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // No variables set - no content - let content = await Presentation.presentation.getContent({ + let content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), @@ -123,13 +124,14 @@ describe("Learning Snippets", () => { // Set DISPLAY_CATEGORIES to get content of all Category instances in the imodel await Presentation.presentation.vars(ruleset.id).setBool("DISPLAY_CATEGORIES", true); - content = await Presentation.presentation.getContent({ + content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet) + let contentSet = await collect(content!.items); + expect(contentSet) .to.containSubset([ { primaryKeys: [{ className: "BisCore:SpatialCategory", id: "0x17" }], @@ -139,13 +141,14 @@ describe("Learning Snippets", () => { // Set DISPLAY_MODELS to also get geometric model instances' content await Presentation.presentation.vars(ruleset.id).setBool("DISPLAY_MODELS", true); - content = await Presentation.presentation.getContent({ + content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet) + contentSet = await collect(content!.items); + expect(contentSet) .to.containSubset([ { primaryKeys: [{ className: "BisCore:SpatialCategory", id: "0x17" }], @@ -186,7 +189,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - no content should be returned - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), @@ -229,13 +232,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect GeometricModel record to be first even though category rule was defined first - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet) + const contentSet = await collect(content!.items); + expect(contentSet) .to.containSubset([ { primaryKeys: [{ className: "BisCore:PhysicalModel", id: "0x1c" }], @@ -282,13 +286,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect only `GeometricModel` record, as the rule for `SpatialCategory` is skipped due to `onlyIfNotHandled` attribute - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet) + const contentSet = await collect(content!.items); + expect(contentSet) .to.containSubset([ { primaryKeys: [{ className: "BisCore:PhysicalModel", id: "0x1c" }], diff --git a/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentInstancesOfSpecificClasses.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentInstancesOfSpecificClasses.test.ts index c9ffca800668..967da64d4c41 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentInstancesOfSpecificClasses.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentInstancesOfSpecificClasses.test.ts @@ -7,7 +7,7 @@ import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; -import { getFieldByLabel } from "../../../Utils"; +import { collect, getFieldByLabel } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -44,15 +44,15 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ // Ensure only the `bis.PhysicalModel` instances are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); - expect(content!.contentSet[0].primaryKeys[0].className).to.eq("BisCore:PhysicalModel"); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value.primaryKeys[0].className).to.eq("BisCore:PhysicalModel"); }); it("uses `excludedClasses` attribute", async () => { @@ -76,14 +76,15 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ // Ensure that all `bis.PhysicalModel` instances are excluded. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet) + const contentSet = await collect(content!.items); + expect(contentSet) .to.have.lengthOf(7) .and.not.containSubset([{ classInfo: { name: "BisCore:PhysicalModel" } }]); }); @@ -109,13 +110,15 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ // Ensure that derived `bis.ViewDefinition` instances along with their properties are also selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.descriptor.fields) + + const { descriptor, total } = content!; + expect(descriptor.fields) .to.containSubset([ { label: "Category Selector" }, { label: "Code" }, @@ -137,7 +140,7 @@ describe("Learning Snippets", () => { ]) .and.to.have.lengthOf(17); - expect(content!.contentSet.length).to.eq(4); + expect(total).to.eq(4); }); it("uses `instanceFilter` attribute", async () => { @@ -161,18 +164,18 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ // Ensure that only `bis.SpatialViewDefinition` instances that have Pitch >= 0 are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(2); + expect(content!.total).to.eq(2); const field = getFieldByLabel(content!.descriptor.fields, "Pitch"); - content!.contentSet.forEach((record) => { + for await (const record of content!.items) { expect(record.values[field.name]).to.be.not.below(0); - }); + } }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentRelatedInstances.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentRelatedInstances.test.ts index f8d688f25d36..b4cbe59a6209 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentRelatedInstances.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/specifications/ContentRelatedInstances.test.ts @@ -53,16 +53,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that related `bis.Element` instances are returned. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:PhysicalModel", id: "0x1c" }]), descriptor: {}, }); - content!.contentSet.forEach((record) => { + for await (const record of content!.items) { expect(record.primaryKeys[0].className).to.be.oneOf(["Generic:PhysicalObject", "PCJ_TestSchema:TestClass"]); - }); + } }); it("uses `instanceFilter` attribute", async () => { @@ -94,17 +94,19 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ // Ensure that only `bis.SpatialViewDefinition` instances that have Pitch >= 0 are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:ModelSelector", id: "0x30" }]), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); - expect(content!.contentSet[0].primaryKeys[0].className).to.eq("BisCore:SpatialViewDefinition"); - const field = getFieldByLabel(content!.descriptor.fields, "Pitch"); - expect(content!.contentSet[0].values[field.name]).to.be.not.below(0); + const { total, descriptor, items } = content!; + expect(total).to.eq(1); + const first = (await items.next()).value; + expect(first.primaryKeys[0].className).to.eq("BisCore:SpatialViewDefinition"); + const field = getFieldByLabel(descriptor.fields, "Pitch"); + expect(first.values[field.name]).to.be.not.below(0); }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/content/specifications/SelectedNodeInstances.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/specifications/SelectedNodeInstances.test.ts index bbbb7de91e44..bcc9a3dc5c5f 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/specifications/SelectedNodeInstances.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/specifications/SelectedNodeInstances.test.ts @@ -8,6 +8,7 @@ import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -46,7 +47,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only `BisCore` content instances are returned. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([ @@ -56,7 +57,7 @@ describe("Learning Snippets", () => { descriptor: {}, }); - expect(content!.contentSet) + expect(await collect(content!.items)) .to.have.lengthOf(1) .and.to.containSubset([ { @@ -87,7 +88,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only `bis.SpatialViewDefinition` content instances are returned. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([ @@ -97,13 +98,10 @@ describe("Learning Snippets", () => { descriptor: {}, }); - expect(content!.contentSet) - .to.have.lengthOf(1) - .and.to.containSubset([ - { - classInfo: { label: "Spatial View Definition" }, - }, - ]); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value).to.containSubset({ + classInfo: { label: "Spatial View Definition" }, + }); }); it("uses `acceptablePolymorphically` attribute", async () => { @@ -129,7 +127,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only content instances of `bis.ViewDefinition` and derived classes are returned. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([ @@ -139,8 +137,8 @@ describe("Learning Snippets", () => { descriptor: {}, }); - expect(content!.contentSet).to.have.lengthOf(1); - expect(content!.contentSet[0].primaryKeys[0].className).to.equal("BisCore:SpatialViewDefinition"); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value.primaryKeys[0].className).to.equal("BisCore:SpatialViewDefinition"); }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/content/specifications/Shared.test.ts b/full-stack-tests/presentation/src/learning-snippets/content/specifications/Shared.test.ts index 0eabdee7732e..3dee8c101755 100644 --- a/full-stack-tests/presentation/src/learning-snippets/content/specifications/Shared.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/content/specifications/Shared.test.ts @@ -54,18 +54,18 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only `bis.ViewDefinition` instances are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(4); + expect(content!.total).to.eq(4); const field = getFieldByLabel(content!.descriptor.fields, "Category Selector"); - content!.contentSet.forEach((record) => { + for await (const record of content!.items) { expect(record.displayValues[field.name]).to.be.string("Default - View"); - }); + } }); it("uses `priority` attribute", async () => { @@ -97,17 +97,18 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that only `bis.ViewDefinition` instances are selected. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(2); - const field = getFieldByLabel(content!.descriptor.fields, "Modeled Element"); - expect(content!.contentSet[0].displayValues[field.name]).to.eq("BisCore.DictionaryModel"); - expect(content!.contentSet[1].displayValues[field.name]).to.eq("Properties_60InstancesWithUrl2"); + const { total, items, descriptor } = content!; + expect(total).to.eq(2); + const field = getFieldByLabel(descriptor.fields, "Modeled Element"); + expect((await items.next()).value.displayValues[field.name]).to.eq("BisCore.DictionaryModel"); + expect((await items.next()).value.displayValues[field.name]).to.eq("Properties_60InstancesWithUrl2"); }); it("uses `relatedProperties` attribute", async () => { @@ -139,15 +140,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that derived `bis.DisplayStyle` instance properties are also returned with `bis.SpatialViewDefinition` content. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(4); - expect(content!.descriptor.fields) + const { total, descriptor } = content!; + expect(total).to.eq(4); + expect(descriptor.fields) .to.containSubset([ { label: "Display Style", @@ -185,14 +187,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that derived `bis.DisplayStyle` instance properties are also returned with `bis.SpatialViewDefinition` content. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(4); + expect(content!.total).to.eq(4); expect(content!.descriptor.fields) .to.containSubset([{ label: "Camera view direction" }]) .and.to.have.lengthOf(18); @@ -231,7 +233,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the returned content has a custom category `Camera settings` and it contains the right properties. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), @@ -278,14 +280,14 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure that the returned content has an overriden property label `Container Model`. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(4); + expect(content!.total).to.eq(4); expect(content!.descriptor.fields) .to.containSubset([ { label: "Category Selector" }, @@ -328,16 +330,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Ensure only the `bis.ModelSelector` whose related SpatialViewDefinition with Yaw > 0 is returned. - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: {}, }); - expect(content!.contentSet.length).to.eq(1); + expect(content!.total).to.eq(1); const field = getFieldByLabel(content!.descriptor.fields, "Code"); - expect(content!.contentSet[0].values[field.name]).to.eq("Default - View 2"); + expect((await content!.items.next()).value.values[field.name]).to.eq("Default - View 2"); }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/customization/DisabledSortingRule.test.ts b/full-stack-tests/presentation/src/learning-snippets/customization/DisabledSortingRule.test.ts index 75780d32b18a..1c16808298ee 100644 --- a/full-stack-tests/presentation/src/learning-snippets/customization/DisabledSortingRule.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/customization/DisabledSortingRule.test.ts @@ -72,15 +72,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are not sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes).to.be.lengthOf(4); - expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); - expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); - expect(nodes[2]).to.containSubset({ label: { displayValue: "-90.00 x 0.00" } }); - expect(nodes[3]).to.containSubset({ label: { displayValue: "0.00 x 90.00" } }); + + expect(total).to.eq(4); + const expectedDisplayValues = ["-107.42 x -160.99", "-45.00 x -35.26", "-90.00 x 0.00", "0.00 x 90.00"]; + for (const displayValue of expectedDisplayValues) { + expect((await items.next()).value).to.containSubset({ label: { displayValue } }); + } }); it("uses `condition` attribute", async () => { @@ -122,16 +123,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are not sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, rulesetVariables: [{ id: "SORT_INSTANCES", type: VariableValueTypes.Bool, value: true }], }); - expect(nodes).to.be.lengthOf(4); - expect(nodes[0]).to.containSubset({ label: { displayValue: "Default - View 1" } }); - expect(nodes[1]).to.containSubset({ label: { displayValue: "Default - View 2" } }); - expect(nodes[2]).to.containSubset({ label: { displayValue: "Default - View 3" } }); - expect(nodes[3]).to.containSubset({ label: { displayValue: "Default - View 4" } }); + + expect(total).to.eq(4); + for (let i = 0; i < 4; ++i) { + expect((await items.next()).value).to.containSubset({ label: { displayValue: `Default - View ${i + 1}` } }); + } }); it("uses `class` attribute", async () => { @@ -181,15 +182,15 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ printRuleset(ruleset); - const nodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes).to.be.lengthOf(4); - expect(nodes[0]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 1" } }); - expect(nodes[1]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 2" } }); - expect(nodes[2]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 3" } }); - expect(nodes[3]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 4" } }); + + expect(total).to.eq(4); + for (let i = 0; i < 4; ++i) { + expect((await items.next()).value).to.containSubset({ label: { displayValue: `SpatialViewDefinition - Default - View ${i + 1}` } }); + } }); it("uses `isPolymorphic` attribute", async () => { @@ -239,16 +240,15 @@ describe("Learning Snippets", () => { }; // __PUBLISH_EXTRACT_END__ printRuleset(ruleset); - - const nodes = await Presentation.presentation.getNodes({ + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes).to.be.lengthOf(4); - expect(nodes[0]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 1" } }); - expect(nodes[1]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 2" } }); - expect(nodes[2]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 3" } }); - expect(nodes[3]).to.containSubset({ label: { displayValue: "SpatialViewDefinition - Default - View 4" } }); + + expect(total).to.eq(4); + for (let i = 0; i < 4; ++i) { + expect((await items.next()).value).to.containSubset({ label: { displayValue: `SpatialViewDefinition - Default - View ${i + 1}` } }); + } }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/customization/ExtendedDataRule.test.ts b/full-stack-tests/presentation/src/learning-snippets/customization/ExtendedDataRule.test.ts index a712b34f61ba..1a33a8df5d14 100644 --- a/full-stack-tests/presentation/src/learning-snippets/customization/ExtendedDataRule.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/customization/ExtendedDataRule.test.ts @@ -8,6 +8,7 @@ import { KeySet, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -54,15 +55,14 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ printRuleset(ruleset); - const content = await Presentation.presentation.getContent({ + const content = await Presentation.presentation.getContentIterator({ imodel, rulesetOrId: ruleset, keys: new KeySet([{ className: "BisCore:Element", id: "0x61" }]), descriptor: {}, }); - expect(content?.contentSet) - .to.be.lengthOf(1) - .and.to.not.containSubset([{ extendedData: { iconName: "external-source-icon" } }]); + expect(content!.total).to.eq(1); + expect((await content!.items.next()).value).not.to.containSubset({ extendedData: { iconName: "external-source-icon" } }); }); it("uses `condition` attribute", async () => { @@ -101,10 +101,12 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.ExtendedDataRule.Condition.Result // Ensure only "B" node has `extendedData` property. - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(2) .and.to.containSubset([ @@ -154,10 +156,12 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.ExtendedDataRule.Items.Result // Ensure node has `extendedData` property containing items defined in rule. - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(1) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/customization/InstanceLabelOverride.test.ts b/full-stack-tests/presentation/src/learning-snippets/customization/InstanceLabelOverride.test.ts index 862d0e2777f6..10bafbe8d730 100644 --- a/full-stack-tests/presentation/src/learning-snippets/customization/InstanceLabelOverride.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/customization/InstanceLabelOverride.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate, testLocalization } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -66,10 +67,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label was not overriden because imodel has older BisCore schema than required by label override - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(2) .and.to.containSubset([{ label: { displayValue: "Physical Object [0-38]" } }, { label: { displayValue: "Physical Object [0-39]" } }]); @@ -122,13 +125,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label override with higher priority was applied - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "Model B" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "Model B" } }); }); it("uses `onlyIfNotHandled` attribute", async () => { @@ -180,13 +182,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that only label override with higher priority was applied - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "Ñót spêçìfíêd" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "Ñót spêçìfíêd" } }); }); it("uses `class` attribute", async () => { @@ -223,10 +224,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that only `bis.GeometricModel3d` instances label was overriden - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(8) .and.to.containSubset([ @@ -276,13 +279,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label was set to composed value - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "ECClass-PhysicalModel" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "ECClass-PhysicalModel" } }); }); it("uses property value specification", async () => { @@ -319,10 +321,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that labels was set to `Pitch` property value - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(4) .and.to.containSubset([ @@ -372,10 +376,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that labels were set to related `meta.ECSchemaDef` instance `Alias` property value - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(18) .and.to.containSubset([ @@ -434,13 +440,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label was set to "Model Node" - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "Model Node" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "Model Node" } }); }); it("uses class name value specification", async () => { @@ -477,13 +482,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label was set to full class name - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "BisCore:PhysicalModel" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "BisCore:PhysicalModel" } }); }); it("uses class label value specification", async () => { @@ -519,13 +523,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that label value was set to instance ECClass label - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "Physical Model" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "Physical Model" } }); }); it("uses briefcaseId value specification", async () => { @@ -561,13 +564,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that only label override with higher priority was applied - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "0" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "0" } }); }); it("uses localId value specification", async () => { @@ -603,13 +605,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that only label override with higher priority was applied - const nodes = await Presentation.presentation.getNodes({ + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, }); - expect(nodes) - .to.be.lengthOf(1) - .and.to.containSubset([{ label: { displayValue: "S" } }]); + expect(nodes.total).to.eq(1); + expect((await nodes.items.next()).value).to.containSubset({ label: { displayValue: "S" } }); }); it("uses related instance label value specification", async () => { @@ -650,10 +651,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that only label override with higher priority was applied - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes) .to.be.lengthOf(2) .and.to.containSubset([{ label: { displayValue: "Properties_60InstancesWithUrl2" } }, { label: { displayValue: "Properties_60InstancesWithUrl2" } }]); diff --git a/full-stack-tests/presentation/src/learning-snippets/customization/PropertySortingRule.test.ts b/full-stack-tests/presentation/src/learning-snippets/customization/PropertySortingRule.test.ts index 11417afb32ee..b6a86acc4b6f 100644 --- a/full-stack-tests/presentation/src/learning-snippets/customization/PropertySortingRule.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/customization/PropertySortingRule.test.ts @@ -8,6 +8,7 @@ import { Ruleset, VariableValueTypes } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -73,10 +74,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); @@ -125,11 +128,13 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - rulesetVariables: [{ id: "SORT_INSTANCES", type: VariableValueTypes.Bool, value: true }], - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + rulesetVariables: [{ id: "SORT_INSTANCES", type: VariableValueTypes.Bool, value: true }], + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); @@ -178,10 +183,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); @@ -231,10 +238,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes of `bis.SpatialViewDefinition` class instances are sorted - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); @@ -282,10 +291,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are sorted by `Pitch` property - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-107.42 x -160.99" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); @@ -334,10 +345,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // verify that nodes are sorted by `Pitch` in descending order - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "0.00 x 90.00" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-90.00 x 0.00" } }); diff --git a/full-stack-tests/presentation/src/learning-snippets/customization/SortingShared.test.ts b/full-stack-tests/presentation/src/learning-snippets/customization/SortingShared.test.ts index fe35c2a993e2..33451a3fab47 100644 --- a/full-stack-tests/presentation/src/learning-snippets/customization/SortingShared.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/customization/SortingShared.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../IntegrationTests"; import { printRuleset } from "../Utils"; +import { collect } from "../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -65,10 +66,12 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_END__ printRuleset(ruleset); - const nodes = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - }); + const nodes = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + }) + .then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(4); expect(nodes[0]).to.containSubset({ label: { displayValue: "-45.00 x -35.26" } }); expect(nodes[1]).to.containSubset({ label: { displayValue: "-90.00 x 0.00" } }); diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/ClassGroup.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/ClassGroup.test.ts index c38d5d69f0f5..96cf7bf93a1c 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/ClassGroup.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/ClassGroup.test.ts @@ -8,6 +8,7 @@ import { Ruleset, StandardNodeTypes } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -60,7 +61,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm there's a class grouping node for PhysicalElement - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(43) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/PropertyGroup.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/PropertyGroup.test.ts index 35bd72d3ec8c..1f7eee9a1e29 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/PropertyGroup.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/PropertyGroup.test.ts @@ -8,6 +8,7 @@ import { Ruleset, StandardNodeTypes } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -62,7 +63,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm there's no "Not Specified" node - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.not.containSubset([ { key: { @@ -112,7 +113,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Hierarchies.Grouping.PropertyGroup.ImageId.Result // Confirm that all grouping nodes got the `imageId` - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.not.be.empty; nodes.forEach((node) => { expect(node).to.containSubset({ @@ -178,7 +179,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm that elements were correctly grouped into ranges - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(2) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/SameLabelInstanceGroup.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/SameLabelInstanceGroup.test.ts index ed2dffd55b53..5435372932b8 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/SameLabelInstanceGroup.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/SameLabelInstanceGroup.test.ts @@ -10,6 +10,7 @@ import { InstanceKey, NodeKey, Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -61,7 +62,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm that at least some nodes are merged from multiple elements - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.satisfy( () => nodes.length > 0 && @@ -129,7 +130,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm that at least some nodes are merged from multiple elements - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(8) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/Shared.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/Shared.test.ts index 2ff87f4810cd..5bda7e8acf77 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/Shared.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/grouping/Shared.test.ts @@ -8,6 +8,7 @@ import { Ruleset, StandardNodeTypes } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -77,7 +78,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm that only private models have children grouped by property - const modelNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const modelNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(modelNodes) .to.have.lengthOf(8) .and.containSubset([ @@ -119,10 +120,10 @@ describe("Learning Snippets", () => { const expectedChildrenType = privateModels.includes(modelNode.label.displayValue) ? StandardNodeTypes.ECPropertyGroupingNode : StandardNodeTypes.ECInstancesNode; - const childNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: modelNode.key }); - childNodes.forEach((childNode) => { + const { items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: modelNode.key }); + for await (const childNode of items) { expect(childNode.key.type).to.eq(expectedChildrenType, `Unexpected child node type for model "${modelNode.label.displayValue}".`); - }); + } }), ); }); @@ -164,11 +165,11 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Confirm all nodes are property grouping nodes - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); - expect(nodes).to.be.not.empty; - nodes.forEach((node) => { + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }); + expect(total).to.be.greaterThan(0); + for await (const node of items) { expect(node.key.type).to.eq(StandardNodeTypes.ECPropertyGroupingNode); - }); + } }); }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeArtifacts.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeArtifacts.test.ts index cda8c1c738a6..ed0dee21cdc1 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeArtifacts.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeArtifacts.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -80,7 +81,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Hierarchies.NodeArtifacts.Condition.Result // Confirm we get only the GeometricModel3d - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.containSubset([ @@ -148,7 +149,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Hierarchies.NodeArtifacts.Items.Result // Confirm we get only the GeometricModel3d - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeRules.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeRules.test.ts index 943a429991db..78d69bab29d9 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeRules.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/rules/NodeRules.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -57,14 +58,16 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect A root node with a B child - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes).to.containSubset([ { label: { displayValue: "A" }, }, ]); - const childNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); + const childNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }) + .then(async (x) => collect(x.items)); expect(childNodes).to.containSubset([ { label: { displayValue: "B" }, @@ -107,12 +110,12 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // No variables set - no nodes - let nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + let nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.be.empty; // Set DISPLAY_B_NODES to get node B await Presentation.presentation.vars(ruleset.id).setBool("DISPLAY_B_NODES", true); - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -123,7 +126,7 @@ describe("Learning Snippets", () => { // Set DISPLAY_A_NODES to also get node A await Presentation.presentation.vars(ruleset.id).setBool("DISPLAY_A_NODES", true); - nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(2) .and.to.containSubset([ @@ -165,8 +168,8 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The iModel uses BisCore older than 1.0.2 - no nodes should be returned - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); - expect(nodes).to.be.empty; + const { total } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }); + expect(total).to.eq(0); }); it("uses `priority` attribute", async () => { @@ -204,7 +207,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify B comes before A - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.be.lengthOf(2); expect(nodes[0]).containSubset({ label: { displayValue: "B" }, @@ -251,7 +254,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect only "B" node, as the rule for "A" is skipped due to `onlyIfNotHandled` attribute - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -307,7 +310,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Expect global label override to be applied on "A" and nested label override to be applied on "B" - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(2) .and.to.containSubset([ @@ -361,7 +364,7 @@ describe("Learning Snippets", () => { // The root node rule meets schema requirement, but only the first sub-condition's condition // attribute evaluates to `true` - expect only the "A" node. - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -406,7 +409,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // The root node is expected to have `isExpanded = true` - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomNodeSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomNodeSpecification.test.ts index da080cc5ebb1..b092e03ac8ab 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomNodeSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomNodeSpecification.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -58,7 +59,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that node with correct type is returned - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -66,7 +67,9 @@ describe("Learning Snippets", () => { key: { type: "T_ROOT_NODE" }, }, ]); - const childNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); + const childNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }) + .then(async (x) => collect(x.items)); expect(childNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -98,7 +101,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that node with correct label is returned - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -131,7 +134,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that node with correct description is returned - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -165,7 +168,7 @@ describe("Learning Snippets", () => { // __PUBLISH_EXTRACT_START__ Presentation.Hierarchies.CustomNodeSpecification.ImageId.Result // Verify that node with correct image identifier is returned - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -211,7 +214,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify the Parent node is not displayed - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(1) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomQueryInstanceNodesSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomQueryInstanceNodesSpecification.test.ts index 80434efa467c..b500156a822c 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomQueryInstanceNodesSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/CustomQueryInstanceNodesSpecification.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -51,7 +52,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that Model nodes are returned - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(classGroupingNodes) .to.have.lengthOf(7) .and.to.containSubset([ @@ -78,7 +79,9 @@ describe("Learning Snippets", () => { }, ]); - const modelNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[5].key }); + const modelNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[5].key }) + .then(async (x) => collect(x.items)); expect(modelNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -129,7 +132,7 @@ describe("Learning Snippets", () => { // our test iModel doesn't have any elements with ECSQL queries as their property values, so // we can't construct any ruleset that would actually return nodes for this test case - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.be.empty; }); }); diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/InstanceNodesOfSpecificClassesSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/InstanceNodesOfSpecificClassesSpecification.test.ts index 42a3293349cc..c2e449acbc2d 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/InstanceNodesOfSpecificClassesSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/InstanceNodesOfSpecificClassesSpecification.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -46,7 +47,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that PhysicalModel nodes were returned, grouped by class - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(classGroupingNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -55,7 +56,9 @@ describe("Learning Snippets", () => { }, ]); - const instanceNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }); + const instanceNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }) + .then(async (x) => collect(x.items)); expect(instanceNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -88,7 +91,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that DefinitionModel and GroupInformationModel nodes are not included - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(classGroupingNodes) .to.have.lengthOf(3) .and.to.containSubset([ @@ -127,7 +130,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that ViewDefinition nodes ending with "View 1" are not included - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(classGroupingNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -136,7 +139,9 @@ describe("Learning Snippets", () => { }, ]); - const instanceNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }); + const instanceNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }) + .then(async (x) => collect(x.items)); expect(instanceNodes) .to.have.lengthOf(1) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/RelatedInstanceNodesSpecification.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/RelatedInstanceNodesSpecification.test.ts index bb5ace9b1120..1481083e1a30 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/RelatedInstanceNodesSpecification.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/RelatedInstanceNodesSpecification.test.ts @@ -8,6 +8,7 @@ import { Ruleset } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -64,7 +65,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that correct Model Elements are returned, grouped by class - const modelNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const modelNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(modelNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -73,7 +74,9 @@ describe("Learning Snippets", () => { }, ]); - const elementClassGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: modelNodes[0].key }); + const elementClassGroupingNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: modelNodes[0].key }) + .then(async (x) => collect(x.items)); expect(elementClassGroupingNodes) .to.have.lengthOf(2) .and.to.containSubset([ @@ -85,7 +88,9 @@ describe("Learning Snippets", () => { }, ]); - const elementNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: elementClassGroupingNodes[0].key }); + const elementNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: elementClassGroupingNodes[0].key }) + .then(async (x) => collect(x.items)); expect(elementNodes) .to.have.lengthOf(2) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/Shared.test.ts b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/Shared.test.ts index f0fbbc6e35bd..c27086e238fa 100644 --- a/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/Shared.test.ts +++ b/full-stack-tests/presentation/src/learning-snippets/hierarchies/specifications/Shared.test.ts @@ -9,6 +9,7 @@ import { NodeKey, Ruleset, StandardNodeTypes } from "@itwin/presentation-common" import { Presentation } from "@itwin/presentation-frontend"; import { initialize, terminate } from "../../../IntegrationTests"; import { printRuleset } from "../../Utils"; +import { collect } from "../../../Utils"; describe("Learning Snippets", () => { let imodel: IModelConnection; @@ -58,7 +59,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify PhysicalModel's class grouping node is displayed, but the instance node - not - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(classGroupingNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -67,7 +68,9 @@ describe("Learning Snippets", () => { }, ]); - const customNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }); + const customNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNodes[0].key }) + .then(async (x) => collect(x.items)); expect(customNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -128,7 +131,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that only 3d elements' custom node is loaded - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -139,8 +142,8 @@ describe("Learning Snippets", () => { }, ]); - const element3dNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); - expect(element3dNodes).to.not.be.empty; + const { total } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); + expect(total).to.be.greaterThan(0); }); it("uses `hideExpression` attribute", async () => { @@ -194,7 +197,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that only 3d elements' custom node is loaded - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -205,8 +208,8 @@ describe("Learning Snippets", () => { }, ]); - const element3dNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); - expect(element3dNodes).to.not.be.empty; + const { total } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); + expect(total).to.be.greaterThan(0); }); it("uses `suppressSimilarAncestorsCheck` attribute", async () => { @@ -280,7 +283,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that RepositoryModel is repeated in the hierarchy - const rootSubjectNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootSubjectNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootSubjectNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -290,7 +293,9 @@ describe("Learning Snippets", () => { }, ]); - const rootSubjectChildNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootSubjectNodes[0].key }); + const rootSubjectChildNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootSubjectNodes[0].key }) + .then(async (x) => collect(x.items)); expect(rootSubjectChildNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -299,7 +304,9 @@ describe("Learning Snippets", () => { }, ]); - const repositoryModelChildNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootSubjectChildNodes[0].key }); + const repositoryModelChildNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootSubjectChildNodes[0].key }) + .then(async (x) => collect(x.items)); expect(repositoryModelChildNodes) .to.have.lengthOf(11) .and.to.containSubset([ @@ -338,11 +345,13 @@ describe("Learning Snippets", () => { }, ]); - const repositoryModelNodes2 = await Presentation.presentation.getNodes({ - imodel, - rulesetOrId: ruleset, - parentKey: repositoryModelChildNodes.find((n) => n.label.displayValue === "DgnV8Bridge")!.key, - }); + const repositoryModelNodes2 = await Presentation.presentation + .getNodesIterator({ + imodel, + rulesetOrId: ruleset, + parentKey: repositoryModelChildNodes.find((n) => n.label.displayValue === "DgnV8Bridge")!.key, + }) + .then(async (x) => collect(x.items)); expect(repositoryModelNodes2) .to.have.lengthOf(1) .and.to.containSubset([ @@ -380,7 +389,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that SpatialCategory comes before PhysicalModel - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes).to.have.lengthOf(2); expect(nodes[0]).to.containSubset({ label: { displayValue: "Spatial Category" }, @@ -412,7 +421,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that nodes were returned unsorted - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); const sorted = [...nodes].sort((lhs, rhs) => lhs.label.displayValue.localeCompare(rhs.label.displayValue)); expect(nodes).to.not.deep.eq(sorted); }); @@ -440,9 +449,11 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that Models were not grouped by class - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); - expect(nodes).to.not.be.empty; - nodes.forEach((node) => expect(NodeKey.isClassGroupingNodeKey(node.key)).to.be.false); + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }); + expect(total).to.be.greaterThan(0); + for await (const node of items) { + expect(NodeKey.isClassGroupingNodeKey(node.key)).to.be.false; + } }); it("uses `groupByLabel` attribute", async () => { @@ -468,14 +479,18 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that instances were not grouped by label - const classGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const classGroupingNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); const classGroupingNode = classGroupingNodes.find((node) => { assert(NodeKey.isClassGroupingNodeKey(node.key)); return node.key.className === "ECDbMeta:ECPropertyDef"; })!; - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key }); - expect(nodes).to.not.be.empty; - nodes.forEach((node) => expect(NodeKey.isLabelGroupingNodeKey(node.key)).to.be.false); + + // Verify that Models were not grouped by class + const { total, items } = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: classGroupingNode.key }); + expect(total).to.be.greaterThan(0); + for await (const node of items) { + expect(NodeKey.isLabelGroupingNodeKey(node.key)).to.be.false; + } }); it("uses `hasChildren` attribute", async () => { @@ -513,7 +528,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that the custom node has `hasChildren` flag and children - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes) .to.have.lengthOf(1) .and.to.containSubset([ @@ -523,7 +538,9 @@ describe("Learning Snippets", () => { }, ]); - const modelClassGroupingNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }); + const modelClassGroupingNodes = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[0].key }) + .then(async (x) => collect(x.items)); expect(modelClassGroupingNodes) .to.have.lengthOf(7) .and.to.containSubset([ @@ -593,7 +610,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that Elements whose Category contains "a" in either UserLabel or CodeValue are returned - const nodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const nodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(nodes) .to.have.lengthOf(2) .and.to.containSubset([ @@ -650,7 +667,7 @@ describe("Learning Snippets", () => { printRuleset(ruleset); // Verify that PhysicalModel node has a child node - const rootNodes = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset }); + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: ruleset }).then(async (x) => collect(x.items)); expect(rootNodes) .to.have.lengthOf(2) .and.to.containSubset([ @@ -664,7 +681,9 @@ describe("Learning Snippets", () => { }, ]); - const modelChildren = await Presentation.presentation.getNodes({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[1].key }); + const modelChildren = await Presentation.presentation + .getNodesIterator({ imodel, rulesetOrId: ruleset, parentKey: rootNodes[1].key }) + .then(async (x) => collect(x.items)); expect(modelChildren) .to.have.lengthOf(1) .and.to.containSubset([ diff --git a/full-stack-tests/presentation/src/performance/DataViz.test.ts b/full-stack-tests/presentation/src/performance/DataViz.test.ts index a7d758f0b5c1..b1a781941afd 100644 --- a/full-stack-tests/presentation/src/performance/DataViz.test.ts +++ b/full-stack-tests/presentation/src/performance/DataViz.test.ts @@ -10,6 +10,7 @@ import { IModelConnection, SnapshotConnection } from "@itwin/core-frontend"; import { ChildNodeSpecificationTypes, ClassInfo, + Content, ContentSpecificationTypes, DefaultContentDisplayTypes, Descriptor, @@ -17,6 +18,7 @@ import { FieldDescriptor, InstanceKey, KeySet, + Node, NodeKey, PropertiesField, PropertiesFieldDescriptor, @@ -30,7 +32,7 @@ import { import { Presentation } from "@itwin/presentation-frontend"; import { ECClassHierarchy, ECClassInfo } from "../ECClasHierarchy"; import { initialize, terminate } from "../IntegrationTests"; -import { getFieldsByLabel } from "../Utils"; +import { collect, getFieldsByLabel } from "../Utils"; /** * The below specifies what iModel to use and what Fields (properties) to use for simulating DataViz @@ -197,17 +199,19 @@ describe("#performance DataViz requests", () => { // we select related fields as direct ones, so need to clear the relationship path fieldDescriptor.pathFromSelectToPropertyClass = []; } - const res = await Presentation.presentation.getPagedDistinctValues({ + + const { items } = await Presentation.presentation.getDistinctValuesIterator({ imodel: iModel, rulesetOrId: ruleset, descriptor: {}, keys: new KeySet(), fieldDescriptor, }); - res.items.map((dv) => { + + for await (const dv of items) { const displayValue = dv.displayValue ? dv.displayValue.toString() : ""; pushValues(distinctValues, displayValue, dv.groupedRawValues); - }); + } }), ); } @@ -264,17 +268,18 @@ describe("#performance DataViz requests", () => { // make a `getPagedDistinctValues` request with the above ruleset for every filtered field const distinctValues = new Map>(); for (const filteredField of filteredFields) { - const res = await Presentation.presentation.getPagedDistinctValues({ + const { items } = await Presentation.presentation.getDistinctValuesIterator({ imodel: iModel, rulesetOrId: ruleset, keys: new KeySet(), descriptor: descriptor.createDescriptorOverrides(), fieldDescriptor: filteredField.getFieldDescriptor(), }); - res.items.map((dv) => { + + for await (const dv of items) { const displayValue = dv.displayValue ? dv.displayValue.toString() : ""; pushValues(distinctValues, displayValue, dv.groupedRawValues); - }); + } ++requestsCount; } @@ -481,27 +486,34 @@ describe("#performance DataViz requests", () => { childElementIds: 0, }; const idEntries = new Map(); + + async function getNodeKeys(ruleset: Ruleset, node: Node) { + const keys: InstanceKey[] = []; + const key = node.key; + if (NodeKey.isInstancesNodeKey(key)) { + pushToArrayNoSpread(keys, key.instanceKeys); + } + if (node.hasChildren) { + pushToArrayNoSpread(keys, await loadHierarchy(ruleset, key)); + } + return keys; + } + async function loadHierarchy(ruleset: Ruleset, parentKey?: NodeKey): Promise { ++requestsCount.elementIds; - const nodes = await Presentation.presentation.getNodes({ + const { items } = await Presentation.presentation.getNodesIterator({ imodel: iModel, rulesetOrId: ruleset, parentKey, }); - const keysPerNode = await Promise.all( - nodes.map(async (node) => { - const keys: InstanceKey[] = []; - const key = node.key; - if (NodeKey.isInstancesNodeKey(key)) { - pushToArrayNoSpread(keys, key.instanceKeys); - } - if (node.hasChildren) { - pushToArrayNoSpread(keys, await loadHierarchy(ruleset, key)); - } - return keys; - }), - ); - return keysPerNode.reduce((keys, curr) => [...keys, ...curr], []); + + const keysPromises = []; + for await (const node of items) { + keysPromises.push(getNodeKeys(ruleset, node)); + } + + const keysPerNode = await Promise.all(keysPromises); + return keysPerNode.flat(); } // overwhelms ECDb.ConcurrentQuery with too many queries when used like this // await Promise.all( @@ -599,17 +611,19 @@ describe("#performance DataViz requests", () => { }; // retrieve the content with just the filtered properties - const content = await Presentation.presentation.getContent({ - imodel: iModel, - rulesetOrId: ruleset, - descriptor: { - fieldsSelector: { - type: "include", - fields: classFields.map((classField) => classField.filteredField.getFieldDescriptor()), + const content = await Presentation.presentation + .getContentIterator({ + imodel: iModel, + rulesetOrId: ruleset, + descriptor: { + fieldsSelector: { + type: "include", + fields: classFields.map((classField) => classField.filteredField.getFieldDescriptor()), + }, }, - }, - keys: new KeySet(), - }); + keys: new KeySet(), + }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); assert(!!content); ++requestsCount.elementIds; diff --git a/full-stack-tests/rpc-interface/src/frontend/PresentationRpc.test.ts b/full-stack-tests/rpc-interface/src/frontend/PresentationRpc.test.ts index 6f7d01ef7a86..bc9cde0a73a3 100644 --- a/full-stack-tests/rpc-interface/src/frontend/PresentationRpc.test.ts +++ b/full-stack-tests/rpc-interface/src/frontend/PresentationRpc.test.ts @@ -33,21 +33,13 @@ describe("PresentationRpcInterface tests", () => { }); it("getNodes works as expected", async () => { - const rootNodes = await Presentation.presentation.getNodes({ + const rootNodes = await Presentation.presentation.getNodesIterator({ imodel, rulesetOrId: createNodesRuleset(), }); expect(rootNodes).to.not.be.empty; }); - it("getNodesAndCount works as expected", async () => { - const nodesAndCount = await Presentation.presentation.getNodesAndCount({ - imodel, - rulesetOrId: createNodesRuleset(), - }); - expect(nodesAndCount.count).to.not.be.undefined; - }); - it("getNodesCount works as expected", async () => { const count = await Presentation.presentation.getNodesCount({ imodel, @@ -100,6 +92,7 @@ describe("PresentationRpcInterface tests", () => { const key1: InstanceKey = { id: Id64.fromString("0x1"), className: "BisCore:Subject" }; const key2: InstanceKey = { id: Id64.fromString("0x17"), className: "BisCore:SpatialCategory" }; const keys = new KeySet([key1, key2]); + // eslint-disable-next-line deprecation/deprecation const contentAndSize = await Presentation.presentation.getContentAndSize({ imodel, rulesetOrId: createContentRuleset(), @@ -113,6 +106,7 @@ describe("PresentationRpcInterface tests", () => { const key1: InstanceKey = { id: Id64.fromString("0x1"), className: "BisCore:Subject" }; const key2: InstanceKey = { id: Id64.fromString("0x17"), className: "BisCore:SpatialCategory" }; const keys = new KeySet([key1, key2]); + // eslint-disable-next-line deprecation/deprecation const content = await Presentation.presentation.getContent({ imodel, rulesetOrId: createContentRuleset(), @@ -154,7 +148,7 @@ describe("PresentationRpcInterface tests", () => { it("getDisplayLabelDefinitions works as expected", async () => { const key1: InstanceKey = { id: Id64.fromString("0x1"), className: "BisCore:Subject" }; const key2: InstanceKey = { id: Id64.fromString("0x17"), className: "BisCore:SpatialCategory" }; - const displayLabels = await Presentation.presentation.getDisplayLabelDefinitions({ + const displayLabels = await Presentation.presentation.getDisplayLabelDefinitionsIterator({ imodel, keys: [key1, key2], }); diff --git a/presentation/common/src/presentation-common/LocalizationHelper.ts b/presentation/common/src/presentation-common/LocalizationHelper.ts index f8607f62ec35..cec63b79ee43 100644 --- a/presentation/common/src/presentation-common/LocalizationHelper.ts +++ b/presentation/common/src/presentation-common/LocalizationHelper.ts @@ -144,7 +144,7 @@ export class LocalizationHelper { return category; } - private getLocalizedNode(node: Node) { + public getLocalizedNode(node: Node): Node { return { ...node, label: this.getLocalizedLabelDefinition(node.label), diff --git a/presentation/common/src/test/_helpers/Promises.ts b/presentation/common/src/test/_helpers/Promises.ts index 4c35f849ccd8..e36d8cb119d0 100644 --- a/presentation/common/src/test/_helpers/Promises.ts +++ b/presentation/common/src/test/_helpers/Promises.ts @@ -39,20 +39,28 @@ export class PromiseContainer { /** * @internal Used for testing only. */ -export class ResolvablePromise implements PromiseLike { +export class ResolvablePromise implements Promise { private _wrapped: Promise; private _resolve!: (value: T) => void; private _reject!: (msg?: string) => void; + + public [Symbol.toStringTag] = "ResolvablePromise"; public constructor() { this._wrapped = new Promise((resolve: (value: T) => void, reject: (reason?: any) => void) => { this._resolve = resolve; this._reject = reject; }); } - public then( + public async catch(onrejected?: ((reason: any) => TResult | PromiseLike) | null | undefined): Promise { + return this._wrapped.catch(onrejected); + } + public async finally(onfinally?: (() => void) | null | undefined): Promise { + return this._wrapped.finally(onfinally); + } + public async then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null, - ): PromiseLike { + ): Promise { return this._wrapped.then(onfulfilled, onrejected); } public async resolve(result: T) { diff --git a/presentation/frontend/src/presentation-frontend/PresentationManager.ts b/presentation/frontend/src/presentation-frontend/PresentationManager.ts index 4e0c776a7939..858a22292b2b 100644 --- a/presentation/frontend/src/presentation-frontend/PresentationManager.ts +++ b/presentation/frontend/src/presentation-frontend/PresentationManager.ts @@ -35,6 +35,7 @@ import { HierarchyUpdateInfo, InstanceKey, Item, + ItemJSON, Key, KeySet, KoqPropertyValueFormatter, @@ -59,6 +60,7 @@ import { FrontendLocalizationHelper } from "./LocalizationHelper"; import { RulesetManager, RulesetManagerImpl } from "./RulesetManager"; import { RulesetVariablesManager, RulesetVariablesManagerImpl } from "./RulesetVariablesManager"; import { TRANSIENT_ELEMENT_CLASSNAME } from "./selection/SelectionManager"; +import { StreamedResponseGenerator } from "./StreamedResponseGenerator"; /** * Data structure that describes IModel hierarchy change event arguments. @@ -86,6 +88,34 @@ export interface IModelContentChangeEventArgs { imodelKey: string; } +/** + * Options for requests that can return multiple pages of items concurrently. + * @public + */ +export type MultipleValuesRequestOptions = Paged<{ + maxParallelRequests?: number; +}>; + +/** + * Options for requests that retrieve nodes. + * @public + */ +export type GetNodesRequestOptions = HierarchyRequestOptions & ClientDiagnosticsAttribute; + +/** + * Options for requests that retrieve content. + * @public + */ +export type GetContentRequestOptions = ContentRequestOptions & + ClientDiagnosticsAttribute; + +/** + * Options for requests that retrieve distinct values. + * @public + */ +export type GetDistinctValuesRequestOptions = DistinctValuesRequestOptions & + ClientDiagnosticsAttribute; + /** * Properties used to configure [[PresentationManager]] * @public @@ -358,44 +388,55 @@ export class PresentationManager implements IDisposable { return { ...options, rulesetOrId: foundRulesetOrId, rulesetVariables: variables }; } - /** Retrieves nodes */ - public async getNodes( - requestOptions: Paged> & ClientDiagnosticsAttribute, - ): Promise { + /** Returns an iterator that polls nodes asynchronously. */ + public async getNodesIterator( + requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions, + ): Promise<{ total: number; items: AsyncIterableIterator }> { this.startIModelInitialization(requestOptions.imodel); const options = await this.addRulesetAndVariablesToOptions(requestOptions); const rpcOptions = this.toRpcTokenOptions({ ...options }); - const result = await buildPagedArrayResponse(options.paging, async (partialPageOptions) => - this._requestsHandler.getPagedNodes({ ...rpcOptions, paging: partialPageOptions }), - ); - // eslint-disable-next-line deprecation/deprecation - return this._localizationHelper.getLocalizedNodes(result.items.map(Node.fromJSON)); + + const generator = new StreamedResponseGenerator({ + ...requestOptions, + getBatch: async (paging) => { + const result = await this._requestsHandler.getPagedNodes({ ...rpcOptions, paging }); + return { + total: result.total, + // eslint-disable-next-line deprecation/deprecation + items: this._localizationHelper.getLocalizedNodes(result.items.map(Node.fromJSON)), + }; + }, + }); + + return generator.createAsyncIteratorResponse(); + } + + /** + * Retrieves nodes + * @deprecated in 4.5. Use [[getNodesIterator]] instead. + */ + public async getNodes(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise { + const result = await this.getNodesIterator(requestOptions); + return collect(result.items); } /** Retrieves nodes count. */ - public async getNodesCount( - requestOptions: HierarchyRequestOptions & ClientDiagnosticsAttribute, - ): Promise { + public async getNodesCount(requestOptions: GetNodesRequestOptions): Promise { this.startIModelInitialization(requestOptions.imodel); const options = await this.addRulesetAndVariablesToOptions(requestOptions); const rpcOptions = this.toRpcTokenOptions({ ...options }); return this._requestsHandler.getNodesCount(rpcOptions); } - /** Retrieves total nodes count and a single page of nodes. */ - public async getNodesAndCount( - requestOptions: Paged> & ClientDiagnosticsAttribute, - ): Promise<{ count: number; nodes: Node[] }> { - this.startIModelInitialization(requestOptions.imodel); - const options = await this.addRulesetAndVariablesToOptions(requestOptions); - const rpcOptions = this.toRpcTokenOptions({ ...options }); - const result = await buildPagedArrayResponse(options.paging, async (partialPageOptions) => - this._requestsHandler.getPagedNodes({ ...rpcOptions, paging: partialPageOptions }), - ); + /** + * Retrieves total nodes count and a single page of nodes. + * @deprecated in 4.5. Use [[getNodesIterator]] instead. + */ + public async getNodesAndCount(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<{ count: number; nodes: Node[] }> { + const result = await this.getNodesIterator(requestOptions); return { count: result.total, - // eslint-disable-next-line deprecation/deprecation - nodes: this._localizationHelper.getLocalizedNodes(result.items.map(Node.fromJSON)), + nodes: await collect(result.items), }; } @@ -473,9 +514,7 @@ export class PresentationManager implements IDisposable { } /** Retrieves overall content set size. */ - public async getContentSetSize( - requestOptions: ContentRequestOptions & ClientDiagnosticsAttribute, - ): Promise { + public async getContentSetSize(requestOptions: GetContentRequestOptions): Promise { this.startIModelInitialization(requestOptions.imodel); const options = await this.addRulesetAndVariablesToOptions(requestOptions); const rpcOptions = this.toRpcTokenOptions({ @@ -486,66 +525,118 @@ export class PresentationManager implements IDisposable { return this._requestsHandler.getContentSetSize(rpcOptions); } - /** Retrieves content which consists of a content descriptor and a page of records. */ - public async getContent( - requestOptions: Paged> & ClientDiagnosticsAttribute, - ): Promise { - return (await this.getContentAndSize(requestOptions))?.content; - } + private async getContentIteratorInternal( + requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions, + ): Promise<{ descriptor: Descriptor; total: number; items: AsyncIterableIterator } | undefined> { + const options = await this.addRulesetAndVariablesToOptions(requestOptions); + const rpcOptions = this.toRpcTokenOptions({ + ...options, + descriptor: getDescriptorOverrides(requestOptions.descriptor), + keys: stripTransientElementKeys(requestOptions.keys).toJSON(), + ...(!requestOptions.omitFormattedValues && this._schemaContextProvider !== undefined ? { omitFormattedValues: true } : undefined), + }); - /** Retrieves content set size and content which consists of a content descriptor and a page of records. */ - public async getContentAndSize( - requestOptions: Paged> & ClientDiagnosticsAttribute, - ): Promise<{ content: Content; size: number } | undefined> { - this.startIModelInitialization(requestOptions.imodel); - try { - const options = await this.addRulesetAndVariablesToOptions(requestOptions); - const rpcOptions = this.toRpcTokenOptions({ - ...options, - descriptor: getDescriptorOverrides(requestOptions.descriptor), - keys: stripTransientElementKeys(requestOptions.keys).toJSON(), - ...(!requestOptions.omitFormattedValues && this._schemaContextProvider !== undefined ? { omitFormattedValues: true } : undefined), - }); - let descriptor = requestOptions.descriptor instanceof Descriptor ? requestOptions.descriptor : undefined; - const result = await buildPagedArrayResponse(options.paging, async (partialPageOptions, requestIndex) => { - if (0 === requestIndex && !descriptor) { - const content = await this._requestsHandler.getPagedContent({ ...rpcOptions, paging: partialPageOptions }); - if (content) { - descriptor = Descriptor.fromJSON(content.descriptor); - return content.contentSet; - } - return { total: 0, items: [] }; - } - return this._requestsHandler.getPagedContentSet({ ...rpcOptions, paging: partialPageOptions }); - }); - if (!descriptor) { + let contentFormatter: ContentFormatter | undefined; + if (!requestOptions.omitFormattedValues && this._schemaContextProvider) { + const koqPropertyFormatter = new KoqPropertyValueFormatter(this._schemaContextProvider(requestOptions.imodel), this._defaultFormats); + contentFormatter = new ContentFormatter( + new ContentPropertyValueFormatter(koqPropertyFormatter), + requestOptions.unitSystem ?? this._explicitActiveUnitSystem ?? IModelApp.quantityFormatter.activeUnitSystem, + ); + } + + let descriptor = requestOptions.descriptor instanceof Descriptor ? requestOptions.descriptor : undefined; + let firstPage: PagedResponse | undefined; + if (!descriptor) { + const firstPageResponse = await this._requestsHandler.getPagedContent(rpcOptions); + if (!firstPageResponse?.descriptor || !firstPageResponse.contentSet) { return undefined; } + descriptor = Descriptor.fromJSON(firstPageResponse?.descriptor); + firstPage = firstPageResponse?.contentSet; + } - const items = result.items.map((itemJson) => Item.fromJSON(itemJson)).filter((item): item is Item => item !== undefined); - const resultContent = new Content(descriptor, items); - if (!requestOptions.omitFormattedValues && this._schemaContextProvider) { - const koqPropertyFormatter = new KoqPropertyValueFormatter(this._schemaContextProvider(requestOptions.imodel), this._defaultFormats); - const contentFormatter = new ContentFormatter( - new ContentPropertyValueFormatter(koqPropertyFormatter), - requestOptions.unitSystem ?? this._explicitActiveUnitSystem ?? IModelApp.quantityFormatter.activeUnitSystem, - ); - await contentFormatter.formatContent(resultContent); + // istanbul ignore if + if (!descriptor) { + return undefined; + } + + descriptor = this._localizationHelper.getLocalizedContentDescriptor(descriptor); + + const getPage = async (paging: Required, requestIndex: number) => { + let contentSet = requestIndex === 0 ? firstPage : undefined; + contentSet ??= await this._requestsHandler.getPagedContentSet({ ...rpcOptions, paging }); + + let items = contentSet.items.map((x) => Item.fromJSON(x)).filter((x): x is Item => x !== undefined); + if (contentFormatter) { + items = await contentFormatter.formatContentItems(items, descriptor!); } + items = this._localizationHelper.getLocalizedContentItems(items); return { - size: result.total, - content: this._localizationHelper.getLocalizedContent(resultContent), + total: contentSet.total, + items, }; - } finally { - await this.ensureIModelInitialized(requestOptions.imodel); + }; + + const generator = new StreamedResponseGenerator({ + ...requestOptions, + getBatch: getPage, + }); + + return { + ...(await generator.createAsyncIteratorResponse()), + descriptor, + }; + } + + /** Retrieves a content descriptor, item count and async generator for the items themselves. */ + public async getContentIterator( + requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions, + ): Promise<{ descriptor: Descriptor; total: number; items: AsyncIterableIterator } | undefined> { + this.startIModelInitialization(requestOptions.imodel); + const response = await this.getContentIteratorInternal(requestOptions); + if (!response) { + return undefined; } + + await this.ensureIModelInitialized(requestOptions.imodel); + return response; } - /** Retrieves distinct values of specific field from the content. */ - public async getPagedDistinctValues( - requestOptions: DistinctValuesRequestOptions & ClientDiagnosticsAttribute, - ): Promise> { + /** + * Retrieves content which consists of a content descriptor and a page of records. + * @deprecated in 4.5. Use [[getContentIterator]] instead. + */ + public async getContent(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise { + // eslint-disable-next-line deprecation/deprecation + return (await this.getContentAndSize(requestOptions))?.content; + } + + /** + * Retrieves content set size and content which consists of a content descriptor and a page of records. + * @deprecated in 4.5. Use [[getContentIterator]] instead. + */ + public async getContentAndSize( + requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions, + ): Promise<{ content: Content; size: number } | undefined> { + const response = await this.getContentIterator(requestOptions); + if (!response) { + return undefined; + } + + const { descriptor, total } = response; + const items = await collect(response.items); + return { + content: new Content(descriptor, items), + size: total, + }; + } + + /** Returns an iterator that asynchronously polls distinct values of specific field from the content. */ + public async getDistinctValuesIterator( + requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions, + ): Promise<{ total: number; items: AsyncIterableIterator }> { this.startIModelInitialization(requestOptions.imodel); const options = await this.addRulesetAndVariablesToOptions(requestOptions); const rpcOptions = { @@ -553,13 +644,33 @@ export class PresentationManager implements IDisposable { descriptor: getDescriptorOverrides(options.descriptor), keys: stripTransientElementKeys(options.keys).toJSON(), }; - const result = await buildPagedArrayResponse(requestOptions.paging, async (partialPageOptions) => - this._requestsHandler.getPagedDistinctValues({ ...rpcOptions, paging: partialPageOptions }), - ); + + const generator = new StreamedResponseGenerator({ + ...requestOptions, + getBatch: async (paging) => { + const response = await this._requestsHandler.getPagedDistinctValues({ ...rpcOptions, paging }); + return { + total: response.total, + // eslint-disable-next-line deprecation/deprecation + items: response.items.map((x) => this._localizationHelper.getLocalizedDisplayValueGroup(DisplayValueGroup.fromJSON(x))), + }; + }, + }); + + return generator.createAsyncIteratorResponse(); + } + + /** + * Retrieves distinct values of specific field from the content. + * @deprecated in 4.5. Use [[getDistinctValuesIterator]] instead. + */ + public async getPagedDistinctValues( + requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions, + ): Promise> { + const result = await this.getDistinctValuesIterator(requestOptions); return { - ...result, - // eslint-disable-next-line deprecation/deprecation - items: result.items.map(DisplayValueGroup.fromJSON).map((g) => this._localizationHelper.getLocalizedDisplayValueGroup(g)), + total: result.total, + items: await collect(result.items), }; } @@ -584,7 +695,7 @@ export class PresentationManager implements IDisposable { * @public */ public async getContentInstanceKeys( - requestOptions: ContentInstanceKeysRequestOptions & ClientDiagnosticsAttribute, + requestOptions: ContentInstanceKeysRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions, ): Promise<{ total: number; items: () => AsyncGenerator }> { this.startIModelInitialization(requestOptions.imodel); const options = await this.addRulesetAndVariablesToOptions(requestOptions); @@ -593,9 +704,9 @@ export class PresentationManager implements IDisposable { keys: stripTransientElementKeys(options.keys).toJSON(), }; - const props = { - page: requestOptions.paging, - get: async (page: Required) => { + const generator = new StreamedResponseGenerator({ + ...requestOptions, + getBatch: async (page) => { const keys = await this._requestsHandler.getContentInstanceKeys({ ...rpcOptions, paging: page }); return { total: keys.total, @@ -607,8 +718,15 @@ export class PresentationManager implements IDisposable { }, new Array()), }; }, + }); + + const { total, items } = await generator.createAsyncIteratorResponse(); + return { + total, + async *items() { + yield* items; + }, }; - return createPagedGeneratorResponse(props); } /** Retrieves display label definition of specific item. */ @@ -622,16 +740,33 @@ export class PresentationManager implements IDisposable { } /** Retrieves display label definition of specific items. */ - public async getDisplayLabelDefinitions( - requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute, - ): Promise { + public async getDisplayLabelDefinitionsIterator( + requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions, + ): Promise<{ total: number; items: AsyncIterableIterator }> { this.startIModelInitialization(requestOptions.imodel); const rpcOptions = this.toRpcTokenOptions({ ...requestOptions }); - const result = await buildPagedArrayResponse(undefined, async (partialPageOptions) => { - const partialKeys = !partialPageOptions.start ? rpcOptions.keys : rpcOptions.keys.slice(partialPageOptions.start); - return this._requestsHandler.getPagedDisplayLabelDefinitions({ ...rpcOptions, keys: partialKeys }); + const generator = new StreamedResponseGenerator({ + ...requestOptions, + getBatch: async (page) => { + const partialKeys = !page.start ? rpcOptions.keys : rpcOptions.keys.slice(page.start); + const result = await this._requestsHandler.getPagedDisplayLabelDefinitions({ ...rpcOptions, keys: partialKeys }); + result.items = this._localizationHelper.getLocalizedLabelDefinitions(result.items); + return result; + }, }); - return this._localizationHelper.getLocalizedLabelDefinitions(result.items); + + return generator.createAsyncIteratorResponse(); + } + + /** + * Retrieves display label definition of specific items. + * @deprecated in 4.5. Use [[getDisplayLabelDefinitionsIterator]] instead. + */ + public async getDisplayLabelDefinitions( + requestOptions: DisplayLabelsRequestOptions & ClientDiagnosticsAttribute & MultipleValuesRequestOptions, + ): Promise { + const { items } = await this.getDisplayLabelDefinitionsIterator(requestOptions); + return collect(items); } } @@ -642,61 +777,6 @@ const getDescriptorOverrides = (descriptorOrOverrides: Descriptor | DescriptorOv return descriptorOrOverrides; }; -interface PagedGeneratorCreateProps { - page: PageOptions | undefined; - get: (pageStart: Required, requestIndex: number) => Promise<{ total: number; items: TPagedResponseItem[] }>; -} -async function createPagedGeneratorResponse(props: PagedGeneratorCreateProps) { - let pageStart = props.page?.start ?? 0; - let pageSize = props.page?.size ?? 0; - let requestIndex = 0; - - const firstPage = await props.get({ start: pageStart, size: pageSize }, requestIndex++); - return { - total: firstPage.total, - async *items() { - let partialResult = firstPage; - while (true) { - for (const item of partialResult.items) { - yield item; - } - - const receivedItemsCount = partialResult.items.length; - if (partialResult.total !== 0 && receivedItemsCount === 0) { - if (pageStart >= partialResult.total) { - throw new Error(`Requested page with start index ${pageStart} is out of bounds. Total number of items: ${partialResult.total}`); - } - throw new Error("Paged request returned non zero total count but no items"); - } - - if ((pageSize !== 0 && receivedItemsCount >= pageSize) || receivedItemsCount >= partialResult.total - pageStart) { - break; - } - - if (pageSize !== 0) { - pageSize -= receivedItemsCount; - } - pageStart += receivedItemsCount; - - partialResult = await props.get({ start: pageStart, size: pageSize }, requestIndex++); - } - }, - }; -} - -/** @internal */ -export const buildPagedArrayResponse = async ( - requestedPage: PageOptions | undefined, - getter: (page: Required, requestIndex: number) => Promise>, -): Promise> => { - const items = new Array(); - const gen = await createPagedGeneratorResponse({ page: requestedPage, get: getter }); - for await (const item of gen.items()) { - items.push(item); - } - return { total: gen.total, items }; -}; - const stripTransientElementKeys = (keys: KeySet) => { if (!keys.some((key) => Key.isInstanceKey(key) && key.className === TRANSIENT_ELEMENT_CLASSNAME)) { return keys; @@ -714,3 +794,11 @@ const stripTransientElementKeys = (keys: KeySet) => { }); return copy; }; + +async function collect(iter: AsyncIterable): Promise { + const result = new Array(); + for await (const value of iter) { + result.push(value); + } + return result; +} diff --git a/presentation/frontend/src/presentation-frontend/StreamedResponseGenerator.ts b/presentation/frontend/src/presentation-frontend/StreamedResponseGenerator.ts new file mode 100644 index 000000000000..4e06c65385af --- /dev/null +++ b/presentation/frontend/src/presentation-frontend/StreamedResponseGenerator.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import { concat, concatAll, map, mergeMap, Observable, of, range, scan } from "rxjs"; +import { eachValueFrom } from "rxjs-for-await"; +import { PagedResponse, PageOptions } from "@itwin/presentation-common"; +import { SortedArray } from "@itwin/core-bentley"; +import { MultipleValuesRequestOptions } from "./PresentationManager"; + +/** + * Properties for streaming the results. + * @internal + */ +export type StreamedResponseGeneratorProps = MultipleValuesRequestOptions & { + getBatch(page: Required, requestIdx: number): Promise>; +}; + +/** + * This class allows loading values in multiple parallel batches and return them either as an array or an async iterator. + * Pages are prefetched in advanced according to the `parallelism` argument. + * @internal + */ +export class StreamedResponseGenerator { + constructor(private readonly _props: StreamedResponseGeneratorProps) {} + + /** Creates a response with the total item count and an async iterator. */ + public async createAsyncIteratorResponse(): Promise<{ total: number; items: AsyncIterableIterator }> { + const firstPage = await this.fetchFirstPage(); + return { + total: firstPage.total, + items: eachValueFrom(this.getRemainingPages(firstPage).pipe(concatAll())), + }; + } + + /** + * Fetches the first page. + * This function has to be called in order to retrieve the total items count. + */ + private async fetchFirstPage(): Promise> { + const start = this._props.paging?.start ?? 0; + const batchSize = this._props.paging?.size ?? 0; + return this._props.getBatch({ start, size: batchSize }, 0); + } + + private getRemainingPages(firstPage: PagedResponse): Observable { + const pageStart = this._props.paging?.start ?? 0; + const maxParallelRequests = this._props.maxParallelRequests; + const pageSize = this._props.paging?.size; + const { total, items: firstPageItems } = firstPage; + + // If there are no items, return a single empty page. + if (total === 0) { + return of([]); + } + + // If the response is empty, something went wrong. + const receivedItemsLength = firstPage.items.length; + if (!receivedItemsLength) { + handleEmptyPageResult(pageStart, total); + } + + const totalItemsToFetch = total - pageStart; + if (receivedItemsLength === totalItemsToFetch) { + return of(firstPageItems); + } + + let itemsToFetch: number; + let batchSize: number; + if (pageSize) { + itemsToFetch = Math.min(totalItemsToFetch, pageSize) - receivedItemsLength; + batchSize = Math.min(pageSize, receivedItemsLength); + } else { + itemsToFetch = totalItemsToFetch - receivedItemsLength; + batchSize = receivedItemsLength; + } + + const remainingBatches = Math.ceil(itemsToFetch / batchSize); + + // Return the first page and then stream the remaining ones. + return concat( + of(firstPage.items), + range(1, remainingBatches).pipe( + mergeMap(async (idx) => { + const start = pageStart + idx * batchSize; + const size = Math.min(total - start, batchSize); + const page = await this._props.getBatch({ start, size }, idx); + if (!page.items.length) { + handleEmptyPageResult(start, total); + } + + // Pass along the index, so that the items could be sorted. + return { idx, items: page.items }; + }, maxParallelRequests), + scan( + // Collect the emitted pages an emit them in the correct order. + (acc, value) => { + let { lastEmitted } = acc; + const { accumulatedBatches } = acc; + const { idx } = value; + + // If current batch is not in order, put it in the accumulator + if (idx - 1 !== lastEmitted) { + accumulatedBatches.insert(value); + return { lastEmitted, accumulatedBatches, itemsToEmit: [] }; + } + + // Collect all batches to emit in order. + lastEmitted = idx; + const batchesToEmit = [value]; + for (const batch of accumulatedBatches) { + if (batch.idx - 1 !== lastEmitted) { + break; + } + lastEmitted = batch.idx; + batchesToEmit.push(batch); + } + + // Remove batches to emit from the accumulator. + for (const batch of batchesToEmit) { + accumulatedBatches.remove(batch); + } + + const itemsToEmit = batchesToEmit.flatMap((x) => x.items); + return { lastEmitted, accumulatedBatches, itemsToEmit }; + }, + { + lastEmitted: 0, + accumulatedBatches: new SortedArray<{ idx: number; items: TPagedResponseItem[] }>((a, b) => a.idx - b.idx), + itemsToEmit: new Array(), + }, + ), + map(({ itemsToEmit }) => itemsToEmit), + ), + ); + } +} + +function handleEmptyPageResult(pageStart: number, total: number) { + if (pageStart >= total) { + throw new Error(`Requested page with start index ${pageStart} is out of bounds. Total number of items: ${total}`); + } + throw new Error("Paged request returned non zero total count but no items"); +} diff --git a/presentation/frontend/src/presentation-frontend/selection/HiliteSetProvider.ts b/presentation/frontend/src/presentation-frontend/selection/HiliteSetProvider.ts index 76202c9bac0c..959655d81f8e 100644 --- a/presentation/frontend/src/presentation-frontend/selection/HiliteSetProvider.ts +++ b/presentation/frontend/src/presentation-frontend/selection/HiliteSetProvider.ts @@ -112,7 +112,7 @@ export class HiliteSetProvider { for (const batch of keyBatches) { let loadedItems = 0; while (true) { - const content = await Presentation.presentation.getContentAndSize({ + const content = await Presentation.presentation.getContentIterator({ ...options, paging: { start: loadedItems, size: CONTENT_SET_PAGE_SIZE }, keys: batch, @@ -121,11 +121,15 @@ export class HiliteSetProvider { break; } - const result = this.createHiliteSet(content.content.contentSet); + const items = new Array(); + for await (const item of content.items) { + items.push(item); + } + const result = this.createHiliteSet(items); yield result; - loadedItems += content.content.contentSet.length; - if (loadedItems >= content.size) { + loadedItems += items.length; + if (loadedItems >= content.total) { break; } } diff --git a/presentation/frontend/src/test/PresentationManager.test.ts b/presentation/frontend/src/test/PresentationManager.test.ts index 6f88f5a667ea..ddcbcf309dbe 100644 --- a/presentation/frontend/src/test/PresentationManager.test.ts +++ b/presentation/frontend/src/test/PresentationManager.test.ts @@ -66,7 +66,6 @@ import { import { IpcRequestsHandler } from "../presentation-frontend/IpcRequestsHandler"; import { Presentation } from "../presentation-frontend/Presentation"; import { - buildPagedArrayResponse, IModelContentChangeEventArgs, IModelHierarchyChangeEventArgs, PresentationManager, @@ -76,6 +75,8 @@ import { RulesetManagerImpl } from "../presentation-frontend/RulesetManager"; import { RulesetVariablesManagerImpl } from "../presentation-frontend/RulesetVariablesManager"; import { TRANSIENT_ELEMENT_CLASSNAME } from "../presentation-frontend/selection/SelectionManager"; +/* eslint-disable deprecation/deprecation */ + describe("PresentationManager", () => { const rulesetsManagerMock = moq.Mock.ofType(); const rpcRequestsHandlerMock = moq.Mock.ofType(); @@ -505,7 +506,7 @@ describe("PresentationManager", () => { .returns(async () => ({ total: count, items: [Node.toJSON(node1)] })) .verifiable(); rpcRequestsHandlerMock - .setup(async (x) => x.getPagedNodes(toRulesetRpcOptions({ ...options, parentKey: parentNodeKey, paging: { start: 1, size: 0 } }))) + .setup(async (x) => x.getPagedNodes(toRulesetRpcOptions({ ...options, parentKey: parentNodeKey, paging: { start: 1, size: 1 } }))) // eslint-disable-next-line deprecation/deprecation .returns(async () => ({ total: count, items: [Node.toJSON(node2)] })) .verifiable(); @@ -527,7 +528,7 @@ describe("PresentationManager", () => { rpcRequestsHandlerMock .setup(async (x) => x.getPagedNodes(toRulesetRpcOptions(options))) // eslint-disable-next-line deprecation/deprecation - .returns(async () => ({ total: 666, items: result.map(Node.toJSON) })) + .returns(async () => ({ total: result.length, items: result.map(Node.toJSON) })) .verifiable(); const actualResult = await manager.getNodes(options); expect(actualResult).to.deep.eq(result); @@ -595,7 +596,7 @@ describe("PresentationManager", () => { .returns(async () => ({ total: count, items: [Node.toJSON(node1)] })) .verifiable(); rpcRequestsHandlerMock - .setup(async (x) => x.getPagedNodes(toRulesetRpcOptions({ ...options, parentKey: parentNodeKey, paging: { start: 1, size: 0 } }))) + .setup(async (x) => x.getPagedNodes(toRulesetRpcOptions({ ...options, parentKey: parentNodeKey, paging: { start: 1, size: 1 } }))) // eslint-disable-next-line deprecation/deprecation .returns(async () => ({ total: count, items: [Node.toJSON(node2)] })) .verifiable(); @@ -1160,6 +1161,7 @@ describe("PresentationManager", () => { ) .returns(async () => ({ total: 5, items: [item2.toJSON()] })) .verifiable(); + const actualResult = await manager.getContentAndSize(options); expect(actualResult).to.deep.eq({ size: 5, @@ -1264,7 +1266,7 @@ describe("PresentationManager", () => { .returns(async () => ({ total: 2, items: [DisplayValueGroup.toJSON(item1)] })) .verifiable(); rpcRequestsHandlerMock - .setup(async (x) => x.getPagedDistinctValues({ ...rpcHandlerOptions, paging: { start: 1, size: 0 } })) + .setup(async (x) => x.getPagedDistinctValues({ ...rpcHandlerOptions, paging: { start: 1, size: 1 } })) // eslint-disable-next-line deprecation/deprecation .returns(async () => ({ total: 2, items: [DisplayValueGroup.toJSON(item2)] })) .verifiable(); @@ -1348,7 +1350,7 @@ describe("PresentationManager", () => { .returns(async () => ({ total: 4, items: new KeySet(instanceKeys1).toJSON() })) .verifiable(); rpcRequestsHandlerMock - .setup(async (x) => x.getContentInstanceKeys({ ...rpcHandlerOptions, paging: { start: 2, size: 0 } })) + .setup(async (x) => x.getContentInstanceKeys({ ...rpcHandlerOptions, paging: { start: 2, size: 2 } })) .returns(async () => ({ total: 4, items: new KeySet(instanceKeys2).toJSON() })) .verifiable(); const actualResult = await manager.getContentInstanceKeys(managerOptions); @@ -1562,76 +1564,6 @@ describe("PresentationManager", () => { }); }); }); - - describe("buildPagedArrayResponse", () => { - it("calls getter once with 0,0 partial page options when given `undefined` page options", async () => { - const getter = sinon.stub().resolves({ total: 0, items: [] }); - await buildPagedArrayResponse(undefined, getter); - expect(getter).to.be.calledOnceWith({ start: 0, size: 0 }); - }); - - it("calls getter once with 0,0 partial page options when given empty page options", async () => { - const getter = sinon.stub().resolves({ total: 0, items: [] }); - await buildPagedArrayResponse({}, getter); - expect(getter).to.be.calledOnceWith({ start: 0, size: 0 }); - }); - - it("calls getter once with partial page options equal to given page options", async () => { - const getter = sinon.stub().resolves({ total: 0, items: [] }); - await buildPagedArrayResponse({ start: 1, size: 2 }, getter); - expect(getter).to.be.calledOnceWith({ start: 1, size: 2 }); - }); - - it("calls getter multiple times until the whole requested page is received when requesting a page of specified size", async () => { - const getter = sinon.stub(); - getter.onFirstCall().resolves({ total: 5, items: [2] }); - getter.onSecondCall().resolves({ total: 5, items: [3] }); - getter.onThirdCall().resolves({ total: 5, items: [4] }); - const result = await buildPagedArrayResponse({ start: 1, size: 3 }, getter); - expect(getter).to.be.calledThrice; - expect(getter.firstCall).to.be.calledWith({ start: 1, size: 3 }); - expect(getter.secondCall).to.be.calledWith({ start: 2, size: 2 }); - expect(getter.thirdCall).to.be.calledWith({ start: 3, size: 1 }); - expect(result).to.deep.eq({ total: 5, items: [2, 3, 4] }); - }); - - it("calls getter multiple times until the whole requested page is received when requesting a page of unspecified size", async () => { - const getter = sinon.stub(); - getter.onFirstCall().resolves({ total: 5, items: [2, 3] }); - getter.onSecondCall().resolves({ total: 5, items: [4, 5] }); - const result = await buildPagedArrayResponse({ start: 1 }, getter); - expect(getter).to.be.calledTwice; - expect(getter.firstCall).to.be.calledWith({ start: 1, size: 0 }); - expect(getter.secondCall).to.be.calledWith({ start: 3, size: 0 }); - expect(result).to.deep.eq({ total: 5, items: [2, 3, 4, 5] }); - }); - - it("throws when page start index is larger than total number of items", async () => { - const getter = sinon.stub(); - getter.resolves({ total: 5, items: [] }); - await expect(buildPagedArrayResponse({ start: 9 }, getter)).to.eventually.be.rejected; - expect(getter).to.be.calledOnce; - expect(getter).to.be.calledWith({ start: 9, size: 0 }); - }); - - it("throws when partial request returns no items", async () => { - const getter = sinon.stub(); - getter.resolves({ total: 5, items: [] }); - await expect(buildPagedArrayResponse({ start: 1 }, getter)).to.eventually.be.rejected; - expect(getter).to.be.calledOnce; - expect(getter).to.be.calledWith({ start: 1, size: 0 }); - }); - - it("throws when partial request returns less items than requested", async () => { - const getter = sinon.stub(); - getter.onFirstCall().resolves({ total: 5, items: [2, 3] }); - getter.onSecondCall().resolves({ total: 5, items: [] }); - await expect(buildPagedArrayResponse({ start: 1 }, getter)).to.eventually.be.rejected; - expect(getter).to.be.calledTwice; - expect(getter.firstCall).to.be.calledWith({ start: 1, size: 0 }); - expect(getter.secondCall).to.be.calledWith({ start: 3, size: 0 }); - }); - }); }); async function generatedValues(gen: AsyncGenerator) { diff --git a/presentation/frontend/src/test/StreamedResponseGenerator.test.ts b/presentation/frontend/src/test/StreamedResponseGenerator.test.ts new file mode 100644 index 000000000000..146e096a780a --- /dev/null +++ b/presentation/frontend/src/test/StreamedResponseGenerator.test.ts @@ -0,0 +1,235 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import { StreamedResponseGenerator, StreamedResponseGeneratorProps } from "../presentation-frontend/StreamedResponseGenerator"; +import { expect } from "chai"; +import sinon from "sinon"; +import { PagedResponse, PageOptions } from "@itwin/presentation-common"; +import { ResolvablePromise } from "@itwin/presentation-common/lib/cjs/test"; + +describe("StreamedResponseGenerator", () => { + /** Creates a response with the total item count and an array of items for the requested page. */ + async function createItemsResponse(generator: StreamedResponseGenerator): Promise<{ total: number; items: T[] }> { + const response = await generator.createAsyncIteratorResponse(); + const items = new Array(); + for await (const value of response.items) { + items.push(value); + } + return { + total: response.total, + items, + }; + } + + it("should run requests concurrently", async () => { + const total = 10; + const firstBatchPromise = new ResolvablePromise>(); + const restBatchesPromise = new ResolvablePromise>(); + const fakeGetBatch = sinon.fake(async (_, idx: number) => { + return idx ? restBatchesPromise : firstBatchPromise; + }); + + const generator = new StreamedResponseGenerator({ getBatch: fakeGetBatch }); + const getItemsPromise = createItemsResponse(generator); + expect(fakeGetBatch).to.be.calledOnce; + expect(fakeGetBatch).to.be.calledWith({ start: 0, size: 0 }, 0); + + await firstBatchPromise.resolve({ total, items: [1, 2] }); + const expectedCallCount = total / 2; + expect(fakeGetBatch.callCount).to.eq(expectedCallCount); + const expectedCalls = [...new Array(expectedCallCount - 1).keys()].map((i) => [{ start: (i + 1) * 2, size: 2 }, i + 1]); + const actualCalls = fakeGetBatch + .getCalls() + .slice(1) + .map((x) => x.args); + expect(actualCalls).to.deep.eq(expectedCalls); + + await restBatchesPromise.resolve({ total, items: [3, 4] }); + const expectedResult = { total, items: [1, 2].concat(...[...new Array(expectedCallCount - 1).keys()].map(() => [3, 4])) }; + await expect(getItemsPromise).to.eventually.deep.eq(expectedResult); + }); + + it("returns values in correct order when requests resolve in different order than being made", async () => { + const total = 4; + for (const ordering of [ + [2, 1, 0], + [2, 0, 1], + ]) { + const fakePromises: ResolvablePromise>[] = [...new Array(total - 1).keys()].map(() => new ResolvablePromise()); + const generator = new StreamedResponseGenerator({ + getBatch: async (_, idx) => { + if (idx === 0) { + return { total, items: [0] }; + } + + return fakePromises[idx - 1]; + }, + }); + + const itemsPromise = createItemsResponse(generator); + for (const idx of ordering) { + await fakePromises[idx].resolve({ total, items: [idx + 1] }); + } + + const { items } = await itemsPromise; + expect(items).to.deep.eq([...new Array(total).keys()]); + } + }); + + it("should handle a page larger than the item count", async () => { + const items = [1, 2, 3, 4]; + const props: StreamedResponseGeneratorProps = { + paging: { start: 0, size: 8 }, + getBatch: async (page) => ({ total: items.length, items: items.slice(page.start, page.start + page.size) }), + }; + const generator = new StreamedResponseGenerator(props); + const receivedValues = await createItemsResponse(generator); + expect(receivedValues.items).to.deep.equal(items); + }); + + it("should respect the provided parallelism", async () => { + const items = [...new Array(100).keys()]; + const maxParallelRequests = 2; + const fakePageRetriever = sinon.fake(async (page) => ({ + total: items.length, + items: items.slice(page.start, page.start + 10), + })); + const props: StreamedResponseGeneratorProps = { + getBatch: fakePageRetriever, + maxParallelRequests, + }; + + const generator = new StreamedResponseGenerator(props); + const { items: iterator } = await generator.createAsyncIteratorResponse(); + + // The call for the first page should happen immediately + expect(fakePageRetriever).to.be.calledOnce; + + // Then after polling the first page, it should prefetch `2 * parallelism` pages in advance. + await iterator.next(); + expect(fakePageRetriever.callCount).to.equal(maxParallelRequests * 2 + 1); + }); + + it("should fetch items up to requested page size", async () => { + const items = [...new Array(100).keys()]; + const fakePageRetriever = sinon.fake(async (page) => ({ + total: items.length, + items: items.slice(page.start, page.start + 2), + })); + const props: StreamedResponseGeneratorProps = { + paging: { size: 6 }, + getBatch: fakePageRetriever, + }; + + const generator = new StreamedResponseGenerator(props); + const { items: generatedItems } = await createItemsResponse(generator); + + expect(generatedItems).to.deep.eq([0, 1, 2, 3, 4, 5]); + expect(fakePageRetriever).to.be.calledThrice; + }); + + it("should fetch each batch once", async () => { + const items = [0, 1, 2, 3, 4, 5, 6]; + const requestedBatches = new Set>(); + const fakePageRetriever = sinon.fake(async (page) => { + if (requestedBatches.has(page)) { + throw new Error(`Page requested multiple times: ${JSON.stringify(page)}`); + } + + requestedBatches.add(page); + return { + total: items.length, + items: items.slice(page.start, page.start + 2), + }; + }); + + const props: StreamedResponseGeneratorProps = { + getBatch: fakePageRetriever, + }; + + const generator = new StreamedResponseGenerator(props); + await createItemsResponse(generator); + }); + + it("calls getter once with 0,0 partial page options when given `undefined` page options", async () => { + const getter = sinon.stub().resolves({ total: 0, items: [] }); + await createItemsResponse(new StreamedResponseGenerator({ getBatch: getter })); + expect(getter).to.be.calledOnceWith({ start: 0, size: 0 }); + }); + + it("calls getter once with 0,0 partial page options when given empty page options", async () => { + const getter = sinon.stub().resolves({ total: 0, items: [] }); + await createItemsResponse(new StreamedResponseGenerator({ paging: {}, getBatch: getter })); + expect(getter).to.be.calledOnceWith({ start: 0, size: 0 }); + }); + + it("calls getter once with partial page options equal to given page options", async () => { + const getter = sinon.stub().resolves({ total: 0, items: [] }); + await createItemsResponse(new StreamedResponseGenerator({ paging: { start: 1, size: 2 }, getBatch: getter })); + expect(getter).to.be.calledOnceWith({ start: 1, size: 2 }); + }); + + it("calls getter multiple times until the whole requested page is received when requesting a page of specified size", async () => { + const getter = sinon.stub(); + const total = 5; + getter.onFirstCall().resolves({ total, items: [2] }); + getter.onSecondCall().resolves({ total, items: [3] }); + getter.onThirdCall().resolves({ total, items: [4] }); + + const generator = new StreamedResponseGenerator({ paging: { start: 1, size: 3 }, getBatch: getter }); + const { total: actualTotal, items } = await createItemsResponse(generator); + + expect(getter).to.be.calledThrice; + expect(getter.firstCall).to.be.calledWith({ start: 1, size: 3 }); + expect(getter.secondCall).to.be.calledWith({ start: 2, size: 1 }); + expect(getter.thirdCall).to.be.calledWith({ start: 3, size: 1 }); + expect(items).to.deep.eq([2, 3, 4]); + expect(actualTotal).to.eq(total); + }); + + it("calls getter multiple times until the whole requested page is received when requesting a page of unspecified size", async () => { + const getter = sinon.stub(); + getter.onFirstCall().resolves({ total: 5, items: [2, 3] }); + getter.onSecondCall().resolves({ total: 5, items: [4, 5] }); + + const generator = new StreamedResponseGenerator({ paging: { start: 1 }, getBatch: getter }); + const { total, items } = await createItemsResponse(generator); + + expect(getter).to.be.calledTwice; + expect(getter.firstCall).to.be.calledWith({ start: 1, size: 0 }); + expect(getter.secondCall).to.be.calledWith({ start: 3, size: 2 }); + expect(items).to.deep.eq([2, 3, 4, 5]); + expect(total).to.eq(5); + }); + + it("throws when page start index is larger than total number of items", async () => { + const getter = sinon.stub(); + getter.resolves({ total: 5, items: [] }); + const generator = new StreamedResponseGenerator({ paging: { start: 9 }, getBatch: getter }); + + await expect(createItemsResponse(generator)).to.eventually.be.rejected; + expect(getter).to.be.calledOnce; + expect(getter).to.be.calledWith({ start: 9, size: 0 }); + }); + + it("throws when partial request returns no items", async () => { + const getter = sinon.stub(); + getter.resolves({ total: 5, items: [] }); + const generator = new StreamedResponseGenerator({ paging: { start: 1 }, getBatch: getter }); + + await expect(createItemsResponse(generator)).to.eventually.be.rejected; + expect(getter).to.be.calledOnce; + expect(getter).to.be.calledWith({ start: 1, size: 0 }); + }); + + it("throws when partial request returns less items than requested", async () => { + const getter = sinon.stub(); + getter.onFirstCall().resolves({ total: 5, items: [2, 3] }); + getter.onSecondCall().resolves({ total: 5, items: [] }); + const generator = new StreamedResponseGenerator({ paging: { start: 1 }, getBatch: getter }); + + await expect(createItemsResponse(generator)).to.eventually.be.rejected; + expect(getter).to.be.called; + }); +}); diff --git a/presentation/frontend/src/test/selection/HiliteSetProvider.test.ts b/presentation/frontend/src/test/selection/HiliteSetProvider.test.ts index 410206b9a978..e28a53a86ac1 100644 --- a/presentation/frontend/src/test/selection/HiliteSetProvider.test.ts +++ b/presentation/frontend/src/test/selection/HiliteSetProvider.test.ts @@ -6,23 +6,31 @@ import { expect } from "chai"; import * as moq from "typemoq"; import { IModelConnection } from "@itwin/core-frontend"; -import { Content, DEFAULT_KEYS_BATCH_SIZE, Item, KeySet } from "@itwin/presentation-common"; +import { Content, DEFAULT_KEYS_BATCH_SIZE, Descriptor, Item, KeySet } from "@itwin/presentation-common"; import { createRandomECInstanceKey, createRandomTransientId, createTestContentDescriptor } from "@itwin/presentation-common/lib/cjs/test"; -import { HiliteSetProvider, Presentation, PresentationManager } from "../../presentation-frontend"; +import { HiliteSetProvider } from "../../presentation-frontend/selection/HiliteSetProvider"; import { TRANSIENT_ELEMENT_CLASSNAME } from "../../presentation-frontend/selection/SelectionManager"; +import sinon from "sinon"; +import { Presentation } from "../../presentation-frontend/Presentation"; +import { GetContentRequestOptions, MultipleValuesRequestOptions, PresentationManager } from "../../presentation-frontend"; describe("HiliteSetProvider", () => { const imodelMock = moq.Mock.ofType(); - const presentationManagerMock = moq.Mock.ofType(); + const fakeGetContentIterator = sinon.stub< + [GetContentRequestOptions & MultipleValuesRequestOptions], + Promise<{ descriptor: Descriptor; total: number; items: AsyncIterableIterator } | undefined> + >(); + + before(() => { + const managerMock = sinon.createStubInstance(PresentationManager, { + getContentIterator: fakeGetContentIterator, + }); + Presentation.setPresentationManager(managerMock); + }); beforeEach(() => { imodelMock.reset(); - presentationManagerMock.reset(); - Presentation.setPresentationManager(presentationManagerMock.object); - }); - - afterEach(() => { - Presentation.terminate(); + fakeGetContentIterator.reset(); }); describe("create", () => { @@ -44,31 +52,31 @@ describe("HiliteSetProvider", () => { const resultContent = new Content(createTestContentDescriptor({ fields: [] }), [ new Item([createRandomECInstanceKey()], "", "", undefined, {}, {}, [], {}), // element ]); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => ({ size: 1, content: resultContent })); + fakeGetContentIterator.callsFake(async () => ({ total: 1, descriptor: resultContent.descriptor, items: iterate(resultContent.contentSet) })); const keys = new KeySet([createRandomECInstanceKey()]); await provider.getHiliteSet(keys); // records are fetched for the first request - presentationManagerMock.verify(async (x) => x.getContentAndSize(moq.It.isAny()), moq.Times.once()); + expect(fakeGetContentIterator).to.be.calledOnce; await provider.getHiliteSet(keys); // keys didn't change - result returned from cache - presentationManagerMock.verify(async (x) => x.getContentAndSize(moq.It.isAny()), moq.Times.once()); + expect(fakeGetContentIterator).to.be.calledOnce; keys.add(createRandomECInstanceKey()); await provider.getHiliteSet(keys); // keys did change - result fetched again - presentationManagerMock.verify(async (x) => x.getContentAndSize(moq.It.isAny()), moq.Times.exactly(2)); + expect(fakeGetContentIterator).to.be.calledTwice; await provider.getHiliteSet(keys); // keys didn't change - result returned from cache - presentationManagerMock.verify(async (x) => x.getContentAndSize(moq.It.isAny()), moq.Times.exactly(2)); + expect(fakeGetContentIterator).to.be.calledTwice; }); it("creates result for transient element keys", async () => { const transientKey = { className: TRANSIENT_ELEMENT_CLASSNAME, id: createRandomTransientId() }; - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.is((opts) => opts.keys.isEmpty))).returns(async () => undefined); + fakeGetContentIterator.withArgs(sinon.match((opts: GetContentRequestOptions) => opts.keys.isEmpty)).resolves(undefined); const result = await provider.getHiliteSet(new KeySet([transientKey])); expect(result.models).to.be.undefined; @@ -82,8 +90,11 @@ describe("HiliteSetProvider", () => { const resultContent = new Content(createTestContentDescriptor({ fields: [] }), [ new Item([resultKey], "", "", undefined, {}, {}, [], {}), // element ]); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => ({ size: 1, content: resultContent })); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => undefined); + + fakeGetContentIterator + .onFirstCall() + .callsFake(async () => ({ total: 1, descriptor: resultContent.descriptor, items: iterate(resultContent.contentSet) })); + fakeGetContentIterator.onSecondCall().resolves(undefined); const result = await provider.getHiliteSet(new KeySet([persistentKey])); expect(result.models).to.be.undefined; @@ -95,8 +106,11 @@ describe("HiliteSetProvider", () => { const persistentKey = createRandomECInstanceKey(); const resultKey = createRandomECInstanceKey(); const resultContent = new Content(createTestContentDescriptor({ fields: [] }), [new Item([resultKey], "", "", undefined, {}, {}, [], { isModel: true })]); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => ({ size: 1, content: resultContent })); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => undefined); + + fakeGetContentIterator + .onFirstCall() + .callsFake(async () => ({ total: 1, descriptor: resultContent.descriptor, items: iterate(resultContent.contentSet) })); + fakeGetContentIterator.onSecondCall().resolves(undefined); const result = await provider.getHiliteSet(new KeySet([persistentKey])); expect(result.models).to.deep.eq([resultKey.id]); @@ -110,8 +124,10 @@ describe("HiliteSetProvider", () => { const resultContent = new Content(createTestContentDescriptor({ fields: [] }), [ new Item([resultKey], "", "", undefined, {}, {}, [], { isSubCategory: true }), ]); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => ({ size: 1, content: resultContent })); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => undefined); + fakeGetContentIterator + .onFirstCall() + .callsFake(async () => ({ total: 1, descriptor: resultContent.descriptor, items: iterate(resultContent.contentSet) })); + fakeGetContentIterator.onSecondCall().resolves(undefined); const result = await provider.getHiliteSet(new KeySet([persistentKey])); expect(result.models).to.be.undefined; @@ -131,8 +147,10 @@ describe("HiliteSetProvider", () => { new Item([resultSubCategoryKey], "", "", undefined, {}, {}, [], { isSubCategory: true }), new Item([resultElementKey], "", "", undefined, {}, {}, [], {}), // element ]); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => ({ size: 3, content: resultContent })); - presentationManagerMock.setup(async (x) => x.getContentAndSize(moq.It.isAny())).returns(async () => undefined); + fakeGetContentIterator + .onFirstCall() + .callsFake(async () => ({ total: 1, descriptor: resultContent.descriptor, items: iterate(resultContent.contentSet) })); + fakeGetContentIterator.onSecondCall().resolves(undefined); const result = await provider.getHiliteSet(new KeySet([transientKey, persistentKey])); expect(result.models).to.deep.eq([resultModelKey.id]); @@ -152,14 +170,13 @@ describe("HiliteSetProvider", () => { const resultContent1 = new Content(createTestContentDescriptor({ fields: [] }), [ new Item([elementKey], "", "", undefined, {}, {}, [], {}), // element ]); - presentationManagerMock - .setup(async (x) => x.getContentAndSize(moq.It.is((opts) => opts.keys.size === DEFAULT_KEYS_BATCH_SIZE))) - .returns(async () => ({ size: 1, content: resultContent1 })); - - // second request returns no content - presentationManagerMock - .setup(async (x) => x.getContentAndSize(moq.It.is((opts) => opts.keys.size === DEFAULT_KEYS_BATCH_SIZE))) - .returns(async () => undefined); + fakeGetContentIterator + .withArgs(sinon.match((opts: GetContentRequestOptions) => opts.keys.size === DEFAULT_KEYS_BATCH_SIZE)) + .onFirstCall() + .callsFake(async () => ({ total: 1, descriptor: resultContent1.descriptor, items: iterate(resultContent1.contentSet) })) + // second request returns no content + .onSecondCall() + .resolves(undefined); // third request returns content with subcategory and model keys const subCategoryKey = createRandomECInstanceKey(); @@ -168,9 +185,9 @@ describe("HiliteSetProvider", () => { new Item([subCategoryKey], "", "", undefined, {}, {}, [], { isSubCategory: true }), new Item([modelKey], "", "", undefined, {}, {}, [], { isModel: true }), ]); - presentationManagerMock - .setup(async (x) => x.getContentAndSize(moq.It.is((opts) => opts.keys.size === 1))) - .returns(async () => ({ size: 2, content: resultContent2 })); + fakeGetContentIterator + .withArgs(sinon.match((opts: GetContentRequestOptions) => opts.keys.size === 1)) + .callsFake(async () => ({ total: 2, descriptor: resultContent2.descriptor, items: iterate(resultContent2.contentSet) })); const result = await provider.getHiliteSet(new KeySet(inputKeys)); expect(result.models).to.deep.eq([modelKey.id]); @@ -193,12 +210,13 @@ describe("HiliteSetProvider", () => { const resultContent1 = new Content(createTestContentDescriptor({ fields: [] }), items.slice(0, 1000)); const resultContent2 = new Content(createTestContentDescriptor({ fields: [] }), items.slice(1000)); - presentationManagerMock - .setup(async (x) => x.getContentAndSize(moq.It.is((opts) => opts.paging?.start === 0))) - .returns(async () => ({ size: 1001, content: resultContent1 })); - presentationManagerMock - .setup(async (x) => x.getContentAndSize(moq.It.is((opts) => (opts.paging?.start ?? 0) > 0))) - .returns(async () => ({ size: 1001, content: resultContent2 })); + fakeGetContentIterator + .withArgs(sinon.match((opts: MultipleValuesRequestOptions) => !opts.paging?.start)) + .callsFake(async () => ({ total: 1001, descriptor: resultContent1.descriptor, items: iterate(resultContent1.contentSet) })); + + fakeGetContentIterator.withArgs(sinon.match((opts: MultipleValuesRequestOptions) => !!opts.paging?.start)).callsFake(async () => { + return { total: 1001, descriptor: resultContent2.descriptor, items: iterate(resultContent2.contentSet) }; + }); const iterator = provider.getHiliteSetIterator(new KeySet([{ id: "0x1", className: "TestElement" }])); let index = 0; @@ -214,3 +232,9 @@ describe("HiliteSetProvider", () => { }); }); }); + +async function* iterate(items: T[]): AsyncIterableIterator { + for (const item of items) { + yield item; + } +}