mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(automations): reveal full prompt from detail view (#16067)
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
import type { AutomationTargetAvailability } from './automation-target-availability'
|
||||
import { getAutomationSourceDisplay } from './automation-source-display'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { AutomationPromptDisclosure } from './AutomationPromptDisclosure'
|
||||
|
||||
type AutomationDetailProps = {
|
||||
automation: Automation | null
|
||||
@@ -301,21 +302,10 @@ export function AutomationDetail({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-border/50 bg-muted/20 shadow-sm">
|
||||
<div className="border-b border-border/50 px-3 py-2 text-sm font-medium">
|
||||
{translate('auto.components.automations.AutomationDetail.007c8ad874', 'Prompt')}
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-medium uppercase text-muted-foreground">
|
||||
{translate('auto.components.automations.AutomationDetail.007c8ad874', 'Prompt')}
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-4 whitespace-pre-wrap text-sm text-foreground">
|
||||
{automation.prompt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AutomationPromptDisclosure
|
||||
key={`${automation.id}:${automation.prompt}`}
|
||||
prompt={automation.prompt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import type React from 'react'
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Automation } from '../../../../shared/automations-types'
|
||||
import { i18n } from '@/i18n/i18n'
|
||||
import { AutomationDetail } from './AutomationDetail'
|
||||
import { AutomationPromptDisclosure } from './AutomationPromptDisclosure'
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
let promptNaturalHeight = 0
|
||||
let resizeCallback: ResizeObserverCallback | null = null
|
||||
|
||||
class PromptResizeObserver implements ResizeObserver {
|
||||
constructor(callback: ResizeObserverCallback) {
|
||||
resizeCallback = callback
|
||||
}
|
||||
|
||||
disconnect(): void {}
|
||||
observe(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
function makeAutomation(
|
||||
updatedAt: number,
|
||||
prompt = `Synthetic opening.\n${'Synthetic detail. '.repeat(80)}`
|
||||
): Automation {
|
||||
return {
|
||||
id: 'synthetic-automation',
|
||||
name: 'Synthetic automation',
|
||||
prompt,
|
||||
precheck: null,
|
||||
agentId: 'codex',
|
||||
projectId: 'synthetic-project',
|
||||
executionTargetType: 'local',
|
||||
executionTargetId: 'local',
|
||||
schedulerOwner: 'local_host_service',
|
||||
workspaceMode: 'existing',
|
||||
workspaceId: 'synthetic-workspace',
|
||||
baseBranch: null,
|
||||
reuseSession: false,
|
||||
timezone: 'UTC',
|
||||
rrule: 'FREQ=DAILY',
|
||||
dtstart: 1,
|
||||
enabled: false,
|
||||
nextRunAt: 2,
|
||||
missedRunPolicy: 'run_once_within_grace',
|
||||
missedRunGraceMinutes: 720,
|
||||
createdAt: 1,
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
const detailCallbacks = {
|
||||
onRunNow: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
onToggle: vi.fn(),
|
||||
onDelete: vi.fn()
|
||||
}
|
||||
|
||||
describe('AutomationPromptDisclosure', () => {
|
||||
beforeEach(async () => {
|
||||
await i18n.changeLanguage('en')
|
||||
vi.stubGlobal('ResizeObserver', PromptResizeObserver)
|
||||
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(
|
||||
function (this: HTMLElement) {
|
||||
if (this.tagName !== 'P') {
|
||||
return 0
|
||||
}
|
||||
return this.classList.contains('line-clamp-4')
|
||||
? Math.min(promptNaturalHeight, 80)
|
||||
: promptNaturalHeight
|
||||
}
|
||||
)
|
||||
vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(
|
||||
function (this: HTMLElement) {
|
||||
return this.tagName === 'P' ? promptNaturalHeight : 0
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup()
|
||||
resizeCallback = null
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
await i18n.changeLanguage('en')
|
||||
})
|
||||
|
||||
it('leaves a short prompt fully readable without a disclosure control', () => {
|
||||
promptNaturalHeight = 40
|
||||
render(<AutomationPromptDisclosure prompt="Synthetic short prompt." />)
|
||||
|
||||
expect(screen.getByText('Synthetic short prompt.')).toBeVisible()
|
||||
expect(screen.queryByRole('button', { name: 'Show more' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('labels the prompt correctly in Spanish', async () => {
|
||||
promptNaturalHeight = 40
|
||||
await i18n.changeLanguage('es')
|
||||
render(<AutomationPromptDisclosure prompt="Synthetic short prompt." />)
|
||||
|
||||
expect(screen.getByText('Prompt')).toBeVisible()
|
||||
expect(screen.queryByText('Inmediato')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reveals the complete long prompt from the keyboard and keeps it selectable', async () => {
|
||||
promptNaturalHeight = 240
|
||||
const prompt = `Synthetic opening.\n${'Synthetic detail. '.repeat(80)}\nSYNTHETIC-END-MARKER`
|
||||
const user = userEvent.setup()
|
||||
render(<AutomationPromptDisclosure prompt={prompt} />)
|
||||
|
||||
const content = screen.getByText(/SYNTHETIC-END-MARKER/)
|
||||
const toggle = screen.getByRole('button', { name: 'Show more' })
|
||||
expect(screen.getAllByText('Prompt')).toHaveLength(1)
|
||||
expect(content).toHaveClass('line-clamp-4', 'select-text', '[overflow-wrap:anywhere]')
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(toggle).toHaveAttribute('aria-controls', content.id)
|
||||
expect(toggle).toHaveAttribute('data-variant', 'ghost')
|
||||
expect(toggle).toHaveAttribute('data-size', 'xs')
|
||||
expect(toggle).not.toHaveClass('h-auto', 'p-0')
|
||||
|
||||
await user.tab()
|
||||
expect(toggle).toHaveFocus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Show less' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true'
|
||||
)
|
||||
expect(content).not.toHaveClass('line-clamp-4')
|
||||
expect(content).toHaveTextContent('SYNTHETIC-END-MARKER')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Show less' }))
|
||||
expect(screen.getByRole('button', { name: 'Show more' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false'
|
||||
)
|
||||
expect(content).toHaveClass('line-clamp-4')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Show more' }))
|
||||
act(() => resizeCallback?.([], {} as ResizeObserver))
|
||||
expect(screen.getByRole('button', { name: 'Show less' })).toHaveFocus()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Show less' }))
|
||||
expect(screen.getByRole('button', { name: 'Show more' })).toHaveFocus()
|
||||
expect(content).toHaveClass('line-clamp-4')
|
||||
})
|
||||
|
||||
it('offers disclosure after a narrow resize makes the prompt overflow', () => {
|
||||
promptNaturalHeight = 60
|
||||
render(<AutomationPromptDisclosure prompt="Synthetic prompt that reflows at narrow widths." />)
|
||||
expect(screen.queryByRole('button', { name: 'Show more' })).not.toBeInTheDocument()
|
||||
|
||||
promptNaturalHeight = 180
|
||||
act(() => resizeCallback?.([], {} as ResizeObserver))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Show more' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('moves focus to the fully visible prompt when resizing removes the disclosure', () => {
|
||||
promptNaturalHeight = 180
|
||||
render(<AutomationPromptDisclosure prompt="Synthetic prompt that fits after widening." />)
|
||||
const content = screen.getByText('Synthetic prompt that fits after widening.')
|
||||
screen.getByRole('button', { name: 'Show more' }).focus()
|
||||
|
||||
promptNaturalHeight = 60
|
||||
act(() => resizeCallback?.([], {} as ResizeObserver))
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Show more' })).not.toBeInTheDocument()
|
||||
expect(content).toHaveFocus()
|
||||
})
|
||||
|
||||
it('preserves expansion and focus across unrelated automation updates', async () => {
|
||||
promptNaturalHeight = 240
|
||||
const user = userEvent.setup()
|
||||
const { rerender } = render(
|
||||
<AutomationDetail
|
||||
automation={makeAutomation(1)}
|
||||
runs={[]}
|
||||
projectName="Synthetic project"
|
||||
workspaceName="Synthetic workspace"
|
||||
projectDefaultBaseRef={null}
|
||||
runNowAvailability={null}
|
||||
now={1}
|
||||
{...detailCallbacks}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Show more' }))
|
||||
const showLess = screen.getByRole('button', { name: 'Show less' })
|
||||
showLess.focus()
|
||||
|
||||
rerender(
|
||||
<AutomationDetail
|
||||
automation={makeAutomation(2)}
|
||||
runs={[]}
|
||||
projectName="Synthetic project"
|
||||
workspaceName="Synthetic workspace"
|
||||
projectDefaultBaseRef={null}
|
||||
runNowAvailability={null}
|
||||
now={2}
|
||||
{...detailCallbacks}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Show less' })).toHaveFocus()
|
||||
|
||||
promptNaturalHeight = 40
|
||||
rerender(
|
||||
<AutomationDetail
|
||||
automation={makeAutomation(3, 'Synthetic replacement prompt.')}
|
||||
runs={[]}
|
||||
projectName="Synthetic project"
|
||||
workspaceName="Synthetic workspace"
|
||||
projectDefaultBaseRef={null}
|
||||
runNowAvailability={null}
|
||||
now={3}
|
||||
{...detailCallbacks}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Synthetic replacement prompt.')).toBeVisible()
|
||||
expect(screen.queryByRole('button', { name: 'Show less' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export function AutomationPromptDisclosure({ prompt }: { prompt: string }): React.JSX.Element {
|
||||
const contentId = useId()
|
||||
const expandedRef = useRef(false)
|
||||
const toggleRef = useRef<HTMLButtonElement>(null)
|
||||
const [promptElement, setPromptElement] = useState<HTMLParagraphElement | null>(null)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [overflows, setOverflows] = useState(false)
|
||||
|
||||
const measureOverflow = useCallback((element: HTMLParagraphElement) => {
|
||||
const nextOverflows = element.scrollHeight > element.clientHeight + 1
|
||||
if (!nextOverflows && document.activeElement === toggleRef.current) {
|
||||
element.focus({ preventScroll: true })
|
||||
}
|
||||
setOverflows((current) => (current === nextOverflows ? current : nextOverflows))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!promptElement || expanded) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateOverflow = () => {
|
||||
if (!expandedRef.current) {
|
||||
measureOverflow(promptElement)
|
||||
}
|
||||
}
|
||||
updateOverflow()
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
window.addEventListener('resize', updateOverflow)
|
||||
return () => window.removeEventListener('resize', updateOverflow)
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(updateOverflow)
|
||||
observer.observe(promptElement)
|
||||
return () => observer.disconnect()
|
||||
}, [expanded, measureOverflow, promptElement])
|
||||
|
||||
const toggleExpanded = (): void => {
|
||||
const nextExpanded = !expanded
|
||||
expandedRef.current = nextExpanded
|
||||
setExpanded(nextExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border/50 bg-muted/20 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-border/50 px-3 py-2">
|
||||
<div className="text-sm font-medium">
|
||||
{translate('auto.components.automations.AutomationEditorDialog.058c23cb3f', 'Prompt')}
|
||||
</div>
|
||||
{overflows || expanded ? (
|
||||
<Button
|
||||
ref={toggleRef}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="-mr-2 text-muted-foreground hover:text-foreground"
|
||||
aria-expanded={expanded}
|
||||
aria-controls={contentId}
|
||||
onClick={toggleExpanded}
|
||||
>
|
||||
{expanded
|
||||
? translate(
|
||||
'auto.components.automations.AutomationPromptDisclosure.showLess',
|
||||
'Show less'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.automations.AutomationPromptDisclosure.showMore',
|
||||
'Show more'
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 px-3 py-3">
|
||||
<p
|
||||
ref={setPromptElement}
|
||||
id={contentId}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'select-text whitespace-pre-wrap text-sm text-foreground [overflow-wrap:anywhere]',
|
||||
!expanded && 'line-clamp-4'
|
||||
)}
|
||||
>
|
||||
{prompt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15008,6 +15008,10 @@
|
||||
"0e1de0358b": "Month",
|
||||
"77e96bded6": "Weekday"
|
||||
},
|
||||
"AutomationPromptDisclosure": {
|
||||
"showLess": "Show less",
|
||||
"showMore": "Show more"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"007c8ad874": "Prompt",
|
||||
"a1d52c2189": "Usage coverage",
|
||||
|
||||
@@ -13612,6 +13612,10 @@
|
||||
"0e1de0358b": "Mes",
|
||||
"77e96bded6": "Día de la semana"
|
||||
},
|
||||
"AutomationPromptDisclosure": {
|
||||
"showLess": "Mostrar menos",
|
||||
"showMore": "Mostrar más"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"007c8ad874": "Inmediato",
|
||||
"a1d52c2189": "Cobertura de uso",
|
||||
|
||||
@@ -13612,6 +13612,10 @@
|
||||
"0e1de0358b": "月",
|
||||
"77e96bded6": "曜日"
|
||||
},
|
||||
"AutomationPromptDisclosure": {
|
||||
"showLess": "折りたたむ",
|
||||
"showMore": "さらに表示"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"007c8ad874": "プロンプト",
|
||||
"a1d52c2189": "使用範囲",
|
||||
|
||||
@@ -13641,6 +13641,10 @@
|
||||
"0e1de0358b": "월",
|
||||
"77e96bded6": "요일"
|
||||
},
|
||||
"AutomationPromptDisclosure": {
|
||||
"showLess": "접기",
|
||||
"showMore": "더 보기"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"007c8ad874": "프롬프트",
|
||||
"a1d52c2189": "사용 범위",
|
||||
|
||||
@@ -13632,6 +13632,10 @@
|
||||
"0e1de0358b": "月",
|
||||
"77e96bded6": "星期"
|
||||
},
|
||||
"AutomationPromptDisclosure": {
|
||||
"showLess": "收起",
|
||||
"showMore": "显示更多"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"007c8ad874": "提示词",
|
||||
"a1d52c2189": "使用范围",
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
|
||||
const SHORT_NAME = 'synthetic-short-prompt-demo'
|
||||
const RESIZE_NAME = 'synthetic-resize-prompt-demo'
|
||||
const LONG_NAME = 'visual-proof-long-prompt-demo'
|
||||
const END_MARKER = 'SYNTHETIC-END-MARKER'
|
||||
const RESIZE_PROMPT = `Synthetic resize focus validation. ${'placeholder '.repeat(24)}`
|
||||
|
||||
test('automation detail keeps short prompts readable and reveals a very long prompt at narrow width', async ({
|
||||
orcaPage
|
||||
}) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await orcaPage.setViewportSize({ width: 820, height: 700 })
|
||||
|
||||
await orcaPage.evaluate(
|
||||
async ({ shortName, resizeName, resizePrompt, longName, endMarker }) => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('window.__store is not available')
|
||||
}
|
||||
const repo = store.getState().repos[0]
|
||||
if (!repo) {
|
||||
throw new Error('Seeded test repo is not available')
|
||||
}
|
||||
const base = {
|
||||
agentId: 'codex' as const,
|
||||
projectId: repo.id,
|
||||
workspaceMode: 'new_per_run' as const,
|
||||
reuseSession: false,
|
||||
timezone: 'UTC',
|
||||
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
|
||||
dtstart: Date.now(),
|
||||
enabled: false,
|
||||
missedRunGraceMinutes: 720
|
||||
}
|
||||
await window.api.automations.create({
|
||||
...base,
|
||||
name: shortName,
|
||||
prompt: 'Synthetic short prompt.'
|
||||
})
|
||||
await window.api.automations.create({
|
||||
...base,
|
||||
name: longName,
|
||||
prompt: [
|
||||
'Synthetic prompt preview validation only.',
|
||||
'',
|
||||
...Array.from(
|
||||
{ length: 20 },
|
||||
(_, index) =>
|
||||
`Synthetic step ${index + 1}: inspect placeholder input and summarize placeholder output.`
|
||||
),
|
||||
'',
|
||||
`UNBROKEN_SYNTHETIC_${'X'.repeat(240)}`,
|
||||
'',
|
||||
endMarker
|
||||
].join('\n')
|
||||
})
|
||||
await window.api.automations.create({
|
||||
...base,
|
||||
name: resizeName,
|
||||
prompt: resizePrompt
|
||||
})
|
||||
store.getState().openAutomationsPage()
|
||||
},
|
||||
{
|
||||
shortName: SHORT_NAME,
|
||||
resizeName: RESIZE_NAME,
|
||||
resizePrompt: RESIZE_PROMPT,
|
||||
longName: LONG_NAME,
|
||||
endMarker: END_MARKER
|
||||
}
|
||||
)
|
||||
|
||||
await orcaPage.getByRole('button', { name: new RegExp(`^${SHORT_NAME}`) }).click()
|
||||
await expect(orcaPage.getByText('Synthetic short prompt.')).toBeVisible()
|
||||
await expect(orcaPage.getByRole('button', { name: 'Show more' })).toHaveCount(0)
|
||||
|
||||
await orcaPage.getByRole('button', { name: 'All automations' }).click()
|
||||
await orcaPage.getByRole('button', { name: new RegExp(`^${RESIZE_NAME}`) }).click()
|
||||
const resizePrompt = orcaPage.getByText(RESIZE_PROMPT)
|
||||
const resizeToggle = orcaPage.getByRole('button', { name: 'Show more' })
|
||||
await expect(resizeToggle).toBeVisible()
|
||||
await resizeToggle.focus()
|
||||
await orcaPage.setViewportSize({ width: 1400, height: 700 })
|
||||
await expect(resizeToggle).toHaveCount(0)
|
||||
await expect(resizePrompt).toBeFocused()
|
||||
|
||||
await orcaPage.setViewportSize({ width: 820, height: 700 })
|
||||
await orcaPage.getByRole('button', { name: 'All automations' }).click()
|
||||
await orcaPage.getByRole('button', { name: new RegExp(`^${LONG_NAME}`) }).click()
|
||||
|
||||
const prompt = orcaPage.getByText(new RegExp(END_MARKER))
|
||||
const showMore = orcaPage.getByRole('button', { name: 'Show more' })
|
||||
await expect(showMore).toBeVisible()
|
||||
expect(await showMore.getAttribute('aria-controls')).toBe(await prompt.getAttribute('id'))
|
||||
const collapsedMetrics = await prompt.evaluate((element) => ({
|
||||
clientHeight: element.clientHeight,
|
||||
scrollHeight: element.scrollHeight,
|
||||
lineClamp: getComputedStyle(element).webkitLineClamp
|
||||
}))
|
||||
expect(collapsedMetrics.scrollHeight).toBeGreaterThan(collapsedMetrics.clientHeight)
|
||||
expect(collapsedMetrics.lineClamp).toBe('4')
|
||||
|
||||
await showMore.focus()
|
||||
await orcaPage.keyboard.press('Enter')
|
||||
await expect(orcaPage.getByRole('button', { name: 'Show less' })).toBeFocused()
|
||||
|
||||
const expandedMetrics = await prompt.evaluate((element) => ({
|
||||
clientHeight: element.clientHeight,
|
||||
scrollHeight: element.scrollHeight,
|
||||
scrollWidth: element.scrollWidth,
|
||||
clientWidth: element.clientWidth,
|
||||
lineClamp: getComputedStyle(element).webkitLineClamp,
|
||||
overflowWrap: getComputedStyle(element).overflowWrap
|
||||
}))
|
||||
expect(expandedMetrics.clientHeight).toBeGreaterThan(80)
|
||||
expect(expandedMetrics.scrollHeight).toBeLessThanOrEqual(expandedMetrics.clientHeight + 1)
|
||||
expect(expandedMetrics.scrollWidth).toBeLessThanOrEqual(expandedMetrics.clientWidth + 1)
|
||||
expect(expandedMetrics.lineClamp).toBe('none')
|
||||
expect(expandedMetrics.overflowWrap).toBe('anywhere')
|
||||
|
||||
const markerProof = await prompt.evaluate(async (element, marker) => {
|
||||
const text = element.firstChild
|
||||
if (!(text instanceof Text)) {
|
||||
throw new Error('Prompt text node is unavailable')
|
||||
}
|
||||
const start = text.data.indexOf(marker)
|
||||
if (start === -1) {
|
||||
throw new Error('Prompt end marker is unavailable')
|
||||
}
|
||||
const range = document.createRange()
|
||||
range.setStart(text, start)
|
||||
range.setEnd(text, start + marker.length)
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
const scrollContainer = element.closest('[role="tabpanel"]')
|
||||
if (!(scrollContainer instanceof HTMLElement)) {
|
||||
throw new Error('Automation overview scroll container is unavailable')
|
||||
}
|
||||
scrollContainer.scrollTop +=
|
||||
range.getBoundingClientRect().bottom - scrollContainer.getBoundingClientRect().bottom + 16
|
||||
await new Promise(requestAnimationFrame)
|
||||
const markerRect = range.getBoundingClientRect()
|
||||
const containerRect = scrollContainer.getBoundingClientRect()
|
||||
return {
|
||||
selectedText: selection?.toString(),
|
||||
markerTop: markerRect.top,
|
||||
markerBottom: markerRect.bottom,
|
||||
visibleTop: containerRect.top,
|
||||
visibleBottom: containerRect.bottom
|
||||
}
|
||||
}, END_MARKER)
|
||||
expect(markerProof.selectedText).toBe(END_MARKER)
|
||||
expect(markerProof.markerTop).toBeGreaterThanOrEqual(markerProof.visibleTop)
|
||||
expect(markerProof.markerBottom).toBeLessThanOrEqual(markerProof.visibleBottom)
|
||||
})
|
||||
Reference in New Issue
Block a user