Skip to content

Commit ce7cfb5

Browse files
committed
Extend test coverage for the History view (#30)
Cover the anonymous login redirect, back-button navigation, empty histories, the absolute-date tooltip, and the revert action's input validation and error responses.
1 parent 70d8e6c commit ce7cfb5

3 files changed

Lines changed: 138 additions & 0 deletions

File tree

packages/cmsui/acceptance/tests/history.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@ test.describe('History route', () => {
5151
await expect(page.locator('tbody tr').first()).toBeVisible();
5252
});
5353

54+
test('redirects anonymous visitors to the login', async ({ page }) => {
55+
// a published page: on private content the middleware already fails the
56+
// anonymous content fetch (error boundary) before the loader's auth
57+
// guard can redirect
58+
await createContent(page, {
59+
contentType: 'Document',
60+
contentId: 'public-page',
61+
contentTitle: 'Public Page',
62+
transition: 'publish',
63+
});
64+
await page.context().clearCookies();
65+
66+
await page.goto('/@@history/public-page');
67+
68+
await expect(page).toHaveURL(/\/login/);
69+
});
70+
71+
test('navigates back to the content via the toolbar back button', async ({
72+
page,
73+
}) => {
74+
await openHistory(page);
75+
76+
await page.getByRole('link', { name: 'Back' }).click();
77+
78+
await expect(page).toHaveURL(/\/my-page$/);
79+
await expect(
80+
page.getByRole('heading', { name: 'My Page', exact: true }),
81+
).toBeVisible();
82+
});
83+
5484
test('asks for confirmation before reverting', async ({ page }) => {
5585
// two edits, so an older, revertable version exists
5686
await editTitle(page, 'My Page (v2)');

packages/cmsui/components/History/HistoryView.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,33 @@ describe('HistoryView', () => {
213213
);
214214
});
215215

216+
it('shows the absolute date as tooltip on the relative time', () => {
217+
render(<HistoryView content={content} history={history} />);
218+
219+
const times = document.querySelectorAll('time');
220+
expect(times.length).toBe(history.length);
221+
for (const time of times) {
222+
expect(time).toHaveAttribute('dateTime');
223+
// the full-date tooltip (browser locale, so only check presence)
224+
expect(time.getAttribute('title')).toBeTruthy();
225+
}
226+
});
227+
228+
it('renders an empty history without errors', () => {
229+
render(
230+
<HistoryView content={content} history={[] as GetHistoryResponse} />,
231+
);
232+
233+
expect(
234+
screen.getByRole('heading', {
235+
level: 1,
236+
name: 'cmsui.history.changesTo',
237+
}),
238+
).toBeInTheDocument();
239+
// only the header row remains
240+
expect(screen.getAllByRole('row')).toHaveLength(1);
241+
});
242+
216243
it('survives entries with an unparsable time', () => {
217244
render(
218245
<HistoryView
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { expect, describe, it, vi, afterEach } from 'vitest';
2+
import { RouterContextProvider } from 'react-router';
3+
import {
4+
ploneClientContext,
5+
ploneContentContext,
6+
} from '@plone/aurora/app/middleware.server';
7+
import { action } from './history';
8+
9+
vi.mock('@plone/react-router', () => ({
10+
requireAuthCookie: vi.fn().mockResolvedValue('token'),
11+
}));
12+
13+
function buildArgs(body: Record<string, string>, revertMock: unknown) {
14+
const context = new RouterContextProvider();
15+
context.set(ploneClientContext, { revertHistory: revertMock } as never);
16+
context.set(ploneContentContext, { '@id': '/my-page' } as never);
17+
18+
const formData = new FormData();
19+
for (const [key, value] of Object.entries(body)) {
20+
formData.append(key, value);
21+
}
22+
const request = new Request('http://example.com/@@history/my-page', {
23+
method: 'POST',
24+
body: formData,
25+
});
26+
27+
return {
28+
request,
29+
params: {},
30+
context,
31+
unstable_pattern: '/@@history/*',
32+
unstable_url: new URL(request.url),
33+
};
34+
}
35+
36+
describe('action', () => {
37+
afterEach(() => {
38+
vi.clearAllMocks();
39+
});
40+
41+
it('reverts to the requested version', async () => {
42+
const revertMock = vi.fn().mockResolvedValue({});
43+
44+
const result = await action(buildArgs({ version: '2' }, revertMock));
45+
46+
expect(revertMock).toHaveBeenCalledWith({
47+
path: '/my-page',
48+
data: { version: 2 },
49+
});
50+
expect(result.data).toEqual({ ok: true });
51+
});
52+
53+
it('rejects a missing version without calling the API', async () => {
54+
const revertMock = vi.fn();
55+
56+
const result = await action(buildArgs({}, revertMock));
57+
58+
expect(revertMock).not.toHaveBeenCalled();
59+
expect(result.data).toEqual({ ok: false, error: 'invalidVersion' });
60+
expect(result.init).toEqual({ status: 400 });
61+
});
62+
63+
it('rejects a non-numeric version without calling the API', async () => {
64+
const revertMock = vi.fn();
65+
66+
const result = await action(buildArgs({ version: 'abc' }, revertMock));
67+
68+
expect(revertMock).not.toHaveBeenCalled();
69+
expect(result.data).toEqual({ ok: false, error: 'invalidVersion' });
70+
expect(result.init).toEqual({ status: 400 });
71+
});
72+
73+
it('returns the failure as data when the revert call throws', async () => {
74+
const revertMock = vi.fn().mockRejectedValue(new Error('forbidden'));
75+
76+
const result = await action(buildArgs({ version: '1' }, revertMock));
77+
78+
expect(result.data).toEqual({ ok: false, error: 'revertFailed' });
79+
expect(result.init).toEqual({ status: 502 });
80+
});
81+
});

0 commit comments

Comments
 (0)