// @vitest-environment jsdom import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { InstanceEditor, type InstanceDataSource, type ManagedInstance } from './instance-crud.js'; const owner: ManagedInstance = { id: 'owner', name: 'Owner modem', origin: 'https://owner.example', tags: ['west', 'lab'], revision: 3, credentialConfigured: true, }; function dataSource(overrides: Partial = {}): InstanceDataSource { return { get: vi.fn(async () => owner), create: vi.fn(async () => owner), update: vi.fn(async () => ({ ...owner, revision: 4 })), testConnection: vi.fn(async () => ({ reachable: true, authenticated: true })), delete: vi.fn(async () => undefined), ...overrides, }; } afterEach(cleanup); describe('Instance CRUD form', () => { it('creates with tags and an explicit set-password action without rendering the secret', async () => { const user = userEvent.setup(); const source = dataSource(); render(); await user.type(screen.getByLabelText('Name'), 'Lab modem'); await user.type(screen.getByLabelText('Origin'), 'https://lab.example/admin'); await user.type(screen.getByLabelText('Tags'), 'lab, west, lab'); await user.selectOptions(screen.getByLabelText('Authentication method'), 'password'); await user.type(screen.getByLabelText('Password'), 'do-not-render'); expect(document.body.textContent).not.toContain('do-not-render'); await user.click(screen.getByRole('button', { name: 'Add instance' })); expect(source.create).toHaveBeenCalledWith({ name: 'Lab modem', origin: 'https://lab.example/admin', tags: ['lab', 'west'], password: { action: 'set', password: 'do-not-render' }, }); }); it('loads only the route owner and preserves, sets, or clears its credential explicitly', async () => { const user = userEvent.setup(); const source = dataSource(); const { rerender } = render( , ); expect(await screen.findByDisplayValue('Owner modem')).toBeTruthy(); expect(source.get).toHaveBeenCalledWith('owner'); await user.clear(screen.getByLabelText('Name')); await user.type(screen.getByLabelText('Name'), 'Renamed'); await user.click(screen.getByRole('button', { name: 'Save changes' })); expect(source.update).toHaveBeenLastCalledWith( 'owner', 3, expect.objectContaining({ password: { action: 'preserve' } }), ); await user.selectOptions(screen.getByLabelText('Password action'), 'clear'); await user.click(screen.getByRole('button', { name: 'Save changes' })); expect(source.update).toHaveBeenLastCalledWith( 'owner', 4, expect.objectContaining({ password: { action: 'clear' } }), ); rerender(); expect((await screen.findByRole('alert')).textContent).toMatch(/does not match this route/i); expect(screen.queryByDisplayValue('Owner modem')).toBeNull(); }); it('tests the persisted owner connection and keeps safe errors visible while editing', async () => { const user = userEvent.setup(); const source = dataSource({ update: vi.fn(async () => { throw new Error('Could not save this instance.'); }), }); render(); await screen.findByDisplayValue('Owner modem'); await user.click(screen.getByRole('button', { name: 'Test connection' })); expect((await screen.findByRole('status')).textContent).toMatch(/reachable and authenticated/i); await user.click(screen.getByRole('button', { name: 'Save changes' })); expect((await screen.findByRole('alert')).textContent).toContain( 'Could not save this instance.', ); await user.type(screen.getByLabelText('Name'), ' still here'); expect(screen.getByRole('alert')).toBeTruthy(); }); it('requires an owner-bound typed confirmation before deletion', async () => { const user = userEvent.setup(); const source = dataSource(); render(); await screen.findByDisplayValue('Owner modem'); await user.click(screen.getByRole('button', { name: 'Delete instance' })); expect( (screen.getByRole('button', { name: 'Confirm deletion' }) as HTMLButtonElement).disabled, ).toBe(true); await user.type(screen.getByLabelText('Type owner to confirm'), 'owner'); await user.click(screen.getByRole('button', { name: 'Confirm deletion' })); expect(source.delete).toHaveBeenCalledWith('owner', 3); }); });