// FE-ADMIN-GH-001 to FE-ADMIN-GH-016
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { resetAllStores } from '../../../tests/helpers/store';
import GitHubPanel from './GitHubPanel';
function buildRelease(overrides = {}) {
const id = Math.random();
return {
id,
tag_name: 'v1.0.0',
name: 'Initial Release',
body: '## Changes\n- Fixed bug\n- **Bold improvement**\n- `code snippet`',
published_at: '2025-01-15T12:00:00Z',
created_at: '2025-01-15T12:00:00Z',
prerelease: false,
author: { login: 'mauriceboe' },
...overrides,
};
}
const PAGE_1 = Array.from({ length: 10 }, (_, i) =>
buildRelease({ id: i + 1, tag_name: `v1.${i}.0` }),
);
const PAGE_2 = Array.from({ length: 5 }, (_, i) =>
buildRelease({ id: 100 + i, tag_name: `v0.${i}.0` }),
);
beforeEach(() => {
resetAllStores();
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json([])),
);
});
afterEach(() => {
server.resetHandlers();
});
describe('GitHubPanel', () => {
it('FE-ADMIN-GH-001: support link cards always render', async () => {
render(
expect(container.querySelector('code')).toBeInTheDocument();
expect(container.querySelector('code')!.textContent).toBe('inline code');
});
it('FE-ADMIN-GH-010: "Load more" button visible when full page returned', async () => {
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json(PAGE_1)),
);
render( );
await screen.findByText(`v1.0.0`);
expect(screen.getByText('Load more')).toBeInTheDocument();
});
it('FE-ADMIN-GH-011: "Load more" hidden when partial page returned', async () => {
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json(PAGE_2)),
);
render( );
await screen.findByText('v0.0.0');
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
});
it('FE-ADMIN-GH-013: release body renders plain paragraph text', async () => {
const r = buildRelease({
id: 40,
tag_name: 'v1.7.0',
body: 'This is a plain paragraph without any markdown syntax.',
});
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json([r])),
);
const user = userEvent.setup();
render( );
await screen.findByText('v1.7.0');
await user.click(screen.getByText('Show details'));
await screen.findByText('This is a plain paragraph without any markdown syntax.');
});
it('FE-ADMIN-GH-014: markdown link with safe href renders as anchor', async () => {
const r = buildRelease({
id: 41,
tag_name: 'v1.8.0',
body: '- [click here](https://example.com)',
});
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json([r])),
);
const user = userEvent.setup();
render( );
await screen.findByText('v1.8.0');
await user.click(screen.getByText('Show details'));
const link = await screen.findByText('click here');
expect(link.closest('a') || link.tagName.toLowerCase() === 'a' ? link : null).not.toBeNull();
});
it('FE-ADMIN-GH-015: javascript: link is sanitized to #', async () => {
const r = buildRelease({
id: 42,
tag_name: 'v1.9.0',
body: '- [evil](javascript:alert(1))',
});
server.use(
http.get('/api/admin/github-releases', () => HttpResponse.json([r])),
);
const user = userEvent.setup();
render( );
await screen.findByText('v1.9.0');
await user.click(screen.getByText('Show details'));
const link = await screen.findByText('evil');
const anchor = link.closest('a') ?? link;
// The unsafe href is replaced with '#'
expect(anchor).toHaveAttribute('href', '#');
});
it('FE-ADMIN-GH-016: support card hover effects fire without error', async () => {
render( );
await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument());
const kofiLink = screen.getByText('Ko-fi').closest('a')!;
fireEvent.mouseEnter(kofiLink);
fireEvent.mouseLeave(kofiLink);
const discordLink = screen.getByText('Discord').closest('a')!;
fireEvent.mouseEnter(discordLink);
fireEvent.mouseLeave(discordLink);
const bugLink = screen.getByText('Report a Bug').closest('a')!;
fireEvent.mouseEnter(bugLink);
fireEvent.mouseLeave(bugLink);
const featureLink = screen.getByText('Feature Request').closest('a')!;
fireEvent.mouseEnter(featureLink);
fireEvent.mouseLeave(featureLink);
const wikiLink = screen.getByText('Wiki').closest('a')!;
fireEvent.mouseEnter(wikiLink);
fireEvent.mouseLeave(wikiLink);
const bmcLink = screen.getByText('Buy Me a Coffee').closest('a')!;
fireEvent.mouseEnter(bmcLink);
fireEvent.mouseLeave(bmcLink);
// All links still visible
expect(screen.getByText('Ko-fi')).toBeInTheDocument();
});
it('FE-ADMIN-GH-012: clicking "Load more" appends next page', async () => {
server.use(
http.get('/api/admin/github-releases', ({ request }) => {
const url = new URL(request.url);
const page = url.searchParams.get('page');
if (page === '2') {
return HttpResponse.json(PAGE_2);
}
return HttpResponse.json(PAGE_1);
}),
);
const user = userEvent.setup();
render( );
await screen.findByText('v1.0.0');
// All 10 items from page 1 visible
expect(screen.getAllByText(/v1\.\d\.0/).length).toBe(10);
// Click Load more
await user.click(screen.getByText('Load more'));
// Wait for page 2 items to appear
await screen.findByText('v0.0.0');
// Total: 10 from page 1 + 5 from page 2 = 15
const tagEls = screen.getAllByText(/^v[01]\.\d\.0$/);
expect(tagEls.length).toBe(15);
// Load more should be hidden (PAGE_2 < 10)
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
});
});