117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
// @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> = {}): 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(<InstanceEditor mode="create" dataSource={source} />);
|
|
|
|
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(
|
|
<InstanceEditor mode="edit" instanceId="owner" dataSource={source} />,
|
|
);
|
|
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(<InstanceEditor mode="edit" instanceId="intruder" dataSource={source} />);
|
|
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(<InstanceEditor mode="edit" instanceId="owner" dataSource={source} />);
|
|
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(<InstanceEditor mode="edit" instanceId="owner" dataSource={source} />);
|
|
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);
|
|
});
|
|
});
|