feat(release): stage GitWorkshop and next node updates
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NostrTabSigner from '@/views/NostrTabSigner.vue'
|
||||
|
||||
describe('NostrTabSigner visibility', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
window.history.replaceState({}, '', '/nostr-signer')
|
||||
})
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
function parentMessage(data: Record<string, unknown>) {
|
||||
const event = new MessageEvent('message', { data, origin: window.location.origin })
|
||||
Object.defineProperty(event, 'source', { value: window.parent })
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
it('does not reveal the full-screen frame for a silent remembered request', async () => {
|
||||
localStorage.setItem('archipelago_app_identity_archipelago-source', JSON.stringify({
|
||||
id: 'identity-a',
|
||||
name: 'Alice',
|
||||
nostr_pubkey: 'abc123',
|
||||
}))
|
||||
const postMessage = vi.spyOn(window.parent, 'postMessage')
|
||||
const wrapper = shallowMount(NostrTabSigner)
|
||||
expect(document.documentElement.classList.contains('nostr-signer-route')).toBe(true)
|
||||
expect(document.body.classList.contains('nostr-signer-route')).toBe(true)
|
||||
|
||||
parentMessage({
|
||||
type: 'archipelago:signer-init',
|
||||
appId: 'archipelago-source',
|
||||
appName: 'GitWorkshop',
|
||||
})
|
||||
postMessage.mockClear()
|
||||
|
||||
parentMessage({ type: 'nostr-request', id: 1, method: 'getRelays', params: {} })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:signer-show' }),
|
||||
expect.anything(),
|
||||
)
|
||||
|
||||
wrapper.unmount()
|
||||
expect(document.documentElement.classList.contains('nostr-signer-route')).toBe(false)
|
||||
expect(document.body.classList.contains('nostr-signer-route')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import { NEW_TAB_APPS, directAppUrl, resolveAppUrl } from '../appSessionConfig'
|
||||
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
|
||||
import { HOST_FRAME_APPS, NEW_TAB_APPS, directAppUrl, resolveAppUrl } from '../appSessionConfig'
|
||||
import { GENERATED_HOST_FRAME_APPS, GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
|
||||
import { __setSignedCatalogForTests } from '../../discover/curatedApps'
|
||||
|
||||
// Mirror of the live signed catalog's embedded manifests (the ports[] auth
|
||||
@@ -45,6 +45,11 @@ describe('appSessionConfig', () => {
|
||||
expect(GENERATED_NEW_TAB_APPS.has('tailscale')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not force GitWorkshop into a dashboard iframe in Companion', () => {
|
||||
expect(GENERATED_HOST_FRAME_APPS.has('archipelago-source')).toBe(false)
|
||||
expect(HOST_FRAME_APPS.has('archipelago-source')).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves direct app ports against the current browser host', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: '192.0.2.10' },
|
||||
@@ -147,4 +152,17 @@ describe('appSessionConfig', () => {
|
||||
// Cuprate's UI port is auth:none — plain HTTP stays plain.
|
||||
expect(resolveAppUrl('cuprate', undefined, 'http://localhost:18090')).toBe('http://192.0.2.10:18090')
|
||||
})
|
||||
|
||||
it('keeps the pre-catalog Source app on the dashboard origin', () => {
|
||||
stubLocation({ hostname: '192.0.2.10', protocol: 'https:' })
|
||||
|
||||
// Source is intentionally absent from SIGNED until owner UAT passes. It
|
||||
// must follow the already-working dashboard ingress instead of assuming
|
||||
// that the same address also exposes a dedicated high port.
|
||||
expect(resolveAppUrl('archipelago-source')).toBe('/app/archipelago-source/')
|
||||
expect(resolveAppUrl('archipelago-source', undefined, 'http://localhost:8337'))
|
||||
.toBe('/app/archipelago-source/')
|
||||
expect(resolveAppUrl('archipelago-source', '/search'))
|
||||
.toBe('/app/archipelago-source/search')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { consentKey, hasRememberedConsent, rememberConsent } from '../nostrConsent'
|
||||
|
||||
describe('NIP-07 consent storage', () => {
|
||||
beforeEach(() => localStorage.clear())
|
||||
|
||||
it('binds remembered approval to origin, app, identity and method', () => {
|
||||
const key = consentKey('https://node.example', 'archipelago-source', 'identity-a', 'signEvent')
|
||||
rememberConsent(key)
|
||||
|
||||
expect(hasRememberedConsent(key)).toBe(true)
|
||||
expect(hasRememberedConsent(consentKey('https://node.example', 'archipelago-source', 'identity-b', 'signEvent'))).toBe(false)
|
||||
expect(hasRememberedConsent(consentKey('https://node.example', 'archipelago-source', 'identity-a', 'nip44.decrypt'))).toBe(false)
|
||||
expect(hasRememberedConsent(consentKey('https://other-node.example', 'archipelago-source', 'identity-a', 'signEvent'))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,314 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const providerSource = readFileSync(
|
||||
resolve(process.cwd(), 'public/nostr-provider.js'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
type ProviderWindow = Window & {
|
||||
__archipelagoNostr?: boolean
|
||||
ArchipelagoSurface?: {
|
||||
expectPageTransition: () => void
|
||||
}
|
||||
nostr?: { getPublicKey: () => Promise<string> }
|
||||
archipelagoNostr?: {
|
||||
selectIdentity: () => Promise<unknown>
|
||||
getSelectedIdentity: () => { nostr_pubkey: string } | null
|
||||
onIdentitySelected: (
|
||||
callback: (identity: { nostr_pubkey: string }) => void,
|
||||
) => () => void
|
||||
}
|
||||
}
|
||||
|
||||
describe('nostr-provider identity selection', () => {
|
||||
let providerWindow: ProviderWindow
|
||||
|
||||
beforeEach(() => {
|
||||
providerWindow = window as ProviderWindow
|
||||
delete providerWindow.__archipelagoNostr
|
||||
delete providerWindow.nostr
|
||||
delete providerWindow.archipelagoNostr
|
||||
delete providerWindow.ArchipelagoSurface
|
||||
document.documentElement.innerHTML = '<head><title>IndeedHub</title></head><body></body>'
|
||||
window.history.replaceState({}, '', '/app/indeedhub/')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
Reflect.deleteProperty(document, 'readyState')
|
||||
delete providerWindow.__archipelagoNostr
|
||||
delete providerWindow.nostr
|
||||
delete providerWindow.archipelagoNostr
|
||||
delete providerWindow.ArchipelagoSurface
|
||||
})
|
||||
|
||||
function loadProvider(userActivated: boolean) {
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
configurable: true,
|
||||
value: { isActive: userActivated },
|
||||
})
|
||||
window.eval(providerSource)
|
||||
window.dispatchEvent(new Event('load'))
|
||||
const frame = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
const postMessage = vi.spyOn(frame.contentWindow!, 'postMessage')
|
||||
const signerOriginUrl = new URL(window.location.href)
|
||||
signerOriginUrl.port = ''
|
||||
const signerOrigin = signerOriginUrl.origin
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
postMessage.mockClear()
|
||||
return { frame, postMessage, signerOrigin }
|
||||
}
|
||||
|
||||
it('reopens the chooser for a user-triggered NIP-07 login', async () => {
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(true)
|
||||
const publicKey = providerWindow.nostr!.getPublicKey()
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:signer-select-identity', force: true }),
|
||||
signerOrigin,
|
||||
)
|
||||
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'abc123' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
await Promise.resolve()
|
||||
|
||||
await expect(publicKey).resolves.toBe('abc123')
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the remembered identity for background account restoration', () => {
|
||||
const { postMessage, signerOrigin } = loadProvider(false)
|
||||
void providerWindow.nostr!.getPublicKey()
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:signer-select-identity' }),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an eager picker choice for the app login that follows', async () => {
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'fast-choice' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
postMessage.mockClear()
|
||||
|
||||
await expect(providerWindow.nostr!.getPublicKey()).resolves.toBe('fast-choice')
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('parks the hidden broker off-screen and reuses it for the next request', async () => {
|
||||
const surface = {
|
||||
expectPageTransition: vi.fn(),
|
||||
}
|
||||
providerWindow.ArchipelagoSurface = surface
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const show = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-show' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(show, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(show)
|
||||
expect(frame.style.display).toBe('block')
|
||||
|
||||
const hide = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-hide' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(hide, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(hide)
|
||||
|
||||
expect(frame.style.display).toBe('block')
|
||||
expect(frame.style.width).toBe('1px')
|
||||
expect(frame.style.height).toBe('1px')
|
||||
expect(frame.style.opacity).toBe('0')
|
||||
expect(frame.style.pointerEvents).toBe('none')
|
||||
expect(frame.style.transform).toContain('-10000px')
|
||||
expect(document.querySelector('#archipelago-nostr-signer')).toBe(frame)
|
||||
|
||||
const publicKey = providerWindow.nostr!.getPublicKey()
|
||||
const replacement = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
expect(replacement).toBe(frame)
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: replacement.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
|
||||
const request = postMessage.mock.calls
|
||||
.map(call => call[0] as { type: string; id?: number })
|
||||
.find(message => message.type === 'nostr-request')!
|
||||
expect(request).toBeDefined()
|
||||
const response = new MessageEvent('message', {
|
||||
data: { type: 'nostr-response', id: request.id, result: 'recreated-key' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(response, 'source', { value: replacement.contentWindow })
|
||||
window.dispatchEvent(response)
|
||||
await expect(publicKey).resolves.toBe('recreated-key')
|
||||
})
|
||||
|
||||
it('delivers an eager identity to an app listener that mounts afterward', () => {
|
||||
const { frame, signerOrigin } = loadProvider(false)
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'late-listener' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = providerWindow.archipelagoNostr!.onIdentitySelected(listener)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({ nostr_pubkey: 'late-listener' })
|
||||
expect(providerWindow.archipelagoNostr!.getSelectedIdentity())
|
||||
.toEqual({ nostr_pubkey: 'late-listener' })
|
||||
|
||||
unsubscribe()
|
||||
const changed = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'after-unsubscribe' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(changed, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(changed)
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('turns an automatic IndeeHub identity into a NIP-98 signing request', async () => {
|
||||
vi.useFakeTimers()
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const { postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const identity = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:identity', nostr_pubkey: 'indeedhub-key' },
|
||||
origin: window.location.origin,
|
||||
})
|
||||
Object.defineProperty(identity, 'source', { value: window })
|
||||
window.dispatchEvent(identity)
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/api/nostr-auth/health`,
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
)
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'nostr-request',
|
||||
method: 'signEvent',
|
||||
params: { event: expect.objectContaining({ kind: 27235, pubkey: 'indeedhub-key' }) },
|
||||
}),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('waits for the signer success surface to hide before reloading after NIP-98', async () => {
|
||||
vi.useFakeTimers()
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ accessToken: 'real-token', refreshToken: 'refresh' }),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const raf = vi.spyOn(window, 'requestAnimationFrame').mockImplementation(() => 1)
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const identity = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:identity', nostr_pubkey: 'indeedhub-key' },
|
||||
origin: window.location.origin,
|
||||
})
|
||||
Object.defineProperty(identity, 'source', { value: window })
|
||||
window.dispatchEvent(identity)
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
|
||||
const signRequest = postMessage.mock.calls
|
||||
.map(call => call[0] as { type: string; id?: number })
|
||||
.find(message => message.type === 'nostr-request' && message.id != null)!
|
||||
const show = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-show' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(show, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(show)
|
||||
|
||||
const signed = new MessageEvent('message', {
|
||||
data: { type: 'nostr-response', id: signRequest.id, result: { id: 'signed-event' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(signed, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(signed)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(sessionStorage.getItem('nostr_token')).toBe('real-token')
|
||||
expect(raf).not.toHaveBeenCalled()
|
||||
|
||||
const hide = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-hide' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(hide, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(hide)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(raf).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('queues a request until signer-init when the signer iframe wins the load race', () => {
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
configurable: true,
|
||||
value: { isActive: false },
|
||||
})
|
||||
Object.defineProperty(document, 'readyState', {
|
||||
configurable: true,
|
||||
value: 'loading',
|
||||
})
|
||||
window.eval(providerSource)
|
||||
const frame = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
const postMessage = vi.spyOn(frame.contentWindow!, 'postMessage')
|
||||
const signerOriginUrl = new URL(window.location.href)
|
||||
signerOriginUrl.port = ''
|
||||
const signerOrigin = signerOriginUrl.origin
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
|
||||
void providerWindow.nostr!.getPublicKey()
|
||||
expect(postMessage).not.toHaveBeenCalled()
|
||||
|
||||
window.dispatchEvent(new Event('load'))
|
||||
expect(postMessage.mock.calls.map(call => (call[0] as { type: string }).type))
|
||||
.toEqual(['archipelago:signer-init', 'nostr-request'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useAppIdentity, type SelectedIdentity } from '../useAppIdentity'
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
||||
|
||||
const alice: SelectedIdentity = {
|
||||
id: 'alice-id',
|
||||
name: 'Alice',
|
||||
did: 'did:key:alice',
|
||||
pubkey: 'identity-key',
|
||||
nostr_pubkey: 'nostr-key',
|
||||
}
|
||||
|
||||
describe('useAppIdentity explicit identity selection', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.mocked(rpcClient.call).mockResolvedValue({ signature: 'proof' })
|
||||
})
|
||||
|
||||
it('reuses a stored identity normally but reopens the picker for login', async () => {
|
||||
localStorage.setItem('archipelago_app_identity_archipelago-source', JSON.stringify(alice))
|
||||
const postMessage = vi.fn()
|
||||
const frame = ref({ contentWindow: { postMessage } } as unknown as HTMLIFrameElement)
|
||||
const showPicker = ref(false)
|
||||
const identity = useAppIdentity(ref('archipelago-source'), frame, showPicker)
|
||||
|
||||
identity.handleIdentityRequest()
|
||||
await vi.waitFor(() => expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:identity', nostr_pubkey: 'nostr-key' }),
|
||||
'*',
|
||||
))
|
||||
|
||||
postMessage.mockClear()
|
||||
identity.handleIdentityRequest(true)
|
||||
expect(showPicker.value).toBe(true)
|
||||
expect(postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('notifies the requesting app when the chooser is cancelled', () => {
|
||||
const postMessage = vi.fn()
|
||||
const frame = ref({ contentWindow: { postMessage } } as unknown as HTMLIFrameElement)
|
||||
const showPicker = ref(true)
|
||||
const identity = useAppIdentity(ref('archipelago-source'), frame, showPicker)
|
||||
|
||||
identity.cancelIdentitySelection()
|
||||
|
||||
expect(showPicker.value).toBe(false)
|
||||
expect(postMessage).toHaveBeenCalledWith({ type: 'archipelago:identity-cancelled' }, '*')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useNostrBridge } from '../useNostrBridge'
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
||||
|
||||
describe('useNostrBridge consent presentation', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.useFakeTimers()
|
||||
vi.mocked(rpcClient.call).mockResolvedValue({ id: 'signed-event' })
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('keeps the contained identity loader visible through signing and completion', async () => {
|
||||
const source = { postMessage: vi.fn() } as unknown as Window
|
||||
const bridge = useNostrBridge(
|
||||
() => ({ id: 'identity-a', name: 'Alice', nostr_pubkey: 'pubkey-a' } as never),
|
||||
{
|
||||
appId: () => 'archipelago-source', appName: () => 'GitWorkshop',
|
||||
appUrl: () => 'https://node.test/app/archipelago-source/', frameWindow: () => source,
|
||||
},
|
||||
)
|
||||
const event = {
|
||||
data: { type: 'nostr-request', id: 'request-1', method: 'signEvent', params: { event: { kind: 1621, content: 'Fix it' } } },
|
||||
source, origin: 'https://node.test',
|
||||
} as MessageEvent
|
||||
|
||||
const handling = bridge.handleNostrRequest(event)
|
||||
await Promise.resolve()
|
||||
expect(bridge.showConsent.value).toBe(true)
|
||||
expect(bridge.consentPhase.value).toBe('review')
|
||||
|
||||
bridge.approveConsent(false)
|
||||
expect(bridge.consentPhase.value).toBe('signing')
|
||||
expect(bridge.showConsent.value).toBe(true)
|
||||
await handling
|
||||
expect(source.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'nostr-response', id: 'request-1' }), 'https://node.test')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
expect(bridge.consentPhase.value).toBe('success')
|
||||
await vi.advanceTimersByTimeAsync(325)
|
||||
expect(bridge.showConsent.value).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user