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
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user