import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { chromium, type Browser } from 'playwright'; import type { ReadingView } from '../src/model.ts'; import { renderHtml, renderText } from '../src/render.ts'; function profileView(description: string | undefined): ReadingView { return { id: 'v-profile', snapshot: { kind: 'profile', requested: 'reader.example', fetchedAt: '2026-09-05T12:00:00.000Z', source: { mode: 'public', service: 'https://public.api.bsky.app/?source=profile&mode=exact', }, author: { did: 'did:plc:profilefixture', handle: 'reader.example', displayName: 'Profile Reader', }, description, counts: {}, labels: [], }, refs: { 'v-profile-a1': { kind: 'profile', value: 'did:plc:profilefixture', label: 'Profile Reader', }, }, }; } let browser: Browser; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser?.close(); }); describe('profile rendering', () => { test('keeps missing optional profile data distinct from explicit empty data without empty portrait furniture', () => { const missingView = profileView(undefined); const emptyView = profileView(''); if (emptyView.snapshot.kind !== 'profile') throw new Error('profile fixture mismatch'); emptyView.snapshot.author.avatar = ''; emptyView.snapshot.author.pronouns = ''; emptyView.snapshot.banner = ''; emptyView.snapshot.website = ''; emptyView.snapshot.warnings = []; emptyView.snapshot.enrichments = []; const missingText = renderText(missingView); const missingHtml = renderHtml(missingView); expect(missingText).toContain('Avatar: '); expect(missingText).toContain('Native profile pronouns: '); expect(missingText).toContain('Description: '); expect(missingText).toContain('Banner: '); expect(missingText).toContain('Website: '); expect(missingText).toContain('Warnings: '); expect(missingText).toContain('Enrichments: '); expect(missingHtml).not.toContain('class="profile-about"'); expect(missingHtml).not.toContain('class="profile-banner'); expect(missingHtml).not.toContain('class="profile-pronouns"'); expect(missingHtml).not.toContain('class="profile-website"'); expect(missingHtml).not.toContain('class="profile-enrichments"'); expect(missingHtml).not.toContain('class="profile-recent-posts"'); expect(missingText).toContain(`Recent author posts ref: [no stored ref] · author-feed did:plc:profilefixture`); expect(missingHtml).toContain('
Description field
not supplied
'); expect(missingHtml).toContain('
Followers count
not supplied
'); expect(missingHtml).toContain('
Following count
not supplied
'); expect(missingHtml).toContain('
Posts count
not supplied
'); const zeroCountView = profileView(undefined); if (zeroCountView.snapshot.kind !== 'profile') throw new Error('profile fixture mismatch'); zeroCountView.snapshot.counts.followers = 0; zeroCountView.snapshot.author.displayName = ' '; const zeroCountHtml = renderHtml(zeroCountView); expect(zeroCountHtml).toContain('

Meet @reader.example

'); expect(zeroCountHtml).toContain('
Followers count
0
'); expect(zeroCountHtml).toContain('
Following count
not supplied
'); expect(zeroCountHtml).toContain('
Posts count
not supplied
'); const emptyText = renderText(emptyView); const emptyHtml = renderHtml(emptyView); expect(emptyText).toContain('Avatar: '); expect(emptyText).toContain('Native profile pronouns: '); expect(emptyText).toContain('Description: '); expect(emptyText).toContain('Banner: '); expect(emptyText).toContain('Website: '); expect(emptyText).toContain('Warnings: '); expect(emptyText).toContain('Enrichments: '); expect(emptyHtml).not.toContain('class="profile-about"'); expect(emptyHtml).toContain('class="profile-banner-failure" data-source-presence="empty"'); expect(emptyHtml).not.toContain('class="profile-pronouns"'); expect(emptyHtml).not.toContain('class="profile-website"'); expect(emptyHtml).not.toContain('class="profile-enrichments"'); expect(emptyHtml).toContain('
Description field
present and empty
'); }); test('preserves exact nonempty description and source data without treating prose as markup', () => { const description = 'First & "quoted" line.\nSecond line: 日本語 🪶'; const view = profileView(description); const original = JSON.stringify(view); const text = renderText(view); const html = renderHtml(view); expect(text).toContain(`Description: ${description}`); expect(text).toContain('Source service: https://public.api.bsky.app/?source=profile&mode=exact'); expect(html).toContain('First <field> & "quoted" line.\nSecond line: 日本語 🪶'); expect(html).toContain('https://public.api.bsky.app/?source=profile&mode=exact'); expect(html).not.toContain(''); expect(JSON.stringify(view)).toBe(original); }); test('renders navigable identity, qualified labels, and sourced enrichment without executing untrusted content', async () => { const authorDid = 'did:plc:profilefixture'; const hostile = ''; const view: ReadingView = { id: 'v-profile-rich', snapshot: { kind: 'profile', requested: 'reader.example', fetchedAt: '2026-09-05T12:00:00.000Z', source: { mode: 'authenticated', service: 'https://appview.example/?surface=', viewerDid: 'did:plc:viewer', }, author: { did: authorDid, handle: 'reader.example', displayName: 'Profile Reader', avatar: `https://cdn.example/avatar.png?caption=${hostile}`, pronouns: 'she/her', following: true, followedBy: false, }, description: `A person, not markup. ${hostile}`, banner: `https://cdn.example/banner.png?caption=${hostile}`, website: `https://reader.example/?about=${hostile}`, createdAt: '2025-01-02T03:04:05.000Z', indexedAt: '2026-09-05T11:59:59.000Z', associated: { lists: 2 }, verification: { trustedVerifierStatus: 'valid' }, pinnedPost: { uri: 'at://did:plc:profilefixture/app.bsky.feed.post/pinned', cid: 'bafypinned' }, counts: { followers: 12, follows: 34, posts: 56 }, labels: [ `legacy ${hostile}`, { value: 'same-label', issuerDid: authorDid, uri: 'at://did:plc:profilefixture/app.bsky.actor.profile/self', cid: '', createdAt: '', expiresAt: '', signature: '', }, { value: 'same-label', issuerDid: 'did:plc:independentlabeler', uri: 'at://did:plc:profilefixture/app.bsky.actor.profile/self', negated: true, expiresAt: '2027-01-02T03:04:05.000Z', }, ], warnings: [`Warning retained literally: ${hostile}`], enrichments: [ { id: 'tangled-profile', title: 'Tangled profile', status: 'available', items: [ { label: 'Tangled pronouns', value: `they/them ${hostile}`, uri: 'at://did:plc:profilefixture/sh.tangled.actor.profile/self' }, { label: 'Tangled description', value: 'Distinct public-PDS biography.' }, { label: 'Present-empty value', value: '', uri: '' }, { label: 'Present-blank value', value: ' ' }, ], source: { service: 'https://public-pds.example/?source=', actorDid: authorDid, fetchedAt: '2026-09-05T11:58:00.000Z', uri: 'at://did:plc:profilefixture/sh.tangled.actor.profile/self', cid: '', }, note: `Exact provider note: ${hostile}`, }, { id: 'known-collections', title: 'Known public repository collections', status: 'absent', items: [], source: { service: 'https://public-pds.example', actorDid: authorDid, fetchedAt: '2026-09-05T11:58:00.000Z', }, note: 'No additional allowlisted collection matched.', }, { id: 'greengale-publication', title: 'GreenGale publication', status: 'absent', items: [], source: { service: 'https://public-pds.example', actorDid: authorDid, fetchedAt: '2026-09-05T11:58:00.000Z', uri: '', cid: '', }, note: '', }, { id: 'secondary-service', title: 'Secondary service', status: 'unavailable', items: [], source: { service: 'https://other-public-pds.example', actorDid: authorDid, fetchedAt: '2026-09-05T11:57:00.000Z', }, note: `Provider timeout: ${hostile}`, }, ], }, refs: { 'v-profile-a1': { kind: 'profile', value: authorDid, label: 'Profile Reader' }, 'v-profile-r1': { kind: 'author-feed', value: authorDid, label: 'Recent posts by Profile Reader' }, }, }; const original = JSON.stringify(view); const text = renderText(view); const html = renderHtml(view); expect(text).toContain('Enrichment 1 item 3 URI: '); expect(text).toContain('Enrichment 1 item 4 URI: '); const page = await browser.newPage({ viewport: { width: 1280, height: 960 } }); try { await page.setContent(html); expect(await page.locator('script, iframe, [onerror], [onload]').count()).toBe(0); expect(await page.locator('a').count()).toBe(0); expect(await page.evaluate('globalThis.profileInjected')).toBeUndefined(); expect(await page.locator('.profile-pronouns strong').innerText()).toBe('she/her'); expect(await page.locator('.profile-relationships').innerText()).toBe('Following'); expect(await page.locator('.profile-record-details').textContent()).toMatch(/Author follows viewer\s*false/iu); expect(await page.locator('.profile-about').innerText()).toContain(hostile); expect(await page.locator('.profile-website-value').innerText()).toContain(hostile); const navigation = page.locator('.profile-recent-posts'); expect(await navigation.innerText()).toContain('Recent posts'); expect(await navigation.innerText()).toContain('[v-profile-r1]'); expect(await navigation.innerText()).not.toContain(authorDid); expect(text).toContain(`Recent author posts ref: [v-profile-r1] · author-feed ${authorDid}`); const references = await page.locator('.reference').allInnerTexts(); expect(references).toEqual(['[v-profile-a1]', '[v-profile-r1]']); expect(references.every((reference) => reference === reference.toLowerCase())).toBe(true); const referenceStyle = await page.locator('.reference').first().evaluate((element) => { const style = getComputedStyle(element); return { color: style.color, backgroundColor: style.backgroundColor, textTransform: style.textTransform }; }); expect(referenceStyle.color).not.toBe(referenceStyle.backgroundColor); expect(referenceStyle.textTransform).toBe('none'); const historicalLabel = page.locator('[data-label-format="historical"]'); expect((await historicalLabel.innerText()).toLowerCase()).toContain('source unknown'); expect((await historicalLabel.innerText()).toLowerCase()).not.toContain('self-issued'); expect(await historicalLabel.locator('details').getAttribute('open')).toBeNull(); expect(await historicalLabel.locator('details').textContent()).toContain('no recorded issuer, target, polarity, or expiry'); const selfIssued = page.locator('[data-profile-label="2"]'); const thirdParty = page.locator('[data-profile-label="3"]'); expect((await selfIssued.locator('.badge.self-issued').innerText()).toLowerCase()).toBe('self-issued'); expect(await thirdParty.locator('.badge.self-issued').count()).toBe(0); expect((await thirdParty.locator('.badge.third-party').innerText()).toLowerCase()).toContain('did:plc:independentlabeler'); expect((await thirdParty.locator('.badge.negated').innerText()).toLowerCase()).toBe('negation · retracted'); expect(await thirdParty.locator('.profile-label-expiry').innerText()).toContain('2027-01-02T03:04:05.000Z'); expect(await selfIssued.innerText()).not.toContain('Target URI'); expect(await selfIssued.locator('details').textContent()).toContain('present and empty'); const activeEnrichments = page.locator('.enrichment-card'); expect(await activeEnrichments.count()).toBe(2); const tangled = page.locator('[data-enrichment-id="tangled-profile"]'); expect(await tangled.locator('.enrichment-items > div').count()).toBe(2); expect(await tangled.locator('.enrichment-items').innerText()).toContain('they/them'); expect(await tangled.locator('.enrichment-omission').innerText()).toContain('2 additional exact values'); expect(await tangled.locator('details').getAttribute('open')).toBeNull(); expect(await tangled.locator('details').textContent()).toContain('Exact provider note'); expect(await tangled.locator('details').textContent()).toContain('present and empty'); const unavailable = page.locator('[data-enrichment-id="secondary-service"]'); expect(await unavailable.getAttribute('data-enrichment-status')).toBe('unavailable'); expect(await unavailable.innerText()).toContain('Provider status: unavailable.'); const absentSources = page.locator('.enrichment-absent-sources'); expect(await absentSources.locator(':scope > summary').innerText()).toBe('Other checked sources (2 absent)'); expect(await absentSources.getAttribute('open')).toBeNull(); expect(await absentSources.locator(':scope > ol > li').count()).toBe(2); expect(await absentSources.innerText()).not.toContain('No additional allowlisted collection matched.'); expect(await absentSources.textContent()).toContain('No additional allowlisted collection matched.'); expect(await absentSources.textContent()).toContain('present and empty'); expect(await page.locator('body').innerText()).not.toContain('No record found at this public source'); expect(await page.locator('details[open]').count()).toBe(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true); } finally { await page.close(); } expect(JSON.stringify(view)).toBe(original); }); });