mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
// FE-MOB-ADEV-001 to FE-MOB-ADEV-015
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { http, HttpResponse } from 'msw';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { render, screen, waitFor } from '../../../helpers/render';
|
|
import { server } from '../../../helpers/msw/server';
|
|
import { resetAllStores, seedStore } from '../../../helpers/store';
|
|
import { buildAdmin } from '../../../helpers/factories';
|
|
import { useAuthStore } from '../../../../src/store/authStore';
|
|
import { ToastContainer } from '../../../../src/components/shared/Toast';
|
|
import MAdminDevNotificationsPanel from '../../../../src/mobile/screens/admin/MAdminDevNotificationsPanel';
|
|
|
|
const ADMIN = buildAdmin({ id: 7, username: 'testadmin', email: 'admin@example.com' });
|
|
|
|
function renderPanel() {
|
|
return render(
|
|
<>
|
|
<ToastContainer />
|
|
<MAdminDevNotificationsPanel />
|
|
</>,
|
|
);
|
|
}
|
|
|
|
/** Captures every POST body the panel sends while a test runs. */
|
|
function captureSends() {
|
|
const bodies: Record<string, unknown>[] = [];
|
|
server.use(
|
|
http.post('/api/admin/dev/test-notification', async ({ request }) => {
|
|
bodies.push((await request.json()) as Record<string, unknown>);
|
|
return HttpResponse.json({ success: true });
|
|
}),
|
|
);
|
|
return bodies;
|
|
}
|
|
|
|
function clickButton(user: ReturnType<typeof userEvent.setup>, label: string) {
|
|
return user.click(screen.getByText(label).closest('button')!);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
seedStore(useAuthStore, { isAuthenticated: true, user: ADMIN });
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
});
|
|
|
|
describe('MAdminDevNotificationsPanel', () => {
|
|
it('FE-MOB-ADEV-001: renders the dev badge and all four sections once the data is in', async () => {
|
|
renderPanel();
|
|
|
|
expect(screen.getByText('DEV ONLY')).toBeInTheDocument();
|
|
expect(screen.getByText('Notification Testing')).toBeInTheDocument();
|
|
expect(screen.getByText('Type Testing')).toBeInTheDocument();
|
|
expect(screen.getByText('Admin-Scoped Events')).toBeInTheDocument();
|
|
await screen.findByText('Trip-Scoped Events');
|
|
await screen.findByText('User-Scoped Events');
|
|
});
|
|
|
|
it('FE-MOB-ADEV-002: the trip selector lists the trips from the API and preselects the first', async () => {
|
|
renderPanel();
|
|
await screen.findByText('Trip-Scoped Events');
|
|
|
|
const [tripSelect] = screen.getAllByRole('combobox');
|
|
const labels = Array.from(tripSelect.querySelectorAll('option')).map((o) => o.textContent);
|
|
expect(labels).toContain('Paris Adventure');
|
|
expect(labels).toContain('Tokyo Trip');
|
|
expect((tripSelect as HTMLSelectElement).value).toBe(
|
|
(tripSelect.querySelector('option') as HTMLOptionElement).value,
|
|
);
|
|
});
|
|
|
|
it('FE-MOB-ADEV-003: the user selector shows username and email of every user', async () => {
|
|
renderPanel();
|
|
await screen.findByText('User-Scoped Events');
|
|
|
|
const userSelect = screen.getAllByRole('combobox')[1];
|
|
const labels = Array.from(userSelect.querySelectorAll('option')).map((o) => o.textContent ?? '');
|
|
expect(labels).toContain('admin (admin@example.com)');
|
|
expect(labels).toContain('alice (alice@example.com)');
|
|
});
|
|
|
|
it('FE-MOB-ADEV-004: the type-testing buttons send the documented payloads', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Type Testing');
|
|
|
|
await clickButton(user, 'Simple → Me');
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toEqual({ event: 'test_simple', scope: 'user', targetId: 7, params: {} });
|
|
|
|
await clickButton(user, 'Boolean → Me');
|
|
await waitFor(() => expect(bodies).toHaveLength(2));
|
|
expect(bodies[1]).toMatchObject({
|
|
event: 'test_boolean',
|
|
scope: 'user',
|
|
targetId: 7,
|
|
inApp: {
|
|
type: 'boolean',
|
|
positiveCallback: { action: 'test_approve', payload: {} },
|
|
negativeCallback: { action: 'test_deny', payload: {} },
|
|
},
|
|
});
|
|
|
|
await clickButton(user, 'Navigate → Me');
|
|
await waitFor(() => expect(bodies).toHaveLength(3));
|
|
expect(bodies[2]).toMatchObject({ event: 'test_navigate', scope: 'user', targetId: 7 });
|
|
|
|
await clickButton(user, 'Simple → All Admins');
|
|
await waitFor(() => expect(bodies).toHaveLength(4));
|
|
expect(bodies[3]).toMatchObject({ event: 'test_simple', scope: 'admin', targetId: 0 });
|
|
});
|
|
|
|
it('FE-MOB-ADEV-005: every trip-scoped button targets the selected trip with its own params', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Trip-Scoped Events');
|
|
|
|
const [tripSelect] = screen.getAllByRole('combobox');
|
|
const tripId = Number((tripSelect as HTMLSelectElement).value);
|
|
const tripTitle = tripSelect.querySelector('option')!.textContent;
|
|
|
|
await clickButton(user, 'booking_change');
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toEqual({
|
|
event: 'booking_change',
|
|
scope: 'trip',
|
|
targetId: tripId,
|
|
params: {
|
|
actor: 'testadmin',
|
|
trip: tripTitle,
|
|
booking: 'Test Hotel',
|
|
type: 'hotel',
|
|
tripId: String(tripId),
|
|
},
|
|
});
|
|
|
|
await clickButton(user, 'trip_reminder');
|
|
await waitFor(() => expect(bodies).toHaveLength(2));
|
|
expect(bodies[1]).toEqual({
|
|
event: 'trip_reminder',
|
|
scope: 'trip',
|
|
targetId: tripId,
|
|
params: { trip: tripTitle, tripId: String(tripId) },
|
|
});
|
|
|
|
await clickButton(user, 'photos_shared');
|
|
await waitFor(() => expect(bodies).toHaveLength(3));
|
|
expect(bodies[2]).toMatchObject({ event: 'photos_shared', params: { count: '5' } });
|
|
|
|
await clickButton(user, 'collab_message');
|
|
await waitFor(() => expect(bodies).toHaveLength(4));
|
|
expect(bodies[3]).toMatchObject({
|
|
event: 'collab_message',
|
|
params: { preview: 'This is a test message preview.' },
|
|
});
|
|
|
|
await clickButton(user, 'packing_tagged');
|
|
await waitFor(() => expect(bodies).toHaveLength(5));
|
|
expect(bodies[4]).toMatchObject({ event: 'packing_tagged', params: { category: 'Clothing' } });
|
|
});
|
|
|
|
it('FE-MOB-ADEV-006: the user-scoped buttons target the selected recipient', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('User-Scoped Events');
|
|
|
|
const userSelect = screen.getAllByRole('combobox')[1] as HTMLSelectElement;
|
|
const recipientId = Number(userSelect.value);
|
|
|
|
await clickButton(user, 'trip_invite');
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toMatchObject({
|
|
event: 'trip_invite',
|
|
scope: 'user',
|
|
targetId: recipientId,
|
|
params: { actor: 'testadmin', invitee: 'admin@example.com' },
|
|
});
|
|
|
|
await clickButton(user, 'vacay_invite');
|
|
await waitFor(() => expect(bodies).toHaveLength(2));
|
|
expect(bodies[1]).toEqual({
|
|
event: 'vacay_invite',
|
|
scope: 'user',
|
|
targetId: recipientId,
|
|
params: { actor: 'testadmin', planId: '1' },
|
|
});
|
|
});
|
|
|
|
it('FE-MOB-ADEV-007: the admin-scoped button announces a fake version', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Admin-Scoped Events');
|
|
|
|
await clickButton(user, 'version_available');
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toEqual({
|
|
event: 'version_available',
|
|
scope: 'admin',
|
|
targetId: 0,
|
|
params: { version: '9.9.9-test' },
|
|
});
|
|
});
|
|
|
|
it('FE-MOB-ADEV-008: changing the trip selector changes the payload target', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Trip-Scoped Events');
|
|
|
|
const [tripSelect] = screen.getAllByRole('combobox');
|
|
const tokyo = Array.from(tripSelect.querySelectorAll('option')).find(
|
|
(o) => o.textContent === 'Tokyo Trip',
|
|
)!;
|
|
await user.selectOptions(tripSelect, tokyo.value);
|
|
await clickButton(user, 'trip_reminder');
|
|
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toMatchObject({ targetId: Number(tokyo.value), params: { trip: 'Tokyo Trip' } });
|
|
});
|
|
|
|
it('FE-MOB-ADEV-009: changing the user selector changes the recipient', async () => {
|
|
const bodies = captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('User-Scoped Events');
|
|
|
|
const userSelect = screen.getAllByRole('combobox')[1];
|
|
const alice = Array.from(userSelect.querySelectorAll('option')).find(
|
|
(o) => o.textContent?.startsWith('alice'),
|
|
)!;
|
|
await user.selectOptions(userSelect, alice.value);
|
|
await clickButton(user, 'trip_invite');
|
|
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toMatchObject({
|
|
targetId: Number(alice.value),
|
|
params: { invitee: 'alice@example.com' },
|
|
});
|
|
});
|
|
|
|
it('FE-MOB-ADEV-010: a successful send raises a toast naming the fired button', async () => {
|
|
captureSends();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Type Testing');
|
|
|
|
await clickButton(user, 'Simple → Me');
|
|
expect(await screen.findByText('Sent: simple-me')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-ADEV-011: a failing send raises an error toast', async () => {
|
|
server.use(
|
|
http.post('/api/admin/dev/test-notification', () => HttpResponse.json({}, { status: 500 })),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Type Testing');
|
|
|
|
await clickButton(user, 'Simple → Me');
|
|
expect(await screen.findByText(/failed|error/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-ADEV-012: every button is disabled while a send is in flight', async () => {
|
|
server.use(
|
|
http.post('/api/admin/dev/test-notification', async () => {
|
|
await new Promise(() => {});
|
|
return HttpResponse.json({ success: true });
|
|
}),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('Type Testing');
|
|
|
|
void clickButton(user, 'Simple → Me');
|
|
|
|
await waitFor(() => {
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(1);
|
|
buttons.forEach((btn) => expect(btn).toBeDisabled());
|
|
});
|
|
// The fired button shows its own spinner
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-ADEV-013: the scoped sections stay hidden when both lookups fail', async () => {
|
|
server.use(
|
|
http.get('/api/trips', () => HttpResponse.json({}, { status: 500 })),
|
|
http.get('/api/admin/users', () => HttpResponse.json({}, { status: 500 })),
|
|
);
|
|
renderPanel();
|
|
|
|
await screen.findByText('Type Testing');
|
|
await waitFor(() => expect(screen.queryAllByRole('combobox')).toHaveLength(0));
|
|
expect(screen.queryByText('Trip-Scoped Events')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('User-Scoped Events')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-ADEV-014: bare arrays are accepted as well as the wrapped payloads', async () => {
|
|
server.use(
|
|
http.get('/api/trips', () => HttpResponse.json([{ id: 55, title: 'Bare Trip' }])),
|
|
http.get('/api/admin/users', () =>
|
|
HttpResponse.json([{ id: 66, username: 'bare', email: 'bare@example.com' }]),
|
|
),
|
|
);
|
|
renderPanel();
|
|
await screen.findByText('Trip-Scoped Events');
|
|
|
|
const [tripSelect, userSelect] = screen.getAllByRole('combobox') as HTMLSelectElement[];
|
|
expect(tripSelect.value).toBe('55');
|
|
expect(screen.getByText('Bare Trip')).toBeInTheDocument();
|
|
expect(userSelect.value).toBe('66');
|
|
expect(screen.getByText('bare (bare@example.com)')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-ADEV-015: without trips or a signed-in user the invite falls back to defaults', async () => {
|
|
const bodies = captureSends();
|
|
server.use(http.get('/api/trips', () => HttpResponse.json({ trips: [] })));
|
|
resetAllStores();
|
|
const user = userEvent.setup();
|
|
renderPanel();
|
|
await screen.findByText('User-Scoped Events');
|
|
|
|
await clickButton(user, 'trip_invite');
|
|
|
|
await waitFor(() => expect(bodies).toHaveLength(1));
|
|
expect(bodies[0]).toMatchObject({
|
|
event: 'trip_invite',
|
|
params: { actor: 'Admin', trip: 'Test Trip', tripId: '0' },
|
|
});
|
|
});
|
|
});
|