diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
index de85fa1042..cd92605875 100644
--- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
+++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
+import type { GetEntitiesResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
alertApiRef,
@@ -23,7 +23,10 @@ import {
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
+import { translationApiRef } from '@backstage/core-plugin-api/alpha';
+import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
+import { useMountEffect } from '@react-hookz/web';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import { PropsWithChildren } from 'react';
@@ -37,10 +40,9 @@ import {
EntityTypeFilter,
EntityUserFilter,
} from '../filters';
-import { EntityListProvider, useEntityList } from './useEntityListProvider';
-import { useMountEffect } from '@react-hookz/web';
-import { translationApiRef } from '@backstage/core-plugin-api/alpha';
+import { MockPromise } from '../testUtils/MockPromise';
import { EntityListPagination } from '../types';
+import { EntityListProvider, useEntityList } from './useEntityListProvider';
const entities: Entity[] = [
{
@@ -341,6 +343,62 @@ describe('', () => {
filter: { kind: 'group' },
});
});
+
+ it('uses the last applied filter even if an earlier request finishes later', async () => {
+ const { result } = renderHook(() => useEntityList(), {
+ wrapper: createWrapper({ pagination }),
+ });
+
+ const firstResult = new MockPromise();
+ const secondResult = new MockPromise();
+
+ await waitFor(() => {
+ expect(result.current.backendEntities.length).toBeGreaterThan(0);
+ });
+ expect(result.current.totalItems).toBe(2);
+ expect(result.current.backendEntities.length).toBe(2);
+ expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(1);
+
+ mockCatalogApi.getEntities!.mockReturnValueOnce(firstResult);
+
+ await act(async () => {
+ result.current.updateFilters({
+ kind: new EntityKindFilter('api', 'API'),
+ });
+ });
+
+ await waitFor(() => {
+ expect(mockCatalogApi.getEntities).toHaveBeenNthCalledWith(2, {
+ filter: { kind: 'api' },
+ });
+ });
+
+ mockCatalogApi.getEntities!.mockReturnValueOnce(secondResult);
+
+ await act(async () => {
+ result.current.updateFilters({
+ kind: new EntityKindFilter('system', 'System'),
+ });
+ });
+
+ await waitFor(() => {
+ expect(mockCatalogApi.getEntities).toHaveBeenNthCalledWith(3, {
+ filter: { kind: 'system' },
+ });
+ });
+
+ await act(async () => {
+ secondResult.resolve({
+ items: [],
+ });
+ firstResult.resolve({
+ items: entities,
+ });
+ });
+
+ expect(result.current.filters.kind!.value).toBe('system');
+ expect(result.current.backendEntities.length).toBe(0);
+ });
});
describe('', () => {
diff --git a/plugins/catalog-react/src/testUtils/MockPromise.test.ts b/plugins/catalog-react/src/testUtils/MockPromise.test.ts
new file mode 100644
index 0000000000..6d6789f7a2
--- /dev/null
+++ b/plugins/catalog-react/src/testUtils/MockPromise.test.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MockPromise } from './MockPromise.ts';
+
+describe('MockPromise', () => {
+ it('is resolved when its resolve function is called', async () => {
+ const p = new MockPromise();
+ p.resolve(12);
+ return expect(p).resolves.toBe(12);
+ });
+
+ it('is rejected when its reject function is called', async () => {
+ const p = new MockPromise();
+ p.reject('Test rejection');
+ return expect(p).rejects.toMatch('Test rejection');
+ });
+});
diff --git a/plugins/catalog-react/src/testUtils/MockPromise.ts b/plugins/catalog-react/src/testUtils/MockPromise.ts
new file mode 100644
index 0000000000..cc46eaafda
--- /dev/null
+++ b/plugins/catalog-react/src/testUtils/MockPromise.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+type ResolveFn = (value: T | PromiseLike) => void;
+type RejectFn = (reason?: any) => void;
+type Executor = (resolve: ResolveFn, reject: RejectFn) => void;
+
+export class MockPromise extends Promise {
+ private _resolve!: ResolveFn;
+ private _reject!: RejectFn;
+
+ constructor(executor?: Executor) {
+ if (executor !== undefined) {
+ super(executor);
+ return;
+ }
+
+ let res: ResolveFn;
+ let rej: RejectFn;
+ super((resolve, reject) => {
+ res = resolve;
+ rej = reject;
+ });
+ this._resolve = res!;
+ this._reject = rej!;
+ }
+
+ get resolve() {
+ return this._resolve;
+ }
+
+ get reject() {
+ return this._reject;
+ }
+}