test: cover CLI workspace resolver, date picker, saturating version
Add regression tests for the bugs found in this smoke test: - resolve_workspace: by-name, by-UUID, unknown-identifier, current-fallback, actionable no-workspace message. - DateTimePicker: selected-day highlight must be month-scoped; committing after navigating months uses the selected month, not the viewed one. - create_task: version is saturating_add on u64::MAX (doesn't panic/wrap). Also fixes the three pre-existing clippy warnings (WorkspaceMode now uses #[derive(Default)] + #[default], repository test drops unused binding, sync test uses struct-update syntax instead of field-reassign-default).
This commit is contained in:
parent
efb4ccaaef
commit
a79dcc4617
74
apps/tauri/src/lib/components/DateTimePicker.test.ts
Normal file
74
apps/tauri/src/lib/components/DateTimePicker.test.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/svelte";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import DateTimePicker from "./DateTimePicker.svelte";
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("DateTimePicker — selected highlight", () => {
|
||||
it("only marks the selected day in the month/year that was actually picked", async () => {
|
||||
const user = userEvent.setup();
|
||||
// Pick a date in the current month so the component opens on it.
|
||||
const now = new Date();
|
||||
const existing = new Date(now.getFullYear(), now.getMonth(), 15, 0, 0, 0).toISOString();
|
||||
|
||||
render(DateTimePicker, {
|
||||
value: existing,
|
||||
has_time: false,
|
||||
onchange: vi.fn(),
|
||||
onclose: vi.fn(),
|
||||
});
|
||||
|
||||
// The "15" button for the current month should be rendered with the
|
||||
// selected styling (bg-primary).
|
||||
const day15 = screen.getByRole("button", { name: "15" });
|
||||
expect(day15.className).toMatch(/bg-primary/);
|
||||
|
||||
// Navigate one month forward. The same "15" cell must NOT be marked as
|
||||
// selected, because the user hasn't picked a day in that month yet.
|
||||
const nextMonthBtn = screen.getAllByRole("button").find((b) =>
|
||||
b.querySelector("svg path[d*='M7.21 14.77']"),
|
||||
) as HTMLElement;
|
||||
await user.click(nextMonthBtn);
|
||||
|
||||
const nextMonth15 = screen.getByRole("button", { name: "15" });
|
||||
expect(nextMonth15.className).not.toMatch(/bg-primary/);
|
||||
});
|
||||
|
||||
it("commits based on the last-selected month, not the currently-viewed month", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onchange = vi.fn();
|
||||
const onclose = vi.fn();
|
||||
|
||||
// Start with April 10 selected (use a fixed month/year so the test is stable).
|
||||
const existing = new Date(2026, 3, 10, 0, 0, 0).toISOString();
|
||||
render(DateTimePicker, {
|
||||
value: existing,
|
||||
has_time: false,
|
||||
onchange,
|
||||
onclose,
|
||||
});
|
||||
|
||||
// Pick the 20th while viewing April.
|
||||
await user.click(screen.getByRole("button", { name: "20" }));
|
||||
|
||||
// Flip to May.
|
||||
const nextMonthBtn = screen.getAllByRole("button").find((b) =>
|
||||
b.querySelector("svg path[d*='M7.21 14.77']"),
|
||||
) as HTMLElement;
|
||||
await user.click(nextMonthBtn);
|
||||
|
||||
// Hit Done.
|
||||
await user.click(screen.getByRole("button", { name: "Done" }));
|
||||
|
||||
expect(onchange).toHaveBeenCalled();
|
||||
const committed = new Date(onchange.mock.calls[0][0] as string);
|
||||
// April == month 3 (0-indexed). We navigated to May without reselecting,
|
||||
// so the committed date must still be April 20.
|
||||
expect(committed.getMonth()).toBe(3);
|
||||
expect(committed.getDate()).toBe(20);
|
||||
expect(committed.getFullYear()).toBe(2026);
|
||||
});
|
||||
});
|
||||
|
|
@ -53,3 +53,60 @@ pub fn get_repository(workspace_identifier: Option<String>) -> Result<(TaskRepos
|
|||
|
||||
Ok((repo, name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_config_with(ws: &[(&str, &str)]) -> (AppConfig, Vec<String>) {
|
||||
let mut config = AppConfig::new();
|
||||
let ids: Vec<String> = ws.iter()
|
||||
.map(|(name, path)| config.add_workspace(WorkspaceConfig::new(name.to_string(), PathBuf::from(path))))
|
||||
.collect();
|
||||
(config, ids)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_by_name() {
|
||||
let (config, _ids) = make_config_with(&[("dev", "/tmp/dev"), ("home", "/tmp/home")]);
|
||||
let (id, ws) = resolve_workspace(&config, Some("dev")).unwrap();
|
||||
assert_eq!(ws.name, "dev");
|
||||
assert!(config.workspaces.contains_key(&id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_by_uuid() {
|
||||
let (config, ids) = make_config_with(&[("dev", "/tmp/dev")]);
|
||||
let target = ids[0].clone();
|
||||
let (id, ws) = resolve_workspace(&config, Some(&target)).unwrap();
|
||||
assert_eq!(id, target);
|
||||
assert_eq!(ws.name, "dev");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_unknown_identifier_errors() {
|
||||
let (config, _ids) = make_config_with(&[("dev", "/tmp/dev")]);
|
||||
let err = resolve_workspace(&config, Some("ghost")).unwrap_err();
|
||||
assert!(err.to_string().contains("Workspace 'ghost' not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_current() {
|
||||
let (mut config, ids) = make_config_with(&[("a", "/tmp/a"), ("b", "/tmp/b")]);
|
||||
config.set_current_workspace(ids[1].clone()).unwrap();
|
||||
let (id, ws) = resolve_workspace(&config, None).unwrap();
|
||||
assert_eq!(id, ids[1]);
|
||||
assert_eq!(ws.name, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_no_current_gives_actionable_message() {
|
||||
let config = AppConfig::new();
|
||||
let err = resolve_workspace(&config, None).unwrap_err();
|
||||
let msg = err.to_string();
|
||||
// The message should point the user at the right sub-commands, not
|
||||
// at the obsolete 'onyx init' suggestion.
|
||||
assert!(msg.contains("workspace add") || msg.contains("workspace switch"),
|
||||
"expected actionable message, got: {msg}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,15 @@ use serde::{Deserialize, Serialize};
|
|||
use uuid::Uuid;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WorkspaceMode {
|
||||
#[default]
|
||||
Local,
|
||||
Webdav,
|
||||
GoogleTasks,
|
||||
}
|
||||
|
||||
impl Default for WorkspaceMode {
|
||||
fn default() -> Self {
|
||||
Self::Local
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkspaceConfig {
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ mod tests {
|
|||
|
||||
// Create a task
|
||||
let task = Task::new("Test Task".to_string());
|
||||
let created_task = repo.create_task(list.id, task).unwrap();
|
||||
let _ = repo.create_task(list.id, task).unwrap();
|
||||
|
||||
// List tasks
|
||||
let tasks = repo.list_tasks(list.id).unwrap();
|
||||
|
|
@ -165,6 +165,20 @@ mod tests {
|
|||
assert_eq!(tasks[0].title, "Test Task");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_task_saturates_version_at_max() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
||||
let list = repo.create_list("L".to_string()).unwrap();
|
||||
|
||||
// Simulate a task that is already at u64::MAX. A plain `+=` would
|
||||
// overflow — saturating_add must clamp.
|
||||
let mut task = Task::new("max".to_string());
|
||||
task.version = u64::MAX;
|
||||
let created = repo.create_task(list.id, task).unwrap();
|
||||
assert_eq!(created.version, u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_task() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -1137,8 +1137,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_sync_state_save_load_roundtrip() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut state = SyncState::default();
|
||||
state.last_sync = Some(Utc::now());
|
||||
let mut state = SyncState { last_sync: Some(Utc::now()), ..Default::default() };
|
||||
state.record_file("test.md", "abc123", Some("2026-01-01T00:00:00Z"), 42);
|
||||
|
||||
state.save(temp_dir.path()).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue