diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 1b7bdf9..41b1b58 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -35,16 +35,18 @@ jobs: .venv/bin/pip install --upgrade pip pip install uv uv sync --frozen - shell: bash - name: Install frontend dependencies working-directory: ./frontend - run: npm install - shell: bash + run: | + npm install + npx playwright install --with-deps - name: Run QA checks run: make qa - shell: bash + + #- name: Run end-to-end tests + # run: make e2e-test - name: Upload coverage reports uses: actions/upload-artifact@v3 @@ -53,3 +55,4 @@ jobs: path: | server/htmlcov frontend/coverage + frontend/playwright-report diff --git a/Makefile b/Makefile index b3455ca..fa23948 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ MYPY=mypy ISORT=isort PYTEST=pytest chatgptserver PYCOVERAGE=coverage run -m pytest chatgptserver && coverage report --fail-under=95 && coverage html -DJANGO_MANAGE=chatgptserver/manage.py +DJANGO_RUNSERVER=chatgptserver/manage.py runserver OPENAPI_SCHEMA=chatgptserver/manage.py spectacular --color --validate --file schema.yml # Frontend tools @@ -35,12 +35,13 @@ ESLINT=npm run lint:fix PRETTIER=npm run format TSC=npm run build JEST=npm run test -START=npm start +NEXT_START=npm run start +PLAYWRIGHT=npm run test:e2e # Targets .PHONY: all lint format type-check backend-lint backend-format \ backend-type-check frontend-lint frontend-format backend-validate-api-schema \ - frontend-type-check test backend-test frontend-test qa \ + frontend-type-check test backend-test frontend-test e2e-test qa \ run-backend run-frontend run # Run linters for both backend and frontend @@ -97,29 +98,37 @@ lint-markdown: @echo "$(COLOR_BLUE_BG)Linting markdown files...$(COLOR_RESET)" markdownlint README.md -# Run all QA tools -qa-frontend: frontend-lint frontend-format frontend-type-check frontend-test -qa-backend: backend-lint backend-format backend-type-check backend-test -qa: format lint type-check backend-validate-api-schema test - # Run backend server run-backend: @echo "$(COLOR_BLUE_BG)Running backend server...$(COLOR_RESET)" - cd $(SERVER_DIR) && $(VENV_ACTIVATE) && $(DJANGO_MANAGE) runserver + cd $(SERVER_DIR) && $(VENV_ACTIVATE) && $(DJANGO_RUNSERVER) & echo $$! > backend.pid # Run frontend server run-frontend: @echo "$(COLOR_BLUE_BG)Running frontend server...$(COLOR_RESET)" - cd $(FRONTEND_DIR) && $(START) + cd $(FRONTEND_DIR) && $(NEXT_START) & echo $$! > frontend.pid -# Run backend and frontend servers -run: - @$(MAKE) run-backend & +# Run QA checks +qa-frontend: frontend-lint frontend-format frontend-type-check frontend-test +qa-backend: backend-lint backend-format backend-type-check backend-test +qa: format lint type-check backend-validate-api-schema test + +# End-to-end testing with backend and frontend running +e2e-test: + @echo "$(COLOR_BLUE_BG)Starting backend and frontend services...$(COLOR_RESET)" + @$(MAKE) run-backend @$(MAKE) run-frontend + @sleep 10 # Wait for services to start (adjust this as necessary) + @echo "$(COLOR_BLUE_BG)Running end-to-end tests...$(COLOR_RESET)" + cd $(FRONTEND_DIR) && $(PLAYWRIGHT) -all: qa run # Run all services -run-services: +docker-up: @echo "$(COLOR_BLUE_BG)Running containerized services...$(COLOR_RESET)" docker-compose up --build --force-recreate + +# Stop all services +docker-down: + @echo "$(COLOR_BLUE_BG)Stopping containerized services...$(COLOR_RESET)" + docker-compose down diff --git a/frontend/.gitignore b/frontend/.gitignore index fd3dbb5..10280a2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -34,3 +34,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/client/helpers.ts b/frontend/client/helpers.ts new file mode 100644 index 0000000..784071a --- /dev/null +++ b/frontend/client/helpers.ts @@ -0,0 +1,19 @@ +import { AssistantModel, AssistantTemperature } from "./types/assistant"; + +const getModelDisplay = (model: AssistantModel) => + model === AssistantModel.FULL ? "GPT-4o" : "GPT-4o Mini"; + +const getTemperatureDisplay = (temperature: AssistantTemperature) => { + switch (temperature) { + case AssistantTemperature.DETERMINISTIC: + return "0.2 - More Deterministic"; + case AssistantTemperature.BALANCED: + return "0.7 - Balanced"; + case AssistantTemperature.CREATIVE: + return "0.9 - More Creative"; + default: + return ""; + } +}; + +export { getModelDisplay, getTemperatureDisplay }; diff --git a/frontend/components/parameters/AssistantModelParameter.tsx b/frontend/components/parameters/AssistantModelParameter.tsx index 16635db..6f899ad 100644 --- a/frontend/components/parameters/AssistantModelParameter.tsx +++ b/frontend/components/parameters/AssistantModelParameter.tsx @@ -2,6 +2,7 @@ import { Bot, ChevronDown } from "lucide-react"; import React, { useState } from "react"; import { AssistantModel } from "@/client/types/assistant"; +import { getModelDisplay } from "@/client/helpers"; import { Box, IconButton, Menu, MenuItem, Tooltip } from "@mui/material"; interface AssistantModelParameterProps { @@ -28,9 +29,6 @@ const AssistantModelParameter: React.FC = ({ handleClose(); }; - const getModelDisplay = () => - model === AssistantModel.FULL ? "GPT-4o" : "GPT-4o Mini"; - return ( <> @@ -39,7 +37,7 @@ const AssistantModelParameter: React.FC = ({ Choose Assistant Model
- (Current: {getModelDisplay()}) + (Current: {getModelDisplay(model)})
} > diff --git a/frontend/components/parameters/AssistantTemperatureParameter.tsx b/frontend/components/parameters/AssistantTemperatureParameter.tsx index 5a8872b..10b292e 100644 --- a/frontend/components/parameters/AssistantTemperatureParameter.tsx +++ b/frontend/components/parameters/AssistantTemperatureParameter.tsx @@ -2,6 +2,8 @@ import { ChevronDown, Thermometer } from "lucide-react"; import React, { useState } from "react"; import { AssistantTemperature } from "@/client/types/assistant"; +import { getTemperatureDisplay } from "@/client/helpers"; + import { Box, IconButton, Menu, MenuItem, Tooltip } from "@mui/material"; interface AssistantTemperatureParameterProps { @@ -27,19 +29,6 @@ const AssistantTemperatureParameter: React.FC< handleClose(); }; - const getTemperatureDisplay = () => { - switch (temperature) { - case AssistantTemperature.DETERMINISTIC: - return "0.2 - More Deterministic"; - case AssistantTemperature.BALANCED: - return "0.7 - Balanced"; - case AssistantTemperature.CREATIVE: - return "0.9 - More Creative"; - default: - return ""; - } - }; - return ( <> @@ -48,7 +37,7 @@ const AssistantTemperatureParameter: React.FC< Choose Temperature
- (Current: {getTemperatureDisplay()}) + (Current: {getTemperatureDisplay(temperature)})
} > diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index a6b2f43..6566421 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -32,6 +32,9 @@ const config: Config = { // The test environment that will be used for testing testEnvironment: "jsdom", + // Don't run Playwright e2e tests + testPathIgnorePatterns: ["tests"], + // Indicates whether each individual test should be reported during the run verbose: true, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2a70b81..5134ed1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "swr": "^2.2.5" }, "devDependencies": { + "@playwright/test": "^1.48.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -2363,6 +2364,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -11701,6 +11718,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a21a829..d97c310 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,8 @@ "format": "prettier --write .", "format:check": "prettier --check .", "test": "jest", - "test:update": "jest -u" + "test:update": "jest -u", + "test:e2e": "npx playwright test" }, "dependencies": { "@emotion/react": "^11.13.3", @@ -30,6 +31,7 @@ "swr": "^2.2.5" }, "devDependencies": { + "@playwright/test": "^1.48.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..1dfeeca --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: false, + /* 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: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* 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:3000", + + /* 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"] }, + }, + + //{ + // name: "firefox", + // use: { ...devices["Desktop Firefox"] }, + //}, + + //{ + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + //}, + + ///* Test against mobile viewports. */ + //{ + // name: "Mobile Chrome", + // use: { ...devices["Pixel 5"] }, + //}, + //{ + // name: "Mobile Safari", + // use: { ...devices["iPhone 12"] }, + //}, + + ///* Test against branded browsers. */ + //{ + // name: "Microsoft Edge", + // use: { ...devices["Desktop Edge"], channel: "msedge" }, + //}, + //{ + // name: "Google Chrome", + // use: { ...devices["Desktop Chrome"], channel: "chrome" }, + //}, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/frontend/tests/chat-page.spec.ts b/frontend/tests/chat-page.spec.ts new file mode 100644 index 0000000..dae8c3a --- /dev/null +++ b/frontend/tests/chat-page.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from "@playwright/test"; + +import { AssistantModel, AssistantTemperature } from "@/client/types/assistant"; +import { getModelDisplay, getTemperatureDisplay } from "@/client/helpers"; + +const models = [AssistantModel.MINI, AssistantModel.FULL]; +const temperatures = [AssistantTemperature.DETERMINISTIC]; + +test.describe("Chat Page Model and Temperature Combinations", () => { + test("should return correct response for selected model and temperature", async ({ + page, + }) => { + await page.goto("/chat"); + + // Verify the initial assistant message is present + await expect( + page.getByText("Hi, I am a chat bot. How can I help you today?") + ).toBeVisible(); + + // Set the model and temperature (using selectors or any UI interaction method you have) + // These are placeholder selectors; adjust according to your actual component structure + await page.getByRole("button").first().click(); + await page + .getByRole("menuitem", { + name: getModelDisplay(AssistantModel.MINI), + exact: true, + }) + .click(); + await page.getByRole("button").nth(1).click(); + await page + .getByRole("menuitem", { + name: getTemperatureDisplay(AssistantTemperature.DETERMINISTIC), + exact: true, + }) + .click(); + await page.getByPlaceholder("Type your message...").click(); + // Send a user message + await page.getByPlaceholder("Type your message...").click(); + await page.getByPlaceholder("Type your message...").fill("test message"); + await page.locator("form").getByRole("button").click(); + + await page.fill('input[type="text"]', "Test message"); + await page.keyboard.press("Enter"); + + const expectedResponse = "Test received! How can I assist you today?"; + await expect(page.getByText(expectedResponse)).toBeVisible(); + }); +}); diff --git a/server/api-manual-testing/gpt-4o.sh b/server/api-manual-testing/gpt-4o.sh index 87594ab..b27e788 100644 --- a/server/api-manual-testing/gpt-4o.sh +++ b/server/api-manual-testing/gpt-4o.sh @@ -1,40 +1,40 @@ #!/bin/bash # valid curl -X 'POST' \ - 'http://127.0.0.1:8000/api/v1/chat/gpt-4o/?temperature=balanced' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ - -d '{ + 'http://localhost:8000/api/v1/chat/gpt-4o/?temperature=balanced' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ + -d '{ "prompt": "what is AI?" }' # invalid: wrong model name curl -X 'POST' \ - 'http://127.0.0.1:8000/api/v1/chat/gpt-bla/?temperature=balanced' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ - -d '{ + 'http://localhost:8000/api/v1/chat/gpt-bla/?temperature=balanced' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ + -d '{ "prompt": "what is AI?" }' # invalid: wrong temperature curl -X 'POST' \ - 'http://127.0.0.1:8000/api/v1/chat/gpt-4o/?temperature=hot' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ - -d '{ + 'http://localhost:8000/api/v1/chat/gpt-4o/?temperature=hot' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ + -d '{ "prompt": "what is AI?" }' # invalid: missing prompt curl -X 'POST' \ - 'http://127.0.0.1:8000/api/v1/chat/gpt-4o/?temperature=balanced' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ - -d '{ + 'http://localhost:8000/api/v1/chat/gpt-4o/?temperature=balanced' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H 'X-CSRFTOKEN: 2m4lWrUeSyNpMX7tGw8wmaqWNUkI2xc0wQo7xaH2OfKmpZYUN27izfheYDceUSMw' \ + -d '{ "prompt": "" }' diff --git a/server/chatgptserver/chatgptserver/settings.py b/server/chatgptserver/chatgptserver/settings.py index f0688a9..38dfb01 100644 --- a/server/chatgptserver/chatgptserver/settings.py +++ b/server/chatgptserver/chatgptserver/settings.py @@ -45,6 +45,7 @@ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] CORS_ORIGIN_ALLOW_ALL = True CORS_ALLOWED_ORIGINS = [ "http://localhost:3000",