/* @vitest-environment jsdom */ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import { expect, test, vi } from 'vitest'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import { AuthUiContext } from '../auth/AuthUiContext'; import { buildMergedGroupOutlinePath, resolveDraggedMergedGroupLayer, resolveDraggedPieceCellLayer, resolveDraggedPieceLayer, } from './puzzleRuntimeShape'; import { PuzzleRuntimeShell } from './PuzzleRuntimeShell'; vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({ useResolvedAssetReadUrl: (src: string | null) => ({ resolvedUrl: src ?? '', isResolving: false, shouldResolve: false, }), })); vi.mock('../ResolvedAssetImage', () => ({ ResolvedAssetImage: () => null, })); function createAuthValue() { return { user: null, canAccessProtectedData: false, openLoginModal: () => {}, requireAuth: (action: () => void) => action(), openSettingsModal: () => {}, openAccountModal: () => {}, setCurrentUser: vi.fn(), logout: async () => {}, musicVolume: 0.42, setMusicVolume: vi.fn(), platformTheme: 'light' as const, setPlatformTheme: vi.fn(), isHydratingSettings: false, isPersistingSettings: false, settingsError: null, }; } function renderPuzzleRuntime( ui: React.ReactElement, authValue = createAuthValue(), ) { return render( {ui}, ); } function dispatchPointerEvent( target: HTMLElement, type: string, options: { pointerId: number; clientX: number; clientY: number }, ) { const event = new Event(type, { bubbles: true, cancelable: true }); Object.assign(event, options); target.dispatchEvent(event); } const clearedRun: PuzzleRunSnapshot = { runId: 'run-1', entryProfileId: 'profile-1', clearedLevelCount: 1, currentLevelIndex: 1, currentGridSize: 3, playedProfileIds: ['profile-1'], previousLevelTags: ['奇幻'], recommendedNextProfileId: 'profile-2', leaderboardEntries: [ { rank: 1, nickname: '测试作者', elapsedMs: 12_340, isCurrentPlayer: true, }, { rank: 2, nickname: '星桥旅人', elapsedMs: 18_120, }, ], currentLevel: { runId: 'run-1', levelIndex: 1, levelId: 'puzzle-level-1', gridSize: 3, profileId: 'profile-1', levelName: '潮雾拼图', authorDisplayName: '测试作者', themeTags: ['奇幻'], coverImageSrc: null, status: 'cleared', startedAtMs: 1000, clearedAtMs: 13_340, elapsedMs: 12_340, timeLimitMs: 300_000, remainingMs: 287_660, pausedAccumulatedMs: 0, pauseStartedAtMs: null, freezeAccumulatedMs: 0, freezeStartedAtMs: null, freezeUntilMs: null, leaderboardEntries: [ { rank: 1, nickname: '测试作者', elapsedMs: 12_340, isCurrentPlayer: true, }, { rank: 2, nickname: '星桥旅人', elapsedMs: 18_120, }, ], board: { rows: 3, cols: 3, selectedPieceId: null, allTilesResolved: true, mergedGroups: [], pieces: Array.from({ length: 9 }, (_, index) => ({ pieceId: `piece-${index}`, correctRow: Math.floor(index / 3), correctCol: index % 3, currentRow: Math.floor(index / 3), currentCol: index % 3, mergedGroupId: null, })), }, }, }; test('通关后显示结算弹窗、排行榜和下一关按钮', () => { vi.useFakeTimers(); const onAdvanceNextLevel = vi.fn(); renderPuzzleRuntime( , ); expect(screen.queryByRole('dialog', { name: '通关完成' })).toBeNull(); expect(screen.getByTestId('puzzle-clear-flash')).toBeTruthy(); act(() => { vi.advanceTimersByTime(1_400); }); const dialog = screen.getByRole('dialog', { name: '通关完成' }); expect(within(dialog).getAllByText('0:12.34').length).toBeGreaterThan(0); expect(within(dialog).getByText('排行榜')).toBeTruthy(); expect(within(dialog).getByText('#1')).toBeTruthy(); expect(within(dialog).getByText('测试作者')).toBeTruthy(); fireEvent.click(within(dialog).getByRole('button', { name: '下一关' })); expect(onAdvanceNextLevel).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); test('首次点击左上返回弹出作品改造引导,保存并退出后不再重复弹出', () => { const onBack = vi.fn(); const onRemodelWork = vi.fn(); window.localStorage.clear(); renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '返回上一页' })); const dialog = screen.getByRole('dialog', { name: /体验不佳?\s*试试改造功能!/u, }); expect(dialog).toBeTruthy(); expect(onBack).not.toHaveBeenCalled(); fireEvent.click(within(dialog).getByRole('button', { name: '保存并退出' })); expect(onBack).toHaveBeenCalledTimes(1); expect( window.localStorage.getItem( 'genarrative.puzzle-runtime.exit-remodel-prompt.v1:profile-1', ), ).toBe('1'); fireEvent.click(screen.getByRole('button', { name: '返回上一页' })); expect(screen.queryByRole('dialog')).toBeNull(); expect(onBack).toHaveBeenCalledTimes(2); expect(onRemodelWork).not.toHaveBeenCalled(); }); test('首次退出引导的作品改造按钮进入改造流程', () => { const onRemodelWork = vi.fn(); window.localStorage.clear(); renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '返回上一页' })); fireEvent.click(screen.getByRole('button', { name: '作品改造' })); expect(onRemodelWork).toHaveBeenCalledTimes(1); expect(onRemodelWork).toHaveBeenCalledWith('profile-remodel'); expect(screen.queryByRole('dialog')).toBeNull(); }); test('顶部作者显示头像昵称,底部功能居中放大且不显示等待候选', () => { const runWithoutNext: PuzzleRunSnapshot = { ...clearedRun, recommendedNextProfileId: null, }; renderPuzzleRuntime( , ); const avatar = screen.getByText('测'); const timer = screen.getByText('4:48'); const hintButton = screen.getByRole('button', { name: '提示' }); const referenceButton = screen.getByRole('button', { name: '原图' }); const freezeButton = screen.getByRole('button', { name: '冻结' }); expect(avatar.className).toContain('rounded-full'); expect(screen.getByText('测试作者')).toBeTruthy(); expect(timer.className).toContain('text-2xl'); expect(hintButton.className).toContain('h-16'); expect(referenceButton.className).toContain('h-16'); expect(freezeButton.className).toContain('h-16'); expect(screen.queryByText('等待下一关候选')).toBeNull(); }); test('关闭通关弹窗后保留底部下一关入口', () => { vi.useFakeTimers(); const onAdvanceNextLevel = vi.fn(); const runWithoutRecommendedNextProfile: PuzzleRunSnapshot = { ...clearedRun, recommendedNextProfileId: null, nextLevelMode: 'sameWork', nextLevelProfileId: 'profile-1', nextLevelId: 'puzzle-level-2', recommendedNextWorks: [], }; renderPuzzleRuntime( , ); act(() => { vi.advanceTimersByTime(1_400); }); fireEvent.click(screen.getByRole('button', { name: '关闭通关弹窗' })); expect(screen.queryByRole('dialog', { name: '通关完成' })).toBeNull(); const nextButton = screen.getByRole('button', { name: /下一关/u }); expect(nextButton).toBeTruthy(); fireEvent.click(nextButton); expect(onAdvanceNextLevel).toHaveBeenCalledTimes(1); expect(onAdvanceNextLevel).toHaveBeenCalledWith({ profileId: 'profile-1', levelId: 'puzzle-level-2', }); vi.useRealTimers(); }); test('当前作品没有下一关时展示三个相似作品并可选择进入', () => { vi.useFakeTimers(); const onAdvanceNextLevel = vi.fn(); const similarWorksRun: PuzzleRunSnapshot = { ...clearedRun, recommendedNextProfileId: 'profile-similar-1', nextLevelMode: 'similarWorks', nextLevelProfileId: 'profile-similar-1', nextLevelId: null, recommendedNextWorks: [ { profileId: 'profile-similar-1', levelName: '雾海遗迹', authorDisplayName: '星桥旅人', themeTags: ['奇幻', '遗迹'], coverImageSrc: null, similarityScore: 0.91, }, { profileId: 'profile-similar-2', levelName: '风塔试炼', authorDisplayName: '晨风', themeTags: ['奇幻', '机关'], coverImageSrc: null, similarityScore: 0.84, }, { profileId: 'profile-similar-3', levelName: '月井秘路', authorDisplayName: '月井守望', themeTags: ['秘境', '魔法'], coverImageSrc: null, similarityScore: 0.79, }, ], }; renderPuzzleRuntime( , ); act(() => { vi.advanceTimersByTime(1_400); }); const dialog = screen.getByRole('dialog', { name: '通关完成' }); expect(within(dialog).getByText('雾海遗迹')).toBeTruthy(); expect(within(dialog).getByText('风塔试炼')).toBeTruthy(); expect(within(dialog).getByText('月井秘路')).toBeTruthy(); expect(within(dialog).queryByRole('button', { name: '下一关' })).toBeNull(); fireEvent.click(within(dialog).getByRole('button', { name: /风塔试炼/u })); expect(onAdvanceNextLevel).toHaveBeenCalledTimes(1); expect(onAdvanceNextLevel).toHaveBeenCalledWith({ profileId: 'profile-similar-2', }); vi.useRealTimers(); }); test('当前作品没有下一关时底部入口打开相似作品选择', () => { vi.useFakeTimers(); const similarWorksRun: PuzzleRunSnapshot = { ...clearedRun, recommendedNextProfileId: 'profile-similar-1', nextLevelMode: 'similarWorks', nextLevelProfileId: 'profile-similar-1', nextLevelId: null, recommendedNextWorks: [ { profileId: 'profile-similar-1', levelName: '雾海遗迹', authorDisplayName: '星桥旅人', themeTags: ['奇幻', '遗迹'], coverImageSrc: null, similarityScore: 0.91, }, ], }; renderPuzzleRuntime( , ); act(() => { vi.advanceTimersByTime(1_400); }); fireEvent.click(screen.getByRole('button', { name: '关闭通关弹窗' })); fireEvent.click(screen.getByRole('button', { name: /换个作品/u })); expect(screen.getByRole('dialog', { name: '通关完成' })).toBeTruthy(); expect(screen.getByRole('button', { name: /雾海遗迹/u })).toBeTruthy(); vi.useRealTimers(); }); test('右上角设置按钮打开拼图设置并支持音量调节', () => { const authValue = createAuthValue(); renderPuzzleRuntime( , authValue, ); fireEvent.click(screen.getByRole('button', { name: '打开拼图设置' })); const dialog = screen.getByRole('dialog', { name: '拼图设置' }); const slider = within(dialog).getByRole('slider', { name: '拼图音乐音量' }); fireEvent.change(slider, { target: { value: '77' } }); expect(within(dialog).getByText('第 1 关')).toBeTruthy(); expect(authValue.setMusicVolume).toHaveBeenCalledWith(0.77); }); test('拼图棋盘使用贴近移动端边缘的正方形舞台承载切块', () => { const { container } = renderPuzzleRuntime( , ); const board = screen.getByTestId('puzzle-board'); expect(board.className).toContain('aspect-square'); expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_16.5rem))]'); expect(board.className).not.toContain('aspect-video'); expect(board.className).not.toContain('aspect-[9/16]'); expect(board.getAttribute('style')).toContain('grid-template-rows'); expect(container.querySelector('.min-h-\\[4\\.5rem\\]')).toBeNull(); }); test('合并块按实际拼块外轮廓描边', () => { const mergedRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, mergedGroups: [ { groupId: 'group-l', pieceIds: ['piece-0', 'piece-1', 'piece-3'], occupiedCells: [ { row: 0, col: 0 }, { row: 0, col: 1 }, { row: 1, col: 0 }, ], }, ], pieces: clearedRun.currentLevel!.board.pieces.map((piece) => ['piece-0', 'piece-1', 'piece-3'].includes(piece.pieceId) ? { ...piece, mergedGroupId: 'group-l' } : piece, ), }, }, }; const { container } = renderPuzzleRuntime( , ); const outlinedPieces = container.querySelectorAll( '[data-merged-piece-outline="true"]', ); expect(outlinedPieces).toHaveLength(3); expect(container.querySelector('.ring-2.ring-emerald-100\\/58')).toBeNull(); expect( container.querySelector('[data-merged-group-outline="true"]'), ).toBeTruthy(); const outlineStroke = container.querySelector( '[data-merged-group-outline-stroke="true"]', ); expect(outlineStroke).toBeTruthy(); expect(outlineStroke?.getAttribute('d')).toContain('Q 2 1 1.84 1'); expect(outlineStroke?.getAttribute('d')).toContain('Q 1 1 1 1.16'); expect((outlinedPieces[0] as HTMLElement).style.clipPath).toBe(''); const clippedLayer = container.querySelector( '[style*="clip-path"]', ) as HTMLElement | null; expect(clippedLayer?.style.clipPath).toContain('url(#'); }); test('合并块轮廓路径为内凹角生成圆角曲线', () => { const outlinePath = buildMergedGroupOutlinePath({ rowSpan: 2, colSpan: 2, pieces: [ { localRow: 0, localCol: 0, }, { localRow: 0, localCol: 1, }, { localRow: 1, localCol: 0, }, ], }); expect(outlinePath).toContain('Q 2 1 1.84 1'); expect(outlinePath).toContain('Q 1 1 1 1.16'); }); test('基础单块使用圆角裁剪图片', () => { const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), coverImageSrc: '/puzzle.png', board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; const { container } = renderPuzzleRuntime( , ); const basePiece = container.querySelector( '[data-piece-id="piece-0"]', ) as HTMLElement | null; expect(basePiece?.className).toContain('overflow-hidden'); expect(basePiece?.className).toContain('rounded-[0.85rem]'); }); test('移动端点击拼图片时立即触发一次震动反馈', () => { const originalVibrate = navigator.vibrate; const originalRequestAnimationFrame = window.requestAnimationFrame; const originalCancelAnimationFrame = window.cancelAnimationFrame; const vibrate = vi.fn(); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; Object.defineProperty(navigator, 'vibrate', { configurable: true, value: vibrate, }); Object.defineProperty(window, 'requestAnimationFrame', { configurable: true, value: vi.fn(() => 1), }); Object.defineProperty(window, 'cancelAnimationFrame', { configurable: true, value: vi.fn(), }); const { container, unmount } = renderPuzzleRuntime( , ); const piece = container.querySelector( '[data-piece-id="piece-0"]', ) as HTMLElement | null; if (!piece) { throw new Error('缺少测试拼图片'); } act(() => { dispatchPointerEvent(piece, 'pointerdown', { pointerId: 1, clientX: 100, clientY: 100, }); }); expect(vibrate).toHaveBeenCalledTimes(1); expect(vibrate).toHaveBeenCalledWith([12]); act(() => { dispatchPointerEvent(piece, 'pointermove', { pointerId: 1, clientX: 104, clientY: 104, }); }); expect(vibrate).toHaveBeenCalledTimes(1); act(() => { dispatchPointerEvent(piece, 'pointermove', { pointerId: 1, clientX: 112, clientY: 100, }); }); act(() => { dispatchPointerEvent(piece, 'pointermove', { pointerId: 1, clientX: 132, clientY: 100, }); }); expect(vibrate).toHaveBeenCalledTimes(1); unmount(); Object.defineProperty(navigator, 'vibrate', { configurable: true, value: originalVibrate, }); Object.defineProperty(window, 'requestAnimationFrame', { configurable: true, value: originalRequestAnimationFrame, }); Object.defineProperty(window, 'cancelAnimationFrame', { configurable: true, value: originalCancelAnimationFrame, }); }); test('道具确认弹窗暂停时间,提示只演示不直接移动拼块', async () => { const onPauseChange = vi.fn(); const onUseProp = vi.fn().mockResolvedValue(clearedRun); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), remainingMs: 180_000, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, pieces: clearedRun.currentLevel!.board.pieces.map((piece, index) => { if (index === 0) { return { ...piece, currentRow: 2, currentCol: 2 }; } if (index === 8) { return { ...piece, currentRow: 0, currentCol: 0 }; } return piece; }), }, }, }; const { container } = renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '提示' })); expect(screen.getByRole('dialog', { name: '使用提示' })).toBeTruthy(); expect(screen.getByText('消耗 1 光点')).toBeTruthy(); expect(onPauseChange).toHaveBeenLastCalledWith(true); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); expect(onUseProp).toHaveBeenCalledWith('hint'); expect(onPauseChange).toHaveBeenLastCalledWith(false); expect( (container.querySelector('[data-piece-cell-id="piece-0"]') as HTMLElement) .style.transform, ).toBe('translate(-66.66666666666666%, -66.66666666666666%) scale(1.03)'); }); test('道具使用失败时保留确认弹窗和暂停态', async () => { const onPauseChange = vi.fn(); const onUseProp = vi.fn().mockRejectedValue(new Error('光点余额不足')); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), remainingMs: 180_000, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '冻结' })); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); expect(screen.getByRole('dialog', { name: '冻结时间' })).toBeTruthy(); expect(screen.getByText('光点余额不足')).toBeTruthy(); expect(onPauseChange).toHaveBeenLastCalledWith(true); }); test('冻结确认期间后端同步失败态时关闭确认窗并展示失败面板', async () => { const onUseProp = vi.fn().mockResolvedValue({ ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'failed', elapsedMs: 180_000, remainingMs: 0, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), remainingMs: 180_000, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; const { rerender } = renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '冻结' })); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); rerender( , ); expect(screen.queryByRole('dialog', { name: '冻结时间' })).toBeNull(); expect(screen.getByRole('dialog', { name: '关卡失败' })).toBeTruthy(); expect(screen.queryByTestId('puzzle-freeze-effect')).toBeNull(); }); test('倒计时归零时通知父层同步失败态', () => { vi.useFakeTimers(); const onTimeExpired = vi.fn(); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now() - 181_000, timeLimitMs: 180_000, remainingMs: 0, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; renderPuzzleRuntime( , ); expect(screen.getByRole('dialog', { name: '关卡失败' })).toBeTruthy(); expect(onTimeExpired).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); test('失败弹窗支持重开当前关和续时确认', async () => { const onRestartLevel = vi.fn(); const onUseProp = vi.fn().mockResolvedValue({ ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', remainingMs: 60_000, }, }); const failedRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'failed', elapsedMs: 180_000, remainingMs: 0, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; renderPuzzleRuntime( , ); const failedDialog = screen.getByRole('dialog', { name: '关卡失败' }); fireEvent.click(within(failedDialog).getByRole('button', { name: '重新开始' })); expect(onRestartLevel).toHaveBeenCalledTimes(1); fireEvent.click( within(failedDialog).getByRole('button', { name: '继续1分钟' }), ); expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy(); expect(screen.getByText('消耗 1 光点')).toBeTruthy(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); expect(onUseProp).toHaveBeenCalledWith('extendTime'); }); test('失败续时扣费失败时保留确认弹窗', async () => { const onUseProp = vi.fn().mockRejectedValue(new Error('光点余额不足')); const failedRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'failed', elapsedMs: 180_000, remainingMs: 0, board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '继续1分钟' })); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy(); expect(screen.getByText('光点余额不足')).toBeTruthy(); }); test('查看原图开关打开覆盖层并在关闭后恢复计时', async () => { const onPauseChange = vi.fn(); const onUseProp = vi.fn().mockResolvedValue(clearedRun); const playingRun: PuzzleRunSnapshot = { ...clearedRun, currentLevel: { ...clearedRun.currentLevel!, status: 'playing', startedAtMs: Date.now(), remainingMs: 180_000, coverImageSrc: '/puzzle.png', board: { ...clearedRun.currentLevel!.board, allTilesResolved: false, }, }, }; renderPuzzleRuntime( , ); fireEvent.click(screen.getByRole('button', { name: '原图' })); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '确定' })); }); expect(onUseProp).toHaveBeenCalledWith('reference'); expect(screen.getByTestId('puzzle-original-overlay')).toBeTruthy(); expect(onPauseChange).toHaveBeenLastCalledWith(true); fireEvent.click(screen.getByRole('button', { name: '原图' })); expect(screen.queryByTestId('puzzle-original-overlay')).toBeNull(); expect(onPauseChange).toHaveBeenLastCalledWith(false); }); test('拖拽层级辅助函数只提升当前被拖动对象', () => { expect(resolveDraggedPieceCellLayer('piece-0', 'piece-0', false)).toBe(80); expect( resolveDraggedPieceCellLayer('piece-0', 'piece-1', false), ).toBeUndefined(); expect( resolveDraggedPieceCellLayer('piece-0', 'piece-0', true), ).toBeUndefined(); expect(resolveDraggedPieceLayer('piece-0', 'piece-0', false)).toBe(81); expect(resolveDraggedPieceLayer('piece-0', null, false)).toBeUndefined(); expect(resolveDraggedPieceLayer('piece-0', 'piece-0', true)).toBeUndefined(); expect(resolveDraggedMergedGroupLayer('group-1', 'group-1')).toBe(90); expect(resolveDraggedMergedGroupLayer('group-1', 'group-2')).toBeUndefined(); });