116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
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() }
|
||
}))
|
||
|
||
const originalPlatform = process.platform
|
||
|
||
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')
|
||
}
|
||
|
||
function setPlatform(platform: NodeJS.Platform): void {
|
||
Object.defineProperty(process, 'platform', {
|
||
value: platform,
|
||
configurable: true
|
||
})
|
||
}
|
||
|
||
beforeEach(() => {
|
||
vi.resetModules()
|
||
setPlatform('win32')
|
||
h.calls = 0
|
||
h.execImpl = (_cmd, _opts, cb) => cb(null, { stdout: '' })
|
||
h.log.info.mockClear()
|
||
h.log.warn.mockClear()
|
||
})
|
||
|
||
afterEach(() => {
|
||
setPlatform(originalPlatform)
|
||
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 () => {
|
||
setPlatform('linux')
|
||
const { isMeetingActive } = await load()
|
||
expect(await isMeetingActive()).toBe(false)
|
||
expect(h.calls).toBe(0)
|
||
})
|
||
})
|