Skip to content
Open
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
23 changes: 23 additions & 0 deletions src/components/ContactBook/AddContact.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { AddContact } from './AddContact'

describe('AddContact accessible labeling', () => {
it('associates inputs with accessible labels', () => {
render(<AddContact onAdd={vi.fn()} onCancel={vi.fn()} />)

expect(screen.getByLabelText(/contact name/i)).toBeInTheDocument()
expect(screen.getByLabelText(/stellar address/i)).toBeInTheDocument()
})

it('validates required fields on submit', () => {
const onAdd = vi.fn()
render(<AddContact onAdd={onAdd} onCancel={vi.fn()} />)

fireEvent.click(screen.getByRole('button', { name: /add/i }))
expect(screen.getByText(/name is required/i)).toBeInTheDocument()
expect(screen.getByText(/invalid stellar address/i)).toBeInTheDocument()
expect(onAdd).not.toHaveBeenCalled()
})
})
41 changes: 31 additions & 10 deletions src/components/ContactBook/AddContact.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import { isValidStellarAddress } from '../../utils/stellar'
import { VisuallyHidden } from '../common/VisuallyHidden'
import type { Contact } from './ContactItem'

interface Props {
Expand All @@ -23,16 +24,36 @@ export function AddContact({ onAdd, onCancel }: Props) {
return (
<div className="mt-4 p-4 bg-gray-50 rounded-lg space-y-3">
<h3 className="font-medium">Add contact</h3>
<input
value={name} onChange={e => setName(e.target.value)}
placeholder="Name" className="input w-full"
/>
{errors.name && <p className="text-red-500 text-xs">{errors.name}</p>}
<input
value={address} onChange={e => setAddress(e.target.value.trim())}
placeholder="G... address" className="input w-full font-mono"
/>
{errors.address && <p className="text-red-500 text-xs">{errors.address}</p>}
<div>
<label htmlFor="contact-name">
<VisuallyHidden>Contact name</VisuallyHidden>
</label>
<input
id="contact-name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Name"
className="input w-full"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? "contact-name-error" : undefined}
/>
{errors.name && <p id="contact-name-error" className="text-red-500 text-xs">{errors.name}</p>}
</div>
<div>
<label htmlFor="contact-address">
<VisuallyHidden>Stellar address</VisuallyHidden>
</label>
<input
id="contact-address"
value={address}
onChange={e => setAddress(e.target.value.trim())}
placeholder="G... address"
className="input w-full font-mono"
aria-invalid={!!errors.address}
aria-describedby={errors.address ? "contact-address-error" : undefined}
/>
{errors.address && <p id="contact-address-error" className="text-red-500 text-xs">{errors.address}</p>}
</div>
<div className="flex gap-2">
<button onClick={submit} className="btn-primary btn-sm">Add</button>
<button onClick={onCancel} className="btn-secondary btn-sm">Cancel</button>
Expand Down