mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
Rows lead with the file name and put its folder under it, so names stop truncating. The whole row opens the diff, one quiet button stages or unstages, and discard moves to a left swipe that still asks first. A status line, a Commits row and a conflict banner with Abort sit above Uncommitted and Staged sections that carry Stage all and Unstage all. The rarer git actions move into the header's grouped menu.
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildMobileSourceControlSections,
|
|
canOpenMobileGitStatusEntry,
|
|
countStagedEntries,
|
|
countUnstagedEntries,
|
|
getStageablePaths,
|
|
getUnstageablePaths,
|
|
isMobileGitDiscardableEntry,
|
|
isMobileGitStageableEntry,
|
|
isMobileGitTransientRefreshError,
|
|
isMobileGitUnavailable,
|
|
type MobileGitStatusEntry
|
|
} from './mobile-git-status'
|
|
|
|
const entries: MobileGitStatusEntry[] = [
|
|
{ path: 'b.ts', status: 'modified', area: 'staged' },
|
|
{ path: 'a.ts', status: 'modified', area: 'unstaged' },
|
|
{ path: 'new.ts', status: 'untracked', area: 'untracked' }
|
|
]
|
|
|
|
describe('mobile source control status helpers', () => {
|
|
it('builds sections in the mobile source control order', () => {
|
|
const sections = buildMobileSourceControlSections(entries, 'changes-first')
|
|
|
|
expect(sections.map((section) => section.title)).toEqual(['Uncommitted', 'Staged'])
|
|
})
|
|
|
|
it('computes actionable path sets', () => {
|
|
expect(countUnstagedEntries(entries)).toBe(2)
|
|
expect(countStagedEntries(entries)).toBe(1)
|
|
expect(getStageablePaths(entries)).toEqual(['a.ts', 'new.ts'])
|
|
expect(getUnstageablePaths(entries)).toEqual(['b.ts'])
|
|
})
|
|
|
|
it('keeps unresolved conflicts out of stage actions', () => {
|
|
const conflictedEntries: MobileGitStatusEntry[] = [
|
|
{ path: 'ready.ts', status: 'modified', area: 'unstaged' },
|
|
{
|
|
path: 'conflicted.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
},
|
|
{
|
|
path: 'resolved.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
}
|
|
]
|
|
|
|
expect(getStageablePaths(conflictedEntries)).toEqual(['ready.ts', 'resolved.ts'])
|
|
expect(isMobileGitStageableEntry(conflictedEntries[1])).toBe(false)
|
|
expect(isMobileGitDiscardableEntry(conflictedEntries[1])).toBe(false)
|
|
expect(isMobileGitDiscardableEntry(conflictedEntries[2])).toBe(false)
|
|
})
|
|
|
|
it('allows opening deleted files so pre-delete text/image diffs can load', () => {
|
|
expect(
|
|
canOpenMobileGitStatusEntry({ path: 'deleted.png', status: 'deleted', area: 'unstaged' })
|
|
).toBe(true)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({ path: 'logo.png', status: 'modified', area: 'unstaged' })
|
|
).toBe(true)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({
|
|
path: 'conflict.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({
|
|
path: 'resolved.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('pins conflicts into their own section ahead of changes, like desktop', () => {
|
|
const sections = buildMobileSourceControlSections(
|
|
[
|
|
{ path: 'zeta.ts', status: 'modified', area: 'unstaged' },
|
|
{
|
|
path: 'beta.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
},
|
|
{
|
|
path: 'alpha.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
},
|
|
{ path: 'aardvark.ts', status: 'added', area: 'unstaged' }
|
|
],
|
|
'changes-first'
|
|
)
|
|
|
|
expect(
|
|
sections.map((section) => [section.title, section.data.map((entry) => entry.path)])
|
|
).toEqual([
|
|
['Conflicts', ['alpha.ts', 'beta.ts']],
|
|
['Uncommitted', ['aardvark.ts', 'zeta.ts']]
|
|
])
|
|
})
|
|
|
|
it('recognizes old-desktop unavailable responses', () => {
|
|
expect(isMobileGitUnavailable('forbidden', 'Method is not available to mobile clients')).toBe(
|
|
true
|
|
)
|
|
expect(isMobileGitUnavailable('method_not_found', 'Unknown method')).toBe(true)
|
|
expect(isMobileGitUnavailable('bad_request', 'Missing worktree selector')).toBe(false)
|
|
})
|
|
|
|
it('recognizes transient status refresh aborts', () => {
|
|
expect(isMobileGitTransientRefreshError('runtime_error', 'Aborting')).toBe(true)
|
|
expect(isMobileGitTransientRefreshError('request_aborted', 'request_aborted')).toBe(true)
|
|
expect(isMobileGitTransientRefreshError('runtime_error', 'fatal: not a git repository')).toBe(
|
|
false
|
|
)
|
|
})
|
|
})
|