From 4b80f15cb0a05bf984b916a10445ed32aa8124f8 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Jun 2026 19:00:08 +0200 Subject: [PATCH 1/3] fix(print): ensure all images are loaded before opening print dialog Fixes: #2553 Pass `noLazyImages` to createEditor when loading reader in print view, so images are fetched straight away and not lazy-loaded. Requires https://github.com/nextcloud/text/pull/8719 Also await Text's `onLoaded` callback in setupReader to await the async component loading of MarkdownContentEditor. Signed-off-by: Jonas --- src/composables/useReader.ts | 8 ++++++++ src/views/CollectivePrintView.vue | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/composables/useReader.ts b/src/composables/useReader.ts index 4ea9903a8..196b6a383 100644 --- a/src/composables/useReader.ts +++ b/src/composables/useReader.ts @@ -110,6 +110,11 @@ export function useReader(content: Ref) { customElements.define('page-info-bar', PageInfoBarCE) } + let resolveLoaded: () => void + const loadedPromise = new Promise((resolve) => { + resolveLoaded = resolve + }) + const readerInstance = await window.OCA.Text.createEditor({ el: readerEl.value, fileId, @@ -122,12 +127,14 @@ export function useReader(content: Ref) { component: 'page-info-bar', props: {}, }, + noLazyImages: rootStore.printView, openLinkHandler: window.OCA.Collectives.openLink, onOutlineToggle: pagesStore.setOutlineForCurrentPage, onLoaded: () => { nextTick(updateReadonlyBarProps) reader.value?.setSearchQuery(searchStore.searchQuery, searchStore.matchAll) reader.value?.setShowOutline(showCurrentPageOutline.value) + resolveLoaded() }, onSearch: (results: unknown) => { searchStore.setSearchResults(results) @@ -140,6 +147,7 @@ export function useReader(content: Ref) { // Use markRaw to prevent Vue 3 from proxying the Vue 2 editor instance reader.value = markRaw(readerInstance) + await loadedPromise if (!rootStore.loading('pageContent')) { reader.value?.setContent(content.value.trim()) diff --git a/src/views/CollectivePrintView.vue b/src/views/CollectivePrintView.vue index 92262133a..307d0dbdb 100644 --- a/src/views/CollectivePrintView.vue +++ b/src/views/CollectivePrintView.vue @@ -35,7 +35,7 @@ export default { ...mapState(useCollectivesStore, ['currentCollective']), }, - mounted() { + created() { this.setPrintView() }, From 9b8c3d8a8a902aeff1ee5e5f449d25fd39ef3088 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Jun 2026 19:06:31 +0200 Subject: [PATCH 2/3] fix(print): hide outline and table of contents Signed-off-by: Jonas --- src/components/PagePrint.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/PagePrint.vue b/src/components/PagePrint.vue index f308a7f5a..575e8807e 100644 --- a/src/components/PagePrint.vue +++ b/src/components/PagePrint.vue @@ -105,4 +105,9 @@ export default { :deep(.text-menubar) { display: none; } + +:deep(.editor__toc-container) { + // Hide outline + table of contents + display: none !important; +} From 7bfa71bc16ac2cbcbfe0689090d037c33c91334e Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Jun 2026 19:56:39 +0200 Subject: [PATCH 3/3] test(playwright): test that all images are loaded in print view Signed-off-by: Jonas --- playwright/e2e/collective-print.spec.ts | 56 +++++++++++++++++++ playwright/support/fixtures/CollectivePage.ts | 41 ++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 playwright/e2e/collective-print.spec.ts diff --git a/playwright/e2e/collective-print.spec.ts b/playwright/e2e/collective-print.spec.ts new file mode 100644 index 000000000..475a84169 --- /dev/null +++ b/playwright/e2e/collective-print.spec.ts @@ -0,0 +1,56 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect } from '@playwright/test' +import { test } from '../support/fixtures/create-collectives.ts' + +test.describe('Collective print view', () => { + test('loads all images before opening print dialog', async ({ user, page, collective }) => { + // Create two pages each with an image. The second page is below initial viewport. + for (let i = 0; i < 2; i++) { + const collectivePage = await collective.createPage({ + title: `Page with image ${i}`, + user, + page, + }) + const src = await collectivePage.uploadImage({ + filename: 'test.png', + user, + page, + }) + await collectivePage.setContent({ + content: `# Image ${i}\n\n![image ${i}](${src})\n`, + user, + page, + }) + } + + // Stub window.print so the real print dialog doesn't block the test + await page.addInitScript(() => { + (window as Window & { __printCalls?: number }).__printCalls = 0 + window.print = () => { + (window as Window & { __printCalls?: number }).__printCalls! += 1 + } + }) + + await page.goto(`/index.php/apps/collectives/_/print/${collective.getCollectiveUrlPart()}`) + + await expect(page.getByText('Preparing collective for exporting or printing')) + .toBeVisible() + await expect(page.getByText('Preparing collective for exporting or printing')) + .toBeHidden({ timeout: 30000 }) + + const printCalls = await page.evaluate(() => (window as Window & { __printCalls?: number }).__printCalls) + expect(printCalls).toBeGreaterThan(0) + + const imgs = page.locator('div.ProseMirror figure.image img.image__main') + const count = await imgs.count() + expect(count).toBe(2) + for (let i = 0; i < count; i++) { + const naturalWidth = await imgs.nth(i).evaluate((el: HTMLImageElement) => el.naturalWidth) + expect(naturalWidth, `image ${i} should have loaded`).toBeGreaterThan(0) + } + }) +}) diff --git a/playwright/support/fixtures/CollectivePage.ts b/playwright/support/fixtures/CollectivePage.ts index 68f83e08e..7305ebca5 100644 --- a/playwright/support/fixtures/CollectivePage.ts +++ b/playwright/support/fixtures/CollectivePage.ts @@ -6,6 +6,8 @@ import type { Page } from '@playwright/test' import type { User } from './User.ts' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { webdavUrl } from '../helpers/urls.ts' type CollectivePageData = { @@ -156,4 +158,43 @@ export class CollectivePage { const content = `## Link\n\n[${linkText}](${linkUrl})` await this.setContent({ content, user, page }) } + + async uploadImage({ filename, mimetype = 'image/png', user, page }: { + filename: string + mimetype?: string + user: User + page: Page + }): Promise { + const attachmentsDir = `.attachments.${this.data.id}` + + // MKCOL is idempotent enough: 201 = created, 405 = already exists + const dirUrl = webdavUrl( + user.account.userId, + this.data.collectivePath, + this.data.filePath, + attachmentsDir, + ) + const mkcol = await page.request.fetch(dirUrl, { method: 'MKCOL' }) + if (![201, 405].includes(mkcol.status())) { + throw new Error(`MKCOL ${dirUrl} failed: ${mkcol.status()}`) + } + + const filepath = resolve(import.meta.dirname, 'files', filename) + await page.request.put( + webdavUrl( + user.account.userId, + this.data.collectivePath, + this.data.filePath, + attachmentsDir, + filename, + ), + { + headers: { 'Content-Type': mimetype }, + data: readFileSync(filepath), + failOnStatusCode: true, + }, + ) + + return `${attachmentsDir}/${filename}` + } }