import { expect, test, type Page } from '@playwright/test' import { executeCommand, openCommandPalette } from './helpers' import { seedAutoGitSavedChange } from './testBridge' type MockHandler = (args?: Record) => unknown function installAutoGitMocks() { type BrowserWindow = Window & typeof globalThis & { __getDirtyPathCount?: () => number __gitCommitAttempts?: number __gitCommitMessages?: string[] __gitPushCalls?: number __mockHandlers?: Record __setMockAppActive?: (active: boolean) => void __setMockGitCommitFailure?: (message: string | null) => void } const browserWindow = window as BrowserWindow const dirtyPaths = new Set() let appActive = true let ahead = 0 let gitCommitFailure: string | null = null const createModifiedFiles = () => [...dirtyPaths].map((path) => ({ path, relativePath: path.split('/').pop() ?? path, status: 'modified', })) const isPatched = (handlers?: Record | null) => !handlers || (handlers as Record).__autogitPatched === true const patchSaveNoteContent = (handlers: Record) => { const originalSaveNoteContent = handlers.save_note_content handlers.save_note_content = (args?: Record) => { const path = typeof args?.path === 'string' ? args.path : null if (path) dirtyPaths.add(path) return originalSaveNoteContent?.(args) } } const patchGitHandlers = (handlers: Record) => { handlers.get_modified_files = () => createModifiedFiles() handlers.git_commit = (args?: Record) => { browserWindow.__gitCommitAttempts = (browserWindow.__gitCommitAttempts ?? 0) + 1 if (gitCommitFailure) { throw new Error(gitCommitFailure) } const message = typeof args?.message === 'string' ? args.message : '' browserWindow.__gitCommitMessages?.push(message) dirtyPaths.clear() ahead = 1 return `[main abc1234] ${message}` } handlers.git_push = () => { browserWindow.__gitPushCalls = (browserWindow.__gitPushCalls ?? 0) + 1 ahead = 0 return { status: 'ok', message: 'Pushed to remote' } } handlers.git_remote_status = () => ({ branch: 'main', ahead, behind: 0, hasRemote: true, }) } const markPatched = (handlers: Record) => { Object.defineProperty(handlers, '__autogitPatched', { configurable: true, enumerable: false, value: true, }) } const patchHandlers = (handlers?: Record | null) => { if (isPatched(handlers)) { return handlers ?? null } patchSaveNoteContent(handlers) patchGitHandlers(handlers) markPatched(handlers) return handlers } document.hasFocus = () => appActive browserWindow.__setMockAppActive = (active: boolean) => { appActive = active } browserWindow.__setMockGitCommitFailure = (message: string | null) => { gitCommitFailure = message } browserWindow.__getDirtyPathCount = () => dirtyPaths.size browserWindow.__gitCommitAttempts = 0 browserWindow.__gitCommitMessages = [] browserWindow.__gitPushCalls = 0 let ref = patchHandlers(browserWindow.__mockHandlers) ?? null Object.defineProperty(browserWindow, '__mockHandlers', { configurable: true, get() { return patchHandlers(ref) ?? ref }, set(value) { ref = patchHandlers(value as Record | undefined) ?? null }, }) } async function openFirstNote(page: Page) { const noteList = page.locator('[data-testid="note-list-container"]') await noteList.waitFor({ timeout: 5_000 }) await noteList.locator('.cursor-pointer').first().click() await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 }) } async function openAutoGitSettings(page: Page) { await openCommandPalette(page) await executeCommand(page, 'Open Settings') const settingsPanel = page.getByTestId('settings-panel') await expect(settingsPanel).toBeVisible({ timeout: 5_000 }) const autoGitSwitch = settingsPanel.getByRole('switch', { name: 'AutoGit' }) await expect(autoGitSwitch).toBeEnabled({ timeout: 5_000 }) await autoGitSwitch.click({ force: true }) await page.getByTestId('settings-autogit-idle-threshold').fill('2') await page.getByTestId('settings-autogit-inactive-threshold').fill('2') await page.getByTestId('settings-save').click() await expect(settingsPanel).not.toBeVisible({ timeout: 5_000 }) } async function expectDirtyPathCount(page: Page, expectedCount: number) { await expect.poll(async () => page.evaluate(() => (window as Window & { __getDirtyPathCount?: () => number }).__getDirtyPathCount?.() ?? 0), ).toBe(expectedCount) } async function expectCommitMessageCount(page: Page, expectedCount: number) { await expect.poll(async () => page.evaluate(() => (window as Window & { __gitCommitMessages?: string[] }).__gitCommitMessages?.length ?? 0), ).toBe(expectedCount) } async function expectCommitAttemptCount(page: Page, expectedCount: number) { await expect.poll(async () => page.evaluate(() => (window as Window & { __gitCommitAttempts?: number }).__gitCommitAttempts ?? 0), ).toBe(expectedCount) } async function expectCommitMessage(page: Page, index: number, expectedMessage: string) { await expect.poll(async () => page.evaluate( ({ targetIndex }) => (window as Window & { __gitCommitMessages?: string[] }).__gitCommitMessages?.[targetIndex] ?? '', { targetIndex: index }, ), ).toBe(expectedMessage) } async function expectPushCount(page: Page, expectedCount: number) { await expect.poll(async () => page.evaluate(() => (window as Window & { __gitPushCalls?: number }).__gitPushCalls ?? 0), ).toBe(expectedCount) } async function expectCheckpoint(page: Page, count: number) { await expectCommitMessageCount(page, count) await expectCommitMessage(page, count - 1, 'Updated 1 note') await expectPushCount(page, count) } async function seedSavedChange(page: Page) { await seedAutoGitSavedChange(page) await expectDirtyPathCount(page, 1) } async function setMockAppActive(page: Page, active: boolean) { await page.evaluate((nextActive) => { ;(window as Window & { __setMockAppActive?: (active: boolean) => void }).__setMockAppActive?.(nextActive) window.dispatchEvent(new Event(nextActive ? 'focus' : 'blur')) }, active) } async function setMockGitCommitFailure(page: Page, message: string | null) { await page.evaluate((failureMessage) => { ;(window as Window & { __setMockGitCommitFailure?: (message: string | null) => void }).__setMockGitCommitFailure?.(failureMessage) }, message) } async function triggerQuickCommit(page: Page) { const commitButton = page.getByTestId('status-commit-push') await expect(commitButton).toBeEnabled({ timeout: 5_000 }) await commitButton.click() } test('@smoke AutoGit checkpoints on idle, and the bottom bar reuses the same message', async ({ page }) => { await page.addInitScript(installAutoGitMocks) await page.goto('/') await page.waitForLoadState('networkidle') await openAutoGitSettings(page) await openFirstNote(page) await seedSavedChange(page) await expectCheckpoint(page, 1) await seedSavedChange(page) await triggerQuickCommit(page) await expectCheckpoint(page, 2) await expect(page.locator('.fixed.bottom-8')).toContainText('Committed and pushed', { timeout: 5_000 }) }) test('AutoGit checkpoints when the app is inactive', async ({ page }) => { await page.addInitScript(installAutoGitMocks) await page.goto('/') await page.waitForLoadState('networkidle') await openAutoGitSettings(page) await openFirstNote(page) await setMockAppActive(page, false) await seedSavedChange(page) await expectCheckpoint(page, 1) }) test('AutoGit does not retry a failed checkpoint until the next saved change', async ({ page }) => { await page.addInitScript(installAutoGitMocks) await page.goto('/') await page.waitForLoadState('networkidle') await openAutoGitSettings(page) await openFirstNote(page) await setMockGitCommitFailure(page, 'Author identity unknown') await seedSavedChange(page) await expectCommitAttemptCount(page, 1) await expectDirtyPathCount(page, 1) await expect(page.locator('.fixed.bottom-8')).toContainText('Set a Git author before AutoGit can commit', { timeout: 5_000 }) await page.waitForTimeout(3_500) await expectCommitAttemptCount(page, 1) await seedSavedChange(page) await expectCommitAttemptCount(page, 2) })