diff --git a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx index b61e74a3b..d68840dcb 100644 --- a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx +++ b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx @@ -84,6 +84,7 @@ const TableSettings = ({ searchText={searchParams.searchText} handleInputChange={(val) => updatePath({ searchText: val })} handleClose={() => updatePath({ searchText: '' })} + testId="search-pools" /> </div> diff --git a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx index 2e018f346..56168cf50 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx @@ -2,7 +2,7 @@ import Stack from '@mui/material/Stack' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { TableFilters, useColumnFilters } from '@ui-kit/shared/ui/TableFilters' import { t } from '@lingui/macro' -import { CompactUsdCell, LineGraphCell, PoolTitleCell, UtilizationCell } from './cells' +import { CompactUsdCell, LineGraphCell, PoolTitleCell, RateCell, UtilizationCell } from './cells' import { DataTable } from '@ui-kit/shared/ui/DataTable' import { LlamaMarket } from '@/loan/entities/llama-markets' import { @@ -16,11 +16,7 @@ import { import { LendingMarketsFilters } from '@/loan/components/PageLlamaMarkets/LendingMarketsFilters' import { useSortFromQueryString } from '@ui-kit/hooks/useSortFromQueryString' import { DeepKeys } from '@tanstack/table-core/build/lib/utils' -import { - isFeatureVisible, - useVisibilitySettings, - VisibilityGroup, -} from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' +import { useVisibilitySettings, VisibilityGroup } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' const { ColumnWidth, Spacing, MaxWidth } = SizesAndSpaces @@ -43,17 +39,32 @@ const columns = [ size: ColumnWidth.lg, }), columnHelper.accessor('rates.borrow', { - header: t`7D Borrow Rate`, - cell: (c) => <LineGraphCell market={c.row.original} type="borrow" showChart={isFeatureVisible(c, borrowChartId)} />, + header: t`7D Avg Borrow Rate`, + cell: (c) => <RateCell market={c.row.original} type="borrow" />, meta: { type: 'numeric' }, + size: ColumnWidth.sm, + }), + columnHelper.accessor('rates.borrow', { + id: borrowChartId, + header: t`7D Borrow Rate Chart`, + cell: (c) => <LineGraphCell market={c.row.original} type="borrow" />, size: ColumnWidth.md, + enableSorting: false, }), columnHelper.accessor('rates.lend', { - header: t`7D Supply Yield`, - cell: (c) => <LineGraphCell market={c.row.original} type="lend" showChart={isFeatureVisible(c, lendChartId)} />, + header: t`7D Avg Supply Yield`, + cell: (c) => <RateCell market={c.row.original} type="lend" />, meta: { type: 'numeric' }, + size: ColumnWidth.sm, + sortUndefined: 'last', + }), + columnHelper.accessor('rates.lend', { + id: lendChartId, + header: t`7D Supply Yield Chart`, + cell: (c) => <LineGraphCell market={c.row.original} type="lend" />, size: ColumnWidth.md, sortUndefined: 'last', + enableSorting: false, }), columnHelper.accessor('utilizationPercent', { header: t`Utilization`, @@ -79,17 +90,17 @@ const DEFAULT_VISIBILITY: VisibilityGroup[] = [ { label: t`Markets`, options: [ - { label: t`Available Liquidity`, id: 'totalSupplied.usdTotal', active: true, type: 'column' }, - { label: t`Utilization`, id: 'utilizationPercent', active: true, type: 'column' }, + { label: t`Available Liquidity`, id: 'totalSupplied.usdTotal', active: true }, + { label: t`Utilization`, id: 'utilizationPercent', active: true }, ], }, { label: t`Borrow`, - options: [{ label: t`Chart`, id: borrowChartId, active: true, type: 'feature' }], + options: [{ label: t`Chart`, id: borrowChartId, active: true }], }, { label: t`Lend`, - options: [{ label: t`Chart`, id: lendChartId, active: true, type: 'feature' }], + options: [{ label: t`Chart`, id: lendChartId, active: true }], }, ] @@ -103,8 +114,7 @@ export const LendingMarketsTable = ({ headerHeight: string }) => { const [columnFilters, columnFiltersById, setColumnFilter] = useColumnFilters() - const { columnSettings, columnVisibility, featureVisibility, toggleVisibility } = - useVisibilitySettings(DEFAULT_VISIBILITY) + const { columnSettings, columnVisibility, toggleVisibility } = useVisibilitySettings(DEFAULT_VISIBILITY) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) const table = useReactTable({ @@ -113,7 +123,7 @@ export const LendingMarketsTable = ({ getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), - state: { sorting, columnVisibility, featureVisibility, columnFilters }, + state: { sorting, columnVisibility, columnFilters }, onSortingChange, maxMultiSortColCount: 3, // allow 3 columns to be sorted at once }) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx index 78da4d4fd..f565eba19 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/LineGraphCell.tsx @@ -1,21 +1,15 @@ -import { LendingSnapshot, useLendingSnapshots } from '@/loan/entities/lending-snapshots' -import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' +import { LlamaMarket } from '@/loan/entities/llama-markets' import { Line, LineChart, YAxis } from 'recharts' import { useTheme } from '@mui/material/styles' import { DesignSystem } from '@ui-kit/themes/design' -import Stack from '@mui/material/Stack' import Skeleton from '@mui/material/Skeleton' import Typography from '@mui/material/Typography' import { t } from '@lingui/macro' -import { useMemo } from 'react' -import { meanBy } from 'lodash' import Box from '@mui/material/Box' -import { useCrvUsdSnapshots } from '@/loan/entities/crvusd' +import { GraphType, useSnapshots } from '../hooks/useSnapshots' const graphSize = { width: 172, height: 48 } -type GraphType = 'borrow' | 'lend' - /** * Get the color for the line graph. Will be green if the last value is higher than the first, red if lower, and blue if equal. */ @@ -34,65 +28,40 @@ const calculateDomain = return [first - diff, first + diff] } -type LineGraphCellProps = { +type RateCellProps = { market: LlamaMarket type: GraphType - showChart: boolean // chart is hidden depending on the chart settings -} - -function useSnapshots({ address, blockchainId, controllerAddress, type: marketType }: LlamaMarket, type: GraphType) { - const isPool = marketType == LlamaMarketType.Pool - const showMintGraph = !isPool && type === 'borrow' - const contractAddress = isPool ? controllerAddress : address - const params = { blockchainId: blockchainId, contractAddress } - const { data: poolSnapshots, isLoading: poolIsLoading } = useLendingSnapshots(params, isPool) - const { data: mintSnapshots, isLoading: mintIsLoading } = useCrvUsdSnapshots(params, showMintGraph) - if (isPool) { - return { snapshots: poolSnapshots, isLoading: poolIsLoading, snapshotKey: `${type}_apy` as const } - } - return { snapshots: showMintGraph ? mintSnapshots : null, isLoading: mintIsLoading, snapshotKey: 'rate' as const } } /** * Line graph cell that displays the average historical APY for a vault and a given type (borrow or lend). */ -export const LineGraphCell = ({ market, type, showChart }: LineGraphCellProps) => { - const { snapshots, snapshotKey, isLoading } = useSnapshots(market, type) - const currentValue = market.rates[type] - const rate = useMemo( - () => (snapshots?.length ? meanBy(snapshots, (row) => row[snapshotKey]) : currentValue), - [snapshots, currentValue, snapshotKey], - ) +export const LineGraphCell = ({ market, type }: RateCellProps) => { + const { snapshots, snapshotKey, isLoading, rate } = useSnapshots(market, type) const { design } = useTheme() if (rate == null) { return '-' } - return ( - <Stack direction="row" alignItems="center" justifyContent="end" gap={3} data-testid={`line-graph-cell-${type}`}> - {rate.toPrecision(4)}% - {showChart && ( - <Box data-testid={`line-graph-${type}`}> - {snapshots?.length ? ( - <LineChart data={snapshots} {...graphSize} compact> - <YAxis hide type="number" domain={calculateDomain(snapshots[0][snapshotKey])} /> - <Line - type="monotone" - dataKey={snapshotKey} - stroke={getColor(design, snapshots, snapshotKey)} - strokeWidth={1} - dot={<></>} - /> - </LineChart> - ) : isLoading ? ( - <Skeleton {...graphSize} /> - ) : ( - <Typography sx={{ ...graphSize, alignContent: 'center', textAlign: 'left' }} variant="bodyXsBold"> - {t`No historical data`} - </Typography> - )} - </Box> + <Box data-testid={`line-graph-${type}`}> + {snapshots?.length ? ( + <LineChart data={snapshots} {...graphSize} compact> + <YAxis hide type="number" domain={calculateDomain(snapshots[0][snapshotKey] as number)} /> + <Line + type="monotone" + dataKey={snapshotKey} + stroke={getColor(design, snapshots, snapshotKey)} + strokeWidth={1} + dot={<></>} + /> + </LineChart> + ) : isLoading ? ( + <Skeleton {...graphSize} /> + ) : ( + <Typography sx={{ ...graphSize, alignContent: 'center', textAlign: 'left' }} variant="bodyXsBold"> + {t`No historical data`} + </Typography> )} - </Stack> + </Box> ) } diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx new file mode 100644 index 000000000..c2504d827 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx @@ -0,0 +1,8 @@ +import { LlamaMarket } from '@/loan/entities/llama-markets' +import { useSnapshots } from '../hooks/useSnapshots' +import { GraphType } from '@/loan/components/PageLlamaMarkets/hooks/useSnapshots' + +export const RateCell = ({ market, type }: { market: LlamaMarket; type: GraphType }) => { + const { rate } = useSnapshots(market, type) + return rate == null ? '-' : `${rate.toPrecision(4)}%` +} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts b/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts index 7fb9d0a6f..8c59115eb 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts @@ -1,4 +1,5 @@ export * from './CompactUsdCell' export * from './PoolTitleCell' export * from './LineGraphCell' +export * from './RateCell' export * from './UtilizationCell' diff --git a/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts new file mode 100644 index 000000000..c39f17f36 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts @@ -0,0 +1,46 @@ +import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' +import { LendingSnapshot, useLendingSnapshots } from '@/loan/entities/lending-snapshots' +import { CrvUsdSnapshot, useCrvUsdSnapshots } from '@/loan/entities/crvusd' +import { useMemo } from 'react' +import { meanBy } from 'lodash' + +export type GraphType = 'borrow' | 'lend' + +type UseSnapshotsResult<T> = { + snapshots: T[] | null + isLoading: boolean + snapshotKey: keyof T + rate: number | null +} + +export function useSnapshots<T = CrvUsdSnapshot | LendingSnapshot>( + { address, blockchainId, controllerAddress, type: marketType, rates }: LlamaMarket, + type: GraphType, +): UseSnapshotsResult<T> { + const isPool = marketType == LlamaMarketType.Pool + const showMintGraph = !isPool && type === 'borrow' + const contractAddress = isPool ? controllerAddress : address + const params = { blockchainId: blockchainId, contractAddress } + const { data: poolSnapshots, isLoading: poolIsLoading } = useLendingSnapshots(params, isPool) + const { data: mintSnapshots, isLoading: mintIsLoading } = useCrvUsdSnapshots(params, showMintGraph) + + const currentValue = rates[type] + const { snapshots, isLoading, snapshotKey } = isPool + ? { + snapshots: poolSnapshots ?? null, + isLoading: poolIsLoading, + snapshotKey: `${type}_apy` as const, + } + : { + snapshots: (showMintGraph && mintSnapshots) || null, + isLoading: mintIsLoading, + snapshotKey: 'rate' as const, + } + + const rate = useMemo( + () => (snapshots?.length ? meanBy(snapshots as T[], (row) => row[snapshotKey as keyof T]) : (currentValue ?? null)), + [snapshots, currentValue, snapshotKey], + ) + + return { snapshots, isLoading, snapshotKey, rate } as UseSnapshotsResult<T> +} diff --git a/apps/main/src/loan/entities/crvusd.ts b/apps/main/src/loan/entities/crvusd.ts index c4b4f926c..aad860668 100644 --- a/apps/main/src/loan/entities/crvusd.ts +++ b/apps/main/src/loan/entities/crvusd.ts @@ -1,56 +1,20 @@ -import { ContractParams, PoolParams, PoolQuery, queryFactory, rootKeys } from '@ui-kit/lib/model/query' +import { ContractParams, ContractQuery, queryFactory, rootKeys } from '@ui-kit/lib/model/query' import { contractValidationSuite } from '@ui-kit/lib/model/query/contract-validation' +import { getSnapshots } from '@curvefi/prices-api/crvusd' +import { Chain } from '@curvefi/prices-api' +import type { Snapshot } from '@curvefi/prices-api/crvusd/models' -type CrvUsdSnapshotFromApi = { - rate: number - borrow_apy: number - lend_apy: number - liquidation_discount: number - loan_discount: number - n_loans: number - price_oracle: number - amm_price: number - base_price: number - total_debt: number - total_assets: number - total_debt_usd: number - total_assets_usd: number - minted: number - redeemed: number - minted_usd: number - redeemed_usd: number - min_band: number - max_band: number - collateral_balance: number - borrowed_balance: number - collateral_balance_usd: number - borrowed_balance_usd: number - sum_debt_squared: number - timestamp: string -} - -type CrvUsdSnapshotsFromApi = { - chain: string - market_id: number - data: CrvUsdSnapshotFromApi[] -} - -export const _getCrvUsdSnapshots = async ({ - blockchainId, - contractAddress, -}: ContractParams): Promise<CrvUsdSnapshotFromApi[]> => { - const url = `https://prices.curve.fi/v1/crvusd/markets/${blockchainId}/${contractAddress}/snapshots` - const response = await fetch(url) - const { data } = (await response.json()) as CrvUsdSnapshotsFromApi - if (!data) { - throw new Error('Failed to fetch crvUSD snapshots') - } - return data -} +export type CrvUsdSnapshot = Snapshot export const { useQuery: useCrvUsdSnapshots } = queryFactory({ queryKey: (params: ContractParams) => [...rootKeys.contract(params), 'crvUsd', 'snapshots'] as const, - queryFn: _getCrvUsdSnapshots, - staleTime: '1d', + queryFn: async ({ blockchainId, contractAddress }: ContractQuery): Promise<CrvUsdSnapshot[]> => { + const snapshots = await getSnapshots(blockchainId as Chain, contractAddress) + return snapshots.map((snapshot) => ({ + ...snapshot, + rate: snapshot.rate * 100, // Convert to percentage for consistency with lending snapshots + })) + }, + staleTime: '10m', validationSuite: contractValidationSuite, }) diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable.tsx index 8ea0730a5..58d0a5d92 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable.tsx @@ -97,16 +97,15 @@ const HeaderCell = <T extends unknown>({ header }: { header: Header<T, unknown> variant="tableHeaderS" > {flexRender(column.columnDef.header, header.getContext())} - {canSort && ( - <ArrowDownIcon - sx={{ - ...(sort === 'asc' && { transform: `rotate(180deg)` }), - verticalAlign: 'text-bottom', - fontSize: sort ? 20 : 0, - transition: `transform ${TransitionFunction}, font-size ${TransitionFunction}`, - }} - /> - )} + <ArrowDownIcon + sx={{ + ...(sort === 'asc' && { transform: `rotate(180deg)` }), + verticalAlign: 'text-bottom', + fontSize: sort ? 20 : 0, + transition: `transform ${TransitionFunction}, font-size ${TransitionFunction}`, + visibility: canSort ? 'visible' : 'hidden', // render it invisible to avoid layout shift + }} + /> </Typography> ) ) diff --git a/packages/curve-ui-kit/src/shared/ui/TableVisibilitySettingsPopover.tsx b/packages/curve-ui-kit/src/shared/ui/TableVisibilitySettingsPopover.tsx index f90fc1ef3..d52e7442f 100644 --- a/packages/curve-ui-kit/src/shared/ui/TableVisibilitySettingsPopover.tsx +++ b/packages/curve-ui-kit/src/shared/ui/TableVisibilitySettingsPopover.tsx @@ -5,11 +5,9 @@ import Switch from '@mui/material/Switch' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { useCallback, useMemo, useState } from 'react' import { FormControlLabel } from '@mui/material' -import { CellContext } from '@tanstack/react-table' export type VisibilityOption = { id: string - type: 'column' | 'feature' // columns hide the whole column, features hide a specific feature in a column active: boolean label: string } @@ -78,13 +76,14 @@ export const TableVisibilitySettingsPopover = ({ </Popover> ) -const flatten = (visibilitySettings: VisibilityGroup[], type: VisibilityOption['type']): Record<string, boolean> => +/** + * Converts a grouped visibility settings object to a flat object with column ids as keys and visibility as values. + */ +const flatten = (visibilitySettings: VisibilityGroup[]): Record<string, boolean> => visibilitySettings.reduce( (acc, group) => ({ ...acc, - ...group.options - .filter((option) => option.type === type) - .reduce((acc, { active, id }) => ({ ...acc, [cleanColumnId(id)]: active }), {}), + ...group.options.reduce((acc, { active, id }) => ({ ...acc, [cleanColumnId(id)]: active }), {}), }), {}, ) @@ -96,7 +95,7 @@ export const useVisibilitySettings = (groups: VisibilityGroup[]) => { /** current visibility settings in grouped format */ const [visibilitySettings, setVisibilitySettings] = useState(groups) - /** toggle visibility of a column or feature by its id */ + /** toggle visibility of a column by its id */ const toggleColumnVisibility = useCallback( (id: string): void => setVisibilitySettings((prev) => @@ -109,18 +108,11 @@ export const useVisibilitySettings = (groups: VisibilityGroup[]) => { ) /** current column visibility state as used internally by tanstack */ - const columnVisibility = useMemo(() => flatten(visibilitySettings, 'column'), [visibilitySettings]) - - /** current feature visibility state as used by `isFeatureVisible` */ - const featureVisibility = useMemo(() => flatten(visibilitySettings, 'feature'), [visibilitySettings]) + const columnVisibility = useMemo(() => flatten(visibilitySettings), [visibilitySettings]) return { columnSettings: visibilitySettings, columnVisibility, - featureVisibility, toggleVisibility: toggleColumnVisibility, } } - -export const isFeatureVisible = <TData, TValue>(c: CellContext<TData, TValue>, featureId: string) => - c.table.getState().featureVisibility[featureId] diff --git a/packages/curve-ui-kit/src/shared/ui/tanstack-table.overrides.d.ts b/packages/curve-ui-kit/src/shared/ui/tanstack-table.overrides.d.ts index 06821bd5d..22422cf67 100644 --- a/packages/curve-ui-kit/src/shared/ui/tanstack-table.overrides.d.ts +++ b/packages/curve-ui-kit/src/shared/ui/tanstack-table.overrides.d.ts @@ -10,9 +10,4 @@ declare module '@tanstack/table-core' { hidden?: boolean variant?: TypographyVariantKey } - - /** Tanstack only supports hiding the whole column, but we want to hide features inside the column */ - interface VisibilityTableState { - featureVisibility: VisibilityState - } } diff --git a/packages/ui/src/SearchInput/SearchListInput.tsx b/packages/ui/src/SearchInput/SearchListInput.tsx index d32f733f0..ef7a730a0 100644 --- a/packages/ui/src/SearchInput/SearchListInput.tsx +++ b/packages/ui/src/SearchInput/SearchListInput.tsx @@ -9,6 +9,7 @@ interface Props extends React.InputHTMLAttributes<HTMLInputElement> { searchText: string handleInputChange(val: string): void handleClose(): void + testId?: string } function SearchListInput({ className = '', searchText, handleInputChange, handleClose, ...inputProps }: Props) { diff --git a/tests/cypress/e2e/all/header.cy.ts b/tests/cypress/e2e/all/header.cy.ts index df1ceb01e..b58f39a2a 100644 --- a/tests/cypress/e2e/all/header.cy.ts +++ b/tests/cypress/e2e/all/header.cy.ts @@ -75,7 +75,7 @@ describe('Header', () => { }) it('should change chains', () => { - if (['loan', 'dao'].includes(appPath)) { + if (['crvusd', 'dao'].includes(appPath)) { cy.get(`[data-testid='btn-change-chain']`).click() cy.get(`[data-testid='alert-eth-only']`).should('be.visible') cy.get("[data-testid='app-link-main']").invoke('attr', 'href').should('eq', `${mainAppUrl}/#/ethereum`) @@ -140,7 +140,7 @@ describe('Header', () => { }) it('should change chains', () => { - if (['loan', 'dao'].includes(appPath)) { + if (['crvusd', 'dao'].includes(appPath)) { cy.get(`[data-testid='btn-change-chain']`).click() cy.get(`[data-testid='alert-eth-only']`).should('be.visible') cy.get(`[data-testid='menu-toggle']`).click() @@ -157,7 +157,12 @@ describe('Header', () => { }) function waitIsLoaded(appPath: AppPath) { - const testId = appPath == 'dao' ? 'proposal-title' : 'btn-connect-prompt' + const testId = { + dao: 'proposal-title', + crvusd: 'btn-connect-prompt', + lend: 'btn-connect-prompt', + dex: 'inp-search-pools', + }[appPath || 'dex'] cy.get(`[data-testid='${testId}']`).should('be.visible') // wait for loading } diff --git a/tests/cypress/e2e/loan/llamalend-markets.cy.ts b/tests/cypress/e2e/loan/llamalend-markets.cy.ts index bc0ec55d3..557c23da9 100644 --- a/tests/cypress/e2e/loan/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/loan/llamalend-markets.cy.ts @@ -38,8 +38,8 @@ describe('LlamaLend Markets', () => { it('should show graphs', () => { const [green, red] = [isDarkMode ? '#32ce79' : '#167d4a', '#ed242f'] - cy.get('[data-testid="line-graph-cell-lend"] path').first().should('have.attr', 'stroke', green) - cy.get('[data-testid="line-graph-cell-borrow"] path').first().should('have.attr', 'stroke', red) + cy.get('[data-testid="line-graph-lend"] path').first().should('have.attr', 'stroke', green) + cy.get('[data-testid="line-graph-borrow"] path').first().should('have.attr', 'stroke', red) // check that scrolling loads more snapshots: cy.get(`@snapshots.all`).then((calls1) => {