mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
545 lines
20 KiB
TypeScript
545 lines
20 KiB
TypeScript
import type { ElectronApplication, Page, TestInfo } from '@stablyai/playwright-test'
|
||
import { test, expect } from './helpers/orca-app'
|
||
import {
|
||
ensureTerminalVisible,
|
||
getActiveWorktreeId,
|
||
getAllWorktreeIds,
|
||
switchToWorktree,
|
||
waitForSessionReady,
|
||
waitForActiveWorktree
|
||
} from './helpers/store'
|
||
import {
|
||
splitActiveTerminalPane,
|
||
waitForActivePanePtyId,
|
||
waitForActiveTerminalManager
|
||
} from './helpers/terminal'
|
||
|
||
// #1532: drive production fit/driver IPC → renderer state → banner; DOM assertions cover mounting/dismissal and screenshots expose terminal-obscuring scrims.
|
||
|
||
test.describe.configure({ mode: 'serial' })
|
||
|
||
test('mobile subscribe mounts overlay; collapse → chip; Take back dismisses', async ({
|
||
orcaPage,
|
||
electronApp
|
||
}, testInfo) => {
|
||
await waitForSessionReady(orcaPage)
|
||
await waitForActiveWorktree(orcaPage)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||
await installRestoreTerminalFitRecorder(electronApp)
|
||
|
||
const overlay = orcaPage.locator('.mobile-driver-banner')
|
||
await expect(overlay).toHaveCount(0)
|
||
|
||
// Send the auto-subscribe fit/driver events that main emits to mount the loud overlay.
|
||
await sendMobileSubscribeIpc(electronApp, { ptyId, cols: 45, rows: 20 })
|
||
|
||
await expect(overlay).toBeVisible({ timeout: 15_000 })
|
||
await expect(overlay).toContainText(/your phone is in control/i)
|
||
await expectExpandedOverlayLeavesPaneReadable(orcaPage, ptyId)
|
||
|
||
const takeBackThisTerminal = overlay.getByRole('button', { name: /^take back$/i })
|
||
const takeBackAllTerminals = overlay.getByRole('button', { name: /^take back all$/i })
|
||
const collapse = overlay.getByRole('button', { name: /^minimize$/i })
|
||
await expect(takeBackThisTerminal).toBeVisible()
|
||
await expect(takeBackAllTerminals).toBeVisible()
|
||
await expect(collapse).toBeVisible()
|
||
|
||
await captureAttachment(orcaPage, testInfo, 'overlay-loud.png')
|
||
|
||
// Minimize keeps the lock while showing a compact take-back chip over live output.
|
||
await collapse.click()
|
||
await expect(overlay).toContainText(/phone driving/i)
|
||
await expect(overlay.getByRole('button', { name: /take back/i })).toBeVisible()
|
||
await expect(overlay).not.toContainText(/your phone is in control/i)
|
||
await expectChipIsCompactInPane(orcaPage, ptyId)
|
||
|
||
await captureAttachment(orcaPage, testInfo, 'overlay-collapsed.png')
|
||
|
||
await overlay.getByRole('button', { name: /phone driving/i }).click()
|
||
await expect(overlay).toContainText(/your phone is in control/i)
|
||
await expectExpandedOverlayLeavesPaneReadable(orcaPage, ptyId)
|
||
|
||
await collapse.click()
|
||
await expect(overlay).not.toContainText(/your phone is in control/i)
|
||
|
||
// Mirror main’s desktop-fit and driver events after proving Take back called the production IPC.
|
||
await overlay.getByRole('button', { name: /take back/i }).click()
|
||
await expectRestoreTerminalFitCalls(electronApp, [ptyId])
|
||
await sendDesktopRestoreIpc(electronApp, { ptyId })
|
||
await expect(overlay).toBeHidden({ timeout: 15_000 })
|
||
})
|
||
|
||
test('held phone-fit state mounts restore overlay without collapse', async ({
|
||
orcaPage,
|
||
electronApp
|
||
}, testInfo) => {
|
||
await waitForSessionReady(orcaPage)
|
||
await waitForActiveWorktree(orcaPage)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||
await installRestoreTerminalFitRecorder(electronApp)
|
||
|
||
const overlay = orcaPage.locator('.mobile-driver-banner')
|
||
await expect(overlay).toHaveCount(0)
|
||
|
||
// After disconnect, idle driver plus held phone fit stays loud because no mobile output remains to watch.
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId, cols: 45, rows: 20 })
|
||
|
||
await expect(overlay).toBeVisible({ timeout: 15_000 })
|
||
await expect(overlay).toContainText(/still sized for your phone/i)
|
||
await expect(overlay.getByRole('button', { name: /^restore$/i })).toBeVisible()
|
||
await expect(overlay.getByRole('button', { name: /^restore all$/i })).toBeVisible()
|
||
await expect(overlay.getByRole('button', { name: /^minimize$/i })).toHaveCount(0)
|
||
await expect(overlay.getByRole('button', { name: /take back/i })).toHaveCount(0)
|
||
await expectExpandedOverlayLeavesPaneReadable(orcaPage, ptyId)
|
||
|
||
await captureAttachment(orcaPage, testInfo, 'overlay-held-fit.png')
|
||
|
||
await overlay.getByRole('button', { name: /^restore$/i }).click()
|
||
await expectRestoreTerminalFitCalls(electronApp, [ptyId])
|
||
await sendDesktopRestoreIpc(electronApp, { ptyId })
|
||
await expect(overlay).toBeHidden({ timeout: 15_000 })
|
||
})
|
||
|
||
test('restore refits the active restored pane', async ({ orcaPage, electronApp }) => {
|
||
await waitForSessionReady(orcaPage)
|
||
await waitForActiveWorktree(orcaPage)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||
await installRestoreTerminalFitAutoRestoreRecorder(electronApp)
|
||
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId, cols: 1, rows: 20 })
|
||
await expect(orcaPage.locator('.mobile-driver-banner')).toHaveCount(1, { timeout: 15_000 })
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, ptyId), {
|
||
message: 'test harness should hold the active pane in the bad narrow state'
|
||
})
|
||
.toBeLessThanOrEqual(2)
|
||
|
||
await orcaPage
|
||
.locator(`[data-pty-id="${ptyId}"] .mobile-driver-banner`)
|
||
.getByRole('button', { name: /^restore$/i })
|
||
.click()
|
||
|
||
await expectRestoreTerminalFitCalls(electronApp, [ptyId])
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, ptyId), {
|
||
timeout: 5_000,
|
||
message: 'Restore should refit the active restored pane'
|
||
})
|
||
.toBeGreaterThan(20)
|
||
})
|
||
|
||
test('restore all refits non-focused restored terminal panes', async ({
|
||
orcaPage,
|
||
electronApp
|
||
}) => {
|
||
await waitForSessionReady(orcaPage)
|
||
await waitForActiveWorktree(orcaPage)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
await splitActiveTerminalPane(orcaPage, 'vertical')
|
||
const ptyIds = await waitForVisiblePanePtyIds(orcaPage, 2)
|
||
const focusPtyId = await waitForActivePanePtyId(orcaPage)
|
||
const inactivePtyId = ptyIds.find((ptyId) => ptyId !== focusPtyId)
|
||
if (!inactivePtyId || !focusPtyId) {
|
||
throw new Error('Expected two visible terminal panes with PTY bindings')
|
||
}
|
||
await installRestoreTerminalFitAutoRestoreRecorder(electronApp)
|
||
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId: inactivePtyId, cols: 45, rows: 20 })
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId: focusPtyId, cols: 45, rows: 20 })
|
||
await expect(orcaPage.locator('.mobile-driver-banner')).toHaveCount(2, { timeout: 15_000 })
|
||
|
||
await forcePaneToOneColumn(orcaPage, inactivePtyId)
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, inactivePtyId), {
|
||
message: 'test harness should force the non-focused pane into the bad narrow state'
|
||
})
|
||
.toBeLessThanOrEqual(2)
|
||
|
||
await orcaPage
|
||
.locator(`[data-pty-id="${focusPtyId}"] .mobile-driver-banner`)
|
||
.getByRole('button', { name: /^restore all$/i })
|
||
.click()
|
||
|
||
await expectRestoreTerminalFitCallSet(electronApp, [inactivePtyId, focusPtyId])
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, inactivePtyId), {
|
||
timeout: 5_000,
|
||
message: 'Restore all should refit the non-focused restored pane'
|
||
})
|
||
.toBeGreaterThan(20)
|
||
})
|
||
|
||
test('restore all recovers a hidden workspace held at narrow terminal geometry', async ({
|
||
orcaPage,
|
||
electronApp
|
||
}) => {
|
||
await waitForSessionReady(orcaPage)
|
||
const firstWorktreeId = await waitForActiveWorktree(orcaPage)
|
||
const secondWorktreeId = (await getAllWorktreeIds(orcaPage)).find(
|
||
(worktreeId) => worktreeId !== firstWorktreeId
|
||
)
|
||
test.skip(!secondWorktreeId, 'hidden-workspace restore repro needs the seeded secondary worktree')
|
||
if (!secondWorktreeId) {
|
||
return
|
||
}
|
||
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
const hiddenWorkspacePtyId = await waitForActivePanePtyId(orcaPage)
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId: hiddenWorkspacePtyId, cols: 45, rows: 20 })
|
||
await expect(orcaPage.locator('.mobile-driver-banner')).toHaveCount(1, { timeout: 15_000 })
|
||
|
||
await forcePaneToOneColumnAndSwitchWorktree(orcaPage, hiddenWorkspacePtyId, secondWorktreeId)
|
||
await expect
|
||
.poll(() => getActiveWorktreeId(orcaPage), {
|
||
timeout: 5_000,
|
||
message: 'second worktree should become active before restore-all'
|
||
})
|
||
.toBe(secondWorktreeId)
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, hiddenWorkspacePtyId), {
|
||
message: 'test harness should hold workspace 1 in the bad narrow state'
|
||
})
|
||
.toBeLessThanOrEqual(2)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
const activeWorkspacePtyId = await waitForActivePanePtyId(orcaPage)
|
||
await installRestoreTerminalFitAutoRestoreRecorder(electronApp)
|
||
await sendHeldPhoneFitIpc(electronApp, { ptyId: activeWorkspacePtyId, cols: 45, rows: 20 })
|
||
await expect(
|
||
orcaPage.locator(`[data-pty-id="${activeWorkspacePtyId}"] .mobile-driver-banner`)
|
||
).toBeVisible({ timeout: 15_000 })
|
||
|
||
await orcaPage
|
||
.locator(`[data-pty-id="${activeWorkspacePtyId}"] .mobile-driver-banner`)
|
||
.getByRole('button', { name: /^restore all$/i })
|
||
.click()
|
||
|
||
await expectRestoreTerminalFitCallSet(electronApp, [hiddenWorkspacePtyId, activeWorkspacePtyId])
|
||
|
||
await switchToWorktree(orcaPage, firstWorktreeId)
|
||
await expect
|
||
.poll(() => getActiveWorktreeId(orcaPage), {
|
||
timeout: 5_000,
|
||
message: 'first worktree should become active after restore-all'
|
||
})
|
||
.toBe(firstWorktreeId)
|
||
await ensureTerminalVisible(orcaPage)
|
||
await waitForActiveTerminalManager(orcaPage)
|
||
await expect
|
||
.poll(() => getPaneTerminalCols(orcaPage, hiddenWorkspacePtyId), {
|
||
timeout: 5_000,
|
||
message: 'Restore all should refit the hidden workspace when it becomes visible'
|
||
})
|
||
.toBeGreaterThan(20)
|
||
})
|
||
|
||
async function sendMobileSubscribeIpc(
|
||
electronApp: ElectronApplication,
|
||
args: { ptyId: string; cols: number; rows: number }
|
||
): Promise<void> {
|
||
await electronApp.evaluate(({ BrowserWindow }, payload) => {
|
||
const wins = BrowserWindow.getAllWindows()
|
||
for (const win of wins) {
|
||
win.webContents.send('runtime:terminalFitOverrideChanged', {
|
||
ptyId: payload.ptyId,
|
||
mode: 'mobile-fit',
|
||
cols: payload.cols,
|
||
rows: payload.rows
|
||
})
|
||
win.webContents.send('runtime:terminalDriverChanged', {
|
||
ptyId: payload.ptyId,
|
||
driver: { kind: 'mobile', clientId: 'fake-phone-1' }
|
||
})
|
||
}
|
||
}, args)
|
||
}
|
||
|
||
async function sendHeldPhoneFitIpc(
|
||
electronApp: ElectronApplication,
|
||
args: { ptyId: string; cols: number; rows: number }
|
||
): Promise<void> {
|
||
await electronApp.evaluate(({ BrowserWindow }, payload) => {
|
||
const wins = BrowserWindow.getAllWindows()
|
||
for (const win of wins) {
|
||
win.webContents.send('runtime:terminalFitOverrideChanged', {
|
||
ptyId: payload.ptyId,
|
||
mode: 'mobile-fit',
|
||
cols: payload.cols,
|
||
rows: payload.rows
|
||
})
|
||
win.webContents.send('runtime:terminalDriverChanged', {
|
||
ptyId: payload.ptyId,
|
||
driver: { kind: 'idle' }
|
||
})
|
||
}
|
||
}, args)
|
||
}
|
||
|
||
async function sendDesktopRestoreIpc(
|
||
electronApp: ElectronApplication,
|
||
args: { ptyId: string }
|
||
): Promise<void> {
|
||
await electronApp.evaluate(({ BrowserWindow }, payload) => {
|
||
const wins = BrowserWindow.getAllWindows()
|
||
for (const win of wins) {
|
||
win.webContents.send('runtime:terminalFitOverrideChanged', {
|
||
ptyId: payload.ptyId,
|
||
mode: 'desktop-fit',
|
||
cols: 0,
|
||
rows: 0
|
||
})
|
||
win.webContents.send('runtime:terminalDriverChanged', {
|
||
ptyId: payload.ptyId,
|
||
driver: { kind: 'idle' }
|
||
})
|
||
}
|
||
}, args)
|
||
}
|
||
|
||
async function installRestoreTerminalFitRecorder(electronApp: ElectronApplication): Promise<void> {
|
||
await electronApp.evaluate(({ ipcMain }) => {
|
||
const testGlobal = globalThis as typeof globalThis & {
|
||
__mobileBannerRestoreCalls?: string[]
|
||
}
|
||
testGlobal.__mobileBannerRestoreCalls = []
|
||
// No real subscriber exists: intercept only main’s restore handler to verify the production channel/payload, then mirror its events.
|
||
ipcMain.removeHandler('runtime:restoreTerminalFit')
|
||
ipcMain.handle('runtime:restoreTerminalFit', (_event, args: { ptyId: string }) => {
|
||
testGlobal.__mobileBannerRestoreCalls?.push(args.ptyId)
|
||
return { restored: true }
|
||
})
|
||
})
|
||
}
|
||
|
||
async function installRestoreTerminalFitAutoRestoreRecorder(
|
||
electronApp: ElectronApplication
|
||
): Promise<void> {
|
||
await electronApp.evaluate(({ BrowserWindow, ipcMain }) => {
|
||
const testGlobal = globalThis as typeof globalThis & {
|
||
__mobileBannerRestoreCalls?: string[]
|
||
}
|
||
testGlobal.__mobileBannerRestoreCalls = []
|
||
// Production clears the fit override and restores desktop control; mirror both events.
|
||
ipcMain.removeHandler('runtime:restoreTerminalFit')
|
||
ipcMain.handle('runtime:restoreTerminalFit', (_event, args: { ptyId: string }) => {
|
||
testGlobal.__mobileBannerRestoreCalls?.push(args.ptyId)
|
||
for (const win of BrowserWindow.getAllWindows()) {
|
||
win.webContents.send('runtime:terminalFitOverrideChanged', {
|
||
ptyId: args.ptyId,
|
||
mode: 'desktop-fit',
|
||
cols: 0,
|
||
rows: 0
|
||
})
|
||
win.webContents.send('runtime:terminalDriverChanged', {
|
||
ptyId: args.ptyId,
|
||
driver: { kind: 'desktop' }
|
||
})
|
||
}
|
||
return { restored: true }
|
||
})
|
||
})
|
||
}
|
||
|
||
async function expectRestoreTerminalFitCalls(
|
||
electronApp: ElectronApplication,
|
||
expected: string[]
|
||
): Promise<void> {
|
||
await expect
|
||
.poll(
|
||
() =>
|
||
electronApp.evaluate(
|
||
() =>
|
||
(
|
||
globalThis as typeof globalThis & {
|
||
__mobileBannerRestoreCalls?: string[]
|
||
}
|
||
).__mobileBannerRestoreCalls ?? []
|
||
),
|
||
{ message: 'restoreTerminalFit should be invoked through the production IPC channel' }
|
||
)
|
||
.toEqual(expected)
|
||
}
|
||
|
||
async function expectRestoreTerminalFitCallSet(
|
||
electronApp: ElectronApplication,
|
||
expected: string[]
|
||
): Promise<void> {
|
||
await expect
|
||
.poll(
|
||
() =>
|
||
electronApp.evaluate(
|
||
() =>
|
||
(
|
||
globalThis as typeof globalThis & {
|
||
__mobileBannerRestoreCalls?: string[]
|
||
}
|
||
).__mobileBannerRestoreCalls ?? []
|
||
),
|
||
{ message: 'restore all should invoke the production restore channel for each PTY' }
|
||
)
|
||
.toEqual(expect.arrayContaining(expected))
|
||
}
|
||
|
||
async function waitForVisiblePanePtyIds(page: Page, expectedCount: number): Promise<string[]> {
|
||
let ptyIds: string[] = []
|
||
await expect
|
||
.poll(
|
||
async () => {
|
||
ptyIds = await page.evaluate(() => {
|
||
const state = window.__store?.getState()
|
||
const tabId = state?.activeTabId
|
||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||
return (manager?.getPanes?.() ?? [])
|
||
.map((pane) => pane.container?.dataset?.ptyId ?? null)
|
||
.filter((ptyId): ptyId is string => Boolean(ptyId))
|
||
})
|
||
return ptyIds.length
|
||
},
|
||
{
|
||
timeout: 15_000,
|
||
message: `Expected ${expectedCount} visible panes with PTY bindings`
|
||
}
|
||
)
|
||
.toBe(expectedCount)
|
||
return ptyIds
|
||
}
|
||
|
||
async function forcePaneToOneColumn(page: Page, ptyId: string): Promise<void> {
|
||
await page.evaluate((targetPtyId) => {
|
||
for (const manager of window.__paneManagers?.values?.() ?? []) {
|
||
const pane = manager
|
||
.getPanes?.()
|
||
.find((candidate) => candidate.container.dataset.ptyId === targetPtyId)
|
||
if (pane) {
|
||
pane.terminal.resize(1, Math.max(8, pane.terminal.rows))
|
||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||
return
|
||
}
|
||
}
|
||
throw new Error(`No pane found for PTY ${targetPtyId}`)
|
||
}, ptyId)
|
||
}
|
||
|
||
async function forcePaneToOneColumnAndSwitchWorktree(
|
||
page: Page,
|
||
ptyId: string,
|
||
worktreeId: string
|
||
): Promise<void> {
|
||
await page.evaluate(
|
||
({ targetPtyId, targetWorktreeId }) => {
|
||
for (const manager of window.__paneManagers?.values?.() ?? []) {
|
||
const pane = manager
|
||
.getPanes?.()
|
||
.find((candidate) => candidate.container.dataset.ptyId === targetPtyId)
|
||
if (pane) {
|
||
// Reproduce phone-sized xterm layout inherited while the workspace is hidden.
|
||
pane.terminal.resize(1, Math.max(8, pane.terminal.rows))
|
||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||
window.__store?.getState().setActiveWorktree(targetWorktreeId)
|
||
return
|
||
}
|
||
}
|
||
throw new Error(`No pane found for PTY ${targetPtyId}`)
|
||
},
|
||
{ targetPtyId: ptyId, targetWorktreeId: worktreeId }
|
||
)
|
||
}
|
||
|
||
async function getPaneTerminalCols(page: Page, ptyId: string): Promise<number> {
|
||
return page.evaluate((targetPtyId) => {
|
||
for (const manager of window.__paneManagers?.values?.() ?? []) {
|
||
const pane = manager
|
||
.getPanes?.()
|
||
.find((candidate) => candidate.container.dataset.ptyId === targetPtyId)
|
||
if (pane) {
|
||
return pane.terminal.cols
|
||
}
|
||
}
|
||
return 0
|
||
}, ptyId)
|
||
}
|
||
|
||
async function expectExpandedOverlayLeavesPaneReadable(page: Page, ptyId: string): Promise<void> {
|
||
await expect
|
||
.poll(
|
||
() =>
|
||
page.evaluate((targetPtyId) => {
|
||
const pane = Array.from(document.querySelectorAll<HTMLElement>('[data-pty-id]')).find(
|
||
(node) => node.dataset.ptyId === targetPtyId
|
||
)
|
||
const overlay = document.querySelector<HTMLElement>('.mobile-driver-banner')
|
||
if (!pane || !overlay) {
|
||
return false
|
||
}
|
||
const style = getComputedStyle(overlay)
|
||
const hasTransparentBackground =
|
||
style.backgroundColor === 'rgba(0, 0, 0, 0)' || style.backgroundColor === 'transparent'
|
||
const webkitBackdropFilter = (
|
||
style as CSSStyleDeclaration & { webkitBackdropFilter?: string }
|
||
).webkitBackdropFilter
|
||
const hasNoBackdropFilter =
|
||
(style.backdropFilter === 'none' || style.backdropFilter === '') &&
|
||
(webkitBackdropFilter === undefined ||
|
||
webkitBackdropFilter === '' ||
|
||
webkitBackdropFilter === 'none')
|
||
const paneBox = pane.getBoundingClientRect()
|
||
const overlayBox = overlay.getBoundingClientRect()
|
||
return (
|
||
hasTransparentBackground &&
|
||
hasNoBackdropFilter &&
|
||
Math.abs(overlayBox.left - paneBox.left) <= 2 &&
|
||
Math.abs(overlayBox.top - paneBox.top) <= 2 &&
|
||
overlayBox.width >= paneBox.width - 2 &&
|
||
overlayBox.height >= paneBox.height - 2
|
||
)
|
||
}, ptyId),
|
||
{ message: 'expanded overlay should not dim or blur the terminal pane' }
|
||
)
|
||
.toBe(true)
|
||
}
|
||
|
||
async function expectChipIsCompactInPane(page: Page, ptyId: string): Promise<void> {
|
||
await expect
|
||
.poll(
|
||
() =>
|
||
page.evaluate((targetPtyId) => {
|
||
const pane = Array.from(document.querySelectorAll<HTMLElement>('[data-pty-id]')).find(
|
||
(node) => node.dataset.ptyId === targetPtyId
|
||
)
|
||
const chip = document.querySelector<HTMLElement>('.mobile-driver-banner')
|
||
if (!pane || !chip) {
|
||
return false
|
||
}
|
||
const paneBox = pane.getBoundingClientRect()
|
||
const chipBox = chip.getBoundingClientRect()
|
||
const rightInset = paneBox.right - chipBox.right
|
||
const topInset = chipBox.top - paneBox.top
|
||
return (
|
||
chipBox.width < paneBox.width * 0.6 &&
|
||
chipBox.height <= 40 &&
|
||
rightInset >= 6 &&
|
||
rightInset <= 16 &&
|
||
topInset >= 6 &&
|
||
topInset <= 16
|
||
)
|
||
}, ptyId),
|
||
{ message: 'collapsed chip should stay compact in the terminal pane corner' }
|
||
)
|
||
.toBe(true)
|
||
}
|
||
|
||
// Attach via outputPath so list-reporter screenshots reliably reach playwright-traces artifacts.
|
||
async function captureAttachment(page: Page, testInfo: TestInfo, fileName: string): Promise<void> {
|
||
const dest = testInfo.outputPath(fileName)
|
||
await page.screenshot({ path: dest, fullPage: true })
|
||
await testInfo.attach(fileName, { path: dest, contentType: 'image/png' })
|
||
}
|