Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,14 +655,27 @@ Launch with `mode: "plan"` to produce a PRD before writing code. Plan Mode restr

## Tests

Tests live next to the code they cover under `electron/`:
Tests live next to the code they cover, under both `electron/` (store/lib/MCP logic) and `src/` (store slices, pure libs, and React components):

```bash
npm test # run all tests
npm run test:watch # watch mode — great for TDD
npm run test:coverage # coverage report
npm run test:unit # node project only (store/lib/electron logic)
npm run test:component # component project only (React + jsdom)
```

vitest runs **two projects** (see `vitest.config.ts`):

- **`node`** — the default fast suite for store slices, pure libs, and the
electron/MCP layer. Runs in the `node` environment with the native SQLite
shim. Matches `**/*.test.ts` (and `src/**/*.test.tsx`), excluding component tests.
- **`component`** — React component tests rendered in **jsdom** via
`@testing-library/react`. Matches `src/**/*.component.test.tsx` and loads
`vitest.setup.components.ts` (jest-dom matchers + auto-cleanup). Use these for
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
presentational behavior (disabled states, ARIA attributes, click handlers).
Store-connected components can mock `@/store` with `vi.mock`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

| Test file | What it covers |
|-----------|---------------|
| `electron/db/queries.test.ts` | SQLite query helpers — CRUD, search, soft-delete |
Expand Down
21 changes: 21 additions & 0 deletions changelogs/v2.3.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## What's new

This is an internal quality release — no user-facing behavior changes. It hardens the test suite around the v2.3.2 features and adds a React component-testing harness.

### Changes

- **Shared "What's New" gate**: Extracted the duplicated "unseen latest features" logic (previously copy-pasted in `NewFeatureModal` and the boot path in `page.tsx`) into a single pure `getUnseenLatestFeatures(registry, seenFeatures, forceOpen?)`. Both call sites now share one implementation, eliminating drift between the modal and the tour-deferral gate. No behavior change.

### Tests

- **Component test harness**: Added a second vitest project (`component`) running **jsdom + Testing Library** alongside the existing `node` project, plus `npm run test:unit` / `npm run test:component` scripts and a jest-dom setup file. Component tests live in `*.component.test.tsx`.
- **Onboarding component tests**: `StepCreateProject` — submit gating on empty/whitespace names, icon picker selection and `aria-pressed` state, accessible labels on icon-only buttons, and all controls (back, skip, icon picker) disabled while a create is in flight.
- **What's New modal component tests**: boot gating (unseen shows, seen hidden), "Done" marks the feature seen and closes, and — via the `forceOpen` (Settings) path — pagination dots, Next navigation, and "Skip All" marking every displayed feature seen.
- **Feature-gate unit tests**: full coverage of `getUnseenLatestFeatures` (forceOpen, empty registry, latest-version-only filtering, seen filtering, order-derived latest version).
- **Store & pure-logic unit tests**:
- `createProject` — icon persistence (the v2.3.2 onboarding fix), icon omitted when absent, the 5 optimistic columns with correct types/orders, and append-not-replace semantics.
- UI slice tour/seen-feature actions — `markFeatureAsSeen` (append/idempotent), `setTutorialActive` (with its step-index reset), `setTutorialStepIndex`.
- Board slice — `moveCard` (same/cross-column reindex, index clamping, untouched columns) and `getReadyCards` (blocker resolution, done/archived handling, orphan blockers, project scoping).
- `buildFolderTree` — folder-path normalization edge cases (undefined vs empty, leading/trailing/double slashes, deep nesting, alphabetical sort).
- `searchAll` — empty-query, title/content/description matching, snippet length, archived skipping, 50-result cap.
- Date helpers — `formatRelative` thresholds and `getDueDateStatus` (timezone-robust day comparison).
61 changes: 32 additions & 29 deletions electron/splash/bootsplash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ function getAppVersion(): string {
}
}

function buildSplashHtml(iconDataUrl: string | null): string {
function buildSplashHtml(iconDataUrl: string | null, colors: Record<string, string>): string {
const version = getAppVersion();
// Bake the resolved theme palette into the initial CSS so the splash paints
// with the correct (light or dark) colors on first frame — no dark flash
// before the post-load `splash:theme` IPC override arrives.
const initialVars = Object.keys(colors)
.map((key) => ` --splash-${key}: ${colors[key]};`)
.join("\n");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const logoHtml = iconDataUrl
? `<img src="${iconDataUrl}" width="64" height="64" alt="Cairn" />`
: `<div style="width:64px;height:64px;border-radius:12px;background:var(--splash-accent);display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;color:var(--splash-background);">C</div>`;
Expand All @@ -103,13 +109,8 @@ function buildSplashHtml(iconDataUrl: string | null): string {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 48px 32px;
--splash-text: #e8e8e8;
--splash-text-dim: #666;
--splash-accent: #8b7bd8;
--splash-progress-bg: #222;
--splash-success: #22c55e;
--splash-error: #ef4444;
--splash-background: #0d0d0d;
${initialVars}
background: var(--splash-background);
}
.logo {
width: 64px; height: 64px;
Expand Down Expand Up @@ -310,34 +311,36 @@ function buildSplashHtml(iconDataUrl: string | null): string {
}

function getThemeColors(): Record<string, string> {
// Mirrors the app's real design tokens in src/app/globals.css so the splash
// matches the window that follows it (light branch = [data-theme="light"]).
const light: Record<string, string> = {
text: "#1a1917", // --text-primary (light)
"text-dim": "#9e9a94", // --text-tertiary (light)
accent: "#6457e8", // --accent (light)
"progress-bg": "#f0eeeb", // --surface-2 (light)
success: "#22c55e",
error: "#ef4444",
background: "#f5f4f1", // --background (light)
};
const dark: Record<string, string> = {
text: "#e8e4dc", // --text-primary (dark)
"text-dim": "#66635f", // --text-tertiary (dark)
accent: "#7c6af7", // --accent (dark)
"progress-bg": "#1a1a1a", // --surface-2 (dark)
success: "#22c55e",
error: "#ef4444",
background: "#0d0d0d", // --background (dark)
};
try {
const themeFile = path.join(app.getPath("userData"), "theme.json");
if (fs.existsSync(themeFile)) {
const t = JSON.parse(fs.readFileSync(themeFile, "utf8")).theme;
if (t === "light") {
return {
text: "#1a1a1a",
"text-dim": "#888",
accent: "#6366f1",
"progress-bg": "#e0e0e0",
success: "#22c55e",
error: "#ef4444",
background: "#f5f4f1",
};
}
if (t === "light") return light;
}
} catch {
// ignore — use dark defaults
}
return {
text: "#e8e8e8",
"text-dim": "#666",
accent: "#8b7bd8",
"progress-bg": "#222",
success: "#22c55e",
error: "#ef4444",
background: "#0d0d0d",
};
return dark;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

export class BootSplash {
Expand All @@ -364,7 +367,7 @@ export class BootSplash {

this.win.loadURL(
"data:text/html;charset=utf-8," +
encodeURIComponent(buildSplashHtml(getIconDataUrl())),
encodeURIComponent(buildSplashHtml(getIconDataUrl(), colors)),
);

this.win.webContents.on("did-finish-load", () => {
Expand Down
Loading