Compare commits

...

6 Commits

Author SHA1 Message Date
amanape
f8050cab7f Workflow test 5 2025-04-11 20:12:17 +04:00
amanape
c6bf0b63f6 Workflow test 4 2025-04-11 19:52:42 +04:00
amanape
a55c127c4f Workflow test 3 2025-04-11 19:51:27 +04:00
amanape
9988781de8 Workflow test 2 2025-04-11 19:49:53 +04:00
amanape
a2ca28c777 Workflow test 2025-04-11 19:49:35 +04:00
amanape
b3e47ad3fc Lint rules 2025-04-11 19:41:16 +04:00
24 changed files with 275 additions and 168 deletions

View File

@@ -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

View File

@@ -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"],
},
},
],
}

View File

@@ -1,4 +1,3 @@
cd frontend
npm run check-unlocalized-strings
npx lint-staged
npm test

View File

@@ -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();

View File

@@ -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");

View File

@@ -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;
},

View File

@@ -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";

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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");

View File

@@ -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();

View File

@@ -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();

View File

@@ -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", () => {

View File

@@ -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

View File

@@ -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");

View File

@@ -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(", ")}`,
);
}
});
});

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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**",
);
});
});
});

View File

@@ -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>;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -52,7 +52,7 @@ export default defineConfig(({ mode }) => {
},
},
watch: {
ignored: ['**/node_modules/**', '**/.git/**'],
ignored: ["**/node_modules/**", "**/.git/**"],
},
},
ssr: {