Files
orca/tests/e2e/diff-note-draft.spec.ts
T
Jinjing 0bad8b7490 Better add ai notes ui (#21719)
* feat(diff-comments): draft inline notes as editor view zones

Move comment drafting from floating popover to inline view zone. The draft card now appears in the editor flow, preventing overlap with code and integrating naturally with the diff layout. Includes styled margin indicator, auto-resizing textarea, and keyboard/submission handling.

* Preserve inline draft comments when switching diff views

- Reanchor draft zones to new models when file/line mapping changes
- Disable draft mode on large diffs to maintain performance
- Enhance draft card UX: shadow depth, outside-click handling, toast errors
- Carry draft body and position when reopening comments

* Ensure draft comment textarea auto-focuses and preserve drafts through e

- Focus textarea on mount via requestAnimationFrame for reliable focusing
- Add onDomNodeTop callback to focus textarea when zone reaches viewport
- Preserve pending draft when editor model refreshes and re-anchor on reload
- Add editor.getModel() checks before opening and re-anchoring drafts
- Test that textarea is focused on creation and errors are surfaced

* fix(diff-comments): prevent draft loss and duplicate submission on swap

- Track submission state to prevent carrying in-flight text to new cards
- Restore failed submissions for retry after model swap with unmount safety
- Use effects for proper ref management per React patterns
- Add isDraftOpen() guard to prevent re-opening the keyboard chord while composing
- Separate concerns between user clicks and draft-open state in decorator

* fix(diff-comments): show save error and restore focus intelligently

- Toast error when draft submission fails, so users see why it didn't save
- Return focus to editor only if the card still holds it when save completes, preventing focus theft on slow saves

* minor fix

* fix(diff-comments): park focus before submit button disables

Chromium moves focus to <body> when a focused button becomes
disabled, causing the draft zone to lose focus context. By
explicitly moving focus to the textarea before the button disables,
the zone can properly return focus to the editor after save.

* add all translation
2026-09-26 11:09:36 -07:00

121 lines
4.9 KiB
TypeScript

import type { Page, TestInfo } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
const DRAFT_LINE = 6
const FOLLOWING_LINE = 'export const line07 = "draft-following-line-marker"'
const NOTE_BODY = 'This note was added from the inline draft card.'
async function attachDiffScreenshot(page: Page, testInfo: TestInfo, name: string): Promise<void> {
const screenshotPath = testInfo.outputPath(`${name}.png`)
await page.locator('.monaco-diff-editor').first().screenshot({ path: screenshotPath })
await testInfo.attach(name, { path: screenshotPath, contentType: 'image/png' })
}
test.describe('Diff note draft', () => {
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
})
test('opens an inline draft without overlapping code and saves it', async ({
orcaPage
}, testInfo) => {
await orcaPage.setViewportSize({ width: 1200, height: 800 })
const worktreeId = await waitForActiveWorktree(orcaPage)
const relativePath = await orcaPage.evaluate(async (wId) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
const worktree = Object.values(state.worktreesByRepo)
.flat()
.find((entry) => entry.id === wId)
if (!worktree) {
throw new Error('active worktree not found')
}
const separator = worktree.path.includes('\\') ? '\\' : '/'
const relative = `src${separator}diff-note-draft.ts`
const lines = Array.from({ length: 14 }, (_, index) => {
const number = String(index + 1).padStart(2, '0')
const value = index + 1 === 7 ? 'draft-following-line-marker' : `value-${number}`
return `export const line${number} = "${value}"`
})
await window.api.fs.writeFile({
filePath: `${worktree.path}${separator}${relative}`,
content: `${lines.join('\n')}\n`
})
await state.updateSettings({ diffDefaultView: 'side-by-side' })
state.openDiff(wId, `${worktree.path}${separator}${relative}`, relative, 'typescript', false)
return relative
}, worktreeId)
const followingLine = orcaPage
.locator('.modified-in-monaco-diff-editor .view-lines .view-line')
.filter({ hasText: FOLLOWING_LINE })
.first()
await expect(followingLine).toBeVisible({ timeout: 15_000 })
// Let the filesystem watcher finish its model refresh before opening a draft in that model.
await orcaPage.waitForTimeout(3_000)
const draftLine = orcaPage
.locator('.modified-in-monaco-diff-editor .view-lines .view-line')
.filter({ hasText: `export const line${String(DRAFT_LINE).padStart(2, '0')} = "value-06"` })
.first()
await draftLine.hover({ position: { x: 4, y: 8 } })
const addButton = orcaPage.locator('.orca-diff-comment-add-btn')
await expect(addButton).toBeVisible()
await addButton.click()
const draftCard = orcaPage.locator('.orca-diff-comment-draft-card')
const textarea = draftCard.locator('textarea')
await expect(draftCard).toBeVisible({ timeout: 15_000 })
await expect(draftCard).toContainText('Line 6')
await expect(draftCard).not.toContainText('You')
await expect
.poll(() => textarea.evaluate((element) => document.activeElement === element))
.toBe(true)
await expect(orcaPage.locator('.orca-diff-comment-draft-margin')).toBeVisible()
await expect
.poll(
async () => {
const [cardBox, lineBox] = await Promise.all([
draftCard.boundingBox(),
followingLine.boundingBox()
])
return cardBox && lineBox ? lineBox.y - (cardBox.y + cardBox.height) : -1
},
{ message: 'inline draft overlaps the following diff line' }
)
.toBeGreaterThanOrEqual(0)
await attachDiffScreenshot(orcaPage, testInfo, 'inline-diff-note-draft')
await textarea.fill(NOTE_BODY)
const submitButton = draftCard.locator('button').filter({ hasText: /add note/i })
await expect(submitButton).toBeEnabled()
await submitButton.click()
await expect(draftCard).toBeHidden()
const savedCard = orcaPage
.locator('.orca-diff-comment-card')
.filter({ hasText: NOTE_BODY })
.first()
await expect(savedCard).toBeVisible({ timeout: 15_000 })
await expect
.poll(
async () =>
await orcaPage.evaluate(
({ wId, filePath, body }) => {
const comments = window.__store?.getState().getDiffComments(wId) ?? []
return comments.some(
(comment) => comment.filePath === filePath && comment.body === body
)
},
{ wId: worktreeId, filePath: relativePath, body: NOTE_BODY }
)
)
.toBe(true)
await attachDiffScreenshot(orcaPage, testInfo, 'saved-inline-diff-note')
})
})