mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
test(mobile): verify hosted Terminal preferences in session consumers
Exercise text scale, autocorrect and custom shortcut persistence through the real hosted settings and session. Reuse picker and terminal inspection logic and restore original preferences. Full iOS adversarial run passed with ok:true against build eab7fd8f5eb4a37e593526e90dcc5105552691db072da8a982cd1ed30784a2e3; all code gates and export passed.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { waitHostedSettingsPickerOption } from './hosted-settings-picker-option.mjs'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
activateHostedWebViewControl,
|
||||
@@ -27,7 +28,7 @@ export async function verifyHostedIosBrowserSettings(args) {
|
||||
async function choose(document, label, timeoutMs) {
|
||||
await waitMode(document, undefined, timeoutMs)
|
||||
await activateHostedWebViewControl(document, { kind: 'label', value: 'Open terminal links' })
|
||||
await waitPickerOption(document, label, timeoutMs)
|
||||
await waitHostedSettingsPickerOption(document, label, timeoutMs)
|
||||
await activateHostedWebViewControl(document, { kind: 'text', value: label })
|
||||
await waitMode(document, label, timeoutMs)
|
||||
}
|
||||
@@ -52,25 +53,3 @@ async function waitMode(document, label, timeoutMs) {
|
||||
}
|
||||
throw new Error('Browser preference did not finish loading or saving')
|
||||
}
|
||||
|
||||
async function waitPickerOption(document, label, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
const visible = await evaluateHostedDocumentWithRetry(
|
||||
document,
|
||||
`JSON.stringify(
|
||||
Array.from(document.querySelectorAll('body *')).some((element) => {
|
||||
if (element.children.length || element.textContent.trim() !== ${JSON.stringify(label)}) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' &&
|
||||
rect.width > 0 && rect.height > 0 && rect.top < innerHeight && rect.bottom > 0;
|
||||
}))`
|
||||
)
|
||||
if (JSON.parse(visible)) {
|
||||
return
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Browser option did not become visible: ${label}`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
activateHostedWebViewControl,
|
||||
evaluateHostedDocumentWithRetry,
|
||||
waitForVisibleHostedWebView
|
||||
} from './hosted-webview-cdp-session.mjs'
|
||||
import { activateHostedWorkspaceRow } from './hosted-webview-workspace-activation.mjs'
|
||||
import { HOSTED_TERMINAL_INSTANCE_LOOKUP } from './hosted-terminal-instance-inspection.mjs'
|
||||
|
||||
export async function verifyHostedIosTerminalPreferenceConsumer(args, expected) {
|
||||
await activateHostedWorkspaceRow(
|
||||
args.workspaceDocument,
|
||||
args.expectedWorkspace,
|
||||
activateHostedWebViewControl,
|
||||
args.timeoutMs,
|
||||
() =>
|
||||
waitForVisibleHostedWebView({
|
||||
discoveryUrl: args.discoveryUrl,
|
||||
expectedText: args.expectedWorkspace,
|
||||
timeoutMs: args.timeoutMs
|
||||
})
|
||||
)
|
||||
const document = await waitForVisibleHostedWebView({
|
||||
discoveryUrl: args.discoveryUrl,
|
||||
expectedHrefIncludes: '/session/',
|
||||
expectedText: 'Mobile Emulator',
|
||||
timeoutMs: args.timeoutMs
|
||||
})
|
||||
await activateHostedWebViewControl(document, { kind: 'text', value: 'Mobile Emulator' })
|
||||
const wasLive = JSON.parse(
|
||||
await evaluateHostedDocumentWithRetry(
|
||||
document,
|
||||
`JSON.stringify(Boolean(document.querySelector('[aria-label="Switch to buffered command input"]')))`
|
||||
)
|
||||
)
|
||||
if (wasLive) {
|
||||
await activateHostedWebViewControl(document, {
|
||||
kind: 'label',
|
||||
value: 'Switch to buffered command input'
|
||||
})
|
||||
}
|
||||
const deadline = Date.now() + args.timeoutMs
|
||||
let state
|
||||
while (Date.now() < deadline) {
|
||||
state = JSON.parse(
|
||||
await evaluateHostedDocumentWithRetry(
|
||||
document,
|
||||
`(() => {
|
||||
const visible = (element) => { const r=element.getBoundingClientRect(); return r.width>0&&r.height>0; };
|
||||
const terminalElement=Array.from(document.querySelectorAll('.xterm')).find(visible);
|
||||
const terminal=terminalElement ? findTerminal(terminalElement) : null;
|
||||
const input=Array.from(document.querySelectorAll('[placeholder="Type a command…"]')).find(visible);
|
||||
const custom=document.querySelector(${JSON.stringify(`[aria-label="Send ${expected.shortcutLabel}"]`)});
|
||||
return JSON.stringify({fontSize:terminal?.options.fontSize, spellcheck:input?.spellcheck,
|
||||
autocorrect:input?.getAttribute('autocorrect'), customShortcutPresent:Boolean(custom)});
|
||||
${HOSTED_TERMINAL_INSTANCE_LOOKUP}
|
||||
})()`
|
||||
)
|
||||
)
|
||||
if (
|
||||
state.fontSize === expected.fontSize &&
|
||||
state.spellcheck === expected.autocomplete &&
|
||||
state.autocorrect === (expected.autocomplete ? 'on' : 'off') &&
|
||||
state.customShortcutPresent === expected.shortcutPresent
|
||||
) {
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
if (
|
||||
state?.fontSize !== expected.fontSize ||
|
||||
state?.spellcheck !== expected.autocomplete ||
|
||||
state?.autocorrect !== (expected.autocomplete ? 'on' : 'off') ||
|
||||
state?.customShortcutPresent !== expected.shortcutPresent
|
||||
) {
|
||||
throw new Error(`Hosted terminal did not consume saved preferences: ${JSON.stringify(state)}`)
|
||||
}
|
||||
if (wasLive) {
|
||||
await activateHostedWebViewControl(document, {
|
||||
kind: 'label',
|
||||
value: 'Switch to live terminal input'
|
||||
})
|
||||
}
|
||||
await activateHostedWebViewControl(document, { kind: 'label', value: 'Back to worktrees' })
|
||||
const workspaceDocument = await waitForVisibleHostedWebView({
|
||||
discoveryUrl: args.discoveryUrl,
|
||||
expectedText: args.expectedWorkspace,
|
||||
expectedPathname: '/',
|
||||
timeoutMs: args.timeoutMs
|
||||
})
|
||||
return { workspaceDocument, evidence: state }
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import path from 'node:path'
|
||||
import {
|
||||
activateHostedWebViewControl,
|
||||
evaluateHostedDocumentWithRetry,
|
||||
setHostedWebViewInput
|
||||
} from './hosted-webview-cdp-session.mjs'
|
||||
import { openHostedIosSettings, closeHostedIosSettings } from './hosted-ios-settings-navigation.mjs'
|
||||
import { waitHostedSettingsPickerOption } from './hosted-settings-picker-option.mjs'
|
||||
import { verifyHostedIosTerminalPreferenceConsumer } from './hosted-ios-terminal-preference-consumer.mjs'
|
||||
import { captureAgentHistorySimulatorScreenshot as captureScreenshot } from './hosted-ios-agent-history-parity.mjs'
|
||||
|
||||
const autocompleteLabel = 'Autocomplete and autocorrect'
|
||||
|
||||
export async function verifyHostedIosTerminalSettings(args) {
|
||||
const open = (document) =>
|
||||
openHostedIosSettings(args, document, 'Terminal', '/terminal-settings', 'for this paired host')
|
||||
let settings = await open(args.workspaceDocument)
|
||||
const original = await waitSettings(settings, {}, args.timeoutMs)
|
||||
const targetSize = original.textSize === 'Large (125%)' ? 'Smaller (75%)' : 'Large (125%)'
|
||||
const targetAutocomplete = !original.autocomplete
|
||||
const shortcutLabel = `OTA-${Date.now() % 1_000_000}`
|
||||
await chooseSize(settings, targetSize, args.timeoutMs)
|
||||
await activateHostedWebViewControl(settings, {
|
||||
kind: 'label',
|
||||
value: autocompleteLabel,
|
||||
reveal: true
|
||||
})
|
||||
await waitSettings(
|
||||
settings,
|
||||
{ textSize: targetSize, autocomplete: targetAutocomplete },
|
||||
args.timeoutMs
|
||||
)
|
||||
await addShortcut(settings, shortcutLabel, args.timeoutMs)
|
||||
settings = await open(await closeHostedIosSettings(args, settings))
|
||||
const changed = await waitSettings(
|
||||
settings,
|
||||
{
|
||||
textSize: targetSize,
|
||||
autocomplete: targetAutocomplete,
|
||||
shortcutLabel,
|
||||
shortcutPresent: true
|
||||
},
|
||||
args.timeoutMs
|
||||
)
|
||||
await evaluateHostedDocumentWithRetry(
|
||||
settings,
|
||||
`document.querySelector('[aria-label="${autocompleteLabel}"]').scrollIntoView({block:'center'}); 'ok'`
|
||||
)
|
||||
const screenshot = path.join(args.runtimeDirectory, 'hosted-terminal-settings.png')
|
||||
await captureScreenshot(args.deviceUdid, screenshot)
|
||||
const consumer = await verifyHostedIosTerminalPreferenceConsumer(
|
||||
{ ...args, workspaceDocument: await closeHostedIosSettings(args, settings) },
|
||||
{
|
||||
fontSize: fontSizeForLabel(targetSize),
|
||||
autocomplete: targetAutocomplete,
|
||||
shortcutLabel,
|
||||
shortcutPresent: true
|
||||
}
|
||||
)
|
||||
settings = await open(consumer.workspaceDocument)
|
||||
await chooseSize(settings, original.textSize, args.timeoutMs)
|
||||
await waitSettings(settings, {}, args.timeoutMs)
|
||||
await activateHostedWebViewControl(settings, {
|
||||
kind: 'label',
|
||||
value: autocompleteLabel,
|
||||
reveal: true
|
||||
})
|
||||
await waitSettings(settings, { autocomplete: original.autocomplete }, args.timeoutMs)
|
||||
await activateHostedWebViewControl(settings, {
|
||||
kind: 'label',
|
||||
value: `Delete ${shortcutLabel}`,
|
||||
reveal: true
|
||||
})
|
||||
await waitSettings(settings, { shortcutLabel, shortcutPresent: false }, args.timeoutMs)
|
||||
settings = await open(await closeHostedIosSettings(args, settings))
|
||||
const restored = await waitSettings(
|
||||
settings,
|
||||
{
|
||||
textSize: original.textSize,
|
||||
autocomplete: original.autocomplete,
|
||||
shortcutLabel,
|
||||
shortcutPresent: false
|
||||
},
|
||||
args.timeoutMs
|
||||
)
|
||||
const restoredConsumer = await verifyHostedIosTerminalPreferenceConsumer(
|
||||
{ ...args, workspaceDocument: await closeHostedIosSettings(args, settings) },
|
||||
{
|
||||
fontSize: fontSizeForLabel(original.textSize),
|
||||
autocomplete: original.autocomplete,
|
||||
shortcutLabel,
|
||||
shortcutPresent: false
|
||||
}
|
||||
)
|
||||
return {
|
||||
workspaceDocument: restoredConsumer.workspaceDocument,
|
||||
evidence: {
|
||||
persistenceAfterReopen: true,
|
||||
original,
|
||||
changed,
|
||||
consumer: consumer.evidence,
|
||||
restored,
|
||||
restoredConsumer: restoredConsumer.evidence,
|
||||
screenshot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseSize(document, label, timeoutMs) {
|
||||
await waitSettings(document, {}, timeoutMs)
|
||||
await activateHostedWebViewControl(document, { kind: 'label', value: 'Text size', reveal: true })
|
||||
await waitHostedSettingsPickerOption(document, label, timeoutMs)
|
||||
await activateHostedWebViewControl(document, { kind: 'text', value: label })
|
||||
await waitSettings(document, { textSize: label }, timeoutMs)
|
||||
}
|
||||
|
||||
async function addShortcut(document, label, timeoutMs) {
|
||||
await activateHostedWebViewControl(document, {
|
||||
kind: 'label',
|
||||
value: 'Add custom shortcut',
|
||||
reveal: true
|
||||
})
|
||||
await waitHostedSettingsPickerOption(document, 'Text Macro', timeoutMs)
|
||||
await activateHostedWebViewControl(document, { kind: 'text', value: 'Text Macro' })
|
||||
await waitHostedSettingsPickerOption(document, 'Add Shortcut', timeoutMs)
|
||||
await setHostedWebViewInput(document, { placeholder: 'e.g. Build', value: label })
|
||||
await setHostedWebViewInput(document, { placeholder: 'e.g. pnpm build', value: ':' })
|
||||
await activateHostedWebViewControl(document, { kind: 'text', value: 'Add Shortcut' })
|
||||
await waitSettings(document, { shortcutLabel: label, shortcutPresent: true }, timeoutMs)
|
||||
}
|
||||
|
||||
async function waitSettings(document, expected, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let state
|
||||
while (Date.now() < deadline) {
|
||||
state = JSON.parse(
|
||||
await evaluateHostedDocumentWithRetry(
|
||||
document,
|
||||
`(() => {
|
||||
const error=Array.from(document.querySelectorAll('[role="alert"]')).find(e=>e.textContent.includes('Could not'));
|
||||
if(error) throw new Error(error.textContent);
|
||||
const size=document.querySelector('[aria-label="Text size"]');
|
||||
const autocomplete=document.querySelector('[aria-label="${autocompleteLabel}"]');
|
||||
const add=document.querySelector('[aria-label="Add custom shortcut"]');
|
||||
const ready=[size,autocomplete,add].every(e=>e&&!e.disabled&&e.getAttribute('aria-disabled')!=='true'&&e.getAttribute('aria-busy')!=='true');
|
||||
return JSON.stringify({ready,
|
||||
textSize:size?.textContent.match(/(?:Smallest|Smaller|Default|Larger|Largest|Large) \\(\\d+%\\)/)?.[0],
|
||||
autocomplete:autocomplete?.getAttribute('aria-checked')==='true'||autocomplete?.checked===true||autocomplete?.querySelector('input')?.checked===true,
|
||||
shortcutPresent:Boolean(document.querySelector(${JSON.stringify(`[aria-label="Delete ${expected.shortcutLabel ?? ''}"]`)}))});
|
||||
})()`
|
||||
)
|
||||
)
|
||||
if (
|
||||
state.ready &&
|
||||
state.textSize &&
|
||||
(expected.textSize === undefined || state.textSize === expected.textSize) &&
|
||||
(expected.autocomplete === undefined || state.autocomplete === expected.autocomplete) &&
|
||||
(expected.shortcutPresent === undefined || state.shortcutPresent === expected.shortcutPresent)
|
||||
) {
|
||||
return state
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Terminal preferences did not load or persist: ${JSON.stringify(state)}`)
|
||||
}
|
||||
|
||||
function fontSizeForLabel(label) {
|
||||
const percent = Number(label.match(/\((\d+)%\)/)?.[1])
|
||||
if (!Number.isFinite(percent) || percent <= 0) {
|
||||
throw new Error(`Unexpected terminal text size: ${label}`)
|
||||
}
|
||||
return (13 * percent) / 100
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { verifyHostedIosTerminalSettings } from './hosted-ios-terminal-settings-journey.mjs'
|
||||
import { evidenceStep } from './hosted-webview-e2e-report.mjs'
|
||||
import { verifyHostedIosNativeAlertJourney } from './hosted-ios-native-alert-journey.mjs'
|
||||
import { verifyHostedIosBrowserSettings } from './hosted-ios-browser-settings-journey.mjs'
|
||||
@@ -20,11 +21,21 @@ export async function verifyHostedIosWorkspaceDeviceCapabilities(args) {
|
||||
})
|
||||
)
|
||||
: null
|
||||
const terminalSettings = browserSettings
|
||||
? await evidenceStep('hosted terminal preference persistence and consumers', () =>
|
||||
verifyHostedIosTerminalSettings({
|
||||
...args,
|
||||
workspaceDocument: browserSettings.workspaceDocument
|
||||
})
|
||||
)
|
||||
: null
|
||||
return {
|
||||
nativeAlert,
|
||||
terminalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
workspaceDocument:
|
||||
terminalSettings?.workspaceDocument ??
|
||||
browserSettings?.workspaceDocument ??
|
||||
chatSettings?.workspaceDocument ??
|
||||
nativeAlert.workspaceDocument
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { evaluateHostedDocumentWithRetry } from './hosted-webview-cdp-session.mjs'
|
||||
|
||||
export async function waitHostedSettingsPickerOption(document, label, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
const visible = await evaluateHostedDocumentWithRetry(
|
||||
document,
|
||||
`JSON.stringify(
|
||||
Array.from(document.querySelectorAll('body *')).some((element) => {
|
||||
if (element.children.length || element.textContent.trim() !== ${JSON.stringify(label)}) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' &&
|
||||
rect.width > 0 && rect.height > 0 && rect.top < innerHeight && rect.bottom > 0;
|
||||
}))`
|
||||
)
|
||||
if (JSON.parse(visible)) {
|
||||
return
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Settings option did not become visible: ${label}`)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const HOSTED_TERMINAL_INSTANCE_LOOKUP = `function findTerminal(element) {
|
||||
for (
|
||||
let ancestor = element;
|
||||
ancestor instanceof HTMLElement;
|
||||
ancestor = ancestor.parentElement
|
||||
) {
|
||||
const fiberKey = Object.keys(ancestor).find((key) => key.startsWith('__reactFiber$'));
|
||||
let fiber = fiberKey ? ancestor[fiberKey] : null;
|
||||
for (let depth = 0; fiber && depth < 32; depth += 1, fiber = fiber.return) {
|
||||
let hook = fiber.memoizedState;
|
||||
for (let index = 0; hook && index < 32; index += 1, hook = hook.next) {
|
||||
const current = hook.memoizedState?.current;
|
||||
if (
|
||||
current &&
|
||||
Number.isInteger(current.cols) &&
|
||||
Number.isInteger(current.rows) &&
|
||||
current.buffer?.active
|
||||
) return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}`
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HOSTED_TERMINAL_INSTANCE_LOOKUP } from './hosted-terminal-instance-inspection.mjs'
|
||||
import { WebSocket } from 'ws'
|
||||
import { evaluateHostedDocumentWithRetry } from './hosted-webview-cdp-session.mjs'
|
||||
|
||||
@@ -74,29 +75,7 @@ export async function readHostedTerminalLinkPoints(document, operations = {}) {
|
||||
screenWidth: Number(screen.width)
|
||||
});
|
||||
|
||||
function findTerminal(element) {
|
||||
for (
|
||||
let ancestor = element;
|
||||
ancestor instanceof HTMLElement;
|
||||
ancestor = ancestor.parentElement
|
||||
) {
|
||||
const fiberKey = Object.keys(ancestor).find((key) => key.startsWith('__reactFiber$'));
|
||||
let fiber = fiberKey ? ancestor[fiberKey] : null;
|
||||
for (let depth = 0; fiber && depth < 32; depth += 1, fiber = fiber.return) {
|
||||
let hook = fiber.memoizedState;
|
||||
for (let index = 0; hook && index < 32; index += 1, hook = hook.next) {
|
||||
const current = hook.memoizedState?.current;
|
||||
if (
|
||||
current &&
|
||||
Number.isInteger(current.cols) &&
|
||||
Number.isInteger(current.rows) &&
|
||||
current.buffer?.active
|
||||
) return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
${HOSTED_TERMINAL_INSTANCE_LOOKUP}
|
||||
})()`,
|
||||
WebSocket
|
||||
)
|
||||
@@ -156,29 +135,7 @@ export async function describeHostedTerminalLinkPoint(document, point, operation
|
||||
viewportTop
|
||||
});
|
||||
|
||||
function findTerminal(element) {
|
||||
for (
|
||||
let ancestor = element;
|
||||
ancestor instanceof HTMLElement;
|
||||
ancestor = ancestor.parentElement
|
||||
) {
|
||||
const fiberKey = Object.keys(ancestor).find((key) => key.startsWith('__reactFiber$'));
|
||||
let fiber = fiberKey ? ancestor[fiberKey] : null;
|
||||
for (let depth = 0; fiber && depth < 32; depth += 1, fiber = fiber.return) {
|
||||
let hook = fiber.memoizedState;
|
||||
for (let index = 0; hook && index < 32; index += 1, hook = hook.next) {
|
||||
const current = hook.memoizedState?.current;
|
||||
if (
|
||||
current &&
|
||||
Number.isInteger(current.cols) &&
|
||||
Number.isInteger(current.rows) &&
|
||||
current.buffer?.active
|
||||
) return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
${HOSTED_TERMINAL_INSTANCE_LOOKUP}
|
||||
|
||||
function describeRow(terminal, row) {
|
||||
const line = terminal.buffer.active.getLine(row);
|
||||
|
||||
@@ -30,6 +30,7 @@ export function printHostedWebViewE2eReport(evidence) {
|
||||
nativeAlert: evidence.nativeAlert.evidence,
|
||||
chatSettings: evidence.chatSettings,
|
||||
browserSettings: evidence.browserSettings,
|
||||
terminalSettings: evidence.terminalSettings,
|
||||
documentUpload: evidence.terminalDeviceInput?.documentUpload?.evidence ?? null,
|
||||
photoPermissionDenial:
|
||||
evidence.terminalDeviceInput?.photoPermissionDenial?.evidence ?? null,
|
||||
|
||||
Reference in New Issue
Block a user