68 lines
4.3 KiB
JavaScript
68 lines
4.3 KiB
JavaScript
// Public fixtures only. No network or production mutations.
|
|
import assert from 'node:assert/strict';
|
|
import { parseHTML } from 'linkedom';
|
|
import { createServer } from 'vite';
|
|
const { window } = parseHTML('<html><body><div id="root"></div></body></html>');
|
|
Object.assign(globalThis, { window, document: window.document, HTMLElement: window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true });
|
|
window.location = new URL('https://test.invalid/');
|
|
const { default: React, act } = await import('react');
|
|
const { createRoot } = await import('react-dom/client');
|
|
const { renderToString } = await import('react-dom/server');
|
|
const { MemoryRouter } = await import('react-router-dom');
|
|
const vite = await createServer({ server: { middlewareMode: true, hmr: false }, appType: 'custom' });
|
|
const { useApi } = await vite.ssrLoadModule('/src/lib/useApi.js');
|
|
const { ApiDataProvider } = await vite.ssrLoadModule('/src/context/ApiData.jsx');
|
|
const { SiteDataProvider } = await vite.ssrLoadModule('/src/context/SiteData.jsx');
|
|
const { default: HeroSlider } = await vite.ssrLoadModule('/src/components/HeroSlider.jsx');
|
|
const { api } = await vite.ssrLoadModule('/src/api.js');
|
|
let calls = 0;
|
|
let siteCalls = 0;
|
|
api.branches = api.settings = async () => { siteCalls++; return { data: [] }; };
|
|
const pending = [];
|
|
const load = (signal) => { calls++; return new Promise((resolve) => pending.push({ signal, resolve })); };
|
|
function Probe({ query }) {
|
|
const result = useApi(load, [query], query);
|
|
return React.createElement('output', {}, result.data || 'loading');
|
|
}
|
|
const container = document.getElementById('root');
|
|
const root = createRoot(container);
|
|
const snapshot = { first: { data: 'SSR first' }, second: { data: 'SSR second' }, site: { phone: 'test' }, __receivedAt: Date.now() };
|
|
async function mount(query, initial = snapshot, key = 'same') {
|
|
await act(async () => root.render(React.createElement(React.StrictMode, {},
|
|
React.createElement(ApiDataProvider, { value: initial }, React.createElement(SiteDataProvider, {},
|
|
React.createElement(Probe, { key, query }))))));
|
|
}
|
|
const banners = Array.from({ length: 6 }, (_, id) => ({ id, image: `/test-${id}.webp`, title: `Banner ${id}` }));
|
|
const images = () => [...container.querySelectorAll('.hero-slide img[src]')].map((node) => node.getAttribute('src'));
|
|
try {
|
|
await mount('first');
|
|
assert.equal(calls, 0, 'Hydration including StrictMode must not repeat catalog request');
|
|
assert.equal(siteCalls, 0, 'No redundant branch/settings requests');
|
|
assert.equal(container.querySelector('output').textContent, 'SSR first');
|
|
await mount('second');
|
|
assert.equal(calls, 0);
|
|
assert.equal(container.querySelector('output').textContent, 'SSR second');
|
|
await mount('uncached');
|
|
assert.equal(calls, 1, 'Client-only route still loads');
|
|
await mount('new-query');
|
|
await act(async () => pending.at(-2).resolve({ data: 'STALE' }));
|
|
assert.doesNotMatch(container.textContent, /STALE/);
|
|
await act(async () => pending.at(-1).resolve({ data: 'Fresh query' }));
|
|
assert.match(container.textContent, /Fresh query/);
|
|
await mount('first', { ...snapshot, __receivedAt: Date.now() - 31_000 }, 'expired');
|
|
assert.ok(calls >= 3, 'Old SSR snapshot must revalidate');
|
|
for (const request of pending) await act(async () => request.resolve({ data: 'Refreshed' }));
|
|
const ssr = renderToString(React.createElement(MemoryRouter, {}, React.createElement(HeroSlider, { banners })));
|
|
assert.equal(new Set([...ssr.matchAll(/src="(\/test-[^"]+)"/g)].map((m) => m[1])).size, 3, 'SSR exposes only current/neighbor images');
|
|
await act(async () => root.render(React.createElement(MemoryRouter, {}, React.createElement(HeroSlider, { banners }))));
|
|
assert.deepEqual([...new Set(images())].sort(), ['/test-0.webp', '/test-1.webp', '/test-5.webp']);
|
|
assert.equal(container.querySelector('.hero__item--active picture img').getAttribute('fetchpriority'), 'high');
|
|
const next = container.querySelector('[aria-label="Следующий слайд"]');
|
|
for (let i = 0; i < 6; i++) await act(async () => next.dispatchEvent(new window.Event('click', { bubbles: true })));
|
|
assert.equal(new Set(images()).size, 6, 'All banners load on demand as the visitor browses');
|
|
console.log('PASS: SSR catalog/site requests 0, uncached/expired queries load, stale replies ignored; initial banner images 6 → 3, all slides reachable');
|
|
} finally {
|
|
await act(async () => root.unmount());
|
|
await vite.close();
|
|
}
|