From 3ebac45dc34ff72ea650b4d02a314affefe57e35 Mon Sep 17 00:00:00 2001 From: Mark Kraemer Date: Wed, 7 Oct 2026 14:47:53 -0400 Subject: [PATCH 1/2] Cancel superseded PDF renders so a stale zoom can't paint the page A page re-renders each time its scale changes. When a render was cancelled, its catch block cleared _renderTask even though that field already pointed at the newer render. The next scale change then couldn't cancel it: it resized the canvas under the still-running render, and pdf.js rejected the new render because the canvas was in use. The stale render finished drawing at the old zoom. Only clear _renderTask when it still holds this call's own task. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/components/pdf-viewer/page/pdf-page.js | 10 ++-- test/components/pdf-page.test.js | 58 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 test/components/pdf-page.test.js diff --git a/package.json b/package.json index bf32583..b9e7dbc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@rolemodel/spider", "description": "Shared high level web components for RoleModel Software and beyond", "packageManager": "yarn@4.12.0", - "version": "0.0.10", + "version": "0.0.11", "author": "RoleModel Software", "license": "MIT", "type": "module", diff --git a/src/components/pdf-viewer/page/pdf-page.js b/src/components/pdf-viewer/page/pdf-page.js index ba77ad5..108f451 100644 --- a/src/components/pdf-viewer/page/pdf-page.js +++ b/src/components/pdf-viewer/page/pdf-page.js @@ -65,14 +65,16 @@ export default class PDFPage extends PDFViewerComponent { const scaledViewport = this.page.getViewport({ scale: this.scale * devicePixelRatio }) + let renderTask + try { - this._renderTask = this.page.render({ + renderTask = this._renderTask = this.page.render({ canvasContext, viewport: scaledViewport }) - await this._renderTask.promise - this._renderTask = null + await renderTask.promise + if (this._renderTask === renderTask) this._renderTask = null const textLayerDiv = this.shadowRoot.querySelector('.text-layer') textLayerDiv.style.width = `${viewport.width}px` @@ -91,7 +93,7 @@ export default class PDFPage extends PDFViewerComponent { if (error.name !== 'RenderingCancelledException') { console.error('Error rendering page:', error) } - this._renderTask = null + if (this._renderTask === renderTask) this._renderTask = null } } diff --git a/test/components/pdf-page.test.js b/test/components/pdf-page.test.js new file mode 100644 index 0000000..eba2726 --- /dev/null +++ b/test/components/pdf-page.test.js @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import '../../src/components/pdf-viewer/page/pdf-page.js' +import { createMockPage } from '../helpers/test-utils.js' + +function createPendingRenderTask() { + let reject + const task = { + promise: new Promise((_, r) => { reject = r }), + cancel: vi.fn(() => { + const error = new Error('Rendering cancelled') + error.name = 'RenderingCancelledException' + reject(error) + }) + } + return task +} + +const flushPromises = () => new Promise((resolve) => setTimeout(resolve)) + +describe('PDFPage Component', () => { + let element + let renderTasks + + beforeEach(async () => { + renderTasks = [] + const page = createMockPage(1) + page.render = vi.fn(() => { + const task = createPendingRenderTask() + renderTasks.push(task) + return task + }) + + element = document.createElement('rm-pdf-page') + document.body.appendChild(element) + await element.updateComplete + + element.page = page + await flushPromises() + }) + + afterEach(() => { + element.remove() + }) + + describe('Overlapping renders', () => { + it('cancels every superseded render so only the latest one draws', async () => { + element.renderPage() + await flushPromises() + element.renderPage() + await flushPromises() + + expect(renderTasks).toHaveLength(3) + expect(renderTasks[0].cancel).toHaveBeenCalled() + expect(renderTasks[1].cancel).toHaveBeenCalled() + expect(renderTasks[2].cancel).not.toHaveBeenCalled() + }) + }) +}) From 65229d642e6dda2929b85e9f44ccdf3ff1670504 Mon Sep 17 00:00:00 2001 From: Mark Kraemer Date: Wed, 7 Oct 2026 15:58:52 -0400 Subject: [PATCH 2/2] Remove stray ) rendered below the PDF pages The pages loop in the canvas template closed with `)})`, leaving a literal ")" in the container under the last page. Co-Authored-By: Claude Opus 5.5 --- src/components/pdf-viewer/canvas/pdf-canvas.js | 2 +- test/components/pdf-viewer.test.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index 9698a18..40e1e22 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -124,7 +124,7 @@ export default class PDFCanvas extends PDFViewerComponent { .searchMatches=${this.context?.searchMatches || []} .currentMatchIndex=${this.context?.currentMatchIndex ?? -1}> - `)}) + `)} ` } diff --git a/test/components/pdf-viewer.test.js b/test/components/pdf-viewer.test.js index 8fd2325..889ffa9 100644 --- a/test/components/pdf-viewer.test.js +++ b/test/components/pdf-viewer.test.js @@ -76,6 +76,17 @@ describe('PDFViewer Component', () => { expect(element.pdfDoc).toBeDefined() expect(element.totalPages).toBe(5) }) + + it('should render only pages in the canvas container', async () => { + element = await createViewer({ src: '/test.pdf', open: true }) + await waitForCondition(() => element.pdfDoc !== null) + + const canvas = element.shadowRoot.querySelector('rm-pdf-canvas') + await waitForCondition(() => canvas?.shadowRoot?.querySelectorAll('rm-pdf-page').length === 5) + + const container = canvas.shadowRoot.querySelector('.canvas-container') + expect(container.textContent.trim()).toBe('') + }) }) describe('Navigation', () => {