fix(meeting): persist transcript segment edits, restore live recording view, and route settings deep links by tab id
This commit is contained in:
parent
a8983c583a
commit
a85b24d385
15 changed files with 974 additions and 69 deletions
|
|
@ -0,0 +1,116 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { ErrorCode } from '@d3ro/core/errors'
|
||||
import type { IPCResult } from '@d3ro/core/errors'
|
||||
import type { CaptionSegment, MeetingModeStateInfo } from '@d3ro/core/types'
|
||||
|
||||
// 회의 전사 세그먼트 편집이 main 에 핸들러가 없는 채널을 호출하던 회귀
|
||||
// (preload editSegment → 'meetingMode:editSegment' → No handler registered).
|
||||
|
||||
const invokeHandlers = vi.hoisted(
|
||||
() => new Map<string, (event: unknown, ...args: unknown[]) => unknown>(),
|
||||
)
|
||||
|
||||
type FakeMeetingService = EventEmitter & {
|
||||
stateInfo: MeetingModeStateInfo
|
||||
getStateInfo: () => MeetingModeStateInfo
|
||||
updateTranscript: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
// vi.mock 팩토리는 모듈을 처음 import 할 때(beforeEach 안) 불리므로 이 상수는 이미 초기화되어 있다.
|
||||
const service = Object.assign(new EventEmitter(), {
|
||||
stateInfo: { state: 'recording', sessionId: 's1', elapsedMs: 0, memoCount: 0, segmentCount: 0 },
|
||||
getStateInfo(this: FakeMeetingService): MeetingModeStateInfo {
|
||||
return this.stateInfo
|
||||
},
|
||||
updateTranscript: vi.fn(),
|
||||
}) as FakeMeetingService
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: vi.fn((channel: string, handler: (event: unknown, ...args: unknown[]) => unknown) => {
|
||||
invokeHandlers.set(channel, handler)
|
||||
}),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/main/services/MeetingModeService', () => ({
|
||||
getMeetingModeService: () => service,
|
||||
}))
|
||||
|
||||
async function invoke<T>(channel: string, ...args: unknown[]): Promise<IPCResult<T>> {
|
||||
const handler = invokeHandlers.get(channel)
|
||||
if (!handler) throw new Error(`No handler registered for '${channel}'`)
|
||||
return (await handler({}, ...args)) as IPCResult<T>
|
||||
}
|
||||
|
||||
function segment(id: string, text: string): CaptionSegment {
|
||||
return { id, text, timestamp: 0, isFinal: true }
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
invokeHandlers.clear()
|
||||
service.removeAllListeners()
|
||||
service.updateTranscript.mockReset()
|
||||
service.stateInfo = { state: 'recording', sessionId: 's1', elapsedMs: 0, memoCount: 0, segmentCount: 0 }
|
||||
const mod = await import('../../../src/main/ipc/meeting-mode-handlers')
|
||||
mod.registerMeetingModeHandlers()
|
||||
})
|
||||
|
||||
describe('MEETING_MODE.EDIT_SEGMENT', () => {
|
||||
it('핸들러가 등록되어 있다', () => {
|
||||
expect(invokeHandlers.has(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT)).toBe(true)
|
||||
})
|
||||
|
||||
it('녹음 중 수정이 세션 완료 시 editedTranscript 로 저장된다', async () => {
|
||||
service.emit('segment', segment('a', '안녕하세요'))
|
||||
service.emit('segment', segment('b', '오늘 안건'))
|
||||
|
||||
const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, {
|
||||
sessionId: 's1',
|
||||
segmentId: 'b',
|
||||
text: '오늘 안건은 예산',
|
||||
})
|
||||
expect(res.success).toBe(true)
|
||||
|
||||
service.emit('session-completed', {
|
||||
id: 's1',
|
||||
rawTranscript: '[00:01] 안녕하세요\n[00:02] 오늘 안건',
|
||||
})
|
||||
expect(service.updateTranscript).toHaveBeenCalledWith(
|
||||
's1',
|
||||
'[00:01] 안녕하세요\n[00:02] 오늘 안건은 예산',
|
||||
)
|
||||
})
|
||||
|
||||
it('진행 중이 아닌 세션이면 MeetingNotRecording 으로 거부한다', async () => {
|
||||
service.stateInfo = { ...service.stateInfo, state: 'idle', sessionId: null }
|
||||
const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, {
|
||||
sessionId: 's1',
|
||||
segmentId: 'a',
|
||||
text: 'x',
|
||||
})
|
||||
expect(res.success).toBe(false)
|
||||
if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingNotRecording)
|
||||
})
|
||||
|
||||
it('모르는 세그먼트는 MeetingTranscriptUpdateFailed 로 거부한다', async () => {
|
||||
const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, {
|
||||
sessionId: 's1',
|
||||
segmentId: 'nope',
|
||||
text: 'x',
|
||||
})
|
||||
expect(res.success).toBe(false)
|
||||
if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingTranscriptUpdateFailed)
|
||||
})
|
||||
|
||||
it('다시 등록해도 세션 완료 리스너가 중복되지 않는다', async () => {
|
||||
const mod = await import('../../../src/main/ipc/meeting-mode-handlers')
|
||||
mod.registerMeetingModeHandlers()
|
||||
expect(service.listenerCount('session-completed')).toBe(1)
|
||||
expect(service.listenerCount('segment')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
CaptionSegment,
|
||||
MeetingModeStateInfo,
|
||||
MeetingSessionDetail,
|
||||
} from '@d3ro/core/types'
|
||||
import {
|
||||
applyLiveSegmentEdits,
|
||||
LiveSegmentEditor,
|
||||
normalizeSegmentText,
|
||||
} from '../../../src/main/services/meeting/live-segment-edits'
|
||||
import type { LiveMeetingPort } from '../../../src/main/services/meeting/live-segment-edits'
|
||||
|
||||
function seg(id: string, text: string, timestamp = 0): CaptionSegment {
|
||||
return { id, text, timestamp, isFinal: true }
|
||||
}
|
||||
|
||||
function detail(id: string, rawTranscript: string | null): MeetingSessionDetail {
|
||||
return { id, rawTranscript } as unknown as MeetingSessionDetail
|
||||
}
|
||||
|
||||
function fakePort(initial: Partial<MeetingModeStateInfo> = {}) {
|
||||
let state: MeetingModeStateInfo = {
|
||||
state: 'recording',
|
||||
sessionId: 's1',
|
||||
elapsedMs: 0,
|
||||
memoCount: 0,
|
||||
segmentCount: 0,
|
||||
...initial,
|
||||
}
|
||||
const segmentListeners = new Set<(s: CaptionSegment) => void>()
|
||||
const completedListeners = new Set<(d: MeetingSessionDetail) => void>()
|
||||
const saveEditedTranscript = vi.fn<(sessionId: string, text: string) => void>()
|
||||
const port: LiveMeetingPort = {
|
||||
getStateInfo: () => state,
|
||||
onSegment: (l) => {
|
||||
segmentListeners.add(l)
|
||||
return () => segmentListeners.delete(l)
|
||||
},
|
||||
onSessionCompleted: (l) => {
|
||||
completedListeners.add(l)
|
||||
return () => completedListeners.delete(l)
|
||||
},
|
||||
saveEditedTranscript,
|
||||
}
|
||||
return {
|
||||
port,
|
||||
saveEditedTranscript,
|
||||
setState: (next: Partial<MeetingModeStateInfo>) => { state = { ...state, ...next } },
|
||||
emitSegment: (s: CaptionSegment) => segmentListeners.forEach((l) => l(s)),
|
||||
emitCompleted: (d: MeetingSessionDetail) => completedListeners.forEach((l) => l(d)),
|
||||
listenerCount: () => segmentListeners.size + completedListeners.size,
|
||||
}
|
||||
}
|
||||
|
||||
describe('applyLiveSegmentEdits', () => {
|
||||
const segments = [seg('a', '안녕하세요'), seg('b', '오늘 안건은'), seg('c', '예산입니다')]
|
||||
const raw = '[00:01] 안녕하세요\n[00:02] 오늘 안건은\n[00:03] 예산입니다'
|
||||
|
||||
it('고친 세그먼트 줄의 텍스트만 바꾸고 시각 접두사는 유지한다', () => {
|
||||
const out = applyLiveSegmentEdits(raw, segments, new Map([['b', '오늘 안건은 두 가지']]))
|
||||
expect(out).toBe('[00:01] 안녕하세요\n[00:02] 오늘 안건은 두 가지\n[00:03] 예산입니다')
|
||||
})
|
||||
|
||||
it('접두사 형식과 무관하게 동작한다', () => {
|
||||
const odd = '[29000000:01] 안녕하세요\n[29000000:02] 오늘 안건은\n[29000000:03] 예산입니다'
|
||||
expect(applyLiveSegmentEdits(odd, segments, new Map([['c', '예산 확정']]))).toBe(
|
||||
'[29000000:01] 안녕하세요\n[29000000:02] 오늘 안건은\n[29000000:03] 예산 확정',
|
||||
)
|
||||
})
|
||||
|
||||
it('줄바꿈이 든 세그먼트가 있어도 올바른 줄 묶음을 바꾼다', () => {
|
||||
const multi = [seg('a', '첫 줄\n둘째 줄'), seg('b', '다음')]
|
||||
const text = '[00:01] 첫 줄\n둘째 줄\n[00:02] 다음'
|
||||
expect(applyLiveSegmentEdits(text, multi, new Map([['b', '다음 발화']]))).toBe(
|
||||
'[00:01] 첫 줄\n둘째 줄\n[00:02] 다음 발화',
|
||||
)
|
||||
})
|
||||
|
||||
it('줄 수가 세그먼트와 맞지 않으면 덮어쓰지 않고 null', () => {
|
||||
expect(applyLiveSegmentEdits('[00:01] 안녕하세요', segments, new Map([['a', 'x']]))).toBeNull()
|
||||
})
|
||||
|
||||
it('수정이 없거나 원문과 같으면 null', () => {
|
||||
expect(applyLiveSegmentEdits(raw, segments, new Map())).toBeNull()
|
||||
expect(applyLiveSegmentEdits(raw, segments, new Map([['a', '안녕하세요']]))).toBeNull()
|
||||
})
|
||||
|
||||
it('줄바꿈은 공백으로 접는다', () => {
|
||||
expect(normalizeSegmentText(' 하나\n 둘 \r\n셋 ')).toBe('하나 둘 셋')
|
||||
})
|
||||
})
|
||||
|
||||
describe('LiveSegmentEditor', () => {
|
||||
it('녹음 중 수정은 세션 완료 시 editedTranscript 로 저장된다(원문은 그대로)', () => {
|
||||
const f = fakePort()
|
||||
const editor = new LiveSegmentEditor(f.port)
|
||||
f.emitSegment(seg('a', '안녕하세요'))
|
||||
f.emitSegment(seg('b', '오늘 안건'))
|
||||
|
||||
expect(editor.edit('s1', 'b', '오늘 안건은 예산')).toBe('ok')
|
||||
f.setState({ state: 'processing' })
|
||||
f.emitCompleted(detail('s1', '[00:01] 안녕하세요\n[00:02] 오늘 안건'))
|
||||
|
||||
expect(f.saveEditedTranscript).toHaveBeenCalledWith('s1', '[00:01] 안녕하세요\n[00:02] 오늘 안건은 예산')
|
||||
})
|
||||
|
||||
it('수정이 없으면 저장하지 않는다', () => {
|
||||
const f = fakePort()
|
||||
new LiveSegmentEditor(f.port)
|
||||
f.emitSegment(seg('a', '안녕'))
|
||||
f.emitCompleted(detail('s1', '[00:01] 안녕'))
|
||||
expect(f.saveEditedTranscript).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('진행 중이 아닌 세션·모르는 세그먼트·빈 내용은 거부한다', () => {
|
||||
const f = fakePort()
|
||||
const editor = new LiveSegmentEditor(f.port)
|
||||
f.emitSegment(seg('a', '안녕'))
|
||||
expect(editor.edit('other', 'a', 'x')).toBe('not-live')
|
||||
expect(editor.edit('s1', 'zzz', 'x')).toBe('unknown-segment')
|
||||
expect(editor.edit('s1', 'a', ' \n ')).toBe('empty')
|
||||
f.setState({ state: 'idle', sessionId: null })
|
||||
expect(editor.edit('s1', 'a', 'x')).toBe('not-live')
|
||||
})
|
||||
|
||||
it('다음 세션은 이전 세션의 세그먼트·수정을 물려받지 않는다', () => {
|
||||
const f = fakePort()
|
||||
const editor = new LiveSegmentEditor(f.port)
|
||||
f.emitSegment(seg('a', '첫 회의'))
|
||||
editor.edit('s1', 'a', '첫 회의 수정')
|
||||
f.setState({ sessionId: 's2' })
|
||||
f.emitSegment(seg('b', '둘째 회의'))
|
||||
f.emitCompleted(detail('s2', '[00:01] 둘째 회의'))
|
||||
expect(f.saveEditedTranscript).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('저장이 던져도 서비스 emit 으로 예외를 올리지 않는다', () => {
|
||||
const f = fakePort()
|
||||
f.saveEditedTranscript.mockImplementation(() => { throw new Error('db') })
|
||||
const warn = vi.fn()
|
||||
const editor = new LiveSegmentEditor(f.port, { warn, info: vi.fn() })
|
||||
f.emitSegment(seg('a', '안녕'))
|
||||
editor.edit('s1', 'a', '반가워요')
|
||||
expect(() => f.emitCompleted(detail('s1', '[00:01] 안녕'))).not.toThrow()
|
||||
expect(warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('dispose 하면 구독을 해지한다', () => {
|
||||
const f = fakePort()
|
||||
const editor = new LiveSegmentEditor(f.port)
|
||||
expect(f.listenerCount()).toBe(2)
|
||||
editor.dispose()
|
||||
expect(f.listenerCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CaptionSegment } from '@d3ro/core/types'
|
||||
import {
|
||||
applyTranscriptSegmentEdit,
|
||||
parseTranscriptSegments,
|
||||
transcriptSegmentIndex,
|
||||
} from '../../src/renderer/components/meeting/transcript-segments'
|
||||
import {
|
||||
isActiveMeetingState,
|
||||
recordingStartFromElapsed,
|
||||
replaceSegmentText,
|
||||
rollbackSegmentText,
|
||||
} from '../../src/renderer/pages/meeting/live-session'
|
||||
|
||||
describe('상세 뷰 세그먼트 편집 — 전사 재조립', () => {
|
||||
const raw = '[00:01] [화자 1] 안녕하세요\n[00:05] 오늘 안건은\n[01:10] 예산입니다'
|
||||
|
||||
it('세그먼트 id 는 줄 순번이다', () => {
|
||||
const segs = parseTranscriptSegments(raw, null)
|
||||
expect(segs.map((s) => s.id)).toEqual(['seg-0', 'seg-1', 'seg-2'])
|
||||
expect(segs[0].speaker).toBe('화자 1')
|
||||
expect(transcriptSegmentIndex('seg-12')).toBe(12)
|
||||
expect(transcriptSegmentIndex('x-1')).toBeNull()
|
||||
expect(transcriptSegmentIndex('seg-')).toBeNull()
|
||||
})
|
||||
|
||||
it('고친 줄만 바뀐 전사를 돌려준다(시각·화자 유지)', () => {
|
||||
const next = applyTranscriptSegmentEdit(raw, 'seg-1', '오늘 안건은 두 가지')
|
||||
expect(next).toBe('[00:01] [화자 1] 안녕하세요\n[00:05] 오늘 안건은 두 가지\n[01:10] 예산입니다')
|
||||
// 재조립한 전사를 다시 나누면 수정이 그대로 보인다 — 원문으로 되돌아가지 않는다
|
||||
expect(parseTranscriptSegments(raw, next)[1].text).toBe('오늘 안건은 두 가지')
|
||||
})
|
||||
|
||||
it('기존 수정본 위에 이어서 고친다', () => {
|
||||
const first = applyTranscriptSegmentEdit(raw, 'seg-0', '반갑습니다')
|
||||
const second = applyTranscriptSegmentEdit(first, 'seg-2', '예산 확정')
|
||||
const texts = parseTranscriptSegments(raw, second).map((s) => s.text)
|
||||
expect(texts).toEqual(['반갑습니다', '오늘 안건은', '예산 확정'])
|
||||
})
|
||||
|
||||
it('없는 세그먼트·빈 내용·같은 내용이면 null', () => {
|
||||
expect(applyTranscriptSegmentEdit(raw, 'seg-9', 'x')).toBeNull()
|
||||
expect(applyTranscriptSegmentEdit(raw, 'bogus', 'x')).toBeNull()
|
||||
expect(applyTranscriptSegmentEdit(raw, 'seg-0', ' ')).toBeNull()
|
||||
expect(applyTranscriptSegmentEdit(raw, 'seg-0', '안녕하세요')).toBeNull()
|
||||
})
|
||||
|
||||
it('줄바꿈은 한 줄로 접어 다른 세그먼트를 만들지 않는다', () => {
|
||||
const next = applyTranscriptSegmentEdit(raw, 'seg-1', '둘\n셋')
|
||||
expect(parseTranscriptSegments(raw, next)).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('녹음 뷰 — 세션 복원과 실시간 편집 롤백', () => {
|
||||
const segs: CaptionSegment[] = [
|
||||
{ id: 'a', text: '하나', timestamp: 0, isFinal: true },
|
||||
{ id: 'b', text: '둘', timestamp: 0, isFinal: true },
|
||||
]
|
||||
|
||||
it('녹음·후처리 중이면 녹음 뷰를 복원한다', () => {
|
||||
expect(isActiveMeetingState('recording')).toBe(true)
|
||||
expect(isActiveMeetingState('processing')).toBe(true)
|
||||
expect(isActiveMeetingState('idle')).toBe(false)
|
||||
expect(isActiveMeetingState(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('경과 시간으로 시작 시각을 되짚는다', () => {
|
||||
expect(recordingStartFromElapsed(5_000, 100_000)).toBe(95_000)
|
||||
expect(recordingStartFromElapsed(-1, 100_000)).toBe(100_000)
|
||||
expect(recordingStartFromElapsed(Number.NaN, 100_000)).toBe(100_000)
|
||||
})
|
||||
|
||||
it('실패한 편집만 되돌리고, 그 사이 다시 고친 내용은 덮어쓰지 않는다', () => {
|
||||
const edited = replaceSegmentText(segs, 'b', '둘 수정')
|
||||
expect(edited[1].text).toBe('둘 수정')
|
||||
expect(rollbackSegmentText(edited, 'b', '둘 수정', '둘')[1].text).toBe('둘')
|
||||
|
||||
const reEdited = replaceSegmentText(edited, 'b', '둘 재수정')
|
||||
expect(rollbackSegmentText(reEdited, 'b', '둘 수정', '둘')[1].text).toBe('둘 재수정')
|
||||
})
|
||||
})
|
||||
|
||||
describe('MeetingModePage 구조 회귀', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '../../src/renderer/pages/MeetingModePage.tsx'),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
it('목록 뷰도 오류 다이얼로그(강제 초기화 버튼)를 렌더한다', () => {
|
||||
const start = source.search(/if \(view === 'list'\) \{\s*return \(/)
|
||||
expect(start).toBeGreaterThan(-1)
|
||||
const end = source.indexOf("if (view === 'recording')", start)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
const listBranch = source.slice(start, end)
|
||||
expect(listBranch).toContain('{errorDialog}')
|
||||
})
|
||||
|
||||
it('마운트 시 main 의 회의 상태를 물어 진행 중 녹음을 복원한다', () => {
|
||||
expect(source).toMatch(/meetingMode\s*\.getState\(\)/)
|
||||
})
|
||||
|
||||
it('설정 딥링크에 숫자 인덱스를 쓰지 않는다', () => {
|
||||
expect(source).not.toMatch(/detail:\s*\{\s*tab:\s*\d/)
|
||||
})
|
||||
})
|
||||
73
apps/desktop/tests/unit/settings-tabs-redteam-r1-29.test.ts
Normal file
73
apps/desktop/tests/unit/settings-tabs-redteam-r1-29.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
OPEN_SETTINGS_EVENT,
|
||||
SETTINGS_TAB_ORDER,
|
||||
isSettingsTabId,
|
||||
openSettings,
|
||||
readOpenSettingsTab,
|
||||
resolveSettingsTab,
|
||||
settingsTabIndex,
|
||||
} from '../../src/renderer/navigation/settingsTabs'
|
||||
import type { SettingsTabId } from '../../src/renderer/navigation/settingsTabs'
|
||||
|
||||
// 로그인 CTA 가 detail.tab='cloud' 를 보내면 AppLayout 이 그대로 숫자 인덱스 자리에 넣어
|
||||
// 아무 패널도 없는 빈 설정 창이 열리던 회귀.
|
||||
|
||||
function capture(target: EventTarget): () => SettingsTabId | null {
|
||||
let received: SettingsTabId | null = null
|
||||
target.addEventListener(OPEN_SETTINGS_EVENT, (e) => {
|
||||
received = readOpenSettingsTab(e)
|
||||
})
|
||||
return () => received
|
||||
}
|
||||
|
||||
describe('settingsTabs', () => {
|
||||
it('탭 순서는 SettingsModal 의 8개 탭과 같고 cloud 는 6번째(0 기준)다', () => {
|
||||
expect(SETTINGS_TAB_ORDER).toEqual(['general', 'audio', 'stt', 'llm', 'input', 'license', 'cloud', 'about'])
|
||||
expect(settingsTabIndex('cloud')).toBe(6)
|
||||
expect(settingsTabIndex('stt')).toBe(2)
|
||||
expect(settingsTabIndex('audio')).toBe(1)
|
||||
})
|
||||
|
||||
it('알려진 id 는 그대로, 모르는 값은 general 로 바꾼다', () => {
|
||||
expect(resolveSettingsTab('cloud')).toBe('cloud')
|
||||
expect(resolveSettingsTab('nope')).toBe('general')
|
||||
expect(resolveSettingsTab(undefined)).toBe('general')
|
||||
expect(resolveSettingsTab(null)).toBe('general')
|
||||
expect(resolveSettingsTab(99)).toBe('general')
|
||||
expect(resolveSettingsTab(1.5)).toBe('general')
|
||||
})
|
||||
|
||||
it('예전 숫자 인덱스 딥링크도 같은 탭으로 연다', () => {
|
||||
expect(resolveSettingsTab(2)).toBe('stt')
|
||||
expect(resolveSettingsTab(1)).toBe('audio')
|
||||
})
|
||||
|
||||
it('isSettingsTabId 는 문자열 id 만 통과시킨다', () => {
|
||||
expect(isSettingsTabId('about')).toBe(true)
|
||||
expect(isSettingsTabId(0)).toBe(false)
|
||||
expect(isSettingsTabId('Cloud')).toBe(false)
|
||||
})
|
||||
|
||||
it("openSettings('cloud') 를 받은 쪽은 cloud 탭을 연다", () => {
|
||||
const target = new EventTarget()
|
||||
const received = capture(target)
|
||||
openSettings('cloud', target)
|
||||
expect(received()).toBe('cloud')
|
||||
})
|
||||
|
||||
it('탭 없이 열거나 detail 없는 이벤트는 general', () => {
|
||||
const target = new EventTarget()
|
||||
const received = capture(target)
|
||||
openSettings(undefined, target)
|
||||
expect(received()).toBe('general')
|
||||
target.dispatchEvent(new Event(OPEN_SETTINGS_EVENT))
|
||||
expect(received()).toBe('general')
|
||||
})
|
||||
|
||||
it("예전 문자열 'cloud' detail 을 보내던 이벤트도 빈 창이 아니라 cloud 로 읽힌다", () => {
|
||||
const event = new CustomEvent(OPEN_SETTINGS_EVENT, { detail: { tab: 'cloud' } })
|
||||
expect(readOpenSettingsTab(event)).toBe('cloud')
|
||||
expect(readOpenSettingsTab(new CustomEvent(OPEN_SETTINGS_EVENT, { detail: { tab: 'bogus' } }))).toBe('general')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue