accounts
Version:
Tempo Accounts SDK
325 lines (268 loc) • 9.83 kB
text/typescript
import { afterEach, describe, expect, test, vi } from 'vp/test'
import * as Dialog from './Dialog.js'
import * as Storage from './Storage.js'
import * as Store from './Store.js'
const host = 'https://wallet-next.tempo.xyz/remote'
function setup() {
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'dialog-test' }),
})
const dialog = Dialog.iframe()
const handle = dialog({ host, store })
lastHandle = handle
return { handle, store }
}
let lastHandle: Dialog.Instance | undefined
afterEach(() => {
lastHandle?.destroy()
lastHandle = undefined
document.querySelectorAll('dialog[data-tempo-wallet]').forEach((el) => el.remove())
document.body.style.overflow = ''
})
describe('Dialog.iframe', () => {
test('default: appends dialog and iframe to document.body', () => {
setup()
const dialog = document.querySelector('dialog[data-tempo-wallet]')
expect(dialog).not.toBeNull()
const iframe = dialog!.querySelector('iframe')
expect(iframe).not.toBeNull()
})
test('behavior: singleton — multiple calls reuse same iframe', () => {
const { handle: a } = setup()
const { handle: b } = setup()
const { handle: c } = setup()
expect(a).toBe(b)
expect(b).toBe(c)
const dialogs = document.querySelectorAll('dialog[data-tempo-wallet]')
expect(dialogs.length).toBe(1)
})
test('behavior: iframe has correct sandbox attributes', () => {
setup()
const iframe = document.querySelector('dialog[data-tempo-wallet] iframe')!
expect(iframe.getAttribute('sandbox')).toMatchInlineSnapshot(
`"allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"`,
)
})
test('behavior: iframe has correct allow attributes', () => {
setup()
const iframe = document.querySelector('dialog[data-tempo-wallet] iframe')!
const allow = iframe.getAttribute('allow')!
expect(allow).toContain('publickey-credentials-get')
expect(allow).toContain('publickey-credentials-create')
})
test('behavior: iframe src points to host', () => {
setup()
const iframe = document.querySelector('dialog[data-tempo-wallet] iframe') as HTMLIFrameElement
expect(iframe.src).toMatchInlineSnapshot(`"https://wallet-next.tempo.xyz/remote"`)
expect(iframe.src).toContain(host)
})
test('behavior: open shows dialog', () => {
const { handle } = setup()
handle.open()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
expect(dialog.open).toBe(true)
})
test('behavior: close hides dialog', () => {
const { handle } = setup()
handle.open()
handle.close()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
expect(dialog.open).toBe(false)
})
test('behavior: destroy removes dialog from DOM', () => {
const { handle } = setup()
handle.destroy()
expect(document.querySelector('dialog[data-tempo-wallet]')).toBeNull()
})
test('behavior: body scroll locked on open', () => {
const { handle } = setup()
handle.open()
expect(document.body.style.overflow).toBe('hidden')
})
test('behavior: body scroll restored on close', () => {
const { handle } = setup()
document.body.style.overflow = 'auto'
handle.open()
handle.close()
expect(document.body.style.overflow).toBe('auto')
})
test('behavior: open is idempotent', () => {
const { handle } = setup()
handle.open()
expect(() => handle.open()).not.toThrow()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
expect(dialog.open).toBe(true)
handle.close()
})
test('behavior: close without open does not throw', () => {
const { handle } = setup()
expect(() => handle.close()).not.toThrow()
})
test('behavior: destroy restores body scroll', () => {
const { handle } = setup()
document.body.style.overflow = 'auto'
handle.open()
handle.destroy()
expect(document.body.style.overflow).toBe('auto')
})
test('behavior: destroy closes open dialog', () => {
const { handle } = setup()
handle.open()
handle.destroy()
expect(document.querySelector('dialog[data-tempo-wallet]')).toBeNull()
})
test('behavior: cancel event rejects pending requests', () => {
const { handle, store } = setup()
handle.open()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
dialog.dispatchEvent(new Event('cancel'))
const queue = store.getState().requestQueue
for (const q of queue) expect(q.status).toBe('error')
})
test('behavior: focus restored to previous element on close', () => {
const button = document.createElement('button')
document.body.appendChild(button)
button.focus()
expect(document.activeElement).toBe(button)
const { handle } = setup()
handle.open()
handle.close()
expect(document.activeElement).toBe(button)
button.remove()
})
test('behavior: backdrop click rejects pending requests', () => {
const { handle, store } = setup()
handle.open()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
dialog.dispatchEvent(new MouseEvent('click', { bubbles: true }))
const queue = store.getState().requestQueue
for (const q of queue) expect(q.status).toBe('error')
})
test('behavior: click inside iframe does not close dialog', () => {
const { handle } = setup()
handle.open()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
const iframe = dialog.querySelector('iframe')!
iframe.dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(dialog.open).toBe(true)
})
test('behavior: 1Password inert attribute stripped from dialog', async () => {
const { handle } = setup()
handle.open()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
dialog.setAttribute('inert', '')
await new Promise((resolve) => setTimeout(resolve, 10))
expect(dialog.hasAttribute('inert')).toBe(false)
handle.close()
})
test('behavior: iframe has accessibility attributes', () => {
setup()
const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement
expect(dialog.getAttribute('role')).toBe('dialog')
expect(dialog.getAttribute('aria-label')).toBe('Tempo Wallet')
})
})
describe('Dialog.popup', () => {
test('default: window.open called with correct URL', () => {
const openSpy = vi.spyOn(window, 'open').mockReturnValue({
closed: false,
close: vi.fn(),
} as unknown as Window)
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'popup-test' }),
})
const dialog = Dialog.popup()
const handle = dialog({ host, store })
handle.open()
expect(openSpy).toHaveBeenCalledOnce()
expect(openSpy.mock.calls[0]![0]).toBe(host)
handle.destroy()
openSpy.mockRestore()
})
test('behavior: window.open called with centered position', () => {
const openSpy = vi.spyOn(window, 'open').mockReturnValue({
closed: false,
close: vi.fn(),
} as unknown as Window)
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'popup-test' }),
})
const dialog = Dialog.popup()
const handle = dialog({ host, store })
handle.open()
const features = openSpy.mock.calls[0]![2] as string
expect(features).toContain('width=')
expect(features).toContain('height=')
expect(features).toContain('left=')
expect(features).toContain('top=')
handle.destroy()
openSpy.mockRestore()
})
test('behavior: close calls popup.close()', () => {
const popupClose = vi.fn()
const openSpy = vi.spyOn(window, 'open').mockReturnValue({
closed: false,
close: popupClose,
} as unknown as Window)
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'popup-test' }),
})
const dialog = Dialog.popup()
const handle = dialog({ host, store })
handle.open()
handle.close()
expect(popupClose).toHaveBeenCalledOnce()
handle.destroy()
openSpy.mockRestore()
})
test('behavior: open throws if popup blocked', () => {
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'popup-test' }),
})
const dialog = Dialog.popup()
const handle = dialog({ host, store })
expect(() => handle.open()).toThrow('Failed to open popup')
openSpy.mockRestore()
})
test('behavior: destroy cleans up', () => {
const popupClose = vi.fn()
const openSpy = vi.spyOn(window, 'open').mockReturnValue({
closed: false,
close: popupClose,
} as unknown as Window)
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'popup-test' }),
})
const dialog = Dialog.popup()
const handle = dialog({ host, store })
handle.open()
handle.destroy()
expect(popupClose).toHaveBeenCalled()
openSpy.mockRestore()
})
})
describe('Dialog.noop', () => {
test('default: open, close, destroy are callable without error', () => {
const store = Store.create({
chainId: 1,
storage: Storage.memory({ key: 'noop-test' }),
})
const dialog = Dialog.noop()
const handle = dialog({ host, store })
expect(() => handle.open()).not.toThrow()
expect(() => handle.close()).not.toThrow()
expect(() => handle.destroy()).not.toThrow()
})
})
describe('isSafari', () => {
test('default: returns false in non-Safari environment', () => {
expect(Dialog.isSafari()).toBe(false)
})
})