Skip to content

WIP add testing #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Get api key and create an assistant at https://platform.openai.com/
### OPENAI ###
# Get api key at https://platform.openai.com/
OPEN_AI_API_KEY=
OPENAI_ASST_ID_CREATE_GAME=

### REDIS ###
# Get a token and db url. See https://upstash.com/docs/redis/overall/getstarted
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@
!.env.example

# Misc
tmp
tmp

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

public/mock*

# Test
coverage/
175 changes: 175 additions & 0 deletions app/.server/openai/getAssistantOutput.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getAsstOutput } from "./getAssistantOutput";

// Mock the entire openai module
vi.mock("./openai", () => {
const mockCreate = vi.fn();
return {
openai: {
beta: {
threads: {
runs: {
create: mockCreate,
},
},
},
},
};
});

// Import the mocked module
import { openai } from "./openai";

describe("getAsstOutput", () => {
const mockThreadId = "thread-123";
const mockAsstId = "asst-123";
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

beforeEach(() => {
vi.clearAllMocks();
consoleSpy.mockClear();
});

afterAll(() => {
consoleSpy.mockRestore();
});

it("should successfully process streaming response", async () => {
// Mock streaming response
const mockStream = [
{
event: "thread.message.delta",
data: {
delta: {
content: [
{
text: {
value: '{"key":',
},
},
],
},
},
},
{
event: "thread.message.delta",
data: {
delta: {
content: [
{
text: {
value: '"value"}',
},
},
],
},
},
},
];

// Create async iterator for mock stream
const mockAsyncIterator = {
async *[Symbol.asyncIterator]() {
for (const chunk of mockStream) {
yield chunk;
}
},
};

// Setup mock
openai.beta.threads.runs.create = vi
.fn()
.mockResolvedValue(mockAsyncIterator);

const result = await getAsstOutput({
threadId: mockThreadId,
asstId: mockAsstId,
});

expect(result).toEqual({ key: "value" });
expect(openai.beta.threads.runs.create).toHaveBeenCalledWith(mockThreadId, {
assistant_id: mockAsstId,
stream: true,
});
});

it("should handle errors gracefully", async () => {
// Setup mock to throw error
openai.beta.threads.runs.create = vi
.fn()
.mockRejectedValue(new Error("API Error"));

const result = await getAsstOutput({
threadId: mockThreadId,
asstId: mockAsstId,
});

expect(result).toBe("");
expect(openai.beta.threads.runs.create).toHaveBeenCalledWith(mockThreadId, {
assistant_id: mockAsstId,
stream: true,
});
expect(consoleSpy).toHaveBeenCalledWith("ERROR: ", expect.any(Error));
});

it("should handle invalid JSON response", async () => {
// Mock streaming response with invalid JSON
const mockStream = [
{
event: "thread.message.delta",
data: {
delta: {
content: [
{
text: {
value: "invalid json",
},
},
],
},
},
},
];

const mockAsyncIterator = {
async *[Symbol.asyncIterator]() {
for (const chunk of mockStream) {
yield chunk;
}
},
};

openai.beta.threads.runs.create = vi
.fn()
.mockResolvedValue(mockAsyncIterator);

const result = await getAsstOutput({
threadId: mockThreadId,
asstId: mockAsstId,
});

expect(result).toBe("");
expect(consoleSpy).toHaveBeenCalledWith("ERROR: ", expect.any(SyntaxError));
});

it("should handle empty stream response", async () => {
// Mock empty stream
const mockAsyncIterator = {
async *[Symbol.asyncIterator]() {
// Empty iterator
},
};

openai.beta.threads.runs.create = vi
.fn()
.mockResolvedValue(mockAsyncIterator);

const result = await getAsstOutput({
threadId: mockThreadId,
asstId: mockAsstId,
});

expect(result).toBe("");
expect(consoleSpy).toHaveBeenCalledWith("ERROR: ", expect.any(SyntaxError));
});
});
1 change: 1 addition & 0 deletions app/.server/openai/getAssistantOutput.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AsstDataRequestInput } from "~/types/assistant";
import { openai } from "./openai";

// TODO: add vitest - mock openai
export async function getAsstOutput({
threadId,
asstId,
Expand Down
59 changes: 59 additions & 0 deletions app/.server/utils/getHostUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { getHostUrl } from "./getHostUrl";

describe("getHostUrl", () => {
it("should use X-Forwarded-Host header when available", () => {
const request = new Request("https://example.com", {
headers: {
"X-Forwarded-Host": "forwarded.example.com",
},
});

expect(getHostUrl(request)).toBe("https://forwarded.example.com");
});

it("should fallback to host header when X-Forwarded-Host is not available", () => {
const request = new Request("https://example.com", {
headers: {
host: "host.example.com",
},
});

expect(getHostUrl(request)).toBe("https://host.example.com");
});

it("should fallback to URL host when no headers are available", () => {
const request = new Request("https://url.example.com");

expect(getHostUrl(request)).toBe("https://url.example.com");
});

it("should use http protocol for localhost", () => {
const request = new Request("http://localhost:3000");

expect(getHostUrl(request)).toBe("http://localhost:3000");
});

it("should use https protocol for non-localhost domains", () => {
const request = new Request("http://example.com");

expect(getHostUrl(request)).toBe("https://example.com");
});

it("should handle localhost with different ports", () => {
const request = new Request("http://localhost:8080");

expect(getHostUrl(request)).toBe("http://localhost:8080");
});

it("should prioritize headers correctly", () => {
const request = new Request("https://url.example.com", {
headers: {
"X-Forwarded-Host": "forwarded.example.com",
host: "host.example.com",
},
});

expect(getHostUrl(request)).toBe("https://forwarded.example.com");
});
});
9 changes: 9 additions & 0 deletions app/.server/utils/getHostUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// src: epic stack
export function getHostUrl(request: Request) {
const host =
request.headers.get("X-Forwarded-Host") ??
request.headers.get("host") ??
new URL(request.url).host;
const protocol = host.includes("localhost") ? "http" : "https";
return `${protocol}://${host}`;
}
8 changes: 5 additions & 3 deletions app/app.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

@plugin "daisyui" {
themes: light --default, dark --prefersdark, lemonade;
}

html,
body {
Expand Down
18 changes: 18 additions & 0 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

async function prepareApp() {
return Promise.resolve();
}

prepareApp().then(() => {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
});
Loading
Loading