mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
e4e49381d8
On the phone, the admin GitHub panel showed "Latest updates from mauriceboe/TREK" and its releases, bug report, feature request and wiki links, as well as the same three links in the settings About section, all went to the old mauriceboe/TREK repository. The desktop panels already use liketrek/TREK. Cause: the phone screens kept the repository name from before the move to the liketrek organisation. Fix: MAdminGitHubPanel and MSettingsAbout now use liketrek/TREK, like GitHubPanel and AboutTab. No other user facing mauriceboe/TREK link is left in the client (the Ko-fi, Buy Me a Coffee and Docker image names are unchanged and identical on both sides). Red test: FE-MOB-SET-ABOUT-001 (new, MSettingsAbout.test.tsx) failed with "Expected the element to have attribute: href= https://github.com/liketrek/TREK/issues/new?template=bug_report.yml". Changed assertions in tests/unit/mobile/admin/MAdminGitHubPanel.test.tsx: - FE-MOB-AGH-001: wiki href was https://github.com/mauriceboe/TREK/wiki, now https://github.com/liketrek/TREK/wiki - FE-MOB-AGH-004: subtitle was "Latest updates from mauriceboe/TREK", now "Latest updates from liketrek/TREK"; GitHub link was https://github.com/mauriceboe/TREK/releases, now https://github.com/liketrek/TREK/releases
302 lines
12 KiB
TypeScript
302 lines
12 KiB
TypeScript
// FE-MOB-AGH-001 to FE-MOB-AGH-019
|
|
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 } from '../../../helpers/store';
|
|
import MAdminGitHubPanel from '../../../../src/mobile/screens/admin/MAdminGitHubPanel';
|
|
|
|
interface ReleaseOverrides {
|
|
id?: number;
|
|
tag_name?: string;
|
|
name?: string | null;
|
|
body?: string | null;
|
|
published_at?: string | null;
|
|
created_at?: string;
|
|
prerelease?: boolean;
|
|
author?: { login: string } | null;
|
|
}
|
|
|
|
function buildRelease(overrides: ReleaseOverrides = {}) {
|
|
return {
|
|
id: 1,
|
|
tag_name: 'v1.0.0',
|
|
name: 'Initial Release',
|
|
body: null,
|
|
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: 3 }, (_, i) => buildRelease({ id: 100 + i, tag_name: `v0.${i}.0` }));
|
|
|
|
function releasesRespondWith(payload: unknown) {
|
|
server.use(http.get('/api/admin/github-releases', () => HttpResponse.json(payload)));
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
releasesRespondWith([]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
});
|
|
|
|
async function renderPanel(props: { isPrerelease?: boolean } = {}) {
|
|
const view = render(<MAdminGitHubPanel {...props} />);
|
|
await waitFor(() => expect(document.querySelector('.animate-spin')).not.toBeInTheDocument());
|
|
return view;
|
|
}
|
|
|
|
describe('MAdminGitHubPanel', () => {
|
|
it('FE-MOB-AGH-001: renders the six support cards with their external hrefs', async () => {
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('Ko-fi').closest('a')).toHaveAttribute('href', 'https://ko-fi.com/mauriceboe');
|
|
expect(screen.getByText('Buy Me a Coffee').closest('a')).toHaveAttribute(
|
|
'href',
|
|
'https://buymeacoffee.com/mauriceboe',
|
|
);
|
|
const discord = screen.getByText('Discord').closest('a')!;
|
|
expect(discord).toHaveAttribute('href', 'https://discord.gg/NhZBDSd4qW');
|
|
expect(discord).toHaveAttribute('target', '_blank');
|
|
expect(discord).toHaveAttribute('rel', 'noopener noreferrer');
|
|
expect(screen.getByText('Report a Bug')).toBeInTheDocument();
|
|
expect(screen.getByText('Feature Request')).toBeInTheDocument();
|
|
expect(screen.getByText('Wiki').closest('a')).toHaveAttribute('href', 'https://github.com/liketrek/TREK/wiki');
|
|
expect(screen.getAllByText('Helps me keep building TREK')).toHaveLength(2);
|
|
});
|
|
|
|
it('FE-MOB-AGH-002: shows a spinner while the releases request is in flight', () => {
|
|
server.use(
|
|
http.get('/api/admin/github-releases', async () => {
|
|
await new Promise(() => {});
|
|
return HttpResponse.json([]);
|
|
}),
|
|
);
|
|
render(<MAdminGitHubPanel />);
|
|
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
expect(screen.queryByText('Release History')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-003: renders the error card with the axios message when the request fails', async () => {
|
|
server.use(
|
|
http.get('/api/admin/github-releases', () => HttpResponse.json({ message: 'boom' }, { status: 500 })),
|
|
);
|
|
render(<MAdminGitHubPanel />);
|
|
|
|
await screen.findByText('Failed to load releases');
|
|
expect(screen.getByText(/500/)).toBeInTheDocument();
|
|
expect(screen.queryByText('Release History')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-004: renders the timeline header with the repo subtitle and the GitHub link', async () => {
|
|
releasesRespondWith([buildRelease()]);
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('Release History')).toBeInTheDocument();
|
|
expect(screen.getByText('Latest updates from liketrek/TREK')).toBeInTheDocument();
|
|
expect(screen.getByText('GitHub').closest('a')).toHaveAttribute(
|
|
'href',
|
|
'https://github.com/liketrek/TREK/releases',
|
|
);
|
|
});
|
|
|
|
it('FE-MOB-AGH-005: marks only the first release as latest and shows the release name and author', async () => {
|
|
releasesRespondWith([
|
|
buildRelease({ id: 1, tag_name: 'v2.0.0', name: 'Big Bang' }),
|
|
buildRelease({ id: 2, tag_name: 'v1.9.0', name: 'v1.9.0' }),
|
|
]);
|
|
await renderPanel();
|
|
|
|
expect(screen.getAllByText('Latest')).toHaveLength(1);
|
|
expect(screen.getByText('v2.0.0')).toBeInTheDocument();
|
|
// name === tag_name on the second release, so it is not repeated
|
|
expect(screen.getByText('Big Bang')).toBeInTheDocument();
|
|
expect(screen.getAllByText('v1.9.0')).toHaveLength(1);
|
|
expect(screen.getAllByText('by mauriceboe')).toHaveLength(2);
|
|
});
|
|
|
|
it('FE-MOB-AGH-006: falls back to created_at when published_at is null and hides a missing author', async () => {
|
|
releasesRespondWith([
|
|
buildRelease({ published_at: null, created_at: '2024-03-02T08:00:00Z', author: null, name: null }),
|
|
]);
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('Mar 2, 2024')).toBeInTheDocument();
|
|
expect(screen.queryByText(/by /)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-007: hides prereleases unless the panel runs in prerelease mode', async () => {
|
|
releasesRespondWith([
|
|
buildRelease({ id: 1, tag_name: 'v3.0.0-beta.1', prerelease: true }),
|
|
buildRelease({ id: 2, tag_name: 'v2.9.0' }),
|
|
]);
|
|
await renderPanel();
|
|
|
|
expect(screen.queryByText('v3.0.0-beta.1')).not.toBeInTheDocument();
|
|
expect(screen.getByText('v2.9.0')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-008: prerelease mode shows the pre-release badge', async () => {
|
|
releasesRespondWith([buildRelease({ id: 1, tag_name: 'v3.0.0-beta.1', prerelease: true })]);
|
|
await renderPanel({ isPrerelease: true });
|
|
|
|
expect(screen.getByText('v3.0.0-beta.1')).toBeInTheDocument();
|
|
expect(screen.getByText('Pre-release')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-009: no toggle is offered for a release without a body', async () => {
|
|
releasesRespondWith([buildRelease({ body: null })]);
|
|
await renderPanel();
|
|
|
|
expect(screen.queryByText('Show details')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-010: expands and collapses the release notes', async () => {
|
|
releasesRespondWith([buildRelease({ body: '- Fixed a bug' })]);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
|
|
expect(screen.queryByText('Fixed a bug')).not.toBeInTheDocument();
|
|
await user.click(screen.getByText('Show details'));
|
|
expect(await screen.findByText('Fixed a bug')).toBeInTheDocument();
|
|
|
|
await user.click(screen.getByText('Hide details'));
|
|
await waitFor(() => expect(screen.queryByText('Fixed a bug')).not.toBeInTheDocument());
|
|
expect(screen.getByText('Show details')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-011: renders headings, list items and paragraphs from the markdown body', async () => {
|
|
releasesRespondWith([
|
|
buildRelease({
|
|
body: '## Highlights\n- first item\n- second item\n\n### Details\nA plain paragraph.',
|
|
}),
|
|
]);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
await user.click(screen.getByText('Show details'));
|
|
|
|
expect((await screen.findByText('Highlights')).tagName).toBe('H3');
|
|
expect(screen.getByText('Details').tagName).toBe('H4');
|
|
expect(screen.getByText('first item').closest('li')).toBeInTheDocument();
|
|
expect(screen.getByText('second item').closest('li')).toBeInTheDocument();
|
|
expect(screen.getByText('A plain paragraph.').tagName).toBe('P');
|
|
});
|
|
|
|
it('FE-MOB-AGH-012: renders bold and inline code inside list items', async () => {
|
|
releasesRespondWith([buildRelease({ body: '- **bold text**\n- `inline code`' })]);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
await user.click(screen.getByText('Show details'));
|
|
|
|
expect((await screen.findByText('bold text')).tagName).toBe('STRONG');
|
|
expect(screen.getByText('inline code').tagName).toBe('CODE');
|
|
});
|
|
|
|
it('FE-MOB-AGH-013: keeps http links and rewrites unsafe link targets to #', async () => {
|
|
releasesRespondWith([
|
|
buildRelease({ body: '- [click here](https://example.com)\n- [evil](javascript:alert(1))' }),
|
|
]);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
await user.click(screen.getByText('Show details'));
|
|
|
|
const safe = await screen.findByText('click here');
|
|
expect(safe.tagName).toBe('A');
|
|
expect(safe).toHaveAttribute('href', 'https://example.com');
|
|
expect(safe).toHaveAttribute('rel', 'noopener noreferrer');
|
|
expect(screen.getByText('evil')).toHaveAttribute('href', '#');
|
|
});
|
|
|
|
it('FE-MOB-AGH-014: escapes html in the release body instead of rendering it', async () => {
|
|
releasesRespondWith([buildRelease({ body: 'plain <img src=x onerror="alert(1)"> text' })]);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
await user.click(screen.getByText('Show details'));
|
|
|
|
expect(await screen.findByText('plain <img src=x onerror="alert(1)"> text')).toBeInTheDocument();
|
|
expect(document.querySelector('img')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-015: hides "Load more" when the first page is not full', async () => {
|
|
releasesRespondWith(PAGE_2);
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('v0.0.0')).toBeInTheDocument();
|
|
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-016: "Load more" appends the second page and then disappears', async () => {
|
|
server.use(
|
|
http.get('/api/admin/github-releases', ({ request }) => {
|
|
const page = new URL(request.url).searchParams.get('page');
|
|
return HttpResponse.json(page === '2' ? PAGE_2 : PAGE_1);
|
|
}),
|
|
);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
|
|
expect(screen.getAllByText(/^v1\.\d\.0$/)).toHaveLength(10);
|
|
await user.click(screen.getByText('Load more'));
|
|
|
|
expect(await screen.findByText('v0.0.0')).toBeInTheDocument();
|
|
expect(screen.getAllByText(/^v[01]\.\d\.0$/)).toHaveLength(13);
|
|
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-017: a failing "Load more" keeps the timeline and can be retried', async () => {
|
|
let calls = 0;
|
|
server.use(
|
|
http.get('/api/admin/github-releases', () => {
|
|
calls += 1;
|
|
if (calls === 2) return HttpResponse.json({ message: 'nope' }, { status: 500 });
|
|
return HttpResponse.json(calls === 1 ? PAGE_1 : PAGE_2);
|
|
}),
|
|
);
|
|
const user = userEvent.setup();
|
|
await renderPanel();
|
|
|
|
await user.click(screen.getByText('Load more'));
|
|
await screen.findByText(/Failed to load releases/);
|
|
expect(screen.getAllByText(/^v1\.\d\.0$/)).toHaveLength(10);
|
|
|
|
// the retry succeeds and clears the error
|
|
await user.click(screen.getByText('Load more'));
|
|
expect(await screen.findByText('v0.0.0')).toBeInTheDocument();
|
|
await waitFor(() => expect(screen.queryByText(/Failed to load releases/)).not.toBeInTheDocument());
|
|
});
|
|
|
|
it('FE-MOB-AGH-018: a non-array response is treated as an empty release list', async () => {
|
|
releasesRespondWith({ message: 'rate limited' });
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('Release History')).toBeInTheDocument();
|
|
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Latest')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-AGH-019: skips over a full page that only contains prereleases', async () => {
|
|
const prereleasePage = Array.from({ length: 10 }, (_, i) =>
|
|
buildRelease({ id: 200 + i, tag_name: `v4.0.0-beta.${i}`, prerelease: true }),
|
|
);
|
|
server.use(
|
|
http.get('/api/admin/github-releases', ({ request }) => {
|
|
const page = new URL(request.url).searchParams.get('page');
|
|
return HttpResponse.json(page === '1' ? prereleasePage : PAGE_2);
|
|
}),
|
|
);
|
|
await renderPanel();
|
|
|
|
expect(screen.getByText('v0.0.0')).toBeInTheDocument();
|
|
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
|
|
});
|
|
});
|