Skip to content

Commit

Permalink
Initial OBS E2E tests
Browse files Browse the repository at this point in the history
  • Loading branch information
markspolakovs committed Aug 30, 2023
1 parent 0d9444f commit 6865f76
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 15 deletions.
87 changes: 87 additions & 0 deletions desktop/e2e/obs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
ElectronApplication,
Page,
test as base,
_electron as electron,
expect,
} from "@playwright/test";
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "bowser-server/app/api/_router";
import MockOBSWebSocket from "@bowser/testing/MockOBSWebSocket";
import SuperJSON from "superjson";

const api = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "http://localhost:3000/trpc",
headers: () => ({
Authorization: "Bearer aaa",
}),
}),
],
transformer: SuperJSON,
});

const test = base.extend<{
app: [ElectronApplication, Page];
obs: MockOBSWebSocket;
}>({
app: async ({ request }, use) => {
const app = await electron.launch({ args: [".vite/build/main.js"] });
const win = await app.firstWindow();

await win.waitForLoadState("domcontentloaded");

await win.getByLabel("Server address").fill("http://localhost:3000");
await win.getByLabel("Server Password").fill("aaa");

await win.getByRole("button", { name: "Connect" }).click();

await expect(
win.getByRole("heading", { name: "Select a show" }),
).toBeVisible();

await use([app, win]);

await win.close();
await app.close();
},
obs: async (_, use) => {
const mows = await MockOBSWebSocket.create(expect);
await use(mows);
await mows.close();
},
});

test.beforeEach(async ({ request }) => {
await request.post(
"http://localhost:3000/api/resetDBInTestsDoNotUseOrYouWillBeFired",
);
await api.shows.create.mutate({
name: "Test Show",
start: new Date("2026-01-01T19:00:00Z"),
continuityItems: {
create: {
name: "Test Continuity",
durationSeconds: 0,
order: 0,
},
},
});
});

