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))
|
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 uuid::Uuid;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum WorkspaceMode {
|
pub enum WorkspaceMode {
|
||||||
|
#[default]
|
||||||
Local,
|
Local,
|
||||||
Webdav,
|
Webdav,
|
||||||
GoogleTasks,
|
GoogleTasks,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WorkspaceMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceConfig {
|
pub struct WorkspaceConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ mod tests {
|
||||||
|
|
||||||
// Create a task
|
// Create a task
|
||||||
let task = Task::new("Test Task".to_string());
|
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
|
// List tasks
|
||||||
let tasks = repo.list_tasks(list.id).unwrap();
|
let tasks = repo.list_tasks(list.id).unwrap();
|
||||||
|
|
@ -165,6 +165,20 @@ mod tests {
|
||||||
assert_eq!(tasks[0].title, "Test Task");
|
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]
|
#[test]
|
||||||
fn test_update_task() {
|
fn test_update_task() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1137,8 +1137,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_state_save_load_roundtrip() {
|
fn test_sync_state_save_load_roundtrip() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let mut state = SyncState::default();
|
let mut state = SyncState { last_sync: Some(Utc::now()), ..Default::default() };
|
||||||
state.last_sync = Some(Utc::now());
|
|
||||||
state.record_file("test.md", "abc123", Some("2026-01-01T00:00:00Z"), 42);
|
state.record_file("test.md", "abc123", Some("2026-01-01T00:00:00Z"), 42);
|
||||||
|
|
||||||
state.save(temp_dir.path()).unwrap();
|
state.save(temp_dir.path()).unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue