feat(525): SPA previews saved logos, drops stale external-URL copy
This commit is contained in:
@@ -552,7 +552,7 @@ describe('ChannelEditScreen', () => {
|
||||
expect(preview.style.top).toBe('');
|
||||
});
|
||||
|
||||
it('does not render the preview when an external logo URL is set', async () => {
|
||||
it('does not offer the stale "external URL cannot drive the bug" copy', async () => {
|
||||
mockApi({
|
||||
channelOverrides: {
|
||||
logo: { path: 'iptv/logos/cartoons.png', contentType: 'image/png' },
|
||||
@@ -565,15 +565,116 @@ describe('ChannelEditScreen', () => {
|
||||
await screen.findByDisplayValue('Cartoons');
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Branding/ }));
|
||||
|
||||
// Preview renders from the uploaded logo first.
|
||||
await screen.findByAltText('On-screen bug preview');
|
||||
const urlInput = screen.getByPlaceholderText('https://example.com/logo.png');
|
||||
fireEvent.change(urlInput, { target: { value: 'https://example.com/new-logo.png' } });
|
||||
|
||||
// #525: a saved external URL is downloaded server-side and comes back as a normal cached
|
||||
// logo, so the bug preview works fine for it — the old blanket "cannot be used" claim is gone.
|
||||
expect(screen.queryByText(/cannot be used as the on-screen bug/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the bug preview for a logo that was saved via an external URL', async () => {
|
||||
const puts: unknown[] = [];
|
||||
mockApi({
|
||||
channelOverrides: {
|
||||
logo: { path: '', contentType: '' },
|
||||
watermarkId: 9
|
||||
},
|
||||
watermarks: [{ id: 9, imageSource: 'ChannelLogo', name: 'Channel Bug' }],
|
||||
onPut: (body) => puts.push(body),
|
||||
// The PUT response models what the backend now does (#525): the external URL was
|
||||
// downloaded and cached server-side, so the saved channel comes back with a normal,
|
||||
// non-external cached logo rather than the URL itself.
|
||||
putResponseOverrides: {
|
||||
logo: { path: 'iptv/logos/downloaded.png', contentType: 'image/png', isExternalUrl: false },
|
||||
watermarkId: 9
|
||||
}
|
||||
});
|
||||
render(<ChannelEditScreen />);
|
||||
|
||||
await screen.findByDisplayValue('Cartoons');
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Branding/ }));
|
||||
|
||||
const urlInput = screen.getByPlaceholderText('https://example.com/logo.png');
|
||||
fireEvent.change(urlInput, { target: { value: 'https://example.com/new-logo.png' } });
|
||||
|
||||
// An external URL can never resolve to a real bug (see the comment on the WatermarkSelector
|
||||
// File.Exists behavior), so the preview must disappear once one is set.
|
||||
await waitFor(() => expect(screen.queryByAltText('On-screen bug preview')).toBeNull());
|
||||
const saveButton = await screen.findByRole('button', { name: 'Save changes' });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => expect(puts).toHaveLength(1));
|
||||
expect(await screen.findByText('Channel saved')).toBeInTheDocument();
|
||||
|
||||
const preview = await screen.findByAltText('On-screen bug preview');
|
||||
expect(preview).toHaveAttribute('src', 'iptv/logos/downloaded.png');
|
||||
|
||||
// The URL field is cleared post-save since the channel's logo is no longer external.
|
||||
expect(screen.getByPlaceholderText('https://example.com/logo.png')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('shows a 400 save error inline near the URL field and does not navigate away', async () => {
|
||||
mockApi({
|
||||
channelOverrides: { logo: { path: '', contentType: '' } }
|
||||
});
|
||||
vi.spyOn(window, 'fetch').mockImplementation((input, init) => {
|
||||
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : (input as Request).url;
|
||||
const method = (init?.method ?? 'GET').toUpperCase();
|
||||
|
||||
if (url === '/api/v1/channels/5' && method === 'PUT') {
|
||||
return Promise.resolve(
|
||||
json({ status: 400, title: 'Bad Request', detail: 'Could not download logo: host timed out' }, 400)
|
||||
);
|
||||
}
|
||||
|
||||
if (url === '/api/v1/channels/5') {
|
||||
return Promise.resolve(json({ ...channel, logo: { path: '', contentType: '' } }));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/ffmpeg/profiles') {
|
||||
return Promise.resolve(json([{ id: 1, name: 'Default profile' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/watermarks') {
|
||||
return Promise.resolve(json([{ id: 2, name: 'Corner bug', imageSource: 'Custom' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/filler-presets') {
|
||||
return Promise.resolve(json([{ id: 3, name: 'Bumpers' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/channels') {
|
||||
return Promise.resolve(json([{ id: 5, number: '5', name: 'Cartoons', group: 'ChicoryTV' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/languages') {
|
||||
return Promise.resolve(json([{ code: 'eng', englishName: 'English' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/channels/music-video-credits-templates') {
|
||||
return Promise.resolve(json(['default']));
|
||||
}
|
||||
|
||||
if (url === '/api/v1/channels/stream-selectors') {
|
||||
return Promise.resolve(json(['selector.py']));
|
||||
}
|
||||
|
||||
return Promise.resolve(json({ status: 404, title: 'Not Found' }, 404));
|
||||
});
|
||||
render(<ChannelEditScreen />);
|
||||
|
||||
await screen.findByDisplayValue('Cartoons');
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Branding/ }));
|
||||
|
||||
const urlInput = screen.getByPlaceholderText('https://example.com/logo.png');
|
||||
fireEvent.change(urlInput, { target: { value: 'https://example.com/bad-logo.png' } });
|
||||
|
||||
const saveButton = await screen.findByRole('button', { name: 'Save changes' });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(await screen.findByText('Could not download logo: host timed out')).toBeInTheDocument();
|
||||
|
||||
// Still on the edit screen, with the failed URL still in the field.
|
||||
expect(screen.getByPlaceholderText('https://example.com/logo.png')).toHaveValue('https://example.com/bad-logo.png');
|
||||
expect(screen.queryByText('Channel saved')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -679,9 +679,6 @@ function BrandingPane({
|
||||
const referenced = data.watermarks.find((watermark) => watermark.id === draft.watermarkId) ?? null;
|
||||
const logoBugEnabled = referenced?.imageSource === 'ChannelLogo';
|
||||
const logoBugTarget = findLogoBugWatermark(data.watermarks);
|
||||
// An external-URL logo is resolved by WatermarkSelector to the URL itself and then File.Exists-ed,
|
||||
// which is never true, so no bug renders (#502). Don't promise one in the preview.
|
||||
const externalUrlLogo = trimmedUrl.length > 0;
|
||||
|
||||
// Keyed by the watermark id it was fetched for, so a stale response (or a disabled toggle) is
|
||||
// filtered out by comparing against the CURRENT draft.watermarkId at render time — no reset
|
||||
@@ -797,9 +794,7 @@ function BrandingPane({
|
||||
help={
|
||||
logoBugTarget == null
|
||||
? 'No logo-driven watermark preset exists yet.'
|
||||
: externalUrlLogo
|
||||
? 'An external logo URL cannot be used as the on-screen bug — upload an image instead.'
|
||||
: 'Overlays this channel’s own logo on the stream, using the shared preset’s position and size.'
|
||||
: 'Overlays this channel’s own logo on the stream, using the shared preset’s position and size.'
|
||||
}
|
||||
label="Use logo as on-screen bug"
|
||||
>
|
||||
@@ -814,7 +809,7 @@ function BrandingPane({
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
{logoBugEnabled && bugGeometry && previewSrc && !externalUrlLogo && (
|
||||
{logoBugEnabled && bugGeometry && previewSrc && (
|
||||
// bugGeometry is cached keyed by `id` (see fetchedGeometry above); pass the
|
||||
// geometry fields explicitly rather than spreading so the cache key never leaks
|
||||
// into BugPreview's props.
|
||||
|
||||
Reference in New Issue
Block a user