// FE-MOB-APERM-001 to FE-MOB-APERM-011
import React from 'react';
import { describe, it, expect, beforeEach, afterEach } 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 { usePermissionsStore } from '../../../../src/store/permissionsStore';
import { ToastContainer } from '../../../../src/components/shared/Toast';
import MAdminPermissionsPanel from '../../../../src/mobile/screens/admin/MAdminPermissionsPanel';
const ALLOWED = ['admin', 'trip_owner', 'trip_member', 'everybody'];
function buildPermission(key: string, level = 'trip_member', defaultLevel = 'trip_member') {
return { key, level, defaultLevel, allowedLevels: [...ALLOWED] };
}
const ALL_KEYS = [
'trip_create', 'trip_edit', 'trip_delete', 'trip_archive', 'trip_cover_upload',
'member_manage',
'file_upload', 'file_edit', 'file_delete',
'place_edit', 'day_edit', 'reservation_edit',
'budget_edit', 'packing_edit', 'collab_edit', 'share_manage',
];
const SAMPLE_PERMISSIONS = ALL_KEYS.map((k) => buildPermission(k));
function renderPanel() {
return render(
<>
>,
);
}
beforeEach(() => {
resetAllStores();
server.use(
http.get('/api/admin/permissions', () => HttpResponse.json({ permissions: SAMPLE_PERMISSIONS })),
);
});
afterEach(() => {
server.resetHandlers();
});
describe('MAdminPermissionsPanel', () => {
it('FE-MOB-APERM-001: renders only the spinner while loading', () => {
server.use(
http.get('/api/admin/permissions', async () => {
await new Promise(() => {});
return HttpResponse.json({ permissions: [] });
}),
);
renderPanel();
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
expect(screen.queryByText('Permission Settings')).not.toBeInTheDocument();
});
it('FE-MOB-APERM-002: renders every category and its actions after load', async () => {
renderPanel();
await screen.findByText('Permission Settings');
expect(screen.getByText('Control who can perform actions across the application')).toBeInTheDocument();
expect(screen.getByText('Trip Management')).toBeInTheDocument();
expect(screen.getByText('Member Management')).toBeInTheDocument();
expect(screen.getByText('Files')).toBeInTheDocument();
expect(screen.getByText('Content & Schedule')).toBeInTheDocument();
expect(screen.getByText('Budget, Packing & Collaboration')).toBeInTheDocument();
expect(screen.getByText('Create trips')).toBeInTheDocument();
expect(screen.getByText('Who can create new trips')).toBeInTheDocument();
expect(screen.getAllByRole('combobox')).toHaveLength(ALL_KEYS.length);
});
it('FE-MOB-APERM-003: a select reflects the stored level and offers every allowed level', async () => {
renderPanel();
await screen.findByText('Permission Settings');
const select = screen.getByRole('combobox', { name: 'Create trips' }) as HTMLSelectElement;
expect(select.value).toBe('trip_member');
expect(Array.from(select.options).map((o) => o.textContent)).toEqual([
'Admin only', 'Trip owner', 'Trip members', 'Everyone',
]);
});
it('FE-MOB-APERM-004: keys missing from the payload are skipped', async () => {
server.use(
http.get('/api/admin/permissions', () =>
HttpResponse.json({ permissions: [buildPermission('trip_create')] }),
),
);
renderPanel();
await screen.findByText('Permission Settings');
expect(screen.getByText('Create trips')).toBeInTheDocument();
expect(screen.queryByText('Delete trips')).not.toBeInTheDocument();
expect(screen.getAllByRole('combobox')).toHaveLength(1);
});
it('FE-MOB-APERM-005: the customized badge shows only where level differs from the default', async () => {
server.use(
http.get('/api/admin/permissions', () =>
HttpResponse.json({
permissions: [
buildPermission('trip_create', 'admin', 'trip_member'),
buildPermission('trip_edit', 'trip_member', 'trip_member'),
],
}),
),
);
renderPanel();
await screen.findByText('Permission Settings');
expect(screen.getAllByText('customized')).toHaveLength(1);
});
it('FE-MOB-APERM-006: Save stays disabled until a select changes', async () => {
const user = userEvent.setup();
renderPanel();
await screen.findByText('Permission Settings');
const save = screen.getByRole('button', { name: /Save/ });
expect(save).toBeDisabled();
await user.selectOptions(screen.getByRole('combobox', { name: 'Create trips' }), 'admin');
expect(save).not.toBeDisabled();
expect(screen.getAllByText('customized')).toHaveLength(1);
});
it('FE-MOB-APERM-007: Reset puts every value back to its default and marks the form dirty', async () => {
const user = userEvent.setup();
server.use(
http.get('/api/admin/permissions', () =>
HttpResponse.json({
permissions: [
buildPermission('trip_create', 'admin', 'trip_member'),
buildPermission('trip_edit', 'everybody', 'trip_owner'),
],
}),
),
);
renderPanel();
await screen.findByText('Permission Settings');
expect(screen.getAllByText('customized')).toHaveLength(2);
await user.click(screen.getByRole('button', { name: 'Reset to defaults' }));
await waitFor(() => expect(screen.queryByText('customized')).not.toBeInTheDocument());
expect((screen.getByRole('combobox', { name: 'Create trips' }) as HTMLSelectElement).value).toBe('trip_member');
expect((screen.getByRole('combobox', { name: 'Edit trip details' }) as HTMLSelectElement).value).toBe('trip_owner');
expect(screen.getByRole('button', { name: /Save/ })).not.toBeDisabled();
});
it('FE-MOB-APERM-008: saving PUTs the current values and pushes them into the store', async () => {
const user = userEvent.setup();
let sent: Record = {};
server.use(
http.put('/api/admin/permissions', async ({ request }) => {
sent = (await request.json()) as Record;
return HttpResponse.json({ permissions: { trip_create: 'admin' } });
}),
);
renderPanel();
await screen.findByText('Permission Settings');
await user.selectOptions(screen.getByRole('combobox', { name: 'Create trips' }), 'admin');
await user.click(screen.getByRole('button', { name: /Save/ }));
await screen.findByText('Permission settings saved');
expect((sent.permissions as Record).trip_create).toBe('admin');
expect(usePermissionsStore.getState().permissions.trip_create).toBe('admin');
await waitFor(() => expect(screen.getByRole('button', { name: /Save/ })).toBeDisabled());
});
it('FE-MOB-APERM-009: a response without permissions leaves the store untouched', async () => {
const user = userEvent.setup();
const before = usePermissionsStore.getState().permissions;
server.use(http.put('/api/admin/permissions', () => HttpResponse.json({ ok: true })));
renderPanel();
await screen.findByText('Permission Settings');
await user.selectOptions(screen.getByRole('combobox', { name: 'Create trips' }), 'admin');
await user.click(screen.getByRole('button', { name: /Save/ }));
await screen.findByText('Permission settings saved');
expect(usePermissionsStore.getState().permissions).toBe(before);
});
it('FE-MOB-APERM-010: a failing save toasts and keeps the form dirty', async () => {
const user = userEvent.setup();
server.use(
http.put('/api/admin/permissions', () => HttpResponse.json({ error: 'boom' }, { status: 500 })),
);
renderPanel();
await screen.findByText('Permission Settings');
await user.selectOptions(screen.getByRole('combobox', { name: 'Create trips' }), 'admin');
const save = screen.getByRole('button', { name: /Save/ });
await user.click(save);
await screen.findByText('Error');
expect(save).not.toBeDisabled();
});
it('FE-MOB-APERM-011: a failing load toasts and renders no categories', async () => {
server.use(
http.get('/api/admin/permissions', () => HttpResponse.json({ error: 'boom' }, { status: 500 })),
);
renderPanel();
await screen.findByText('Error');
expect(screen.getByText('Permission Settings')).toBeInTheDocument();
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
});
});