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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -91,10 +91,10 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTooltip(v => !v)}
|
||||
aria-label={`More info about ${field.label}`}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,10 +24,10 @@ export function PasswordField({ id, label, error, registration, placeholder, hel
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTooltip(v => !v)}
|
||||
aria-label={`More info about ${label}`}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('RemoteConfigStep', () => {
|
||||
it('shows SAS URL field by default (default auth method)', async () => {
|
||||
renderWithBackend('azureblob');
|
||||
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' }));
|
||||
await waitFor(() => {
|
||||
// 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 keyInput = screen.getByLabelText(/access key/i);
|
||||
const keyInput = screen.getByLabelText(/access key/i, { selector: 'input' });
|
||||
const keyWrapper = keyInput.closest('div.block');
|
||||
expect(sasWrapper).toBeDefined();
|
||||
expect(keyWrapper).toBeDefined();
|
||||
@@ -67,11 +67,11 @@ describe('RemoteConfigStep', () => {
|
||||
|
||||
// Wait for form to render
|
||||
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
|
||||
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');
|
||||
|
||||
// Switch to Access Key
|
||||
@@ -82,7 +82,7 @@ describe('RemoteConfigStep', () => {
|
||||
|
||||
// The value should still be there
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -126,7 +126,7 @@ describe('RemoteConfigStep', () => {
|
||||
it('renders OAuth Token field', async () => {
|
||||
renderWithBackend('onedrive' as BackendType);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/oauth token/i)).toBeDefined();
|
||||
expect(screen.getByLabelText(/oauth token/i, { selector: 'input' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
it('renders Drive ID field', async () => {
|
||||
@@ -271,7 +271,7 @@ describe('RemoteConfigStep', () => {
|
||||
});
|
||||
await user.type(screen.getByLabelText(/storage account name/i), 'mystorageaccount');
|
||||
// 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 waitFor(() => {
|
||||
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 () => {
|
||||
renderWithBackend('azureblob');
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -353,9 +352,8 @@ describe('RemoteConfigStep', () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithBackend('azureblob');
|
||||
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 waitFor(() => {
|
||||
// Tooltip text should be visible after clicking ⓘ
|
||||
@@ -367,9 +365,8 @@ describe('RemoteConfigStep', () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithBackend('azureblob');
|
||||
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 waitFor(() => {
|
||||
expect(screen.getByText(/shared access signature/i)).toBeDefined();
|
||||
@@ -392,9 +389,8 @@ describe('RemoteConfigStep', () => {
|
||||
it('renders ⓘ button on OneDrive token field', async () => {
|
||||
renderWithBackend('onedrive' as BackendType);
|
||||
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
|
||||
expect(screen.getByRole('button', { name: /more info about oauth token/i })).toBeDefined();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user