Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions __tests__/ContractCardAccessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import ContractCard from '../components/ContractCard'
import { WatchedContract } from '@/types'

const mockContract: WatchedContract = {
id: 'test-contract-1',
label: 'Test Escrow Contract',
contract_id: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7',
network: 'testnet',
rules: [
{ type: 'AnyTransaction' },
],
webhook_url: 'https://example.com/webhook',
created_at: 1700000000000,
updated_at: 1700000000000,
enabled: true,
}

describe('ContractCard Accessibility in Selection Mode (Issue #217)', () => {
it('renders a label element instead of a div role="button" when selectable', () => {
const onToggleSelect = jest.fn()
const { container } = render(
<ContractCard
contract={mockContract}
selectable={true}
selected={false}
onToggleSelect={onToggleSelect}
/>
)

// Ensure there is no div with role="button" nesting the checkbox
expect(screen.queryByRole('button', { name: /select/i })).not.toBeInTheDocument()
expect(container.querySelector('[role="button"]')).toBeNull()

// The root element must be a <label>
const label = container.querySelector('label')
expect(label).toBeInTheDocument()
expect(label).toHaveAttribute('id', 'contract-test-contract-1')

// The checkbox must be present and accessible
const checkbox = screen.getByRole('checkbox', {
name: `Select ${mockContract.label}`,
})
expect(checkbox).toBeInTheDocument()
expect(checkbox).not.toBeChecked()

// Clicking the checkbox calls onToggleSelect
fireEvent.click(checkbox)
expect(onToggleSelect).toHaveBeenCalledWith(mockContract.id)
})

it('toggles selection when clicking the label', () => {
const onToggleSelect = jest.fn()
const { container } = render(
<ContractCard
contract={mockContract}
selectionMode={true}
selected={false}
onToggleSelected={onToggleSelect}
/>
)

const checkbox = screen.getByRole('checkbox', {
name: `Select ${mockContract.label}`,
})
expect(checkbox).not.toBeChecked()

// Trigger change via checkbox (label clicks forward to input)
fireEvent.change(checkbox, { target: { checked: true } })
expect(onToggleSelect).toHaveBeenCalledWith(mockContract.id)
})

it('renders a link when not in selection mode', () => {
const { container } = render(
<ContractCard
contract={mockContract}
selectable={false}
/>
)

const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', `/contracts/${mockContract.id}`)
expect(container.querySelector('input[type="checkbox"]')).toBeNull()
})
})
122 changes: 122 additions & 0 deletions __tests__/ContractsPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,4 +674,126 @@ describe('contracts page — bulk delete API sync (issue #215)', () => {
expect(mockSyncDelete).toHaveBeenCalledWith('fail-id')
expect(failCount).toBe(1)
})
})

describe('contracts page — rule type and paused state filtering (issue #219)', () => {
type ContractWithRules = {
id: string
label: string
enabled?: boolean
rules: { type: string }[]
}

const contracts: ContractWithRules[] = [
{
id: 'c1',
label: 'Admin Escrow',
enabled: true,
rules: [{ type: 'AdminFunctionCalled' }, { type: 'LargeTransfer' }],
},
{
id: 'c2',
label: 'Paused Token',
enabled: false,
rules: [{ type: 'FunctionCalled' }],
},
{
id: 'c3',
label: 'Active Failures Watcher',
enabled: true,
rules: [{ type: 'TransactionFailed' }],
},
]

function filterContracts(
list: ContractWithRules[],
status: 'all' | 'active' | 'paused',
ruleTypes: string[]
): ContractWithRules[] {
return list.filter((c) => {
const isPaused = c.enabled === false
if (status === 'active' && isPaused) return false
if (status === 'paused' && !isPaused) return false

if (ruleTypes.length > 0) {
const matchesRule = c.rules.some((r) => ruleTypes.includes(r.type))
if (!matchesRule) return false
}
return true
})
}

it('filters by active status only', () => {
const active = filterContracts(contracts, 'active', [])
expect(active.map((c) => c.id)).toEqual(['c1', 'c3'])
})

it('filters by paused status only', () => {
const paused = filterContracts(contracts, 'paused', [])
expect(paused.map((c) => c.id)).toEqual(['c2'])
})

it('filters by single rule type', () => {
const admin = filterContracts(contracts, 'all', ['AdminFunctionCalled'])
expect(admin.map((c) => c.id)).toEqual(['c1'])
})

it('filters by multiple rule types (union matching)', () => {
const matched = filterContracts(contracts, 'all', ['AdminFunctionCalled', 'TransactionFailed'])
expect(matched.map((c) => c.id)).toEqual(['c1', 'c3'])
})

it('combines status and rule type filters', () => {
const activeAdmin = filterContracts(contracts, 'active', ['AdminFunctionCalled'])
expect(activeAdmin.map((c) => c.id)).toEqual(['c1'])

const pausedAdmin = filterContracts(contracts, 'paused', ['AdminFunctionCalled'])
expect(pausedAdmin).toHaveLength(0)
})

it('serializes and parses status and ruleTypes in URL params', () => {
const params = new URLSearchParams()
params.set('status', 'paused')
params.set('ruleTypes', 'AdminFunctionCalled,LargeTransfer')

expect(params.get('status')).toBe('paused')
expect(params.get('ruleTypes')?.split(',')).toEqual(['AdminFunctionCalled', 'LargeTransfer'])
})
})

describe('contracts page — refresh failure banner and retry (issue #218)', () => {
it('handles refresh failure result with error banner state', async () => {
const mockRefresh = jest.fn().mockResolvedValue({
contracts: [{ id: 'c1', label: 'Cached Contract' }],
error: 'Network connection failed (503)',
})

const res = await mockRefresh()
expect(res.contracts).toHaveLength(1)
expect(res.error).toBe('Network connection failed (503)')

// Simulating banner state
let refreshError: string | null = res.error
let bannerDismissed = false

const dismissBanner = () => {
bannerDismissed = true
}

expect(refreshError).toBeTruthy()
expect(bannerDismissed).toBe(false)

dismissBanner()
expect(bannerDismissed).toBe(true)

// Retry resets dismissed state and triggers refresh again
const retry = async () => {
bannerDismissed = false
return mockRefresh()
}

const retryRes = await retry()
expect(mockRefresh).toHaveBeenCalledTimes(2)
expect(retryRes.contracts).toHaveLength(1)
})
})
Loading