feat(web): complete fleet and instance management slices
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// @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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user