feat(robustness+ui): отказоустойчивость main, тесты, a11y-полировка, лицензия
Надёжность main-процесса: - глобальные uncaughtException/unhandledRejection (лог + flushNow) - safeHandle/safeOn вокруг всех IPC-хендлеров (не падаем молча, generic-ошибка наружу) - таймаут 4s на tasklist, Atomics.wait вместо busy-spin на exit-записи - единый log.error для фоновых сбоев вместо console.error/тишины Тесты (178 -> 203): meeting-detect, scheduler-gating, store (миграции/карантин/cap). UI/UX: - prefers-reduced-motion через MotionConfig + CSS media-блок - Spinner/Skeleton примитивы, loading-состояния вместо пустых заглушек - aria-live анонсы достижений и выполнения (useAnnounce) - оформленные пустые состояния, клавиатура в меню ExerciseCard Лицензия: проприетарный LICENSE + правка README/CLAUDE.md, счётчик тестов. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
109
src/main/meeting-detect.test.ts
Normal file
109
src/main/meeting-detect.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
/**
|
||||
* Тесты эвристики «человек на ВКС». Мокаем `node:child_process.exec`
|
||||
* (через него идёт `tasklist`), electron BrowserWindow (broadcast no-op) и
|
||||
* logger. resetModules + dynamic import в каждом тесте — чтобы сбросить
|
||||
* module-level кэш (`cachedActive`, `lastCheckAt`).
|
||||
*/
|
||||
|
||||
type ExecCb = (err: Error | null, res?: { stdout: string }) => void
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
// Текущая реализация exec для конкретного теста.
|
||||
execImpl: ((_cmd: string, _opts: unknown, cb: ExecCb) =>
|
||||
cb(null, { stdout: '' })) as (
|
||||
cmd: string,
|
||||
opts: unknown,
|
||||
cb: ExecCb
|
||||
) => void,
|
||||
calls: 0,
|
||||
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
exec: (cmd: string, opts: unknown, cb: ExecCb) => {
|
||||
h.calls += 1
|
||||
h.execImpl(cmd, opts, cb)
|
||||
}
|
||||
}))
|
||||
vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: () => [] } }))
|
||||
vi.mock('./logger', () => ({ log: h.log }))
|
||||
|
||||
/** CSV-строка tasklist для заданного набора .exe. */
|
||||
function csv(...procs: string[]): string {
|
||||
return procs.map((p) => `"${p}","1234","Console","1","85,432 K"`).join('\r\n')
|
||||
}
|
||||
|
||||
async function load(): Promise<typeof import('./meeting-detect')> {
|
||||
return import('./meeting-detect')
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
h.calls = 0
|
||||
h.execImpl = (_cmd, _opts, cb) => cb(null, { stdout: '' })
|
||||
h.log.info.mockClear()
|
||||
h.log.warn.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('isMeetingActive', () => {
|
||||
it('детектит zoom.exe', async () => {
|
||||
h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('zoom.exe') })
|
||||
const { isMeetingActive } = await load()
|
||||
expect(await isMeetingActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('детектит новые Teams (ms-teams.exe)', async () => {
|
||||
h.execImpl = (_c, _o, cb) =>
|
||||
cb(null, { stdout: csv('explorer.exe', 'ms-teams.exe') })
|
||||
const { isMeetingActive } = await load()
|
||||
expect(await isMeetingActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('возвращает false когда ВКС-процессов нет', async () => {
|
||||
h.execImpl = (_c, _o, cb) =>
|
||||
cb(null, { stdout: csv('explorer.exe', 'code.exe', 'chrome.exe') })
|
||||
const { isMeetingActive } = await load()
|
||||
expect(await isMeetingActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('кэширует результат в пределах CACHE_MS (exec вызывается один раз)', async () => {
|
||||
h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('discord.exe') })
|
||||
const { isMeetingActive } = await load()
|
||||
await isMeetingActive()
|
||||
await isMeetingActive()
|
||||
expect(h.calls).toBe(1)
|
||||
})
|
||||
|
||||
it('при падении tasklist возвращает false и логирует warn', async () => {
|
||||
h.execImpl = (_c, _o, cb) => cb(new Error('ETIMEDOUT'))
|
||||
const { isMeetingActive } = await load()
|
||||
expect(await isMeetingActive()).toBe(false)
|
||||
expect(h.log.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('isMeetingActiveSync отражает последний известный результат', async () => {
|
||||
h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('webex.exe') })
|
||||
const mod = await load()
|
||||
expect(mod.isMeetingActiveSync()).toBe(false) // до первого запроса
|
||||
await mod.isMeetingActive()
|
||||
expect(mod.isMeetingActiveSync()).toBe(true)
|
||||
})
|
||||
|
||||
it('на не-Windows возвращает false без вызова tasklist', async () => {
|
||||
const original = process.platform
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' })
|
||||
try {
|
||||
const { isMeetingActive } = await load()
|
||||
expect(await isMeetingActive()).toBe(false)
|
||||
expect(h.calls).toBe(0)
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: original })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user