-
Notifications
You must be signed in to change notification settings - Fork 638
Add slot filters for addresses' transactions #1024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anselsol
wants to merge
22
commits into
solana-foundation:master
Choose a base branch
from
anselsol:feat/add-slot-filters-for-programs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,337
−63
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
34dc7fc
Add slot based filtering on /address/[addy] page
anselsol 1b31cc0
Add beforeSlot filter
anselsol a604e7d
Add history provider tests
anselsol 5057d1e
Add filters component tests
anselsol 6cb93af
Fix responsive
anselsol a4521e2
Simplify slots filters guard
anselsol 1476563
Fix tests naming
anselsol 46493db
Finalize responsive beahvior to be in sync with the refrehs btn
anselsol 3a063f1
Switch to most up to date gTFA method
anselsol 34cc733
Remove token account filter to simplify filters
anselsol daa9c05
Fix greptile feedbacks
anselsol 1f13fc3
Add fallback for non triton and non helius rpcs
anselsol cc9e865
Fix foundOldest limit bug
anselsol 90c549c
Fix lint and formatting under updated codestyle rules
anselsol cd2fc20
Merge remote-tracking branch 'upstream/master' into feat/add-slot-fil…
anselsol 57269cf
Fix accounts history provider
anselsol 5e246e5
Fix tests for the updated history provider
anselsol 14b6bd5
Remove standard rpc unsupported untilSlot/beforeSlot filters in fallback
anselsol 76eaec4
Fix popup calendar icon color
anselsol 979ef69
Merge upstream/master into feat/add-slot-filters-for-programs
anselsol 40541e2
Fix FilterChip clear-button rendering as UA-chromed button
anselsol 0397b5c
Merge upstream/master into feat/add-slot-filters-for-programs
anselsol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
174 changes: 174 additions & 0 deletions
174
app/components/account/history/__tests__/HistoryFilterBar.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| /* eslint-disable no-restricted-syntax -- test assertions use RegExp for pattern matching */ | ||
| import { fireEvent, render, renderHook, screen } from '@testing-library/react'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| const replaceMock = vi.fn(); | ||
| let mockSearchString = ''; | ||
|
|
||
| vi.mock('next/navigation', () => ({ | ||
| usePathname: () => '/address/testAddress', | ||
| useRouter: () => ({ replace: replaceMock }), | ||
| useSearchParams: () => new URLSearchParams(mockSearchString), | ||
| })); | ||
|
|
||
| // Must import after mocks | ||
| import { HistoryFilterBar, useHistoryFilters } from '../HistoryFilterBar'; | ||
|
|
||
| function setSearch(search: string) { | ||
| mockSearchString = search; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| replaceMock.mockClear(); | ||
| setSearch(''); | ||
| }); | ||
|
|
||
| describe('useHistoryFilters', () => { | ||
| it('should return all-undefined when no params are set', () => { | ||
| setSearch(''); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current).toEqual({ | ||
| blockTime: undefined, | ||
| slot: undefined, | ||
| status: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('should parse both slot bounds from the gTFA filter paths', () => { | ||
| setSearch('slot.gte=100&slot.lte=200'); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current.slot).toEqual({ gte: 100, lte: 200 }); | ||
| }); | ||
|
|
||
| it('should ignore negative and non-numeric values', () => { | ||
| setSearch('slot.gte=-5&slot.lte=abc'); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current.slot).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should floor fractional values', () => { | ||
| setSearch('slot.gte=100.9'); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current.slot).toEqual({ gte: 100, lte: undefined }); | ||
| }); | ||
|
|
||
| it('should parse the status enum, rejecting unknown values', () => { | ||
| setSearch('status=failed'); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current).toMatchObject({ status: 'failed' }); | ||
|
|
||
| setSearch('status=bogus'); | ||
| const { result: rejected } = renderHook(() => useHistoryFilters()); | ||
| expect(rejected.current).toMatchObject({ status: undefined }); | ||
| }); | ||
|
|
||
| it('should parse block-time bounds from the gTFA filter paths', () => { | ||
| setSearch('blockTime.gte=1700000000&blockTime.lte=1700100000'); | ||
| const { result } = renderHook(() => useHistoryFilters()); | ||
| expect(result.current.blockTime).toEqual({ gte: 1_700_000_000, lte: 1_700_100_000 }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('HistoryFilterBar', () => { | ||
| it('should show a single Filters button when no filter is active', () => { | ||
| render(<HistoryFilterBar />); | ||
| expect(screen.getByRole('button', { name: /^Filters$/ })).toBeInTheDocument(); | ||
| expect(screen.queryByText(/Slot ≥:/)).not.toBeInTheDocument(); | ||
| expect(screen.queryByText(/Slot ≤:/)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render a chip per active slot bound', () => { | ||
| render(<HistoryFilterBar slot={{ gte: 100, lte: 2_000_000 }} />); | ||
| expect(screen.getByText(/Slot ≥:\s*100/)).toBeInTheDocument(); | ||
| // Use a regex since jsdom's toLocaleString thousand separator varies by ICU build. | ||
| expect(screen.getByText(/Slot ≤:\s*2[,.\s ]?000[,.\s ]?000/)).toBeInTheDocument(); | ||
| expect(screen.getByRole('button', { name: /Edit filters/ })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should clear a single slot bound when its chip × is clicked', () => { | ||
| setSearch('slot.gte=100&slot.lte=200'); | ||
| render(<HistoryFilterBar slot={{ gte: 100, lte: 200 }} />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /Clear slot ≥ filter/ })); | ||
|
|
||
| expect(replaceMock).toHaveBeenCalledTimes(1); | ||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toBe('/address/testAddress?slot.lte=200'); | ||
| }); | ||
|
|
||
| it('should apply both slot bounds from the popover', () => { | ||
| render(<HistoryFilterBar />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /^Filters$/ })); | ||
| fireEvent.change(screen.getByPlaceholderText(/lower bound/), { target: { value: '100' } }); | ||
| fireEvent.change(screen.getByPlaceholderText(/upper bound/), { target: { value: '500' } }); | ||
| fireEvent.click(screen.getByRole('button', { name: /^Apply$/ })); | ||
|
|
||
| expect(replaceMock).toHaveBeenCalledTimes(1); | ||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toMatch(/slot\.gte=100/); | ||
| expect(href).toMatch(/slot\.lte=500/); | ||
| }); | ||
|
|
||
| it('should preserve unrelated query params when updating the URL', () => { | ||
| setSearch('cluster=devnet&slot.gte=100'); | ||
| render(<HistoryFilterBar slot={{ gte: 100 }} />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /Clear slot ≥ filter/ })); | ||
|
|
||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toBe('/address/testAddress?cluster=devnet'); | ||
| }); | ||
|
|
||
| it('should disable Apply and show an error when slot ≥ exceeds slot ≤', () => { | ||
| render(<HistoryFilterBar />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /^Filters$/ })); | ||
| fireEvent.change(screen.getByPlaceholderText(/lower bound/), { target: { value: '500' } }); | ||
| fireEvent.change(screen.getByPlaceholderText(/upper bound/), { target: { value: '100' } }); | ||
|
|
||
| const apply = screen.getByRole('button', { name: /^Apply$/ }); | ||
| expect(apply).toBeDisabled(); | ||
| expect(screen.getByText(/Slot ≥ must be/)).toBeInTheDocument(); | ||
|
|
||
| fireEvent.click(apply); | ||
| expect(replaceMock).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should apply the status select', () => { | ||
| render(<HistoryFilterBar />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /^Filters$/ })); | ||
| fireEvent.change(screen.getByLabelText('Status'), { target: { value: 'succeeded' } }); | ||
| fireEvent.click(screen.getByRole('button', { name: /^Apply$/ })); | ||
|
|
||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toMatch(/status=succeeded/); | ||
| }); | ||
|
|
||
| it('should render a chip for the status filter', () => { | ||
| render(<HistoryFilterBar status="failed" />); | ||
| expect(screen.getByText(/Status:\s*Failed/)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should clear a single non-slot filter via its chip', () => { | ||
| setSearch('status=failed&slot.lte=200'); | ||
| render(<HistoryFilterBar slot={{ lte: 200 }} status="failed" />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /Clear status filter/ })); | ||
|
|
||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toBe('/address/testAddress?slot.lte=200'); | ||
| }); | ||
|
|
||
| it('should clear all filters via Clear all', () => { | ||
| setSearch('slot.gte=100&slot.lte=500'); | ||
| render(<HistoryFilterBar slot={{ gte: 100, lte: 500 }} />); | ||
|
|
||
| fireEvent.click(screen.getByRole('button', { name: /Edit filters/ })); | ||
| fireEvent.click(screen.getByRole('button', { name: /Clear all/ })); | ||
|
|
||
| const [href] = replaceMock.mock.calls[0]; | ||
| expect(href).toBe('/address/testAddress'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.