chore: update build config, linting, and add test infrastructure
Replace install -D with mkdir -p + install for macOS portability, add vitest with jsdom and testing-library, configure eslint for react-hooks v7 warnings, and add tokio test deps for Rust.
This commit is contained in:
6
Makefile
6
Makefile
@@ -98,12 +98,14 @@ export DESKTOP_ENTRY
|
|||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build ## Build release and install to system (PREFIX=/usr/local)
|
install: build ## Build release and install to system (PREFIX=/usr/local)
|
||||||
install -Dm755 $(TARGET_DIR)/$(APP_NAME) $(DESTDIR)$(BINDIR)/$(APP_NAME)
|
@mkdir -p $(DESTDIR)$(BINDIR)
|
||||||
|
install -m755 $(TARGET_DIR)/$(APP_NAME) $(DESTDIR)$(BINDIR)/$(APP_NAME)
|
||||||
@mkdir -p $(DESTDIR)$(DATADIR)/applications
|
@mkdir -p $(DESTDIR)$(DATADIR)/applications
|
||||||
@echo "$$DESKTOP_ENTRY" > $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
|
@echo "$$DESKTOP_ENTRY" > $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
|
||||||
@for size in 32x32 128x128; do \
|
@for size in 32x32 128x128; do \
|
||||||
if [ -f src-tauri/icons/$$size.png ]; then \
|
if [ -f src-tauri/icons/$$size.png ]; then \
|
||||||
install -Dm644 src-tauri/icons/$$size.png \
|
mkdir -p $(DESTDIR)$(DATADIR)/icons/hicolor/$$size/apps; \
|
||||||
|
install -m644 src-tauri/icons/$$size.png \
|
||||||
$(DESTDIR)$(DATADIR)/icons/hicolor/$$size/apps/$(APP_NAME).png; \
|
$(DESTDIR)$(DATADIR)/icons/hicolor/$$size/apps/$(APP_NAME).png; \
|
||||||
fi; \
|
fi; \
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
|
|||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist', 'src-tauri']),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
@@ -19,5 +19,11 @@ export default defineConfig([
|
|||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
// TODO: fix these incrementally — pre-existing violations from react-hooks v7 upgrade
|
||||||
|
'react-hooks/set-state-in-effect': 'warn',
|
||||||
|
'react-hooks/refs': 'warn',
|
||||||
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
1132
package-lock.json
generated
1132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -8,6 +8,8 @@
|
|||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -18,9 +20,9 @@
|
|||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"@types/dagre": "^0.7.53",
|
"@types/dagre": "^0.7.54",
|
||||||
"@uiw/react-codemirror": "^4.25.4",
|
"@uiw/react-codemirror": "^4.25.4",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -40,6 +42,8 @@
|
|||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tauri-apps/cli": "^2.10.0",
|
"@tauri-apps/cli": "^2.10.0",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -48,11 +52,13 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
"shadcn": "^3.8.4",
|
"shadcn": "^3.8.4",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,6 @@ rmcp = { version = "0.15", features = ["server", "macros", "transport-streamable
|
|||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
schemars = "1"
|
schemars = "1"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
217
src/stores/__tests__/app-store.test.ts
Normal file
217
src/stores/__tests__/app-store.test.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import { useAppStore } from "../app-store";
|
||||||
|
import type { Tab } from "@/types";
|
||||||
|
|
||||||
|
function resetStore() {
|
||||||
|
useAppStore.setState({
|
||||||
|
connections: [],
|
||||||
|
activeConnectionId: null,
|
||||||
|
currentDatabase: null,
|
||||||
|
connectedIds: new Set(),
|
||||||
|
readOnlyMap: {},
|
||||||
|
dbFlavors: {},
|
||||||
|
tabs: [],
|
||||||
|
activeTabId: null,
|
||||||
|
sidebarWidth: 260,
|
||||||
|
pgVersion: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeTab = (id: string, type: Tab["type"] = "query"): Tab => ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
title: `Tab ${id}`,
|
||||||
|
connectionId: "conn-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AppStore", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Connections ────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("connections", () => {
|
||||||
|
it("should set active connection id", () => {
|
||||||
|
useAppStore.getState().setActiveConnectionId("conn-1");
|
||||||
|
expect(useAppStore.getState().activeConnectionId).toBe("conn-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear active connection id", () => {
|
||||||
|
useAppStore.getState().setActiveConnectionId("conn-1");
|
||||||
|
useAppStore.getState().setActiveConnectionId(null);
|
||||||
|
expect(useAppStore.getState().activeConnectionId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add connected id and default to read-only", () => {
|
||||||
|
useAppStore.getState().addConnectedId("conn-1");
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.connectedIds.has("conn-1")).toBe(true);
|
||||||
|
expect(state.readOnlyMap["conn-1"]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove connected id and clean up maps", () => {
|
||||||
|
const s = useAppStore.getState();
|
||||||
|
s.addConnectedId("conn-1");
|
||||||
|
s.setDbFlavor("conn-1", "postgresql");
|
||||||
|
s.setReadOnly("conn-1", false);
|
||||||
|
|
||||||
|
useAppStore.getState().removeConnectedId("conn-1");
|
||||||
|
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.connectedIds.has("conn-1")).toBe(false);
|
||||||
|
expect(state.readOnlyMap["conn-1"]).toBeUndefined();
|
||||||
|
expect(state.dbFlavors["conn-1"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not affect other connections on remove", () => {
|
||||||
|
const s = useAppStore.getState();
|
||||||
|
s.addConnectedId("conn-1");
|
||||||
|
s.addConnectedId("conn-2");
|
||||||
|
s.setDbFlavor("conn-1", "postgresql");
|
||||||
|
s.setDbFlavor("conn-2", "greenplum");
|
||||||
|
|
||||||
|
useAppStore.getState().removeConnectedId("conn-1");
|
||||||
|
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.connectedIds.has("conn-2")).toBe(true);
|
||||||
|
expect(state.readOnlyMap["conn-2"]).toBe(true);
|
||||||
|
expect(state.dbFlavors["conn-2"]).toBe("greenplum");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle read-only mode", () => {
|
||||||
|
useAppStore.getState().addConnectedId("conn-1");
|
||||||
|
expect(useAppStore.getState().readOnlyMap["conn-1"]).toBe(true);
|
||||||
|
|
||||||
|
useAppStore.getState().setReadOnly("conn-1", false);
|
||||||
|
expect(useAppStore.getState().readOnlyMap["conn-1"]).toBe(false);
|
||||||
|
|
||||||
|
useAppStore.getState().setReadOnly("conn-1", true);
|
||||||
|
expect(useAppStore.getState().readOnlyMap["conn-1"]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addConnectedId forces read-only true on reconnect", () => {
|
||||||
|
useAppStore.getState().addConnectedId("conn-1");
|
||||||
|
useAppStore.getState().setReadOnly("conn-1", false);
|
||||||
|
// Reconnect resets to read-only
|
||||||
|
useAppStore.getState().addConnectedId("conn-1");
|
||||||
|
expect(useAppStore.getState().readOnlyMap["conn-1"]).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Tabs ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("tabs", () => {
|
||||||
|
it("should add tab and set it as active", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.tabs).toHaveLength(1);
|
||||||
|
expect(state.tabs[0].id).toBe("tab-1");
|
||||||
|
expect(state.activeTabId).toBe("tab-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should activate the most recently added tab", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-2"));
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe("tab-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close tab and activate last remaining", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-2"));
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-3"));
|
||||||
|
useAppStore.getState().setActiveTabId("tab-2");
|
||||||
|
|
||||||
|
useAppStore.getState().closeTab("tab-2");
|
||||||
|
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.tabs).toHaveLength(2);
|
||||||
|
// When closing the active tab, last remaining tab becomes active
|
||||||
|
expect(state.activeTabId).toBe("tab-3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set activeTabId to null when closing the only tab", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().closeTab("tab-1");
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs).toHaveLength(0);
|
||||||
|
expect(useAppStore.getState().activeTabId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve activeTabId when closing a non-active tab", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-2"));
|
||||||
|
useAppStore.getState().setActiveTabId("tab-1");
|
||||||
|
|
||||||
|
useAppStore.getState().closeTab("tab-2");
|
||||||
|
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe("tab-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update tab fields without affecting others", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-2"));
|
||||||
|
|
||||||
|
useAppStore.getState().updateTab("tab-1", { title: "Updated" });
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs[0].title).toBe("Updated");
|
||||||
|
expect(useAppStore.getState().tabs[1].title).toBe("Tab tab-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle closing non-existent tab gracefully", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("tab-1"));
|
||||||
|
useAppStore.getState().closeTab("non-existent");
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs).toHaveLength(1);
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe("tab-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle different tab types", () => {
|
||||||
|
useAppStore.getState().addTab(makeTab("t1", "query"));
|
||||||
|
useAppStore.getState().addTab(makeTab("t2", "table"));
|
||||||
|
useAppStore.getState().addTab(makeTab("t3", "erd"));
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs.map((t) => t.type)).toEqual([
|
||||||
|
"query",
|
||||||
|
"table",
|
||||||
|
"erd",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Database state ────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("database state", () => {
|
||||||
|
it("should set and clear current database", () => {
|
||||||
|
useAppStore.getState().setCurrentDatabase("mydb");
|
||||||
|
expect(useAppStore.getState().currentDatabase).toBe("mydb");
|
||||||
|
|
||||||
|
useAppStore.getState().setCurrentDatabase(null);
|
||||||
|
expect(useAppStore.getState().currentDatabase).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set pg version", () => {
|
||||||
|
useAppStore.getState().setPgVersion("16.2");
|
||||||
|
expect(useAppStore.getState().pgVersion).toBe("16.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set db flavor", () => {
|
||||||
|
useAppStore.getState().setDbFlavor("conn-1", "greenplum");
|
||||||
|
expect(useAppStore.getState().dbFlavors["conn-1"]).toBe("greenplum");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sidebar ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("sidebar", () => {
|
||||||
|
it("should set sidebar width", () => {
|
||||||
|
useAppStore.getState().setSidebarWidth(400);
|
||||||
|
expect(useAppStore.getState().sidebarWidth).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have default sidebar width of 260", () => {
|
||||||
|
expect(useAppStore.getState().sidebarWidth).toBe(260);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
15
vitest.config.ts
Normal file
15
vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: ["./src/test/setup.ts"],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user