From fdfecb1dbff8072da3242e231149a34e720e9297 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 8 Jul 2025 17:48:57 +0200 Subject: [PATCH 1/8] feat: save table filters in local storage --- .../llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx | 9 ++++----- .../PageLlamaMarkets/filters/RangeSliderFilter.tsx | 6 ++++-- packages/curve-ui-kit/src/hooks/useLocalStorage.ts | 7 +++++++ .../src/shared/ui/DataTable/TableFilters.tsx | 12 ++++++------ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx index b1765333f..11e2a3f32 100644 --- a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx +++ b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx @@ -26,9 +26,6 @@ import { DataTable } from '@ui-kit/shared/ui/DataTable' import { type Option, SelectFilter } from '@ui-kit/shared/ui/DataTable/SelectFilter' import { TableFilters, useColumnFilters } from '@ui-kit/shared/ui/DataTable/TableFilters' import { useVisibilitySettings } from '@ui-kit/shared/ui/DataTable/TableVisibilitySettingsPopover' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' - -const { Spacing, MaxWidth } = SizesAndSpaces /** * Hook to manage the visibility of columns in the Llama Markets table. @@ -43,6 +40,8 @@ const useVisibility = (sorting: SortingState, hasPositions: boolean | undefined) return { sortField, ...visibilitySettings, ...(useIsMobile() && { columnVisibility }) } } +const TITLE = 'Llamalend Markets' // not using the t`` here as the value is used as a key in the local storage + export const LlamaMarketsTable = ({ onReload, result, @@ -55,7 +54,7 @@ export const LlamaMarketsTable = ({ minLiquidity: number }) => { const { markets: data = [], hasPositions, hasFavorites } = result ?? {} - const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters([ + const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters(TITLE, [ { id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }, ]) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) @@ -87,7 +86,7 @@ export const LlamaMarketsTable = ({ shouldStickFirstColumn={useIsTablet() && !!hasPositions} > - title={t`Llamalend Markets`} + title={TITLE} subtitle={t`Borrow with the power of Curve soft liquidations`} onReload={onReload} visibilityGroups={columnSettings} diff --git a/apps/main/src/llamalend/PageLlamaMarkets/filters/RangeSliderFilter.tsx b/apps/main/src/llamalend/PageLlamaMarkets/filters/RangeSliderFilter.tsx index a8c1c8882..54bc4a606 100644 --- a/apps/main/src/llamalend/PageLlamaMarkets/filters/RangeSliderFilter.tsx +++ b/apps/main/src/llamalend/PageLlamaMarkets/filters/RangeSliderFilter.tsx @@ -55,9 +55,11 @@ export const RangeSliderFilter = ({ (newRange: NumberRange) => setColumnFilter( id, - newRange.every((value, i) => value === defaultValue[i]) ? undefined : (newRange as NumberRange), + newRange.every((value, i) => value === defaultValue[i]) + ? undefined // remove the filter if the range is the same as the default + : [newRange[0] === defaultMinimum ? null : newRange[0], newRange[1] === maxValue ? null : newRange[1]], ), - [defaultValue, id, setColumnFilter], + [defaultMinimum, defaultValue, id, maxValue, setColumnFilter], ), ) diff --git a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts index 0f3163160..935309b03 100644 --- a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts +++ b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts @@ -1,6 +1,7 @@ import { kebabCase } from 'lodash' import { useMemo } from 'react' import type { Address } from '@curvefi/prices-api' +import type { ColumnFiltersState } from '@tanstack/table-core' import { isBetaDefault } from '@ui-kit/utils' import { useStoredState } from './useStoredState' @@ -38,6 +39,12 @@ export const useBetaFlag = () => useLocalStorage('beta', isBetaDefault) export const useFilterExpanded = (tableTitle: string) => useLocalStorage(`filter-expanded-${kebabCase(tableTitle)}`, false) +export const useTableFilters = (tableTitle: string, defaultFilters: ColumnFiltersState) => + useLocalStorage( + `table-filters-${kebabCase(tableTitle)}`, + useMemo(() => [], []), + ) + export const getFavoriteMarkets = () => getFromLocalStorage('favoriteMarkets') ?? [] export const useFavoriteMarkets = () => { const initialValue = useMemo(() => [], []) diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx index fd0af3c13..2f24f711a 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable/TableFilters.tsx @@ -1,4 +1,4 @@ -import { forwardRef, ReactNode, useCallback, useMemo, useRef, useState } from 'react' +import { forwardRef, ReactNode, useCallback, useMemo, useRef } from 'react' import Box from '@mui/material/Box' import Collapse from '@mui/material/Collapse' import Grid from '@mui/material/Grid2' @@ -8,7 +8,7 @@ import SvgIcon from '@mui/material/SvgIcon' import Typography from '@mui/material/Typography' import { ColumnFiltersState } from '@tanstack/react-table' import { useIsMobile, useIsTiny } from '@ui-kit/hooks/useBreakpoints' -import { useFilterExpanded } from '@ui-kit/hooks/useLocalStorage' +import { useFilterExpanded, useTableFilters } from '@ui-kit/hooks/useLocalStorage' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { FilterIcon } from '../../icons/FilterIcon' @@ -142,8 +142,8 @@ export const TableFilters = ({ /** * A hook to manage filters for a table. Currently saved in the state, but the URL could be a better place. */ -export function useColumnFilters(defaultFilters: ColumnFiltersState = []) { - const [columnFilters, setColumnFilters] = useState(defaultFilters) +export function useColumnFilters(tableTitle: string, defaultFilters: ColumnFiltersState = []) { + const [columnFilters, setColumnFilters] = useTableFilters(tableTitle, defaultFilters) const setColumnFilter = useCallback( (id: string, value: unknown) => setColumnFilters((filters) => [ @@ -157,7 +157,7 @@ export function useColumnFilters(defaultFilters: ColumnFiltersState = []) { }, ]), ]), - [], + [setColumnFilters], ) const columnFiltersById: Record = useMemo( () => @@ -171,7 +171,7 @@ export function useColumnFilters(defaultFilters: ColumnFiltersState = []) { [columnFilters], ) - const resetFilters = useCallback(() => setColumnFilters(defaultFilters), [defaultFilters]) + const resetFilters = useCallback(() => setColumnFilters(defaultFilters), [defaultFilters, setColumnFilters]) return [columnFilters, columnFiltersById, setColumnFilter, resetFilters] as const } From a3bd02ce0d336a16d86dc5616b0fd3856714bb9b Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 15 Jul 2025 14:26:05 +0200 Subject: [PATCH 2/8] fix: cypress imports, utilization test --- packages/tsconfig/cypress.json | 2 +- tests/cypress/e2e/llamalend/llamalend-markets.cy.ts | 11 ++++++----- tests/cypress/support/helpers/lending-mocks.ts | 8 ++++---- tests/cypress/tsconfig.json | 1 + tests/tsconfig.json | 9 ++++++++- tests/vite.config.ts | 10 ++++++++++ 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/tsconfig/cypress.json b/packages/tsconfig/cypress.json index 678e0a4e1..5ddb85289 100644 --- a/packages/tsconfig/cypress.json +++ b/packages/tsconfig/cypress.json @@ -3,7 +3,7 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "lib": ["es5", "dom"], "types": ["cypress", "node"], "resolveJsonModule": true diff --git a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts index 1e2650c6e..927bb8552 100644 --- a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts @@ -18,7 +18,7 @@ import { oneViewport, RETRY_IN_CI, } from '@/support/ui' -import type { GetMarketsResponse } from '@curvefi/prices-api/dist/llamalend' +import type { GetMarketsResponse } from '@curvefi/prices-api/llamalend' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' describe(`LlamaLend Markets`, () => { @@ -65,24 +65,25 @@ describe(`LlamaLend Markets`, () => { }) it('should sort', () => { + const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% if (breakpoint == 'mobile') { cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') cy.get('[data-testid="select-filter-sort"]').click() cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) cy.get(`[data-testid^="data-table-row"]`) - .first() + .eq(index) .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) .should('exist') expandFirstRowOnMobile() // note: not possible currently to sort ascending - cy.get('[data-testid="metric-utilizationPercent"]').first().contains('99.99%', LOAD_TIMEOUT) + cy.get('[data-testid="metric-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) } else { cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').first().contains('99.99%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').first().contains('0.00%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) } }) diff --git a/tests/cypress/support/helpers/lending-mocks.ts b/tests/cypress/support/helpers/lending-mocks.ts index fcf738db1..c038566fb 100644 --- a/tests/cypress/support/helpers/lending-mocks.ts +++ b/tests/cypress/support/helpers/lending-mocks.ts @@ -1,7 +1,7 @@ import { MAX_USD_VALUE, oneAddress, oneFloat, oneInt, oneOf, onePrice, range } from '@/support/generators' import { oneToken } from '@/support/helpers/tokens' -import type { GetMarketsResponse } from '@curvefi/prices-api/src/llamalend' -import { fromEntries } from '../../../../packages/prices-api/src/objects.util' +import type { GetMarketsResponse } from '@curvefi/prices-api/llamalend' +import { fromEntries } from '@curvefi/prices-api/objects.util' const LendingChains = ['ethereum', 'fraxtal', 'arbitrum'] as const export type Chain = (typeof LendingChains)[number] @@ -64,12 +64,12 @@ export const HighUtilizationAddress = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA function oneLendingVaultResponse(chain: Chain): GetMarketsResponse { const count = oneInt(2, 20) const data = [ - ...range(count).map((index) => oneLendingPool(chain, index / (count - 1))), + ...range(count).map((index) => oneLendingPool(chain, index / count)), ...(chain == 'ethereum' ? ([ { // fixed vault address to test campaign rewards - ...oneLendingPool(chain, oneFloat()), + ...oneLendingPool(chain, oneFloat(0.98)), vault: '0xc28c2fd809fc1795f90de1c9da2131434a77721d', }, { diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json index 0e826f6e3..7f1a021fc 100644 --- a/tests/cypress/tsconfig.json +++ b/tests/cypress/tsconfig.json @@ -6,6 +6,7 @@ "paths": { "@/*": ["*"], "@curvefi/prices-api": ["../../packages/prices-api/src"], + "@curvefi/prices-api/*": ["../../packages/prices-api/src/*"], "@ui-kit/*": ["../../packages/curve-ui-kit/src/*"] } }, diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 6a93c1f7e..03ce4db6c 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -3,7 +3,14 @@ "compilerOptions": { "types": ["hardhat"], "baseUrl": "./cypress", - "resolveJsonModule": true + "resolveJsonModule": true, + "paths": { + "@ui": ["../../packages/ui/src/index.ts"], + "@ui/*": ["../../packages/ui/src/*"], + "@curvefi/prices-api": ["../../packages/prices-api/src/index.ts"], + "@curvefi/prices-api/*": ["../../packages/prices-api/src/*"], + "@ui-kit/*": ["../../packages/curve-ui-kit/src/*"], + } }, "include": [ "./**/*.ts", diff --git a/tests/vite.config.ts b/tests/vite.config.ts index 5face1ce2..a33728546 100644 --- a/tests/vite.config.ts +++ b/tests/vite.config.ts @@ -1,7 +1,17 @@ +import { resolve } from 'path' import { defineConfig } from 'vite' import tsconfigPaths from 'vite-tsconfig-paths' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react(), tsconfigPaths()], + resolve: { + alias: [ + { find: '@ui', replacement: resolve(__dirname, '../packages/ui/src/') }, + { find: '@ui-kit', replacement: resolve(__dirname, '../packages/curve-ui-kit/src') }, + { find: '@external-rewards', replacement: resolve(__dirname, '../packages/external-rewards/src/index.ts') }, + { find: '@curvefi/prices-api', replacement: resolve(__dirname, '../packages/prices-api/src') }, + { find: '@curvefi/prices-api/', replacement: resolve(__dirname, '../packages/prices-api/src/') }, + ], + }, }) From d3fc09376c334de1b712f4a52a8288743bba1af2 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 15 Jul 2025 14:39:28 +0200 Subject: [PATCH 3/8] fix: mobile row expansion --- .../e2e/llamalend/llamalend-markets.cy.ts | 502 +++++++++--------- 1 file changed, 253 insertions(+), 249 deletions(-) diff --git a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts index 927bb8552..4063178eb 100644 --- a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts @@ -21,288 +21,292 @@ import { import type { GetMarketsResponse } from '@curvefi/prices-api/llamalend' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' -describe(`LlamaLend Markets`, () => { - let breakpoint: Breakpoint - let width: number, height: number - let vaultData: Record +range(5).forEach(() => + describe(`LlamaLend Markets`, () => { + let breakpoint: Breakpoint + let width: number, height: number + let vaultData: Record - beforeEach(() => { - ;[width, height, breakpoint] = oneViewport() - vaultData = createLendingVaultChainsResponse() - mockTokenPrices() - mockLendingVaults(vaultData) - mockLendingSnapshots().as('lend-snapshots') - mockMintMarkets() - mockMintSnapshots() + beforeEach(() => { + ;[width, height, breakpoint] = oneViewport() + vaultData = createLendingVaultChainsResponse() + mockTokenPrices() + mockLendingVaults(vaultData) + mockLendingSnapshots().as('lend-snapshots') + mockMintMarkets() + mockMintSnapshots() - cy.viewport(width, height) - cy.setCookie('cypress', 'true') // disable server data fetching so the app can use the mocks - cy.visit('/llamalend/ethereum/markets/', { - onBeforeLoad: (window) => window.localStorage.clear(), - ...LOAD_TIMEOUT, + cy.viewport(width, height) + cy.setCookie('cypress', 'true') // disable server data fetching so the app can use the mocks + cy.visit('/llamalend/ethereum/markets/', { + onBeforeLoad: (window) => window.localStorage.clear(), + ...LOAD_TIMEOUT, + }) + cy.get('[data-testid="data-table"]', LOAD_TIMEOUT).should('be.visible') }) - cy.get('[data-testid="data-table"]', LOAD_TIMEOUT).should('be.visible') - }) - const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) - it('should have sticky headers', () => { - cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) - cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() - cy.get('[data-testid="data-table-head"] th').eq(1).then(assertInViewport) - cy.get(`[data-testid^="pool-type-"]`).should('be.visible') // wait for the table to render + const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) + it('should have sticky headers', () => { + cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) + cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() + cy.get('[data-testid="data-table-head"] th').eq(1).then(assertInViewport) + cy.get(`[data-testid^="pool-type-"]`).should('be.visible') // wait for the table to render - // filter height changes because text wraps depending on the width - const filterHeight = { - // the height of the header changes depending on how often the description text wraps - mobile: [194, 180, 156, 144], - // on tablet, we expect 3 rows until 900px, then 2 rows - tablet: [208, 164], - // on desktop, we expect 2 rows always - desktop: [172], - }[breakpoint] - cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('be.oneOf', filterHeight) - cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', 65) - }) - - it('should sort', () => { - const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% - if (breakpoint == 'mobile') { - cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') - cy.get('[data-testid="select-filter-sort"]').click() - cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() - cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) - cy.get(`[data-testid^="data-table-row"]`) - .eq(index) - .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) - .should('exist') - expandFirstRowOnMobile() - // note: not possible currently to sort ascending - cy.get('[data-testid="metric-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) - } else { - cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') - cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) - cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) - } - }) - - // todo: retry cause this fails in large screens with small data set (laziness not triggered, everything is shown) - it('should show charts', RETRY_IN_CI, () => { - withFilterChips(() => { - cy.get(`[data-testid="chip-lend"]`).click() - cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') + // filter height changes because text wraps depending on the width + const filterHeight = { + // the height of the header changes depending on how often the description text wraps + mobile: [194, 180, 156, 144], + // on tablet, we expect 3 rows until 900px, then 2 rows + tablet: [208, 164], + // on desktop, we expect 2 rows always + desktop: [172], + }[breakpoint] + cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('be.oneOf', filterHeight) + cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', 65) }) - expandFirstRowOnMobile() - if (breakpoint != 'mobile') { - enableGraphColumn() - } - checkLineGraphColor('borrow', '#ed242f') - // check that scrolling loads more snapshots: - cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls1) => { - cy.get('[data-testid^="data-table-row"]') - .last() - .scrollIntoView({ offset: { top: -height / 2, left: 0 } }) // scroll to the last row, make sure it's still visible + it('should sort', () => { + const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% if (breakpoint == 'mobile') { - cy.get(`[data-testid="expand-icon"]`).last().scrollIntoView() - cy.get(`[data-testid="expand-icon"]`).last().click() + cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') + cy.get('[data-testid="select-filter-sort"]').click() + cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() + cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) + cy.get(`[data-testid^="data-table-row"]`) + .eq(index) + .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) + .should('exist') + expandRowOnMobile(index) + // note: not possible currently to sort ascending + cy.get('[data-testid="metric-utilizationPercent"]').contains('99.99%', LOAD_TIMEOUT) + } else { + cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') + cy.get('[data-testid="data-table-header-utilizationPercent"]').click() + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-header-utilizationPercent"]').click() + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) } - cy.wait('@lend-snapshots') - cy.get('[data-testid^="data-table-row"]').last().should('contain.html', 'path') // wait for the graph to render - cy.wait(range(calls1.length).map(() => '@lend-snapshots')) - cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls2) => - expect(calls2.length).to.be.greaterThan(calls1.length), - ) }) - }) - it('should find markets by text', () => { - cy.get("[data-testid='table-text-search']").type('wstETH crvUSD') - cy.scrollTo(0, 0) - // sfrxETH market is filtered out - cy.get(`[data-testid='market-link-0x136e783846ef68C8Bd00a3369F787dF8d683a696']`).should('not.exist') - // wstETH market is shown - cy.get(`[data-testid="market-link-0x37417B2238AA52D0DD2D6252d989E728e8f706e4"]`).should('exist') - }) + // todo: retry cause this fails in large screens with small data set (laziness not triggered, everything is shown) + it('should show charts', RETRY_IN_CI, () => { + withFilterChips(() => { + cy.get(`[data-testid="chip-lend"]`).click() + cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') + }) + expandRowOnMobile() + if (breakpoint != 'mobile') { + enableGraphColumn() + } + checkLineGraphColor('borrow', '#ed242f') - it(`should allow filtering by using a slider`, () => { - const [columnId, initialFilterText] = oneOf( - ['liquidityUsd', 'Liquidity: $10,000 -'], - ['utilizationPercent', 'Utilization: 0.00% -'], - ) - cy.viewport(1200, 800) // use fixed viewport to have consistent slider width - cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('contain', initialFilterText) - cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click() - /** - * Using `force: true` to bypass Cypress' element visibility check. - * The slider may have pseudo-elements that interfere with Cypress' ability to interact with it. - * We've tried alternative approaches (adding waits, adjusting click coordinates) but they didn't resolve the issue. - * The application behavior works correctly despite this test accommodation. - */ - cy.get(`[data-testid="slider-${columnId}"]`).click(60, 20, { force: true }) - cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.contain', initialFilterText) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + // check that scrolling loads more snapshots: + cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls1) => { + cy.get('[data-testid^="data-table-row"]') + .last() + .scrollIntoView({ offset: { top: -height / 2, left: 0 } }) // scroll to the last row, make sure it's still visible + if (breakpoint == 'mobile') { + cy.get(`[data-testid="expand-icon"]`).last().scrollIntoView() + cy.get(`[data-testid="expand-icon"]`).last().click() + } + cy.wait('@lend-snapshots') + cy.get('[data-testid^="data-table-row"]').last().should('contain.html', 'path') // wait for the graph to render + cy.wait(range(calls1.length).map(() => '@lend-snapshots')) + cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls2) => + expect(calls2.length).to.be.greaterThan(calls1.length), + ) + }) }) - }) - it('should allow filtering by chain', () => { - const chains = Object.keys(vaultData) - const chain = oneOf(...chains) - cy.get('[data-testid="multi-select-filter-chain"]').should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click() + it('should find markets by text', () => { + cy.get("[data-testid='table-text-search']").type('wstETH crvUSD') + cy.scrollTo(0, 0) + // sfrxETH market is filtered out + cy.get(`[data-testid='market-link-0x136e783846ef68C8Bd00a3369F787dF8d683a696']`).should('not.exist') + // wstETH market is shown + cy.get(`[data-testid="market-link-0x37417B2238AA52D0DD2D6252d989E728e8f706e4"]`).should('exist') + }) - selectChain(chain) - cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid="chain-icon-${chain}"]`).should('be.visible') + it(`should allow filtering by using a slider`, () => { + const [columnId, initialFilterText] = oneOf( + ['liquidityUsd', 'Liquidity: $10,000 -'], + ['utilizationPercent', 'Utilization: 0.00% -'], + ) + cy.viewport(1200, 800) // use fixed viewport to have consistent slider width + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('contain', initialFilterText) + cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click() + /** + * Using `force: true` to bypass Cypress' element visibility check. + * The slider may have pseudo-elements that interfere with Cypress' ability to interact with it. + * We've tried alternative approaches (adding waits, adjusting click coordinates) but they didn't resolve the issue. + * The application behavior works correctly despite this test accommodation. + */ + cy.get(`[data-testid="slider-${columnId}"]`).click(60, 20, { force: true }) + cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.contain', initialFilterText) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + }) + }) - const otherChain = oneOf(...chains.filter((c) => c !== chain)) - selectChain(otherChain) - ;[chain, otherChain].forEach((c) => cy.get(`[data-testid="chain-icon-${c}"]`).should('be.visible')) - }) + it('should allow filtering by chain', () => { + const chains = Object.keys(vaultData) + const chain = oneOf(...chains) + cy.get('[data-testid="multi-select-filter-chain"]').should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click() - it(`should allow filtering by token`, () => { - const type = oneTokenType() - const tokenField = (type + '_token') as `${typeof type}_token` + selectChain(chain) + cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid="chain-icon-${chain}"]`).should('be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click() - const coins = vaultData.ethereum.data - .filter((d) => d.total_assets_usd - d.total_debt_usd > SMALL_POOL_TVL) - .map((d) => d[tokenField].symbol) - const coin1 = oneOf(...coins) - const coin2 = oneOf(...coins.filter((c) => c !== coin1)) - selectCoin(coin1, type) - selectCoin(coin2, type) - }) + const otherChain = oneOf(...chains.filter((c) => c !== chain)) + selectChain(otherChain) + ;[chain, otherChain].forEach((c) => cy.get(`[data-testid="chain-icon-${c}"]`).should('be.visible')) + }) - it('should allow filtering favorites', { scrollBehavior: false }, () => { - expandFirstRowOnMobile() - if (breakpoint == 'desktop') { - // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force - cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) - } else { - cy.get(`[data-testid="favorite-icon"]:visible`).first().click() - } - withFilterChips(() => cy.get(`[data-testid="chip-favorites"]`).click()) - cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) - cy.get(`[data-testid="favorite-icon"]:visible`).should('not.exist') - cy.get(`[data-testid="favorite-icon-filled"]:visible`).click() - cy.get(`[data-testid="table-empty-row"]`).should('exist') - withFilterChips(() => cy.get(`[data-testid="reset-filter"]`).click()) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) - }) + it(`should allow filtering by token`, () => { + const type = oneTokenType() + const tokenField = (type + '_token') as `${typeof type}_token` - it(`should allow filtering by market type`, () => - withFilterChips(() => - cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { - const [type, otherType] = shuffle('mint', 'lend') - cy.get(`[data-testid="chip-${type}"]`).click() - cy.get(`[data-testid^="pool-type-"]`).each(($el) => expect($el.attr('data-testid')).equals(`pool-type-${type}`)) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) - cy.get(`[data-testid="chip-${otherType}"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length', length) - }), - )) + cy.get(`[data-testid="btn-expand-filters"]`).click() + const coins = vaultData.ethereum.data + .filter((d) => d.total_assets_usd - d.total_debt_usd > SMALL_POOL_TVL) + .map((d) => d[tokenField].symbol) + const coin1 = oneOf(...coins) + const coin2 = oneOf(...coins.filter((c) => c !== coin1)) + selectCoin(coin1, type) + selectCoin(coin2, type) + }) - it(`should copy the market address`, RETRY_IN_CI, () => { - if (breakpoint === 'mobile') { - expandFirstRowOnMobile() - } - // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) - range(2).forEach(() => - breakpoint === 'desktop' - ? // on desktop, the copy button is not visible until hovered - but cypress doesn't support that so use force - cy.get(`[data-testid^="copy-market-address"]`).first().click({ force: true }) - : cy.get(`[data-testid^="copy-market-address"]:visible`).first().click(), - ) - cy.get(`[data-testid="copy-confirmation"]`).should('be.visible') - }) + it('should allow filtering favorites', { scrollBehavior: false }, () => { + expandRowOnMobile() + if (breakpoint == 'desktop') { + // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) + } else { + cy.get(`[data-testid="favorite-icon"]:visible`).first().click() + } + withFilterChips(() => cy.get(`[data-testid="chip-favorites"]`).click()) + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + cy.get(`[data-testid="favorite-icon"]:visible`).should('not.exist') + cy.get(`[data-testid="favorite-icon-filled"]:visible`).click() + cy.get(`[data-testid="table-empty-row"]`).should('exist') + withFilterChips(() => cy.get(`[data-testid="reset-filter"]`).click()) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + }) + + it(`should allow filtering by market type`, () => + withFilterChips(() => + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + const [type, otherType] = shuffle('mint', 'lend') + cy.get(`[data-testid="chip-${type}"]`).click() + cy.get(`[data-testid^="pool-type-"]`).each(($el) => + expect($el.attr('data-testid')).equals(`pool-type-${type}`), + ) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + cy.get(`[data-testid="chip-${otherType}"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', length) + }), + )) - it(`should navigate to market details`, () => { - const [type, urlRegex] = oneOf( - ['mint', /\/crvusd\/\w+\/markets\/.+\/create/], - ['lend', /\/lend\/\w+\/markets\/.+\/create/], - ) - withFilterChips(() => { - cy.get(`[data-testid="chip-${type}"]`).click() - firstRow().contains(capitalize(type)) + it(`should copy the market address`, RETRY_IN_CI, () => { + if (breakpoint === 'mobile') { + expandRowOnMobile() + } + // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) + range(2).forEach(() => + breakpoint === 'desktop' + ? // on desktop, the copy button is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid^="copy-market-address"]`).first().click({ force: true }) + : cy.get(`[data-testid^="copy-market-address"]:visible`).first().click(), + ) + cy.get(`[data-testid="copy-confirmation"]`).should('be.visible') }) - cy.get(`[data-testid^="market-link-"]`).first().click() - if (breakpoint === 'mobile') { - cy.get(`[data-testid^="llama-market-go-to-market"]:visible`).click() - } - cy.url(LOAD_TIMEOUT).should('match', urlRegex) - }) - it(`should allow filtering by rewards`, { scrollBehavior: false }, () => { - cy.get(`[data-testid^="data-table-row"]`).should('have.length.at.least', 1) - withFilterChips(() => { - cy.get(`[data-testid="chip-rewards"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + it(`should navigate to market details`, () => { + const [type, urlRegex] = oneOf( + ['mint', /\/crvusd\/\w+\/markets\/.+\/create/], + ['lend', /\/lend\/\w+\/markets\/.+\/create/], + ) + withFilterChips(() => { + cy.get(`[data-testid="chip-${type}"]`).click() + firstRow().contains(capitalize(type)) + }) + cy.get(`[data-testid^="market-link-"]`).first().click() + if (breakpoint === 'mobile') { + cy.get(`[data-testid^="llama-market-go-to-market"]:visible`).click() + } + cy.url(LOAD_TIMEOUT).should('match', urlRegex) }) - expandFirstRowOnMobile() - cy.get(`[data-testid="rewards-icons"]`).should('be.visible') - withFilterChips(() => { - cy.get(`[data-testid="chip-rewards"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + + it(`should allow filtering by rewards`, { scrollBehavior: false }, () => { + cy.get(`[data-testid^="data-table-row"]`).should('have.length.at.least', 1) + withFilterChips(() => { + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + }) + expandRowOnMobile() + cy.get(`[data-testid="rewards-icons"]`).should('be.visible') + withFilterChips(() => { + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + }) }) - }) - it('should hide columns', () => { - if (breakpoint == 'mobile') { - // mobile viewports do not have this feature - cy.viewport(...oneDesktopViewport()) - } - const { toggle, element } = oneOf( - { toggle: 'liquidityUsd', element: 'data-table-header-liquidityUsd' }, - { toggle: 'utilizationPercent', element: 'data-table-header-utilizationPercent' }, - ) - cy.get(`[data-testid="${element}"]`).first().scrollIntoView() - cy.get(`[data-testid="${element}"]`).should('be.visible') - cy.get(`[data-testid="btn-visibility-settings"]`).click() - cy.get(`[data-testid="visibility-toggle-${toggle}"]`).click() - cy.get(`[data-testid="${element}"]`).should('not.exist') - }) + it('should hide columns', () => { + if (breakpoint == 'mobile') { + // mobile viewports do not have this feature + cy.viewport(...oneDesktopViewport()) + } + const { toggle, element } = oneOf( + { toggle: 'liquidityUsd', element: 'data-table-header-liquidityUsd' }, + { toggle: 'utilizationPercent', element: 'data-table-header-utilizationPercent' }, + ) + cy.get(`[data-testid="${element}"]`).first().scrollIntoView() + cy.get(`[data-testid="${element}"]`).should('be.visible') + cy.get(`[data-testid="btn-visibility-settings"]`).click() + cy.get(`[data-testid="visibility-toggle-${toggle}"]`).click() + cy.get(`[data-testid="${element}"]`).should('not.exist') + }) - function expandFirstRowOnMobile() { - if (breakpoint == 'mobile') { - cy.get(`[data-testid="expand-icon"]`).first().click() - cy.get(`[data-testid="data-table-expansion-row"]`).should('be.visible') + function expandRowOnMobile(index = 0) { + if (breakpoint == 'mobile') { + cy.get(`[data-testid="expand-icon"]`).eq(index).click() + cy.get(`[data-testid="data-table-expansion-row"]`).should('be.visible') + } } - } - /** - * Makes sure that the filter chips are visible during the given callback. - * On mobile, the filters are hidden behind a button and need to be expanded for some actions. - */ - function withFilterChips(callback: () => void) { - if (breakpoint != 'mobile') { - return callback() + /** + * Makes sure that the filter chips are visible during the given callback. + * On mobile, the filters are hidden behind a button and need to be expanded for some actions. + */ + function withFilterChips(callback: () => void) { + if (breakpoint != 'mobile') { + return callback() + } + cy.scrollTo(0, 0) + cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="chip-lend"]`).should('be.visible') + callback() + cy.scrollTo(0, 0) + cy.get(`[data-testid="chip-lend"]`).should('be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') } - cy.scrollTo(0, 0) - cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="chip-lend"]`).should('be.visible') - callback() - cy.scrollTo(0, 0) - cy.get(`[data-testid="chip-lend"]`).should('be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') - } - function checkLineGraphColor(type: 'lend' | 'borrow', color: string) { - // the graphs are lazy loaded, so we need to scroll to them first before checking the color - if (breakpoint != 'mobile') { - // no need to scroll on mobile, the graph is already in view after collapsing the row - cy.get(`[data-testid="line-graph-${type}"]:visible`).first().scrollIntoView() + function checkLineGraphColor(type: 'lend' | 'borrow', color: string) { + // the graphs are lazy loaded, so we need to scroll to them first before checking the color + if (breakpoint != 'mobile') { + // no need to scroll on mobile, the graph is already in view after collapsing the row + cy.get(`[data-testid="line-graph-${type}"]:visible`).first().scrollIntoView() + } + cy.get(`[data-testid="line-graph-${type}"] path`).first().should('have.attr', 'stroke', color) } - cy.get(`[data-testid="line-graph-${type}"] path`).first().should('have.attr', 'stroke', color) - } -}) + }), +) function selectChain(chain: string) { cy.get('[data-testid="multi-select-filter-chain"]').click() From bee92c2a1d15705e0d2b7a7bad3fe6446abf4fb9 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 16 Jul 2025 10:07:48 +0200 Subject: [PATCH 4/8] fix: review comment --- packages/curve-ui-kit/src/hooks/useLocalStorage.ts | 2 +- tests/cypress/e2e/all/disclaimers.cy.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts index 935309b03..1ae7f51c5 100644 --- a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts +++ b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts @@ -42,7 +42,7 @@ export const useFilterExpanded = (tableTitle: string) => export const useTableFilters = (tableTitle: string, defaultFilters: ColumnFiltersState) => useLocalStorage( `table-filters-${kebabCase(tableTitle)}`, - useMemo(() => [], []), + defaultFilters, ) export const getFavoriteMarkets = () => getFromLocalStorage('favoriteMarkets') ?? [] diff --git a/tests/cypress/e2e/all/disclaimers.cy.ts b/tests/cypress/e2e/all/disclaimers.cy.ts index bafdf268b..26ec74759 100644 --- a/tests/cypress/e2e/all/disclaimers.cy.ts +++ b/tests/cypress/e2e/all/disclaimers.cy.ts @@ -1,5 +1,12 @@ import { oneOf } from '@/support/generators' -import { LOAD_TIMEOUT, oneAppPath, oneDesktopViewport, oneTabletViewport, oneViewport } from '@/support/ui' +import { + API_LOAD_TIMEOUT, + LOAD_TIMEOUT, + oneAppPath, + oneDesktopViewport, + oneTabletViewport, + oneViewport +} from '@/support/ui' describe('Disclaimers', () => { describe('Footer link', () => { @@ -11,7 +18,7 @@ describe('Disclaimers', () => { // Navigate to risk disclaimer from footer. cy.get(`[data-testid='footer']`, LOAD_TIMEOUT).should('be.visible') cy.get(`[data-testid='footer'] a`).contains('disclaimer', { matchCase: false }).click() - cy.url(LOAD_TIMEOUT).should('match', /\/disclaimer\/?(\?tab=(lend|crvusd))?$/) + cy.url(API_LOAD_TIMEOUT).should('match', /\/disclaimer\/?(\?tab=(lend|crvusd))?$/) cy.get(`[data-testid='disclaimer']`, LOAD_TIMEOUT).should('be.visible') }) }) From d6cb8197e58b2228811abcea82fe82ef014a14f1 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 16 Jul 2025 10:11:51 +0200 Subject: [PATCH 5/8] chore: self-review --- .../curve-ui-kit/src/hooks/useLocalStorage.ts | 5 +- tests/cypress/e2e/all/disclaimers.cy.ts | 2 +- .../e2e/llamalend/llamalend-markets.cy.ts | 500 +++++++++--------- 3 files changed, 250 insertions(+), 257 deletions(-) diff --git a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts index 1ae7f51c5..af33c9899 100644 --- a/packages/curve-ui-kit/src/hooks/useLocalStorage.ts +++ b/packages/curve-ui-kit/src/hooks/useLocalStorage.ts @@ -40,10 +40,7 @@ export const useFilterExpanded = (tableTitle: string) => useLocalStorage(`filter-expanded-${kebabCase(tableTitle)}`, false) export const useTableFilters = (tableTitle: string, defaultFilters: ColumnFiltersState) => - useLocalStorage( - `table-filters-${kebabCase(tableTitle)}`, - defaultFilters, - ) + useLocalStorage(`table-filters-${kebabCase(tableTitle)}`, defaultFilters) export const getFavoriteMarkets = () => getFromLocalStorage('favoriteMarkets') ?? [] export const useFavoriteMarkets = () => { diff --git a/tests/cypress/e2e/all/disclaimers.cy.ts b/tests/cypress/e2e/all/disclaimers.cy.ts index 26ec74759..963ee92be 100644 --- a/tests/cypress/e2e/all/disclaimers.cy.ts +++ b/tests/cypress/e2e/all/disclaimers.cy.ts @@ -5,7 +5,7 @@ import { oneAppPath, oneDesktopViewport, oneTabletViewport, - oneViewport + oneViewport, } from '@/support/ui' describe('Disclaimers', () => { diff --git a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts index 4063178eb..66367b614 100644 --- a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts @@ -21,292 +21,288 @@ import { import type { GetMarketsResponse } from '@curvefi/prices-api/llamalend' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' -range(5).forEach(() => - describe(`LlamaLend Markets`, () => { - let breakpoint: Breakpoint - let width: number, height: number - let vaultData: Record +describe(`LlamaLend Markets`, () => { + let breakpoint: Breakpoint + let width: number, height: number + let vaultData: Record - beforeEach(() => { - ;[width, height, breakpoint] = oneViewport() - vaultData = createLendingVaultChainsResponse() - mockTokenPrices() - mockLendingVaults(vaultData) - mockLendingSnapshots().as('lend-snapshots') - mockMintMarkets() - mockMintSnapshots() + beforeEach(() => { + ;[width, height, breakpoint] = oneViewport() + vaultData = createLendingVaultChainsResponse() + mockTokenPrices() + mockLendingVaults(vaultData) + mockLendingSnapshots().as('lend-snapshots') + mockMintMarkets() + mockMintSnapshots() - cy.viewport(width, height) - cy.setCookie('cypress', 'true') // disable server data fetching so the app can use the mocks - cy.visit('/llamalend/ethereum/markets/', { - onBeforeLoad: (window) => window.localStorage.clear(), - ...LOAD_TIMEOUT, - }) - cy.get('[data-testid="data-table"]', LOAD_TIMEOUT).should('be.visible') + cy.viewport(width, height) + cy.setCookie('cypress', 'true') // disable server data fetching so the app can use the mocks + cy.visit('/llamalend/ethereum/markets/', { + onBeforeLoad: (window) => window.localStorage.clear(), + ...LOAD_TIMEOUT, }) + cy.get('[data-testid="data-table"]', LOAD_TIMEOUT).should('be.visible') + }) - const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) - it('should have sticky headers', () => { - cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) - cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() - cy.get('[data-testid="data-table-head"] th').eq(1).then(assertInViewport) - cy.get(`[data-testid^="pool-type-"]`).should('be.visible') // wait for the table to render + const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) + it('should have sticky headers', () => { + cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) + cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() + cy.get('[data-testid="data-table-head"] th').eq(1).then(assertInViewport) + cy.get(`[data-testid^="pool-type-"]`).should('be.visible') // wait for the table to render - // filter height changes because text wraps depending on the width - const filterHeight = { - // the height of the header changes depending on how often the description text wraps - mobile: [194, 180, 156, 144], - // on tablet, we expect 3 rows until 900px, then 2 rows - tablet: [208, 164], - // on desktop, we expect 2 rows always - desktop: [172], - }[breakpoint] - cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('be.oneOf', filterHeight) - cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', 65) + // filter height changes because text wraps depending on the width + const filterHeight = { + // the height of the header changes depending on how often the description text wraps + mobile: [194, 180, 156, 144], + // on tablet, we expect 3 rows until 900px, then 2 rows + tablet: [208, 164], + // on desktop, we expect 2 rows always + desktop: [172], + }[breakpoint] + cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('be.oneOf', filterHeight) + cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', 65) + }) + + it('should sort', () => { + const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% + if (breakpoint == 'mobile') { + cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') + cy.get('[data-testid="select-filter-sort"]').click() + cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() + cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) + cy.get(`[data-testid^="data-table-row"]`) + .eq(index) + .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) + .should('exist') + expandRowOnMobile(index) + // note: not possible currently to sort ascending + cy.get('[data-testid="metric-utilizationPercent"]').contains('99.99%', LOAD_TIMEOUT) + } else { + cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') + cy.get('[data-testid="data-table-header-utilizationPercent"]').click() + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-header-utilizationPercent"]').click() + cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) + } + }) + + // todo: retry cause this fails in large screens with small data set (laziness not triggered, everything is shown) + it('should show charts', RETRY_IN_CI, () => { + withFilterChips(() => { + cy.get(`[data-testid="chip-lend"]`).click() + cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') }) + expandRowOnMobile() + if (breakpoint != 'mobile') { + enableGraphColumn() + } + checkLineGraphColor('borrow', '#ed242f') - it('should sort', () => { - const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% + // check that scrolling loads more snapshots: + cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls1) => { + cy.get('[data-testid^="data-table-row"]') + .last() + .scrollIntoView({ offset: { top: -height / 2, left: 0 } }) // scroll to the last row, make sure it's still visible if (breakpoint == 'mobile') { - cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') - cy.get('[data-testid="select-filter-sort"]').click() - cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() - cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) - cy.get(`[data-testid^="data-table-row"]`) - .eq(index) - .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) - .should('exist') - expandRowOnMobile(index) - // note: not possible currently to sort ascending - cy.get('[data-testid="metric-utilizationPercent"]').contains('99.99%', LOAD_TIMEOUT) - } else { - cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') - cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) - cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) + cy.get(`[data-testid="expand-icon"]`).last().scrollIntoView() + cy.get(`[data-testid="expand-icon"]`).last().click() } + cy.wait('@lend-snapshots') + cy.get('[data-testid^="data-table-row"]').last().should('contain.html', 'path') // wait for the graph to render + cy.wait(range(calls1.length).map(() => '@lend-snapshots')) + cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls2) => + expect(calls2.length).to.be.greaterThan(calls1.length), + ) }) + }) - // todo: retry cause this fails in large screens with small data set (laziness not triggered, everything is shown) - it('should show charts', RETRY_IN_CI, () => { - withFilterChips(() => { - cy.get(`[data-testid="chip-lend"]`).click() - cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') - }) - expandRowOnMobile() - if (breakpoint != 'mobile') { - enableGraphColumn() - } - checkLineGraphColor('borrow', '#ed242f') + it('should find markets by text', () => { + cy.get("[data-testid='table-text-search']").type('wstETH crvUSD') + cy.scrollTo(0, 0) + // sfrxETH market is filtered out + cy.get(`[data-testid='market-link-0x136e783846ef68C8Bd00a3369F787dF8d683a696']`).should('not.exist') + // wstETH market is shown + cy.get(`[data-testid="market-link-0x37417B2238AA52D0DD2D6252d989E728e8f706e4"]`).should('exist') + }) - // check that scrolling loads more snapshots: - cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls1) => { - cy.get('[data-testid^="data-table-row"]') - .last() - .scrollIntoView({ offset: { top: -height / 2, left: 0 } }) // scroll to the last row, make sure it's still visible - if (breakpoint == 'mobile') { - cy.get(`[data-testid="expand-icon"]`).last().scrollIntoView() - cy.get(`[data-testid="expand-icon"]`).last().click() - } - cy.wait('@lend-snapshots') - cy.get('[data-testid^="data-table-row"]').last().should('contain.html', 'path') // wait for the graph to render - cy.wait(range(calls1.length).map(() => '@lend-snapshots')) - cy.get(`@lend-snapshots.all`, LOAD_TIMEOUT).then((calls2) => - expect(calls2.length).to.be.greaterThan(calls1.length), - ) - }) + it(`should allow filtering by using a slider`, () => { + const [columnId, initialFilterText] = oneOf( + ['liquidityUsd', 'Liquidity: $10,000 -'], + ['utilizationPercent', 'Utilization: 0.00% -'], + ) + cy.viewport(1200, 800) // use fixed viewport to have consistent slider width + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('contain', initialFilterText) + cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click() + /** + * Using `force: true` to bypass Cypress' element visibility check. + * The slider may have pseudo-elements that interfere with Cypress' ability to interact with it. + * We've tried alternative approaches (adding waits, adjusting click coordinates) but they didn't resolve the issue. + * The application behavior works correctly despite this test accommodation. + */ + cy.get(`[data-testid="slider-${columnId}"]`).click(60, 20, { force: true }) + cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.contain', initialFilterText) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) }) + }) - it('should find markets by text', () => { - cy.get("[data-testid='table-text-search']").type('wstETH crvUSD') - cy.scrollTo(0, 0) - // sfrxETH market is filtered out - cy.get(`[data-testid='market-link-0x136e783846ef68C8Bd00a3369F787dF8d683a696']`).should('not.exist') - // wstETH market is shown - cy.get(`[data-testid="market-link-0x37417B2238AA52D0DD2D6252d989E728e8f706e4"]`).should('exist') - }) + it('should allow filtering by chain', () => { + const chains = Object.keys(vaultData) + const chain = oneOf(...chains) + cy.get('[data-testid="multi-select-filter-chain"]').should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click() - it(`should allow filtering by using a slider`, () => { - const [columnId, initialFilterText] = oneOf( - ['liquidityUsd', 'Liquidity: $10,000 -'], - ['utilizationPercent', 'Utilization: 0.00% -'], - ) - cy.viewport(1200, 800) // use fixed viewport to have consistent slider width - cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('contain', initialFilterText) - cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click() - /** - * Using `force: true` to bypass Cypress' element visibility check. - * The slider may have pseudo-elements that interfere with Cypress' ability to interact with it. - * We've tried alternative approaches (adding waits, adjusting click coordinates) but they didn't resolve the issue. - * The application behavior works correctly despite this test accommodation. - */ - cy.get(`[data-testid="slider-${columnId}"]`).click(60, 20, { force: true }) - cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.contain', initialFilterText) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) - }) - }) + selectChain(chain) + cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid="chain-icon-${chain}"]`).should('be.visible') - it('should allow filtering by chain', () => { - const chains = Object.keys(vaultData) - const chain = oneOf(...chains) - cy.get('[data-testid="multi-select-filter-chain"]').should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click() + const otherChain = oneOf(...chains.filter((c) => c !== chain)) + selectChain(otherChain) + ;[chain, otherChain].forEach((c) => cy.get(`[data-testid="chain-icon-${c}"]`).should('be.visible')) + }) - selectChain(chain) - cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid="chain-icon-${chain}"]`).should('be.visible') + it(`should allow filtering by token`, () => { + const type = oneTokenType() + const tokenField = (type + '_token') as `${typeof type}_token` - const otherChain = oneOf(...chains.filter((c) => c !== chain)) - selectChain(otherChain) - ;[chain, otherChain].forEach((c) => cy.get(`[data-testid="chain-icon-${c}"]`).should('be.visible')) - }) + cy.get(`[data-testid="btn-expand-filters"]`).click() + const coins = vaultData.ethereum.data + .filter((d) => d.total_assets_usd - d.total_debt_usd > SMALL_POOL_TVL) + .map((d) => d[tokenField].symbol) + const coin1 = oneOf(...coins) + const coin2 = oneOf(...coins.filter((c) => c !== coin1)) + selectCoin(coin1, type) + selectCoin(coin2, type) + }) - it(`should allow filtering by token`, () => { - const type = oneTokenType() - const tokenField = (type + '_token') as `${typeof type}_token` + it('should allow filtering favorites', { scrollBehavior: false }, () => { + expandRowOnMobile() + if (breakpoint == 'desktop') { + // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) + } else { + cy.get(`[data-testid="favorite-icon"]:visible`).first().click() + } + withFilterChips(() => cy.get(`[data-testid="chip-favorites"]`).click()) + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + cy.get(`[data-testid="favorite-icon"]:visible`).should('not.exist') + cy.get(`[data-testid="favorite-icon-filled"]:visible`).click() + cy.get(`[data-testid="table-empty-row"]`).should('exist') + withFilterChips(() => cy.get(`[data-testid="reset-filter"]`).click()) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + }) - cy.get(`[data-testid="btn-expand-filters"]`).click() - const coins = vaultData.ethereum.data - .filter((d) => d.total_assets_usd - d.total_debt_usd > SMALL_POOL_TVL) - .map((d) => d[tokenField].symbol) - const coin1 = oneOf(...coins) - const coin2 = oneOf(...coins.filter((c) => c !== coin1)) - selectCoin(coin1, type) - selectCoin(coin2, type) - }) + it(`should allow filtering by market type`, () => + withFilterChips(() => + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + const [type, otherType] = shuffle('mint', 'lend') + cy.get(`[data-testid="chip-${type}"]`).click() + cy.get(`[data-testid^="pool-type-"]`).each(($el) => expect($el.attr('data-testid')).equals(`pool-type-${type}`)) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + cy.get(`[data-testid="chip-${otherType}"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', length) + }), + )) - it('should allow filtering favorites', { scrollBehavior: false }, () => { + it(`should copy the market address`, RETRY_IN_CI, () => { + if (breakpoint === 'mobile') { expandRowOnMobile() - if (breakpoint == 'desktop') { - // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force - cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) - } else { - cy.get(`[data-testid="favorite-icon"]:visible`).first().click() - } - withFilterChips(() => cy.get(`[data-testid="chip-favorites"]`).click()) - cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) - cy.get(`[data-testid="favorite-icon"]:visible`).should('not.exist') - cy.get(`[data-testid="favorite-icon-filled"]:visible`).click() - cy.get(`[data-testid="table-empty-row"]`).should('exist') - withFilterChips(() => cy.get(`[data-testid="reset-filter"]`).click()) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) - }) - - it(`should allow filtering by market type`, () => - withFilterChips(() => - cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { - const [type, otherType] = shuffle('mint', 'lend') - cy.get(`[data-testid="chip-${type}"]`).click() - cy.get(`[data-testid^="pool-type-"]`).each(($el) => - expect($el.attr('data-testid')).equals(`pool-type-${type}`), - ) - cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) - cy.get(`[data-testid="chip-${otherType}"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length', length) - }), - )) + } + // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) + range(2).forEach(() => + breakpoint === 'desktop' + ? // on desktop, the copy button is not visible until hovered - but cypress doesn't support that so use force + cy.get(`[data-testid^="copy-market-address"]`).first().click({ force: true }) + : cy.get(`[data-testid^="copy-market-address"]:visible`).first().click(), + ) + cy.get(`[data-testid="copy-confirmation"]`).should('be.visible') + }) - it(`should copy the market address`, RETRY_IN_CI, () => { - if (breakpoint === 'mobile') { - expandRowOnMobile() - } - // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) - range(2).forEach(() => - breakpoint === 'desktop' - ? // on desktop, the copy button is not visible until hovered - but cypress doesn't support that so use force - cy.get(`[data-testid^="copy-market-address"]`).first().click({ force: true }) - : cy.get(`[data-testid^="copy-market-address"]:visible`).first().click(), - ) - cy.get(`[data-testid="copy-confirmation"]`).should('be.visible') + it(`should navigate to market details`, () => { + const [type, urlRegex] = oneOf( + ['mint', /\/crvusd\/\w+\/markets\/.+\/create/], + ['lend', /\/lend\/\w+\/markets\/.+\/create/], + ) + withFilterChips(() => { + cy.get(`[data-testid="chip-${type}"]`).click() + firstRow().contains(capitalize(type)) }) + cy.get(`[data-testid^="market-link-"]`).first().click() + if (breakpoint === 'mobile') { + cy.get(`[data-testid^="llama-market-go-to-market"]:visible`).click() + } + cy.url(LOAD_TIMEOUT).should('match', urlRegex) + }) - it(`should navigate to market details`, () => { - const [type, urlRegex] = oneOf( - ['mint', /\/crvusd\/\w+\/markets\/.+\/create/], - ['lend', /\/lend\/\w+\/markets\/.+\/create/], - ) - withFilterChips(() => { - cy.get(`[data-testid="chip-${type}"]`).click() - firstRow().contains(capitalize(type)) - }) - cy.get(`[data-testid^="market-link-"]`).first().click() - if (breakpoint === 'mobile') { - cy.get(`[data-testid^="llama-market-go-to-market"]:visible`).click() - } - cy.url(LOAD_TIMEOUT).should('match', urlRegex) + it(`should allow filtering by rewards`, { scrollBehavior: false }, () => { + cy.get(`[data-testid^="data-table-row"]`).should('have.length.at.least', 1) + withFilterChips(() => { + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) }) - - it(`should allow filtering by rewards`, { scrollBehavior: false }, () => { - cy.get(`[data-testid^="data-table-row"]`).should('have.length.at.least', 1) - withFilterChips(() => { - cy.get(`[data-testid="chip-rewards"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) - }) - expandRowOnMobile() - cy.get(`[data-testid="rewards-icons"]`).should('be.visible') - withFilterChips(() => { - cy.get(`[data-testid="chip-rewards"]`).click() - cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) - }) + expandRowOnMobile() + cy.get(`[data-testid="rewards-icons"]`).should('be.visible') + withFilterChips(() => { + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) }) + }) - it('should hide columns', () => { - if (breakpoint == 'mobile') { - // mobile viewports do not have this feature - cy.viewport(...oneDesktopViewport()) - } - const { toggle, element } = oneOf( - { toggle: 'liquidityUsd', element: 'data-table-header-liquidityUsd' }, - { toggle: 'utilizationPercent', element: 'data-table-header-utilizationPercent' }, - ) - cy.get(`[data-testid="${element}"]`).first().scrollIntoView() - cy.get(`[data-testid="${element}"]`).should('be.visible') - cy.get(`[data-testid="btn-visibility-settings"]`).click() - cy.get(`[data-testid="visibility-toggle-${toggle}"]`).click() - cy.get(`[data-testid="${element}"]`).should('not.exist') - }) + it('should hide columns', () => { + if (breakpoint == 'mobile') { + // mobile viewports do not have this feature + cy.viewport(...oneDesktopViewport()) + } + const { toggle, element } = oneOf( + { toggle: 'liquidityUsd', element: 'data-table-header-liquidityUsd' }, + { toggle: 'utilizationPercent', element: 'data-table-header-utilizationPercent' }, + ) + cy.get(`[data-testid="${element}"]`).first().scrollIntoView() + cy.get(`[data-testid="${element}"]`).should('be.visible') + cy.get(`[data-testid="btn-visibility-settings"]`).click() + cy.get(`[data-testid="visibility-toggle-${toggle}"]`).click() + cy.get(`[data-testid="${element}"]`).should('not.exist') + }) - function expandRowOnMobile(index = 0) { - if (breakpoint == 'mobile') { - cy.get(`[data-testid="expand-icon"]`).eq(index).click() - cy.get(`[data-testid="data-table-expansion-row"]`).should('be.visible') - } + function expandRowOnMobile(index = 0) { + if (breakpoint == 'mobile') { + cy.get(`[data-testid="expand-icon"]`).eq(index).click() + cy.get(`[data-testid="data-table-expansion-row"]`).should('be.visible') } + } - /** - * Makes sure that the filter chips are visible during the given callback. - * On mobile, the filters are hidden behind a button and need to be expanded for some actions. - */ - function withFilterChips(callback: () => void) { - if (breakpoint != 'mobile') { - return callback() - } - cy.scrollTo(0, 0) - cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="chip-lend"]`).should('be.visible') - callback() - cy.scrollTo(0, 0) - cy.get(`[data-testid="chip-lend"]`).should('be.visible') - cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) - cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') + /** + * Makes sure that the filter chips are visible during the given callback. + * On mobile, the filters are hidden behind a button and need to be expanded for some actions. + */ + function withFilterChips(callback: () => void) { + if (breakpoint != 'mobile') { + return callback() } + cy.scrollTo(0, 0) + cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="chip-lend"]`).should('be.visible') + callback() + cy.scrollTo(0, 0) + cy.get(`[data-testid="chip-lend"]`).should('be.visible') + cy.get(`[data-testid="btn-expand-filters"]`).click({ waitForAnimations: true }) + cy.get(`[data-testid="chip-lend"]`).should('not.be.visible') + } - function checkLineGraphColor(type: 'lend' | 'borrow', color: string) { - // the graphs are lazy loaded, so we need to scroll to them first before checking the color - if (breakpoint != 'mobile') { - // no need to scroll on mobile, the graph is already in view after collapsing the row - cy.get(`[data-testid="line-graph-${type}"]:visible`).first().scrollIntoView() - } - cy.get(`[data-testid="line-graph-${type}"] path`).first().should('have.attr', 'stroke', color) + function checkLineGraphColor(type: 'lend' | 'borrow', color: string) { + // the graphs are lazy loaded, so we need to scroll to them first before checking the color + if (breakpoint != 'mobile') { + // no need to scroll on mobile, the graph is already in view after collapsing the row + cy.get(`[data-testid="line-graph-${type}"]:visible`).first().scrollIntoView() } - }), -) + cy.get(`[data-testid="line-graph-${type}"] path`).first().should('have.attr', 'stroke', color) + } +}) function selectChain(chain: string) { cy.get('[data-testid="multi-select-filter-chain"]').click() From c8c58f7adc6a4e80472796f0e5abd479e92c2952 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 16 Jul 2025 10:25:39 +0200 Subject: [PATCH 6/8] chore: increase timeout --- tests/cypress/e2e/all/header.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cypress/e2e/all/header.cy.ts b/tests/cypress/e2e/all/header.cy.ts index 95ae59b25..38a671d36 100644 --- a/tests/cypress/e2e/all/header.cy.ts +++ b/tests/cypress/e2e/all/header.cy.ts @@ -179,7 +179,7 @@ describe('Header', () => { cy.get(`[data-testid='chain-icon-${eth}']`, LOAD_TIMEOUT).should('be.visible') cy.get(`[data-testid='btn-change-chain']`).click() cy.get(`[data-testid='menu-item-chain-${arbitrum}']`).click() - cy.get(`[data-testid^='menu-item-chain-']`, LOAD_TIMEOUT).should('not.exist') + cy.get(`[data-testid^='menu-item-chain-']`, API_LOAD_TIMEOUT).should('not.exist') cy.get(`[data-testid='chain-icon-${arbitrum}']`).should('be.visible') } }) From c5c5b5549bcc2a15dbc15f1636c708616bd47f1f Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 16 Jul 2025 13:07:54 +0200 Subject: [PATCH 7/8] fix: update depth error --- .../PageLlamaMarkets/LlamaMarketsTable.tsx | 7 +++--- .../e2e/llamalend/llamalend-markets.cy.ts | 23 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx index 11e2a3f32..31144c60f 100644 --- a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx +++ b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx @@ -54,9 +54,10 @@ export const LlamaMarketsTable = ({ minLiquidity: number }) => { const { markets: data = [], hasPositions, hasFavorites } = result ?? {} - const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters(TITLE, [ - { id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }, - ]) + const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters( + TITLE, + useMemo(() => [{ id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }], [minLiquidity]), + ) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) const { columnSettings, columnVisibility, toggleVisibility, sortField } = useVisibility(sorting, result?.hasPositions) const [expanded, setExpanded] = useState({}) diff --git a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts index 66367b614..a9905374c 100644 --- a/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/llamalend/llamalend-markets.cy.ts @@ -44,7 +44,7 @@ describe(`LlamaLend Markets`, () => { cy.get('[data-testid="data-table"]', LOAD_TIMEOUT).should('be.visible') }) - const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).eq(0) + const firstRow = () => cy.get(`[data-testid^="data-table-row-"]`).first() it('should have sticky headers', () => { cy.get('[data-testid^="data-table-row"]').last().then(assertNotInViewport) cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() @@ -65,25 +65,24 @@ describe(`LlamaLend Markets`, () => { }) it('should sort', () => { - const index = 1 // there is one mint market with always 100%, we test the generated lend market with 99.99% if (breakpoint == 'mobile') { cy.get(`[data-testid="data-table-cell-liquidityUsd"]`).first().contains('$') cy.get('[data-testid="select-filter-sort"]').click() cy.get('[data-testid="menu-sort"] [value="utilizationPercent"]').click() cy.get('[data-testid="select-filter-sort"]').contains('Utilization', LOAD_TIMEOUT) cy.get(`[data-testid^="data-table-row"]`) - .eq(index) + .first() .find(`[data-testid="market-link-${HighUtilizationAddress}"]`) .should('exist') - expandRowOnMobile(index) + expandFirstRowOnMobile() // note: not possible currently to sort ascending cy.get('[data-testid="metric-utilizationPercent"]').contains('99.99%', LOAD_TIMEOUT) } else { cy.get(`[data-testid="data-table-cell-rates_borrow"]`).first().contains('%') cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('99.99%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-cell-utilizationPercent"]').first().contains('99.99%', LOAD_TIMEOUT) cy.get('[data-testid="data-table-header-utilizationPercent"]').click() - cy.get('[data-testid="data-table-cell-utilizationPercent"]').eq(index).contains('0.00%', LOAD_TIMEOUT) + cy.get('[data-testid="data-table-cell-utilizationPercent"]').first().contains('0.00%', LOAD_TIMEOUT) } }) @@ -93,7 +92,7 @@ describe(`LlamaLend Markets`, () => { cy.get(`[data-testid="chip-lend"]`).click() cy.get(`[data-testid="pool-type-mint"]`).should('not.exist') }) - expandRowOnMobile() + expandFirstRowOnMobile() if (breakpoint != 'mobile') { enableGraphColumn() } @@ -180,7 +179,7 @@ describe(`LlamaLend Markets`, () => { }) it('should allow filtering favorites', { scrollBehavior: false }, () => { - expandRowOnMobile() + expandFirstRowOnMobile() if (breakpoint == 'desktop') { // on desktop, the favorite icon is not visible until hovered - but cypress doesn't support that so use force cy.get(`[data-testid="favorite-icon"]`).first().click({ force: true }) @@ -210,7 +209,7 @@ describe(`LlamaLend Markets`, () => { it(`should copy the market address`, RETRY_IN_CI, () => { if (breakpoint === 'mobile') { - expandRowOnMobile() + expandFirstRowOnMobile() } // unfortunately we need to click twice on Chromium, the first one doesn't work (maybe due to the tooltip) range(2).forEach(() => @@ -244,7 +243,7 @@ describe(`LlamaLend Markets`, () => { cy.get(`[data-testid="chip-rewards"]`).click() cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) }) - expandRowOnMobile() + expandFirstRowOnMobile() cy.get(`[data-testid="rewards-icons"]`).should('be.visible') withFilterChips(() => { cy.get(`[data-testid="chip-rewards"]`).click() @@ -268,9 +267,9 @@ describe(`LlamaLend Markets`, () => { cy.get(`[data-testid="${element}"]`).should('not.exist') }) - function expandRowOnMobile(index = 0) { + function expandFirstRowOnMobile() { if (breakpoint == 'mobile') { - cy.get(`[data-testid="expand-icon"]`).eq(index).click() + cy.get(`[data-testid="expand-icon"]`).first().click() cy.get(`[data-testid="data-table-expansion-row"]`).should('be.visible') } } From bd43fe8512cd695a0e6e73064578df398bf7d629 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 16 Jul 2025 15:10:50 +0200 Subject: [PATCH 8/8] refactor: lift prop --- .../PageLlamaMarkets/LlamaMarketsTable.tsx | 14 ++++++++++---- apps/main/src/llamalend/PageLlamaMarkets/Page.tsx | 10 +--------- tests/cypress/support/ui.ts | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx index 31144c60f..69249f184 100644 --- a/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx +++ b/apps/main/src/llamalend/PageLlamaMarkets/LlamaMarketsTable.tsx @@ -19,6 +19,8 @@ import { SortingState, useReactTable, } from '@tanstack/react-table' +import { useUserProfileStore } from '@ui-kit/features/user-profile' +import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' import { useIsMobile, useIsTablet } from '@ui-kit/hooks/useBreakpoints' import { useSortFromQueryString } from '@ui-kit/hooks/useSortFromQueryString' import { t } from '@ui-kit/lib/i18n' @@ -42,25 +44,29 @@ const useVisibility = (sorting: SortingState, hasPositions: boolean | undefined) const TITLE = 'Llamalend Markets' // not using the t`` here as the value is used as a key in the local storage +const useDefaultLlamaFilter = (minLiquidity: number) => + useMemo(() => [{ id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }], [minLiquidity]) + export const LlamaMarketsTable = ({ onReload, result, isError, - minLiquidity, }: { onReload: () => void result: LlamaMarketsResult | undefined isError: boolean - minLiquidity: number }) => { const { markets: data = [], hasPositions, hasFavorites } = result ?? {} + + const minLiquidity = useUserProfileStore((s) => s.hideSmallPools) ? SMALL_POOL_TVL : 0 const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters( TITLE, - useMemo(() => [{ id: LlamaMarketColumnId.LiquidityUsd, value: [minLiquidity, undefined] }], [minLiquidity]), + useDefaultLlamaFilter(minLiquidity), ) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) - const { columnSettings, columnVisibility, toggleVisibility, sortField } = useVisibility(sorting, result?.hasPositions) + const { columnSettings, columnVisibility, toggleVisibility, sortField } = useVisibility(sorting, hasPositions) const [expanded, setExpanded] = useState({}) + const table = useReactTable({ columns: LLAMA_MARKET_COLUMNS, data, diff --git a/apps/main/src/llamalend/PageLlamaMarkets/Page.tsx b/apps/main/src/llamalend/PageLlamaMarkets/Page.tsx index 908362d11..66f5ef202 100644 --- a/apps/main/src/llamalend/PageLlamaMarkets/Page.tsx +++ b/apps/main/src/llamalend/PageLlamaMarkets/Page.tsx @@ -14,8 +14,6 @@ import { LendTableFooter } from '@/llamalend/PageLlamaMarkets/LendTableFooter' import { LlamaMarketsTable } from '@/llamalend/PageLlamaMarkets/LlamaMarketsTable' import { Stack } from '@mui/material' import Box from '@mui/material/Box' -import { useUserProfileStore } from '@ui-kit/features/user-profile' -import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' import { useIsTiny } from '@ui-kit/hooks/useBreakpoints' import { logSuccess } from '@ui-kit/lib' import { WithSkeleton } from '@ui-kit/shared/ui/WithSkeleton' @@ -56,7 +54,6 @@ export const LlamaMarketsPage = (props: LlamalendServerData) => { useInjectServerData(props) const { address } = useAccount() const { data, isError, isLoading } = useLlamaMarkets(address) - const minLiquidity = useUserProfileStore((s) => s.hideSmallPools) ? SMALL_POOL_TVL : 0 const showSkeleton = !data && (!isError || isLoading) // on initial render isLoading is still false return ( @@ -70,12 +67,7 @@ export const LlamaMarketsPage = (props: LlamalendServerData) => { minHeight: MinHeight.pageContent, }} > - onReload(address)} - result={data} - isError={isError} - minLiquidity={minLiquidity} - /> + onReload(address)} result={data} isError={isError} /> diff --git a/tests/cypress/support/ui.ts b/tests/cypress/support/ui.ts index c54a2aa2b..74fde3353 100644 --- a/tests/cypress/support/ui.ts +++ b/tests/cypress/support/ui.ts @@ -37,7 +37,7 @@ export const oneAppPath = () => oneOf(...([oneDexPath(), 'lend', 'dao', 'crvusd' export type AppPath = ReturnType export const LOAD_TIMEOUT = { timeout: 30000 } -export const API_LOAD_TIMEOUT = { timeout: 60000 } +export const API_LOAD_TIMEOUT = { timeout: 120000 } // unfortunately the prices API can be REAL SLOW 😭 // scrollbar in px for the test browser. Firefox behaves when headless. export const SCROLL_WIDTH = Cypress.browser.name === 'firefox' ? (Cypress.browser.isHeadless ? 12 : 0) : 15