diff --git a/adev/shared-docs/components/search-dialog/search-dialog.component.spec.ts b/adev/shared-docs/components/search-dialog/search-dialog.component.spec.ts index e1e18eb32af..dc6bcf5e257 100644 --- a/adev/shared-docs/components/search-dialog/search-dialog.component.spec.ts +++ b/adev/shared-docs/components/search-dialog/search-dialog.component.spec.ts @@ -14,7 +14,7 @@ import {Router, provideRouter} from '@angular/router'; import {SearchDialog} from './search-dialog.component'; import {ENVIRONMENT, WINDOW} from '../../providers'; import {ALGOLIA_CLIENT, Search} from '../../services'; -import {FakeEventTarget} from '../../testing/index'; +import {FakeEventTarget, timeout, useAutoTick} from '../../testing/index'; import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component'; import {SearchResult} from '../../interfaces'; @@ -27,8 +27,10 @@ describe('SearchDialog', () => { let search: Search; + useAutoTick(); + beforeEach(async () => { - searchResults.and.returnValue([]); + searchResults.and.returnValue(Promise.resolve({results: [{hits: []}]})); TestBed.configureTestingModule({ imports: [SearchDialog], @@ -55,6 +57,9 @@ describe('SearchDialog', () => { // Fire the request TestBed.inject(ApplicationRef).tick(); + // The delay from debounced (200ms) + await timeout(300); + // Wait for the resource to resolve await TestBed.inject(ApplicationRef).whenStable(); @@ -85,6 +90,9 @@ describe('SearchDialog', () => { // Fire the request TestBed.inject(ApplicationRef).tick(); + // The delay from debounced (200ms) + await timeout(300); + // Wait for the resource to resolve await TestBed.inject(ApplicationRef).whenStable(); @@ -96,7 +104,7 @@ describe('SearchDialog', () => { }); it('should display `Start typing to see results` message when there are no provided query', () => { - searchResults.and.returnValue(undefined); + searchResults.and.returnValue(Promise.resolve(undefined)); const startTypingContainer = fixture.debugElement.query( By.css('.docs-search-results__start-typing'), @@ -112,6 +120,9 @@ describe('SearchDialog', () => { // Fire the request TestBed.inject(ApplicationRef).tick(); + // The delay from debounced (200ms) + await timeout(300); + // Wait for the resource to resolve await TestBed.inject(ApplicationRef).whenStable(); diff --git a/adev/shared-docs/services/search.service.ts b/adev/shared-docs/services/search.service.ts index eb44dfa42f8..ca8da784493 100644 --- a/adev/shared-docs/services/search.service.ts +++ b/adev/shared-docs/services/search.service.ts @@ -10,6 +10,7 @@ import { Injectable, InjectionToken, Provider, + debounced, inject, linkedSignal, resource, @@ -48,13 +49,11 @@ export class Search { private readonly config = inject(ENVIRONMENT); private readonly client = inject(ALGOLIA_CLIENT); + debounceParams = debounced(this.searchQuery, SEARCH_DELAY); + readonly resultsResource = resource({ - params: () => this.searchQuery() || undefined, // coerces empty string to undefined - loader: async ({params: query, abortSignal}) => { - // Until we have a better alternative we debounce by awaiting for a short delay. - await wait(SEARCH_DELAY, abortSignal); - return this.searchWithQuery(query); - }, + params: () => this.debounceParams.value() || undefined, // coerces empty string to undefined + loader: async ({params}) => this.searchWithQuery(params), }); readonly searchResults = linkedSignal({ @@ -223,27 +222,6 @@ function matched(snippet: SnippetResult | undefined): boolean { return snippet?.matchLevel !== undefined && snippet.matchLevel !== 'none'; } -/** - * Temporary helper to implement the debounce functionality on the search resource - */ -function wait(ms: number, signal: AbortSignal): Promise { - return new Promise((resolve, reject) => { - let timeout: ReturnType | undefined; - - const onAbort = () => { - clearTimeout(timeout); - reject(new Error('Operation aborted')); - }; - - timeout = setTimeout(() => { - signal.removeEventListener('abort', onAbort); - resolve(); - }, ms); - - signal.addEventListener('abort', onAbort, {once: true}); - }); -} - function extractPackageNameFromUrl(url: string): string | null { const extractedSegment = url.match(/\/api\/(.*)\/.*#?/); if (extractedSegment == null) { diff --git a/adev/shared-docs/testing/BUILD.bazel b/adev/shared-docs/testing/BUILD.bazel index fc29ce309ed..410cab63786 100644 --- a/adev/shared-docs/testing/BUILD.bazel +++ b/adev/shared-docs/testing/BUILD.bazel @@ -21,8 +21,10 @@ ts_project( "index.ts", ], ), + tsconfig = "//adev/shared-docs:tsconfig_test", deps = [ "//adev:node_modules/@angular/core", + "//adev:node_modules/@types/jasmine", "//adev:node_modules/@webcontainer/api", ], ) diff --git a/adev/shared-docs/testing/testing-helper.ts b/adev/shared-docs/testing/testing-helper.ts index 819cd0aee5f..ad9b882119c 100644 --- a/adev/shared-docs/testing/testing-helper.ts +++ b/adev/shared-docs/testing/testing-helper.ts @@ -181,3 +181,46 @@ export class FakeWebContainerProcess implements WebContainerProcess { kill(): void {} resize(dimensions: {cols: number; rows: number}): void {} } + +// Copy from utils in packages/private/testing + +/** + * Returns a promise that resolves after the specified time. + * + * @param ms - Time to wait in milliseconds. Defaults to 0. + * + * @example + * ```ts + * await timeout(100); // Wait 100ms + * ``` + */ +export async function timeout(ms?: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** + * Installs Jasmine's fake clock with auto-tick enabled for all tests in the describe block. + * Call at the top level of a describe block to automatically advance time for async operations. + * + * @example + * ```ts + * describe('MyComponent', () => { + * useAutoTick(); + * + * it('should handle timers', () => { + * // setTimeout, setInterval, etc. will execute synchronously + * }); + * }); + * ``` + */ +export function useAutoTick() { + beforeEach(() => { + jasmine.clock().install(); + jasmine.clock().autoTick(); + }); + afterEach(() => { + jasmine.clock().uninstall(); + }); +}