diff --git a/assets/js/autocomplete/__tests__/context.ts b/assets/js/autocomplete/__tests__/context.ts index fabfab23..89e949eb 100644 --- a/assets/js/autocomplete/__tests__/context.ts +++ b/assets/js/autocomplete/__tests__/context.ts @@ -6,7 +6,7 @@ import { fireEvent } from '@testing-library/dom'; import { assertNotNull } from '../../utils/assert'; import { TextInputElement } from '../input'; import store from '../../utils/store'; -import { GetTagSuggestionsResponse } from 'autocomplete/client'; +import { GetTagSuggestionsResponse, TagSuggestion } from 'autocomplete/client'; /** * A reusable test environment for autocompletion tests. Note that it does no @@ -44,27 +44,30 @@ export class TestContext { } const url = new URL(request.url); - if (url.searchParams.get('term')?.toLowerCase() !== 'mar') { - const suggestions: GetTagSuggestionsResponse = { suggestions: [] }; - return JSON.stringify(suggestions); - } + const term = url.searchParams.get('term'); + + const termLower = assertNotNull(term).toLowerCase(); + + const fakeSuggestions: TagSuggestion[] = [ + { + alias: 'marvelous', + canonical: 'beautiful', + images: 30, + }, + { + canonical: 'mare', + images: 20, + }, + { + canonical: 'market', + images: 10, + }, + ]; const suggestions: GetTagSuggestionsResponse = { - suggestions: [ - { - alias: 'marvelous', - canonical: 'beautiful', - images: 30, - }, - { - canonical: 'mare', - images: 20, - }, - { - canonical: 'market', - images: 10, - }, - ], + suggestions: fakeSuggestions.filter(suggestion => + (suggestion.alias || suggestion.canonical).startsWith(termLower), + ), }; return JSON.stringify(suggestions); diff --git a/assets/js/autocomplete/__tests__/server-side-completions-aborting.spec.ts b/assets/js/autocomplete/__tests__/server-side-completions-aborting.spec.ts deleted file mode 100644 index 4bf8617e..00000000 --- a/assets/js/autocomplete/__tests__/server-side-completions-aborting.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { init } from './context'; - -it('ignores the autocompletion results if Escape was pressed', async () => { - const ctx = await init(); - - await Promise.all([ctx.setInput('mar'), ctx.keyDown('Escape')]); - - // The input must be empty because the user typed `mar` and pressed `Escape` right after that - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "mar<>", - "suggestions": [], - } - `); - - // First request for the local autocomplete index. - expect(fetch).toHaveBeenCalledTimes(1); - - await ctx.setInput('mar'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "mar<>", - "suggestions": [ - "marvelous → beautiful 30", - "mare 20", - "market 10", - ], - } - `); - - // Second request for the server-side suggestions. - expect(fetch).toHaveBeenCalledTimes(2); -}); diff --git a/assets/js/autocomplete/__tests__/server-side-completions/debouncing.spec.ts b/assets/js/autocomplete/__tests__/server-side-completions/debouncing.spec.ts new file mode 100644 index 00000000..f3bd94a4 --- /dev/null +++ b/assets/js/autocomplete/__tests__/server-side-completions/debouncing.spec.ts @@ -0,0 +1,71 @@ +import { init } from '../context'; + +it('ignores the autocompletion results if Escape was pressed', async () => { + const ctx = await init(); + + // First request for the local autocomplete index was done + expect(fetch).toHaveBeenCalledTimes(1); + + await Promise.all([ctx.setInput('mar'), ctx.keyDown('Escape')]); + + // The input must be empty because the user typed `mar` and pressed `Escape` right after that + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "mar<>", + "suggestions": [], + } + `); + + // No new requests must've been sent because the input was debounced early + expect(fetch).toHaveBeenCalledTimes(1); + + await ctx.setInput('mar'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "mar<>", + "suggestions": [ + "marvelous → beautiful 30", + "mare 20", + "market 10", + ], + } + `); + + // Second request for the server-side suggestions. + expect(fetch).toHaveBeenCalledTimes(2); + + ctx.setInput('mare'); + + // After 300 milliseconds the debounce threshold is over, and the server-side + // completions request is issued. + vi.advanceTimersByTime(300); + + await ctx.keyDown('Escape'); + + expect(fetch).toHaveBeenCalledTimes(3); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "mare<>", + "suggestions": [], + } + `); + + ctx.setInput('mare'); + + // Make sure that the user gets the results immediately without any debouncing (0 ms) + await vi.advanceTimersByTimeAsync(0); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "mare<>", + "suggestions": [ + "mare 20", + ], + } + `); + + // The results must come from the cache, so no new fetch calls must have been made + expect(fetch).toHaveBeenCalledTimes(3); +}); diff --git a/assets/js/autocomplete/__tests__/server-side-completions-smoke.spec.ts b/assets/js/autocomplete/__tests__/server-side-completions/smoke.spec.ts similarity index 96% rename from assets/js/autocomplete/__tests__/server-side-completions-smoke.spec.ts rename to assets/js/autocomplete/__tests__/server-side-completions/smoke.spec.ts index fcf53eb7..e487b281 100644 --- a/assets/js/autocomplete/__tests__/server-side-completions-smoke.spec.ts +++ b/assets/js/autocomplete/__tests__/server-side-completions/smoke.spec.ts @@ -1,4 +1,4 @@ -import { init } from './context'; +import { init } from '../context'; it('requests server-side autocomplete if local autocomplete returns no results', async () => { const ctx = await init();