feat(frontend): Redirect user to app after a project upload or repo selection (and add e2e tests) (#4751)

This commit is contained in:
sp.wack 2024-11-05 17:12:58 +02:00 committed by GitHub
parent eeb2342509
commit 6eafe0d2a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 253 additions and 8 deletions

5
frontend/.gitignore vendored
View File

@ -2,3 +2,8 @@
public/locales/**/*
src/i18n/declaration.ts
.env
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -46,6 +46,7 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
@ -3379,6 +3380,21 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
"dev": true,
"dependencies": {
"playwright": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
@ -19422,6 +19438,50 @@
"pathe": "^1.1.2"
}
},
"node_modules/playwright": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
"dev": true,
"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,
"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",

View File

@ -50,6 +50,7 @@
"build": "npm run make-i18n && tsc && remix vite:build",
"start": "npx sirv-cli build/ --single",
"test": "vitest run",
"test:e2e": "playwright test",
"test:coverage": "npm run make-i18n && vitest run --coverage",
"dev_wsl": "VITE_WATCH_USE_POLLING=true vite",
"preview": "vite preview",
@ -71,6 +72,7 @@
]
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",

View File

@ -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: true,
/* 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: process.env.CI ? 1 : undefined,
/* 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://127.0.0.1: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 dev:mock -- --port 3000",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});

View File

@ -2,6 +2,7 @@ import clsx from "clsx";
import React from "react";
interface ModalButtonProps {
testId?: string;
variant?: "default" | "text-like";
onClick?: () => void;
text: string;
@ -13,6 +14,7 @@ interface ModalButtonProps {
}
function ModalButton({
testId,
variant = "default",
onClick,
text,
@ -24,6 +26,7 @@ function ModalButton({
}: ModalButtonProps) {
return (
<button
data-testid={testId}
type={type === "submit" ? "submit" : "button"}
disabled={disabled}
onClick={onClick}

View File

@ -53,6 +53,7 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
<div className="flex flex-col gap-2 w-full">
<ModalButton
testId="connect-to-github"
type="submit"
text="Connect"
disabled={fetcher.state === "submitting"}

View File

@ -1,7 +1,7 @@
import { delay, http, HttpResponse } from "msw";
const openHandsHandlers = [
http.get("http://localhost:3001/api/options/models", async () => {
http.get("/api/options/models", async () => {
await delay();
return HttpResponse.json([
"gpt-3.5-turbo",
@ -10,12 +10,12 @@ const openHandsHandlers = [
]);
}),
http.get("http://localhost:3001/api/options/agents", async () => {
http.get("/api/options/agents", async () => {
await delay();
return HttpResponse.json(["CodeActAgent", "CoActAgent"]);
}),
http.get("http://localhost:3001/api/options/security-analyzers", async () => {
http.get("/api/options/security-analyzers", async () => {
await delay();
return HttpResponse.json(["mock-invariant"]);
}),
@ -71,7 +71,7 @@ const openHandsHandlers = [
export const handlers = [
...openHandsHandlers,
http.get("https://api.github.com/user/repos", async ({ request }) => {
await delay(3500);
if (import.meta.env.MODE !== "test") await delay(3500);
const token = request.headers
.get("Authorization")
@ -87,10 +87,20 @@ export const handlers = [
{ id: 2, full_name: "octocat/earth" },
]);
}),
http.get("https://api.github.com/user", () => {
const user: GitHubUser = {
id: 1,
login: "octocat",
avatar_url: "https://avatars.githubusercontent.com/u/583231?v=4",
};
return HttpResponse.json(user);
}),
http.post("http://localhost:3001/api/submit-feedback", async () =>
HttpResponse.json({ statusCode: 200 }, { status: 200 }),
),
http.post("https://us.i.posthog.com/e", async () =>
HttpResponse.json(null, { status: 200 }),
),
http.get("/config.json", () => HttpResponse.json({ APP_MODE: "oss" })),
];

View File

@ -1,5 +1,6 @@
import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setSelectedRepository } from "#/state/initial-query-slice";
interface GitHubRepositorySelectorProps {
@ -9,6 +10,7 @@ interface GitHubRepositorySelectorProps {
export function GitHubRepositorySelector({
repositories,
}: GitHubRepositorySelectorProps) {
const navigate = useNavigate();
const dispatch = useDispatch();
const handleRepoSelection = (id: string | null) => {
@ -16,6 +18,7 @@ export function GitHubRepositorySelector({
if (repo) {
// set query param
dispatch(setSelectedRepository(repo.full_name));
navigate("/app");
}
};
@ -26,6 +29,7 @@ export function GitHubRepositorySelector({
return (
<Autocomplete
data-testid="github-repo-selector"
name="repo"
aria-label="GitHub Repository"
placeholder="Select a GitHub project"
@ -39,7 +43,11 @@ export function GitHubRepositorySelector({
clearButtonProps={{ onClick: handleClearSelection }}
>
{repositories.map((repo) => (
<AutocompleteItem key={repo.id} value={repo.id}>
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
>
{repo.full_name}
</AutocompleteItem>
))}

View File

@ -5,6 +5,7 @@ import {
defer,
redirect,
useLoaderData,
useNavigate,
useRouteLoaderData,
} from "@remix-run/react";
import React, { Suspense } from "react";
@ -62,12 +63,16 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
};
function Home() {
const navigate = useNavigate();
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const { repositories, githubAuthUrl } = useLoaderData<typeof clientLoader>();
const [importedFile, setImportedFile] = React.useState<File | null>(null);
return (
<div className="bg-root-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto">
<div
data-testid="root-index"
className="bg-root-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto"
>
<HeroHeading />
<div className="flex flex-col gap-16 w-[600px] items-center">
<div className="flex flex-col gap-2 w-full">
@ -113,6 +118,7 @@ function Home() {
if (event.target.files) {
const zip = event.target.files[0];
setImportedFile(zip);
navigate("/app");
} else {
// TODO: handle error
}

View File

@ -229,7 +229,10 @@ export default function MainApp() {
};
return (
<div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
<div
data-testid="root-layout"
className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3"
>
<aside className="px-1 flex flex-col gap-1">
<div className="w-[34px] h-[34px] flex items-center justify-center">
{navigation.state === "loading" && <LoadingSpinner size="small" />}

View File

@ -6,6 +6,7 @@ import { configureStore } from "@reduxjs/toolkit";
// eslint-disable-next-line import/no-extraneous-dependencies
import { RenderOptions, render } from "@testing-library/react";
import { AppStore, RootState, rootReducer } from "./src/store";
import { SocketProvider } from "#/context/socket";
const setupStore = (preloadedState?: Partial<RootState>): AppStore =>
configureStore({
@ -32,7 +33,11 @@ export function renderWithProviders(
}: ExtendedRenderOptions = {},
) {
function Wrapper({ children }: PropsWithChildren<object>): JSX.Element {
return <Provider store={store}>{children}</Provider>;
return (
<Provider store={store}>
<SocketProvider>{children}</SocketProvider>
</Provider>
);
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

0
frontend/tests/fixtures/project.zip vendored Normal file
View File

View File

@ -0,0 +1,61 @@
import { expect, Page, test } from "@playwright/test";
import path from "path";
import { fileURLToPath } from "url";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
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,
});
await configSaveButton.click();
const confirmChanges = page.getByRole("button", {
name: /yes, close settings/i,
});
await confirmChanges.click();
};
test("should redirect to /app after uploading a project zip", async ({
page,
}) => {
await page.goto("/");
const fileInput = page.getByLabel("Upload a .zip");
const filePath = path.join(dirname, "fixtures/project.zip");
await fileInput.setInputFiles(filePath);
await page.waitForURL("/app");
});
test("should redirect to /app after selecting a repo", async ({ page }) => {
await page.goto("/");
await confirmSettings(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("/app");
expect(page.url()).toBe("http://127.0.0.1:3000/app");
});

View File

@ -5,6 +5,7 @@ import { defineConfig, loadEnv } from "vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
import svgr from "vite-plugin-svgr";
import { vitePlugin as remix } from "@remix-run/dev";
import { configDefaults } from "vitest/config";
export default defineConfig(({ mode }) => {
const {
@ -90,6 +91,7 @@ export default defineConfig(({ mode }) => {
test: {
environment: "jsdom",
setupFiles: ["vitest.setup.ts"],
exclude: [...configDefaults.exclude, "tests"],
coverage: {
reporter: ["text", "json", "html", "lcov", "text-summary"],
reportsDirectory: "coverage",