mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
The extension is hosted in a sibling repo and
is distributed off-store. Each WROLPi release ships the prebuilt artifacts
under wrolpi/static/extensions/ and serves them at /api/extensions/<file>:
- wrolpi-chrome.zip Load-Unpacked sideload for Chrome / Brave / Edge
- wrolpi-firefox.xpi Mozilla-signed (unlisted self-distribution),
one-click install in any Firefox
- versions.json Per-browser version metadata for the install page
Backend (modules/archive/api.py, wrolpi/root_api.py):
- POST /api/archive/upload accepts an optional repeated `tagNames`
multipart form field; the upload switch handler applies tags to the
new FileGroup via FileGroup.set_tags(). Unknown tag names are auto-
created inside a per-name SAVEPOINT so concurrent inserts on the same
novel name don't abort the transaction (Tag.name has unique=True).
- GET /api/extensions returns metadata (per-file availability + size +
versions.json contents) for the install page.
- GET /api/extensions/<filename> serves the binary, whitelisted by
filename. Firefox .xpi is served as application/x-xpinstall WITHOUT a
Content-Disposition: attachment header, so Firefox triggers its
add-on install handler instead of saving the file.
Frontend (app/src/):
- New /admin/extension page (linked from AdminRoute) detects the user's
browser via navigator.userAgent and renders an install card per
Firefox / Chromium / Other. Pulls metadata from /api/extensions to
show file size and version, and gracefully reports 'Not yet
available' when binaries are missing.
- Dashboard's Getters component supports a deep link from the
extension's 'Configure Download' button:
/?downloader=<archive|video|...>&download_url=<u1>&download_url=<u2>
On mount, the matching downloader form auto-opens with URLs pre-
filled. Query params are cleared on close so a refresh doesn't
re-open the modal.
- DownloadMenu and the per-downloader forms accept an `initialUrls`
seed via the existing `download` prop pattern, joined with newlines
for the `urls` textarea (and first URL for single-URL forms).
Behavior is fully backwards compatible: pre-existing API callers can
still POST to /api/archive/upload without `tagNames`, and the
DashboardPage modal continues to open via the existing button click
when no deep-link query params are present.
78 lines
3.3 KiB
JavaScript
78 lines
3.3 KiB
JavaScript
import React from 'react';
|
|
import {render, screen} from '@testing-library/react';
|
|
import {MemoryRouter} from 'react-router';
|
|
import {Getters} from './DashboardPage';
|
|
import {StatusContext, ThemeContext} from './contexts/contexts';
|
|
|
|
// Replace DownloadMenu with a stub that records its props. The deep-link
|
|
// behavior under test is just "Getters reads URL params and forwards them
|
|
// to DownloadMenu" — we don't need the real downloader forms to verify that.
|
|
jest.mock('./components/Download', () => ({
|
|
DownloadMenu: ({initialDownloader, initialUrls, disabled}) => (
|
|
<div data-testid="download-menu">
|
|
<span data-testid="initial-downloader">{initialDownloader || ''}</span>
|
|
<span data-testid="initial-urls">{JSON.stringify(initialUrls || [])}</span>
|
|
<span data-testid="disabled">{String(!!disabled)}</span>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
jest.mock('./hooks/customHooks', () => ({
|
|
...jest.requireActual('./hooks/customHooks'),
|
|
useWROLMode: () => false,
|
|
}));
|
|
|
|
function renderAtUrl(url) {
|
|
const status = {flags: {refresh_complete: true}};
|
|
const theme = {
|
|
i: {}, s: {}, t: {}, theme: 'light', inverted: false, setInverted: () => {},
|
|
};
|
|
return render(
|
|
<MemoryRouter initialEntries={[url]}>
|
|
<ThemeContext.Provider value={theme}>
|
|
<StatusContext.Provider value={{status}}>
|
|
<Getters/>
|
|
</StatusContext.Provider>
|
|
</ThemeContext.Provider>
|
|
</MemoryRouter>
|
|
);
|
|
}
|
|
|
|
describe('DashboardPage Getters deep-link', () => {
|
|
it('does not auto-open the download modal without query params', () => {
|
|
renderAtUrl('/');
|
|
expect(screen.queryByTestId('download-menu')).toBeNull();
|
|
});
|
|
|
|
it('auto-opens the modal and forwards downloader as initialDownloader', async () => {
|
|
renderAtUrl('/?downloader=archive&download_url=https%3A%2F%2Fexample.com%2Fpage');
|
|
const menu = await screen.findByTestId('download-menu');
|
|
expect(menu).toBeInTheDocument();
|
|
expect(screen.getByTestId('initial-downloader').textContent).toBe('archive');
|
|
expect(JSON.parse(screen.getByTestId('initial-urls').textContent))
|
|
.toEqual(['https://example.com/page']);
|
|
});
|
|
|
|
it('forwards multiple download_url params as an array', async () => {
|
|
renderAtUrl(
|
|
'/?downloader=video' +
|
|
'&download_url=https%3A%2F%2Fa.com' +
|
|
'&download_url=https%3A%2F%2Fb.com'
|
|
);
|
|
await screen.findByTestId('download-menu');
|
|
expect(screen.getByTestId('initial-downloader').textContent).toBe('video');
|
|
expect(JSON.parse(screen.getByTestId('initial-urls').textContent))
|
|
.toEqual(['https://a.com', 'https://b.com']);
|
|
});
|
|
|
|
it('also handles downloader=rss', async () => {
|
|
renderAtUrl('/?downloader=rss&download_url=https%3A%2F%2Ffeed.example.com');
|
|
await screen.findByTestId('download-menu');
|
|
expect(screen.getByTestId('initial-downloader').textContent).toBe('rss');
|
|
});
|
|
});
|
|
|
|
// The DownloadMenu's seed-object construction (joining initialUrls with
|
|
// newlines into the `download` prop's `urls` field) is exercised in the
|
|
// browser via manual verification, not unit-tested here — reaching into the
|
|
// per-form trees pulls in heavy fetch-driven components.
|