forked from coracle/flotilla
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91145c38fb |
@@ -15,3 +15,4 @@ android/capacitor-cordova-android-plugins
|
||||
android/app/src/androidTest
|
||||
android/app/src/test
|
||||
node_modules
|
||||
.svelte-kit
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Generated assets
|
||||
static/favicon.ico
|
||||
static/pwa-64x64.png
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {expect, test} from "@playwright/test"
|
||||
|
||||
test("boots the SPA on the home page", async ({page}) => {
|
||||
const response = await page.goto("/")
|
||||
|
||||
expect(response?.ok()).toBeTruthy()
|
||||
|
||||
// adapter-static serves an empty shell that hydrates client-side, so the presence of
|
||||
// rendered text proves the Svelte app actually mounted (not just that a file was served).
|
||||
// TODO: tighten this to assert concrete onboarding UI once the markup is settled.
|
||||
await expect(page.locator("body")).toContainText(/\S/, {timeout: 15_000})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
import type {RelayMockConfig} from "../../src/lib/test/relayMocks"
|
||||
|
||||
import relay1Events from "./fixtures/relay1.json"
|
||||
|
||||
// Fake relay urls used by tests. Each maps to a json fixture under ./fixtures/ and an entry in
|
||||
// EVENTS_BY_RELAY below. To add a relay: drop a `<name>.json` file in ./fixtures/, import it, add a
|
||||
// url here, and wire it into EVENTS_BY_RELAY.
|
||||
export const FIXTURE_RELAYS = {
|
||||
relay1: "wss://relay1.test/",
|
||||
} as const
|
||||
|
||||
// The events each fake relay serves. The json files hold static, pre-signed events: schnorr
|
||||
// signatures are non-deterministic, so events are signed once and committed verbatim (they pass
|
||||
// verifyEvent, which netContext.isEventValid enforces). Regenerate with @welshman/signer:
|
||||
// await Nip01Signer.fromSecret(secret).sign(makeEvent(kind, {content, created_at}))
|
||||
const EVENTS_BY_RELAY: Record<string, SignedEvent[]> = {
|
||||
[FIXTURE_RELAYS.relay1]: relay1Events as SignedEvent[],
|
||||
}
|
||||
|
||||
// Build a RelayMockConfig populating the given fixture relays (all of them when none are passed).
|
||||
// Any relay not included returns nothing, keeping tests offline.
|
||||
export const relayFixtures = (...urls: string[]): RelayMockConfig => {
|
||||
const selected = urls.length > 0 ? urls : Object.keys(EVENTS_BY_RELAY)
|
||||
|
||||
return {
|
||||
relays: Object.fromEntries(selected.map(url => [url, EVENTS_BY_RELAY[url] ?? []])),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"kind": 0,
|
||||
"content": "{\"name\":\"Alice\"}",
|
||||
"tags": [],
|
||||
"created_at": 1700000000,
|
||||
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"id": "9b3d138641b38364945b20d800268006c2cb7d974bb4b1d63a9f90f5ab974b90",
|
||||
"sig": "de6b86274e7bcf6c02aa881ada1feee9e01ba320691711d4975916b3cd231ab43cf469a47c5db99503ed72707d5db85fede1ad3763c4fbd7c998d04f00eda6bc"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "hello from the fixture relay",
|
||||
"tags": [],
|
||||
"created_at": 1700000000,
|
||||
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"id": "b9874875bfa8d830c5c9ef3673104360cf21b94848a311febfaf52f0a652b1a9",
|
||||
"sig": "85df94a2e9884ac3d280145492d5191cde2948d49a824c443a1f5d2143633eff1e1789fa7e8843b6efc3dd2dc0d7e33322edb628125d8e35de8ddca1d06ca970"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "reply from bob",
|
||||
"tags": [],
|
||||
"created_at": 1700000001,
|
||||
"pubkey": "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
|
||||
"id": "171dcbdd63d474ba46da609e8b0104cbcf4801fbb581b6c343d9426280f9e1be",
|
||||
"sig": "eecf26e616a6b70dc67c7eae16fc4fe159314647ba1d4581332257de7f070410aa9a9e41f571b9403ff145d16c9b32766846fce08516201263e25cf08c1ed8f1"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
import type {Page} from "@playwright/test"
|
||||
import type {RelayMockConfig} from "../../src/lib/test/relayMocks"
|
||||
|
||||
// Must match RELAY_MOCKS_KEY in src/lib/test/relayMocks.ts.
|
||||
const RELAY_MOCKS_KEY = "__RELAY_MOCKS__"
|
||||
|
||||
// Hard safety net: intercept every real websocket so a test can never reach the network, even if
|
||||
// some code path opens a socket directly (e.g. relay AUTH) rather than going through the adapter
|
||||
// layer. We never call route.connectToServer(), so the socket connects to Playwright's in-process
|
||||
// mock and simply receives nothing.
|
||||
export const blockWebsockets = (page: Page) => page.routeWebSocket(/^wss?:\/\//, () => {})
|
||||
|
||||
// Inject the relay-mock config the app reads on startup. addInitScript runs before any page script
|
||||
// on every navigation, so this must be called before page.goto().
|
||||
export const injectRelayConfig = (page: Page, config: RelayMockConfig) =>
|
||||
page.addInitScript(
|
||||
([key, value]) => {
|
||||
Object.assign(window, {[key]: value})
|
||||
},
|
||||
[RELAY_MOCKS_KEY, config] as const,
|
||||
)
|
||||
|
||||
// Full network isolation plus optional fixtures, in one call. With no config, every relay returns
|
||||
// nothing (requirement 1). Pass {relays: {url: events}} to populate specific relays (requirement 2).
|
||||
export const setupRelayMocks = async (page: Page, config: RelayMockConfig = {}) => {
|
||||
await blockWebsockets(page)
|
||||
await injectRelayConfig(page, config)
|
||||
}
|
||||
|
||||
export type {RelayMockConfig}
|
||||
@@ -11,6 +11,8 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check src && eslint src",
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
||||
"format:all": "prettier --write src",
|
||||
"prepare": "husky"
|
||||
@@ -18,6 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@sveltejs/kit": "^2.61.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {defineConfig, devices} from "@playwright/test"
|
||||
|
||||
// E2E tests live in ./e2e and run against the dev server (port 1847 from vite.config.ts).
|
||||
// Run with `pnpm test:e2e` (after `pnpm exec playwright install` to fetch browsers).
|
||||
export default defineConfig({
|
||||
testDir: "e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: "html",
|
||||
use: {
|
||||
baseURL: "http://localhost:1847",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
// Boots the SvelteKit dev server before the suite and reuses one if already running locally.
|
||||
webServer: {
|
||||
command: "pnpm dev",
|
||||
url: "http://localhost:1847",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
projects: [
|
||||
{name: "chromium", use: {...devices["Desktop Chrome"]}},
|
||||
{name: "firefox", use: {...devices["Desktop Firefox"]}},
|
||||
{name: "webkit", use: {...devices["Desktop Safari"]}},
|
||||
],
|
||||
})
|
||||
Generated
+38
@@ -189,6 +189,9 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.4
|
||||
'@playwright/test':
|
||||
specifier: ^1.49.1
|
||||
version: 1.60.0
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.61.1
|
||||
version: 2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.9(@typescript-eslint/types@8.60.0))(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)))(svelte@5.55.9(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))
|
||||
@@ -1465,6 +1468,11 @@ packages:
|
||||
resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@playwright/test@1.60.0':
|
||||
resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -3013,6 +3021,11 @@ packages:
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -3975,6 +3988,16 @@ packages:
|
||||
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
playwright-core@1.60.0:
|
||||
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.60.0:
|
||||
resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
plist@3.1.1:
|
||||
resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==}
|
||||
engines: {node: '>=10.4.0'}
|
||||
@@ -6549,6 +6572,10 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
tsyringe: 4.10.0
|
||||
|
||||
'@playwright/test@1.60.0':
|
||||
dependencies:
|
||||
playwright: 1.60.0
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@pomade/core@0.2.5(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))':
|
||||
@@ -8240,6 +8267,9 @@ snapshots:
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -9129,6 +9159,14 @@ snapshots:
|
||||
|
||||
pify@3.0.0: {}
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
||||
playwright@1.60.0:
|
||||
dependencies:
|
||||
playwright-core: 1.60.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
plist@3.1.1:
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.9.10
|
||||
|
||||
@@ -92,27 +92,6 @@ const syncParticipantMedia = (participant: Participant) => {
|
||||
})
|
||||
}
|
||||
|
||||
// LiveKit does not emit ParticipantConnected/Disconnected during reconnect.
|
||||
const resyncAfterReconnect = (room: LiveKitRoom) => {
|
||||
const next = new Map<string, {muted: boolean; cameraOn: boolean}>()
|
||||
for (const p of [room.localParticipant, ...room.remoteParticipants.values()]) {
|
||||
next.set(p.identity, {muted: !p.isMicrophoneEnabled, cameraOn: p.isCameraEnabled})
|
||||
}
|
||||
participantMediaState.set(next)
|
||||
|
||||
const session = get(currentVoiceSession)
|
||||
if (session?.room !== room) return
|
||||
|
||||
const {localParticipant} = room
|
||||
voiceMicMuted.set(!localParticipant.isMicrophoneEnabled)
|
||||
currentVoiceSession.set({
|
||||
...session,
|
||||
cameraOn: localParticipant.isCameraEnabled,
|
||||
screenShareOn: localParticipant.isScreenShareEnabled,
|
||||
})
|
||||
triggerVideoFeedCount()
|
||||
}
|
||||
|
||||
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
|
||||
syncParticipantMedia(participant)
|
||||
}
|
||||
@@ -212,11 +191,6 @@ const setUpMicrophone = async (
|
||||
// (after switching calls or an engine reconnect give-up) must not clobber it.
|
||||
let activeRoom: LiveKitRoom | undefined
|
||||
|
||||
const makeOnRoomReconnected = (room: LiveKitRoom) => () => {
|
||||
if (room !== activeRoom) return
|
||||
resyncAfterReconnect(room)
|
||||
}
|
||||
|
||||
const makeOnRoomDisconnected = (room: LiveKitRoom) => (reason?: DisconnectReason) => {
|
||||
// Ignore disconnects from rooms that are no longer the active session.
|
||||
if (room !== activeRoom) return
|
||||
@@ -346,7 +320,6 @@ export const joinVoiceRoom = async (
|
||||
activeRoom = liveKitRoom
|
||||
|
||||
liveKitRoom.on(RoomEvent.Disconnected, makeOnRoomDisconnected(liveKitRoom))
|
||||
liveKitRoom.on(RoomEvent.Reconnected, makeOnRoomReconnected(liveKitRoom))
|
||||
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
voiceMicMuted,
|
||||
voiceState,
|
||||
} from "@app/call/stores"
|
||||
import {cancelJoinVoiceRoom, joinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
||||
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
||||
|
||||
const {relay, h} = $derived($page.params)
|
||||
const url = $derived(relay ? decodeRelay(relay) : undefined)
|
||||
@@ -86,11 +86,6 @@
|
||||
pushModal(VoiceRoomJoinDialog, {url: targetRoom.url, h: targetRoom.h})
|
||||
}
|
||||
|
||||
const onReconnect = () => {
|
||||
if (!targetRoom) return
|
||||
void joinVoiceRoom(targetRoom.url, targetRoom.h)
|
||||
}
|
||||
|
||||
const goToRoom = () => {
|
||||
if (!targetRoom) return
|
||||
const path = makeRoomPath(targetRoom.url, targetRoom.h)
|
||||
@@ -242,13 +237,6 @@
|
||||
onclick={leaveVoiceRoom}>
|
||||
<Icon icon={PhoneRounded} size={4} />
|
||||
</Button>
|
||||
{:else if $currentVoiceRoom}
|
||||
<Button
|
||||
data-tip="Reconnect"
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
||||
onclick={onReconnect}>
|
||||
<Icon icon={PhoneCallingRounded} size={4} />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
data-tip="Join Voice"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import {on} from "@welshman/lib"
|
||||
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {AbstractAdapter, AdapterEvent, LocalAdapter, Repository, netContext} from "@welshman/net"
|
||||
import type {ClientMessage, NetContext, RelayMessage} from "@welshman/net"
|
||||
|
||||
// The window key Playwright writes the mock config to (see e2e/support/relayMocks.ts). Keep it in
|
||||
// sync with the literal duplicated there.
|
||||
export const RELAY_MOCKS_KEY = "__RELAY_MOCKS__"
|
||||
|
||||
export type RelayMockConfig = {
|
||||
// Map of relay url -> events that relay should return. Any relay NOT listed returns nothing (an
|
||||
// immediate EOSE), which is what keeps tests offline and reproducible by default.
|
||||
relays?: Record<string, TrustedEvent[]>
|
||||
}
|
||||
|
||||
// Wraps welshman's LocalAdapter so we reuse its REQ/EVENT/CLOSE handling against an in-memory
|
||||
// Repository, but re-emits its messages under the real relay url instead of LOCAL_RELAY_URL. That
|
||||
// keeps relay attribution / relay-scoped behaviour (e.g. NIP-29 groups) working as it would over a
|
||||
// real socket. (Composition rather than inheritance because LocalAdapter emits via a private method
|
||||
// that hardcodes LOCAL_RELAY_URL.)
|
||||
class FixtureAdapter extends AbstractAdapter {
|
||||
readonly local: LocalAdapter
|
||||
|
||||
constructor(
|
||||
readonly url: string,
|
||||
repository: Repository,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.local = new LocalAdapter(repository)
|
||||
|
||||
const forward = (message: RelayMessage) => this.emit(AdapterEvent.Receive, message, this.url)
|
||||
|
||||
this._unsubscribers.push(
|
||||
on(this.local, AdapterEvent.Receive, forward),
|
||||
() => this.local.cleanup(),
|
||||
)
|
||||
}
|
||||
|
||||
get sockets() {
|
||||
return this.local.sockets
|
||||
}
|
||||
|
||||
get urls() {
|
||||
return [this.url]
|
||||
}
|
||||
|
||||
send(message: ClientMessage) {
|
||||
this.local.send(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Override netContext.getAdapter so every real relay url is served from memory and no websocket is
|
||||
// ever created for it. Non-relay urls (e.g. the local:// repository relay) fall through to
|
||||
// welshman's default handling.
|
||||
export const installRelayMocks = (config: RelayMockConfig) => {
|
||||
const reposByUrl = new Map<string, Repository>()
|
||||
|
||||
for (const [url, events] of Object.entries(config.relays ?? {})) {
|
||||
const repository = new Repository()
|
||||
repository.load(events)
|
||||
reposByUrl.set(normalizeRelayUrl(url), repository)
|
||||
}
|
||||
|
||||
const emptyRepository = new Repository()
|
||||
const fallback = netContext.getAdapter
|
||||
|
||||
netContext.getAdapter = ((url: string, context: NetContext) => {
|
||||
if (!isRelayUrl(url)) {
|
||||
return fallback ? fallback(url, context) : undefined
|
||||
}
|
||||
|
||||
const repository = reposByUrl.get(normalizeRelayUrl(url)) ?? emptyRepository
|
||||
|
||||
return new FixtureAdapter(url, repository)
|
||||
}) as NetContext["getAdapter"]
|
||||
}
|
||||
|
||||
// Called once on app startup. Installs the mocks only when Playwright has injected a config, so it
|
||||
// is a no-op for real users.
|
||||
export const maybeInstallRelayMocks = () => {
|
||||
const config = (globalThis as Record<string, unknown>)[RELAY_MOCKS_KEY] as
|
||||
| RelayMockConfig
|
||||
| undefined
|
||||
|
||||
if (config) {
|
||||
installRelayMocks(config)
|
||||
}
|
||||
|
||||
return Boolean(config)
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
import * as app from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import * as implicit from "@lib/implicit"
|
||||
import {maybeInstallRelayMocks} from "@lib/test/relayMocks"
|
||||
import AppContainer from "@app/components/AppContainer.svelte"
|
||||
import ModalContainer from "@app/components/ModalContainer.svelte"
|
||||
import {setupHistory} from "@app/util/history"
|
||||
@@ -46,6 +47,12 @@
|
||||
|
||||
const {children} = $props()
|
||||
|
||||
// Test-only: when Playwright has injected window.__RELAY_MOCKS__, serve relays from in-memory
|
||||
// fixtures instead of the network. No-op for real users; stripped from production builds.
|
||||
if (import.meta.env.DEV) {
|
||||
maybeInstallRelayMocks()
|
||||
}
|
||||
|
||||
const policies = [authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy]
|
||||
|
||||
// Add stuff to window for convenience
|
||||
|
||||
Reference in New Issue
Block a user