Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to

### Changed

- ♻️(frontend) hide unavailable manual version actions in the history sidebar
- ⬆️(frontend) update BlockNote preview packages to 83153fda2e
- 🐛(frontend) adapt to the BlockNote 0.56 editor and exporter APIs
- ♻️(frontend) restore bounded editor history through native yhub rollback
Expand All @@ -26,6 +27,14 @@ and this project adheres to

### Fixed

- ✅(e2e) insert tall-slide filler without per-character keyboard overhead
- 🐛(frontend) reconcile current comment state after leaving version history
- 🐛(frontend) authenticate history requests and use consistent snapshot IDs
- ✅(e2e) exercise sidebar history and renderer-independent PDF metadata
- 🐛(frontend) bind collaborative editing after HTTP or offline readiness
- ✅(e2e) locate editor and comment menus outside their trigger containers
- 🐛(frontend) confirm history restores and show author-only attribution
- 🐛(export) preserve DOCX table children and accept modern Yjs updates
- 🐛(frontend) fetch named versions fresh after deleting one
- 🐛(collaboration) restore editor versions without exposing full history
- 🐛(collaboration) grant bounded named-version access for the history sidebar
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ test.describe('Doc Comments', () => {
// Edit Comment
await thread.getByText('This is a comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
await page.getByRole('menuitem', { name: 'Edit comment' }).click();
const commentEditor = thread.getByText('This is a comment').first();
await commentEditor.fill('This is an edited comment');
const saveBtn = thread.locator('button[data-test="save"]').first();
Expand All @@ -187,7 +187,7 @@ test.describe('Doc Comments', () => {
// Delete second comment
await thread.getByText('This is a second comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await page.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(
thread.getByText('This is a second comment').first(),
).toBeHidden();
Expand Down Expand Up @@ -240,7 +240,7 @@ test.describe('Doc Comments', () => {

await thread.getByText('This is a new comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await page.getByRole('menuitem', { name: 'Delete comment' }).click();

await expect(editor.getByText('Hello')).not.toHaveClass('bn-thread-mark');
await expect(editor.getByText('Hello')).toHaveCSS(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,23 @@ test.describe('Doc Editor', () => {

// Only heading levels 1 to 3 are offered in the block type select
await toolbar.getByRole('button', { name: 'Paragraph' }).click();
const blockTypeMenu = page.getByRole('menu', { name: 'Paragraph' });
for (const level of [1, 2, 3]) {
await expect(
toolbar.getByRole('menuitem', {
blockTypeMenu.getByRole('menuitem', {
name: `Heading ${level}`,
exact: true,
}),
).toBeVisible();
await expect(
toolbar.getByRole('menuitem', { name: `Toggle Heading ${level}` }),
blockTypeMenu.getByRole('menuitem', {
name: `Toggle Heading ${level}`,
}),
).toBeVisible();
}
for (const level of [4, 5, 6]) {
await expect(
toolbar.getByRole('menuitem', { name: `Heading ${level}` }),
blockTypeMenu.getByRole('menuitem', { name: `Heading ${level}` }),
).toBeHidden();
}
await page.keyboard.press('Escape');
Expand Down
15 changes: 11 additions & 4 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,15 @@ test.describe('Doc Export', () => {
await page.waitForTimeout(1000);

const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfString = pdfBuffer.toString('latin1');

expect(pdfString).toContain('/Lang (fr)');
const pdf = new PDFParse({ data: pdfBuffer });
try {
// Read the catalog's language, independent of PDF token whitespace or
// whether the exporter stores the catalog in a compressed object stream.
const { info } = await pdf.getInfo();
expect(info?.Language).toBe('fr');
} finally {
await pdf.destroy();
}
});

test('it exports the doc to PDF with PRINT feature and checks regressions', async ({
Expand Down Expand Up @@ -461,7 +467,8 @@ test.describe('Doc Export', () => {
// If we need to update the PDF regression fixture, uncomment the line below
//await savePDFToAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');

// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
// Reviewed Typst baseline: keep exact text (including emoji and math),
// page count, dimensions and per-page pixel comparisons enabled.
await comparePDFWithAssetFolder({
originPdfBuffer: pdfBuffer,
filename: 'doc-export-regressions.pdf',
Expand Down
136 changes: 72 additions & 64 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
import { expect, test } from '@playwright/test';
import { Page, expect, test } from '@playwright/test';

import {
createDoc,
goToGridDoc,
mockedDocument,
overrideConfig,
verifyDocName,
} from './utils-common';
import { openSuggestionMenu, writeInEditor } from './utils-editor';

const COLLABORATION_VERSION_GRANULARITY_MS = 2000;

async function openHistory(page: Page) {
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'History', exact: true }).click();
const panel = page.getByRole('complementary', {
name: 'Version history side panel',
});
await expect(panel).toBeVisible();
// Production grouping has a five-minute minimum. Exercise the real debug
// control instead of overriding a setting which the editor deliberately clamps.
await panel.locator('summary').click();
await panel
.getByRole('combobox', { name: 'Grouping time unit' })
.selectOption('1000');
await panel.getByLabel('Group by', { exact: true }).fill('2');
await panel.getByRole('button', { name: 'Apply', exact: true }).click();
await expect(
panel.getByText('Grouping updated.', { exact: true }),
).toBeVisible();
const versions = panel.getByRole('list', { name: 'Versions', exact: true });
await expect(versions.getByRole('listitem').first()).toBeVisible();
await expect(panel.getByRole('alert')).toBeHidden();
return { panel, versions };
}

async function captureLatestVersion(page: Page) {
const { panel, versions } = await openHistory(page);
const id = await versions.getByRole('listitem').first().getAttribute('id');
expect(id).toContain('-snapshot-');
const snapshotId = id!.split('-snapshot-')[1];
await panel.getByRole('button', { name: 'Close', exact: true }).click();
return snapshotId;
}

test.describe('Doc Version', () => {
test('it displays the doc versions', async ({ page, browserName }) => {
await overrideConfig(page, {
COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`,
});

await page.goto('/');

await createDoc(page, 'doc-version', browserName, 1);

// Initially, there is no version
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'History' }).click();
await expect(page.getByText('History', { exact: true })).toBeVisible();

const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('Version list');

await modal.getByRole('button', { name: 'close' }).click();

await writeInEditor({ page, text: 'Hello World' });
const editor = await writeInEditor({ page, text: 'Hello World' });
const firstVersion = await captureLatestVersion(page);

await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10);

Expand All @@ -46,6 +65,7 @@ test.describe('Doc Version', () => {
.first();

await expect(calloutBlock).toBeVisible();
const secondVersion = await captureLatestVersion(page);

await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10);

Expand All @@ -54,42 +74,39 @@ test.describe('Doc Version', () => {

await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10);

await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'History' }).click();

await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
await expect(page.getByRole('status')).toBeHidden();
const items = panel.locator('.version-item');
await expect(items).toHaveCount(3);
await items.nth(2).click();
const { versions } = await openHistory(page);
const items = versions.getByRole('listitem');
// The start of the document is pinned separately from its first edit.
await items.last().click();
await expect(editor.getByText('Hello World')).toBeHidden();
await versions.locator(`[id$="-snapshot-${firstVersion}"]`).click();

await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeHidden();
await expect(editor.getByText('Hello World')).toBeVisible();
await expect(editor.getByText('It will create a version')).toBeHidden();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
editor.locator('div[data-content-type="callout"]').first(),
).toBeHidden();

await items.nth(1).click();
await versions.locator(`[id$="-snapshot-${secondVersion}"]`).click();

await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeVisible();
await expect(editor.getByText('Hello World')).toBeVisible();
await expect(editor.getByText('It will create a version')).toBeVisible();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
editor.locator('div[data-content-type="callout"]').first(),
).toBeVisible();
await expect(
modal.getByText('It will create a second version'),
editor.getByText('It will create a second version'),
).toBeHidden();

await items.nth(0).click();

await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeVisible();
await expect(editor.getByText('Hello World')).toBeVisible();
await expect(editor.getByText('It will create a version')).toBeVisible();
await expect(
modal.locator('div[data-content-type="callout"]').first(),
editor.locator('div[data-content-type="callout"]').first(),
).toBeVisible();
await expect(
modal.getByText('It will create a second version'),
editor.getByText('It will create a second version'),
).toBeVisible();
});

Expand All @@ -114,10 +131,6 @@ test.describe('Doc Version', () => {
});

test('it restores the doc version', async ({ page, browserName }) => {
await overrideConfig(page, {
COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`,
});

await page.goto('/');

await createDoc(page, 'doc-version', browserName, 1);
Expand All @@ -132,6 +145,7 @@ test.describe('Doc Version', () => {
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread).toBeHidden();
const firstVersion = await captureLatestVersion(page);

await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10);

Expand All @@ -144,28 +158,22 @@ test.describe('Doc Version', () => {
await thread.locator('[data-test="resolve"]').click();
await expect(thread).toBeHidden();

await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'History' }).click();

const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('Version list');
await expect(panel).toBeVisible();

await expect(page.getByText('History', { exact: true })).toBeVisible();
const items = panel.locator('.version-item');
await expect(items).toHaveCount(3);
await items.nth(2).click();

await expect(modal.getByText('World')).toBeHidden();

await page.getByRole('button', { name: 'Restore', exact: true }).click();
await expect(
page.getByText(
"The current document will be replaced, but you'll still find it in the version history.",
),
).toBeVisible();

await page.getByLabel('Restore', { exact: true }).click();
const { panel, versions } = await openHistory(page);
const version = versions.locator(`[id$="-snapshot-${firstVersion}"]`);
await version.click();
await expect(editor.getByText('World')).toBeHidden();

await version
.getByRole('button', { name: 'More actions', exact: true })
.click();
await page.getByRole('menuitem', { name: 'Restore', exact: true }).click();
const confirmation = page.getByRole('dialog', { name: 'Restore version' });
await expect(confirmation).toBeVisible();
await confirmation
.getByRole('button', { name: 'Restore', exact: true })
.click();
await expect(confirmation).toBeHidden();
await expect(panel).toBeHidden();

const mainEditor = page.getByLabel('Document editor');

Expand Down
4 changes: 4 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ test.describe('Language', () => {
LANGUAGE_CODE: 'en-us',
});

// The beforeEach navigation has already loaded the default config. Reload
// after installing the override; client-side navigation keeps that cache.
await page.goto('/');

await createDoc(page, 'doc-toolbar', browserName, 1);

const { editor, suggestionMenu } = await openSuggestionMenu({ page });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,14 @@ test.describe('Presenter Mode', () => {
const editor = await writeInEditor({ page, text: 'TOP MARKER' });
for (let i = 0; i < 40; i += 1) {
await editor.press('Enter');
await editor.pressSequentially(`Filler line ${i} to make the slide tall`);
// This exercises layout, not per-character keyboard handling.
await page.keyboard.insertText(`Filler line ${i} to make the slide tall`);
}
await expect(
editor.getByText('Filler line 39 to make the slide tall', {
exact: true,
}),
).toBeVisible();

const overlay = await openPresenter(page);
await overlay.getByRole('button', { name: 'Next slide' }).click();
Expand Down
9 changes: 6 additions & 3 deletions src/frontend/apps/e2e/__tests__/app-impress/utils-editor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Page } from '@playwright/test';
import { Page, expect } from '@playwright/test';

export const getEditor = async ({ page }: { page: Page }) => {
const editor = page.locator('.--docs--editor-container .ProseMirror');
Expand Down Expand Up @@ -34,10 +34,13 @@ export const openSuggestionMenu = async ({
suggestion?: string;
}) => {
const editor = await tryFocusEditorContent({ page });
await page.keyboard.press('Enter');
await page.keyboard.type('/');
// Resolve and focus the current editor for each key: a collaborative editor
// rerender can replace the focused DOM node between these actions.
await editor.press('Enter');
await editor.press('/');

const suggestionMenu = page.locator('.bn-suggestion-menu');
await expect(suggestionMenu).toBeVisible();

if (suggestion) {
await suggestionMenu
Expand Down
Loading
Loading