mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
6 Commits
main
...
ALL-1769/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8050cab7f | ||
|
|
c6bf0b63f6 | ||
|
|
a55c127c4f | ||
|
|
9988781de8 | ||
|
|
a2ca28c777 | ||
|
|
b3e47ad3fc |
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
@@ -7,7 +7,7 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
@@ -33,9 +33,23 @@ jobs:
|
||||
- name: Lint and TypeScript compilation
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
npm run make-i18n && tsc
|
||||
|
||||
# Commit and push changes if any as a result of lint:fix
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git diff --quiet || echo "changes=true" >> $GITHUB_OUTPUT
|
||||
- name: Commit and push if there are changes
|
||||
if: steps.git-check.outputs.changes == 'true'
|
||||
run: |
|
||||
git config --local user.email "openhands@all-hands.dev"
|
||||
git config --local user.name "OpenHands Bot"
|
||||
git add -A
|
||||
git commit -m "🤖 Auto-fix frontend linting issues"
|
||||
git push
|
||||
|
||||
# Run lint on the python code
|
||||
lint-python:
|
||||
name: Lint python
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
"project": "./tsconfig.json",
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
@@ -11,15 +11,12 @@
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@tanstack/query/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
"plugin:@tanstack/query/recommended",
|
||||
],
|
||||
"plugins": ["prettier", "unused-imports"],
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error"
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"prettier/prettier": ["error"],
|
||||
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871
|
||||
"import/extensions": [
|
||||
"error",
|
||||
@@ -27,32 +24,26 @@
|
||||
{
|
||||
"": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
"tsx": "never",
|
||||
},
|
||||
],
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {
|
||||
// Allow state modification in reduce and Redux reducers
|
||||
"no-param-reassign": [
|
||||
"error",
|
||||
{
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": [
|
||||
"acc",
|
||||
"state"
|
||||
]
|
||||
}
|
||||
"ignorePropertyModificationsFor": ["acc", "state"],
|
||||
},
|
||||
],
|
||||
// For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra
|
||||
"no-restricted-syntax": "off",
|
||||
@@ -66,24 +57,19 @@
|
||||
2,
|
||||
{
|
||||
"required": {
|
||||
"some": [
|
||||
"nesting",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
"some": ["nesting", "id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react/no-array-index-key": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"**/tsconfig.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
"project": ["**/tsconfig.json"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
cd frontend
|
||||
npm run check-unlocalized-strings
|
||||
npx lint-staged
|
||||
npm test
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
|
||||
// Mock useParams before importing components
|
||||
vi.mock("react-router", async () => {
|
||||
const actual = await vi.importActual("react-router");
|
||||
@@ -23,10 +27,6 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
|
||||
describe("Browser", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("CopyToClipboardButton", () => {
|
||||
isDisabled={false}
|
||||
onClick={() => {}}
|
||||
mode="copy"
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("copy-to-clipboard");
|
||||
@@ -31,7 +31,7 @@ describe("CopyToClipboardButton", () => {
|
||||
isDisabled={false}
|
||||
onClick={() => {}}
|
||||
mode="copied"
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("copy-to-clipboard");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("posthog-js", () => ({
|
||||
@@ -24,9 +24,9 @@ vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
|
||||
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
|
||||
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
|
||||
ACTION$PUSH_TO_BRANCH: "Push to Branch",
|
||||
ACTION$PUSH_CREATE_PR: "Push & Create PR",
|
||||
ACTION$PUSH_CHANGES_TO_PR: "Push Changes to PR",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { Message } from "#/message";
|
||||
import { act, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import type { Message } from "#/message";
|
||||
import { addUserMessage } from "#/state/chat-slice";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import * as ChatSlice from "#/state/chat-slice";
|
||||
|
||||
@@ -24,7 +24,9 @@ describe("AuthModal", () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AuthModal githubAuthUrl={null} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const button = screen.getByRole("button", {
|
||||
name: "GITHUB$CONNECT_TO_GITHUB",
|
||||
});
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
@@ -45,7 +47,9 @@ describe("AuthModal", () => {
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const button = screen.getByRole("button", {
|
||||
name: "GITHUB$CONNECT_TO_GITHUB",
|
||||
});
|
||||
await user.click(button);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -16,8 +16,6 @@ import { ConversationCard } from "#/components/features/conversation-panel/conve
|
||||
import { clickOnEditButton } from "./utils";
|
||||
|
||||
// We'll use the actual i18next implementation but override the translation function
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
// Mock the t function to return our custom translations
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -27,9 +25,9 @@ vi.mock("react-i18next", async () => {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"CONVERSATION$CREATED": "Created",
|
||||
"CONVERSATION$AGO": "ago",
|
||||
"CONVERSATION$UPDATED": "Updated"
|
||||
CONVERSATION$CREATED: "Created",
|
||||
CONVERSATION$AGO: "ago",
|
||||
CONVERSATION$UPDATED: "Updated",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -82,7 +80,9 @@ describe("ConversationCard", () => {
|
||||
expect(card).toHaveTextContent("ago");
|
||||
|
||||
// Use a regex to match the time part since it might have whitespace
|
||||
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
|
||||
const timeRegex = new RegExp(
|
||||
formatTimeDelta(new Date("2021-10-01T12:00:00Z")),
|
||||
);
|
||||
expect(card).toHaveTextContent(timeRegex);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
QueryClient,
|
||||
QueryClientConfig,
|
||||
} from "@tanstack/react-query";
|
||||
import { QueryClientConfig } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
import { queryClientConfig } from "#/query-client-config";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
@@ -29,9 +23,9 @@ describe("ConversationPanel", () => {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
@@ -84,7 +78,9 @@ describe("ConversationPanel", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
// Setup default mock for getUserConversations
|
||||
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([...mockConversations]);
|
||||
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([
|
||||
...mockConversations,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should render the conversations", async () => {
|
||||
@@ -138,7 +134,9 @@ describe("ConversationPanel", () => {
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /cancel/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is not deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
@@ -151,19 +149,22 @@ describe("ConversationPanel", () => {
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
const deleteUserConversationSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"deleteUserConversation",
|
||||
);
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
const index = mockData.findIndex((conv) => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
// Wait for React Query to update its cache
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
@@ -175,13 +176,18 @@ describe("ConversationPanel", () => {
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /confirm/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update with a longer timeout
|
||||
await waitFor(() => {
|
||||
const updatedCards = screen.getAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
}, { timeout: 2000 });
|
||||
await waitFor(
|
||||
() => {
|
||||
const updatedCards = screen.getAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -218,9 +224,12 @@ describe("ConversationPanel", () => {
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
const deleteUserConversationSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"deleteUserConversation",
|
||||
);
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
const index = mockData.findIndex((conv) => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
@@ -228,7 +237,7 @@ describe("ConversationPanel", () => {
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
@@ -242,7 +251,9 @@ describe("ConversationPanel", () => {
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /confirm/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update
|
||||
await waitFor(() => {
|
||||
@@ -348,9 +359,9 @@ describe("ConversationPanel", () => {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const toggleButton = screen.getByText("Toggle");
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
// Mock useParams before importing components
|
||||
vi.mock("react-router", async () => {
|
||||
const actual = await vi.importActual("react-router");
|
||||
@@ -9,12 +15,6 @@ vi.mock("react-router", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
describe("FeedbackForm", () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseMock = vi.fn();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { Messages } from "#/components/features/chat/messages";
|
||||
import type { Message } from "#/message";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
|
||||
describe("File Operations Messages", () => {
|
||||
it("should show success indicator for successful file read operation", () => {
|
||||
@@ -17,7 +17,9 @@ describe("File Operations Messages", () => {
|
||||
},
|
||||
];
|
||||
|
||||
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
renderWithProviders(
|
||||
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
|
||||
);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
@@ -36,7 +38,9 @@ describe("File Operations Messages", () => {
|
||||
},
|
||||
];
|
||||
|
||||
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
renderWithProviders(
|
||||
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
|
||||
);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
@@ -55,7 +59,9 @@ describe("File Operations Messages", () => {
|
||||
},
|
||||
];
|
||||
|
||||
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
renderWithProviders(
|
||||
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
|
||||
);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
@@ -74,7 +80,9 @@ describe("File Operations Messages", () => {
|
||||
},
|
||||
];
|
||||
|
||||
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
renderWithProviders(
|
||||
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
|
||||
);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImagePreview } from "#/components/features/images/image-preview";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ImagePreview } from "#/components/features/images/image-preview";
|
||||
|
||||
describe("ImagePreview", () => {
|
||||
it("should render an image", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, within, fireEvent } from "@testing-library/react";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
|
||||
@@ -144,7 +144,7 @@ describe("InteractiveChatBox", () => {
|
||||
onStop={onStop}
|
||||
onChange={onChange}
|
||||
value="test message"
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
// Upload an image via the upload button - this should NOT clear the text input
|
||||
@@ -173,7 +173,7 @@ describe("InteractiveChatBox", () => {
|
||||
onStop={onStop}
|
||||
onChange={onChange}
|
||||
value=""
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
// Verify the text input was cleared
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
|
||||
import { jupyterReducer } from "#/state/jupyter-slice";
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
|
||||
describe("JupyterEditor", () => {
|
||||
const mockStore = configureStore({
|
||||
@@ -36,7 +36,7 @@ describe("JupyterEditor", () => {
|
||||
<div style={{ height: "100vh" }}>
|
||||
<JupyterEditor maxWidth={800} />
|
||||
</div>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("jupyter-container");
|
||||
|
||||
@@ -5,7 +5,13 @@ import translations from "../../src/i18n/translation.json";
|
||||
import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
|
||||
|
||||
vi.mock("@heroui/react", () => ({
|
||||
Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
|
||||
Tooltip: ({
|
||||
content,
|
||||
children,
|
||||
}: {
|
||||
content: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div>{content}</div>
|
||||
@@ -13,15 +19,33 @@ vi.mock("@heroui/react", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const supportedLanguages = ['en', 'ja', 'zh-CN', 'zh-TW', 'ko-KR', 'de', 'no', 'it', 'pt', 'es', 'ar', 'fr', 'tr'];
|
||||
const supportedLanguages = [
|
||||
"en",
|
||||
"ja",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"ko-KR",
|
||||
"de",
|
||||
"no",
|
||||
"it",
|
||||
"pt",
|
||||
"es",
|
||||
"ar",
|
||||
"fr",
|
||||
"tr",
|
||||
];
|
||||
|
||||
// Helper function to check if a translation exists for all supported languages
|
||||
function checkTranslationExists(key: string) {
|
||||
const missingTranslations: string[] = [];
|
||||
|
||||
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
|
||||
const translationEntry = (
|
||||
translations as Record<string, Record<string, string>>
|
||||
)[key];
|
||||
if (!translationEntry) {
|
||||
throw new Error(`Translation key "${key}" does not exist in translation.json`);
|
||||
throw new Error(
|
||||
`Translation key "${key}" does not exist in translation.json`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const lang of supportedLanguages) {
|
||||
@@ -53,7 +77,9 @@ function findDuplicateKeys(obj: Record<string, any>) {
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
|
||||
const translationEntry = (
|
||||
translations as Record<string, Record<string, string>>
|
||||
)[key];
|
||||
return translationEntry?.ja || key;
|
||||
},
|
||||
}),
|
||||
@@ -62,7 +88,7 @@ vi.mock("react-i18next", () => ({
|
||||
describe("Landing page translations", () => {
|
||||
test("should render Japanese translations correctly", () => {
|
||||
// Mock a simple component that uses the translations
|
||||
const TestComponent = () => {
|
||||
function TestComponent() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -95,14 +121,16 @@ describe("Landing page translations", () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
// Check main content translations
|
||||
expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
|
||||
expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
|
||||
expect(screen.getByText("テストカバレッジを向上させる")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("テストカバレッジを向上させる"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Dependabot PRを自動マージ")).toBeInTheDocument();
|
||||
expect(screen.getByText("READMEを改善")).toBeInTheDocument();
|
||||
expect(screen.getByText("依存関係を整理")).toBeInTheDocument();
|
||||
@@ -120,8 +148,12 @@ describe("Landing page translations", () => {
|
||||
expect(tabs).toHaveTextContent("コードエディタ");
|
||||
|
||||
// Check workspace label and new project button
|
||||
expect(screen.getByTestId("workspace-label")).toHaveTextContent("ワークスペース");
|
||||
expect(screen.getByTestId("new-project")).toHaveTextContent("新規プロジェクト");
|
||||
expect(screen.getByTestId("workspace-label")).toHaveTextContent(
|
||||
"ワークスペース",
|
||||
);
|
||||
expect(screen.getByTestId("new-project")).toHaveTextContent(
|
||||
"新規プロジェクト",
|
||||
);
|
||||
|
||||
// Check status messages
|
||||
const status = screen.getByTestId("status");
|
||||
@@ -159,12 +191,12 @@ describe("Landing page translations", () => {
|
||||
"STATUS$CONNECTED_TO_SERVER",
|
||||
"TIME$MINUTES_AGO",
|
||||
"TIME$HOURS_AGO",
|
||||
"TIME$DAYS_AGO"
|
||||
"TIME$DAYS_AGO",
|
||||
];
|
||||
|
||||
// Check all keys and collect missing translations
|
||||
const missingTranslationsMap = new Map<string, string[]>();
|
||||
translationKeys.forEach(key => {
|
||||
translationKeys.forEach((key) => {
|
||||
const missing = checkTranslationExists(key);
|
||||
if (missing.length > 0) {
|
||||
missingTranslationsMap.set(key, missing);
|
||||
@@ -174,8 +206,11 @@ describe("Landing page translations", () => {
|
||||
// If any translations are missing, throw an error with all missing translations
|
||||
if (missingTranslationsMap.size > 0) {
|
||||
const errorMessage = Array.from(missingTranslationsMap.entries())
|
||||
.map(([key, langs]) => `\n- "${key}" is missing translations for: ${langs.join(', ')}`)
|
||||
.join('');
|
||||
.map(
|
||||
([key, langs]) =>
|
||||
`\n- "${key}" is missing translations for: ${langs.join(", ")}`,
|
||||
)
|
||||
.join("");
|
||||
throw new Error(`Missing translations:${errorMessage}`);
|
||||
}
|
||||
});
|
||||
@@ -184,7 +219,9 @@ describe("Landing page translations", () => {
|
||||
const duplicates = findDuplicateKeys(translations);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
throw new Error(`Found duplicate translation keys: ${duplicates.join(', ')}`);
|
||||
throw new Error(
|
||||
`Found duplicate translation keys: ${duplicates.join(", ")}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
describe('translation.json', () => {
|
||||
it('should not have duplicate translation keys', () => {
|
||||
describe("translation.json", () => {
|
||||
it("should not have duplicate translation keys", () => {
|
||||
// Read the translation.json file
|
||||
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
|
||||
const translationContent = fs.readFileSync(translationPath, 'utf-8');
|
||||
const translationPath = path.join(
|
||||
__dirname,
|
||||
"../../src/i18n/translation.json",
|
||||
);
|
||||
const translationContent = fs.readFileSync(translationPath, "utf-8");
|
||||
|
||||
// First, let's check for exact string matches of key definitions
|
||||
const keyRegex = /"([^"]+)": {/g;
|
||||
@@ -30,7 +33,7 @@ describe('translation.json', () => {
|
||||
if (uniqueDuplicates.length > 0) {
|
||||
const errorMessage = `Found duplicate translation keys:\n${uniqueDuplicates
|
||||
.map((key) => ` - "${key}" appears ${keyOccurrences.get(key)} times`)
|
||||
.join('\n')}`;
|
||||
.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
@@ -38,10 +41,13 @@ describe('translation.json', () => {
|
||||
expect(uniqueDuplicates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have consistent translations for each key', () => {
|
||||
it("should have consistent translations for each key", () => {
|
||||
// Read the translation.json file
|
||||
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
|
||||
const translationContent = fs.readFileSync(translationPath, 'utf-8');
|
||||
const translationPath = path.join(
|
||||
__dirname,
|
||||
"../../src/i18n/translation.json",
|
||||
);
|
||||
const translationContent = fs.readFileSync(translationPath, "utf-8");
|
||||
const translations = JSON.parse(translationContent);
|
||||
|
||||
// Create a map to store English translations for each key
|
||||
@@ -50,7 +56,7 @@ describe('translation.json', () => {
|
||||
|
||||
// Check each key's English translation
|
||||
Object.entries(translations).forEach(([key, value]: [string, any]) => {
|
||||
if (typeof value === 'object' && value.en !== undefined) {
|
||||
if (typeof value === "object" && value.en !== undefined) {
|
||||
const currentEn = value.en.toLowerCase();
|
||||
const existingEn = englishTranslations.get(key)?.toLowerCase();
|
||||
|
||||
@@ -65,8 +71,10 @@ describe('translation.json', () => {
|
||||
// If there are inconsistencies, create a helpful error message
|
||||
if (inconsistentKeys.length > 0) {
|
||||
const errorMessage = `Found inconsistent translations for keys:\n${inconsistentKeys
|
||||
.map((key) => ` - "${key}" has multiple different English translations`)
|
||||
.join('\n')}`;
|
||||
.map(
|
||||
(key) => ` - "${key}" has multiple different English translations`,
|
||||
)
|
||||
.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,9 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Use queryAllByText to handle multiple elements with the same text
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
|
||||
0,
|
||||
);
|
||||
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
|
||||
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
screen.getByText("BUTTON$SAVE");
|
||||
|
||||
@@ -32,9 +32,11 @@ describe("Actions Service", () => {
|
||||
|
||||
handleStatusMessage(message);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: message,
|
||||
}));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload: message,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log error messages and display them in chat", () => {
|
||||
@@ -53,9 +55,11 @@ describe("Actions Service", () => {
|
||||
metadata: { msgId: "runtime.connection.failed" },
|
||||
});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: message,
|
||||
}));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload: message,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,21 +76,27 @@ describe("Actions Service", () => {
|
||||
final_thought: "",
|
||||
task_completed: "partial",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
thought: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedPartialMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed partially**")) {
|
||||
if (
|
||||
action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes(
|
||||
"believe that the task was **completed partially**",
|
||||
)
|
||||
) {
|
||||
capturedPartialMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messagePartial);
|
||||
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
|
||||
expect(capturedPartialMessage).toContain(
|
||||
"I believe that the task was **completed partially**",
|
||||
);
|
||||
|
||||
// Test not completed
|
||||
const messageNotCompleted: ActionMessage = {
|
||||
@@ -99,21 +109,25 @@ describe("Actions Service", () => {
|
||||
final_thought: "",
|
||||
task_completed: "false",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
thought: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedNotCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **not completed**")) {
|
||||
if (
|
||||
action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **not completed**")
|
||||
) {
|
||||
capturedNotCompletedMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messageNotCompleted);
|
||||
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
|
||||
expect(capturedNotCompletedMessage).toContain(
|
||||
"I believe that the task was **not completed**",
|
||||
);
|
||||
|
||||
// Test completed successfully
|
||||
const messageCompleted: ActionMessage = {
|
||||
@@ -126,21 +140,27 @@ describe("Actions Service", () => {
|
||||
final_thought: "",
|
||||
task_completed: "true",
|
||||
outputs: "",
|
||||
thought: ""
|
||||
}
|
||||
thought: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock implementation to capture the message
|
||||
let capturedCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed successfully**")) {
|
||||
if (
|
||||
action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes(
|
||||
"believe that the task was **completed successfully**",
|
||||
)
|
||||
) {
|
||||
capturedCompletedMessage = action.payload;
|
||||
}
|
||||
});
|
||||
|
||||
handleActionMessage(messageCompleted);
|
||||
expect(capturedCompletedMessage).toContain("I believe that the task was **completed successfully**");
|
||||
expect(capturedCompletedMessage).toContain(
|
||||
"I believe that the task was **completed successfully**",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,15 +5,16 @@ const mockI18n = {
|
||||
language: "ja",
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
|
||||
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
|
||||
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
|
||||
"LANDING$TITLE": "一緒に開発を始めましょう!",
|
||||
"OPEN_IN_VSCODE": "VS Codeで開く",
|
||||
"INCREASE_TEST_COVERAGE": "テストカバレッジを向上",
|
||||
"AUTO_MERGE_PRS": "PRを自動マージ",
|
||||
"FIX_README": "READMEを修正",
|
||||
"CLEAN_DEPENDENCIES": "依存関係を整理"
|
||||
SUGGESTIONS$TODO_APP: "ToDoリストアプリを開発する",
|
||||
LANDING$BUILD_APP_BUTTON: "プルリクエストを表示するアプリを開発する",
|
||||
SUGGESTIONS$HACKER_NEWS:
|
||||
"Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
|
||||
LANDING$TITLE: "一緒に開発を始めましょう!",
|
||||
OPEN_IN_VSCODE: "VS Codeで開く",
|
||||
INCREASE_TEST_COVERAGE: "テストカバレッジを向上",
|
||||
AUTO_MERGE_PRS: "PRを自動マージ",
|
||||
FIX_README: "READMEを修正",
|
||||
CLEAN_DEPENDENCIES: "依存関係を整理",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -23,7 +24,5 @@ const mockI18n = {
|
||||
};
|
||||
|
||||
export function I18nTestProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>
|
||||
);
|
||||
return <I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>;
|
||||
}
|
||||
|
||||
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -83,6 +83,7 @@
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
@@ -9173,6 +9174,22 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unused-imports": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
|
||||
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
|
||||
"eslint": "^9.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BetaBadge } from "./beta-badge";
|
||||
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
|
||||
|
||||
interface NavTabProps {
|
||||
to: string;
|
||||
|
||||
@@ -52,7 +52,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/node_modules/**', '**/.git/**'],
|
||||
ignored: ["**/node_modules/**", "**/.git/**"],
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
|
||||
Reference in New Issue
Block a user