2026-04-12 17:08:07 +02:00
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
2026-04-12 22:14:53 +02:00
|
|
|
import { fireEvent, render, screen, within } from '@testing-library/react'
|
2026-02-22 13:42:20 +01:00
|
|
|
import { SettingsPanel } from './SettingsPanel'
|
|
|
|
|
import type { Settings } from '../types'
|
2026-04-24 22:26:07 +02:00
|
|
|
import { THEME_MODE_STORAGE_KEY } from '../lib/themeMode'
|
2026-02-22 13:42:20 +01:00
|
|
|
|
2026-04-30 02:58:36 +02:00
|
|
|
const { trackEventMock } = vi.hoisted(() => ({
|
|
|
|
|
trackEventMock: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
vi.mock('../lib/telemetry', () => ({
|
|
|
|
|
trackEvent: trackEventMock,
|
|
|
|
|
}))
|
|
|
|
|
|
2026-02-22 13:42:20 +01:00
|
|
|
const emptySettings: Settings = {
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
auto_pull_interval_minutes: null,
|
2026-04-16 23:38:30 +02:00
|
|
|
autogit_enabled: null,
|
|
|
|
|
autogit_idle_threshold_seconds: null,
|
|
|
|
|
autogit_inactive_threshold_seconds: null,
|
2026-04-24 15:04:08 +09:00
|
|
|
auto_advance_inbox_after_organize: null,
|
2026-03-25 16:05:13 +01:00
|
|
|
telemetry_consent: null,
|
|
|
|
|
crash_reporting_enabled: null,
|
|
|
|
|
analytics_enabled: null,
|
|
|
|
|
anonymous_id: null,
|
2026-04-04 12:11:26 +02:00
|
|
|
release_channel: null,
|
2026-04-24 22:26:07 +02:00
|
|
|
theme_mode: null,
|
2026-04-26 08:18:47 +02:00
|
|
|
ui_language: null,
|
2026-04-28 08:55:40 +02:00
|
|
|
default_ai_agent: null,
|
|
|
|
|
hide_gitignored_files: null,
|
2026-04-30 02:02:59 +02:00
|
|
|
all_notes_show_pdfs: null,
|
|
|
|
|
all_notes_show_images: null,
|
|
|
|
|
all_notes_show_unsupported: null,
|
2026-02-22 13:42:20 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:14:53 +02:00
|
|
|
function installPointerCapturePolyfill() {
|
|
|
|
|
if (!HTMLElement.prototype.hasPointerCapture) {
|
|
|
|
|
HTMLElement.prototype.hasPointerCapture = () => false
|
|
|
|
|
}
|
|
|
|
|
if (!HTMLElement.prototype.setPointerCapture) {
|
|
|
|
|
HTMLElement.prototype.setPointerCapture = () => {}
|
|
|
|
|
}
|
|
|
|
|
if (!HTMLElement.prototype.releasePointerCapture) {
|
|
|
|
|
HTMLElement.prototype.releasePointerCapture = () => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 22:26:07 +02:00
|
|
|
function createStorageMock(): Storage {
|
|
|
|
|
const values = new Map<string, string>()
|
|
|
|
|
return {
|
|
|
|
|
get length() { return values.size },
|
|
|
|
|
clear: vi.fn(() => { values.clear() }),
|
|
|
|
|
getItem: vi.fn((key: string) => values.get(key) ?? null),
|
|
|
|
|
key: vi.fn((index: number) => Array.from(values.keys())[index] ?? null),
|
|
|
|
|
removeItem: vi.fn((key: string) => { values.delete(key) }),
|
|
|
|
|
setItem: vi.fn((key: string, value: string) => { values.set(key, value) }),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:42:20 +01:00
|
|
|
describe('SettingsPanel', () => {
|
|
|
|
|
const onSave = vi.fn()
|
|
|
|
|
const onClose = vi.fn()
|
2026-04-24 22:26:07 +02:00
|
|
|
const localStorageMock = createStorageMock()
|
2026-02-22 13:42:20 +01:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
2026-04-30 02:58:36 +02:00
|
|
|
trackEventMock.mockClear()
|
2026-04-24 22:26:07 +02:00
|
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true })
|
|
|
|
|
window.localStorage.clear()
|
2026-04-28 21:16:30 +02:00
|
|
|
document.documentElement.removeAttribute('data-theme')
|
|
|
|
|
document.documentElement.classList.remove('dark')
|
2026-04-12 22:14:53 +02:00
|
|
|
installPointerCapturePolyfill()
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders nothing when not open', () => {
|
|
|
|
|
const { container } = render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={false} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
expect(container.innerHTML).toBe('')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders modal when open', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
expect(screen.getByText('Settings')).toBeInTheDocument()
|
2026-04-17 10:53:56 +02:00
|
|
|
expect(screen.getByText('Sync & Updates')).toBeInTheDocument()
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-26 09:46:11 +02:00
|
|
|
it('updates the draft language when stored settings finish loading', () => {
|
|
|
|
|
const { rerender } = render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
rerender(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
2026-04-27 13:06:59 +02:00
|
|
|
settings={{ ...emptySettings, ui_language: 'zh-CN' }}
|
2026-04-26 09:46:11 +02:00
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('设置')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Settings')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-12 22:14:53 +02:00
|
|
|
it('calls onSave with stable defaults on save', () => {
|
2026-02-22 13:42:20 +01:00
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
2026-04-12 22:14:53 +02:00
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
auto_pull_interval_minutes: 5,
|
2026-04-16 23:38:30 +02:00
|
|
|
autogit_enabled: false,
|
|
|
|
|
autogit_idle_threshold_seconds: 90,
|
|
|
|
|
autogit_inactive_threshold_seconds: 30,
|
2026-04-12 22:14:53 +02:00
|
|
|
release_channel: null,
|
2026-04-24 22:26:07 +02:00
|
|
|
theme_mode: 'light',
|
2026-04-28 08:55:40 +02:00
|
|
|
hide_gitignored_files: true,
|
2026-04-30 02:02:59 +02:00
|
|
|
all_notes_show_pdfs: false,
|
|
|
|
|
all_notes_show_images: false,
|
|
|
|
|
all_notes_show_unsupported: false,
|
2026-04-28 08:55:40 +02:00
|
|
|
}))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves Gitignored content visibility immediately for keyboard close', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-hide-gitignored-files'))
|
|
|
|
|
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Escape' })
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
hide_gitignored_files: false,
|
2026-04-12 22:14:53 +02:00
|
|
|
}))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-30 02:02:59 +02:00
|
|
|
it('renders All Notes file visibility checkboxes off by default', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('All Notes visibility')).toBeInTheDocument()
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves saved All Notes file visibility checkboxes', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={{
|
|
|
|
|
...emptySettings,
|
|
|
|
|
all_notes_show_pdfs: true,
|
|
|
|
|
all_notes_show_images: true,
|
|
|
|
|
all_notes_show_unsupported: false,
|
|
|
|
|
}}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves All Notes file visibility from keyboard toggles before Escape close', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const pdfCheckbox = within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox')
|
|
|
|
|
pdfCheckbox.focus()
|
|
|
|
|
fireEvent.keyDown(pdfCheckbox, { key: ' ', code: 'Space' })
|
|
|
|
|
fireEvent.keyUp(pdfCheckbox, { key: ' ', code: 'Space' })
|
|
|
|
|
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Escape' })
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
all_notes_show_pdfs: true,
|
|
|
|
|
all_notes_show_images: false,
|
|
|
|
|
all_notes_show_unsupported: false,
|
|
|
|
|
}))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-30 02:58:36 +02:00
|
|
|
it('tracks All Notes visibility toggles with categorical metadata only', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox'))
|
|
|
|
|
|
|
|
|
|
expect(trackEventMock).toHaveBeenCalledWith('all_notes_visibility_changed', {
|
|
|
|
|
category: 'images',
|
|
|
|
|
enabled: 1,
|
|
|
|
|
})
|
|
|
|
|
expect(trackEventMock).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.objectContaining({ path: expect.any(String) }),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 22:26:07 +02:00
|
|
|
it('defaults the color mode control to light', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('settings-theme-mode')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole('radio', { name: 'Light' })).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
expect(screen.getByRole('radio', { name: 'Dark' })).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-26 08:18:47 +02:00
|
|
|
it('defaults the language selector to system language', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={emptySettings}
|
|
|
|
|
locale="en"
|
2026-04-27 13:06:59 +02:00
|
|
|
systemLocale="zh-CN"
|
2026-04-26 08:18:47 +02:00
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('settings-ui-language')).toHaveAttribute('data-value', 'system')
|
|
|
|
|
expect(screen.getByText('系统(简体中文)')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the language selector keyboard accessible', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const trigger = screen.getByTestId('settings-ui-language')
|
|
|
|
|
trigger.focus()
|
|
|
|
|
fireEvent.keyDown(trigger, { key: 'ArrowDown', code: 'ArrowDown' })
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('option', { name: 'Simplified Chinese' })).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves the selected UI language and updates visible settings text', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.pointerDown(screen.getByTestId('settings-ui-language'), { button: 0, pointerType: 'mouse' })
|
|
|
|
|
fireEvent.click(screen.getByRole('option', { name: 'Simplified Chinese' }))
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('设置')).toBeInTheDocument()
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
2026-04-27 13:06:59 +02:00
|
|
|
ui_language: 'zh-CN',
|
2026-04-26 08:18:47 +02:00
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 22:26:07 +02:00
|
|
|
it('uses the stored color mode mirror when settings have no saved mode', () => {
|
|
|
|
|
window.localStorage.setItem(THEME_MODE_STORAGE_KEY, 'dark')
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('radio', { name: 'Dark' })).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves the selected dark color mode', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('radio', { name: 'Dark' }))
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
2026-04-28 21:16:30 +02:00
|
|
|
theme_mode: 'dark',
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('applies the selected dark color mode immediately while settings stays open', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('radio', { name: 'Dark' }))
|
|
|
|
|
|
|
|
|
|
expect(document.documentElement).toHaveAttribute('data-theme', 'dark')
|
|
|
|
|
expect(document.documentElement).toHaveClass('dark')
|
|
|
|
|
expect(window.localStorage.getItem(THEME_MODE_STORAGE_KEY)).toBe('dark')
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
2026-04-24 22:26:07 +02:00
|
|
|
theme_mode: 'dark',
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves a saved dark color mode until changed', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={{ ...emptySettings, theme_mode: 'dark' }}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('radio', { name: 'Dark' })).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
theme_mode: 'dark',
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-12 22:14:53 +02:00
|
|
|
it('defaults the release channel trigger to stable', () => {
|
2026-02-22 13:42:20 +01:00
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
|
2026-04-12 22:14:53 +02:00
|
|
|
expect(screen.getByTestId('settings-release-channel')).toHaveAttribute('data-value', 'stable')
|
|
|
|
|
expect(screen.queryByText(/Beta\/Stable/i)).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-23 20:09:49 +02:00
|
|
|
it('anchors the default agent dropdown with the popper strategy', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.pointerDown(screen.getByTestId('settings-default-ai-agent'), { button: 0, pointerType: 'mouse' })
|
|
|
|
|
|
|
|
|
|
expect(document.querySelector('[data-anchor-strategy="popper"]')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps keyboard opening enabled for the default agent dropdown', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const trigger = screen.getByTestId('settings-default-ai-agent')
|
|
|
|
|
trigger.focus()
|
|
|
|
|
fireEvent.keyDown(trigger, { key: 'ArrowDown', code: 'ArrowDown' })
|
|
|
|
|
|
|
|
|
|
expect(document.querySelector('[data-anchor-strategy="popper"]')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole('option', { name: /Codex/i })).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-13 12:35:02 +02:00
|
|
|
it('treats a legacy beta release channel as stable', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={{ ...emptySettings, release_channel: 'beta' }}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('settings-release-channel')).toHaveAttribute('data-value', 'stable')
|
|
|
|
|
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-12 22:14:53 +02:00
|
|
|
it('preserves alpha when alpha is already selected', () => {
|
|
|
|
|
const alphaSettings: Settings = {
|
|
|
|
|
...emptySettings,
|
|
|
|
|
release_channel: 'alpha',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={alphaSettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-22 13:42:20 +01:00
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
2026-03-25 16:05:13 +01:00
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
2026-04-12 22:14:53 +02:00
|
|
|
release_channel: 'alpha',
|
2026-03-25 16:05:13 +01:00
|
|
|
}))
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-10 13:52:39 +02:00
|
|
|
it('defaults the organization workflow switch to on', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getByRole('switch', { name: 'Organize notes explicitly' })).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 15:04:08 +09:00
|
|
|
it('defaults auto-advance to the next inbox item to off', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getByRole('switch', { name: 'Auto-advance to next Inbox item' })).toHaveAttribute('aria-checked', 'false')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-16 12:18:11 +02:00
|
|
|
it('defaults the initial H1 auto-rename switch to on', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getByRole('switch', { name: 'Auto-rename untitled notes from first H1' })).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-16 23:38:30 +02:00
|
|
|
it('defaults AutoGit to off with recommended thresholds', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-17 10:53:56 +02:00
|
|
|
expect(screen.getByRole('switch', { name: 'Enable AutoGit' })).toHaveAttribute('aria-checked', 'false')
|
2026-04-16 23:38:30 +02:00
|
|
|
expect(screen.getByTestId('settings-autogit-idle-threshold')).toHaveValue(90)
|
|
|
|
|
expect(screen.getByTestId('settings-autogit-inactive-threshold')).toHaveValue(30)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves AutoGit preferences when toggled and edited', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-17 10:53:56 +02:00
|
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Enable AutoGit' }))
|
2026-04-16 23:38:30 +02:00
|
|
|
fireEvent.change(screen.getByTestId('settings-autogit-idle-threshold'), { target: { value: '120' } })
|
|
|
|
|
fireEvent.change(screen.getByTestId('settings-autogit-inactive-threshold'), { target: { value: '45' } })
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
autogit_enabled: true,
|
|
|
|
|
autogit_idle_threshold_seconds: 120,
|
|
|
|
|
autogit_inactive_threshold_seconds: 45,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('disables AutoGit controls when the current vault is not git-enabled', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={emptySettings}
|
|
|
|
|
isGitVault={false}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-17 10:53:56 +02:00
|
|
|
expect(screen.getByRole('switch', { name: 'Enable AutoGit' })).toBeDisabled()
|
2026-04-16 23:38:30 +02:00
|
|
|
expect(screen.getByTestId('settings-autogit-idle-threshold')).toBeDisabled()
|
|
|
|
|
expect(screen.getByTestId('settings-autogit-inactive-threshold')).toBeDisabled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-16 12:18:11 +02:00
|
|
|
it('saves the initial H1 auto-rename preference when toggled off', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Auto-rename untitled notes from first H1' }))
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
initial_h1_auto_rename_enabled: false,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-10 13:52:39 +02:00
|
|
|
it('saves the organization workflow preference when toggled off', () => {
|
|
|
|
|
const onSaveExplicitOrganization = vi.fn()
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={emptySettings}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
explicitOrganizationEnabled={true}
|
|
|
|
|
onSaveExplicitOrganization={onSaveExplicitOrganization}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Organize notes explicitly' }))
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSaveExplicitOrganization).toHaveBeenCalledWith(false)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 15:04:08 +09:00
|
|
|
it('saves the auto-advance inbox preference when toggled on', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Auto-advance to next Inbox item' }))
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
auto_advance_inbox_after_organize: true,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-22 13:42:20 +01:00
|
|
|
it('calls onClose when Cancel is clicked', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
fireEvent.click(screen.getByText('Cancel'))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('calls onClose when close button is clicked', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
fireEvent.click(screen.getByTitle('Close settings'))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('calls onClose on Escape key', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Escape' })
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves on Cmd+Enter', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Enter', metaKey: true })
|
|
|
|
|
|
2026-03-25 16:05:13 +01:00
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
auto_pull_interval_minutes: 5,
|
2026-03-25 16:05:13 +01:00
|
|
|
}))
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('calls onClose when clicking backdrop', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
fireEvent.click(screen.getByTestId('settings-panel'))
|
|
|
|
|
expect(onClose).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows keyboard shortcut hint in footer', () => {
|
|
|
|
|
render(
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
2026-02-22 13:42:20 +01:00
|
|
|
)
|
|
|
|
|
expect(screen.getByText(/to open settings/)).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-29 13:39:05 +02:00
|
|
|
it('copies the MCP config from the AI Agents section', () => {
|
|
|
|
|
const onCopyMcpConfig = vi.fn()
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
open={true}
|
|
|
|
|
settings={emptySettings}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onCopyMcpConfig={onCopyMcpConfig}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Copy MCP config' }))
|
|
|
|
|
|
|
|
|
|
expect(onCopyMcpConfig).toHaveBeenCalledOnce()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-25 16:10:39 +01:00
|
|
|
describe('Privacy & Telemetry section', () => {
|
|
|
|
|
it('renders crash reporting and analytics toggles', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getByTestId('settings-crash-reporting')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByTestId('settings-analytics')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('toggles reflect initial settings state', () => {
|
|
|
|
|
const withTelemetry: Settings = {
|
|
|
|
|
...emptySettings,
|
|
|
|
|
telemetry_consent: true,
|
|
|
|
|
crash_reporting_enabled: true,
|
|
|
|
|
analytics_enabled: false,
|
|
|
|
|
anonymous_id: 'test-uuid',
|
|
|
|
|
}
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={withTelemetry} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
2026-04-12 22:14:53 +02:00
|
|
|
|
|
|
|
|
const crashCheckbox = within(screen.getByTestId('settings-crash-reporting')).getByRole('checkbox')
|
|
|
|
|
const analyticsCheckbox = within(screen.getByTestId('settings-analytics')).getByRole('checkbox')
|
|
|
|
|
|
|
|
|
|
expect(crashCheckbox).toHaveAttribute('aria-checked', 'true')
|
|
|
|
|
expect(analyticsCheckbox).toHaveAttribute('aria-checked', 'false')
|
2026-03-25 16:10:39 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves telemetry settings when toggled and saved', () => {
|
|
|
|
|
render(
|
|
|
|
|
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
|
|
|
|
)
|
2026-04-12 22:14:53 +02:00
|
|
|
|
|
|
|
|
fireEvent.click(within(screen.getByTestId('settings-crash-reporting')).getByRole('checkbox'))
|
2026-03-25 16:10:39 +01:00
|
|
|
fireEvent.click(screen.getByTestId('settings-save'))
|
|
|
|
|
|
|
|
|
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
crash_reporting_enabled: true,
|
|
|
|
|
analytics_enabled: false,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-02-22 13:42:20 +01:00
|
|
|
})
|