feat(19-04): AdminPage LOCAL ACCOUNTS + SettingsSheet change-password / link-OIDC

- Add hasLocalCredential to AdminMember type (mirrors API extension from plan 19-02)
- Add createMember mutation + Surface 11A inline add-member form in AdminPage
- Add Surface 11B Reset-password button in MemberRow (hasLocalCredential gate)
- Add ResetPasswordSheet component (bottom-sheet, role=dialog, focus-managed, Escape closes)
- Add Surface 12 Change-password row in SettingsSheet (hasLocalCredential gate)
- Add Surface 13 Link-OIDC identity row in SettingsSheet (hasLocalCredential + oidcEnabled gate)
- Add ChangePasswordSheet component (current/new/confirm fields, change-password mutation)
- Add LinkOidcSheet component (confirmation dialog; uses generic OIDC copy per D-06, no provider branding)
- Fix: update InstructionSheet.test.tsx to wrap with QueryClientProvider (Rule 1 - now uses useQuery)
- Fix: remove stale eslint-disable in App.test.tsx (lint --max-warnings 0 would fail)
- All 263 tests pass; typecheck clean; lint clean
This commit is contained in:
Lucas Berger
2026-06-17 17:21:20 -04:00
parent 32d0408774
commit 19c45eb069
5 changed files with 1286 additions and 28 deletions
@@ -7,8 +7,10 @@
* - onClose (the sheet-close prop) is NOT called when the dialog opens
*/
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// ── Module mocks ──────────────────────────────────────────────────────────────
@@ -22,6 +24,17 @@ vi.mock('../hooks/usePushSubscription.js', () => ({
readNotificationsEnabled: vi.fn(() => false),
}));
// Phase 19: SettingsSheet now calls fetchMe and fetchAuthMode inside useQuery.
// Mock client so the test doesn't make real network calls.
vi.mock('../api/client.js', () => ({
fetchMe: vi.fn().mockResolvedValue({
user: { id: 1, displayName: 'Test', color: '#4a90d9', isAdmin: false, needsProviderSetup: false, hasLocalCredential: false },
}),
fetchAuthMode: vi.fn().mockResolvedValue({ localEnabled: true, oidcEnabled: false }),
fetchChangePassword: vi.fn().mockResolvedValue(undefined),
fetchLinkOidc: vi.fn().mockResolvedValue({ redirectUrl: '/oidc' }),
}));
// ── Minimal Notification stub (jsdom lacks it) ────────────────────────────────
beforeEach(() => {
@@ -44,12 +57,21 @@ beforeEach(() => {
import { SettingsSheet } from './SettingsSheet.js';
// ── Test helper ───────────────────────────────────────────────────────────────
function renderWithQueryClient(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe('SettingsSheet — "How to enable" wiring (UAT-05-T4)', () => {
it('clicking "How to enable" opens the InstructionSheet dialog and does NOT call onClose', () => {
const onCloseSpy = vi.fn();
render(<SettingsSheet isOpen={true} onClose={onCloseSpy} />);
renderWithQueryClient(<SettingsSheet isOpen={true} onClose={onCloseSpy} />);
// No instruction dialog yet
expect(screen.queryByRole('dialog', { name: /re-enable notifications/i })).toBeNull();
@@ -71,7 +93,7 @@ describe('SettingsSheet — "How to enable" wiring (UAT-05-T4)', () => {
it('InstructionSheet "Done" button closes the instruction dialog without calling sheet onClose', () => {
const onCloseSpy = vi.fn();
render(<SettingsSheet isOpen={true} onClose={onCloseSpy} />);
renderWithQueryClient(<SettingsSheet isOpen={true} onClose={onCloseSpy} />);
// Open the instruction sheet
fireEvent.click(screen.getByText('How to enable'));