Skip to content

Commit f1a8ad9

Browse files
✅(frontend) cover present from block
Exercise block-to-slide mapping and the editor side-menu action. Keep coverage scoped to starting the presenter from a block.
1 parent d0030b4 commit f1a8ad9

2 files changed

Lines changed: 158 additions & 4 deletions

File tree

src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,27 @@ test.describe('Presenter Mode', () => {
382382
);
383383
});
384384

385+
test('opens the presenter at the targeted slide from a block side menu', async ({
386+
page,
387+
browserName,
388+
}) => {
389+
await createDoc(page, 'presenter-side-menu', browserName, 1);
390+
await writeMultiSlideDoc(page);
391+
392+
await page
393+
.locator('.bn-block-outer')
394+
.filter({ hasText: 'Slide two' })
395+
.first()
396+
.hover();
397+
await page.locator('.bn-side-menu > button').last().click();
398+
await page.getByRole('menuitem', { name: 'Present' }).click();
399+
400+
const overlay = page.getByRole('dialog', { name: 'Presenter mode' });
401+
await expect(overlay).toBeVisible();
402+
await expect(overlay.getByText('3 / 4')).toBeVisible();
403+
await expect(overlay.getByText('Slide two')).toBeVisible();
404+
});
405+
385406
test('opens presenter deep-links and normalizes slide params', async ({
386407
page,
387408
browserName,

src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, test } from 'vitest';
22

3-
import { getSlideTitle, splitBlocksIntoSlides } from '../hooks/useSlides';
3+
import {
4+
getContentSlideIndexForBlock,
5+
getSlideTitle,
6+
splitBlocksIntoSlides,
7+
} from '../hooks/useSlides';
48
import type { PresenterBlock } from '../types';
59

610
type TestBlock = PresenterBlock;
@@ -23,13 +27,14 @@ const textContent = (text: string) => ({
2327
type: 'text' as const,
2428
});
2529

26-
const para = (text = 'hello'): TestBlock =>
30+
const para = (text = 'hello', id?: string): TestBlock =>
2731
block('paragraph', {
2832
content: text === '' ? [] : [textContent(text)],
33+
...(id ? { id } : {}),
2934
});
3035

31-
const divider = (children: TestBlock[] = []): TestBlock =>
32-
block('divider', { children });
36+
const divider = (children: TestBlock[] = [], id?: string): TestBlock =>
37+
block('divider', { children, ...(id ? { id } : {}) });
3338

3439
const image = (): TestBlock => block('image', { props: { url: 'x' } });
3540

@@ -201,6 +206,134 @@ describe('splitBlocksIntoSlides', () => {
201206
});
202207
});
203208

209+
describe('getContentSlideIndexForBlock', () => {
210+
test('returns the slide containing a regular block', () => {
211+
expect(
212+
getContentSlideIndexForBlock(
213+
[para('a', 'a'), divider(), para('b', 'b'), para('c', 'c')],
214+
'c',
215+
),
216+
).toBe(1);
217+
});
218+
219+
test('returns the following slide for a divider', () => {
220+
expect(
221+
getContentSlideIndexForBlock(
222+
[para('a', 'a'), divider(undefined, 'divider'), para('b', 'b')],
223+
'divider',
224+
),
225+
).toBe(1);
226+
});
227+
228+
test('returns the first non-empty following slide for consecutive dividers', () => {
229+
expect(
230+
getContentSlideIndexForBlock(
231+
[
232+
para('a', 'a'),
233+
divider(undefined, 'first-divider'),
234+
divider(undefined, 'second-divider'),
235+
para('b', 'b'),
236+
],
237+
'first-divider',
238+
),
239+
).toBe(1);
240+
});
241+
242+
test('skips empty-only slides after dividers when mapping divider links', () => {
243+
expect(
244+
getContentSlideIndexForBlock(
245+
[
246+
para('a', 'a'),
247+
divider(undefined, 'first-divider'),
248+
para('', 'empty-after-divider'),
249+
divider(undefined, 'second-divider'),
250+
para('b', 'b'),
251+
],
252+
'first-divider',
253+
),
254+
).toBe(1);
255+
});
256+
257+
test('maps a stripped empty block after a divider to the following content slide', () => {
258+
expect(
259+
getContentSlideIndexForBlock(
260+
[
261+
para('a', 'a'),
262+
divider(undefined, 'divider'),
263+
para('', 'empty-after-divider'),
264+
para('b', 'b'),
265+
],
266+
'empty-after-divider',
267+
),
268+
).toBe(1);
269+
});
270+
271+
test('maps a stripped empty block before a divider to the previous content slide', () => {
272+
expect(
273+
getContentSlideIndexForBlock(
274+
[
275+
para('a', 'a'),
276+
para('', 'empty-before-divider'),
277+
divider(undefined, 'divider'),
278+
para('b', 'b'),
279+
],
280+
'empty-before-divider',
281+
),
282+
).toBe(0);
283+
});
284+
285+
test('maps a stripped empty-only slide to the next rendered content slide', () => {
286+
expect(
287+
getContentSlideIndexForBlock(
288+
[
289+
para('a', 'a'),
290+
divider(undefined, 'first-divider'),
291+
para('', 'empty-between-dividers'),
292+
divider(undefined, 'second-divider'),
293+
para('b', 'b'),
294+
],
295+
'empty-between-dividers',
296+
),
297+
).toBe(1);
298+
});
299+
300+
test('maps a leading divider to the first content slide', () => {
301+
expect(
302+
getContentSlideIndexForBlock(
303+
[divider(undefined, 'divider'), para('a', 'a')],
304+
'divider',
305+
),
306+
).toBe(0);
307+
});
308+
309+
test('maps a trailing divider to the last rendered content slide', () => {
310+
expect(
311+
getContentSlideIndexForBlock(
312+
[para('a', 'a'), divider(undefined, 'divider')],
313+
'divider',
314+
),
315+
).toBe(0);
316+
});
317+
318+
test('finds blocks nested under a structural divider', () => {
319+
expect(
320+
getContentSlideIndexForBlock(
321+
[para('a', 'a'), divider([para('nested', 'nested')], 'divider')],
322+
'nested',
323+
),
324+
).toBe(1);
325+
});
326+
327+
test('returns the first content slide when the block is missing', () => {
328+
expect(
329+
getContentSlideIndexForBlock(
330+
[para('a', 'a'), divider(), para('b', 'b')],
331+
'x',
332+
),
333+
).toBe(0);
334+
});
335+
});
336+
204337
const heading = (text: string): TestBlock =>
205338
block('heading', { content: [textContent(text)] });
206339

0 commit comments

Comments
 (0)