// @vitest-environment jsdom import { flushSync, mount, unmount } from 'svelte' import { afterEach, describe, expect, it, vi } from 'vitest' import { DISCLOSURE } from '$lib/private-mode.js' import JoinPrivate from './JoinPrivate.svelte' // The one rule this card exists to keep: nothing public is written before the disclosure has been // read (ADR §3). It is asserted against a real DOM rather than argued for, because "the paragraph is // above the button" is a claim about a layout and "the button does not work yet" is a claim about // behaviour — and only the second one survives somebody rearranging the card. vi.mock('$lib/owned.svelte.js', () => ({ forgetOwned: () => undefined })) let target: HTMLElement | undefined let component: Record | undefined function render(accept: (text: string) => Promise): { join: HTMLButtonElement ticket: HTMLTextAreaElement ack: HTMLInputElement } { target = document.createElement('div') document.body.appendChild(target) component = mount(JoinPrivate, { target, props: { close: () => undefined, open: () => undefined, accept }, }) as Record flushSync() const buttons = [...target.querySelectorAll('button')] as HTMLButtonElement[] return { join: buttons.find((button) => button.textContent?.trim() === 'Join') as HTMLButtonElement, ticket: target.querySelector('#private-ticket') as HTMLTextAreaElement, ack: target.querySelector('.disclose-ack input') as HTMLInputElement, } } const type = (field: HTMLTextAreaElement, text: string): void => { field.value = text field.dispatchEvent(new Event('input', { bubbles: true })) flushSync() } afterEach(() => { if (component) unmount(component as never) target?.remove() component = undefined target = undefined }) describe('joining a private space', () => { it('will not accept a ticket until the disclosure has been acknowledged', async () => { const accept = vi.fn(async () => { throw new Error('should not have been called') }) const { join, ticket, ack } = render(accept as never) // Every disclosure subject is on screen before anything can be written. for (const point of DISCLOSURE) { expect(target?.textContent).toContain(point.title) } expect(join.disabled).toBe(true) type(ticket, 'radial-ticket-1.abc') // A ticket and no acknowledgement is still no. expect(join.disabled).toBe(true) ack.click() flushSync() expect(join.disabled).toBe(false) expect(accept).not.toHaveBeenCalled() }) it('reports what accepting published, and does not claim it made you a member', async () => { const accept = vi.fn(async () => ({ meta: { spaceUri: 'at://did:plc:founder/com.disnetdev.radial.space/s', founder: 'did:plc:founder', protocol: 1, createdAt: '2026-03-01T00:00:00.000Z' }, device: { did: 'did:plc:member', deviceKeyId: 'browser-abc', publicKey: 'aaaa', kind: 'browser' as const, createdAt: '2026-03-01T00:00:00.000Z' }, fingerprint: 'ticket-fp', bookmark: 'created' as const, warnings: [], })) const { join, ticket, ack } = render(accept as never) type(ticket, 'radial-ticket-1.abc') ack.click() flushSync() join.click() await vi.waitFor(() => { flushSync() expect(target?.textContent).toContain('browser-abc') }) expect(accept).toHaveBeenCalledWith('radial-ticket-1.abc', {}) // The sentence that keeps a bookmark from reading as a grant (§3). expect(target?.textContent).toContain('You hold no membership in it until an admin grants you one') }) })