mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
220 lines
9.0 KiB
TypeScript
220 lines
9.0 KiB
TypeScript
// FE-MOB-SETAIR-001 onwards
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { render, screen, waitFor } from '../../../helpers/render';
|
|
import { server } from '../../../helpers/msw/server';
|
|
import { resetAllStores } from '../../../helpers/store';
|
|
import { ToastContainer } from '../../../../src/components/shared/Toast';
|
|
import MAirTrailConnectionSection from '../../../../src/mobile/screens/settings/MAirTrailConnectionSection';
|
|
|
|
interface SavedBody {
|
|
url: string;
|
|
apiKey?: string;
|
|
allowInsecureTls?: boolean;
|
|
writeEnabled?: boolean;
|
|
}
|
|
|
|
function stubSettings(over: Record<string, unknown> = {}) {
|
|
server.use(
|
|
http.get('/api/integrations/airtrail/settings', () =>
|
|
HttpResponse.json({ url: '', allowInsecureTls: false, writeEnabled: false, connected: false, ...over }),
|
|
),
|
|
);
|
|
}
|
|
|
|
function renderSection() {
|
|
return render(
|
|
<>
|
|
<ToastContainer />
|
|
<MAirTrailConnectionSection />
|
|
</>,
|
|
);
|
|
}
|
|
|
|
describe('MAirTrailConnectionSection', () => {
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
stubSettings();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-001: hydrates the form from the stored connection', async () => {
|
|
stubSettings({ url: 'https://air.example.com', allowInsecureTls: true, writeEnabled: true, connected: true });
|
|
renderSection();
|
|
|
|
expect(await screen.findByDisplayValue('https://air.example.com')).toBeInTheDocument();
|
|
expect(screen.getByRole('switch', { name: 'Allow self-signed certificates' })).toHaveAttribute('aria-checked', 'true');
|
|
expect(screen.getByRole('switch', { name: 'Write changes back to AirTrail' })).toHaveAttribute('aria-checked', 'true');
|
|
expect(screen.getByText('Connected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-002: an unconfigured instance renders empty and disconnected', async () => {
|
|
renderSection();
|
|
|
|
await waitFor(() => expect(screen.getByRole('button', { name: /Test connection/ })).toBeDisabled());
|
|
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText('Bearer API key')).toHaveValue('');
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-003: the stored key is masked by the placeholder, never prefilled', async () => {
|
|
stubSettings({ url: 'https://air.example.com', connected: true });
|
|
renderSection();
|
|
|
|
const keyInput = await screen.findByPlaceholderText('••••••••');
|
|
expect(keyInput).toHaveValue('');
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-004: a failing settings fetch still ends the loading state', async () => {
|
|
server.use(
|
|
http.get('/api/integrations/airtrail/settings', () => HttpResponse.json({ error: 'boom' }, { status: 500 })),
|
|
);
|
|
renderSection();
|
|
|
|
// Save stays disabled because there is no URL, but the buttons are rendered
|
|
// rather than stuck behind `loading`.
|
|
await waitFor(() => expect(screen.getByRole('button', { name: /Test connection/ })).toBeDisabled());
|
|
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-005: Save stays disabled until a URL and a key are present', async () => {
|
|
const user = userEvent.setup();
|
|
renderSection();
|
|
|
|
const saveBtn = await screen.findByRole('button', { name: /Save/ });
|
|
expect(saveBtn).toBeDisabled();
|
|
|
|
await user.type(screen.getByPlaceholderText('https://airtrail.example.com'), 'https://air.example.com');
|
|
expect(saveBtn).toBeDisabled();
|
|
|
|
await user.type(screen.getByPlaceholderText('Bearer API key'), 'secret');
|
|
expect(saveBtn).toBeEnabled();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-006: an already-connected instance can be saved without retyping the key', async () => {
|
|
const user = userEvent.setup();
|
|
let body: SavedBody | undefined;
|
|
stubSettings({ url: 'https://air.example.com', connected: true });
|
|
server.use(
|
|
http.put('/api/integrations/airtrail/settings', async ({ request }) => {
|
|
body = (await request.json()) as SavedBody;
|
|
return HttpResponse.json({ ok: true });
|
|
}),
|
|
http.get('/api/integrations/airtrail/status', () => HttpResponse.json({ connected: true })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
|
await screen.findByText('AirTrail connection saved');
|
|
expect(body).toEqual({ url: 'https://air.example.com', allowInsecureTls: false, writeEnabled: false });
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-007: a typed key and the toggles are sent along and the field is cleared', async () => {
|
|
const user = userEvent.setup();
|
|
let body: SavedBody | undefined;
|
|
server.use(
|
|
http.put('/api/integrations/airtrail/settings', async ({ request }) => {
|
|
body = (await request.json()) as SavedBody;
|
|
return HttpResponse.json({ ok: true });
|
|
}),
|
|
http.get('/api/integrations/airtrail/status', () => HttpResponse.json({ connected: true })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.type(await screen.findByPlaceholderText('https://airtrail.example.com'), ' https://air.example.com ');
|
|
await user.type(screen.getByPlaceholderText('Bearer API key'), ' tok-123 ');
|
|
await user.click(screen.getByRole('switch', { name: 'Allow self-signed certificates' }));
|
|
await user.click(screen.getByRole('switch', { name: 'Write changes back to AirTrail' }));
|
|
await user.click(screen.getByRole('button', { name: /Save/ }));
|
|
|
|
await waitFor(() =>
|
|
expect(body).toEqual({
|
|
url: 'https://air.example.com',
|
|
allowInsecureTls: true,
|
|
writeEnabled: true,
|
|
apiKey: 'tok-123',
|
|
}),
|
|
);
|
|
await waitFor(() => expect(screen.getByPlaceholderText('••••••••')).toHaveValue(''));
|
|
expect(screen.getByText('Connected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-008: a warning from the server replaces the success toast', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com', connected: true });
|
|
server.use(
|
|
http.put('/api/integrations/airtrail/settings', () => HttpResponse.json({ warning: 'TLS verification is off' })),
|
|
http.get('/api/integrations/airtrail/status', () => HttpResponse.json({ connected: true })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
|
await screen.findByText('TLS verification is off');
|
|
expect(screen.queryByText('AirTrail connection saved')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-009: a failing status probe after a successful save marks it disconnected', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com', connected: true });
|
|
server.use(
|
|
http.put('/api/integrations/airtrail/settings', () => HttpResponse.json({ ok: true })),
|
|
http.get('/api/integrations/airtrail/status', () => HttpResponse.json({ error: 'down' }, { status: 500 })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
|
await screen.findByText('Not connected');
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-010: a rejected save surfaces the server error message', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com', connected: true });
|
|
server.use(
|
|
http.put('/api/integrations/airtrail/settings', () =>
|
|
HttpResponse.json({ error: 'Instance unreachable' }, { status: 400 }),
|
|
),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
|
await screen.findByText('Instance unreachable');
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-011: a successful test reports the flight count and flips the badge', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com' });
|
|
server.use(
|
|
http.post('/api/integrations/airtrail/test', () => HttpResponse.json({ connected: true, flightCount: 12 })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Test connection/ }));
|
|
await screen.findByText('Connected — 12 flight(s) found');
|
|
expect(screen.getByText('Connected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-012: a rejected test shows the returned error and keeps the badge off', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com' });
|
|
server.use(
|
|
http.post('/api/integrations/airtrail/test', () => HttpResponse.json({ connected: false, error: 'Bad token' })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Test connection/ }));
|
|
await screen.findByText('Bad token');
|
|
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-SETAIR-013: a network failure during the test falls back to the generic message', async () => {
|
|
const user = userEvent.setup();
|
|
stubSettings({ url: 'https://air.example.com' });
|
|
server.use(
|
|
http.post('/api/integrations/airtrail/test', () => HttpResponse.json({ error: 'nope' }, { status: 500 })),
|
|
);
|
|
renderSection();
|
|
|
|
await user.click(await screen.findByRole('button', { name: /Test connection/ }));
|
|
await screen.findByText('Connection failed');
|
|
});
|
|
});
|