test(e2e): add golden E2E tests for workspace session management (#14304)

* test(e2e): add golden E2E tests for workspace session management

- Restore exact file and terminal state after quit/relaunch
- Verify terminal file link activation and external edit detection
- Test worktree creation and switching with isolated terminals
- Isolate test repo paths between concurrent CI runs with UUIDs

* Add platform-aware marker echo command utility

- Create splitMarkerEchoCommand() to generate shell commands that
  safely echo test markers across Windows and Unix platforms
- Split markers into prefix/suffix fragments so output assertions
  prove execution, not just shell echo-back
- Consolidate SORTABLE_TAB export and improve tab bar locator logic
- Refactor terminal link helpers to extract client point calculation
This commit is contained in:
Jinjing
2026-08-13 10:26:20 -07:00
committed by GitHub
parent e84fb46eaf
commit b8d6b21dfa
9 changed files with 457 additions and 1 deletions
@@ -67,6 +67,7 @@ jobs:
if: runner.os == 'Linux'
run: |
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:workspace-session-golden
if [ -f tests/e2e/golden-fresh-profile-terminal.spec.ts ]; then
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-fresh-profile-terminal.spec.ts tests/e2e/golden-shell-command.spec.ts
fi
@@ -78,6 +79,7 @@ jobs:
if: runner.os == 'macOS'
run: |
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:workspace-session-golden
if [ -f tests/e2e/golden-fresh-profile-terminal.spec.ts ]; then
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-fresh-profile-terminal.spec.ts tests/e2e/golden-shell-command.spec.ts
fi
@@ -91,6 +93,7 @@ jobs:
run: |
$env:SKIP_BUILD = '1'
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run --if-present test:e2e:workspace-session-golden
pnpm run --if-present test:e2e:windows-fresh-startup-golden
if (Test-Path tests/e2e/golden-fresh-profile-terminal.spec.ts) {
pnpm run test:e2e -- tests/e2e/golden-fresh-profile-terminal.spec.ts tests/e2e/golden-shell-command.spec.ts
+3
View File
@@ -876,6 +876,7 @@ jobs:
- name: Run terminal rendering golden on Linux
if: runner.os == 'Linux'
run: |
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:workspace-session-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:posix-profile-index-golden
@@ -886,6 +887,7 @@ jobs:
- name: Run terminal rendering golden on macOS
if: runner.os == 'macOS'
run: |
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:workspace-session-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:posix-profile-index-golden
@@ -899,6 +901,7 @@ jobs:
run: |
$env:SKIP_BUILD = '1'
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run --if-present test:e2e:workspace-session-golden
pnpm run --if-present test:e2e:windows-fresh-startup-golden
pnpm run --if-present test:e2e:source-control-golden
+1
View File
@@ -85,6 +85,7 @@
"build:mac:release": "node config/scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build:desktop && ORCA_MAC_RELEASE=1 pnpm run build:computer-macos && ORCA_MAC_RELEASE=1 pnpm run build:notification-status-macos && pnpm run ensure:electron-runtime && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac",
"build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux AppImage deb",
"test:e2e": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project=electron-headless",
"test:e2e:workspace-session-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-quit-relaunch-session.spec.ts tests/e2e/golden-terminal-file-link.spec.ts tests/e2e/golden-worktree-create-switch.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:multi-client-navigation": "node config/scripts/run-multi-client-navigation-e2e.mjs",
"test:e2e:floating-mobile-emulator": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/floating-mobile-emulator-tab.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts tests/e2e/terminal-webgl-atlas-budget.spec.ts --grep @terminal-rendering-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
+6 -1
View File
@@ -17,13 +17,18 @@ import path from 'node:path'
import os from 'node:os'
import { prepareDockerSshRelayImage } from './helpers/docker-ssh-relay-image'
export const E2E_TEST_REPO_PATH_FILE_ENV = 'ORCA_E2E_TEST_REPO_PATH_FILE'
/** Temp file where the test repo path is stored for the fixture to read. */
export const TEST_REPO_PATH_FILE = path.join(os.tmpdir(), 'orca-e2e-test-repo-path.txt')
export const TEST_REPO_PATH_FILE =
process.env[E2E_TEST_REPO_PATH_FILE_ENV] ??
path.join(os.tmpdir(), `orca-e2e-test-repo-path-${randomUUID()}.txt`)
const ELECTRON_E2E_BUILD_TIMEOUT_MS = 300_000
const CLI_E2E_BUILD_TIMEOUT_MS = 120_000
const WEB_E2E_BUILD_TIMEOUT_MS = 300_000
export default function globalSetup(): void {
// Why: workers and teardown need this run's path, never another concurrent run's.
process.env[E2E_TEST_REPO_PATH_FILE_ENV] = TEST_REPO_PATH_FILE
const root = process.cwd()
const outMain = path.join(root, 'out', 'main', 'index.js')
const outCli = path.join(root, 'out', 'cli', 'index.js')
@@ -0,0 +1,109 @@
import { existsSync, readFileSync } from 'node:fs'
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { openFileExplorer } from './helpers/file-explorer'
import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart'
import { ensureTerminalVisible, waitForSessionReady } from './helpers/store'
import { createTerminalTabFromMenu, SORTABLE_TAB } from './helpers/terminal-tab-menu'
import {
execInTerminal,
focusActiveTerminalInput,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForTerminalOutput
} from './helpers/terminal'
import { TEST_REPO_PATH_FILE } from './global-setup'
import { splitMarkerEchoCommand } from './terminal-marker-echo-command'
import { PTY_SESSION_ID_SEPARATOR } from '../../src/shared/pty-session-id-format'
function seededRepoPath(): string {
const repoPath = existsSync(TEST_REPO_PATH_FILE)
? readFileSync(TEST_REPO_PATH_FILE, 'utf8').trim()
: ''
if (!repoPath || !existsSync(repoPath)) {
throw new Error('Golden restart E2E requires the seeded test repo from global setup')
}
return repoPath
}
test('restores the exact file and live extra terminal after quit and relaunch @golden', async (// oxlint-disable-next-line no-empty-pattern -- this test owns both Electron launches.
{}, testInfo) => {
test.setTimeout(300_000)
const repoPath = seededRepoPath()
const session = createRestartSession(testInfo)
let firstApp: ElectronApplication | null = null
let secondApp: ElectronApplication | null = null
try {
const first = await session.launch()
firstApp = first.app
await waitForSessionReady(first.page)
const primaryWorktreeId = await attachRepoAndOpenTerminal(first.page, repoPath)
await ensureTerminalVisible(first.page)
await waitForActiveTerminalManager(first.page, 30_000)
const extraTerminalId = await createTerminalTabFromMenu(first.page)
await waitForActiveTerminalManager(first.page, 30_000)
const extraTerminalPtyId = await waitForActivePanePtyId(first.page, 30_000)
expect(extraTerminalPtyId).toContain(PTY_SESSION_ID_SEPARATOR)
const preQuitMarkerPrefix = 'GOLDEN_RELAUNCH_PERSISTED'
const preQuitMarkerSuffix = `_${Date.now()}`
const preQuitMarker = `${preQuitMarkerPrefix}${preQuitMarkerSuffix}`
await execInTerminal(
first.page,
extraTerminalPtyId,
splitMarkerEchoCommand(preQuitMarkerPrefix, preQuitMarkerSuffix)
)
await waitForTerminalOutput(first.page, preQuitMarker, 20_000)
await openFileExplorer(first.page)
const fileRow = first.page
.locator('[data-file-explorer-row]')
.filter({ hasText: 'package.json' })
.first()
await expect(fileRow).toBeVisible({ timeout: 15_000 })
await fileRow.click()
await expect(first.page.locator('.editor-header-path').first()).toContainText('package.json', {
timeout: 20_000
})
const tabCountAtQuit = await first.page.locator(SORTABLE_TAB).count()
await session.close(firstApp)
firstApp = null
const second = await session.launch()
secondApp = second.app
await waitForSessionReady(second.page)
await expect(
second.page.locator(`[role="option"][data-worktree-id="${primaryWorktreeId}"]`)
).toHaveAttribute('aria-current', 'page', { timeout: 30_000 })
await expect(second.page.locator(SORTABLE_TAB)).toHaveCount(tabCountAtQuit, {
timeout: 30_000
})
await expect(second.page.locator('.editor-header-path').first()).toContainText('package.json', {
timeout: 30_000
})
const restoredExtraTab = second.page.locator(
`${SORTABLE_TAB}[data-tab-id="${extraTerminalId}"]`
)
await expect(restoredExtraTab).toBeVisible()
await restoredExtraTab.click({ force: true })
await waitForActiveTerminalManager(second.page, 30_000)
expect(await waitForActivePanePtyId(second.page, 30_000)).toBe(extraTerminalPtyId)
await waitForTerminalOutput(second.page, preQuitMarker, 20_000)
await expect(second.page.getByRole('button', { name: /Reconnect/i })).toHaveCount(0)
await focusActiveTerminalInput(second.page)
const marker = `GOLDEN_RELAUNCH_LIVE_${Date.now()}`
const command = process.platform === 'win32' ? `Write-Output ${marker}` : `echo ${marker}`
await second.page.keyboard.type(command)
await second.page.keyboard.press('Enter')
await waitForTerminalOutput(second.page, marker, 20_000)
} finally {
for (const app of [secondApp, firstApp]) {
if (app) {
await session.close(app).catch(() => undefined)
}
}
await session.dispose()
}
})
+188
View File
@@ -0,0 +1,188 @@
import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { openFileExplorer } from './helpers/file-explorer'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
getTerminalContent,
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager
} from './helpers/terminal'
import { nodeTerminalCommand } from './terminal-node-command'
import { waitForPtyShellEcho } from './terminal-pty-readiness'
type LinkProbe = { col: number; row: number; tabId: string }
type LinkClientPoint = { x: number; y: number }
const LINK_SCAN_CHAR_LIMIT = 12_000
async function locateLink(page: Page, needle: string): Promise<LinkProbe | null> {
return page.evaluate((needle) => {
const state = window.__store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? state?.activeTabIdByWorktree?.[worktreeId]
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!tabId || !pane) {
return null
}
const buffer = pane.terminal.buffer.active
for (let row = 0; row < pane.terminal.rows; row += 1) {
const line = buffer.getLine(buffer.viewportY + row)
const col = line?.translateToString(true).indexOf(needle) ?? -1
if (col >= 0) {
return { col: col + Math.floor(needle.length / 2), row, tabId }
}
}
return null
}, needle)
}
async function linkClientPoint(page: Page, probe: LinkProbe): Promise<LinkClientPoint> {
return page.evaluate(({ col, row, tabId }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
if (!pane || !screen) {
throw new Error('terminal link surface unavailable')
}
const rect = screen.getBoundingClientRect()
const cell = pane.terminal.dimensions?.css.cell
if (!cell?.width || !cell.height) {
throw new Error('terminal cell dimensions unavailable')
}
return {
x: rect.left + (col + 0.5) * cell.width,
y: rect.top + (row + 0.5) * cell.height
}
}, probe)
}
async function hoverLink(page: Page, probe: LinkProbe): Promise<string | null> {
const point = await linkClientPoint(page, probe)
return page.evaluate(
({ tabId, x, y }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
if (!pane || !screen) {
throw new Error('terminal link surface unavailable')
}
screen.dispatchEvent(
new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y })
)
const terminal = pane.terminal as unknown as {
_core?: { linkifier?: { currentLink?: { link?: { text?: string } } } }
}
return terminal._core?.linkifier?.currentLink?.link?.text ?? null
},
{ tabId: probe.tabId, ...point }
)
}
async function clickLink(page: Page, probe: LinkProbe): Promise<void> {
const target = await linkClientPoint(page, probe)
await page.mouse.move(target.x, target.y)
await page.mouse.click(target.x, target.y)
}
test('opens a terminal file link and observes an external edit @golden', async ({ orcaPage }) => {
test.setTimeout(180_000)
await waitForSessionReady(orcaPage)
const worktreeId = await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const ptyId = await waitForActivePanePtyId(orcaPage)
await waitForPtyShellEcho(orcaPage, ptyId, 15_000)
const worktreePath = await orcaPage.evaluate((id) => {
return (
Object.values(window.__store?.getState().worktreesByRepo ?? {})
.flat()
.find((worktree) => worktree.id === id)?.path ?? ''
)
}, worktreeId)
const filePath = path.join(worktreePath, 'package.json')
const original = readFileSync(filePath, 'utf8')
const clickablePath = process.platform === 'win32' ? filePath.replaceAll('\\', '/') : filePath
const changedMarker = `golden-external-edit-${Date.now()}`
try {
await openFileExplorer(orcaPage)
const explorerRow = orcaPage
.locator('[data-file-explorer-row]')
.filter({ hasText: 'package.json' })
.first()
await expect(explorerRow).toBeVisible({ timeout: 15_000 })
const pathsToPrint = process.platform === 'win32' ? [filePath, clickablePath] : [filePath]
for (const printedPath of pathsToPrint) {
const command = nodeTerminalCommand(['-e', `console.log(${JSON.stringify(printedPath)})`])
await sendToTerminal(orcaPage, ptyId, `${command}\r`)
}
await expect
.poll(() => getTerminalContent(orcaPage, LINK_SCAN_CHAR_LIMIT), { timeout: 15_000 })
.toContain(clickablePath)
let probe: LinkProbe | null = null
await expect
.poll(
async () => {
probe = await locateLink(orcaPage, clickablePath)
return probe ? hoverLink(orcaPage, probe) : null
},
{ timeout: 10_000, message: 'absolute file path did not become clickable' }
)
.toContain('package.json')
if (!probe) {
throw new Error('terminal file link disappeared before activation')
}
await clickLink(orcaPage, probe)
const actionPopover = orcaPage.locator('[data-terminal-link-action-popover]')
await expect(actionPopover).toBeVisible()
// Why: the popover echoes the link text verbatim, which on Windows is the forward-slash form.
await expect(actionPopover.locator('[data-terminal-link-destination]')).toContainText(
clickablePath
)
await actionPopover.getByRole('button', { name: /Open file/i }).click()
const editorHeader = orcaPage.locator('.editor-header-path').first()
await expect(editorHeader).toContainText('package.json', { timeout: 20_000 })
await expect(explorerRow).toHaveAttribute('data-selected', 'true', { timeout: 10_000 })
await expect
.poll(
() =>
orcaPage.evaluate(
(expectedPath) => window.__monacoEditorE2E?.filePath === expectedPath,
filePath
),
{ timeout: 20_000, message: 'Monaco opened a different file identity' }
)
.toBe(true)
writeFileSync(filePath, `${original.trimEnd()}\n\n${changedMarker}\n`)
await expect
.poll(
async () => {
const snapshot = await orcaPage.evaluate(() => window.__monacoEditorE2E?.snapshot())
const reloadVisible = await orcaPage
.getByRole('button', { name: 'Reload from Disk' })
.isVisible()
.catch(() => false)
return Boolean(snapshot?.valueTail.includes(changedMarker) || reloadVisible)
},
{ timeout: 20_000, message: 'editor stayed silently stale after the external edit' }
)
.toBe(true)
} finally {
writeFileSync(filePath, original)
}
})
@@ -0,0 +1,77 @@
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { getActiveWorktreeId, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { createTerminalTabFromMenu } from './helpers/terminal-tab-menu'
import {
execInTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForTerminalOutput
} from './helpers/terminal'
import { splitMarkerEchoCommand } from './terminal-marker-echo-command'
import { waitForPtyShellEcho } from './terminal-pty-readiness'
async function createWorkspace(page: Page, name: string): Promise<void> {
await page.getByRole('button', { name: 'New workspace', exact: true }).click()
const dialog = page.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
await dialog.getByPlaceholder(/Type a name/i).fill(name)
await dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i }).click()
await expect(dialog).toBeHidden({ timeout: 20_000 })
}
async function removeCreatedWorktree(page: Page, worktreeId: string): Promise<void> {
await page.evaluate(async (id) => {
await window.__store?.getState().removeWorktree(id, true)
}, worktreeId)
}
test('creates a worktree, keeps its terminal isolated, and switches back @golden', async ({
orcaPage
}) => {
test.setTimeout(180_000)
await waitForSessionReady(orcaPage)
const originalWorktreeId = await waitForActiveWorktree(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const parentPtyId = await waitForActivePanePtyId(orcaPage)
const workspaceName = `golden-switch-${Date.now()}`
let childWorktreeId: string | null = null
try {
await createWorkspace(orcaPage, workspaceName)
await expect(
orcaPage.locator('[role="option"][aria-current="page"]').filter({ hasText: workspaceName })
).toBeVisible({ timeout: 30_000 })
childWorktreeId = await waitForActiveWorktree(orcaPage)
// Why: the cleanup force-removes childWorktreeId, so it must never resolve to the original.
expect(childWorktreeId).not.toBe(originalWorktreeId)
await expect(
orcaPage.locator(`[role="option"][data-worktree-id="${childWorktreeId}"]`)
).toHaveAttribute('aria-current', 'page')
await createTerminalTabFromMenu(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const childPtyId = await waitForActivePanePtyId(orcaPage)
expect(childPtyId).not.toBe(parentPtyId)
await waitForPtyShellEcho(orcaPage, childPtyId, 15_000)
await execInTerminal(orcaPage, childPtyId, splitMarkerEchoCommand('worktree', '-b'))
await waitForTerminalOutput(orcaPage, 'worktree-b')
await orcaPage.locator(`[role="option"][data-worktree-id="${originalWorktreeId}"]`).click()
await expect(
orcaPage.locator(`[role="option"][data-worktree-id="${originalWorktreeId}"]`)
).toHaveAttribute('aria-current', 'page', { timeout: 20_000 })
await waitForActiveTerminalManager(orcaPage, 30_000)
expect(await waitForActivePanePtyId(orcaPage, 30_000)).toBe(parentPtyId)
} finally {
if (childWorktreeId) {
if ((await getActiveWorktreeId(orcaPage).catch(() => null)) !== originalWorktreeId) {
await orcaPage
.locator(`[role="option"][data-worktree-id="${originalWorktreeId}"]`)
.click()
.catch(() => undefined)
}
await removeCreatedWorktree(orcaPage, childWorktreeId).catch(() => undefined)
}
}
})
+62
View File
@@ -0,0 +1,62 @@
import { expect } from '@stablyai/playwright-test'
import type { Locator, Page } from '@stablyai/playwright-test'
import { getActiveTabId } from './store'
export const SORTABLE_TAB = '[data-testid="sortable-tab"]'
// Why: split groups and hidden worktrees keep extra tab bars mounted, so the "+" has to come from
// the active group's strip; the pre-layout titlebar fallback has no strip to scope to.
async function activeTabBarRoot(page: Page): Promise<Locator> {
const groupId = await page.evaluate(() => {
const state = window.__store?.getState()
const worktreeId = state?.activeWorktreeId
if (!worktreeId) {
return null
}
return (
state?.activeGroupIdByWorktree?.[worktreeId] ??
state?.groupsByWorktree?.[worktreeId]?.[0]?.id ??
null
)
})
if (!groupId) {
return page.locator('body')
}
const strip = page.locator(`[data-tab-group-strip-id="${groupId}"]`)
return (await strip.count()) > 0 ? strip : page.locator('body')
}
export async function createTerminalTabFromMenu(page: Page): Promise<string> {
const tabBar = await activeTabBarRoot(page)
const tabsBefore = await tabBar.locator(SORTABLE_TAB).count()
const activeBefore = await getActiveTabId(page)
await tabBar.getByRole('button', { name: 'New tab' }).click()
await page
.getByRole('menuitem', { name: /New Terminal/i })
.first()
.click()
await expect
.poll(() => tabBar.locator(SORTABLE_TAB).count(), {
timeout: 10_000,
message: 'New Terminal did not render a new tab in the tab bar'
})
.toBe(tabsBefore + 1)
await expect
.poll(
async () => {
const current = await getActiveTabId(page)
return Boolean(current && current !== activeBefore)
},
{ timeout: 10_000, message: 'New Terminal did not become the active tab' }
)
.toBe(true)
const tabId = await getActiveTabId(page)
if (!tabId) {
throw new Error('New Terminal tab id was unavailable after creation')
}
return tabId
}
@@ -0,0 +1,8 @@
// Why: the shell echoes the command line back into the buffer, so a marker written as one literal
// would satisfy an output assertion even if the command never ran. Splitting it into two fragments
// that only rejoin in the command's output makes the assertion proof of execution.
export function splitMarkerEchoCommand(prefix: string, suffix: string): string {
return process.platform === 'win32'
? `Write-Output ('${prefix}' + '${suffix}')`
: `echo "${prefix}""${suffix}"`
}