mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-03 08:46:55 +00:00
@@ -78,6 +78,7 @@
|
||||
'/invite-codes',
|
||||
'/did-document',
|
||||
'/admin',
|
||||
'/about',
|
||||
])
|
||||
|
||||
function getComponent(path: string) {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { _ } from '../../lib/i18n'
|
||||
import { api } from '../../lib/api'
|
||||
import { toast } from '../../lib/toast.svelte'
|
||||
import type { Session, ServerDescription, ServerStats } from '../../lib/types/api'
|
||||
|
||||
interface Props {
|
||||
session: Session
|
||||
}
|
||||
|
||||
let { session }: Props = $props()
|
||||
|
||||
let serverInfo = $state<ServerDescription | null>(null)
|
||||
let serverStats = $state<ServerStats | null>(null)
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
serverInfo = await api.describeServer()
|
||||
} catch {
|
||||
// server info is best-effort — account and environment sections still render
|
||||
}
|
||||
if (session.isAdmin) {
|
||||
try {
|
||||
serverStats = await api.getServerStats(session.accessJwt)
|
||||
} catch {
|
||||
// stats are best-effort
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
})
|
||||
|
||||
const svelteVersion = __SVELTE_VERSION__
|
||||
const svelteI18nVersion = __SVELTE_I18N_VERSION__
|
||||
const viteVersion = __VITE_VERSION__
|
||||
const buildMode = import.meta.env.MODE
|
||||
const userAgent = globalThis.navigator?.userAgent ?? ''
|
||||
const browserLocale = globalThis.navigator?.language ?? ''
|
||||
const screenSize = $derived(
|
||||
`${globalThis.innerWidth ?? 0}x${globalThis.innerHeight ?? 0}`
|
||||
)
|
||||
const serverUrl = globalThis.location?.origin ?? ''
|
||||
|
||||
async function copyDebugInfo() {
|
||||
const lines = [
|
||||
'Tranquil Debug Info',
|
||||
'---',
|
||||
`Server URL: ${serverUrl}`,
|
||||
`PDS Version: ${serverInfo?.version ?? $_('about.unknown')}`,
|
||||
`Server DID: ${serverInfo?.did ?? $_('about.unknown')}`,
|
||||
`Available Domains: ${serverInfo?.availableUserDomains?.join(', ') ?? $_('about.unknown')}`,
|
||||
`Invite Code Required: ${serverInfo?.inviteCodeRequired ? $_('about.yes') : $_('about.no')}`,
|
||||
`Self-Hosted DID:web: ${serverInfo?.selfHostedDidWebEnabled ? $_('about.enabled') : $_('about.disabled')}`,
|
||||
`Contact Email: ${serverInfo?.contact?.email ?? $_('about.notConfigured')}`,
|
||||
`Privacy Policy: ${serverInfo?.links?.privacyPolicy ?? $_('about.notConfigured')}`,
|
||||
`Terms of Service: ${serverInfo?.links?.termsOfService ?? $_('about.notConfigured')}`,
|
||||
...(session.isAdmin ? [
|
||||
`User Count: ${serverStats?.userCount?.toLocaleString() ?? $_('about.unknown')}`,
|
||||
`Available Channels: ${serverInfo?.availableCommsChannels?.join(', ') ?? $_('about.unknown')}`,
|
||||
`Discord Bot: ${serverInfo?.discordBotUsername ?? $_('about.notConfigured')}`,
|
||||
`Discord App ID: ${serverInfo?.discordAppId ?? $_('about.notConfigured')}`,
|
||||
`Telegram Bot: ${serverInfo?.telegramBotUsername ?? $_('about.notConfigured')}`,
|
||||
`Svelte: ${svelteVersion}`,
|
||||
`svelte-i18n: ${svelteI18nVersion}`,
|
||||
`Vite: ${viteVersion}`,
|
||||
`Build Mode: ${buildMode}`,
|
||||
] : []),
|
||||
`DID: ${session.did}`,
|
||||
`Handle: ${session.handle}`,
|
||||
`Account Status: ${session.accountKind}`,
|
||||
`Admin: ${session.isAdmin ? $_('about.yes') : $_('about.no')}`,
|
||||
`User Agent: ${userAgent}`,
|
||||
`Locale: ${browserLocale}`,
|
||||
`Screen: ${screenSize}`,
|
||||
]
|
||||
try {
|
||||
await navigator.clipboard.writeText(lines.join('\n'))
|
||||
toast.success($_('about.copied'))
|
||||
} catch {
|
||||
toast.error($_('about.copyFailed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="about-page">
|
||||
{#if loading}
|
||||
<div class="loading">{$_('common.loading')}</div>
|
||||
{:else}
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.serverSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.serverUrl')}</dt>
|
||||
<dd class="mono">{serverUrl}</dd>
|
||||
<dt>{$_('about.pdsVersion')}</dt>
|
||||
<dd>{serverInfo?.version ?? $_('about.unknown')}</dd>
|
||||
<dt>{$_('about.serverDid')}</dt>
|
||||
<dd class="mono">{serverInfo?.did ?? $_('about.unknown')}</dd>
|
||||
<dt>{$_('about.availableDomains')}</dt>
|
||||
<dd>{serverInfo?.availableUserDomains?.join(', ') ?? $_('about.unknown')}</dd>
|
||||
<dt>{$_('about.inviteCodeRequired')}</dt>
|
||||
<dd>{serverInfo?.inviteCodeRequired ? $_('about.yes') : $_('about.no')}</dd>
|
||||
<dt>{$_('about.selfHostedDidWeb')}</dt>
|
||||
<dd>{serverInfo?.selfHostedDidWebEnabled ? $_('about.enabled') : $_('about.disabled')}</dd>
|
||||
{#if serverStats}
|
||||
<dt>{$_('about.userCount')}</dt>
|
||||
<dd>{serverStats.userCount.toLocaleString()}</dd>
|
||||
{/if}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.contactSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.contactEmail')}</dt>
|
||||
<dd>{serverInfo?.contact?.email ?? $_('about.notConfigured')}</dd>
|
||||
<dt>{$_('about.privacyPolicy')}</dt>
|
||||
<dd>
|
||||
{#if serverInfo?.links?.privacyPolicy}
|
||||
<a href={serverInfo.links.privacyPolicy} target="_blank" rel="noopener noreferrer">{serverInfo.links.privacyPolicy}</a>
|
||||
{:else}
|
||||
{$_('about.notConfigured')}
|
||||
{/if}
|
||||
</dd>
|
||||
<dt>{$_('about.termsOfService')}</dt>
|
||||
<dd>
|
||||
{#if serverInfo?.links?.termsOfService}
|
||||
<a href={serverInfo.links.termsOfService} target="_blank" rel="noopener noreferrer">{serverInfo.links.termsOfService}</a>
|
||||
{:else}
|
||||
{$_('about.notConfigured')}
|
||||
{/if}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if session.isAdmin}
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.communicationSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.availableChannels')}</dt>
|
||||
<dd>{serverInfo?.availableCommsChannels?.join(', ') ?? $_('about.unknown')}</dd>
|
||||
<dt>{$_('about.discordBot')}</dt>
|
||||
<dd>{serverInfo?.discordBotUsername ?? $_('about.notConfigured')}</dd>
|
||||
<dt>{$_('about.discordAppId')}</dt>
|
||||
<dd>{serverInfo?.discordAppId ?? $_('about.notConfigured')}</dd>
|
||||
<dt>{$_('about.telegramBot')}</dt>
|
||||
<dd>{serverInfo?.telegramBotUsername ?? $_('about.notConfigured')}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if session.isAdmin}
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.frontendSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.svelteVersion')}</dt>
|
||||
<dd>{svelteVersion}</dd>
|
||||
<dt>{$_('about.svelteI18nVersion')}</dt>
|
||||
<dd>{svelteI18nVersion}</dd>
|
||||
<dt>{$_('about.viteVersion')}</dt>
|
||||
<dd>{viteVersion}</dd>
|
||||
<dt>{$_('about.buildMode')}</dt>
|
||||
<dd>{buildMode}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.accountSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.did')}</dt>
|
||||
<dd class="mono">{session.did}</dd>
|
||||
<dt>{$_('about.handle')}</dt>
|
||||
<dd>{session.handle}</dd>
|
||||
<dt>{$_('about.accountStatus')}</dt>
|
||||
<dd>{session.accountKind}</dd>
|
||||
<dt>{$_('about.adminStatus')}</dt>
|
||||
<dd>{session.isAdmin ? $_('about.yes') : $_('about.no')}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="about-section">
|
||||
<h3>{$_('about.environmentSection')}</h3>
|
||||
<div class="about-meta">
|
||||
<dl>
|
||||
<dt>{$_('about.userAgent')}</dt>
|
||||
<dd>{userAgent}</dd>
|
||||
<dt>{$_('about.locale')}</dt>
|
||||
<dd>{browserLocale}</dd>
|
||||
<dt>{$_('about.screenSize')}</dt>
|
||||
<dd>{screenSize}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" onclick={copyDebugInfo}>
|
||||
{$_('about.copyDebugInfo')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.about-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.about-section h3 {
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.about-meta dl {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-2) var(--space-4);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about-meta dt {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.about-meta dd {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -189,9 +189,15 @@ export interface ServerLinks {
|
||||
termsOfService?: string;
|
||||
}
|
||||
|
||||
export interface ServerContact {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface ServerDescription {
|
||||
availableUserDomains: string[];
|
||||
inviteCodeRequired: boolean;
|
||||
did: string;
|
||||
contact?: ServerContact;
|
||||
links?: ServerLinks;
|
||||
version?: string;
|
||||
availableCommsChannels?: VerificationChannel[];
|
||||
|
||||
@@ -14,6 +14,7 @@ export const routes = {
|
||||
didDocument: "/did-document",
|
||||
migrate: "/migrate",
|
||||
admin: "/admin",
|
||||
about: "/about",
|
||||
verify: "/verify",
|
||||
resetPassword: "/reset-password",
|
||||
recoverPasskey: "/recover-passkey",
|
||||
|
||||
@@ -145,7 +145,52 @@
|
||||
"navDidDocument": "DID Document",
|
||||
"migrated": "Migrated",
|
||||
"migratedTitle": "Account Migrated",
|
||||
"migratedMessage": "Your account has migrated to {pds}. Your DID document is still hosted here, and you can update it for future migrations."
|
||||
"migratedMessage": "Your account has migrated to {pds}. Your DID document is still hosted here, and you can update it for future migrations.",
|
||||
"navAbout": "About"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"copyDebugInfo": "Copy Debug Info",
|
||||
"copied": "Debug info copied to clipboard",
|
||||
"copyFailed": "Failed to copy debug info",
|
||||
"serverSection": "Server",
|
||||
"frontendSection": "Frontend",
|
||||
"accountSection": "Account",
|
||||
"environmentSection": "Environment",
|
||||
"serverName": "Server Name",
|
||||
"pdsVersion": "PDS Version",
|
||||
"serverUrl": "Server URL",
|
||||
"availableDomains": "Available Domains",
|
||||
"did": "DID",
|
||||
"handle": "Handle",
|
||||
"accountStatus": "Account Status",
|
||||
"adminStatus": "Admin",
|
||||
"userAgent": "User Agent",
|
||||
"locale": "Locale",
|
||||
"screenSize": "Screen Size",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"unknown": "Unknown",
|
||||
"notConfigured": "Not configured",
|
||||
"serverDid": "Server DID",
|
||||
"inviteCodeRequired": "Invite Code Required",
|
||||
"selfHostedDidWeb": "Self-Hosted DID:web",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"contactSection": "Contact & Policies",
|
||||
"contactEmail": "Contact Email",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service",
|
||||
"communicationSection": "Communication",
|
||||
"availableChannels": "Available Channels",
|
||||
"discordBot": "Discord Bot",
|
||||
"discordAppId": "Discord App ID",
|
||||
"telegramBot": "Telegram Bot",
|
||||
"svelteVersion": "Svelte",
|
||||
"svelteI18nVersion": "svelte-i18n",
|
||||
"viteVersion": "Vite",
|
||||
"buildMode": "Build Mode",
|
||||
"userCount": "User Count"
|
||||
},
|
||||
"didEditor": {
|
||||
"preview": "Current DID Document",
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
import InviteCodesContent from '../components/dashboard/InviteCodesContent.svelte'
|
||||
import DidDocumentContent from '../components/dashboard/DidDocumentContent.svelte'
|
||||
import AdminContent from '../components/dashboard/AdminContent.svelte'
|
||||
import AboutContent from '../components/dashboard/AboutContent.svelte'
|
||||
|
||||
type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'invite-codes' | 'did-document' | 'admin'
|
||||
type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'invite-codes' | 'did-document' | 'admin' | 'about'
|
||||
|
||||
const auth = $derived(getAuthState())
|
||||
let dropdownOpen = $state(false)
|
||||
@@ -75,6 +76,7 @@
|
||||
'/invite-codes': 'invite-codes',
|
||||
'/did-document': 'did-document',
|
||||
'/admin': 'admin',
|
||||
'/about': 'about',
|
||||
}
|
||||
return sectionMap[path] ?? null
|
||||
})
|
||||
@@ -150,6 +152,7 @@
|
||||
'invite-codes': '/invite-codes',
|
||||
'did-document': '/did-document',
|
||||
'admin': '/admin',
|
||||
'about': '/about',
|
||||
}
|
||||
|
||||
function selectSection(section: Section) {
|
||||
@@ -181,9 +184,11 @@
|
||||
{ id: 'admin', label: $_('dashboard.navAdmin'), show: session?.isAdmin ?? false, highlight: 'admin' },
|
||||
])
|
||||
|
||||
const aboutItem = { id: 'about' as Section, label: $_('dashboard.navAbout') }
|
||||
const visibleNavItems = $derived(navItems.filter(item => item.show))
|
||||
|
||||
function getSectionTitle(section: Section): string {
|
||||
if (section === 'about') return aboutItem.label
|
||||
const item = navItems.find(i => i.id === section)
|
||||
return item?.label ?? ''
|
||||
}
|
||||
@@ -276,6 +281,18 @@
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="nav-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="nav-item"
|
||||
class:active={currentSection === aboutItem.id}
|
||||
onclick={() => selectSection(aboutItem.id)}
|
||||
>
|
||||
<span class="nav-label">{aboutItem.label}</span>
|
||||
<span class="nav-chevron">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content" class:hidden-mobile={currentSection === null}>
|
||||
@@ -309,6 +326,8 @@
|
||||
<DidDocumentContent {session} />
|
||||
{:else if currentSection === 'admin'}
|
||||
<AdminContent {session} />
|
||||
{:else if currentSection === 'about'}
|
||||
<AboutContent {session} />
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -191,6 +191,11 @@ button.dropdown-item.logout-item {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
padding: var(--space-2);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
|
||||
import AboutContent from "../components/dashboard/AboutContent.svelte";
|
||||
import {
|
||||
clearMocks,
|
||||
jsonResponse,
|
||||
mockData,
|
||||
mockEndpoint,
|
||||
setupAuthenticatedUser,
|
||||
setupFetchMock,
|
||||
setupIndexedDBMock,
|
||||
} from "./mocks.ts";
|
||||
|
||||
describe("AboutContent", () => {
|
||||
let session: ReturnType<typeof setupAuthenticatedUser>;
|
||||
|
||||
beforeEach(() => {
|
||||
clearMocks();
|
||||
setupFetchMock();
|
||||
setupIndexedDBMock();
|
||||
session = setupAuthenticatedUser();
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(
|
||||
mockData.describeServer({ version: "0.4.59" }),
|
||||
),
|
||||
);
|
||||
mockEndpoint("_server.getConfig", () =>
|
||||
jsonResponse({
|
||||
serverName: "Test PDS",
|
||||
primaryColor: null,
|
||||
primaryColorDark: null,
|
||||
secondaryColor: null,
|
||||
secondaryColorDark: null,
|
||||
logoCid: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("displays account information from session", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("did:web:test.tranquil.dev:u:testuser"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("testuser.test.tranquil.dev"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays PDS version from server description", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("0.4.59")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays environment information", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/User Agent/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Locale/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Screen Size/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows section headings", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Contact & Policies")).toBeInTheDocument();
|
||||
expect(screen.getByText("Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("Environment")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows unknown when PDS version is not available", async () => {
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(mockData.describeServer()),
|
||||
);
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Unknown")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("has a copy debug info button", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /copy debug info/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("copies debug info to clipboard on button click", async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
render(AboutContent, { props: { session } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("0.4.59")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", { name: /copy debug info/i }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(writeText).toHaveBeenCalledOnce();
|
||||
const copied = writeText.mock.calls[0][0] as string;
|
||||
expect(copied).toContain("Tranquil Debug Info");
|
||||
expect(copied).toContain("did:web:test.tranquil.dev:u:testuser");
|
||||
expect(copied).toContain("testuser.test.tranquil.dev");
|
||||
expect(copied).toContain("Server DID: did:web:test.tranquil.dev");
|
||||
expect(copied).toContain("Contact Email: admin@test.tranquil.dev");
|
||||
expect(copied).toContain("Privacy Policy: https://example.com/privacy");
|
||||
expect(copied).not.toContain("Discord Bot");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays admin status correctly for admin users", async () => {
|
||||
clearMocks();
|
||||
setupFetchMock();
|
||||
session = setupAuthenticatedUser({ isAdmin: true });
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(mockData.describeServer({ version: "0.4.59" })),
|
||||
);
|
||||
mockEndpoint("_admin.getServerStats", () =>
|
||||
jsonResponse(mockData.serverStats()),
|
||||
);
|
||||
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Yes")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays admin status correctly for non-admin users", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
const noElements = screen.getAllByText("No");
|
||||
expect(noElements.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays server DID", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("did:web:test.tranquil.dev")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays invite code and DID:web status", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
const noElements = screen.getAllByText("No");
|
||||
expect(noElements.length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("Enabled")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays contact email and policy links", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("admin@test.tranquil.dev")).toBeInTheDocument();
|
||||
const privacyLink = screen.getByRole("link", { name: "https://example.com/privacy" });
|
||||
expect(privacyLink).toHaveAttribute("href", "https://example.com/privacy");
|
||||
expect(privacyLink).toHaveAttribute("target", "_blank");
|
||||
const tosLink = screen.getByRole("link", { name: "https://example.com/tos" });
|
||||
expect(tosLink).toHaveAttribute("href", "https://example.com/tos");
|
||||
expect(tosLink).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays communication channels and bot info for admins", async () => {
|
||||
clearMocks();
|
||||
setupFetchMock();
|
||||
const adminSession = setupAuthenticatedUser({ isAdmin: true });
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(mockData.describeServer({ version: "0.4.59" })),
|
||||
);
|
||||
mockEndpoint("_admin.getServerStats", () =>
|
||||
jsonResponse(mockData.serverStats()),
|
||||
);
|
||||
|
||||
render(AboutContent, { props: { session: adminSession } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Communication")).toBeInTheDocument();
|
||||
expect(screen.getByText("email, discord, telegram, signal")).toBeInTheDocument();
|
||||
expect(screen.getByText("test-bot")).toBeInTheDocument();
|
||||
expect(screen.getByText("123456789")).toBeInTheDocument();
|
||||
expect(screen.getByText("test_tg_bot")).toBeInTheDocument();
|
||||
expect(screen.getByText("42")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides admin-only sections for non-admins", async () => {
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Server")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText("Communication")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Frontend")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'Not configured' for missing optional fields", async () => {
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(
|
||||
mockData.describeServer({
|
||||
contact: {},
|
||||
links: {},
|
||||
discordBotUsername: undefined,
|
||||
discordAppId: undefined,
|
||||
telegramBotUsername: undefined,
|
||||
}),
|
||||
),
|
||||
);
|
||||
render(AboutContent, { props: { session } });
|
||||
await waitFor(() => {
|
||||
const notConfigured = screen.getAllByText("Not configured");
|
||||
expect(notConfigured.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -265,14 +265,27 @@ export const mockData = {
|
||||
describeServer: (overrides?: Record<string, unknown>) => ({
|
||||
availableUserDomains: ["test.tranquil.dev"],
|
||||
inviteCodeRequired: false,
|
||||
did: "did:web:test.tranquil.dev",
|
||||
contact: {
|
||||
email: "admin@test.tranquil.dev",
|
||||
},
|
||||
links: {
|
||||
privacyPolicy: "https://example.com/privacy",
|
||||
termsOfService: "https://example.com/tos",
|
||||
},
|
||||
selfHostedDidWebEnabled: true,
|
||||
availableCommsChannels: ["email", "discord", "telegram", "signal"],
|
||||
discordBotUsername: "test-bot",
|
||||
discordAppId: "123456789",
|
||||
telegramBotUsername: "test_tg_bot",
|
||||
...overrides,
|
||||
}),
|
||||
serverStats: () => ({
|
||||
userCount: 42,
|
||||
repoCount: 42,
|
||||
recordCount: 1234,
|
||||
blobStorageBytes: 5678,
|
||||
}),
|
||||
describeRepo: (did: string) => ({
|
||||
handle: "testuser.test.tranquil.dev",
|
||||
did,
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const __SVELTE_VERSION__: string;
|
||||
declare const __SVELTE_I18N_VERSION__: string;
|
||||
declare const __VITE_VERSION__: string;
|
||||
@@ -1,13 +1,23 @@
|
||||
import process from "node:process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
const sveltePkg = JSON.parse(readFileSync("./node_modules/svelte/package.json", "utf-8"));
|
||||
const svelteI18nPkg = JSON.parse(readFileSync("./node_modules/svelte-i18n/package.json", "utf-8"));
|
||||
const vitePkg = JSON.parse(readFileSync("./node_modules/vite/package.json", "utf-8"));
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const target = env.VITE_API_URL || "http://localhost:3000";
|
||||
|
||||
return {
|
||||
plugins: [svelte()],
|
||||
define: {
|
||||
__SVELTE_VERSION__: JSON.stringify(sveltePkg.version),
|
||||
__SVELTE_I18N_VERSION__: JSON.stringify(svelteI18nPkg.version),
|
||||
__VITE_VERSION__: JSON.stringify(vitePkg.version),
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
},
|
||||
|
||||
@@ -6,6 +6,11 @@ export default defineConfig({
|
||||
hot: false,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
__SVELTE_VERSION__: JSON.stringify("0.0.0-test"),
|
||||
__SVELTE_I18N_VERSION__: JSON.stringify("0.0.0-test"),
|
||||
__VITE_VERSION__: JSON.stringify("0.0.0-test"),
|
||||
},
|
||||
resolve: {
|
||||
conditions: ["browser", "development"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user