conductor/frontend/src/components/Identity/BrowserIdentity.test.tsx

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');
});