Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.5",
"version": "0.0.6",
"author": "RoleModel Software",
"license": "MIT",
"type": "module",
Expand Down
5 changes: 4 additions & 1 deletion src/components/pdf-viewer/pdf-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ export default class PDFViewer extends RoleModelElement {
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)

const url = new URL(this.src, window.location.href)
const filename = url.pathname.split('/').pop() || 'document.pdf'

const link = document.createElement('a')
link.href = blobUrl
link.download = this.src.split('/').pop() || 'document.pdf'
link.download = decodeURIComponent(filename)
link.click()

URL.revokeObjectURL(blobUrl)
Expand Down
109 changes: 109 additions & 0 deletions test/components/pdf-viewer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,115 @@ describe('PDFViewer Component', () => {
})
})

describe('Download PDF', () => {
let createdLink
let originalFetch

beforeEach(async () => {
element = await createViewer({ src: '/documents/report.pdf', open: true })

originalFetch = global.fetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
headers: { get: vi.fn().mockReturnValue('application/pdf') },
blob: vi.fn().mockResolvedValue(new Blob(['%PDF'], { type: 'application/pdf' }))
})

global.URL.createObjectURL = vi.fn().mockReturnValue('blob:mock-url')
global.URL.revokeObjectURL = vi.fn()

createdLink = null
const originalCreateElement = document.createElement.bind(document)
vi.spyOn(document, 'createElement').mockImplementation((tag) => {
const el = originalCreateElement(tag)
if (tag === 'a') createdLink = el
return el
})
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
})

afterEach(() => {
global.fetch = originalFetch
vi.restoreAllMocks()
})

it('should extract the filename from a plain URL', async () => {
await element.downloadPDF()

expect(createdLink.download).toBe('report.pdf')
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalled()
})

it('should strip query parameters from the filename', async () => {
element.src = '/documents/KP%20Covenant.pdf?disposition=inline'
await element.updateComplete

await element.downloadPDF()

expect(createdLink.download).toBe('KP Covenant.pdf')
})

it('should decode URL-encoded characters in the filename', async () => {
element.src = '/files/My%20Document%20%282024%29.pdf'
await element.updateComplete

await element.downloadPDF()

expect(createdLink.download).toBe('My Document (2024).pdf')
})

it('should fall back to "document.pdf" when the URL has no filename', async () => {
element.src = 'https://example.com/'
await element.updateComplete

await element.downloadPDF()

expect(createdLink.download).toBe('document.pdf')
})

it('should create a blob URL and revoke it after download', async () => {
await element.downloadPDF()

expect(URL.createObjectURL).toHaveBeenCalled()
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
})

it('should not fetch when src is empty', async () => {
element.src = ''
await element.updateComplete

await element.downloadPDF()

expect(global.fetch).not.toHaveBeenCalled()
})

it('should log an error when the fetch response is not ok', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found'
})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

await element.downloadPDF()

expect(consoleSpy).toHaveBeenCalled()
})

it('should log an error when the response content type is not PDF', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
headers: { get: vi.fn().mockReturnValue('text/html') },
blob: vi.fn()
})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

await element.downloadPDF()

expect(consoleSpy).toHaveBeenCalled()
})
})

describe('Toolbar', () => {
beforeEach(async () => {
element = await createViewer({ src: '/test.pdf', open: true })
Expand Down