diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 7bcecbeac8e..c90ffb0dece 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -194,14 +194,15 @@ export class ExtHostTesting implements ExtHostTestingShape { const collection = disposable.add(this.ownedTests.createForHierarchy( diff => this.proxy.$publishDiff(resource, uriComponents, diff))); disposable.add(toDisposable(() => cancellation.dispose(true))); + const subscribes: Promise[] = []; for (const [id, controller] of this.controllers) { - subscribeFn(id, controller.instance); + subscribes.push(subscribeFn(id, controller.instance)); } - // note: we don't increment the root count initially -- this is done by the + // note: we don't increment the count initially -- this is done by the // main thread, incrementing once per extension host. We just push the // diff to signal that roots have been discovered. - collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]); + Promise.all(subscribes).then(() => collection.pushDiff([TestDiffOpType.IncrementPendingExtHosts, -1])); this.testControllers.set(subscriptionKey, { store: disposable, collection, subscribeFn }); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index a1c6cdb761e..df14612813d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1703,11 +1703,11 @@ export namespace TestItem { extId: item.id, label: item.label, uri: item.uri, - range: Range.from(item.range), + range: Range.from(item.range) || null, debuggable: item.debuggable ?? false, - description: item.description, + description: item.description || null, runnable: item.runnable ?? true, - error: item.error ? MarkdownString.fromStrict(item.error) : undefined, + error: item.error ? (MarkdownString.fromStrict(item.error) || null) : null, }; } @@ -1716,10 +1716,10 @@ export namespace TestItem { extId: item.id, label: item.label, uri: item.uri, - range: Range.from(item.range), + range: Range.from(item.range) || null, debuggable: false, - description: item.description, - error: undefined, + description: item.description || null, + error: null, runnable: true, }; } @@ -1729,22 +1729,22 @@ export namespace TestItem { id: item.extId, label: item.label, uri: URI.revive(item.uri), - range: Range.to(item.range), + range: Range.to(item.range || undefined), addChild: () => undefined, dispose: () => undefined, status: types.TestItemStatus.Pending, data: undefined as never, debuggable: item.debuggable, - description: item.description, + description: item.description || undefined, runnable: item.runnable, }; } export function to(item: ITestItem): types.TestItemImpl { const testItem = new types.TestItemImpl(item.extId, item.label, URI.revive(item.uri), undefined); - testItem.range = Range.to(item.range); + testItem.range = Range.to(item.range || undefined); testItem.debuggable = item.debuggable; - testItem.description = item.description; + testItem.description = item.description || undefined; testItem.runnable = item.runnable; return testItem; } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 87b8e8331c7..b2a9a48ae80 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { TestItemTreeElement, ITestTreeProjection, IActionableTestTreeElement, TestExplorerTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { TestItemTreeElement, ITestTreeProjection, IActionableTestTreeElement, TestExplorerTreeElement, TestTreeErrorMessage, isActionableTestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { ByLocationTestItemElement, ByLocationFolderElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; @@ -26,7 +26,7 @@ const computedStateAccessor: IComputedStateAccessor getOwnState: i => i instanceof TestItemTreeElement ? i.ownState : TestResultState.Unset, getCurrentComputedState: i => i.state, setComputedState: (i, s) => i.state = s, - getChildren: i => i.children.values(), + getChildren: i => Iterable.filter(i.children.values(), isActionableTestTreeElement), *getParents(i) { for (let parent = i.parent; parent; parent = parent.parent) { yield parent; @@ -195,10 +195,12 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes this.changes.addedOrRemoved(toRemove); - const queue: Iterable[] = [[toRemove]]; + const queue: Iterable[] = [[toRemove]]; while (queue.length) { for (const item of queue.pop()!) { - queue.push(this.unstoreItem(items, item)); + if (item instanceof ByLocationTestItemElement) { + queue.push(this.unstoreItem(items, item)); + } } } } @@ -237,7 +239,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes protected createItem(item: InternalTestItem, folder: IWorkspaceFolder): ByLocationTestItemElement { const { items, root } = this.getOrCreateFolderElement(folder); const parent = item.parent ? items.get(item.parent)! : root; - return new ByLocationTestItemElement(item, parent); + return new ByLocationTestItemElement(item, parent, n => this.changes.addedOrRemoved(n)); } protected getOrCreateFolderElement(folder: IWorkspaceFolder) { @@ -271,10 +273,14 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return NodeRenderDirective.Omit; } + if (!(node instanceof ByLocationTestItemElement)) { + return { element: node, children: recurse(node.children) }; + } + return { element: node, - collapsible: node instanceof ByLocationTestItemElement && node.test.expand !== TestItemExpandState.NotExpandable, - collapsed: node instanceof ByLocationTestItemElement && node.test.expand === TestItemExpandState.Expandable ? true : undefined, + collapsible: node.test.expand !== TestItemExpandState.NotExpandable, + collapsed: node.test.expand === TestItemExpandState.Expandable ? true : undefined, children: recurse(node.children), }; }; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index f43b89cbf6d..1d84570af11 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -5,6 +5,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { TestExplorerTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { ByLocationTestItemElement, ByLocationFolderElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; @@ -35,7 +36,7 @@ export class ByNameTestItemElement extends ByLocationTestItemElement { public readonly actualChildren = new Set(); public override get description() { - let description: string | undefined; + let description: string | null = null; for (let parent = this.actualParent; parent && !parent.isTestRoot; parent = parent.actualParent) { description = description ? `${parent.label} › ${description}` : parent.label; } @@ -49,10 +50,10 @@ export class ByNameTestItemElement extends ByLocationTestItemElement { constructor( internal: InternalTestItem, parentItem: ByLocationFolderElement | ByLocationTestItemElement, - private readonly addedOrRemoved: (n: ByNameTestItemElement) => void, + addedOrRemoved: (n: TestExplorerTreeElement) => void, private readonly actualParent?: ByNameTestItemElement, ) { - super(internal, parentItem); + super(internal, parentItem, addedOrRemoved); actualParent?.addChild(this); this.updateLeafTestState(); } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 48a926bf0a8..3d7df816a64 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -3,19 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestItemTreeElement, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testCollection'; /** * Test tree element element that groups be hierarchy. */ export class ByLocationTestItemElement extends TestItemTreeElement { - constructor(test: InternalTestItem, public readonly parent: ByLocationFolderElement | ByLocationTestItemElement) { + private errorChild?: TestTreeErrorMessage; + + constructor( + test: InternalTestItem, + public readonly parent: ByLocationFolderElement | ByLocationTestItemElement, + protected readonly addedOrRemoved: (n: TestExplorerTreeElement) => void, + ) { super({ ...test, item: { ...test.item } }, parent); + this.updateErrorVisiblity(); } public update(patch: ITestItemUpdate) { applyTestItemUpdate(this.test, patch); + this.updateErrorVisiblity(); + } + + private updateErrorVisiblity() { + if (this.errorChild && !this.test.item.error) { + this.addedOrRemoved(this.errorChild); + this.children.delete(this.errorChild); + this.errorChild = undefined; + } else if (this.test.item.error && !this.errorChild) { + this.errorChild = new TestTreeErrorMessage(this.test.item.error, this); + this.children.add(this.errorChild); + this.addedOrRemoved(this.errorChild); + } } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index c519dda1606..93ef737320c 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -6,6 +6,7 @@ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -71,7 +72,7 @@ export interface IActionableTestTreeElement { /** * Test children of this item. */ - children: Set; + children: Set; /** * Depth of the element in the tree. @@ -163,7 +164,7 @@ export class TestItemTreeElement implements IActionableTestTreeElement { /** * @inheritdoc */ - public readonly children = new Set(); + public readonly children = new Set(); /** * @inheritdoc @@ -234,8 +235,19 @@ export class TestItemTreeElement implements IActionableTestTreeElement { export class TestTreeErrorMessage { public readonly treeId = getId(); + public readonly children = new Set(); - constructor(public readonly message: string) { } + public get description() { + return typeof this.message === 'string' ? this.message : this.message.value; + } + + constructor( + public readonly message: string | IMarkdownString, + public readonly parent: TestExplorerTreeElement, + ) { } } +export const isActionableTestTreeElement = (t: unknown): t is (TestItemTreeElement | TestTreeWorkspaceFolder) => + t instanceof TestItemTreeElement || t instanceof TestTreeWorkspaceFolder; + export type TestExplorerTreeElement = TestItemTreeElement | TestTreeWorkspaceFolder | TestTreeErrorMessage; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts index e8aad6310a4..fe6ff452564 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts @@ -73,11 +73,11 @@ export class NodeChangeList(); private isFirstApply = true; - public updated(node: T) { + public updated(node: TestExplorerTreeElement) { this.updatedNodes.add(node); } - public addedOrRemoved(node: T) { + public addedOrRemoved(node: TestExplorerTreeElement) { this.changedParents.add(this.getNearestNotOmittedParent(node)); } @@ -116,9 +116,9 @@ export class NodeChangeList { const pane = await editorService.openEditor({ resource: test.uri, options: { - selection: test.range && { startColumn: test.range.startColumn, startLineNumber: test.range.startLineNumber }, + selection: test.range + ? { startColumn: test.range.startColumn, startLineNumber: test.range.startLineNumber } + : undefined, preserveFocus, }, }); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index ca6407b9f0e..65def80da64 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -22,6 +22,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/testing'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -45,9 +46,9 @@ import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLab import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; +import { IActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { testingHiddenIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; @@ -300,6 +301,7 @@ export class TestingExplorerViewModel extends Disposable { [ instantiationService.createInstance(TestItemRenderer, labels), instantiationService.createInstance(WorkspaceFolderRenderer, labels), + instantiationService.createInstance(ErrorRenderer), ], { simpleKeyboardNavigation: true, @@ -737,7 +739,9 @@ class ListAccessibilityProvider implements IListAccessibilityProvider { return WorkspaceFolderRenderer.ID; } + if (element instanceof TestTreeErrorMessage) { + return ErrorRenderer.ID; + } + return TestItemRenderer.ID; } } @@ -767,6 +775,43 @@ class IdentityProvider implements IIdentityProvider { } } +interface IErrorTemplateData { + label: HTMLElement; +} + +class ErrorRenderer implements ITreeRenderer { + static readonly ID = 'error'; + + private readonly renderer: MarkdownRenderer; + + constructor(@IInstantiationService instantionService: IInstantiationService) { + this.renderer = instantionService.createInstance(MarkdownRenderer, {}); + } + + get templateId(): string { + return ErrorRenderer.ID; + } + + renderTemplate(container: HTMLElement): IErrorTemplateData { + const label = dom.append(container, dom.$('.error')); + return { label }; + } + + renderElement({ element }: ITreeNode, _: number, data: IErrorTemplateData): void { + if (typeof element.message === 'string') { + data.label.innerText = element.message; + } else { + const result = this.renderer.render(element.message, { inline: true }); + data.label.appendChild(result.element); + } + + data.label.title = element.description; + } + + disposeTemplate(): void { + // noop + } +} interface IActionableElementTemplateData { label: IResourceLabel; @@ -888,7 +933,7 @@ class TestItemRenderer extends ActionableItemTemplateData { label.resource = node.element.test.item.uri; options.title = getLabelForTestTreeElement(node.element); options.fileKind = FileKind.FILE; - label.description = node.element.description; + label.description = node.element.description || undefined; data.label.setResource(label, options); } } diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts index 10122ef9916..e7400f3f700 100644 --- a/src/vs/workbench/contrib/testing/common/getComputedState.ts +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -13,8 +13,8 @@ export interface IComputedStateAccessor { getOwnState(item: T): TestResultState | undefined; getCurrentComputedState(item: T): TestResultState; setComputedState(item: T, state: TestResultState): void; - getChildren(item: T): IterableIterator; - getParents(item: T): IterableIterator; + getChildren(item: T): Iterable; + getParents(item: T): Iterable; } /** diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 0e5cd72e71b..89795b576fe 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -325,8 +325,11 @@ export class SingleUseTestCollection implements IDisposable { case 'range': this.pushDiff([TestDiffOpType.Update, { extId, item: { range: Convert.Range.from(value) }, }]); break; + case 'error': + this.pushDiff([TestDiffOpType.Update, { extId, item: { error: Convert.MarkdownString.fromStrict(value) || null }, }]); + break; default: - this.pushDiff([TestDiffOpType.Update, { extId, item: { [key]: value } }]); + this.pushDiff([TestDiffOpType.Update, { extId, item: { [key]: value ?? null } }]); break; } break; diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 814e3243557..e84ddf09084 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -90,9 +90,9 @@ export interface ITestItem { label: string; children?: never; uri: URI; - range: IRange | undefined; - description: string | undefined; - error: string | IMarkdownString | undefined; + range: IRange | null; + description: string | null; + error: string | IMarkdownString | null; runnable: boolean; debuggable: boolean; } @@ -128,7 +128,7 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate internal.expand = patch.expand; } if (patch.item !== undefined) { - Object.assign(internal.item, patch.item); + internal.item = internal.item ? Object.assign(internal.item, patch.item) : patch.item; } }; @@ -179,7 +179,7 @@ export const enum TestDiffOpType { /** Removes a test (and all its children) */ Remove, /** Changes the number of controllers who are yet to publish their collection roots. */ - DeltaRootsComplete, + IncrementPendingExtHosts, /** Retires a test/result */ Retire, } @@ -189,7 +189,7 @@ export type TestsDiffOp = | [op: TestDiffOpType.Update, item: ITestItemUpdate] | [op: TestDiffOpType.Remove, itemId: string] | [op: TestDiffOpType.Retire, itemId: string] - | [op: TestDiffOpType.DeltaRootsComplete, amount: number]; + | [op: TestDiffOpType.IncrementPendingExtHosts, amount: number]; /** * Utility function to get a unique string for a subscription to a resource, @@ -287,7 +287,7 @@ export abstract class AbstractIncrementalTestCollection i.ownComputedState, getCurrentComputedState: i => i.computedState, setComputedState: (i, s) => i.computedState = s, - getChildren: i => i.children[Symbol.iterator](), + getChildren: i => i.children, getParents: i => { const { testById: testByExtId } = this; return (function* () { diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 0cd4a5515df..afb4d17de34 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -453,7 +453,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * @inheritdoc */ public getReviverDiff() { - const ops: TestsDiff = [[TestDiffOpType.DeltaRootsComplete, this.pendingRootCount]]; + const ops: TestsDiff = [[TestDiffOpType.IncrementPendingExtHosts, this.pendingRootCount]]; const queue = [this.roots]; while (queue.length) { diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 3e82d3ec60a..43587ef035d 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -100,7 +100,7 @@ export class TestTreeTestHarness 'label' in t ? t.label : t.message)); + this.tree = this._register(new TestObjectTree(t => 'label' in t ? t.label : t.message.toString())); this._register(this.tree.onDidChangeCollapseState(evt => { if (evt.node.element instanceof TestItemTreeElement) { this.projection.expandElement(evt.node.element, evt.deep ? Infinity : 0); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index 1d2d205854d..074c4cf3cff 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -75,7 +75,7 @@ suite('Workbench - Test Result Storage', () => { test('limits stored result by budget', async () => { const r = range(100).map(() => makeResult('a'.repeat(2048))); await storage.persist(r); - await assertStored(r.slice(0, 46)); + await assertStored(r.slice(0, 44)); }); test('always stores the min number of results', async () => {