Skip to content

Commit

Permalink
feat(frontend): Redirect user to app after a project upload or repo s…
Browse files Browse the repository at this point in the history
…election (and add e2e tests) (All-Hands-AI#4751)
  • Loading branch information
amanape authored Nov 5, 2024
1 parent eeb2342 commit 6eafe0d
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 8 deletions.
5 changes: 5 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
public/locales/**/*
src/i18n/declaration.ts
.env
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
60 changes: 60 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
79 changes: 79 additions & 0 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
3 changes: 3 additions & 0 deletions frontend/src/components/buttons/ModalButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import clsx from "clsx";
import React from "react";

interface ModalButtonProps {
testId?: string;
variant?: "default" | "text-like";
onClick?: () => void;
text: string;
Expand All @@ -13,6 +14,7 @@ interface ModalButtonProps {
}

function ModalButton({
testId,
variant = "default",
onClick,
text,
Expand All @@ -24,6 +26,7 @@ function ModalButton({
}: ModalButtonProps) {
return (
<button
data-testid={testId}
type={type === "submit" ? "submit" : "button"}
disabled={disabled}
onClick={onClick}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/modals/connect-to-github-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"]);
}),
Expand Down Expand Up @@ -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")
Expand All @@ -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" })),
];
10 changes: 9 additions & 1 deletion frontend/src/routes/_oh._index/github-repo-selector.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,13 +10,15 @@ interface GitHubRepositorySelectorProps {
export function GitHubRepositorySelector({
repositories,
}: GitHubRepositorySelectorProps) {
const navigate = useNavigate();
const dispatch = useDispatch();

const handleRepoSelection = (id: string | null) => {
const repo = repositories.find((r) => r.id.toString() === id);
if (repo) {
// set query param
dispatch(setSelectedRepository(repo.full_name));
navigate("/app");
}
};

Expand All @@ -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"
Expand All @@ -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>
))}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/routes/_oh._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
defer,
redirect,
useLoaderData,
useNavigate,
useRouteLoaderData,
} from "@remix-run/react";
import React, { Suspense } from "react";
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -113,6 +118,7 @@ function Home() {
if (event.target.files) {
const zip = event.target.files[0];
setImportedFile(zip);
navigate("/app");
} else {
// TODO: handle error
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/routes/_oh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />}
Expand Down
7 changes: 6 additions & 1 deletion frontend/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 }) };
}
Empty file.
Loading

0 comments on commit 6eafe0d

Please sign in to comment.