|
|
@@ -6,8 +6,8 @@
|
|
|
* camera stream until it is asked to.
|
|
|
*/
|
|
|
|
|
|
-import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
-import { screen, waitFor } from '@testing-library/react';
|
|
|
+import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
+import { screen, waitFor, fireEvent, act } from '@testing-library/react';
|
|
|
import userEvent from '@testing-library/user-event';
|
|
|
import { http, HttpResponse } from 'msw';
|
|
|
import { render } from '../utils';
|
|
|
@@ -27,7 +27,7 @@ function shownUrl(): string {
|
|
|
|
|
|
describe('StreamOverlayBuilder', () => {
|
|
|
beforeEach(() => {
|
|
|
- server.use(http.get('/api/v1/printers', () => HttpResponse.json(printers)));
|
|
|
+ server.use(http.get('/api/v1/printers', () => HttpResponse.json(printers)), http.get('/api/v1/settings/overlay-logo', () => new HttpResponse(null, { status: 404 })));
|
|
|
});
|
|
|
|
|
|
it('starts on the first printer with the overlay defaults', async () => {
|
|
|
@@ -60,12 +60,12 @@ describe('StreamOverlayBuilder', () => {
|
|
|
await user.click(screen.getByLabelText('Printer name'));
|
|
|
expect(new URL(shownUrl()).searchParams.get('show')).toBe('printer,model,filename,status,progress,layers,eta');
|
|
|
await user.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
- expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl());
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl()));
|
|
|
|
|
|
await user.click(model);
|
|
|
await user.click(screen.getByLabelText('Printer name'));
|
|
|
expect(shownUrl()).toBe(originalUrl);
|
|
|
- expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', originalUrl);
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', originalUrl));
|
|
|
});
|
|
|
|
|
|
it('opts into artwork in the URL and preview and restores the original URL', async () => {
|
|
|
@@ -78,10 +78,30 @@ describe('StreamOverlayBuilder', () => {
|
|
|
await user.selectOptions(artwork, 'Version 2');
|
|
|
expect(new URL(shownUrl()).searchParams.get('artwork')).toBe('2');
|
|
|
await user.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
- expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl());
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl()));
|
|
|
await user.selectOptions(artwork, 'Classic');
|
|
|
expect(shownUrl()).toBe(original);
|
|
|
- expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', original);
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', original));
|
|
|
+ });
|
|
|
+
|
|
|
+ it('only offers background transparency for Version 2 and preserves its selection', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(<StreamOverlayBuilder />);
|
|
|
+ const artwork = await screen.findByLabelText('Artwork');
|
|
|
+ expect(screen.queryByRole('slider', { name: /Background transparency/ })).not.toBeInTheDocument();
|
|
|
+ await user.selectOptions(artwork, 'Version 2');
|
|
|
+ const slider = screen.getByRole('slider', { name: /Background transparency/ });
|
|
|
+ expect(slider).toHaveValue('0');
|
|
|
+ expect(shownUrl()).not.toContain('backgroundTransparency');
|
|
|
+ fireEvent.change(slider, { target: { value: '65' } });
|
|
|
+ expect(new URL(shownUrl()).searchParams.get('backgroundTransparency')).toBe('65');
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl()));
|
|
|
+ await user.selectOptions(artwork, 'Classic');
|
|
|
+ expect(screen.queryByRole('slider', { name: /Background transparency/ })).not.toBeInTheDocument();
|
|
|
+ expect(shownUrl()).not.toContain('backgroundTransparency');
|
|
|
+ await user.selectOptions(artwork, 'Version 2');
|
|
|
+ expect(screen.getByRole('slider', { name: /Background transparency/ })).toHaveValue('65');
|
|
|
});
|
|
|
|
|
|
it('switches printer', async () => {
|
|
|
@@ -190,3 +210,103 @@ describe('StreamOverlayBuilder', () => {
|
|
|
await waitFor(() => expect(shownUrl()).toContain('/overlay/1'));
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+it('adds validated gradient colours to the URL and resets to the default', async () => {
|
|
|
+ render(<StreamOverlayBuilder />);
|
|
|
+ const from = await screen.findByLabelText('From colour (hex)');
|
|
|
+ fireEvent.change(from, { target: { value: '#ff0000' } });
|
|
|
+ fireEvent.change(screen.getByLabelText('To colour (hex)'), { target: { value: '#0000ff' } });
|
|
|
+ expect(new URL(shownUrl()).searchParams.get('progressFrom')).toBe('#ff0000');
|
|
|
+ expect(new URL(shownUrl()).searchParams.get('progressTo')).toBe('#0000ff');
|
|
|
+ fireEvent.change(from, { target: { value: 'invalid' } });
|
|
|
+ expect(new URL(shownUrl()).searchParams.get('progressFrom')).toBe('#ff0000');
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: 'Reset colours' }));
|
|
|
+ expect(shownUrl()).not.toContain('progressFrom');
|
|
|
+ expect(shownUrl()).not.toContain('progressTo');
|
|
|
+ });
|
|
|
+
|
|
|
+it('uploads a logo, includes it in the URL, and removes it from the preview', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ let saved = false;
|
|
|
+ const create = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:logo-preview');
|
|
|
+ const revoke = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
|
|
|
+ server.use(
|
|
|
+ http.get('/api/v1/printers', () => HttpResponse.json(printers)),
|
|
|
+ http.get('/api/v1/settings/overlay-logo', () => saved ? new HttpResponse(new Blob(['png'], { type: 'image/png' })) : new HttpResponse(null, { status: 404 })),
|
|
|
+ http.post('/api/v1/settings/overlay-logo', () => { saved = true; return HttpResponse.json({ status: 'ok' }); }),
|
|
|
+ http.delete('/api/v1/settings/overlay-logo', () => { saved = false; return HttpResponse.json({ status: 'ok' }); }),
|
|
|
+ );
|
|
|
+ try {
|
|
|
+ render(<StreamOverlayBuilder />);
|
|
|
+ await user.upload(await screen.findByLabelText('Upload logo'), new File(['png'], 'logo.png', { type: 'image/png' }));
|
|
|
+ expect(await screen.findByRole('img', { name: 'Custom logo' })).toHaveAttribute('src', 'blob:logo-preview');
|
|
|
+ expect(new URL(shownUrl()).searchParams.get('logo')).toBe('1');
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl()));
|
|
|
+ const previousPreview = screen.getByTitle('Overlay preview');
|
|
|
+ const previousUrl = shownUrl();
|
|
|
+ await user.upload(screen.getByLabelText('Upload logo'), new File(['new png'], 'replacement.png', { type: 'image/png' }));
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).not.toBe(previousPreview));
|
|
|
+ expect(shownUrl()).toBe(previousUrl);
|
|
|
+ expect(await screen.findByRole('img', { name: 'Custom logo' })).toBeInTheDocument();
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Remove' }));
|
|
|
+ await waitFor(() => expect(screen.queryByRole('img', { name: 'Custom logo' })).not.toBeInTheDocument());
|
|
|
+ expect(new URL(shownUrl()).searchParams.has('logo')).toBe(false);
|
|
|
+ await waitFor(() => expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl()));
|
|
|
+ } finally {
|
|
|
+ create.mockRestore();
|
|
|
+ revoke.mockRestore();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+it('debounces continuous preview changes and cancels a pending reload when hidden', async () => {
|
|
|
+ server.use(
|
|
|
+ http.get('/api/v1/printers', () => HttpResponse.json(printers)),
|
|
|
+ http.get('/api/v1/settings/overlay-logo', () => new HttpResponse(null, { status: 404 })),
|
|
|
+ );
|
|
|
+ render(<StreamOverlayBuilder />);
|
|
|
+ await screen.findByRole('option', { name: 'X1 Carbon' });
|
|
|
+ fireEvent.change(screen.getByLabelText('Artwork'), { target: { value: '2' } });
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
+ const original = screen.getByTitle('Overlay preview');
|
|
|
+ vi.useFakeTimers();
|
|
|
+ try {
|
|
|
+ fireEvent.change(screen.getByLabelText('From colour'), { target: { value: '#ff0000' } });
|
|
|
+ act(() => vi.advanceTimersByTime(200));
|
|
|
+ fireEvent.change(screen.getByRole('slider', { name: /Background transparency/ }), { target: { value: '65' } });
|
|
|
+ act(() => vi.advanceTimersByTime(299));
|
|
|
+ expect(screen.getByTitle('Overlay preview')).toBe(original);
|
|
|
+ expect(original).not.toHaveAttribute('src', shownUrl());
|
|
|
+ act(() => vi.advanceTimersByTime(1));
|
|
|
+ expect(screen.getByTitle('Overlay preview')).not.toBe(original);
|
|
|
+ expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl());
|
|
|
+
|
|
|
+ fireEvent.change(screen.getByLabelText('From colour'), { target: { value: '#0000ff' } });
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: 'Hide preview' }));
|
|
|
+ act(() => vi.advanceTimersByTime(300));
|
|
|
+ expect(screen.queryByTitle('Overlay preview')).not.toBeInTheDocument();
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: 'Show preview' }));
|
|
|
+ expect(screen.getByTitle('Overlay preview')).toHaveAttribute('src', shownUrl());
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: 'Hide preview' }));
|
|
|
+ } finally {
|
|
|
+ vi.useRealTimers();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+it.each([
|
|
|
+ [400, { detail: 'Logo must be a static PNG or WebP image' }, 'Logo must be a static PNG or WebP image'],
|
|
|
+ [413, { detail: { message: 'Logo must be at most 2 MiB' } }, 'Logo must be at most 2 MiB'],
|
|
|
+ [502, null, 'HTTP 502'],
|
|
|
+])('shows the server upload error for status %s', async (status, body, message) => {
|
|
|
+ server.use(
|
|
|
+ http.get('/api/v1/printers', () => HttpResponse.json(printers)),
|
|
|
+ http.get('/api/v1/settings/overlay-logo', () => new HttpResponse(null, { status: 404 })),
|
|
|
+ http.post('/api/v1/settings/overlay-logo', () => body ? HttpResponse.json(body, { status }) : new HttpResponse('Bad gateway', { status })),
|
|
|
+ );
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(<StreamOverlayBuilder />);
|
|
|
+ await user.upload(screen.getByLabelText('Upload logo'), new File(['invalid'], 'logo.png', { type: 'image/png' }));
|
|
|
+ expect(await screen.findByText(message)).toBeInTheDocument();
|
|
|
+ expect(new URL(shownUrl()).searchParams.has('logo')).toBe(false);
|
|
|
+ expect(screen.getByLabelText('Upload logo')).toBeEnabled();
|
|
|
+});
|