59 lines
3.4 KiB
TypeScript
59 lines
3.4 KiB
TypeScript
import React, {act} from 'react';
|
|
import {createRoot, type Root} from 'react-dom/client';
|
|
import {BrowserIdentityProvider, usePageIdentity} from './BrowserIdentity';
|
|
import {useAuth} from '../../context/AuthContext';
|
|
|
|
jest.mock('../../context/AuthContext',()=>({useAuth:jest.fn()}));
|
|
jest.mock('../../api/includedAppsApi',()=>({listIncludedApps:jest.fn().mockResolvedValue([])}));
|
|
const auth=useAuth as jest.Mock;
|
|
(globalThis as typeof globalThis&{IS_REACT_ACT_ENVIRONMENT:boolean}).IS_REACT_ACT_ENVIRONMENT=true;
|
|
let container:HTMLDivElement,root:Root;
|
|
function render(element:React.ReactElement){act(()=>root.render(element));return{rerender:(next:React.ReactElement)=>act(()=>root.render(next))};}
|
|
function Page({name,icon}:{name:string;icon?:string}) {usePageIdentity(name,icon);return <p>{name}</p>;}
|
|
let candidates:Array<{onload:(()=>void)|null;onerror:(()=>void)|null;naturalWidth:number;src:string}>;
|
|
let imageMock:jest.SpyInstance;
|
|
function favicon(){return document.querySelector<HTMLLinkElement>('#conductor-favicon')!;}
|
|
|
|
beforeEach(()=>{
|
|
jest.useFakeTimers();candidates=[];
|
|
container=document.createElement('div');document.body.appendChild(container);root=createRoot(container);
|
|
window.history.replaceState({},'','/');
|
|
auth.mockReturnValue({user:{id:'admin-1',role:'admin'},loading:false,setupRequired:false});
|
|
imageMock=jest.spyOn(window,'Image').mockImplementation(()=>{
|
|
const candidate={onload:null,onerror:null,naturalWidth:32,src:''};
|
|
candidates.push(candidate);return candidate as unknown as HTMLImageElement;
|
|
});
|
|
document.head.innerHTML='<link id="conductor-favicon" rel="icon" href="/favicon.svg" />';
|
|
});
|
|
afterEach(()=>{act(()=>root.unmount());container.remove();imageMock.mockRestore();jest.useRealTimers();});
|
|
|
|
test('an earlier icon load cannot overwrite the next application identity',()=>{
|
|
const screen=render(<BrowserIdentityProvider><Page name="First app" icon="/first.png"/></BrowserIdentityProvider>);
|
|
const lateLoad=candidates[candidates.length-1].onload!;
|
|
screen.rerender(<BrowserIdentityProvider><Page name="Second app"/></BrowserIdentityProvider>);
|
|
act(()=>lateLoad());
|
|
expect(document.title).toBe('Second app · Conductor');
|
|
expect(favicon().getAttribute('href')).toBe('/favicon.svg');
|
|
});
|
|
|
|
test('signing out clears app identity even if its icon finishes loading afterward',()=>{
|
|
const screen=render(<BrowserIdentityProvider><Page name="Private app" icon="/private.png"/></BrowserIdentityProvider>);
|
|
const lateLoad=candidates[candidates.length-1].onload!;
|
|
auth.mockReturnValue({user:null,loading:false,setupRequired:false});
|
|
screen.rerender(<BrowserIdentityProvider><Page name="Private app" icon="/private.png"/></BrowserIdentityProvider>);
|
|
act(()=>lateLoad());
|
|
expect(document.title).toBe('Sign in · Conductor');
|
|
expect(favicon().getAttribute('href')).toBe('/favicon.svg');
|
|
});
|
|
|
|
test('failed and timed-out icons retain a working default',()=>{
|
|
const screen=render(<BrowserIdentityProvider><Page name="Unavailable icon" icon="/missing.png"/></BrowserIdentityProvider>);
|
|
act(()=>candidates[candidates.length-1].onerror!());
|
|
expect(favicon().getAttribute('href')).toBe('/favicon.svg');
|
|
screen.rerender(<BrowserIdentityProvider><Page name="Slow icon" icon="/slow.png"/></BrowserIdentityProvider>);
|
|
const lateLoad=candidates[candidates.length-1].onload!;
|
|
act(()=>{jest.advanceTimersByTime(5001);lateLoad();});
|
|
expect(document.title).toBe('Slow icon · Conductor');
|
|
expect(favicon().getAttribute('href')).toBe('/favicon.svg');
|
|
});
|