From 608d6e7fa10cd0c7cf36085786ae947708b0d371 Mon Sep 17 00:00:00 2001 From: amanape <83104063+amanape@users.noreply.github.com> Date: Tue, 9 Dec 2025 19:37:00 +0400 Subject: [PATCH] Add frontend E2E workflow and remove old Playwright tests --- .github/workflows/fe-e2e-tests.yml | 47 +++++++ frontend/tests/conversation-panel.test.ts | 134 -------------------- frontend/tests/fixtures/project.zip | 0 frontend/tests/helpers/confirm-settings.ts | 20 --- frontend/tests/redirect.spec.ts | 66 ---------- frontend/tests/repo-selection-form.test.tsx | 130 ------------------- frontend/tests/settings.spec.ts | 17 --- 7 files changed, 47 insertions(+), 367 deletions(-) create mode 100644 .github/workflows/fe-e2e-tests.yml delete mode 100644 frontend/tests/conversation-panel.test.ts delete mode 100644 frontend/tests/fixtures/project.zip delete mode 100644 frontend/tests/helpers/confirm-settings.ts delete mode 100644 frontend/tests/redirect.spec.ts delete mode 100644 frontend/tests/repo-selection-form.test.tsx delete mode 100644 frontend/tests/settings.spec.ts diff --git a/.github/workflows/fe-e2e-tests.yml b/.github/workflows/fe-e2e-tests.yml new file mode 100644 index 0000000000..7ee79e63fc --- /dev/null +++ b/.github/workflows/fe-e2e-tests.yml @@ -0,0 +1,47 @@ +# Workflow that runs frontend e2e tests with Playwright +name: Run Frontend E2E Tests + +on: + push: + branches: + - main + pull_request: + paths: + - "frontend/**" + - ".github/workflows/fe-e2e-tests.yml" + +concurrency: + group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }} + cancel-in-progress: true + +jobs: + fe-e2e-test: + name: FE E2E Tests + runs-on: blacksmith-4vcpu-ubuntu-2204 + strategy: + matrix: + node-version: [22] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node.js + uses: useblacksmith/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + working-directory: ./frontend + run: npm ci + - name: Install Playwright browsers + working-directory: ./frontend + run: npx playwright install --with-deps chromium + - name: Run Playwright tests + working-directory: ./frontend + run: npx playwright test --project=chromium + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: frontend/playwright-report/ + retention-days: 30 diff --git a/frontend/tests/conversation-panel.test.ts b/frontend/tests/conversation-panel.test.ts deleted file mode 100644 index 6e3f58cd45..0000000000 --- a/frontend/tests/conversation-panel.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import test, { expect, Page } from "@playwright/test"; - -const toggleConversationPanel = async (page: Page) => { - const panel = page.getByTestId("conversation-panel"); - await page.waitForTimeout(1000); // Wait for state to stabilize - const panelIsVisible = await panel.isVisible(); - - if (!panelIsVisible) { - const conversationPanelButton = page.getByTestId( - "toggle-conversation-panel", - ); - await conversationPanelButton.click(); - } - - return page.getByTestId("conversation-panel"); -}; - -const selectConversationCard = async (page: Page, index: number) => { - const panel = await toggleConversationPanel(page); - - // select a conversation - const conversationItem = panel.getByTestId("conversation-card").nth(index); - await conversationItem.click(); - - // panel should close - await expect(panel).not.toBeVisible(); - - await page.waitForURL(`/conversations/${index + 1}`); - expect(page.url()).toBe(`http://localhost:3001/conversations/${index + 1}`); -}; - -test.beforeEach(async ({ page }) => { - await page.goto("/"); -}); - -test("should only display the create new conversation button when in a conversation", async ({ - page, -}) => { - const panel = page.getByTestId("conversation-panel"); - - const newProjectButton = panel.getByTestId("new-conversation-button"); - await expect(newProjectButton).not.toBeVisible(); - - await page.goto("/conversations/1"); - await expect(newProjectButton).toBeVisible(); -}); - -test("redirect to /conversation with the session id as a path param when clicking on a conversation card", async ({ - page, -}) => { - const panel = page.getByTestId("conversation-panel"); - - // select a conversation - const conversationItem = panel.getByTestId("conversation-card").first(); - await conversationItem.click(); - - // panel should close - expect(panel).not.toBeVisible(); - - await page.waitForURL("/conversations/1"); - expect(page.url()).toBe("http://localhost:3001/conversations/1"); -}); - -test("redirect to the home screen if the current session was deleted", async ({ - page, -}) => { - await page.goto("/conversations/1"); - await page.waitForURL("/conversations/1"); - - const panel = page.getByTestId("conversation-panel"); - const firstCard = panel.getByTestId("conversation-card").first(); - - const ellipsisButton = firstCard.getByTestId("ellipsis-button"); - await ellipsisButton.click(); - - const deleteButton = firstCard.getByTestId("delete-button"); - await deleteButton.click(); - - // confirm modal - const confirmButton = page.getByText("Confirm"); - await confirmButton.click(); - - await page.waitForURL("/"); -}); - -test("load relevant files in the file explorer", async ({ page }) => { - await selectConversationCard(page, 0); - - // check if the file explorer has the correct files - const fileExplorer = page.getByTestId("file-explorer"); - - await expect(fileExplorer.getByText("file1.txt")).toBeVisible(); - await expect(fileExplorer.getByText("file2.txt")).toBeVisible(); - await expect(fileExplorer.getByText("file3.txt")).toBeVisible(); - - await selectConversationCard(page, 2); - - // check if the file explorer has the correct files - expect(fileExplorer.getByText("reboot_skynet.exe")).toBeVisible(); - expect(fileExplorer.getByText("target_list.txt")).toBeVisible(); - expect(fileExplorer.getByText("terminator_blueprint.txt")).toBeVisible(); -}); - -test("should redirect to home screen if conversation deos not exist", async ({ - page, -}) => { - await page.goto("/conversations/9999"); - await page.waitForURL("/"); -}); - -test("display the conversation details during a conversation", async ({ - page, -}) => { - const conversationPanelButton = page.getByTestId("toggle-conversation-panel"); - await expect(conversationPanelButton).toBeVisible(); - await conversationPanelButton.click(); - - const panel = page.getByTestId("conversation-panel"); - - // select a conversation - const conversationItem = panel.getByTestId("conversation-card").first(); - await conversationItem.click(); - - // panel should close - await expect(panel).not.toBeVisible(); - - await page.waitForURL("/conversations/1"); - expect(page.url()).toBe("http://localhost:3001/conversations/1"); - - const conversationDetails = page.getByTestId("conversation-card"); - - await expect(conversationDetails).toBeVisible(); - await expect(conversationDetails).toHaveText("Conversation 1"); -}); diff --git a/frontend/tests/fixtures/project.zip b/frontend/tests/fixtures/project.zip deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/tests/helpers/confirm-settings.ts b/frontend/tests/helpers/confirm-settings.ts deleted file mode 100644 index ca82edd35a..0000000000 --- a/frontend/tests/helpers/confirm-settings.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Page } from "@playwright/test"; - -export const confirmSettings = async (page: Page) => { - const confirmPreferenceButton = page.getByRole("button", { - name: /confirm preferences/i, - }); - await confirmPreferenceButton.click(); - - const configSaveButton = page - .getByRole("button", { - name: /save/i, - }) - .first(); - await configSaveButton.click(); - - const confirmChanges = page.getByRole("button", { - name: /yes, close settings/i, - }); - await confirmChanges.click(); -}; diff --git a/frontend/tests/redirect.spec.ts b/frontend/tests/redirect.spec.ts deleted file mode 100644 index 8425345ba6..0000000000 --- a/frontend/tests/redirect.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { expect, test } from "@playwright/test"; -import path from "path"; -import { fileURLToPath } from "url"; - -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); - -test.beforeEach(async ({ page }) => { - await page.goto("/"); -}); - -test("should redirect to /conversations after uploading a project zip", async ({ - page, -}) => { - const fileInput = page.getByLabel("Upload a .zip"); - const filePath = path.join(dirname, "fixtures/project.zip"); - await fileInput.setInputFiles(filePath); - - await page.waitForURL(/\/conversations\/\d+/); -}); - -test("should redirect to /conversations after selecting a repo", async ({ - page, -}) => { - // enter a github token to view the repositories - const connectToGitHubButton = page.getByRole("button", { - name: /connect to github/i, - }); - await connectToGitHubButton.click(); - const tokenInput = page.getByLabel(/github token\*/i); - await tokenInput.fill("fake-token"); - - const submitButton = page.getByTestId("connect-to-github"); - await submitButton.click(); - - // select a repository - const repoDropdown = page.getByLabel(/github repository/i); - await repoDropdown.click(); - - const repoItem = page.getByTestId("github-repo-item").first(); - await repoItem.click(); - - await page.waitForURL(/\/conversations\/\d+/); -}); - -// FIXME: This fails because the MSW WS mocks change state too quickly, -// missing the OPENING status where the initial query is rendered. -test.skip("should redirect the user to /conversation with their initial query after selecting a project", async ({ - page, -}) => { - // enter query - const testQuery = "this is my test query"; - const textbox = page.getByPlaceholder(/what do you want to build/i); - expect(textbox).not.toBeNull(); - await textbox.fill(testQuery); - - const fileInput = page.getByLabel("Upload a .zip"); - const filePath = path.join(dirname, "fixtures/project.zip"); - await fileInput.setInputFiles(filePath); - - await page.waitForURL("/conversation"); - - // get user message - const userMessage = page.getByTestId("user-message"); - expect(await userMessage.textContent()).toBe(testQuery); -}); diff --git a/frontend/tests/repo-selection-form.test.tsx b/frontend/tests/repo-selection-form.test.tsx deleted file mode 100644 index 24666d49fc..0000000000 --- a/frontend/tests/repo-selection-form.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { RepositorySelectionForm } from "../src/components/features/home/repo-selection-form"; -import { useUserRepositories } from "../src/hooks/query/use-user-repositories"; -import { useRepositoryBranches } from "../src/hooks/query/use-repository-branches"; -import { useCreateConversation } from "../src/hooks/mutation/use-create-conversation"; -import { useIsCreatingConversation } from "../src/hooks/use-is-creating-conversation"; - -// Mock the hooks -vi.mock("../src/hooks/query/use-user-repositories"); -vi.mock("../src/hooks/query/use-repository-branches"); -vi.mock("../src/hooks/mutation/use-create-conversation"); -vi.mock("../src/hooks/use-is-creating-conversation"); -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -describe("RepositorySelectionForm", () => { - const mockOnRepoSelection = vi.fn(); - - beforeEach(() => { - vi.resetAllMocks(); - - // Mock the hooks with default values - (useUserRepositories as any).mockReturnValue({ - data: [ - { id: "1", full_name: "test/repo1" }, - { id: "2", full_name: "test/repo2" } - ], - isLoading: false, - isError: false, - }); - - (useRepositoryBranches as any).mockReturnValue({ - data: [ - { name: "main" }, - { name: "develop" } - ], - isLoading: false, - isError: false, - }); - - (useCreateConversation as any).mockReturnValue({ - mutate: vi.fn(() => (useIsCreatingConversation as any).mockReturnValue(true)), - isPending: false, - isSuccess: false, - }); - - (useIsCreatingConversation as any).mockReturnValue(false); - }); - - it("should clear selected branch when input is empty", async () => { - render(); - - // First select a repository to enable the branch dropdown - const repoDropdown = screen.getByTestId("repository-dropdown"); - fireEvent.change(repoDropdown, { target: { value: "test/repo1" } }); - - // Get the branch dropdown and verify it's enabled - const branchDropdown = screen.getByTestId("branch-dropdown"); - expect(branchDropdown).not.toBeDisabled(); - - // Simulate deleting all text in the branch input - fireEvent.change(branchDropdown, { target: { value: "" } }); - - // Verify the branch input is cleared (no selected branch) - expect(branchDropdown).toHaveValue(""); - }); - - it("should clear selected branch when input contains only whitespace", async () => { - render(); - - // First select a repository to enable the branch dropdown - const repoDropdown = screen.getByTestId("repository-dropdown"); - fireEvent.change(repoDropdown, { target: { value: "test/repo1" } }); - - // Get the branch dropdown and verify it's enabled - const branchDropdown = screen.getByTestId("branch-dropdown"); - expect(branchDropdown).not.toBeDisabled(); - - // Simulate entering only whitespace in the branch input - fireEvent.change(branchDropdown, { target: { value: " " } }); - - // Verify the branch input is cleared (no selected branch) - expect(branchDropdown).toHaveValue(""); - }); - - it("should keep branch empty after being cleared even with auto-selection", async () => { - render(); - - // First select a repository to enable the branch dropdown - const repoDropdown = screen.getByTestId("repository-dropdown"); - fireEvent.change(repoDropdown, { target: { value: "test/repo1" } }); - - // Get the branch dropdown and verify it's enabled - const branchDropdown = screen.getByTestId("branch-dropdown"); - expect(branchDropdown).not.toBeDisabled(); - - // The branch should be auto-selected to "main" initially - expect(branchDropdown).toHaveValue("main"); - - // Simulate deleting all text in the branch input - fireEvent.change(branchDropdown, { target: { value: "" } }); - - // Verify the branch input is cleared (no selected branch) - expect(branchDropdown).toHaveValue(""); - - // Trigger a re-render by changing something else - fireEvent.change(repoDropdown, { target: { value: "test/repo2" } }); - fireEvent.change(repoDropdown, { target: { value: "test/repo1" } }); - - // The branch should be auto-selected to "main" again after repo change - expect(branchDropdown).toHaveValue("main"); - - // Clear it again - fireEvent.change(branchDropdown, { target: { value: "" } }); - - // Verify it stays empty - expect(branchDropdown).toHaveValue(""); - - // Simulate a component update without changing repos - // This would normally trigger the useEffect if our fix wasn't working - fireEvent.blur(branchDropdown); - - // Verify it still stays empty - expect(branchDropdown).toHaveValue(""); - }); -}); diff --git a/frontend/tests/settings.spec.ts b/frontend/tests/settings.spec.ts deleted file mode 100644 index e4c4ce3b35..0000000000 --- a/frontend/tests/settings.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import test, { expect } from "@playwright/test"; - -test("do not navigate to /settings/billing if not SaaS mode", async ({ - page, -}) => { - await page.goto("/settings/billing"); - await expect(page.getByTestId("settings-screen")).toBeVisible(); - expect(page.url()).toBe("http://localhost:3001/settings"); -}); - -// FIXME: This test is failing because the config is not being set to SaaS mode -// since MSW is always returning APP_MODE as "oss" -test.skip("navigate to /settings/billing if SaaS mode", async ({ page }) => { - await page.goto("/settings/billing"); - await expect(page.getByTestId("settings-screen")).toBeVisible(); - expect(page.url()).toBe("http://localhost:3001/settings/billing"); -});