From abad6f07778a62b14ff822f209b39c733549f85f Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sat, 9 May 2026 12:51:56 +0900 Subject: [PATCH 1/5] test(query-devtools/utils): add tests for 'getQueryStatusColor' (#10670) --- .../src/__tests__/utils.test.ts | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index c620a9e05b..038d6e1957 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -7,6 +7,7 @@ import { displayValue, getMutationStatusColor, getPreferredColorScheme, + getQueryStatusColor, getQueryStatusColorByLabel, getSidedProp, mutationSortFns, @@ -14,7 +15,12 @@ import { sortFns, updateNestedDataByPath, } from '../utils' -import type { Mutation, MutationStatus, Query } from '@tanstack/query-core' +import type { + FetchStatus, + Mutation, + MutationStatus, + Query, +} from '@tanstack/query-core' describe('Utils tests', () => { describe('updatedNestedDataByPath', () => { @@ -1292,4 +1298,94 @@ describe('Utils tests', () => { }) }) }) + + describe('getQueryStatusColor', () => { + let queryClient: QueryClient + + function makeState(fetchStatus: FetchStatus): Query['state'] { + const query = queryClient.getQueryCache().build(queryClient, { + queryKey: [fetchStatus], + }) + query.setState({ fetchStatus }) + return query.state + } + + beforeEach(() => { + queryClient = new QueryClient() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('should return "blue" when fetchStatus is "fetching"', () => { + expect( + getQueryStatusColor({ + queryState: makeState('fetching'), + observerCount: 1, + isStale: false, + }), + ).toBe('blue') + }) + + it('should return "blue" even when there are no observers if fetchStatus is "fetching"', () => { + expect( + getQueryStatusColor({ + queryState: makeState('fetching'), + observerCount: 0, + isStale: false, + }), + ).toBe('blue') + }) + + it('should return "gray" when there are no observers', () => { + expect( + getQueryStatusColor({ + queryState: makeState('idle'), + observerCount: 0, + isStale: false, + }), + ).toBe('gray') + }) + + it('should return "purple" when fetchStatus is "paused"', () => { + expect( + getQueryStatusColor({ + queryState: makeState('paused'), + observerCount: 1, + isStale: false, + }), + ).toBe('purple') + }) + + it('should return "purple" even when stale if fetchStatus is "paused"', () => { + expect( + getQueryStatusColor({ + queryState: makeState('paused'), + observerCount: 1, + isStale: true, + }), + ).toBe('purple') + }) + + it('should return "yellow" when query is stale', () => { + expect( + getQueryStatusColor({ + queryState: makeState('idle'), + observerCount: 1, + isStale: true, + }), + ).toBe('yellow') + }) + + it('should return "green" when query is active and not stale', () => { + expect( + getQueryStatusColor({ + queryState: makeState('idle'), + observerCount: 1, + isStale: false, + }), + ).toBe('green') + }) + }) }) From 64d0b89894a51937270aa54772ae5debd268fda1 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sat, 9 May 2026 13:23:49 +0900 Subject: [PATCH 2/5] test(query-devtools/utils): add tests for 'getQueryStatusLabel' (#10671) --- .../src/__tests__/utils.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 038d6e1957..af56e4cf64 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -9,6 +9,7 @@ import { getPreferredColorScheme, getQueryStatusColor, getQueryStatusColorByLabel, + getQueryStatusLabel, getSidedProp, mutationSortFns, setupStyleSheet, @@ -20,6 +21,7 @@ import type { Mutation, MutationStatus, Query, + QueryKey, } from '@tanstack/query-core' describe('Utils tests', () => { @@ -1299,6 +1301,121 @@ describe('Utils tests', () => { }) }) + describe('getQueryStatusLabel', () => { + let queryClient: QueryClient + + function buildQuery( + queryKey: QueryKey, + state?: Partial, + ): Query { + const query = queryClient.getQueryCache().build(queryClient, { queryKey }) + if (state) { + query.setState(state) + } + return query + } + + function addObserver(query: Query) { + const observer = new QueryObserver(queryClient, { + queryKey: query.queryKey, + enabled: false, + }) + return observer.subscribe(() => {}) + } + + beforeEach(() => { + queryClient = new QueryClient() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('should return "fetching" when fetchStatus is "fetching"', () => { + const query = buildQuery(['q'], { fetchStatus: 'fetching' }) + + expect(getQueryStatusLabel(query)).toBe('fetching') + }) + + it('should return "inactive" when there are no observers', () => { + const query = buildQuery(['q'], { fetchStatus: 'idle' }) + + expect(getQueryStatusLabel(query)).toBe('inactive') + }) + + it('should return "paused" when fetchStatus is "paused" and there are observers', () => { + const query = buildQuery(['q'], { fetchStatus: 'paused' }) + const unsubscribe = addObserver(query) + + try { + expect(getQueryStatusLabel(query)).toBe('paused') + } finally { + unsubscribe() + } + }) + + it('should return "paused" even when stale if fetchStatus is "paused"', () => { + const observer = new QueryObserver(queryClient, { + queryKey: ['paused-stale'], + staleTime: 0, + }) + const unsubscribe = observer.subscribe(() => {}) + const query = queryClient.getQueryCache().find({ + queryKey: ['paused-stale'], + })! + query.setState({ + ...query.state, + fetchStatus: 'paused', + data: 'data', + dataUpdatedAt: 0, + }) + + try { + expect(query.isStale()).toBe(true) + expect(getQueryStatusLabel(query)).toBe('paused') + } finally { + unsubscribe() + } + }) + + it('should return "stale" when query is idle, has observers, and is stale', () => { + const observer = new QueryObserver(queryClient, { + queryKey: ['stale'], + staleTime: 0, + }) + const unsubscribe = observer.subscribe(() => {}) + const query = queryClient.getQueryCache().find({ queryKey: ['stale'] })! + query.setState({ + ...query.state, + fetchStatus: 'idle', + data: 'data', + dataUpdatedAt: 0, + }) + + try { + expect(query.isStale()).toBe(true) + expect(getQueryStatusLabel(query)).toBe('stale') + } finally { + unsubscribe() + } + }) + + it('should return "fresh" when query is idle, has observers, and is not stale', () => { + const query = buildQuery(['q'], { + fetchStatus: 'idle', + data: 'fresh-data', + dataUpdatedAt: Date.now(), + }) + const unsubscribe = addObserver(query) + + try { + expect(getQueryStatusLabel(query)).toBe('fresh') + } finally { + unsubscribe() + } + }) + }) + describe('getQueryStatusColor', () => { let queryClient: QueryClient From 8dd9724bf24dc9c5abd46a8a46c308e8fb59f3df Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sat, 9 May 2026 14:08:48 +0900 Subject: [PATCH 3/5] test(query-devtools/utils): cover empty path, array nested path, and primitive cases of 'updateNestedDataByPath' (#10672) --- .../src/__tests__/utils.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index af56e4cf64..0c1fb6ec50 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -135,6 +135,51 @@ describe('Utils tests', () => { /* eslint-enable */ }) + describe('empty path', () => { + it('should return the new value as-is when "updatePath" is empty', () => { + const oldData = { title: 'Hello world' } + + expect(updateNestedDataByPath(oldData, [], 'replaced')).toBe('replaced') + }) + }) + + describe('array nested path', () => { + it('should update nested data inside an array correctly', () => { + const oldData = [ + { id: 1, title: 'first' }, + { id: 2, title: 'second' }, + ] + + const newData = updateNestedDataByPath( + oldData, + ['1', 'title'], + 'updated', + ) + + expect(newData).not.toBe(oldData) + expect(newData).toMatchInlineSnapshot(` + [ + { + "id": 1, + "title": "first", + }, + { + "id": 2, + "title": "updated", + }, + ] + `) + }) + }) + + describe('primitive', () => { + it('should return primitive data as-is when it is not an Object/Array/Map/Set', () => { + expect(updateNestedDataByPath(42, ['x'], 'new')).toBe(42) + expect(updateNestedDataByPath('hello', ['x'], 'new')).toBe('hello') + expect(updateNestedDataByPath(null, ['x'], 'new')).toBe(null) + }) + }) + describe('nested data', () => { it('should update data correctly', () => { /* eslint-disable cspell/spellchecker */ From a709b04e4ada050ba23080424a4e48cf7bce101e Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sat, 9 May 2026 14:26:11 +0900 Subject: [PATCH 4/5] test(query-devtools/utils): add tests for 'array nested path' and 'primitive' cases of 'deleteNestedDataByPath' (#10673) --- .../src/__tests__/utils.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 0c1fb6ec50..26ae5a93b6 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -539,6 +539,38 @@ describe('Utils tests', () => { `) }) + describe('array nested path', () => { + it('should delete nested data inside an array correctly', () => { + const oldData = [ + { id: 1, title: 'first' }, + { id: 2, title: 'second' }, + ] + + const newData = deleteNestedDataByPath(oldData, ['1', 'title']) + + expect(newData).not.toBe(oldData) + expect(newData).toMatchInlineSnapshot(` + [ + { + "id": 1, + "title": "first", + }, + { + "id": 2, + }, + ] + `) + }) + }) + + describe('primitive', () => { + it('should return primitive data as-is when it is not an Object/Array/Map/Set', () => { + expect(deleteNestedDataByPath(42, ['x'])).toBe(42) + expect(deleteNestedDataByPath('hello', ['x'])).toBe('hello') + expect(deleteNestedDataByPath(null, ['x'])).toBe(null) + }) + }) + describe('nested data', () => { it('should delete nested items correctly', () => { /* eslint-disable cspell/spellchecker */ From a37c003ca1ecd889e23d534dc120a4eee5db690d Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sat, 9 May 2026 16:38:20 +0900 Subject: [PATCH 5/5] test(query-devtools/utils): add test for stale vs fresh rank in 'status' sort of 'sortFns' (#10674) * test(query-devtools/utils): add test for stale vs fresh rank in 'status' sort of 'sortFns' * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../src/__tests__/utils.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 26ae5a93b6..07dff9ece4 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -1146,6 +1146,37 @@ describe('Utils tests', () => { } }) + it('should place a stale query after a fresh one when both are idle and have observers', () => { + const staleObserver = new QueryObserver(queryClient, { + queryKey: ['stale'], + staleTime: 0, + }) + const staleUnsubscribe = staleObserver.subscribe(() => {}) + const stale = queryClient.getQueryCache().find({ queryKey: ['stale'] })! + stale.setState({ + ...stale.state, + fetchStatus: 'idle', + data: 'data', + dataUpdatedAt: 200, + }) + + const fresh = buildQuery(['fresh'], { + fetchStatus: 'idle', + data: 'fresh-data', + dataUpdatedAt: 100, + }) + const freshUnsubscribe = addObserver(fresh) + + try { + expect(stale.isStale()).toBe(true) + expect(statusSort(fresh, stale)).toBe(-1) + expect(statusSort(stale, fresh)).toBe(1) + } finally { + staleUnsubscribe() + freshUnsubscribe() + } + }) + it('should fall back to "last updated" sort within the same status rank', () => { const older = buildQuery(['older'], { dataUpdatedAt: 100 }) const newer = buildQuery(['newer'], { dataUpdatedAt: 200 })