Files
TREK/client/src/components/OAuth/useScopeSelection.test.ts
T
Maurice 82070c72ec refactor(oauth): share the scope group picker logic between desktop and phone
The grouped scopes, the folded groups and the select all, group and single scope toggles now live in one useScopeSelection hook. Both pickers keep their own rows.
2026-10-09 15:19:47 +02:00

78 lines
3.4 KiB
TypeScript

// FE-COMP-SCOPESEL-001 to -006: the scope picker logic the desktop picker and its phone twin share.
import { act, renderHook } from '@testing-library/react';
import { SCOPE_GROUPS } from '../../api/oauthScopes';
import { useScopeSelection } from './useScopeSelection';
vi.mock('../../i18n', () => ({ useTranslation: () => ({ t: (k: string) => k }) }));
const TRIPS = 'oauth.scope.group.trips';
const ALL = Object.keys(SCOPE_GROUPS);
const TRIP_SCOPES = ALL.filter((s) => SCOPE_GROUPS[s].groupKey === TRIPS);
function setup(selected: string[]) {
const onChange = vi.fn();
const hook = renderHook(({ sel }) => useScopeSelection(sel, onChange), { initialProps: { sel: selected } });
return { ...hook, onChange };
}
describe('useScopeSelection', () => {
it('FE-COMP-SCOPESEL-001: groups every scope under its translated group', () => {
const { result } = setup([]);
expect(result.current.scopesByGroup[TRIPS].map((s) => s.scope)).toEqual(TRIP_SCOPES);
expect(Object.values(result.current.scopesByGroup).flat()).toHaveLength(ALL.length);
});
it('FE-COMP-SCOPESEL-002: select all ticks every scope, and with all ticked it clears them', () => {
const { result, rerender, onChange } = setup([]);
expect(result.current.allSelected).toBe(false);
const grouped = Object.values(result.current.scopesByGroup)
.flat()
.map((s) => s.scope);
act(() => result.current.toggleAll());
expect(onChange).toHaveBeenLastCalledWith(grouped);
rerender({ sel: ALL });
expect(result.current.allSelected).toBe(true);
act(() => result.current.toggleAll());
expect(onChange).toHaveBeenLastCalledWith([]);
});
it('FE-COMP-SCOPESEL-003: group state counts the ticked scopes and flags a partly ticked group as mixed', () => {
const { result } = setup([TRIP_SCOPES[0]]);
const state = result.current.groupState(result.current.scopesByGroup[TRIPS]);
expect(state).toEqual({
keys: TRIP_SCOPES,
allSelected: false,
someSelected: true,
mixed: true,
selectedCount: 1,
});
});
it('FE-COMP-SCOPESEL-004: toggling a group adds its missing scopes once, or removes all of them', () => {
const other = ALL.find((s) => !TRIP_SCOPES.includes(s)) as string;
const { result, rerender, onChange } = setup([other, TRIP_SCOPES[0]]);
act(() => result.current.toggleGroup(result.current.groupState(result.current.scopesByGroup[TRIPS])));
expect(onChange).toHaveBeenLastCalledWith([other, ...TRIP_SCOPES]);
rerender({ sel: [other, ...TRIP_SCOPES] });
act(() => result.current.toggleGroup(result.current.groupState(result.current.scopesByGroup[TRIPS])));
expect(onChange).toHaveBeenLastCalledWith([other]);
});
it('FE-COMP-SCOPESEL-005: toggling one scope appends or removes just that scope', () => {
const { result, onChange } = setup([TRIP_SCOPES[0]]);
act(() => result.current.toggleScope(TRIP_SCOPES[1]));
expect(onChange).toHaveBeenLastCalledWith([TRIP_SCOPES[0], TRIP_SCOPES[1]]);
act(() => result.current.toggleScope(TRIP_SCOPES[0]));
expect(onChange).toHaveBeenLastCalledWith([]);
});
it('FE-COMP-SCOPESEL-006: groups start folded and fold out and back per group', () => {
const { result } = setup([]);
expect(result.current.open[TRIPS]).toBeUndefined();
act(() => result.current.toggleOpen(TRIPS));
expect(result.current.open[TRIPS]).toBe(true);
act(() => result.current.toggleOpen(TRIPS));
expect(result.current.open[TRIPS]).toBe(false);
});
});