451 lines
18 KiB
TypeScript
451 lines
18 KiB
TypeScript
|
|
/**
|
|||
|
|
* KIN-084: LiveConsole — панель лога в реальном времени
|
|||
|
|
*
|
|||
|
|
* Проверяет:
|
|||
|
|
* 1. Тогл видимости (по умолчанию скрыта, открывается/закрывается по клику)
|
|||
|
|
* 2. Поллинг (запуск при открытии+running, since_id, остановка, onUnmounted)
|
|||
|
|
* 3. Отображение логов (формат, цвета уровней, extra_json)
|
|||
|
|
* 4. Автоскролл (scrollTop=scrollHeight при новых логах, отключение при ручном скролле)
|
|||
|
|
* 5. Ограничение массива до 500 строк (FIFO)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|||
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|||
|
|
import LiveConsole from '../components/LiveConsole.vue'
|
|||
|
|
|
|||
|
|
vi.mock('../api', () => ({
|
|||
|
|
api: {
|
|||
|
|
getPipelineLogs: vi.fn(),
|
|||
|
|
},
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
import { api } from '../api'
|
|||
|
|
|
|||
|
|
function makeLog(id: number, level: 'INFO' | 'DEBUG' | 'ERROR' | 'WARN' = 'INFO', extra: Record<string, unknown> | null = null) {
|
|||
|
|
return { id, ts: `2026-03-17T10:00:0${id}`, level, message: `Message ${id}`, extra_json: extra }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mountConsole(pipelineStatus = 'running') {
|
|||
|
|
return mount(LiveConsole, {
|
|||
|
|
props: { pipelineId: 'pipeline-1', pipelineStatus },
|
|||
|
|
attachTo: document.body,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
vi.useFakeTimers()
|
|||
|
|
vi.clearAllMocks()
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValue([])
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
vi.useRealTimers()
|
|||
|
|
vi.restoreAllMocks()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 1. Тогл видимости
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('тогл видимости', () => {
|
|||
|
|
it('панель лога скрыта по умолчанию', () => {
|
|||
|
|
const wrapper = mountConsole()
|
|||
|
|
const panel = wrapper.find('[class*="bg-gray-950"]')
|
|||
|
|
// v-show=false — элемент в DOM, но display: none
|
|||
|
|
expect(panel.element.style.display).toBe('none')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('кнопка показывает «Показать лог» при скрытой панели', () => {
|
|||
|
|
const wrapper = mountConsole()
|
|||
|
|
expect(wrapper.find('button').text()).toContain('Показать лог')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('клик по кнопке открывает панель', async () => {
|
|||
|
|
const wrapper = mountConsole()
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
const panel = wrapper.find('[class*="bg-gray-950"]')
|
|||
|
|
expect(panel.element.style.display).not.toBe('none')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('кнопка показывает «Скрыть лог» когда панель открыта', async () => {
|
|||
|
|
const wrapper = mountConsole()
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
expect(wrapper.find('button').text()).toContain('Скрыть лог')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('повторный клик скрывает панель', async () => {
|
|||
|
|
const wrapper = mountConsole()
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
const panel = wrapper.find('[class*="bg-gray-950"]')
|
|||
|
|
expect(panel.element.style.display).toBe('none')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 2. Поллинг
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('поллинг', () => {
|
|||
|
|
it('при открытии панели с pipelineStatus=running запускается поллинг', async () => {
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
expect(vi.getTimerCount()).toBeGreaterThan(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('при открытии панели с pipelineStatus=in_progress запускается поллинг', async () => {
|
|||
|
|
const wrapper = mountConsole('in_progress')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
expect(vi.getTimerCount()).toBeGreaterThan(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('при открытии панели с pipelineStatus=done поллинг НЕ запускается', async () => {
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
expect(vi.getTimerCount()).toBe(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('since_id=0 при первом запросе', async () => {
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
expect(vi.mocked(api.getPipelineLogs)).toHaveBeenCalledWith('pipeline-1', 0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('since_id = max(id) из предыдущего ответа для следующего запроса', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs)
|
|||
|
|
.mockResolvedValueOnce([makeLog(3), makeLog(7), makeLog(5)] as any)
|
|||
|
|
.mockResolvedValue([])
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const calls = vi.mocked(api.getPipelineLogs).mock.calls
|
|||
|
|
expect(calls[0][1]).toBe(0)
|
|||
|
|
expect(calls[1][1]).toBe(7)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('поллинг тикает каждые 2 секунды', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValue([])
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const callsAfterOpen = vi.mocked(api.getPipelineLogs).mock.calls.length
|
|||
|
|
|
|||
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|||
|
|
await flushPromises()
|
|||
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(vi.mocked(api.getPipelineLogs).mock.calls.length).toBe(callsAfterOpen + 2)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('при закрытии панели поллинг останавливается', async () => {
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(vi.getTimerCount()).toBe(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('onUnmounted — clearInterval вызван, vi.getTimerCount() === 0', async () => {
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(vi.getTimerCount()).toBeGreaterThan(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
expect(vi.getTimerCount()).toBe(0)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('смена pipelineStatus на done останавливает поллинг и делает финальный fetch', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValue([])
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const callsBefore = vi.mocked(api.getPipelineLogs).mock.calls.length
|
|||
|
|
await wrapper.setProps({ pipelineStatus: 'done' })
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
// Финальный fetch
|
|||
|
|
expect(vi.mocked(api.getPipelineLogs).mock.calls.length).toBeGreaterThan(callsBefore)
|
|||
|
|
// Поллинг остановлен
|
|||
|
|
expect(vi.getTimerCount()).toBe(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 3. Отображение логов
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('отображение логов', () => {
|
|||
|
|
it('строка лога отображается с временем и сообщением', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1)] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('2026-03-17T10:00:01')
|
|||
|
|
expect(wrapper.text()).toContain('Message 1')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('строка лога отображает метку уровня [INFO]', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'INFO')] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('[INFO]')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('ERROR → класс text-red-400', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'ERROR')] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const redEl = wrapper.find('.text-red-400')
|
|||
|
|
expect(redEl.exists()).toBe(true)
|
|||
|
|
expect(redEl.text()).toContain('[ERROR]')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('WARN → класс text-yellow-400', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'WARN')] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const yellowEl = wrapper.find('.text-yellow-400')
|
|||
|
|
expect(yellowEl.exists()).toBe(true)
|
|||
|
|
expect(yellowEl.text()).toContain('[WARN]')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('INFO → класс text-gray-300', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'INFO')] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const infoEl = wrapper.find('.text-gray-300')
|
|||
|
|
expect(infoEl.exists()).toBe(true)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('DEBUG → класс text-gray-500', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'DEBUG')] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const debugEls = wrapper.findAll('.text-gray-500')
|
|||
|
|
// text-gray-500 применяется к метке DEBUG и к extra_json
|
|||
|
|
const hasDebug = debugEls.some(el => el.text().includes('[DEBUG]'))
|
|||
|
|
expect(hasDebug).toBe(true)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('extra_json отображается через JSON.stringify когда не null', async () => {
|
|||
|
|
const extra = { key: 'value', num: 42 }
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'INFO', extra)] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('"key"')
|
|||
|
|
expect(wrapper.text()).toContain('"value"')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('extra_json НЕ отображается когда null', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1, 'INFO', null)] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const pres = wrapper.findAll('pre')
|
|||
|
|
expect(pres.length).toBe(0)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('«Нет записей» отображается когда логов нет', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValue([])
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('Нет записей')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('ошибка API отображается в красном блоке', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockRejectedValueOnce(new Error('Network fail'))
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('Ошибка')
|
|||
|
|
expect(wrapper.text()).toContain('Network fail')
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 4. Автоскролл
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('автоскролл', () => {
|
|||
|
|
it('при добавлении логов scrollTop устанавливается в scrollHeight', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1)] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const panel = wrapper.find('[class*="bg-gray-950"]').element as HTMLElement
|
|||
|
|
// В jsdom scrollHeight = 0 (контент не рендерится), поэтому scrollTop тоже 0
|
|||
|
|
// Проверяем что scrollToBottom вызывается — косвенно через scrollTop = scrollHeight
|
|||
|
|
expect(panel.scrollTop).toBe(panel.scrollHeight)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('если userScrolled=true — scrollTop НЕ меняется при новых логах', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs)
|
|||
|
|
.mockResolvedValueOnce([makeLog(1)] as any)
|
|||
|
|
.mockResolvedValue([makeLog(2)] as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const panel = wrapper.find('[class*="bg-gray-950"]').element as HTMLElement
|
|||
|
|
|
|||
|
|
// Симулируем ручной скролл вверх
|
|||
|
|
Object.defineProperty(panel, 'scrollTop', { value: 0, writable: true, configurable: true })
|
|||
|
|
Object.defineProperty(panel, 'scrollHeight', { value: 500, writable: true, configurable: true })
|
|||
|
|
Object.defineProperty(panel, 'clientHeight', { value: 200, writable: true, configurable: true })
|
|||
|
|
|
|||
|
|
// Триггерим событие scroll — компонент установит userScrolled=true
|
|||
|
|
await wrapper.find('[class*="bg-gray-950"]').trigger('scroll')
|
|||
|
|
|
|||
|
|
panel.scrollTop = 10 // запомним текущий scrollTop
|
|||
|
|
|
|||
|
|
// Следующий fetch добавляет лог — scrollToBottom должен быть пропущен
|
|||
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|||
|
|
await flushPromises()
|
|||
|
|
// scrollTop не сбрасывается в 0 (scrollHeight=500)
|
|||
|
|
expect(panel.scrollTop).not.toBe(500)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 5. Ограничение массива до 500 строк (FIFO)
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('ограничение массива до MAX_LOGS=500', () => {
|
|||
|
|
// Нулевое дополнение — уникальные строки без substring-коллизий
|
|||
|
|
function makeLogs(count: number, startId = 1) {
|
|||
|
|
return Array.from({ length: count }, (_, i) => {
|
|||
|
|
const id = startId + i
|
|||
|
|
return {
|
|||
|
|
id,
|
|||
|
|
ts: `ts-${String(id).padStart(6, '0')}`,
|
|||
|
|
level: 'INFO' as const,
|
|||
|
|
message: `Msg-${String(id).padStart(6, '0')}`,
|
|||
|
|
extra_json: null,
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('при 499 строках — все сохранены (N-1)', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce(makeLogs(499) as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
// Проверяем количество строк в DOM
|
|||
|
|
const rows = wrapper.findAll('[class*="mb-1"]')
|
|||
|
|
expect(rows.length).toBe(499)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('при 500 строках — все сохранены (N)', async () => {
|
|||
|
|
vi.mocked(api.getPipelineLogs).mockResolvedValueOnce(makeLogs(500) as any)
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('done')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const rows = wrapper.findAll('[class*="mb-1"]')
|
|||
|
|
expect(rows.length).toBe(500)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('при 501 строке — обрезка до 500, самая старая удалена (N+1)', async () => {
|
|||
|
|
// Первый fetch: 500 логов
|
|||
|
|
vi.mocked(api.getPipelineLogs)
|
|||
|
|
.mockResolvedValueOnce(makeLogs(500, 1) as any)
|
|||
|
|
.mockResolvedValueOnce(makeLogs(1, 501) as any)
|
|||
|
|
.mockResolvedValue([])
|
|||
|
|
|
|||
|
|
const wrapper = mountConsole('running')
|
|||
|
|
await wrapper.find('button').trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
// Второй fetch: +1 лог → итого 501 → обрезка до 500
|
|||
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const rows = wrapper.findAll('[class*="mb-1"]')
|
|||
|
|
expect(rows.length).toBe(500)
|
|||
|
|
|
|||
|
|
// Первый лог (id=1, msg="Msg-000001") удалён
|
|||
|
|
const texts = rows.map(r => r.text())
|
|||
|
|
expect(texts.some(t => t.includes('Msg-000001'))).toBe(false)
|
|||
|
|
// Последний лог (id=501) присутствует
|
|||
|
|
expect(texts.some(t => t.includes('Msg-000501'))).toBe(true)
|
|||
|
|
wrapper.unmount()
|
|||
|
|
})
|
|||
|
|
})
|