Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ce5151e59 | |||
| 66ddc182c9 | |||
| 1524ec4a98 |
1185
app/package-lock.json
generated
1185
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
@@ -25,13 +27,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4",
|
"@vitejs/plugin-react": "^4",
|
||||||
"autoprefixer": "^10",
|
"autoprefixer": "^10",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5.7",
|
"typescript": "^5.7",
|
||||||
"vite": "^6"
|
"vite": "^6",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ name = "triple-c"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = ["image-png"] }
|
||||||
tauri-plugin-store = "2"
|
tauri-plugin-store = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ pub fn run() {
|
|||||||
settings_store: SettingsStore::new().expect("Failed to initialize settings store"),
|
settings_store: SettingsStore::new().expect("Failed to initialize settings store"),
|
||||||
exec_manager: ExecSessionManager::new(),
|
exec_manager: ExecSessionManager::new(),
|
||||||
})
|
})
|
||||||
|
.setup(|app| {
|
||||||
|
let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/icon.png"))
|
||||||
|
.expect("Failed to load window icon");
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.set_icon(icon);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||||
let state = window.state::<AppState>();
|
let state = window.state::<AppState>();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||||
"productName": "Triple-C",
|
"productName": "Triple-C",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.triple-c.app",
|
"identifier": "com.triple-c.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
|
|||||||
54
app/src/components/layout/Sidebar.test.tsx
Normal file
54
app/src/components/layout/Sidebar.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import Sidebar from "./Sidebar";
|
||||||
|
|
||||||
|
// Mock zustand store
|
||||||
|
vi.mock("../../store/appState", () => ({
|
||||||
|
useAppState: vi.fn((selector) =>
|
||||||
|
selector({
|
||||||
|
sidebarView: "projects",
|
||||||
|
setSidebarView: vi.fn(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock child components to isolate Sidebar layout testing
|
||||||
|
vi.mock("../projects/ProjectList", () => ({
|
||||||
|
default: () => <div data-testid="project-list">ProjectList</div>,
|
||||||
|
}));
|
||||||
|
vi.mock("../settings/SettingsPanel", () => ({
|
||||||
|
default: () => <div data-testid="settings-panel">SettingsPanel</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Sidebar", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the sidebar with content area", () => {
|
||||||
|
render(<Sidebar />);
|
||||||
|
expect(screen.getByText("Projects")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("content area has min-w-0 to prevent flex overflow", () => {
|
||||||
|
const { container } = render(<Sidebar />);
|
||||||
|
const contentArea = container.querySelector(".overflow-y-auto");
|
||||||
|
expect(contentArea).not.toBeNull();
|
||||||
|
expect(contentArea!.className).toContain("min-w-0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("content area has overflow-x-hidden to prevent horizontal scroll", () => {
|
||||||
|
const { container } = render(<Sidebar />);
|
||||||
|
const contentArea = container.querySelector(".overflow-y-auto");
|
||||||
|
expect(contentArea).not.toBeNull();
|
||||||
|
expect(contentArea!.className).toContain("overflow-x-hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sidebar outer container has overflow-hidden", () => {
|
||||||
|
const { container } = render(<Sidebar />);
|
||||||
|
const sidebar = container.firstElementChild;
|
||||||
|
expect(sidebar).not.toBeNull();
|
||||||
|
expect(sidebar!.className).toContain("overflow-hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,7 +35,7 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-1">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-1 min-w-0">
|
||||||
{sidebarView === "projects" ? <ProjectList /> : <SettingsPanel />}
|
{sidebarView === "projects" ? <ProjectList /> : <SettingsPanel />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
128
app/src/components/projects/ProjectCard.test.tsx
Normal file
128
app/src/components/projects/ProjectCard.test.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent, act } from "@testing-library/react";
|
||||||
|
import ProjectCard from "./ProjectCard";
|
||||||
|
import type { Project } from "../../lib/types";
|
||||||
|
|
||||||
|
// Mock Tauri dialog plugin
|
||||||
|
vi.mock("@tauri-apps/plugin-dialog", () => ({
|
||||||
|
open: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock hooks
|
||||||
|
const mockUpdate = vi.fn();
|
||||||
|
const mockStart = vi.fn();
|
||||||
|
const mockStop = vi.fn();
|
||||||
|
const mockRebuild = vi.fn();
|
||||||
|
const mockRemove = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useProjects", () => ({
|
||||||
|
useProjects: () => ({
|
||||||
|
start: mockStart,
|
||||||
|
stop: mockStop,
|
||||||
|
rebuild: mockRebuild,
|
||||||
|
remove: mockRemove,
|
||||||
|
update: mockUpdate,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useTerminal", () => ({
|
||||||
|
useTerminal: () => ({
|
||||||
|
open: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mockSelectedProjectId: string | null = null;
|
||||||
|
vi.mock("../../store/appState", () => ({
|
||||||
|
useAppState: vi.fn((selector) =>
|
||||||
|
selector({
|
||||||
|
selectedProjectId: mockSelectedProjectId,
|
||||||
|
setSelectedProject: vi.fn(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockProject: Project = {
|
||||||
|
id: "test-1",
|
||||||
|
name: "Test Project",
|
||||||
|
paths: [{ host_path: "/home/user/project", mount_name: "project" }],
|
||||||
|
container_id: null,
|
||||||
|
status: "stopped",
|
||||||
|
auth_mode: "login",
|
||||||
|
bedrock_config: null,
|
||||||
|
allow_docker_access: false,
|
||||||
|
ssh_key_path: null,
|
||||||
|
git_token: null,
|
||||||
|
git_user_name: null,
|
||||||
|
git_user_email: null,
|
||||||
|
custom_env_vars: [],
|
||||||
|
claude_instructions: null,
|
||||||
|
created_at: "2026-01-01T00:00:00Z",
|
||||||
|
updated_at: "2026-01-01T00:00:00Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ProjectCard", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockSelectedProjectId = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders project name and path", () => {
|
||||||
|
render(<ProjectCard project={mockProject} />);
|
||||||
|
expect(screen.getByText("Test Project")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("/workspace/project")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("card root has min-w-0 and overflow-hidden to contain content", () => {
|
||||||
|
const { container } = render(<ProjectCard project={mockProject} />);
|
||||||
|
const card = container.firstElementChild;
|
||||||
|
expect(card).not.toBeNull();
|
||||||
|
expect(card!.className).toContain("min-w-0");
|
||||||
|
expect(card!.className).toContain("overflow-hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when selected and showing config", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSelectedProjectId = "test-1";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expanded area has min-w-0 and overflow-hidden", () => {
|
||||||
|
const { container } = render(<ProjectCard project={mockProject} />);
|
||||||
|
// The expanded section (mt-2 ml-4) contains the auth/action/config controls
|
||||||
|
const expandedSection = container.querySelector(".ml-4.mt-2");
|
||||||
|
expect(expandedSection).not.toBeNull();
|
||||||
|
expect(expandedSection!.className).toContain("min-w-0");
|
||||||
|
expect(expandedSection!.className).toContain("overflow-hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("folder path inputs use min-w-0 to allow shrinking", async () => {
|
||||||
|
const { container } = render(<ProjectCard project={mockProject} />);
|
||||||
|
|
||||||
|
// Click Config button to show config panel
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText("Config"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// After config is shown, check the folder host_path input has min-w-0
|
||||||
|
const hostPathInputs = container.querySelectorAll('input[placeholder="/path/to/folder"]');
|
||||||
|
expect(hostPathInputs.length).toBeGreaterThan(0);
|
||||||
|
expect(hostPathInputs[0].className).toContain("min-w-0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("config panel container has overflow-hidden", async () => {
|
||||||
|
const { container } = render(<ProjectCard project={mockProject} />);
|
||||||
|
|
||||||
|
// Click Config button
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText("Config"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// The config panel has border-t and overflow containment classes
|
||||||
|
const allDivs = container.querySelectorAll("div");
|
||||||
|
const configPanel = Array.from(allDivs).find(
|
||||||
|
(div) => div.className.includes("border-t") && div.className.includes("min-w-0")
|
||||||
|
);
|
||||||
|
expect(configPanel).toBeDefined();
|
||||||
|
expect(configPanel!.className).toContain("overflow-hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -255,7 +255,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedProject(project.id)}
|
onClick={() => setSelectedProject(project.id)}
|
||||||
className={`px-3 py-2 rounded cursor-pointer transition-colors ${
|
className={`px-3 py-2 rounded cursor-pointer transition-colors min-w-0 overflow-hidden ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-[var(--bg-tertiary)]"
|
? "bg-[var(--bg-tertiary)]"
|
||||||
: "hover:bg-[var(--bg-tertiary)]"
|
: "hover:bg-[var(--bg-tertiary)]"
|
||||||
@@ -274,7 +274,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="mt-2 ml-4 space-y-2">
|
<div className="mt-2 ml-4 space-y-2 min-w-0 overflow-hidden">
|
||||||
{/* Auth mode selector */}
|
{/* Auth mode selector */}
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
<span className="text-[var(--text-secondary)] mr-1">Auth:</span>
|
<span className="text-[var(--text-secondary)] mr-1">Auth:</span>
|
||||||
@@ -357,12 +357,13 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Config panel */}
|
{/* Config panel */}
|
||||||
{showConfig && (
|
{showConfig && (
|
||||||
<div className="space-y-2 pt-1 border-t border-[var(--border-color)]" onClick={(e) => e.stopPropagation()}>
|
<div className="space-y-2 pt-1 border-t border-[var(--border-color)] min-w-0 overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||||
{/* Folder paths */}
|
{/* Folder paths */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Folders</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Folders</label>
|
||||||
{paths.map((pp, i) => (
|
{paths.map((pp, i) => (
|
||||||
<div key={i} className="flex gap-1 mb-1 items-center">
|
<div key={i} className="mb-1">
|
||||||
|
<div className="flex gap-1 items-center min-w-0">
|
||||||
<input
|
<input
|
||||||
value={pp.host_path}
|
value={pp.host_path}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -377,7 +378,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}}
|
}}
|
||||||
placeholder="/path/to/folder"
|
placeholder="/path/to/folder"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="flex-1 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
className="flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -393,11 +394,28 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="px-2 py-1 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] disabled:opacity-50 transition-colors"
|
className="flex-shrink-0 px-2 py-1 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-[var(--text-secondary)] flex-shrink-0">/workspace/</span>
|
{paths.length > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const updated = paths.filter((_, j) => j !== i);
|
||||||
|
setPaths(updated);
|
||||||
|
try { await update({ ...project, paths: updated }); } catch (err) {
|
||||||
|
console.error("Failed to remove path:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!isStopped}
|
||||||
|
className="flex-shrink-0 px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-primary)] rounded disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 items-center mt-0.5 min-w-0">
|
||||||
|
<span className="text-xs text-[var(--text-secondary)]">/workspace/</span>
|
||||||
<input
|
<input
|
||||||
value={pp.mount_name}
|
value={pp.mount_name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -412,23 +430,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}}
|
}}
|
||||||
placeholder="name"
|
placeholder="name"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="w-20 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 font-mono"
|
className="flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 font-mono"
|
||||||
/>
|
/>
|
||||||
{paths.length > 1 && (
|
</div>
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const updated = paths.filter((_, j) => j !== i);
|
|
||||||
setPaths(updated);
|
|
||||||
try { await update({ ...project, paths: updated }); } catch (err) {
|
|
||||||
console.error("Failed to remove path:", err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!isStopped}
|
|
||||||
className="px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-primary)] rounded disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
|
|||||||
36
app/src/test/icon-config.test.ts
Normal file
36
app/src/test/icon-config.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync, existsSync } from "fs";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
describe("Window icon configuration", () => {
|
||||||
|
const srcTauriDir = resolve(__dirname, "../../src-tauri");
|
||||||
|
|
||||||
|
it("lib.rs sets window icon using set_icon in setup hook", () => {
|
||||||
|
const libRs = readFileSync(resolve(srcTauriDir, "src/lib.rs"), "utf-8");
|
||||||
|
expect(libRs).toContain("set_icon");
|
||||||
|
expect(libRs).toContain("icon.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Cargo.toml enables image-png feature for icon loading", () => {
|
||||||
|
const cargoToml = readFileSync(resolve(srcTauriDir, "Cargo.toml"), "utf-8");
|
||||||
|
expect(cargoToml).toContain("image-png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("icon.png exists in the icons directory", () => {
|
||||||
|
const iconPath = resolve(srcTauriDir, "icons/icon.png");
|
||||||
|
expect(existsSync(iconPath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("icon.ico exists in the icons directory for Windows", () => {
|
||||||
|
const icoPath = resolve(srcTauriDir, "icons/icon.ico");
|
||||||
|
expect(existsSync(icoPath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tauri.conf.json includes icon.ico in bundle icons", () => {
|
||||||
|
const config = JSON.parse(
|
||||||
|
readFileSync(resolve(srcTauriDir, "tauri.conf.json"), "utf-8")
|
||||||
|
);
|
||||||
|
expect(config.bundle.icon).toContain("icons/icon.ico");
|
||||||
|
expect(config.bundle.icon).toContain("icons/icon.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
1
app/src/test/setup.ts
Normal file
1
app/src/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
@@ -17,5 +17,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"forceConsistentCasingInFileNames": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"]
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/vitest.config.ts
Normal file
11
app/vitest.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ["./src/test/setup.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user