fix(09-01): resolve DEBT-01 aria-label inconsistency in FieldRenderer and PasswordField

- Add FieldRenderer.test.tsx: 4 tests verifying both branches use aria-label consistently
- Fix FieldRenderer.tsx text-branch: replace sr-only span with aria-label={} matching select-branch
- Fix PasswordField.tsx tooltip button: same sr-only -> aria-label fix
- Update RemoteConfigStep.test.tsx: add { selector: 'input' } to getByLabelText queries that
  now conflict with tooltip button aria-labels (regression fix)
This commit is contained in:
2026-04-01 09:24:14 +02:00
parent 0c8403dc79
commit 532a994f6b
4 changed files with 97 additions and 20 deletions
+81
View File
@@ -0,0 +1,81 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { FieldRenderer } from './FieldRenderer';
import type { UseFormRegister } from 'react-hook-form';
function makeRegister(): UseFormRegister<any> {
return vi.fn((name: string) => ({
name,
ref: vi.fn(),
onChange: vi.fn(),
onBlur: vi.fn(),
})) as unknown as UseFormRegister<any>;
}
const textFieldWithTooltip = {
key: 'bucket_name',
label: 'Bucket Name',
inputType: 'text' as const,
tooltipText: 'The name of your storage bucket',
};
const selectFieldWithTooltip = {
key: 'region',
label: 'Region',
inputType: 'select' as const,
tooltipText: 'Select your deployment region',
options: [
{ value: 'us-east', label: 'US East' },
{ value: 'eu-west', label: 'EU West' },
],
};
describe('FieldRenderer - DEBT-01 aria-label consistency', () => {
it('Test 1: text-branch tooltip button has aria-label="More info about {field.label}"', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
expect(tooltipBtn).toBeDefined();
expect(tooltipBtn.getAttribute('aria-label')).toBe('More info about Bucket Name');
});
it('Test 2: select-branch tooltip button has aria-label="More info about {field.label}"', () => {
render(
<FieldRenderer
field={selectFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about region/i });
expect(tooltipBtn).toBeDefined();
expect(tooltipBtn.getAttribute('aria-label')).toBe('More info about Region');
});
it('Test 3: text-branch tooltip button is findable via getByRole with name pattern', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
// This query pattern must work for both branches
const btn = screen.getByRole('button', { name: /more info about/i });
expect(btn).toBeDefined();
});
it('Test 3b: select-branch tooltip button is findable via getByRole with name pattern', () => {
render(
<FieldRenderer
field={selectFieldWithTooltip}
register={makeRegister()}
/>
);
const btn = screen.getByRole('button', { name: /more info about/i });
expect(btn).toBeDefined();
});
});
+2 -2
View File
@@ -91,10 +91,10 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
<button <button
type="button" type="button"
onClick={() => setShowTooltip(v => !v)} onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary text-xs leading-none" className="text-primary hover:text-primary text-xs leading-none"
> >
<span className="sr-only">More info about {field.label}</span>
<span aria-hidden="true"></span>
</button> </button>
)} )}
</div> </div>
+2 -2
View File
@@ -24,10 +24,10 @@ export function PasswordField({ id, label, error, registration, placeholder, hel
<button <button
type="button" type="button"
onClick={() => setShowTooltip(v => !v)} onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary text-xs leading-none" className="text-primary hover:text-primary text-xs leading-none"
> >
<span className="sr-only">More info about {label}</span>
<span aria-hidden="true"></span>
</button> </button>
)} )}
</div> </div>
+12 -16
View File
@@ -40,7 +40,7 @@ describe('RemoteConfigStep', () => {
it('shows SAS URL field by default (default auth method)', async () => { it('shows SAS URL field by default (default auth method)', async () => {
renderWithBackend('azureblob'); renderWithBackend('azureblob');
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/sas url/i)).toBeDefined(); expect(screen.getByLabelText(/sas url/i, { selector: 'input' })).toBeDefined();
}); });
}); });
@@ -52,9 +52,9 @@ describe('RemoteConfigStep', () => {
fireEvent.click(screen.getByText(/access key/i, { selector: 'button' })); fireEvent.click(screen.getByText(/access key/i, { selector: 'button' }));
await waitFor(() => { await waitFor(() => {
// After switching to Access Key, the SAS URL wrapper should be hidden // After switching to Access Key, the SAS URL wrapper should be hidden
const sasInput = screen.getByLabelText(/sas url/i); const sasInput = screen.getByLabelText(/sas url/i, { selector: 'input' });
const sasWrapper = sasInput.closest('div.hidden'); const sasWrapper = sasInput.closest('div.hidden');
const keyInput = screen.getByLabelText(/access key/i); const keyInput = screen.getByLabelText(/access key/i, { selector: 'input' });
const keyWrapper = keyInput.closest('div.block'); const keyWrapper = keyInput.closest('div.block');
expect(sasWrapper).toBeDefined(); expect(sasWrapper).toBeDefined();
expect(keyWrapper).toBeDefined(); expect(keyWrapper).toBeDefined();
@@ -67,11 +67,11 @@ describe('RemoteConfigStep', () => {
// Wait for form to render // Wait for form to render
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/sas url/i)).toBeDefined(); expect(screen.getByLabelText(/sas url/i, { selector: 'input' })).toBeDefined();
}); });
// Type a value into the SAS URL field // Type a value into the SAS URL field
const sasInput = screen.getByLabelText(/sas url/i); const sasInput = screen.getByLabelText(/sas url/i, { selector: 'input' });
await user.type(sasInput, 'https://mystorage.blob.core.windows.net/?sv=test'); await user.type(sasInput, 'https://mystorage.blob.core.windows.net/?sv=test');
// Switch to Access Key // Switch to Access Key
@@ -82,7 +82,7 @@ describe('RemoteConfigStep', () => {
// The value should still be there // The value should still be there
await waitFor(() => { await waitFor(() => {
const input = screen.getByLabelText(/sas url/i) as HTMLInputElement; const input = screen.getByLabelText(/sas url/i, { selector: 'input' }) as HTMLInputElement;
expect(input.value).toBe('https://mystorage.blob.core.windows.net/?sv=test'); expect(input.value).toBe('https://mystorage.blob.core.windows.net/?sv=test');
}); });
}); });
@@ -126,7 +126,7 @@ describe('RemoteConfigStep', () => {
it('renders OAuth Token field', async () => { it('renders OAuth Token field', async () => {
renderWithBackend('onedrive' as BackendType); renderWithBackend('onedrive' as BackendType);
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/oauth token/i)).toBeDefined(); expect(screen.getByLabelText(/oauth token/i, { selector: 'input' })).toBeDefined();
}); });
}); });
it('renders Drive ID field', async () => { it('renders Drive ID field', async () => {
@@ -271,7 +271,7 @@ describe('RemoteConfigStep', () => {
}); });
await user.type(screen.getByLabelText(/storage account name/i), 'mystorageaccount'); await user.type(screen.getByLabelText(/storage account name/i), 'mystorageaccount');
// Fill SAS URL field so form can submit without other required-field errors // Fill SAS URL field so form can submit without other required-field errors
await user.type(screen.getByLabelText(/sas url/i), 'https://mystorage.blob.core.windows.net/?sv=test'); await user.type(screen.getByLabelText(/sas url/i, { selector: 'input' }), 'https://mystorage.blob.core.windows.net/?sv=test');
await user.click(screen.getByRole('button', { name: /next/i })); await user.click(screen.getByRole('button', { name: /next/i }));
await waitFor(() => { await waitFor(() => {
expect(screen.queryByText(/3.{1,5}24 lowercase alphanumeric/i)).toBeNull(); expect(screen.queryByText(/3.{1,5}24 lowercase alphanumeric/i)).toBeNull();
@@ -343,9 +343,8 @@ describe('RemoteConfigStep', () => {
it('renders ⓘ button on azureblob sas_url field', async () => { it('renders ⓘ button on azureblob sas_url field', async () => {
renderWithBackend('azureblob'); renderWithBackend('azureblob');
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/sas url/i)).toBeDefined(); expect(screen.getByLabelText(/sas url/i, { selector: 'input' })).toBeDefined();
}); });
// ⓘ button does not exist yet — will throw "Unable to find role 'button'"
expect(screen.getByRole('button', { name: /more info about sas url/i })).toBeDefined(); expect(screen.getByRole('button', { name: /more info about sas url/i })).toBeDefined();
}); });
@@ -353,9 +352,8 @@ describe('RemoteConfigStep', () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithBackend('azureblob'); renderWithBackend('azureblob');
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/sas url/i)).toBeDefined(); expect(screen.getByLabelText(/sas url/i, { selector: 'input' })).toBeDefined();
}); });
// ⓘ button does not exist yet — getByRole will throw
await user.click(screen.getByRole('button', { name: /more info about sas url/i })); await user.click(screen.getByRole('button', { name: /more info about sas url/i }));
await waitFor(() => { await waitFor(() => {
// Tooltip text should be visible after clicking ⓘ // Tooltip text should be visible after clicking ⓘ
@@ -367,9 +365,8 @@ describe('RemoteConfigStep', () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithBackend('azureblob'); renderWithBackend('azureblob');
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/sas url/i)).toBeDefined(); expect(screen.getByLabelText(/sas url/i, { selector: 'input' })).toBeDefined();
}); });
// ⓘ button does not exist yet — getByRole will throw
await user.click(screen.getByRole('button', { name: /more info about sas url/i })); await user.click(screen.getByRole('button', { name: /more info about sas url/i }));
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/shared access signature/i)).toBeDefined(); expect(screen.getByText(/shared access signature/i)).toBeDefined();
@@ -392,9 +389,8 @@ describe('RemoteConfigStep', () => {
it('renders ⓘ button on OneDrive token field', async () => { it('renders ⓘ button on OneDrive token field', async () => {
renderWithBackend('onedrive' as BackendType); renderWithBackend('onedrive' as BackendType);
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/oauth token/i)).toBeDefined(); expect(screen.getByLabelText(/oauth token/i, { selector: 'input' })).toBeDefined();
}); });
// ⓘ button does not exist yet — will throw "Unable to find role 'button'"
// Note: this matches any "more info about X" button on the OneDrive form // Note: this matches any "more info about X" button on the OneDrive form
expect(screen.getByRole('button', { name: /more info about oauth token/i })).toBeDefined(); expect(screen.getByRole('button', { name: /more info about oauth token/i })).toBeDefined();
}); });