mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
217 lines
5.3 KiB
TypeScript
217 lines
5.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {readFileSync} from 'node:fs';
|
|
import {resolve} from 'node:path';
|
|
import {SHELL_HINT_VERSION} from '@app/features/app/components/skeleton/ShellHint';
|
|
import {createElement, type ReactNode} from 'react';
|
|
import {createRoot} from 'react-dom/client';
|
|
import {afterEach, expect, test, vi} from 'vitest';
|
|
|
|
vi.mock('@lingui/core/macro', () => {
|
|
const descriptor = (value: unknown): unknown => (typeof value === 'string' ? {message: value} : value);
|
|
return {msg: descriptor, t: descriptor, plural: () => '', select: () => '', selectOrdinal: () => ''};
|
|
});
|
|
vi.mock('@lingui/react/macro', () => ({
|
|
Trans: ({children}: {children?: ReactNode}) => children ?? null,
|
|
useLingui: () => ({i18n: {_: (descriptor: {message?: string}) => descriptor.message ?? ''}}),
|
|
}));
|
|
|
|
const html = readFileSync(resolve(process.cwd(), 'index.html'), 'utf8');
|
|
|
|
function readBootScript(): string {
|
|
const body = html.slice(html.indexOf('<div id="root"></div>'));
|
|
const match = /<script[^>]*>([\s\S]*?)<\/script>/u.exec(body);
|
|
expect(match).not.toBeNull();
|
|
return match?.[1] ?? '';
|
|
}
|
|
|
|
interface ShellHintOverrides {
|
|
readonly [key: string]: unknown;
|
|
}
|
|
|
|
const ACCOUNT_KEY = 'https://one.example/api::100';
|
|
|
|
function desktopDMHint(overrides: ShellHintOverrides = {}): Record<string, unknown> {
|
|
return {
|
|
v: SHELL_HINT_VERSION,
|
|
t: Date.now(),
|
|
p: '/channels/@me',
|
|
a: ACCOUNT_KEY,
|
|
mo: 0,
|
|
ua: 0,
|
|
fs: 0,
|
|
sw: 320,
|
|
sv: 'dm',
|
|
ck: 'other',
|
|
cb: 1,
|
|
rf: 1,
|
|
rv: 1,
|
|
ru: [],
|
|
rti: 0,
|
|
ro: 0,
|
|
ri: [
|
|
[0, 0],
|
|
[0, 0],
|
|
[0, 0],
|
|
[0, 0],
|
|
],
|
|
rgi: -1,
|
|
rbm: 15,
|
|
rbi: -1,
|
|
rs: 0,
|
|
gb: 0,
|
|
ga: 1778,
|
|
gn: 0,
|
|
gd: 0,
|
|
gm: 0,
|
|
sr: 6,
|
|
sam: 7,
|
|
ss: 1,
|
|
cid: '',
|
|
ch: 2,
|
|
ct: 0,
|
|
cnw: 0,
|
|
ctw: 0,
|
|
cda: -1,
|
|
cma: -1,
|
|
cfv: 1,
|
|
cst: 0,
|
|
cuv: 0,
|
|
ml: 0,
|
|
co: 4,
|
|
com: 2,
|
|
cdv: 0,
|
|
pc: 0,
|
|
pg: 16,
|
|
pf: 16,
|
|
ps: 16,
|
|
pa: 0,
|
|
pt: 56,
|
|
pv: 800,
|
|
nr: [],
|
|
vh: 0,
|
|
vc: 0,
|
|
mr: 0,
|
|
ms: 0,
|
|
mn: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function setViewportWidth(width: number): void {
|
|
Object.defineProperty(window, 'innerWidth', {value: width, configurable: true});
|
|
const matches = width >= 1024;
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
value: (query: string) => ({matches: query.includes('1024') ? matches : false, media: query}),
|
|
configurable: true,
|
|
});
|
|
}
|
|
|
|
function failScript(): void {
|
|
const script = document.createElement('script');
|
|
document.body.appendChild(script);
|
|
script.dispatchEvent(new Event('error', {bubbles: false}));
|
|
}
|
|
|
|
const bootRoots: Array<HTMLElement> = [];
|
|
|
|
interface BootScriptOptions {
|
|
readonly signedIn?: boolean;
|
|
readonly storage?: Readonly<Record<string, string>>;
|
|
}
|
|
|
|
function runBootScript(
|
|
hint: Record<string, unknown> | null,
|
|
pathname = '/channels/@me',
|
|
{signedIn = true, storage = {}}: BootScriptOptions = {},
|
|
): HTMLElement | null {
|
|
document.body.innerHTML = '<div id="root"></div>';
|
|
window.history.pushState({}, '', pathname);
|
|
localStorage.clear();
|
|
if (signedIn) {
|
|
localStorage.setItem('fluxer:gateway:preboot:session', '1');
|
|
}
|
|
localStorage.setItem('fluxer:auth:active-account-key', ACCOUNT_KEY);
|
|
if (hint != null) {
|
|
localStorage.setItem('fluxer:ui:shell-hint', JSON.stringify(hint));
|
|
}
|
|
for (const [key, value] of Object.entries(storage)) {
|
|
localStorage.setItem(key, value);
|
|
}
|
|
const root = document.getElementById('root');
|
|
if (root != null) {
|
|
bootRoots.push(root);
|
|
}
|
|
new Function(readBootScript())();
|
|
return document.querySelector<HTMLElement>('#fluxer-boot-shell');
|
|
}
|
|
|
|
afterEach(async () => {
|
|
for (const root of bootRoots.splice(0)) {
|
|
root.replaceChildren(document.createElement('span'));
|
|
root.replaceChildren();
|
|
}
|
|
await Promise.resolve();
|
|
localStorage.clear();
|
|
document.body.innerHTML = '';
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
test('a document script that never loads drops the boot shell because nothing else can mount the app', () => {
|
|
setViewportWidth(1400);
|
|
|
|
expect(runBootScript(desktopDMHint())).not.toBeNull();
|
|
|
|
failScript();
|
|
|
|
expect(document.querySelector('#fluxer-boot-shell')).toBeNull();
|
|
});
|
|
|
|
test('the backstop removes a boot shell React never cleared', () => {
|
|
vi.useFakeTimers();
|
|
setViewportWidth(1400);
|
|
|
|
expect(runBootScript(desktopDMHint())).not.toBeNull();
|
|
|
|
vi.advanceTimersByTime(60_000);
|
|
|
|
expect(document.querySelector('#fluxer-boot-shell')).toBeNull();
|
|
});
|
|
|
|
test('the error listener and the backstop stop existing once React has cleared the boot shell', async () => {
|
|
vi.useFakeTimers();
|
|
setViewportWidth(1400);
|
|
|
|
expect(runBootScript(desktopDMHint())).not.toBeNull();
|
|
|
|
document.getElementById('root')?.replaceChildren();
|
|
await Promise.resolve();
|
|
|
|
const marker = document.createElement('div');
|
|
marker.id = 'fluxer-boot-shell';
|
|
document.body.appendChild(marker);
|
|
|
|
failScript();
|
|
vi.advanceTimersByTime(60_000);
|
|
|
|
expect(document.querySelector('#fluxer-boot-shell')).not.toBeNull();
|
|
});
|
|
|
|
test('React clearing its own container is what removes the boot shell', async () => {
|
|
const container = document.createElement('div');
|
|
container.id = 'root';
|
|
const shell = document.createElement('div');
|
|
shell.id = 'fluxer-boot-shell';
|
|
container.appendChild(shell);
|
|
document.body.appendChild(container);
|
|
|
|
createRoot(container).render(createElement('span', null, 'mounted'));
|
|
|
|
await vi.waitFor(() => {
|
|
expect(container.textContent).toContain('mounted');
|
|
});
|
|
|
|
expect(container.querySelector('#fluxer-boot-shell')).toBeNull();
|
|
});
|