Files
portal/app/tests/Frontend/usePolling.spec.ts
T
Дмитрий 01c20e7b6c phase2(polling): usePolling composable 30 сек + Page Visibility pause
Закрывает последний unblocked production-TODO «Polling/SSE для real-time».
Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель.

Composable (composables/usePolling.ts):
- usePolling(loader, {intervalMs=30_000, enabled=true}).
- Page Visibility API: при document.hidden=true interval останавливается;
  при visibilitychange с возвратом hidden=false — restart + немедленный
  loader() (не ждать следующего interval'а).
- Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
- enabled=false — composable не стартует (feature-flag).

Integration:
- DealsView + KanbanView → loadDeals.
- AdminTenantsView → loadTenants.
- AdminBillingView → loadBilling.
- AdminIncidentsView → loadIncidents.

Vitest +6 (usePolling.spec.ts) с vi.useFakeTimers:
- Вызов каждые intervalMs / default 30 сек / skip при document.hidden /
  cleanup на unmount / enabled=false → no-op / visibilitychange
  pause+resume с немедленным loader.

Регресс:
- Lint+type-check+format passed.
- Vitest 319/319 за 18.67 сек (+6 от 313).
- Vite build 899 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.62 сек (backend не тронут).

Реестр v1.71→v1.72 / CLAUDE.md v1.62→v1.63.
ВСЕ unblocked production-TODO закрыты.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:17:51 +03:00

125 lines
4.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils';
import { usePolling } from '../../resources/js/composables/usePolling';
/**
* Тесты polling-composable. Используется fake-timers — `setInterval` под
* контролем `vi.advanceTimersByTime` для детерминированности.
*/
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
// Сбросим document.hidden если он был установлен.
Object.defineProperty(document, 'hidden', { value: false, configurable: true, writable: true });
});
function makeHostComponent(loader: () => void, options?: Parameters<typeof usePolling>[1]) {
return defineComponent({
setup() {
usePolling(loader, options);
return () => h('div');
},
});
}
describe('usePolling', () => {
it('вызывает loader каждые intervalMs миллисекунд', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader, { intervalMs: 5000 });
const wrapper = mount(Host);
expect(loader).not.toHaveBeenCalled(); // только onMounted, без stale-вызова
vi.advanceTimersByTime(5000);
expect(loader).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(5000);
expect(loader).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
it('default intervalMs = 30 секунд', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader);
const wrapper = mount(Host);
vi.advanceTimersByTime(29_999);
expect(loader).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(loader).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('skip loader когда document.hidden=true', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader, { intervalMs: 1000 });
const wrapper = mount(Host);
Object.defineProperty(document, 'hidden', { value: true, configurable: true, writable: true });
vi.advanceTimersByTime(3000);
expect(loader).not.toHaveBeenCalled();
Object.defineProperty(document, 'hidden', { value: false, configurable: true, writable: true });
vi.advanceTimersByTime(1000);
expect(loader).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('cleanup на unmount — больше не вызывает loader', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader, { intervalMs: 1000 });
const wrapper = mount(Host);
vi.advanceTimersByTime(1000);
expect(loader).toHaveBeenCalledTimes(1);
wrapper.unmount();
vi.advanceTimersByTime(10_000);
expect(loader).toHaveBeenCalledTimes(1); // больше не растёт
});
it('enabled=false — loader НЕ вызывается совсем', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader, { intervalMs: 1000, enabled: false });
const wrapper = mount(Host);
vi.advanceTimersByTime(10_000);
expect(loader).not.toHaveBeenCalled();
wrapper.unmount();
});
it('visibilitychange — pause при hidden и resume на возврате с немедленным loader', () => {
const loader = vi.fn();
const Host = makeHostComponent(loader, { intervalMs: 5000 });
const wrapper = mount(Host);
// Скрываем вкладку — interval останавливается.
Object.defineProperty(document, 'hidden', { value: true, configurable: true, writable: true });
document.dispatchEvent(new Event('visibilitychange'));
vi.advanceTimersByTime(20_000);
expect(loader).not.toHaveBeenCalled(); // interval остановлен
// Возвращаемся — должен сразу вызваться loader (без ожидания interval).
Object.defineProperty(document, 'hidden', { value: false, configurable: true, writable: true });
document.dispatchEvent(new Event('visibilitychange'));
expect(loader).toHaveBeenCalledTimes(1);
// И interval снова работает.
vi.advanceTimersByTime(5000);
expect(loader).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
});