f07a34c51b
* fix(invoices): stop popup blockers from silently eating the PDF preview tab A window.open() after an await runs outside the click's transient user activation (~5s, less in Safari), so the preview tab was popup-blocked exactly when generation was slow (cold start + logo re-encode). The request succeeded, nothing opened, no error: the button looked locked (support: carina@cbysea.se). - lib/browser/deferred-tab.ts: open the tab synchronously in the click, navigate it when the result arrives, close it on failure (the pattern AGIPanel already used for its signing tab), with unit tests. - InvoiceEditor: preview uses the deferred tab + popup-blocked toast; revoke the blob URL instead of leaking it; guard the review dialog against an unresolved customer (silent no-op click); 5s timeout on the pre-review next-number fetch; spinner + disable while the submit handler is in flight in create mode. - Same pre-open fix in TransactionAttachmentIndicator, JournalEntryAttachments (failures now toast instead of vanishing), DocumentViewButton, and the Arcim reconnect OAuth popup (its 'trusted gesture' comment was wrong after the await). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): review triage: close blocked preview tab, precise popup hint, test convention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { openDeferredTab } from '@/lib/browser/deferred-tab'
|
|
|
|
function makeFakeTab() {
|
|
return {
|
|
closed: false,
|
|
opener: {} as unknown,
|
|
close: vi.fn(),
|
|
location: { href: '' },
|
|
document: {
|
|
title: '',
|
|
body: { appendChild: vi.fn() },
|
|
createElement: vi.fn(() => ({ textContent: '', style: { cssText: '' } })),
|
|
},
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe('openDeferredTab', () => {
|
|
it('is SSR-safe: reports blocked when window is undefined', () => {
|
|
const tab = openDeferredTab()
|
|
expect(tab.blocked).toBe(true)
|
|
expect(tab.navigate('https://example.com')).toBe(false)
|
|
expect(() => tab.close()).not.toThrow()
|
|
})
|
|
|
|
it('reports blocked and refuses navigation when window.open returns null', () => {
|
|
vi.stubGlobal('window', { open: vi.fn(() => null) })
|
|
const tab = openDeferredTab('Laddar...')
|
|
expect(tab.blocked).toBe(true)
|
|
expect(tab.navigate('https://example.com')).toBe(false)
|
|
expect(() => tab.close()).not.toThrow()
|
|
})
|
|
|
|
it('opens synchronously, severs opener, and navigates the pending tab', () => {
|
|
const fake = makeFakeTab()
|
|
const open = vi.fn(() => fake)
|
|
vi.stubGlobal('window', { open })
|
|
|
|
const tab = openDeferredTab('Genererar...')
|
|
expect(open).toHaveBeenCalledWith('', '_blank')
|
|
expect(tab.blocked).toBe(false)
|
|
expect(fake.opener).toBeNull()
|
|
expect(fake.document.title).toBe('Genererar...')
|
|
expect(fake.document.body.appendChild).toHaveBeenCalledTimes(1)
|
|
|
|
expect(tab.navigate('blob:https://app/abc')).toBe(true)
|
|
expect(fake.location.href).toBe('blob:https://app/abc')
|
|
})
|
|
|
|
it('skips the placeholder when none is given', () => {
|
|
const fake = makeFakeTab()
|
|
vi.stubGlobal('window', { open: vi.fn(() => fake) })
|
|
openDeferredTab()
|
|
expect(fake.document.body.appendChild).not.toHaveBeenCalled()
|
|
expect(fake.document.title).toBe('')
|
|
})
|
|
|
|
it('fails navigation once the user closed the pending tab', () => {
|
|
const fake = makeFakeTab()
|
|
vi.stubGlobal('window', { open: vi.fn(() => fake) })
|
|
const tab = openDeferredTab()
|
|
fake.closed = true
|
|
expect(tab.navigate('https://example.com')).toBe(false)
|
|
})
|
|
|
|
it('close() closes an open tab and is a no-op afterwards', () => {
|
|
const fake = makeFakeTab()
|
|
vi.stubGlobal('window', { open: vi.fn(() => fake) })
|
|
const tab = openDeferredTab()
|
|
tab.close()
|
|
expect(fake.close).toHaveBeenCalledTimes(1)
|
|
fake.closed = true
|
|
tab.close()
|
|
expect(fake.close).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('survives a placeholder document that throws', () => {
|
|
const fake = makeFakeTab()
|
|
fake.document.createElement = vi.fn(() => {
|
|
throw new Error('detached')
|
|
})
|
|
vi.stubGlobal('window', { open: vi.fn(() => fake) })
|
|
const tab = openDeferredTab('Laddar...')
|
|
expect(tab.blocked).toBe(false)
|
|
expect(tab.navigate('https://example.com')).toBe(true)
|
|
})
|
|
})
|