/
githubmirror
/
reveal.js
Обзор
Документация
Войти
/
githubmirror
/
reveal.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
react/src/components/deck.test.tsx
496 строк
11 KB
Hakim El Hattab
react: add support for md via new Markdown component, refactor code structure
18 мар 2026, 20:46
18 мар 2026, 20:46
f1af34f
Код
Авторство
О чём код?
import { render, act, cleanup } from '@testing-library/react'; import { StrictMode } from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; const mockApi = vi.hoisted(() => ({ initialize: vi.fn().mockResolvedValue(undefined), destroy: vi.fn(), sync: vi.fn(), isReady: vi.fn().mockReturnValue(true), on: vi.fn(), off: vi.fn(), configure: vi.fn(), })); const RevealConstructor = vi.hoisted(() => vi.fn(function () { return mockApi; }) ); vi.mock('reveal.js', () => ({ default: RevealConstructor })); import { Deck, Slide, Markdown, useReveal } from '../index'; beforeEach(() => { vi.clearAllMocks(); mockApi.initialize.mockResolvedValue(undefined); mockApi.isReady.mockReturnValue(true); }); describe('Deck', () => { it('renders .reveal > .slides structure', async () => { let container: HTMLElement; await act(async () => { ({ container } = render( <Deck> <Slide>Hello</Slide> </Deck> )); }); const reveal = container!.querySelector('.reveal'); expect(reveal).toBeInTheDocument(); const slides = reveal?.querySelector('.slides'); expect(slides).toBeInTheDocument(); const section = slides?.querySelector('section'); expect(section).toHaveTextContent('Hello'); }); it('calls new Reveal() and initialize() on mount', async () => { await act(async () => { render( <Deck config={{ transition: 'slide', hash: true }}> <Slide>Test</Slide> </Deck> ); }); expect(RevealConstructor).toHaveBeenCalledWith( expect.any(HTMLElement), expect.objectContaining({ transition: 'slide', hash: true, plugins: [], }) ); expect(mockApi.initialize).toHaveBeenCalledTimes(1); }); it('calls destroy() on unmount', async () => { await act(async () => { render( <Deck> <Slide>Test</Slide> </Deck> ); }); await act(async () => { cleanup(); }); expect(mockApi.destroy).toHaveBeenCalledTimes(1); }); it('calls sync() after render when ready', async () => { await act(async () => { render( <Deck> <Slide>Test</Slide> </Deck> ); }); expect(mockApi.sync).toHaveBeenCalled(); }); it('does not call sync() when slide content changes without structural changes', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck> <Slide> <p>Tick 1</p> </Slide> </Deck> )); }); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck> <Slide> <p>Tick 2</p> </Slide> </Deck> ); }); expect(mockApi.sync).not.toHaveBeenCalled(); }); it('calls sync() when Markdown content changes', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck> <Markdown markdown="## Tick 1" /> </Deck> )); }); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck> <Markdown markdown="## Tick 2" /> </Deck> ); }); expect(mockApi.sync).toHaveBeenCalledTimes(1); }); it('calls sync() when Markdown slide attributes change', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck> <Markdown markdown="## Tick" backgroundColor="#111111" /> </Deck> )); }); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck> <Markdown markdown="## Tick" backgroundColor="#222222" /> </Deck> ); }); expect(mockApi.sync).toHaveBeenCalledTimes(1); }); it('does not call sync() twice when configure() already syncs', async () => { mockApi.configure.mockImplementation(() => { mockApi.sync(); }); let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck config={{ transition: 'slide' }}> <Slide>Test</Slide> </Deck> )); }); mockApi.configure.mockClear(); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck config={{ transition: 'convex' }}> <Slide>Test</Slide> </Deck> ); }); expect(mockApi.configure).toHaveBeenCalledTimes(1); expect(mockApi.sync).toHaveBeenCalledTimes(1); }); it('calls sync() when keyed slides are reordered', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck> {['first', 'second'].map((label) => ( <Slide key={label}>{label}</Slide> ))} </Deck> )); }); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck> {['second', 'first'].map((label) => ( <Slide key={label}>{label}</Slide> ))} </Deck> ); }); expect(mockApi.sync).toHaveBeenCalledTimes(1); }); it('calls sync() after children change even when isReady() was false during the config-change render', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck config={{ transition: 'slide' }}> <Slide>Slide 1</Slide> </Deck> )); }); // Make isReady() return true for the configure effect but false for the sync effect, // simulating Reveal briefly being non-ready during configuration. Without the fix the // skip flag would never be reset and the subsequent sync() call would be wrongly skipped. mockApi.isReady.mockReturnValueOnce(true).mockReturnValueOnce(false); await act(async () => { rerender( <Deck config={{ transition: 'convex' }}> <Slide>Slide 1</Slide> </Deck> ); }); mockApi.isReady.mockReturnValue(true); mockApi.sync.mockClear(); await act(async () => { rerender( <Deck config={{ transition: 'convex' }}> <Slide>Slide 1</Slide> <Slide>Slide 2</Slide> </Deck> ); }); expect(mockApi.sync).toHaveBeenCalledTimes(1); }); it('does not reconfigure when config object is recreated with same values', async () => { let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck config={{ transition: 'slide', hash: true }}> <Slide>Test</Slide> </Deck> )); }); mockApi.configure.mockClear(); await act(async () => { rerender( <Deck config={{ transition: 'slide', hash: true }}> <Slide>Test</Slide> </Deck> ); }); expect(mockApi.configure).not.toHaveBeenCalled(); }); it('fires onReady callback after initialization', async () => { const onReady = vi.fn(); await act(async () => { render( <Deck onReady={onReady}> <Slide>Test</Slide> </Deck> ); }); expect(onReady).toHaveBeenCalledWith(mockApi); }); it('wires onSlideChange to the slidechanged event', async () => { const onSlideChange = vi.fn(); await act(async () => { render( <Deck onSlideChange={onSlideChange}> <Slide>Test</Slide> </Deck> ); }); expect(mockApi.on).toHaveBeenCalledWith('slidechanged', onSlideChange); }); it('wires onSlideSync to the slidesync event', async () => { const onSlideSync = vi.fn(); await act(async () => { render( <Deck onSlideSync={onSlideSync}> <Slide>Test</Slide> </Deck> ); }); expect(mockApi.on).toHaveBeenCalledWith('slidesync', onSlideSync); }); it('cleans up event listeners on unmount', async () => { const onSlideChange = vi.fn(); await act(async () => { render( <Deck onSlideChange={onSlideChange}> <Slide>Test</Slide> </Deck> ); }); await act(async () => { cleanup(); }); expect(mockApi.off).toHaveBeenCalledWith('slidechanged', onSlideChange); }); it('passes plugins to the Reveal constructor', async () => { const fakePlugin = () => ({ id: 'fake', init: () => {} }); await act(async () => { render( <Deck plugins={[fakePlugin]}> <Slide>Test</Slide> </Deck> ); }); expect(RevealConstructor).toHaveBeenCalledWith( expect.any(HTMLElement), expect.objectContaining({ plugins: [fakePlugin] }) ); }); it('registers plugins only on initialization', async () => { const pluginA = () => ({ id: 'a', init: () => {} }); const pluginB = () => ({ id: 'b', init: () => {} }); let rerender: ReturnType<typeof render>['rerender']; await act(async () => { ({ rerender } = render( <Deck plugins={[pluginA]}> <Slide>Test</Slide> </Deck> )); }); mockApi.configure.mockClear(); await act(async () => { rerender( <Deck plugins={[pluginB]}> <Slide>Test</Slide> </Deck> ); }); expect(RevealConstructor).toHaveBeenCalledTimes(1); expect(RevealConstructor).toHaveBeenCalledWith( expect.any(HTMLElement), expect.objectContaining({ plugins: [pluginA] }) ); expect(mockApi.configure).not.toHaveBeenCalled(); }); it('applies className and style to the .reveal div', async () => { let container: HTMLElement; await act(async () => { ({ container } = render( <Deck className="custom" style={{ height: '400px' }}> <Slide>Test</Slide> </Deck> )); }); const reveal = container!.querySelector('.reveal'); expect(reveal).toHaveClass('reveal', 'custom'); expect(reveal).toHaveStyle({ height: '400px' }); }); it('does not initialize twice in StrictMode', async () => { await act(async () => { render( <StrictMode> <Deck> <Slide>Test</Slide> </Deck> </StrictMode> ); }); expect(RevealConstructor).toHaveBeenCalledTimes(1); expect(mockApi.initialize).toHaveBeenCalledTimes(1); }); it('updates deckRef when the ref prop changes', async () => { const refA = vi.fn(); const refB = vi.fn(); let rerender: ReturnType<typeof render>['rerender']; let unmount: ReturnType<typeof render>['unmount']; await act(async () => { ({ rerender, unmount } = render( <Deck deckRef={refA}> <Slide>Test</Slide> </Deck> )); }); expect(refA).toHaveBeenCalledWith(mockApi); await act(async () => { rerender( <Deck deckRef={refB}> <Slide>Test</Slide> </Deck> ); }); expect(refA).toHaveBeenCalledWith(null); expect(refB).toHaveBeenCalledWith(mockApi); await act(async () => { unmount(); }); expect(refB).toHaveBeenCalledWith(null); }); }); describe('useReveal', () => { it('returns the RevealApi instance inside a Deck', async () => { let hookResult: any = undefined; function Inspector() { hookResult = useReveal(); return null; } await act(async () => { render( <Deck> <Slide> <Inspector /> </Slide> </Deck> ); }); expect(hookResult).toBe(mockApi); }); it('returns null outside of a Deck', () => { let hookResult: any = 'not-null'; function Inspector() { hookResult = useReveal(); return null; } render(<Inspector />); expect(hookResult).toBeNull(); }); });