test("continuity works", async ({ app: [app, win], obs }) => {
await win.getByRole("button", { name: "Select" }).click();

await expect(win.getByLabel("Settings")).toBeVisible();
await win.getByLabel("Settings").click();

await win.getByRole("tab", { name: "OBS" }).click();

await win.getByLabel("OBS Host").fill("localhost");
await win.getByLabel("OBS WebSocket Port").fill(obs.port.toString(10));
await win.getByLabel("OBS WebSocket Password").fill("there is no password");
await win.getByRole("button", { name: "Connect" }).click();
await expect(win.getByTestId("OBSSettings.error")).not.toBeVisible();
await expect(win.getByTestId("OBSSettings.success")).toBeVisible();
});
2 changes: 1 addition & 1 deletion desktop/src/renderer/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default function MainScreen() {
open={isSettingsOpen}
onOpenChange={(v) => setIsSettingsOpen(v)}
>
<DialogTrigger>
<DialogTrigger aria-label="Settings">
<IoCog className="h-6 w-6" size={24} />
</DialogTrigger>
<DialogContent>
Expand Down
8 changes: 6 additions & 2 deletions desktop/src/renderer/screens/OBS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,16 @@ export function OBSSettings() {
Connect
</Button>
{state.data?.connected && (
<Alert>
<Alert data-testid="OBSSettings.success">
Successfully connected to OBS version {state.data.version} on{" "}
{state.data.platform}
</Alert>
)}
{error && <Alert variant="danger">{error}</Alert>}
{error && (
<Alert variant="danger" data-testid="OBSSettings.error">
{error}
</Alert>
)}
</form>
</div>
);
Expand Down
11 changes: 10 additions & 1 deletion server/app/api/_base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { initTRPC } from "@trpc/server";
import { TRPCError, initTRPC } from "@trpc/server";
import superjson from "superjson";

const t = initTRPC.create({
transformer: superjson,
});

export const publicProcedure = t.procedure;
export const testOnlyProcedure = publicProcedure.use(async ({ next }) => {
if (process.env.E2E_TEST === "true") {
return await next();
}
throw new TRPCError({
code: "UNAUTHORIZED",
message: "This procedure is only available in end-to-end tests.",
});
});
export const router = t.router;
79 changes: 77 additions & 2 deletions server/app/api/_router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { publicProcedure, router } from "./_base";
import { publicProcedure, router, testOnlyProcedure } from "./_base";
import { z } from "zod";
import { db } from "@/lib/db";
import {
Expand All @@ -8,7 +8,14 @@ import {
PartialShowModel,
} from "@bowser/prisma/utilityTypes";
import { getPresignedURL } from "@/lib/s3";
import { ContinuityItemSchema, RundownItemSchema } from "@bowser/prisma/types";
import {
ContinuityItemSchema,
MediaCreateInputSchema,
MediaFileSourceTypeSchema,
RundownItemSchema,
ShowCreateInputSchema,
} from "@bowser/prisma/types";
import { dispatchJobForJobrunner } from "@/lib/jobs";

const ExtendedMediaModelWithDownloadURL = CompleteMediaModel.extend({
continuityItem: ContinuityItemSchema.nullable(),
Expand Down Expand Up @@ -80,6 +87,35 @@ export const appRouter = router({
});
return obj;
}),
create: testOnlyProcedure
.input(ShowCreateInputSchema)
.output(CompleteShowModel)
.mutation(async ({ input }) => {
return await db.show.create({
data: input,
include: {
continuityItems: {
include: {
media: true,
},
},
rundowns: {
include: {
items: {
include: {
media: true,
},
},
assets: {
include: {
media: true,
},
},
},
},
},
});
}),
}),
media: router({
get: publicProcedure
Expand All @@ -103,6 +139,45 @@ export const appRouter = router({
}
return obj as z.infer<typeof ExtendedMediaModelWithDownloadURL>;
}),
create: testOnlyProcedure
.input(
z.object({
media: MediaCreateInputSchema,
sourceType: MediaFileSourceTypeSchema,
source: z.string(),
}),
)
.output(ExtendedMediaModelWithDownloadURL)
.mutation(async ({ input }) => {
const [media, job] = await db.$transaction(async ($db) => {
const media = await $db.media.create({
data: input.media,
include: {
rundownItem: true,
continuityItem: true,
tasks: true,
},
});
const job = await $db.processMediaJob.create({
data: {
sourceType: input.sourceType,
source: input.source,
media: {
connect: {
id: media.id,
},
},
base_job: { create: {} },
},
});
return [media, job] as const;
});
await dispatchJobForJobrunner(job.base_job_id);
return {
...media,
downloadURL: null,
};
}),
}),
rundowns: router({
get: publicProcedure
Expand Down
24 changes: 15 additions & 9 deletions utility/testing/MockOBSWebSocket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { type expect as vitestExpect } from "vitest";
import {
EventSubscription,
OBSEventTypes,
Expand All @@ -8,6 +7,10 @@ import {
import { AddressInfo, WebSocket, WebSocketServer } from "ws";
import { pEvent } from "p-event";
import * as msgpack from "@msgpack/msgpack";
import type { ExpectStatic as VitestExpect } from "vitest";
import type { Expect as PlaywrightExpect } from "@playwright/test";

type Expect = VitestExpect | PlaywrightExpect;

type OBSRequestHandler<
T extends keyof OBSRequestTypes = keyof OBSRequestTypes,
Expand All @@ -27,7 +30,7 @@ export class MockOBSContext {
((h: OBSRequestHandler) => void)[]
>();
constructor(
private readonly expect: typeof vitestExpect,
private readonly expect: Expect,
private readonly socket: OBSSocket,
public eventIntent: EventSubscription,
) {}
Expand Down Expand Up @@ -258,7 +261,7 @@ class OBSSocket {
export default class MockOBSWebSocket {
private openConnections = 0;
private constructor(
private readonly expect: typeof vitestExpect,
private readonly expect: Expect,
private readonly server: WebSocketServer,
public readonly port: number,
) {}
Expand All @@ -284,7 +287,7 @@ export default class MockOBSWebSocket {
});

public static async create(
expect: typeof vitestExpect,
expect: Expect,
actor?: (obs: MockOBSContext) => Promise<unknown>,
) {
const server = new WebSocketServer({
Expand All @@ -305,7 +308,10 @@ export default class MockOBSWebSocket {
socket = new OBSSocket(rawSocket, "msgpack");
break;
default:
expect.fail("unknown protocol " + rawSocket.protocol);
throw new Error(
"MockOBSWebSocket: unrecognised obs-websocket protocol " +
rawSocket.protocol,
);
}
socket.send({
op: 0,
Expand All @@ -330,15 +336,15 @@ export default class MockOBSWebSocket {
actorPromise = actor(ctx);
} else {
if (mows.openConnections > 0) {
expect.unreachable(
"only one connection supported without an actor function",
throw new Error(
"MockOBSWebSocket: test error: only one connection is supported without an actor function",
);
}
mows._ctx = ctx;
}
mows._ready();
socket.onMessage(async (payload: any) => {
expect(payload).toBeTypeOf("object");
expect(typeof payload).toBe("object");
switch (payload.op) {
case 3: // reidentify
ctx.eventIntent =
Expand All @@ -353,7 +359,7 @@ export default class MockOBSWebSocket {
);
break;
case 8: // request batch
expect.fail("request batch not handled yet");
throw new Error("MockOBSWebSocket: request batch not implemented");
}
});
socket.send({ op: 2, d: { negotiatedRpcVersion: 1 } });
Expand Down

0 comments on commit 6865f76

Please sign in to comment.