From ac2b33fed32d642527b24031bf041a5b5eb8d9d7 Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:58:06 +0100 Subject: [PATCH] fix: ensure panel stays closed with many pending requests --- .../src/__tests__/concurrency.test.ts | 117 +++++++++--------- packages/autocomplete-core/src/onInput.ts | 4 +- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/packages/autocomplete-core/src/__tests__/concurrency.test.ts b/packages/autocomplete-core/src/__tests__/concurrency.test.ts index 702a6c2c5..8284887ea 100644 --- a/packages/autocomplete-core/src/__tests__/concurrency.test.ts +++ b/packages/autocomplete-core/src/__tests__/concurrency.test.ts @@ -14,30 +14,12 @@ beforeEach(() => { describe('concurrency', () => { test('resolves the responses in order from getSources', async () => { - // These delays make the second query come back after the third one. - const sourcesDelays = [100, 150, 200]; - const itemsDelays = [0, 150, 0]; - let deferSourcesCount = -1; - let deferItemsCount = -1; - - const getSources = ({ query }) => { - deferSourcesCount++; - - return defer(() => { - return [ - createSource({ - getItems() { - deferItemsCount++; - - return defer( - () => [{ label: query }], - itemsDelays[deferItemsCount] - ); - }, - }), - ]; - }, sourcesDelays[deferSourcesCount]); - }; + const { timeout, delayedGetSources: getSources } = createDelayedGetSources({ + // These delays make the second query come back after the third one. + sources: [100, 150, 200], + items: [0, 150, 0], + }); + const onStateChange = jest.fn(); const autocomplete = createAutocomplete({ getSources, onStateChange }); const { onChange } = autocomplete.getInputProps({ inputElement: null }); @@ -49,10 +31,6 @@ describe('concurrency', () => { userEvent.type(input, 'b'); userEvent.type(input, 'c'); - const timeout = Math.max( - ...sourcesDelays.map((delay, index) => delay + itemsDelays[index]) - ); - await defer(() => {}, timeout); let stateHistory: Array< @@ -98,27 +76,21 @@ describe('concurrency', () => { describe('closing the panel with pending requests', () => { describe('without debug mode', () => { - const delay = 300; - test('keeps the panel closed on Escape', async () => { const onStateChange = jest.fn(); - const getSources = jest.fn(() => { - return defer(() => { - return [ - createSource({ - getItems: () => [{ label: '1' }, { label: '2' }], - }), - ]; - }, delay); + const { timeout, delayedGetSources } = createDelayedGetSources({ + sources: [100, 200], }); + const getSources = jest.fn(delayedGetSources); + const { inputElement } = createPlayground(createAutocomplete, { onStateChange, getSources, }); - userEvent.type(inputElement, 'a{esc}'); + userEvent.type(inputElement, 'ab{esc}'); - await defer(() => {}, delay); + await defer(() => {}, timeout); expect(onStateChange).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -128,20 +100,16 @@ describe('concurrency', () => { }), }) ); - expect(getSources).toHaveBeenCalledTimes(1); + expect(getSources).toHaveBeenCalledTimes(2); }); test('keeps the panel closed on blur', async () => { const onStateChange = jest.fn(); - const getSources = jest.fn(() => { - return defer(() => { - return [ - createSource({ - getItems: () => [{ label: '1' }, { label: '2' }], - }), - ]; - }, delay); + const { timeout, delayedGetSources } = createDelayedGetSources({ + sources: [100, 200], }); + const getSources = jest.fn(delayedGetSources); + const { inputElement } = createPlayground(createAutocomplete, { onStateChange, getSources, @@ -149,7 +117,7 @@ describe('concurrency', () => { userEvent.type(inputElement, 'a{enter}'); - await defer(() => {}, delay); + await defer(() => {}, timeout); expect(onStateChange).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -164,15 +132,11 @@ describe('concurrency', () => { test('keeps the panel closed on touchstart blur', async () => { const onStateChange = jest.fn(); - const getSources = jest.fn(() => { - return defer(() => { - return [ - createSource({ - getItems: () => [{ label: '1' }, { label: '2' }], - }), - ]; - }, delay); + const { timeout, delayedGetSources } = createDelayedGetSources({ + sources: [100, 200], }); + const getSources = jest.fn(delayedGetSources); + const { getEnvironmentProps, inputElement, @@ -195,7 +159,7 @@ describe('concurrency', () => { const customEvent = new CustomEvent('touchstart', { bubbles: true }); window.document.dispatchEvent(customEvent); - await defer(() => {}, delay); + await defer(() => {}, timeout); expect(onStateChange).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -329,3 +293,38 @@ describe('concurrency', () => { }); }); }); + +function createDelayedGetSources(delays: { + sources: number[]; + items?: number[]; +}) { + let deferSourcesCount = -1; + let deferItemsCount = -1; + + const itemsDelays = delays.items || delays.sources.map(() => 0); + + const timeout = Math.max( + ...delays.sources.map((delay, index) => delay + itemsDelays[index]) + ); + + function delayedGetSources({ query }) { + deferSourcesCount++; + + return defer(() => { + return [ + createSource({ + getItems() { + deferItemsCount++; + + return defer( + () => [{ label: query }], + itemsDelays[deferItemsCount] + ); + }, + }), + ]; + }, delays.sources[deferSourcesCount]); + } + + return { timeout, delayedGetSources }; +} diff --git a/packages/autocomplete-core/src/onInput.ts b/packages/autocomplete-core/src/onInput.ts index 6c8c43af9..ba69409a6 100644 --- a/packages/autocomplete-core/src/onInput.ts +++ b/packages/autocomplete-core/src/onInput.ts @@ -123,7 +123,9 @@ export function onInput({ setStatus('idle'); if (store.shouldSkipPendingUpdate) { - store.shouldSkipPendingUpdate = false; + if (!runConcurrentSafePromise.isRunning()) { + store.shouldSkipPendingUpdate = false; + } return; }