From 54d14eb0844b05344c92ebc3dfa3a44ca6419d19 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Mon, 23 Feb 2026 12:22:21 +0100 Subject: [PATCH 1/9] add livestream tests --- e2e-tests/livestream-client/.gitignore | 29 +++ e2e-tests/livestream-client/README.md | 98 ++++++++ e2e-tests/livestream-client/config.ts | 2 + e2e-tests/livestream-client/index.html | 23 ++ e2e-tests/livestream-client/package.json | 39 ++++ .../livestream-client/playwright.config.ts | 58 +++++ .../livestream-client/scenarios/.gitkeep | 1 + .../livestream-client/scenarios/basic.spec.ts | 61 +++++ .../livestream-client/scenarios/utils.ts | 84 +++++++ e2e-tests/livestream-client/src/App.tsx | 209 ++++++++++++++++++ .../livestream-client/src/canvasStream.ts | 90 ++++++++ e2e-tests/livestream-client/src/main.tsx | 14 ++ e2e-tests/livestream-client/src/vite-env.d.ts | 10 + e2e-tests/livestream-client/tsconfig.json | 18 ++ .../livestream-client/tsconfig.node.json | 10 + e2e-tests/livestream-client/vite.config.ts | 7 + .../mobile-client/common/plugins/src/utils.ts | 2 +- .../plugins/src/withLocalWebrtcAndroid.ts | 20 +- .../common/plugins/src/withLocalWebrtcIos.ts | 20 +- .../plugins/src/withLocalWebrtcPaths.ts | 6 +- examples/mobile-client/fishjam-chat/README.md | 3 + .../fishjam-chat/app/(tabs)/room.tsx | 31 ++- .../fishjam-chat/app/_layout.tsx | 30 ++- .../app/livestream/screen-sharing.tsx | 2 +- .../fishjam-chat/components/InCallButton.tsx | 1 - .../fishjam-chat/components/NoCameraView.tsx | 1 - .../fishjam-chat/components/VideosGrid.tsx | 8 +- .../fishjam-chat/eslint.config.js | 6 +- .../mobile-client/fishjam-chat/tsconfig.json | 11 +- .../fishjam-chat/utils/fishjamIdStore.ts | 1 - package.json | 3 +- .../src/hooks/useLivestreamViewer.ts | 5 +- packages/ts-client/src/livestream.ts | 4 + yarn.lock | 33 +++ 34 files changed, 878 insertions(+), 62 deletions(-) create mode 100644 e2e-tests/livestream-client/.gitignore create mode 100644 e2e-tests/livestream-client/README.md create mode 100644 e2e-tests/livestream-client/config.ts create mode 100644 e2e-tests/livestream-client/index.html create mode 100644 e2e-tests/livestream-client/package.json create mode 100644 e2e-tests/livestream-client/playwright.config.ts create mode 100644 e2e-tests/livestream-client/scenarios/.gitkeep create mode 100644 e2e-tests/livestream-client/scenarios/basic.spec.ts create mode 100644 e2e-tests/livestream-client/scenarios/utils.ts create mode 100644 e2e-tests/livestream-client/src/App.tsx create mode 100644 e2e-tests/livestream-client/src/canvasStream.ts create mode 100644 e2e-tests/livestream-client/src/main.tsx create mode 100644 e2e-tests/livestream-client/src/vite-env.d.ts create mode 100644 e2e-tests/livestream-client/tsconfig.json create mode 100644 e2e-tests/livestream-client/tsconfig.node.json create mode 100644 e2e-tests/livestream-client/vite.config.ts diff --git a/e2e-tests/livestream-client/.gitignore b/e2e-tests/livestream-client/.gitignore new file mode 100644 index 000000000..6c3ed0dfd --- /dev/null +++ b/e2e-tests/livestream-client/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Playwright +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/e2e-tests/livestream-client/README.md b/e2e-tests/livestream-client/README.md new file mode 100644 index 000000000..e396b8e71 --- /dev/null +++ b/e2e-tests/livestream-client/README.md @@ -0,0 +1,98 @@ +# Livestream E2E Tests + +This is a lightweight React app for testing WHIP/WHEP livestreaming functionality using the `@fishjam-cloud/ts-client` package. + +## Structure + +``` +livestream-client/ +├── src/ +│ ├── App.tsx # Main React app with WHIP/WHEP controls +│ ├── main.tsx # React entry point +│ └── vite-env.d.ts # Vite environment types +├── scenarios/ # Playwright test scenarios (to be added) +├── config.ts # Default WHIP/WHEP URLs +├── index.html # HTML entry point +├── package.json # Dependencies and scripts +├── playwright.config.ts # Playwright configuration +├── tsconfig.json # TypeScript config for src +├── tsconfig.node.json # TypeScript config for config files +└── vite.config.ts # Vite bundler config +``` + +## Features + +The React app provides two main sections: + +### Receive (WHEP) + +- Input for WHEP URL (default: `http://localhost:4000/whep`) +- Optional token input +- Start/Stop receiving button +- Video player to display received stream +- Connection status and error display + +### Publish (WHIP) + +- Input for WHIP URL (default: `http://localhost:4000/whip`) +- Required token input +- Start/Stop publishing button +- Video preview showing animated emoji (no camera required!) +- Connection status and error display + +## Usage + +### Install dependencies + +```bash +yarn install +``` + +### Run dev server + +```bash +yarn dev +``` + +The app will be available at `http://localhost:5174` + +### Run tests + +```bash +yarn e2e # Run tests +yarn e2e:ui # Run tests with Playwright UI +``` + +### Environment Variables + +You can override default URLs using environment variables: + +```bash +VITE_WHEP_URL=http://example.com/whep VITE_WHIP_URL=http://example.com/whip yarn dev +``` + +## Adding Tests + +Create Playwright test files in the `scenarios/` directory. Example: + +```typescript +import { test, expect } from "@playwright/test"; + +test("can receive livestream", async ({ page }) => { + await page.goto("/"); + + await page.fill("#whep-url-input", "http://localhost:4000/whep"); + await page.click("#receive-button"); + + await expect(page.locator("#receive-status")).toContainText("Receiving"); +}); +``` + +## Notes + +- **No camera required!** The app uses a canvas stream with animated emojis instead of `getUserMedia()` +- This makes it perfect for CI/CD environments like GitHub Actions runners +- The canvas shows a bouncing emoji with changing colors and a frame counter +- All inputs are persisted to localStorage for convenience +- The app runs on port 5174 (different from webrtc-client's 5173) +- Uses React 19 and Vite 6 for modern, fast development diff --git a/e2e-tests/livestream-client/config.ts b/e2e-tests/livestream-client/config.ts new file mode 100644 index 000000000..72089a6f2 --- /dev/null +++ b/e2e-tests/livestream-client/config.ts @@ -0,0 +1,2 @@ +export const FISHJAM_ID = + import.meta.env.VITE_FISHJAM_URL || "http://localhost:4000"; diff --git a/e2e-tests/livestream-client/index.html b/e2e-tests/livestream-client/index.html new file mode 100644 index 000000000..99463d631 --- /dev/null +++ b/e2e-tests/livestream-client/index.html @@ -0,0 +1,23 @@ + + + + + + Livestream E2E Tests + + + +
+ + + diff --git a/e2e-tests/livestream-client/package.json b/e2e-tests/livestream-client/package.json new file mode 100644 index 000000000..36f50ae59 --- /dev/null +++ b/e2e-tests/livestream-client/package.json @@ -0,0 +1,39 @@ +{ + "name": "@fishjam-e2e/livestream-client", + "private": true, + "version": "0.1.0", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint . --ext .ts,.tsx --fix", + "lint:check": "eslint . --ext .ts,.tsx", + "e2e": "playwright test", + "e2e:ui": "playwright test --ui", + "preview": "vite preview" + }, + "dependencies": { + "@fishjam-cloud/react-client": "workspace:*", + "@fishjam-cloud/ts-client": "workspace:*", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.51.1", + "@types/node": "^22.14.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "prettier": "^3.8.1", + "typescript": "^5.8.3", + "vite": "^6.2.6" + } +} diff --git a/e2e-tests/livestream-client/playwright.config.ts b/e2e-tests/livestream-client/playwright.config.ts new file mode 100644 index 000000000..272d1682d --- /dev/null +++ b/e2e-tests/livestream-client/playwright.config.ts @@ -0,0 +1,58 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./scenarios", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ["list"], + [ + "html", + { + outputFolder: "../../playwright-report/livestream-e2e", + open: "never", + }, + ], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:5173", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + launchOptions: { + args: [ + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + ], + }, + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: `VITE_FISHJAM_URL=${process.env.VITE_FISHJAM_URL} yarn dev`, + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/e2e-tests/livestream-client/scenarios/.gitkeep b/e2e-tests/livestream-client/scenarios/.gitkeep new file mode 100644 index 000000000..b5218f7cf --- /dev/null +++ b/e2e-tests/livestream-client/scenarios/.gitkeep @@ -0,0 +1 @@ +# Placeholder for future Playwright test scenarios diff --git a/e2e-tests/livestream-client/scenarios/basic.spec.ts b/e2e-tests/livestream-client/scenarios/basic.spec.ts new file mode 100644 index 000000000..9a3e40a6c --- /dev/null +++ b/e2e-tests/livestream-client/scenarios/basic.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +import { + assertThatVideoIsPlaying, + assertThatVideoStopped, + startPublishing, + startReceiving, + stopPublishing, + stopReceiving, +} from "./utils"; + +test("Displays basic UI", async ({ page }) => { + await page.goto("/"); + + await expect( + page.getByRole("button", { name: "Start Publishing", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Start Receiving", exact: true }), + ).toBeVisible(); + await expect(page.locator("#receive-video")).toBeVisible(); + await expect(page.locator("#publish-video")).toBeVisible(); +}); + +test("Viewer receives a published stream", async ({ page }) => { + await page.goto(`/?room=${encodeURIComponent(crypto.randomUUID())}`); + + await startPublishing(page); + await startReceiving(page); + await assertThatVideoIsPlaying(page); +}); + +test("Viewer stops receiving when disconnected", async ({ page }) => { + await page.goto(`/?room=${encodeURIComponent(crypto.randomUUID())}`); + await startPublishing(page); + await startReceiving(page); + await assertThatVideoIsPlaying(page); + await stopReceiving(page); + await assertThatVideoStopped(page); +}); + +test("Viewer can reconnect after disconnecting", async ({ page }) => { + await page.goto(`/?room=${encodeURIComponent(crypto.randomUUID())}`); + + await startPublishing(page); + await startReceiving(page); + await assertThatVideoIsPlaying(page); + await stopReceiving(page); + await startReceiving(page); + await assertThatVideoIsPlaying(page); +}); + +test("Stream ends when streamer stops publishing", async ({ page }) => { + await page.goto(`/?room=${encodeURIComponent(crypto.randomUUID())}`); + + await startPublishing(page); + await startReceiving(page); + await assertThatVideoIsPlaying(page); + await stopPublishing(page); + await assertThatVideoStopped(page); +}); diff --git a/e2e-tests/livestream-client/scenarios/utils.ts b/e2e-tests/livestream-client/scenarios/utils.ts new file mode 100644 index 000000000..675382a33 --- /dev/null +++ b/e2e-tests/livestream-client/scenarios/utils.ts @@ -0,0 +1,84 @@ +import type { Page } from "@playwright/test"; +import { expect, test } from "@playwright/test"; + +const TO_PASS_TIMEOUT_MILLIS = 15 * 1000; + +const expectWithLongerTimeout = expect.configure({ + timeout: TO_PASS_TIMEOUT_MILLIS, +}); + +type ViewerWindow = typeof window & { + viewer?: { + getStatistics: () => Promise; + }; +}; + +const getDecodedFrames = (page: Page): Promise => + page.evaluate(async () => { + const viewer = (window as ViewerWindow)?.viewer; + if (!viewer) return -1; + const stats = await viewer.getStatistics(); + if (!stats) return -1; + for (const stat of stats.values()) { + if (stat.type === "inbound-rtp") { + return stat.framesDecoded ?? 0; + } + } + return 0; + }); + +export const startPublishing = async (page: Page) => + await test.step("Start publishing", async () => { + await page + .getByRole("button", { name: "Start Publishing", exact: true }) + .click(); + await expectWithLongerTimeout( + page.locator("#publish-status"), + ).toContainText("Publishing"); + }); + +export const stopPublishing = async (page: Page) => + await test.step("Stop publishing", async () => { + await page + .getByRole("button", { name: "Stop Publishing", exact: true }) + .click(); + await expectWithLongerTimeout( + page.locator("#publish-status"), + ).toContainText("Not publishing"); + }); + +export const startReceiving = async (page: Page) => + await test.step("Start receiving", async () => { + await page + .getByRole("button", { name: "Start Receiving", exact: true }) + .click(); + await expectWithLongerTimeout( + page.locator("#receive-status"), + ).toContainText("Receiving"); + }); + +export const stopReceiving = async (page: Page) => + await test.step("Stop receiving", async () => { + await page + .getByRole("button", { name: "Stop Receiving", exact: true }) + .click(); + await expectWithLongerTimeout( + page.locator("#receive-status"), + ).toContainText("Not receiving"); + }); + +export const assertThatVideoIsPlaying = async (page: Page) => + await test.step("Assert that video is playing", async () => { + const firstMeasure = await getDecodedFrames(page); + await expectWithLongerTimeout(async () => + expect(await getDecodedFrames(page)).toBeGreaterThan(firstMeasure), + ).toPass(); + }); + +export const assertThatVideoStopped = async (page: Page) => + await test.step("Assert that video stopped", async () => { + const firstMeasure = await getDecodedFrames(page); + await page.waitForTimeout(500); + const secondMeasure = await getDecodedFrames(page); + expect(secondMeasure - firstMeasure).toBeLessThanOrEqual(0); + }); diff --git a/e2e-tests/livestream-client/src/App.tsx b/e2e-tests/livestream-client/src/App.tsx new file mode 100644 index 000000000..afed23fda --- /dev/null +++ b/e2e-tests/livestream-client/src/App.tsx @@ -0,0 +1,209 @@ +import { + useLivestreamStreamer, + useLivestreamViewer, + useSandbox, +} from "@fishjam-cloud/react-client"; +import { LivestreamError } from "@fishjam-cloud/ts-client"; +import { useEffect, useRef, useState } from "react"; + +import { createCanvasStream, stopCanvasStream } from "./canvasStream"; + +const roomName = + new URLSearchParams(window.location.search).get("room") ?? "livestream-e2e"; + +export function App() { + const { getSandboxLivestream, getSandboxViewerToken } = useSandbox(); + const viewer = useLivestreamViewer(); + const streamer = useLivestreamStreamer(); + + // eslint-disable-next-line + (window as any).viewer = viewer; + + // Receive (WHEP) state + const [receiveError, setReceiveError] = useState(null); + const receiveVideoRef = useRef(null); + + // Publish (WHIP) state + const [publishError, setPublishError] = useState(null); + const publishVideoRef = useRef(null); + const localStreamRef = useRef(null); + + useEffect(() => { + if (!receiveVideoRef.current) return; + receiveVideoRef.current.srcObject = viewer.stream; + }, [viewer.stream]); + + const handleStartReceiving = async () => { + setReceiveError(null); + try { + const viewerToken = await getSandboxViewerToken(roomName); + await viewer.connect({ token: viewerToken }); + } catch (error) { + const errorMessage = + error === LivestreamError.UNAUTHORIZED + ? "Unauthorized: Invalid or missing token" + : error === LivestreamError.STREAM_NOT_FOUND + ? "Stream not found" + : error === LivestreamError.UNKNOWN_ERROR + ? "Unknown error occurred" + : String(error); + + setReceiveError(errorMessage); + console.error("Failed to start receiving:", error); + } + }; + + const handleStopReceiving = async () => { + viewer.disconnect(); + if (receiveVideoRef.current) { + receiveVideoRef.current.srcObject = null; + } + }; + + const handleStartPublishing = async () => { + setPublishError(null); + try { + const { streamerToken } = await getSandboxLivestream(roomName); + + // Create canvas stream with animated emoji + const stream = createCanvasStream(); + + localStreamRef.current = stream; + + if (publishVideoRef.current) { + publishVideoRef.current.srcObject = stream; + } + + await streamer.connect({ + inputs: { video: stream }, + token: streamerToken, + }); + } catch (error) { + const errorMessage = + error === LivestreamError.UNAUTHORIZED + ? "Unauthorized: Invalid or missing token" + : error === LivestreamError.STREAM_NOT_FOUND + ? "Stream not found" + : error === LivestreamError.STREAMER_ALREADY_CONNECTED + ? "Streamer already connected" + : error === LivestreamError.UNKNOWN_ERROR + ? "Unknown error occurred" + : String(error); + + setPublishError(errorMessage); + console.error("Failed to start publishing:", error); + + // Clean up local stream if it was created + if (localStreamRef.current) { + localStreamRef.current.getTracks().forEach((track) => track.stop()); + localStreamRef.current = null; + } + } + }; + + const handleStopPublishing = async () => { + streamer.disconnect(); + + if (localStreamRef.current) { + stopCanvasStream(localStreamRef.current); + localStreamRef.current = null; + } + + if (publishVideoRef.current) { + publishVideoRef.current.srcObject = null; + } + }; + + return ( +
+

Livestream E2E Tests

+ +
+ {/* Receive Section */} +
+

Receive (WHEP)

+
+ + {receiveError && ( +
+ Error: {receiveError} +
+ )} +
+ Status: {viewer.isConnected ? "Receiving" : "Not receiving"} +
+
+ +
+ + {/* Publish Section */} +
+

Publish (WHIP)

+
+ + {publishError && ( +
+ Error: {publishError} +
+ )} +
+ Status: {streamer.isConnected ? "Publishing" : "Not publishing"} +
+
+ +
+
+
+ ); +} diff --git a/e2e-tests/livestream-client/src/canvasStream.ts b/e2e-tests/livestream-client/src/canvasStream.ts new file mode 100644 index 000000000..7777c54f0 --- /dev/null +++ b/e2e-tests/livestream-client/src/canvasStream.ts @@ -0,0 +1,90 @@ +/** + * Creates a MediaStream from a canvas with an animated emoji. + * This works reliably in CI environments like GitHub runners. + */ +export function createCanvasStream(): MediaStream { + const canvas = document.createElement("canvas"); + canvas.width = 640; + canvas.height = 480; + const ctx = canvas.getContext("2d")!; + + const emojis = ["🎥", "📹", "🎬", "🎞️", "📽️"]; + let frame = 0; + let emojiIndex = 0; + + // Animate the canvas + const animate = () => { + // Background with gradient + const gradient = ctx.createLinearGradient( + 0, + 0, + canvas.width, + canvas.height, + ); + gradient.addColorStop(0, `hsl(${frame % 360}, 70%, 50%)`); + gradient.addColorStop(1, `hsl(${(frame + 120) % 360}, 70%, 50%)`); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw emoji + ctx.font = "120px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + + // Bounce effect + const bounce = Math.sin(frame * 0.1) * 30; + const rotation = Math.sin(frame * 0.05) * 0.2; + + ctx.save(); + ctx.translate(canvas.width / 2, canvas.height / 2 + bounce); + ctx.rotate(rotation); + ctx.fillText(emojis[emojiIndex], 0, 0); + ctx.restore(); + + // Draw frame counter + ctx.fillStyle = "white"; + ctx.font = "20px monospace"; + ctx.textAlign = "left"; + ctx.fillText(`Frame: ${frame}`, 10, 30); + ctx.fillText(`Time: ${(frame / 30).toFixed(1)}s`, 10, 60); + + frame++; + + // Change emoji every 60 frames (~2 seconds at 30fps) + if (frame % 60 === 0) { + emojiIndex = (emojiIndex + 1) % emojis.length; + } + }; + + // Start animation loop + const intervalId = setInterval(animate, 1000 / 30); // 30 FPS + + // Initial draw + animate(); + + // Create stream from canvas + const stream = canvas.captureStream(30); // 30 FPS + + // Store interval ID so we can clean it up + (stream as MediaStream & { _intervalId?: NodeJS.Timeout })._intervalId = + intervalId; + + return stream; +} + +/** + * Stops the canvas stream and cleans up the animation interval. + */ +export function stopCanvasStream(stream: MediaStream | null) { + if (!stream) return; + + // Clean up animation interval if it exists + const intervalId = (stream as MediaStream & { _intervalId?: number }) + ._intervalId; + if (intervalId) { + clearInterval(intervalId); + } + + // Stop all tracks + stream.getTracks().forEach((track) => track.stop()); +} diff --git a/e2e-tests/livestream-client/src/main.tsx b/e2e-tests/livestream-client/src/main.tsx new file mode 100644 index 000000000..ac1f4f352 --- /dev/null +++ b/e2e-tests/livestream-client/src/main.tsx @@ -0,0 +1,14 @@ +import { FishjamProvider } from "@fishjam-cloud/react-client"; +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { FISHJAM_ID } from "../config"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/e2e-tests/livestream-client/src/vite-env.d.ts b/e2e-tests/livestream-client/src/vite-env.d.ts new file mode 100644 index 000000000..4b778ed87 --- /dev/null +++ b/e2e-tests/livestream-client/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_WHEP_URL?: string; + readonly VITE_WHIP_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/e2e-tests/livestream-client/tsconfig.json b/e2e-tests/livestream-client/tsconfig.json new file mode 100644 index 000000000..df9af6209 --- /dev/null +++ b/e2e-tests/livestream-client/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "es2022"], + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "jsx": "react-jsx", + "strict": true, + "incremental": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "outDir": "dist" + }, + "include": ["src/**/*"] +} diff --git a/e2e-tests/livestream-client/tsconfig.node.json b/e2e-tests/livestream-client/tsconfig.node.json new file mode 100644 index 000000000..919312183 --- /dev/null +++ b/e2e-tests/livestream-client/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "playwright.config.ts", "config.ts"] +} diff --git a/e2e-tests/livestream-client/vite.config.ts b/e2e-tests/livestream-client/vite.config.ts new file mode 100644 index 000000000..1ff0da0a1 --- /dev/null +++ b/e2e-tests/livestream-client/vite.config.ts @@ -0,0 +1,7 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/examples/mobile-client/common/plugins/src/utils.ts b/examples/mobile-client/common/plugins/src/utils.ts index aa962fcb9..6f16b1633 100644 --- a/examples/mobile-client/common/plugins/src/utils.ts +++ b/examples/mobile-client/common/plugins/src/utils.ts @@ -19,7 +19,7 @@ export function detectLocalWebrtcPath(): string | null { projectRoot, "packages", "mobile-client", - "package.json" + "package.json", ); try { diff --git a/examples/mobile-client/common/plugins/src/withLocalWebrtcAndroid.ts b/examples/mobile-client/common/plugins/src/withLocalWebrtcAndroid.ts index 0972b8e69..25af7bec6 100644 --- a/examples/mobile-client/common/plugins/src/withLocalWebrtcAndroid.ts +++ b/examples/mobile-client/common/plugins/src/withLocalWebrtcAndroid.ts @@ -1,14 +1,20 @@ -import { ConfigPlugin, withSettingsGradle } from '@expo/config-plugins'; -import { INFO_GENERATED_COMMENT_ANDROID } from './utils'; +import { ConfigPlugin, withSettingsGradle } from "@expo/config-plugins"; +import { INFO_GENERATED_COMMENT_ANDROID } from "./utils"; const removeGeneratedBlock = (content: string): string => { - const marker = INFO_GENERATED_COMMENT_ANDROID.trim().split('\n')[0]; - const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`\\n?\\s*${escapedMarker}[\\s\\S]*?(?=\\n[^\\s#/]|$)`, 'g'); - return content.replace(regex, ''); + const marker = INFO_GENERATED_COMMENT_ANDROID.trim().split("\n")[0]; + const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp( + `\\n?\\s*${escapedMarker}[\\s\\S]*?(?=\\n[^\\s#/]|$)`, + "g", + ); + return content.replace(regex, ""); }; -export const withLocalWebrtcAndroid: ConfigPlugin<{ localPath: string }> = (config, { localPath }) => { +export const withLocalWebrtcAndroid: ConfigPlugin<{ localPath: string }> = ( + config, + { localPath }, +) => { return withSettingsGradle(config, (configuration) => { let settings = configuration.modResults.contents; diff --git a/examples/mobile-client/common/plugins/src/withLocalWebrtcIos.ts b/examples/mobile-client/common/plugins/src/withLocalWebrtcIos.ts index 1da970f5f..d663a6756 100644 --- a/examples/mobile-client/common/plugins/src/withLocalWebrtcIos.ts +++ b/examples/mobile-client/common/plugins/src/withLocalWebrtcIos.ts @@ -1,14 +1,20 @@ -import { ConfigPlugin, withPodfile } from '@expo/config-plugins'; -import { INFO_GENERATED_COMMENT_IOS } from './utils'; +import { ConfigPlugin, withPodfile } from "@expo/config-plugins"; +import { INFO_GENERATED_COMMENT_IOS } from "./utils"; const removeGeneratedBlock = (content: string): string => { - const marker = INFO_GENERATED_COMMENT_IOS.trim().split('\n')[0]; - const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`\\n?\\s*${escapedMarker}[\\s\\S]*?(?=\\n[^\\s#]|$)`, 'g'); - return content.replace(regex, ''); + const marker = INFO_GENERATED_COMMENT_IOS.trim().split("\n")[0]; + const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp( + `\\n?\\s*${escapedMarker}[\\s\\S]*?(?=\\n[^\\s#]|$)`, + "g", + ); + return content.replace(regex, ""); }; -export const withLocalWebrtcIos: ConfigPlugin<{ localPath: string }> = (config, { localPath }) => { +export const withLocalWebrtcIos: ConfigPlugin<{ localPath: string }> = ( + config, + { localPath }, +) => { return withPodfile(config, (configuration) => { let podfile = configuration.modResults.contents; diff --git a/examples/mobile-client/common/plugins/src/withLocalWebrtcPaths.ts b/examples/mobile-client/common/plugins/src/withLocalWebrtcPaths.ts index c33461470..db200bf4d 100644 --- a/examples/mobile-client/common/plugins/src/withLocalWebrtcPaths.ts +++ b/examples/mobile-client/common/plugins/src/withLocalWebrtcPaths.ts @@ -17,19 +17,19 @@ export type LocalWebrtcPathsOptions = const withLocalWebrtcPaths: ConfigPlugin = ( config, - options = {} + options = {}, ) => { const localPath = options?.webrtcLocalPath ?? detectLocalWebrtcPath(); if (localPath) { console.log( - `🔧 [local-webrtc-paths] Using local WebRTC path: ${localPath}` + `🔧 [local-webrtc-paths] Using local WebRTC path: ${localPath}`, ); config = withLocalWebrtcIos(config, { localPath }); config = withLocalWebrtcAndroid(config, { localPath }); } else { console.log( - `📦 [local-webrtc-paths] No local path detected, using published WebRTC` + `📦 [local-webrtc-paths] No local path detected, using published WebRTC`, ); } diff --git a/examples/mobile-client/fishjam-chat/README.md b/examples/mobile-client/fishjam-chat/README.md index 7d8eaf802..86615745b 100644 --- a/examples/mobile-client/fishjam-chat/README.md +++ b/examples/mobile-client/fishjam-chat/README.md @@ -51,10 +51,13 @@ yarn android ## Development 1. Whenever you make changes in the `packages` directory, make sure to build the app in the root directory (not in `examples/mobile-client/fishjam-chat`). This ensures that all related workspaces are also built: + ```cmd yarn build ``` + 2. Linter (run in the root directory): + ```cmd yarn lint ``` diff --git a/examples/mobile-client/fishjam-chat/app/(tabs)/room.tsx b/examples/mobile-client/fishjam-chat/app/(tabs)/room.tsx index 854338c9d..f610b93cc 100644 --- a/examples/mobile-client/fishjam-chat/app/(tabs)/room.tsx +++ b/examples/mobile-client/fishjam-chat/app/(tabs)/room.tsx @@ -18,8 +18,7 @@ const FishjamLogo = require("../../assets/images/fishjam-logo.png"); const VIDEOROOM_STAGING_SANDBOX_URL = process.env.EXPO_PUBLIC_VIDEOROOM_STAGING_SANDBOX_URL ?? ""; -const VIDEOROOM_PROD_SANDBOX_URL = - process.env.EXPO_PUBLIC_FISHJAM_ID ?? ""; +const VIDEOROOM_PROD_SANDBOX_URL = process.env.EXPO_PUBLIC_FISHJAM_ID ?? ""; type VideoRoomEnv = "staging" | "prod"; @@ -77,7 +76,7 @@ export default function RoomScreen() { } }; loadData(); - }, []) + }, []), ); const validateInputs = () => { @@ -90,14 +89,14 @@ export default function RoomScreen() { try { validateInputs(); setConnectionError(null); - + const displayName = userName || "Mobile User"; await saveStorageData({ videoRoomEnv, roomName, userName: displayName }); - + Keyboard.dismiss(); router.push({ pathname: "/room/preview", - params: { roomName, userName: displayName}, + params: { roomName, userName: displayName }, }); } catch (e) { const message = @@ -120,19 +119,20 @@ export default function RoomScreen() { /> + }} + >