test: remove repeated wiring and suspended orchestration suites (#41302)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-11 16:16:48 -08:00
committed by GitHub
co-authored by OrcaWin
parent d9d3dfdd08
commit 4509730131
9 changed files with 2 additions and 612 deletions
@@ -4699,7 +4699,6 @@
"src/renderer/src/components/github/code-host-rate-limit-stopped-wsl.test.tsx": 2457,
"src/renderer/src/components/github/github-mention-option-filter.test.ts": 52,
"src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 47,
"src/renderer/src/components/github/github-work-item-issue-target.test.ts": 324,
"src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 348,
"src/renderer/src/components/github/use-image-input.test.ts": 2619,
"src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.test.ts": 2141,
-35
View File
@@ -42,15 +42,6 @@ describe('DroidHookService', () => {
removeTreeSync(hoisted.home)
})
it('does not register PermissionRequest', () => {
const status = new DroidHookService().install()
expect(status.state).toBe('installed')
const hooks = readSettings().hooks
expect(hooks?.PermissionRequest).toBeUndefined()
expect(JSON.stringify(hooks?.Notification)).toContain('droid-hook')
})
it('removes a managed PermissionRequest command and keeps a user command', () => {
mkdirSync(dirname(settingsPath()), { recursive: true })
writeFileSync(
@@ -90,30 +81,4 @@ describe('DroidHookService', () => {
}
])
})
it('drops a previously managed PermissionRequest hook but keeps the user hook on it', () => {
const configPath = join(hoisted.home, '.factory', 'settings.json')
const userDefinition = {
matcher: 'Edit',
hooks: [{ type: 'command', command: '/usr/local/bin/my-approval.sh' }]
}
const managedCommand = join(hoisted.home, '.orca', 'agent-hooks', 'droid-hook.sh')
mkdirSync(dirname(configPath), { recursive: true })
writeFileSync(
configPath,
JSON.stringify({
hooks: {
PermissionRequest: [
{ matcher: '*', hooks: [{ type: 'command', command: managedCommand }] },
userDefinition
]
}
})
)
expect(new DroidHookService().install().state).toBe('installed')
const installed = readSettings()
expect(installed.hooks?.PermissionRequest).toEqual([userDefinition])
})
})
@@ -2,7 +2,6 @@
import { describe, expect, it, vi } from 'vitest'
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-session-contracts'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-session-contracts'
import * as selection from './mobile-session-tab-selection'
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
@@ -19,43 +18,9 @@ const WORKTREE = `id:${TEST_WORKTREE_ID}`
const CHAT = 'agent-session:s1'
/** The three saved tabs on a server with no window, whose spawns bind their pane as the host's do. */
async function headlessRuntime(count = 3) {
async function headlessRuntime() {
const harness = threeTabSurfaceRuntime()
const { runtime, bindPane } = harness
if (count > 3) {
const session = harness.store.getSession()
const tabs = session.tabsByWorktree[TEST_WORKTREE_ID]!
const extra = Array.from({ length: count - 3 }, (_, index) => ({
...tabs[0]!,
id: `00000000-0000-4000-8000-${index.toString().padStart(12, '0')}`,
ptyId: null,
sortOrder: index + 3,
createdAt: index + 4
}))
harness.setSession({
...session,
tabsByWorktree: { ...session.tabsByWorktree, [TEST_WORKTREE_ID]: [...tabs, ...extra] },
terminalLayoutsByTabId: {
...session.terminalLayoutsByTabId,
...Object.fromEntries(
extra.map((tab, index) => {
const leafId = `10000000-0000-4000-8000-${index.toString().padStart(12, '0')}`
return [
tab.id,
{ root: { type: 'leaf', leafId }, activeLeafId: leafId, expandedLeafId: null }
]
})
)
},
tabGroups: {
...session.tabGroups,
[TEST_WORKTREE_ID]: session.tabGroups![TEST_WORKTREE_ID]!.map((group) => ({
...group,
tabOrder: [...group.tabOrder, ...extra.map((tab) => tab.id)]
}))
}
})
}
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
runtime.setPtyController({
spawn: vi.fn(async (args: { worktreeId?: string; tabId?: string; leafId?: string }) => {
@@ -83,60 +48,6 @@ const orderOf = (rows: readonly { type: string; id: string; parentTabId?: string
]
describe("a phone's layout change on a windowless server (switch on)", () => {
it('bounds explicit split selection traversal at N and 10N tabs', async () => {
const measurements: number[] = []
const select = selection.selectMobileSessionTab
for (const count of [50, 500]) {
const { runtime } = await headlessRuntime(count)
let visits = 0
function observed<T>(items: T[]): T[] {
const rows = [...items]
items.forEach((item, index) => {
Object.defineProperty(rows, index, {
get() {
visits++
return item
}
})
})
return rows
}
let selectionVisits = 0
const selected = vi
.spyOn(selection, 'selectMobileSessionTab')
.mockImplementation((snapshot, active) => {
const tabs = observed(snapshot.tabs)
const tabGroups = snapshot.tabGroups?.map((group) => ({
...group,
tabOrder: observed(group.tabOrder)
}))
visits = 0
const result = select({ ...snapshot, tabs, tabGroups }, active)
selectionVisits += visits
return result
})
await runtime.moveMobileSessionTab(WORKTREE, {
kind: 'split',
tabId: SECOND_TAB_ID,
targetGroupId: SAVED_GROUP_ID,
splitDirection: 'right'
})
expect(selected).toHaveBeenCalledTimes(1)
// The original tab has two panes: resolve + flags + one row copy, plus one tab-order scan.
expect(selectionVisits).toBeGreaterThan(0)
expect(selectionVisits).toBeLessThanOrEqual(3 * (count + 1) + count)
measurements.push(selectionVisits)
const stored = runtime.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)!
expect(stored.tabs).toHaveLength(count + 1)
expect(stored.activeTabId).toBe(`${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`)
expect(stored.tabGroups?.[1]?.tabOrder).toEqual([SECOND_TAB_ID])
expect(stored.tabs.filter((row) => row.isActive).map((row) => row.id)).toEqual([
`${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`
])
selected.mockRestore()
}
expect(measurements[1]).toBeLessThanOrEqual(10 * measurements[0]!)
})
it("splits a tab into a group of its own in main's layout, and selects it there", async () => {
const { runtime, store } = await headlessRuntime()
const selected = `${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`
@@ -217,23 +128,6 @@ describe("a phone's layout change on a windowless server (switch on)", () => {
)
})
it("answers a split main refuses with main's refusal code", async () => {
const { runtime } = await headlessRuntime()
vi.spyOn(runtime, 'applyLayoutCommand').mockResolvedValue({ ok: false, code: 'invalid_params' })
await expect(
runtime.moveMobileSessionTab(WORKTREE, {
kind: 'split',
tabId: SECOND_TAB_ID,
targetGroupId: SAVED_GROUP_ID,
splitDirection: 'right'
})
).rejects.toThrow('invalid_params')
expect(tabStrip(await runtime.listMobileSessionTabs(WORKTREE))).toEqual([
[SURFACE_TAB_ID, SECOND_TAB_ID, THIRD_TAB_ID]
])
})
it.each([
{
client: 'a phone with split-group placement',
@@ -76,27 +76,6 @@ describe('app icon selection', () => {
expect(dockSetIconMock.mock.lastCall?.[0].isEmpty()).toBe(true)
})
it('keeps an imported icon out of the app bundle and leaves the live Dock tile alone', async () => {
const commands: string[][] = []
dockSetIconMock.mockClear()
persistMacDockIcon('custom', {
appBundlePath: '/Applications/Orca.app',
execFile: (file, args, _options, callback) => {
commands.push([file, ...args])
callback?.(null)
},
isDevApp: false,
platform: 'darwin'
})
await waitForQueuedPersistence()
expect(commands.some((command) => command.join(' ').includes('setIcon:image'))).toBe(false)
expect(commands.some((command) => command.join(' ').includes('setIcon:(missing value)'))).toBe(
true
)
expect(dockSetIconMock).not.toHaveBeenCalled()
})
it('serializes rapid macOS dock icon persistence so the last icon request wins', async () => {
const pendingCallbacks: (() => void)[] = []
const execFile = vi.fn(
@@ -373,25 +373,6 @@ it('refuses normalized data-image HTML without dropping surrounding text', async
expect(imageUploads.upload).not.toHaveBeenCalled()
})
it('leaves ordinary HTML paste on the editor parser path', async () => {
const view = render(<GitHubMarkdownComposer value="" onChange={() => {}} placeholder="HTML" />)
await waitFor(() => expect(view.container.querySelector('.tiptap')).not.toBeNull())
const parse = vi.spyOn(DOMParser.prototype, 'parseFromString')
try {
fireEvent.paste(getComposerEditor(view.container), {
clipboardData: {
files: [],
getData: (type: string) =>
type === 'text/html' ? '<p>ordinary <strong>formatted</strong> text</p>' : ''
}
})
expect(getComposerEditor(view.container).textContent).toBe('ordinary formatted text')
expect(parse).not.toHaveBeenCalled()
} finally {
parse.mockRestore()
}
})
it('shows a failed hosted image as its filename link without changing the uploaded Markdown', async () => {
imageUploads.upload
.mockReset()
@@ -1,145 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { runIssueUpdate, runWorkItemBodyUpdate } from './github-work-item-edit-mutations'
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
import type { TaskSourceContext } from '../../../../shared/work-items/task-source-context'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
vi.mock('@/runtime/rpc/calls/runtime-rpc-call', () => ({
callRuntimeRpc: vi.fn()
}))
vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn(() => ({ repos: [] })) } }))
vi.mock('@/components/github/github-work-item-comment-mutations', () => ({
notifyWorkItemDetailsMutation: vi.fn()
}))
const ORIGIN = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
const UPSTREAM = { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' }
const projectOrigin = {
...UPSTREAM,
number: 12,
type: 'issue' as const,
projectId: 'project-1',
projectItemId: 'project-item-12',
cacheKey: 'project-cache'
}
const localSource: TaskSourceContext = {
kind: 'task-source',
provider: 'github',
projectId: 'project-1',
hostId: 'local',
repoId: 'repo-1'
}
const item: GitHubWorkItem = {
id: 'issue:12',
type: 'issue',
number: 12,
title: 'Origin issue',
state: 'open',
url: 'https://github.com/fork-owner/widgets/issues/12',
labels: [],
updatedAt: '',
author: null,
repoId: 'repo-1'
}
describe('issue edits retain the displayed repository', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(callRuntimeRpc).mockResolvedValue({ ok: true })
vi.stubGlobal('window', {
api: {
gh: {
updateIssue: vi.fn().mockResolvedValue({ ok: true }),
updateIssueBySlug: vi.fn(),
updatePullRequestBySlug: vi.fn()
}
}
})
})
describe.each(['local', 'runtime:project-source'] as const)('Project source %s', (hostId) => {
const sourceContext = { ...localSource, hostId }
it('saves the Project row body through the original exact slug actor', async () => {
const bodyImageUrls = {
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
}
vi.mocked(window.api.gh.updateIssueBySlug).mockResolvedValue({ ok: true, bodyImageUrls })
vi.mocked(callRuntimeRpc).mockResolvedValue({ ok: true, bodyImageUrls })
const result = await runWorkItemBodyUpdate({
item: { ...item, url: 'https://github.com/upstream-owner/widgets/issues/12' },
repoPath: '/home/fixture/fork-widgets',
sourceContext,
projectOrigin,
body: 'Project edit',
parsedSlug: UPSTREAM
})
expect(result).toEqual({ bodyImageUrls })
})
})
it.each([
{ state: 'closed' as const },
{ addLabels: ['bug'] },
{ addAssignees: ['fork-assignee'] }
])('pins field edits to the opened issue repository: %j', async (updates) => {
await runIssueUpdate({
repoPath: 'C:\\workspace\\widgets',
repoId: 'repo-1',
projectOrigin: undefined,
number: 12,
issueRepo: ORIGIN,
updates
})
expect(window.api.gh.updateIssue).toHaveBeenCalledWith(
expect.objectContaining({
repoPath: 'C:\\workspace\\widgets',
ownerRepo: ORIGIN,
updates
})
)
expect(window.api.gh.updateIssueBySlug).not.toHaveBeenCalled()
})
it.each([true, false])(
'forwards local PR body PATCH maps without adding a reread (%s)',
async (hasMap) => {
const body =
'![shot](https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488)'
vi.mocked(window.api.gh.updatePullRequestBySlug).mockResolvedValue(
hasMap
? {
ok: true,
bodyImageUrls: {
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
}
}
: { ok: true }
)
const result = await runWorkItemBodyUpdate({
item: { ...item, type: 'pr' },
repoPath: '/repo',
sourceContext: localSource,
projectOrigin: undefined,
body,
parsedSlug: ORIGIN
})
expect(result).toEqual({
bodyImageUrls: hasMap
? {
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
}
: undefined
})
expect(window.api.gh.updatePullRequestBySlug).toHaveBeenCalledWith(
expect.objectContaining({
updates: {
body: '![shot](https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488)'
}
})
)
}
)
})
@@ -166,41 +166,4 @@ describe('EphemeralVmsPane', () => {
expect(container.textContent).toContain('Newest Recipe')
expect(container.textContent).not.toContain('Stale Recipe')
})
it('names the credential-like variables a recipe inherits, without values', async () => {
const container = await renderPane()
await vi.waitFor(() =>
expect(container.querySelector('[data-testid="recipe-env-exposure"]')?.textContent).toContain(
'ANTHROPIC_API_KEY, GITHUB_TOKEN'
)
)
expect(container.textContent).toContain('Set passEnv on the recipe')
})
it('lists only allowlisted credentials when the recipe sets passEnv', async () => {
const listRecipeCatalog = window.api.ephemeralVm.listRecipeCatalog as ReturnType<typeof vi.fn>
listRecipeCatalog.mockReset().mockResolvedValue([
{
repoId: 'repo-1',
repoName: 'Repo',
repoPath: '/repo',
diagnostics: [],
recipes: [
{
id: 'limited',
name: 'Limited',
create: 'create',
passEnv: ['GITHUB_TOKEN']
}
]
}
])
const container = await renderPane()
await vi.waitFor(() =>
expect(container.querySelector('[data-testid="recipe-env-exposure"]')?.textContent).toContain(
'from passEnv: GITHUB_TOKEN.'
)
)
expect(container.textContent).not.toContain('ANTHROPIC_API_KEY')
})
})
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getStateMock, inspectRuntimeTerminalProcessMock, getStatusFeedMock } = vi.hoisted(() => ({
getStateMock: vi.fn(),
@@ -22,14 +22,6 @@ import { assessWindowCloseRunningWork } from './window-close-running-work'
const LOCAL_PTY = 'pty-local'
const SSH_PTY = 'ssh:openclaw@@pty-7'
const RUNTIME_PTY = 'remote:env-1@@handle-1'
const OWNERLESS_RUNTIME_PTY = 'remote:handle-2'
const BUSY = {
foregroundProcess: 'pnpm build',
hasChildProcesses: true,
foregroundProcessEvidence: {}
}
const UNVERIFIABLE = {
foregroundProcess: null,
@@ -81,41 +73,7 @@ beforeEach(() => {
getStatusFeedMock.mockReturnValue(feed(null, 'unverifiable'))
})
afterEach(() => {
vi.useRealTimers()
})
describe('assessWindowCloseRunningWork', () => {
it.each(['working', 'attention'])(
'warns for a live %s chat without a PTY or ownership metadata',
async (status) => {
setState([])
representChat()
getStatusFeedMock.mockReturnValue(feed(status))
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'running'
})
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
}
)
it.each([
['working', 'unverifiable', {}],
['idle', 'live', { hostExecutionOwned: true, children: [{ kind: 'agent', state: 'working' }] }],
['idle', 'live', { children: [{ kind: 'agent', state: 'unverifiable' }] }],
[null, 'live', {}],
[undefined, 'live', {}]
])('does not warn for status %s observed as %s', async (status, observation, extra) => {
setState([])
representChat()
getStatusFeedMock.mockReturnValue(feed(status, observation, extra))
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'none'
})
})
it('keeps equal chat IDs separate across hosts and resolves legacy tabs through their workspace owner', async () => {
setState([])
const state = getStateMock()
@@ -135,17 +93,6 @@ describe('assessWindowCloseRunningWork', () => {
})
})
it('does not use a local observation for an unserved SSH chat', async () => {
setState([])
representChat('ssh:openclaw')
getStatusFeedMock.mockReturnValue(feed('working'))
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'none'
})
expect(getStatusFeedMock).not.toHaveBeenCalled()
})
it('uses current chat membership, owner and status after terminal probes settle', async () => {
setState([LOCAL_PTY])
representChat()
@@ -183,72 +130,4 @@ describe('assessWindowCloseRunningWork', () => {
})
expect(getStatusFeedMock).not.toHaveBeenCalled()
})
// #29916: a paired runtime owns its terminals and keeps them after this client quits.
it('does not warn on quit, or probe, for a live process on a paired runtime host', async () => {
setState([RUNTIME_PTY, OWNERLESS_RUNTIME_PTY])
answerWith({ [RUNTIME_PTY]: BUSY, [OWNERLESS_RUNTIME_PTY]: BUSY })
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
kind: 'none'
})
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
})
it('still warns when closing the window over a live process on a paired runtime host', async () => {
setState([RUNTIME_PTY])
answerWith({ [RUNTIME_PTY]: BUSY })
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'running'
})
})
// Unreachable remote work is unverifiable, never proof of exit; quitting silently could orphan it.
it('warns rather than quitting silently when a remote host answers unverifiable', async () => {
setState([SSH_PTY])
answerWith({ [SSH_PTY]: UNVERIFIABLE })
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
kind: 'unverifiable'
})
})
it('reports a live process even when a sibling remote pane is only unverifiable', async () => {
setState([SSH_PTY, RUNTIME_PTY])
answerWith({ [SSH_PTY]: UNVERIFIABLE, [RUNTIME_PTY]: BUSY })
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'running'
})
})
it('still warns about a live local process when closing the window', async () => {
setState([LOCAL_PTY])
answerWith({ [LOCAL_PTY]: BUSY })
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'running'
})
})
// Local probe failure has no lost-transport ambiguity and must not block closing.
it('does not warn when only a local probe is unverifiable', async () => {
setState([LOCAL_PTY])
answerWith({ [LOCAL_PTY]: UNVERIFIABLE })
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
kind: 'none'
})
})
// Quit consents to end this machine’s processes (#524); it does not consent to end remote work.
it('leaves local-only quit unprompted, and never probes for it', async () => {
setState([LOCAL_PTY])
answerWith({ [LOCAL_PTY]: BUSY })
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
kind: 'none'
})
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
})
})
@@ -1,125 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
async function openFixture(page: Page, name: string) {
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const card = page.getByRole('complementary', { name: 'Orchestration' })
await expect(card).toBeVisible()
const rows = card.locator('.overflow-y-auto')
const bounds = await card.boundingBox()
if (!bounds) {
throw new Error('Card has no bounds')
}
// Ten workers never fit a short pane, so the rows must scroll inside the card.
expect(await rows.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
return bounds
}
test('a saved orchestration opt-in keeps the native panel hidden and composer available', async ({
page
}) => {
await page.goto(
'/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:tenWorkersShortChatExpanded'
)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
await expect(page.getByTestId('composer')).toBeVisible()
})
test('in a short terminal the card keeps the bottom quarter clear and scrolls', async ({
page
}) => {
const card = await openFixture(page, 'tenWorkersShortPaneExpanded')
const pane = await page.locator('.pane').boundingBox()
const limit = pane!.y + pane!.height * 0.75
// It reaches the limit, since the rows are taller than the room, but never passes it.
expect(card.y + card.height).toBeLessThanOrEqual(limit + 0.5)
expect(card.y + card.height).toBeGreaterThan(limit - 2)
})
test('in a terminal at its minimum split height the collapse button stays clickable', async ({
page
}) => {
await page.goto(
'/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:twoWorkersMinimumPaneExpanded'
)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const collapse = page.getByRole('button', { name: 'Collapse orchestration panel' })
const button = await collapse.boundingBox()
const pane = await page.locator('.pane').boundingBox()
// Aim at the part of the button the pane doesn't clip.
const x = button!.x + button!.width / 2
const y = (button!.y + Math.min(button!.y + button!.height, pane!.y + pane!.height)) / 2
expect(y).toBeLessThan(pane!.y + pane!.height)
await page.mouse.click(x, y)
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
})
for (const [name, width] of [
['twoWorkersNarrowPaneExpanded', 100],
['twoWorkersMinimumWidthPaneExpanded', 50]
] as const) {
test(`in a ${width}px-wide pane the whole collapse button fits inside the card`, async ({
page
}) => {
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const card = await page.getByRole('complementary', { name: 'Orchestration' }).boundingBox()
const collapse = page.getByRole('button', { name: 'Collapse orchestration panel' })
const button = await collapse.boundingBox()
expect(button!.x).toBeGreaterThanOrEqual(card!.x)
expect(button!.x + button!.width).toBeLessThanOrEqual(card!.x + card!.width + 0.5)
await collapse.click()
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
})
}
for (const name of ['terminalQuestionDetails']) {
for (const width of [280, 360]) {
test(`${name} keeps long expanded details inside a ${width}px frame`, async ({ page }) => {
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const frame = page.locator('#component-fixture-frame')
await frame.evaluate((element, frameWidth) => {
element.style.width = `${frameWidth}px`
}, width)
const card = page.getByRole('complementary', { name: 'Orchestration' })
const details = card.locator('dl')
await expect(details).toBeVisible()
await expect(details).toContainText('dispatched')
expect(await details.innerText()).toContain('unbroken'.repeat(128))
const overflow = await details.evaluate((element) => {
const card = element.closest('aside')!
const bounds = card.getBoundingClientRect()
const violations: string[] = []
for (const value of element.querySelectorAll('dd')) {
const rect = value.getBoundingClientRect()
if (rect.left < bounds.left || rect.right > bounds.right + 1) {
violations.push('detail value outside card')
}
if (value.scrollWidth > value.clientWidth + 1) {
violations.push('detail value scrolls horizontally')
}
const walker = document.createTreeWalker(value, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
const range = document.createRange()
range.selectNodeContents(walker.currentNode)
for (const line of range.getClientRects()) {
if (line.left < bounds.left || line.right > bounds.right + 1) {
violations.push('text outside card')
}
}
}
}
return violations
})
await test.info().attach('expanded-details', {
body: await frame.screenshot(),
contentType: 'image/png'
})
expect(overflow).toEqual([])
await card.getByRole('button', { name: /^Show .*task and latest message$/ }).click()
await expect(details).toHaveCount(0)
})
}
}