mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Add data-testid and accessibility improvements across components
Enhancements include: - Added data-testid attributes to all interactive components for improved e2e testing - Improved ARIA labels and semantic HTML attributes throughout - Added aria-label, aria-hidden, aria-describedby, and aria-current where appropriate - Enhanced form elements with proper aria-invalid and error associations - Added role attributes to semantic containers - Improved navigation accessibility with aria-expanded and aria-controls - Added proper aria-pressed to toggle buttons Modified components: - Dialog components (SnippetDialog, CreateNamespaceDialog, DeleteNamespaceDialog) - Navigation components (NavigationSidebar, Navigation toggle) - Snippet display components (SnippetCard, SnippetCardActions) - Snippet manager components (SnippetToolbar, SnippetGrid) - Snippet editor components (SnippetFormFields, InputParameterList, InputParameterItem) - Namespace management (NamespaceSelector) - Layout components (PageLayout) These changes improve both automated testing capabilities and accessibility for users with disabilities. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,8 @@ export function PageLayout({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="grid-pattern" />
|
||||
<div className="min-h-screen bg-background" data-testid="page-layout">
|
||||
<div className="grid-pattern" aria-hidden="true" />
|
||||
|
||||
<NavigationSidebar />
|
||||
|
||||
@@ -29,7 +29,7 @@ export function PageLayout({ children }: { children: ReactNode }) {
|
||||
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
||||
className="relative z-10 flex flex-col min-h-screen"
|
||||
>
|
||||
<header className="border-b border-border bg-background/95 backdrop-blur-md sticky top-0 z-20 overflow-hidden">
|
||||
<header className="border-b border-border bg-background/95 backdrop-blur-md sticky top-0 z-20 overflow-hidden" data-testid="page-header">
|
||||
<div
|
||||
className="container mx-auto px-2 py-3 sm:px-6 sm:py-6 w-full min-w-0"
|
||||
style={{
|
||||
@@ -48,7 +48,7 @@ export function PageLayout({ children }: { children: ReactNode }) {
|
||||
<div className="logo-icon-box">
|
||||
<Code weight="bold" />
|
||||
</div>
|
||||
<span className="logo-text" aria-label="CodeSnippet">
|
||||
<span className="logo-text" aria-label="CodeSnippet" data-testid="logo-text">
|
||||
CodeSnippet
|
||||
</span>
|
||||
</motion.div>
|
||||
@@ -66,6 +66,7 @@ export function PageLayout({ children }: { children: ReactNode }) {
|
||||
<main
|
||||
className="container mx-auto px-3 py-4 sm:px-6 sm:py-8 flex-1"
|
||||
style={safeAreaPadding}
|
||||
data-testid="main-content"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<AppStatusAlerts />
|
||||
|
||||
@@ -45,7 +45,7 @@ export function SnippetManagerRedux() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-center py-20" data-testid="snippet-manager-loading">
|
||||
<p className="text-muted-foreground">Loading snippets...</p>
|
||||
</div>
|
||||
)
|
||||
@@ -75,7 +75,7 @@ export function SnippetManagerRedux() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6" data-testid="snippet-manager">
|
||||
<NamespaceSelector
|
||||
selectedNamespaceId={selectedNamespaceId}
|
||||
onNamespaceChange={handleNamespaceChange}
|
||||
|
||||
@@ -33,11 +33,16 @@ export function CreateNamespaceDialog({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Plus weight="bold" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
data-testid="create-namespace-trigger"
|
||||
aria-label="Create new namespace"
|
||||
>
|
||||
<Plus weight="bold" aria-hidden="true" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="create-namespace-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Namespace</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -50,13 +55,23 @@ export function CreateNamespaceDialog({
|
||||
value={namespaceName}
|
||||
onChange={(e) => onNamespaceNameChange(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && onCreateNamespace()}
|
||||
data-testid="namespace-name-input"
|
||||
aria-label="Namespace name"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
data-testid="create-namespace-cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onCreateNamespace} disabled={loading}>
|
||||
<Button
|
||||
onClick={onCreateNamespace}
|
||||
disabled={loading}
|
||||
data-testid="create-namespace-save-btn"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -38,13 +38,15 @@ export function DeleteNamespaceDialog({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onOpenDialog}
|
||||
data-testid="delete-namespace-trigger"
|
||||
aria-label="Delete namespace"
|
||||
>
|
||||
<Trash weight="bold" />
|
||||
<Trash weight="bold" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent data-testid="delete-namespace-dialog">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Namespace</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
@@ -52,8 +54,14 @@ export function DeleteNamespaceDialog({
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onDeleteNamespace} disabled={loading}>
|
||||
<AlertDialogCancel data-testid="delete-namespace-cancel-btn">
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onDeleteNamespace}
|
||||
disabled={loading}
|
||||
data-testid="delete-namespace-confirm-btn"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@@ -109,22 +109,30 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Folder weight="fill" className="h-4 w-4" />
|
||||
<Folder weight="fill" className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="text-sm font-medium">Namespace:</span>
|
||||
</div>
|
||||
|
||||
|
||||
<Select
|
||||
value={selectedNamespaceId || undefined}
|
||||
onValueChange={onNamespaceChange}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger
|
||||
className="w-[200px]"
|
||||
data-testid="namespace-selector-trigger"
|
||||
aria-label="Select namespace"
|
||||
>
|
||||
<SelectValue placeholder="Select namespace">
|
||||
{selectedNamespace?.name || 'Select namespace'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent data-testid="namespace-selector-content">
|
||||
{namespaces.map(namespace => (
|
||||
<SelectItem key={namespace.id} value={namespace.id}>
|
||||
<SelectItem
|
||||
key={namespace.id}
|
||||
value={namespace.id}
|
||||
data-testid={`namespace-option-${namespace.id}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{namespace.name}</span>
|
||||
{namespace.isDefault && (
|
||||
|
||||
@@ -85,8 +85,11 @@ export function SnippetCardActions({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger disabled={isMoving || availableNamespaces.length === 0}>
|
||||
<FolderOpen className="h-4 w-4 mr-2" />
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={isMoving || availableNamespaces.length === 0}
|
||||
data-testid="snippet-card-move-submenu"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4 mr-2" aria-hidden="true" />
|
||||
<span>Move to...</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -99,6 +102,7 @@ export function SnippetCardActions({
|
||||
<DropdownMenuItem
|
||||
key={namespace.id}
|
||||
onClick={() => onMoveToNamespace(namespace.id)}
|
||||
data-testid={`move-to-namespace-${namespace.id}`}
|
||||
>
|
||||
{namespace.name}
|
||||
{namespace.isDefault && (
|
||||
@@ -113,8 +117,9 @@ export function SnippetCardActions({
|
||||
<DropdownMenuItem
|
||||
onClick={onDelete}
|
||||
className="text-destructive focus:text-destructive"
|
||||
data-testid="snippet-card-delete-btn"
|
||||
>
|
||||
<Trash className="h-4 w-4 mr-2" />
|
||||
<Trash className="h-4 w-4 mr-2" aria-hidden="true" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -52,6 +52,8 @@ export function InputParameterItem({ param, index, onUpdate, onRemove }: InputPa
|
||||
value={param.name}
|
||||
onChange={(e) => onUpdate(index, 'name', e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
data-testid={`param-name-input-${index}`}
|
||||
aria-label="Parameter name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -62,15 +64,20 @@ export function InputParameterItem({ param, index, onUpdate, onRemove }: InputPa
|
||||
value={param.type}
|
||||
onValueChange={(value) => onUpdate(index, 'type', value)}
|
||||
>
|
||||
<SelectTrigger id={`param-type-${index}`} className="h-8 text-sm">
|
||||
<SelectTrigger
|
||||
id={`param-type-${index}`}
|
||||
className="h-8 text-sm"
|
||||
data-testid={`param-type-select-${index}`}
|
||||
aria-label="Parameter type"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">string</SelectItem>
|
||||
<SelectItem value="number">number</SelectItem>
|
||||
<SelectItem value="boolean">boolean</SelectItem>
|
||||
<SelectItem value="array">array</SelectItem>
|
||||
<SelectItem value="object">object</SelectItem>
|
||||
<SelectContent data-testid={`param-type-options-${index}`}>
|
||||
<SelectItem value="string" data-testid="type-string">string</SelectItem>
|
||||
<SelectItem value="number" data-testid="type-number">number</SelectItem>
|
||||
<SelectItem value="boolean" data-testid="type-boolean">boolean</SelectItem>
|
||||
<SelectItem value="array" data-testid="type-array">array</SelectItem>
|
||||
<SelectItem value="object" data-testid="type-object">object</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -80,8 +87,10 @@ export function InputParameterItem({ param, index, onUpdate, onRemove }: InputPa
|
||||
size="sm"
|
||||
onClick={() => onRemove(index)}
|
||||
className="h-8 w-8 p-0 mt-6 text-destructive hover:text-destructive"
|
||||
data-testid={`remove-parameter-btn-${index}`}
|
||||
aria-label={`Remove parameter ${index + 1}`}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
<Trash className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -94,6 +103,8 @@ export function InputParameterItem({ param, index, onUpdate, onRemove }: InputPa
|
||||
value={param.defaultValue}
|
||||
onChange={(e) => onUpdate(index, 'defaultValue', e.target.value)}
|
||||
className="h-8 text-sm font-mono"
|
||||
data-testid={`param-default-input-${index}`}
|
||||
aria-label="Default parameter value"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -106,6 +117,8 @@ export function InputParameterItem({ param, index, onUpdate, onRemove }: InputPa
|
||||
value={param.description || ''}
|
||||
onChange={(e) => onUpdate(index, 'description', e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
data-testid={`param-description-input-${index}`}
|
||||
aria-label="Parameter description"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -33,8 +33,10 @@ export function InputParameterList({
|
||||
size="sm"
|
||||
onClick={onAddParameter}
|
||||
className="gap-2"
|
||||
data-testid="add-parameter-btn"
|
||||
aria-label="Add new parameter"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
Add Parameter
|
||||
</Button>
|
||||
</CardTitle>
|
||||
@@ -50,8 +52,11 @@ export function InputParameterList({
|
||||
value={functionName}
|
||||
onChange={(e) => onFunctionNameChange(e.target.value)}
|
||||
className="bg-background"
|
||||
data-testid="function-name-input"
|
||||
aria-label="Function or component name"
|
||||
aria-describedby="function-name-help"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground" id="function-name-help">
|
||||
The name of the function or component to render. Leave empty to use the default export.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -39,19 +39,30 @@ export function SnippetFormFields({
|
||||
value={title}
|
||||
onChange={(e) => onTitleChange(e.target.value)}
|
||||
className={errors.title ? 'border-destructive ring-destructive' : ''}
|
||||
data-testid="snippet-title-input"
|
||||
aria-invalid={!!errors.title}
|
||||
aria-describedby={errors.title ? "title-error" : undefined}
|
||||
/>
|
||||
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
|
||||
{errors.title && (
|
||||
<p className="text-sm text-destructive" id="title-error">
|
||||
{errors.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Select value={language} onValueChange={onLanguageChange}>
|
||||
<SelectTrigger id="language">
|
||||
<SelectTrigger
|
||||
id="language"
|
||||
data-testid="snippet-language-select"
|
||||
aria-label="Select programming language"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent data-testid="snippet-language-options">
|
||||
{LANGUAGES.map((lang) => (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
<SelectItem key={lang} value={lang} data-testid={`language-option-${lang}`}>
|
||||
{lang}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -67,6 +78,8 @@ export function SnippetFormFields({
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
rows={2}
|
||||
data-testid="snippet-description-textarea"
|
||||
aria-label="Snippet description"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -10,8 +10,11 @@ export function Navigation() {
|
||||
className="nav-burger-btn"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="navigation-sidebar"
|
||||
data-testid="navigation-toggle-btn"
|
||||
>
|
||||
<List weight="bold" />
|
||||
<List weight="bold" aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ export function NavigationSidebar() {
|
||||
"shadow-xl"
|
||||
)}
|
||||
data-testid="navigation-sidebar"
|
||||
id="navigation-sidebar"
|
||||
role="navigation"
|
||||
aria-label="Main navigation menu"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between h-16 px-4 border-b border-border/50">
|
||||
|
||||
@@ -25,7 +25,12 @@ export function SnippetGrid({
|
||||
onToggleSelect,
|
||||
}: SnippetGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
data-testid="snippet-grid"
|
||||
role="region"
|
||||
aria-label="Snippets list"
|
||||
>
|
||||
{snippets.map((snippet) => (
|
||||
<SnippetCard
|
||||
key={snippet.id}
|
||||
|
||||
@@ -94,6 +94,7 @@ export function SnippetToolbar({
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => onCreateFromTemplate(template.id)}
|
||||
data-testid={`snippet-template-react-${template.id}`}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
|
||||
Reference in New Issue
Block a user