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

Commit

Permalink
Add more e2e test cases (#62)
Browse files Browse the repository at this point in the history
* Add more e2e test cases

* Implement feedback
  • Loading branch information
szebniok authored Jan 5, 2024
1 parent 8cbef8b commit c1042cd
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 55 deletions.
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,
},
},
});
});

0 comments on commit c1042cd

Please sign in to comment.