mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(frontend): Redirect user to app after a project upload or repo selection (and add e2e tests) (#4751)
This commit is contained in:
parent
eeb2342509
commit
6eafe0d2a8
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@ -2,3 +2,8 @@
|
||||
public/locales/**/*
|
||||
src/i18n/declaration.ts
|
||||
.env
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
60
frontend/package-lock.json
generated
60
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
79
frontend/playwright.config.ts
Normal file
79
frontend/playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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}
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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" })),
|
||||
];
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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" />}
|
||||
|
||||
@ -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
0
frontend/tests/fixtures/project.zip
vendored
Normal file
61
frontend/tests/redirect.spec.ts
Normal file
61
frontend/tests/redirect.spec.ts
Normal 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");
|
||||
});
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user