feat: implement useUrlState hook for managing URL-based state in admin pages
- Refactor multiple admin pages (BlogsPage, ClinicsPage, DoctorsPage, etc.) to utilize the new useUrlState hook for managing pagination, search, and filter states via URL. - Ensure that the state persists in the URL, allowing users to return to the same state when navigating back from detail pages. - Update relevant components to handle state changes appropriately and maintain clean URLs by removing default values. - Add SlotPicker component for selecting appointment slots based on availability. - Create tests for useUrlState to validate its functionality and ensure correct behavior when interacting with the URL. - Update API documentation to reflect changes in appointment creation and slot selection processes.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { useUrlState, pageOf } from './useUrlState';
|
||||
import BackButton from '../components/ui/BackButton';
|
||||
|
||||
/**
|
||||
* قرارداد: وضعیت لیست در URL است، پس «بازگشت» (که همان URL قبلی را برمیگرداند)
|
||||
* فیلتر و صفحه را هم برمیگرداند.
|
||||
*/
|
||||
function ListPage() {
|
||||
const [state, setState] = useUrlState({ page: '1', search: '', status: '' });
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="qs">{location.search}</span>
|
||||
<span data-testid="state">{`${state.page}|${state.search}|${state.status}`}</span>
|
||||
<button onClick={() => setState({ search: 'مریم', page: '1' })}>جستجو</button>
|
||||
<button onClick={() => setState({ status: 'paid', page: '1' })}>فیلتر</button>
|
||||
<button onClick={() => setState({ page: '3' })}>صفحه ۳</button>
|
||||
<button onClick={() => { setState({ status: 'paid' }); setState({ page: '2' }); }}>دو تغییر پشتسرهم</button>
|
||||
<a href="/detail" onClick={(e) => { e.preventDefault(); window.history.pushState({}, '', '/detail'); }}>x</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailPage() {
|
||||
return (
|
||||
<>
|
||||
<span>جزئیات</span>
|
||||
<BackButton fallback="/list" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderApp(entries: string[] = ['/list']) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={entries}>
|
||||
<Routes>
|
||||
<Route path="/list" element={<ListPage />} />
|
||||
<Route path="/detail" element={<DetailPage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('useUrlState', () => {
|
||||
it('مقدار اولیه را از URL میخواند', () => {
|
||||
renderApp(['/list?page=2&search=علی&status=paid']);
|
||||
expect(screen.getByTestId('state')).toHaveTextContent('2|علی|paid');
|
||||
});
|
||||
|
||||
it('تغییر فیلتر در query string مینشیند و صفحه را یک میکند', () => {
|
||||
renderApp(['/list?page=4']);
|
||||
fireEvent.click(screen.getByText('فیلتر'));
|
||||
|
||||
expect(screen.getByTestId('qs').textContent).toContain('status=paid');
|
||||
expect(screen.getByTestId('state')).toHaveTextContent('1|');
|
||||
});
|
||||
|
||||
it('مقدارِ برابرِ پیشفرض از URL حذف میشود', () => {
|
||||
renderApp(['/list?page=3']);
|
||||
fireEvent.click(screen.getByText('صفحه ۳'));
|
||||
expect(screen.getByTestId('qs').textContent).toContain('page=3');
|
||||
|
||||
fireEvent.click(screen.getByText('فیلتر')); // page → '1' = پیشفرض
|
||||
expect(screen.getByTestId('qs').textContent).not.toContain('page=');
|
||||
});
|
||||
|
||||
it('دو تغییرِ پشتسرهم در یک handler همدیگر را پاک نمیکنند', () => {
|
||||
renderApp(['/list']);
|
||||
fireEvent.click(screen.getByText('دو تغییر پشتسرهم'));
|
||||
|
||||
// مودالِ فیلتر دقیقاً همین کار را میکند: setFilters(...) و بعد setPage(1).
|
||||
expect(screen.getByTestId('state')).toHaveTextContent('2||paid');
|
||||
});
|
||||
|
||||
it('بازگشت از صفحهٔ جزئیات، فیلتر و صفحهٔ لیست را دستنخورده برمیگرداند', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/list?page=3&search=مریم&status=paid', '/detail']} initialIndex={1}>
|
||||
<Routes>
|
||||
<Route path="/list" element={<ListPage />} />
|
||||
<Route path="/detail" element={<DetailPage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('جزئیات')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /بازگشت/ }));
|
||||
|
||||
expect(screen.getByTestId('state')).toHaveTextContent('3|مریم|paid');
|
||||
});
|
||||
|
||||
it('pageOf مقدار نامعتبر را به ۱ میبرد', () => {
|
||||
expect(pageOf('')).toBe(1);
|
||||
expect(pageOf('0')).toBe(1);
|
||||
expect(pageOf('abc')).toBe(1);
|
||||
expect(pageOf('7')).toBe(7);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user