diff --git a/packages/ui-components/src/__tests__/OrderDetail.test.svelte b/packages/ui-components/src/__tests__/OrderDetail.test.svelte index db5b7d727..98a5b6426 100644 --- a/packages/ui-components/src/__tests__/OrderDetail.test.svelte +++ b/packages/ui-components/src/__tests__/OrderDetail.test.svelte @@ -9,8 +9,13 @@ import type { Readable } from 'svelte/store'; import { Button } from 'flowbite-svelte'; import DepositOrWithdrawButtons from '../lib/components/detail/DepositOrWithdrawButtons.svelte'; + import Refresh from '$lib/components/icon/Refresh.svelte'; + import { useQueryClient } from '@tanstack/svelte-query'; import type { OrderRemoveModalProps } from '../lib/types/modal'; import type { Hex } from 'viem'; + import { invalidateIdQuery } from '$lib/queries/queryClient'; + + const queryClient = useQueryClient(); export let walletAddressMatchesOrBlank: Readable<(address: string) => boolean> | undefined = undefined; @@ -51,6 +56,11 @@ Remove {/if} + + await invalidateIdQuery(queryClient, id)} + spin={$orderDetailQuery.isLoading || $orderDetailQuery.isFetching} + /> diff --git a/packages/ui-components/src/__tests__/OrderDetail.test.ts b/packages/ui-components/src/__tests__/OrderDetail.test.ts index b7dad943a..70dd7459a 100644 --- a/packages/ui-components/src/__tests__/OrderDetail.test.ts +++ b/packages/ui-components/src/__tests__/OrderDetail.test.ts @@ -4,6 +4,7 @@ import { describe, it, vi, type Mock } from 'vitest'; import { expect } from '../lib/test/matchers'; import OrderDetail from './OrderDetail.test.svelte'; import type { OrderSubgraph, Vault } from '@rainlanguage/orderbook/js_api'; +import userEvent from '@testing-library/user-event'; import type { Config } from 'wagmi'; const { mockWalletAddressMatchesOrBlankStore } = await vi.hoisted( @@ -217,4 +218,50 @@ describe('OrderDetail Component', () => { expect(screen.getByText('Input & output vaults')).toBeInTheDocument(); }); }); + + it('refresh button triggers query invalidation when clicked', async () => { + const mockQuery = vi.mocked(await import('@tanstack/svelte-query')); + const mockInvalidateQueries = vi.fn(); + + // Mock the createQuery as in other tests + mockQuery.createQuery = vi.fn((__options, _queryClient) => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + subscribe: (fn: (value: any) => void) => { + fn({ + data: { order: mockOrder, vaults: new Map() }, + status: 'success', + isFetching: false, + refetch: () => {} + }); + return { unsubscribe: () => {} }; + } + })) as Mock; + + // Mock the useQueryClient hook + mockQuery.useQueryClient = vi.fn(() => ({ + invalidateQueries: mockInvalidateQueries + // eslint-disable-next-line @typescript-eslint/no-explicit-any + })) as any; + + render(OrderDetail, { + props: { + id: 'mockId', + subgraphUrl: 'https://example.com', + walletAddressMatchesOrBlank: mockWalletAddressMatchesOrBlankStore, + chainId, + orderbookAddress + } + }); + + const refreshButton = screen.getByTestId('refresh-button'); + await userEvent.click(refreshButton); + + await waitFor(() => { + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: ['mockId'], + refetchType: 'all', + exact: false + }); + }); + }); }); diff --git a/packages/ui-components/src/__tests__/TanstackAppTable.test.svelte b/packages/ui-components/src/__tests__/TanstackAppTable.test.svelte index cae33b3d1..2667fbc8a 100644 --- a/packages/ui-components/src/__tests__/TanstackAppTable.test.svelte +++ b/packages/ui-components/src/__tests__/TanstackAppTable.test.svelte @@ -4,9 +4,10 @@ export let emptyMessage: string; export let title: string; export let head: string; + export let queryKey: string; - +

{title}

diff --git a/packages/ui-components/src/__tests__/TanstackAppTable.test.ts b/packages/ui-components/src/__tests__/TanstackAppTable.test.ts index d55017655..55ac51517 100644 --- a/packages/ui-components/src/__tests__/TanstackAppTable.test.ts +++ b/packages/ui-components/src/__tests__/TanstackAppTable.test.ts @@ -2,176 +2,167 @@ import { render, screen, waitFor } from '@testing-library/svelte'; import { test, expect } from 'vitest'; import TanstackAppTableTest from './TanstackAppTable.test.svelte'; import userEvent from '@testing-library/user-event'; -import { createResolvableInfiniteQuery } from '../lib/__mocks__/queries'; -import { writable } from 'svelte/store'; +import { writable, get } from 'svelte/store'; import type { CreateInfiniteQueryResult, InfiniteData } from '@tanstack/svelte-query'; -test('shows head and title', async () => { - const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { - return ['page' + pageParam]; - }); - - render(TanstackAppTableTest, { - query, +vi.mock('@tanstack/svelte-query', () => ({ + useQueryClient: () => ({}) +})); + +const mockInvalidateIdQuery = vi.fn(); +vi.mock('$lib/queries/queryClient', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + invalidateIdQuery: (queryClient: any, queryKey: string) => + mockInvalidateIdQuery(queryClient, queryKey) +})); + +// Helper function to create base pages +const createPages = (pageData: unknown[] = ['page0']) => + writable({ + pages: [pageData], + pageParams: [0] + }); + +// Helper function to create base mock query +const createMockQuery = (pages: ReturnType, overrides = {}) => { + return writable({ + data: get(pages), + isLoading: false, + isFetching: false, + isFetchingNextPage: false, + hasNextPage: true, + status: 'success' as const, + fetchStatus: 'idle' as const, + fetchNextPage: vi.fn(), + ...overrides + }); +}; + +// Helper function for common render props +const renderTable = (query: ReturnType) => { + return render(TanstackAppTableTest, { + query: query as unknown as CreateInfiniteQueryResult, Error>, emptyMessage: 'No rows', title: 'Test Table', - head: 'Test head' + head: 'Test head', + queryKey: 'test' }); +}; - resolve(); +test('shows head and title', async () => { + const pages = createPages(); + const mockQuery = createMockQuery(pages); + renderTable(mockQuery); await waitFor(() => expect(screen.getByTestId('head')).toHaveTextContent('Test head')); expect(screen.getByTestId('title')).toHaveTextContent('Test Table'); }); test('renders rows', async () => { - const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { - return ['page' + pageParam]; - }); + const pages = createPages(); + const mockQuery = createMockQuery(pages); + renderTable(mockQuery); - render(TanstackAppTableTest, { - query, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' - }); - - resolve(); await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('page0')); }); test('shows empty message', async () => { - const { query, resolve } = createResolvableInfiniteQuery(() => { - return []; - }); - - render(TanstackAppTableTest, { - query, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' - }); - - resolve(); + const pages = createPages([]); + const mockQuery = createMockQuery(pages); + renderTable(mockQuery); await waitFor(() => expect(screen.getByTestId('emptyMessage')).toHaveTextContent('No rows')); }); test('loads more rows', async () => { - const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { - return ['page' + pageParam]; - }); - - render(TanstackAppTableTest, { - query, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' - }); - - resolve(); + const pages = createPages(); + const mockQuery = createMockQuery(pages, { + fetchNextPage: async () => { + mockQuery.update((q) => ({ ...q, isFetchingNextPage: true })); + await new Promise((resolve) => setTimeout(resolve, 0)); + pages.update((data) => ({ + pages: [...data.pages, [`page${data.pages.length}`]], + pageParams: [...data.pageParams, data.pageParams.length] + })); + mockQuery.update((q) => ({ + ...q, + data: get(pages), + isFetchingNextPage: false + })); + } + }); + renderTable(mockQuery); await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('page0')); - // loading more rows const loadMoreButton = screen.getByTestId('loadMoreButton'); await userEvent.click(loadMoreButton); - resolve(); - await waitFor(() => { expect(screen.getAllByTestId('bodyRow')).toHaveLength(2); }); let rows = screen.getAllByTestId('bodyRow'); - - expect(rows).toHaveLength(2); expect(rows[0]).toHaveTextContent('page0'); expect(rows[1]).toHaveTextContent('page1'); - // loading more rows await userEvent.click(loadMoreButton); - resolve(); - await waitFor(() => { expect(screen.getAllByTestId('bodyRow')).toHaveLength(3); }); rows = screen.getAllByTestId('bodyRow'); - - expect(rows).toHaveLength(3); expect(rows[0]).toHaveTextContent('page0'); expect(rows[1]).toHaveTextContent('page1'); expect(rows[2]).toHaveTextContent('page2'); }); test('load more button message changes when loading', async () => { - const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { - return ['page' + pageParam]; - }); - - render(TanstackAppTableTest, { - query, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' + const pages = createPages(); + const mockQuery = createMockQuery(pages, { + fetchNextPage: async () => { + mockQuery.update((q) => ({ ...q, isFetchingNextPage: true })); + await new Promise((resolve) => setTimeout(resolve, 100)); + mockQuery.update((q) => ({ ...q, isFetchingNextPage: false })); + } }); - - resolve(); + renderTable(mockQuery); expect(await screen.findByTestId('loadMoreButton')).toHaveTextContent('Load More'); - // loading more rows const loadMoreButton = screen.getByTestId('loadMoreButton'); await userEvent.click(loadMoreButton); expect(await screen.findByTestId('loadMoreButton')).toHaveTextContent('Loading more...'); - resolve(); - await waitFor(() => { expect(screen.getByTestId('loadMoreButton')).toHaveTextContent('Load More'); }); }); test('shows refresh icon', async () => { - const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { - return ['page' + pageParam]; - }); - - render(TanstackAppTableTest, { - query, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' - }); - - resolve(); + const pages = createPages(); + const mockQuery = createMockQuery(pages); + renderTable(mockQuery); await waitFor(() => expect(screen.getByTestId('refreshButton')).toBeInTheDocument()); }); test('refetches data when refresh button is clicked', async () => { const mockRefetch = vi.fn(); - const mockQuery = writable({ + const mockQuery = createMockQuery(createPages(), { status: 'success', fetchStatus: 'idle', + isLoading: false, + isFetching: false, refetch: mockRefetch }); - - render(TanstackAppTableTest, { - query: mockQuery as unknown as CreateInfiniteQueryResult< - InfiniteData, - Error - >, - emptyMessage: 'No rows', - title: 'Test Table', - head: 'Test head' - }); + renderTable(mockQuery); const refreshButton = screen.getByTestId('refreshButton'); await userEvent.click(refreshButton); expect(mockRefetch).toHaveBeenCalled(); + expect(mockInvalidateIdQuery).toHaveBeenCalledWith(expect.anything(), 'test'); }); diff --git a/packages/ui-components/src/__tests__/TanstackOrderQuote.test.ts b/packages/ui-components/src/__tests__/TanstackOrderQuote.test.ts index 0ce408abf..8813b44cd 100644 --- a/packages/ui-components/src/__tests__/TanstackOrderQuote.test.ts +++ b/packages/ui-components/src/__tests__/TanstackOrderQuote.test.ts @@ -102,7 +102,7 @@ test('refreshes the quote when the refresh icon is clicked', async () => { } ]); - const refreshButton = screen.getByTestId('refreshButton'); + const refreshButton = screen.getByTestId('refresh-button'); fireEvent.click(refreshButton); await waitFor(() => { diff --git a/packages/ui-components/src/__tests__/VaultDetail.test.ts b/packages/ui-components/src/__tests__/VaultDetail.test.ts index a4c2c5014..1ac9fdedb 100644 --- a/packages/ui-components/src/__tests__/VaultDetail.test.ts +++ b/packages/ui-components/src/__tests__/VaultDetail.test.ts @@ -6,6 +6,7 @@ import VaultDetail from '../lib/components/detail/VaultDetail.svelte'; import { readable, writable } from 'svelte/store'; import { darkChartTheme } from '../lib/utils/lightweightChartsThemes'; import type { Config } from 'wagmi'; +import userEvent from '@testing-library/user-event'; // Mock the js_api getVault function vi.mock('@rainlanguage/orderbook/js_api', () => ({ @@ -177,3 +178,68 @@ test('shows deposit/withdraw buttons when signerAddress matches owner', async () expect(screen.getAllByTestId('depositOrWithdrawButton')).toHaveLength(2); }); }); + +test('refresh button triggers query invalidation when clicked', async () => { + const mockData = { + id: '1', + vaultId: '0xabc', + owner: '0x123', + token: { + id: '0x456', + address: '0x456', + name: 'USDC coin', + symbol: 'USDC', + decimals: '6' + }, + balance: '100000000000', + ordersAsInput: [ + { + id: '1', + owner: '0x123' + } + ], + ordersAsOutput: [ + { + id: '2', + owner: '0x123' + } + ], + balanceChanges: [], + orderbook: { + id: '0x00' + } + }; + + const { getVault } = await import('@rainlanguage/orderbook/js_api'); + vi.mocked(getVault).mockResolvedValue(mockData); + const queryClient = new QueryClient(); + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries'); + + const mockWagmiConfig = writable({} as Config); + const mockSignerAddress = writable('0x123'); // Same as owner address + + render(VaultDetail, { + props: { + id: '100', + network: 'mainnet', + activeNetworkRef: writable('mainnet'), + activeOrderbookRef: writable('0x00'), + settings: mockSettings, + lightweightChartsTheme: readable(darkChartTheme), + wagmiConfig: mockWagmiConfig, + signerAddress: mockSignerAddress, + handleDepositOrWithdrawModal: vi.fn() + }, + context: new Map([['$$_queryClient', queryClient]]) + }); + + await waitFor(async () => { + const refreshButton = await screen.findAllByTestId('refresh-button'); + await userEvent.click(refreshButton[0]); + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: ['100'], + refetchType: 'all', + exact: false + }); + }); +}); diff --git a/packages/ui-components/src/lib/components/TanstackAppTable.svelte b/packages/ui-components/src/lib/components/TanstackAppTable.svelte index d713081db..cb5760a8a 100644 --- a/packages/ui-components/src/lib/components/TanstackAppTable.svelte +++ b/packages/ui-components/src/lib/components/TanstackAppTable.svelte @@ -1,12 +1,16 @@ - + diff --git a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte index 04d58d0a4..302f73aac 100644 --- a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte @@ -57,7 +57,12 @@ }); - + {#if tradesCount !== undefined}
{tradesCount} Trades
diff --git a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte index e94ec9817..f73ea0350 100644 --- a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte @@ -27,7 +27,12 @@ }); - + diff --git a/packages/ui-components/src/lib/components/tables/OrdersListTable.svelte b/packages/ui-components/src/lib/components/tables/OrdersListTable.svelte index 4f81f0916..6b532d52f 100644 --- a/packages/ui-components/src/lib/components/tables/OrdersListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrdersListTable.svelte @@ -105,6 +105,7 @@ { activeNetworkRef.set(e.detail.item.subgraphName); diff --git a/packages/ui-components/src/lib/components/tables/VaultBalanceChangesTable.svelte b/packages/ui-components/src/lib/components/tables/VaultBalanceChangesTable.svelte index ecf61eb4f..dbb7ed687 100644 --- a/packages/ui-components/src/lib/components/tables/VaultBalanceChangesTable.svelte +++ b/packages/ui-components/src/lib/components/tables/VaultBalanceChangesTable.svelte @@ -38,6 +38,7 @@ diff --git a/packages/ui-components/src/lib/components/tables/VaultsListTable.svelte b/packages/ui-components/src/lib/components/tables/VaultsListTable.svelte index 59892b75a..5a1586669 100644 --- a/packages/ui-components/src/lib/components/tables/VaultsListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/VaultsListTable.svelte @@ -113,6 +113,7 @@ /> { updateActiveNetworkAndOrderbook(e.detail.item.subgraphName); diff --git a/packages/ui-components/src/lib/queries/queryClient.ts b/packages/ui-components/src/lib/queries/queryClient.ts index 60268f25e..bec18dd28 100644 --- a/packages/ui-components/src/lib/queries/queryClient.ts +++ b/packages/ui-components/src/lib/queries/queryClient.ts @@ -8,3 +8,11 @@ export const queryClient = new QueryClient({ } } }); + +export const invalidateIdQuery = async (queryClient: QueryClient, id: string) => { + await queryClient.invalidateQueries({ + queryKey: [id], + refetchType: 'all', + exact: false + }); +}; diff --git a/packages/ui-components/src/lib/stores/queryClient.ts b/packages/ui-components/src/lib/stores/queryClient.ts deleted file mode 100644 index 60268f25e..000000000 --- a/packages/ui-components/src/lib/stores/queryClient.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { browser } from '$app/environment'; -import { QueryClient } from '@tanstack/svelte-query'; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - enabled: browser - } - } -}); diff --git a/packages/webapp/src/routes/orders/[network]-[id]/+page.svelte b/packages/webapp/src/routes/orders/[network]-[id]/+page.svelte index 5bc355f4f..4ef0325d7 100644 --- a/packages/webapp/src/routes/orders/[network]-[id]/+page.svelte +++ b/packages/webapp/src/routes/orders/[network]-[id]/+page.svelte @@ -38,7 +38,9 @@ $: if ($transactionStore.status === TransactionStatus.SUCCESS) { queryClient.invalidateQueries({ - queryKey: [$page.params.id] + queryKey: [$page.params.id], + refetchType: 'all', + exact: false }); triggerToast(); } diff --git a/packages/webapp/src/routes/vaults/[network]-[id]/+page.svelte b/packages/webapp/src/routes/vaults/[network]-[id]/+page.svelte index 39b7adecc..fa27487f0 100644 --- a/packages/webapp/src/routes/vaults/[network]-[id]/+page.svelte +++ b/packages/webapp/src/routes/vaults/[network]-[id]/+page.svelte @@ -29,7 +29,9 @@ $: if ($transactionStore.status === TransactionStatus.SUCCESS) { queryClient.invalidateQueries({ - queryKey: [$page.params.id] + queryKey: [$page.params.id], + refetchType: 'all', + exact: false }); triggerToast(); }