Skip to content
This repository has been archived by the owner on Jun 28, 2024. It is now read-only.

Add more e2e test cases #62

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
docs
.vscode
.vscode
playwright-report
4 changes: 2 additions & 2 deletions examples/minimal-react/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export const App = () => {
<span>Status: {status}</span>
</div>
{/* Render the remote tracks from other peers*/}
{Object.values(tracks).map(({ stream, trackId }) => (
<VideoPlayer key={trackId} stream={stream} /> // Simple component to render a video element
{Object.values(tracks).map(({ stream, trackId, origin }) => (
<VideoPlayer key={trackId} stream={stream} peerId={origin.id} /> // Simple component to render a video element
))}
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions examples/minimal-react/src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import { RefObject, useEffect, useRef } from "react";

type Props = {
stream: MediaStream | null | undefined;
peerId: string;
};

const VideoPlayer = ({ stream }: Props) => {
const VideoPlayer = ({ stream, peerId }: Props) => {
const videoRef: RefObject<HTMLVideoElement> = useRef<HTMLVideoElement>(null);

useEffect(() => {
if (!videoRef.current) return;
videoRef.current.srcObject = stream || null;
}, [stream]);

return <video autoPlay playsInline muted ref={videoRef} />;
return <video autoPlay playsInline muted data-peer-id={peerId} ref={videoRef} />;
};

export default VideoPlayer;
150 changes: 100 additions & 50 deletions tests/jellyfish.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { JellyfishClient } from "@jellyfish-dev/ts-client-sdk";
import { test, expect, type Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
import {
assertThatOtherVideoIsPlaying,
assertThatRemoteTracksAreVisible,
createRoom,
joinRoomAndAddScreenShare,
} from "./utils";

test("displays basic UI", async ({ page }) => {
await page.goto("/");
Expand All @@ -9,62 +14,107 @@ test("displays basic UI", async ({ page }) => {
await expect(page.getByRole("button", { name: "Connect", exact: true })).toBeVisible();
});

test("connects to Jellyfish server", async ({ page: firstPage, context }) => {
test("Connect 2 peers to 1 room", async ({ page: firstPage, context }) => {
const secondPage = await context.newPage();
await firstPage.goto("/");
await secondPage.goto("/");

const roomRequest = await firstPage.request.post("http://localhost:5002/room");
const roomId = (await roomRequest.json()).data.room.id as string;
const roomId = await createRoom(firstPage);

await joinRoomAndAddTrack(firstPage, roomId);
await joinRoomAndAddTrack(secondPage, roomId);

await expect(firstPage.locator("video")).toBeVisible();
await expect(secondPage.locator("video")).toBeVisible();
const firstPageId = await joinRoomAndAddScreenShare(firstPage, roomId);
const secondPageId = await joinRoomAndAddScreenShare(secondPage, roomId);

await Promise.all([
assertThatRemoteTracksAreVisible(firstPage, [secondPageId]),
assertThatRemoteTracksAreVisible(secondPage, [firstPageId]),
]);
await Promise.all([assertThatOtherVideoIsPlaying(firstPage), assertThatOtherVideoIsPlaying(secondPage)]);
});

async function joinRoomAndAddTrack(page: Page, roomId: string): Promise<string> {
const peerRequest = await page.request.post("http://localhost:5002/room/" + roomId + "/peer", {
data: {
type: "webrtc",
options: {
enableSimulcast: true,
},
test("Client properly sees 3 other peers", async ({ page, context }) => {
const pages = [page, ...(await Promise.all([...Array(3)].map(() => context.newPage())))];

const roomId = await createRoom(page);

const peerIds = await Promise.all(
pages.map(async (page) => {
await page.goto("/");
return await joinRoomAndAddScreenShare(page, roomId);
}),
);

await Promise.all(
pages.map(async (page, idx) => {
await assertThatRemoteTracksAreVisible(
page,
peerIds.filter((id) => id !== peerIds[idx]),
);
await assertThatOtherVideoIsPlaying(page);
}),
);
});

test("Peer see peers just in the same room", async ({ page, context }) => {
const [p1r1, p2r1, p1r2, p2r2] = [page, ...(await Promise.all([...Array(3)].map(() => context.newPage())))];
const [firstRoomPages, secondRoomPages] = [
[p1r1, p2r1],
[p1r2, p2r2],
];

const firstRoomId = await createRoom(page);
const secondRoomId = await createRoom(page);

const firstRoomPeerIds = await Promise.all(
firstRoomPages.map(async (page) => {
await page.goto("/");
return await joinRoomAndAddScreenShare(page, firstRoomId);
}),
);

const secondRoomPeerIds = await Promise.all(
secondRoomPages.map(async (page) => {
await page.goto("/");
return await joinRoomAndAddScreenShare(page, secondRoomId);
}),
);

await Promise.all([
...firstRoomPages.map(async (page, idx) => {
await assertThatRemoteTracksAreVisible(
page,
firstRoomPeerIds.filter((id) => id !== firstRoomPeerIds[idx]),
);
await expect(assertThatRemoteTracksAreVisible(page, secondRoomPeerIds)).rejects.toThrow();
await assertThatOtherVideoIsPlaying(page);
}),
...secondRoomPages.map(async (page, idx) => {
await assertThatRemoteTracksAreVisible(
page,
secondRoomPeerIds.filter((id) => id !== secondRoomPeerIds[idx]),
);
await expect(assertThatRemoteTracksAreVisible(page, firstRoomPeerIds)).rejects.toThrow();
await assertThatOtherVideoIsPlaying(page);
}),
]);
});

test("Client throws an error if joining room at max capacity", async ({ page, context }) => {
const [page1, page2, overflowingPage] = [page, ...(await Promise.all([...Array(2)].map(() => context.newPage())))];

const roomId = await createRoom(page, 2);

await Promise.all(
[page1, page2].map(async (page) => {
await page.goto("/");
return await joinRoomAndAddScreenShare(page, roomId);
}),
);

await overflowingPage.goto("/");
await expect(joinRoomAndAddScreenShare(overflowingPage, roomId)).rejects.toEqual({
status: 503,
response: {
errors: `Reached peer limit in room ${roomId}`,
},
});
const {
peer: { id: peerId },
token: peerToken,
} = (await peerRequest.json()).data;

await page.getByPlaceholder("token").fill(peerToken);
await page.getByRole("button", { name: "Connect", exact: true }).click();
await page.getByRole("button", { name: "Start screen share" }).click();

return peerId;
}

async function assertThatOtherVideoIsPlaying(page: Page) {
const playing = await page.evaluate(async () => {
const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const getDecodedFrames = async () => {
const stats = await peerConnection.getStats();
for (const stat of stats.values()) {
if (stat.type === "inbound-rtp") {
return stat.framesDecoded;
}
}
};

const client = (window as unknown as { client: JellyfishClient<unknown, unknown> }).client;
const peerConnection = (client as unknown as { webrtc: { connection: RTCPeerConnection } }).webrtc.connection;
const firstMeasure = await getDecodedFrames();
await sleep(400);
const secondMeasure = await getDecodedFrames();
return secondMeasure > firstMeasure;
});
expect(playing).toBe(true);
}
});
76 changes: 76 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, Page, test } from "@playwright/test";

export const joinRoomAndAddScreenShare = async (page: Page, roomId: string): Promise<string> =>
test.step("Join room and add track", async () => {
const peerRequest = await createPeer(page, roomId);
try {
const {
peer: { id: peerId },
token: peerToken,
} = (await peerRequest.json()).data;

await test.step("Join room", async () => {
await page.getByPlaceholder("token").fill(peerToken);
await page.getByRole("button", { name: "Connect", exact: true }).click();
await expect(page.getByText("Status: joined")).toBeVisible();
});

await test.step("Add screenshare", async () => {
await page.getByRole("button", { name: "Start screen share", exact: true }).click();
});

return peerId;
} catch (e) {
// todo fix
throw { status: peerRequest.status(), response: await peerRequest.json() };
}
});

export const assertThatRemoteTracksAreVisible = async (page: Page, otherClientIds: string[]) => {
await test.step("Assert that remote tracks are visible", () =>
Promise.all(
otherClientIds.map((peerId) => expect(page.locator(`css=video[data-peer-id="${peerId}"]`)).toBeVisible()),
));
};

export const assertThatOtherVideoIsPlaying = async (page: Page) => {
await test.step("Assert that media is working", async () => {
const getDecodedFrames: () => Promise<number> = () =>
page.evaluate(async () => {
const peerConnection = (
window as typeof window & { client: { webrtc: { connection: RTCPeerConnection | undefined } } }
).client.webrtc.connection;
const stats = await peerConnection?.getStats();
for (const stat of stats?.values() ?? []) {
if (stat.type === "inbound-rtp") {
return stat.framesDecoded;
}
}
return 0;
});
const firstMeasure = await getDecodedFrames();
await expect(async () => expect((await getDecodedFrames()) > firstMeasure).toBe(true)).toPass();
});
};

export const createRoom = async (page: Page, maxPeers?: number) =>
await test.step("Create room", async () => {
const data = {
...(maxPeers ? { maxPeers } : {}),
};

const roomRequest = await page.request.post("http://localhost:5002/room", { data });
return (await roomRequest.json()).data.room.id as string;
});

export const createPeer = async (page: Page, roomId: string, enableSimulcast: boolean = true) =>
await test.step("Create room", async () => {
return await page.request.post("http://localhost:5002/room/" + roomId + "/peer", {
data: {
type: "webrtc",
options: {
enableSimulcast,
},
},
});
});