mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Keep managed file drags out of desktop browser paths (#26698)
This commit is contained in:
@@ -361,7 +361,8 @@ jobs:
|
||||
. != "tests/e2e/ssh-orcad-open-in-owner.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-explorer-selected-host.spec.ts"
|
||||
. != "tests/e2e/ssh-orcad-explorer-selected-host.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-browser-drop-owner.spec.ts"
|
||||
)' <<<"$TEST_FILES_JSON" > "$RUNNER_TEMP/general-e2e-specs"
|
||||
fi
|
||||
mapfile -t TEST_FILES < "$RUNNER_TEMP/general-e2e-specs"
|
||||
@@ -595,7 +596,7 @@ jobs:
|
||||
orcad-auto-convert-docker:
|
||||
name: ssh host auto-converts to managed orcad (Docker)
|
||||
needs: [build, prepare-native-cache]
|
||||
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-open-in-owner.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-explorer-selected-host.spec.ts')
|
||||
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-open-in-owner.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-explorer-selected-host.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-drop-owner.spec.ts')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
@@ -631,7 +632,7 @@ jobs:
|
||||
ORCA_RELAY_PATH: ${{ github.workspace }}/out/relay
|
||||
run: |
|
||||
export ORCA_E2E_ORCAD_CONVERT_TEMPLATE="$RUNNER_TEMP/orcad-convert-template"
|
||||
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts tests/e2e/ssh-orcad-markdown-conversion.spec.ts tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts tests/e2e/ssh-orcad-markdown-live-documents.spec.ts tests/e2e/ssh-orcad-open-in-owner.spec.ts tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts tests/e2e/ssh-orcad-explorer-selected-host.spec.ts --project=electron-headless --workers=1
|
||||
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts tests/e2e/ssh-orcad-markdown-conversion.spec.ts tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts tests/e2e/ssh-orcad-markdown-live-documents.spec.ts tests/e2e/ssh-orcad-open-in-owner.spec.ts tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts tests/e2e/ssh-orcad-explorer-selected-host.spec.ts tests/e2e/ssh-orcad-browser-drop-owner.spec.ts --project=electron-headless --workers=1
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
|
||||
@@ -62,6 +62,7 @@ export const ORCAD_MARKDOWN_LINK_REFRESH_E2E_SPEC =
|
||||
'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts'
|
||||
export const ORCAD_MARKDOWN_LIVE_DOCUMENTS_E2E_SPEC =
|
||||
'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts'
|
||||
export const ORCAD_BROWSER_DROP_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'
|
||||
export const ORCAD_OPEN_IN_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-open-in-owner.spec.ts'
|
||||
export const ORCAD_EDITOR_WATCH_RECOVERY_E2E_SPEC =
|
||||
'tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts'
|
||||
@@ -89,7 +90,8 @@ export const DEDICATED_E2E_SPECS = [
|
||||
ORCAD_OPEN_IN_OWNER_E2E_SPEC,
|
||||
ORCAD_EDITOR_WATCH_RECOVERY_E2E_SPEC,
|
||||
ORCAD_EXPLORER_WATCH_RECOVERY_E2E_SPEC,
|
||||
ORCAD_EXPLORER_SELECTED_HOST_E2E_SPEC
|
||||
ORCAD_EXPLORER_SELECTED_HOST_E2E_SPEC,
|
||||
ORCAD_BROWSER_DROP_OWNER_E2E_SPEC
|
||||
]
|
||||
const dedicatedSpecs = new Set(DEDICATED_E2E_SPECS)
|
||||
const dockerSpecs = new Set(DOCKER_SSH_E2E_SPECS)
|
||||
@@ -142,7 +144,8 @@ export function classifyE2eJobs(input, sshSourceChanged = 'false') {
|
||||
spec === ORCAD_OPEN_IN_OWNER_E2E_SPEC ||
|
||||
spec === ORCAD_EDITOR_WATCH_RECOVERY_E2E_SPEC ||
|
||||
spec === ORCAD_EXPLORER_WATCH_RECOVERY_E2E_SPEC ||
|
||||
spec === ORCAD_EXPLORER_SELECTED_HOST_E2E_SPEC
|
||||
spec === ORCAD_EXPLORER_SELECTED_HOST_E2E_SPEC ||
|
||||
spec === ORCAD_BROWSER_DROP_OWNER_E2E_SPEC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,25 @@ const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8'))
|
||||
const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const classify = (specs, ssh = 'false') => classifyE2eJobs(JSON.stringify(specs), ssh)
|
||||
|
||||
it('runs real browser file-drop ownership in the template-building Docker job', () => {
|
||||
const spec = 'tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'
|
||||
expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true })
|
||||
const job = workflow.jobs['orcad-auto-convert-docker']
|
||||
expect(job.if).toContain(spec)
|
||||
expect(
|
||||
job.steps.some((step) => step.run?.includes(spec) && step.env?.ORCA_E2E_SSH_DOCKER === '1')
|
||||
).toBe(true)
|
||||
for (const source of [
|
||||
'src/renderer/src/lib/file-preview.ts',
|
||||
'src/renderer/src/lib/workspace-file-drag.ts',
|
||||
'src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.ts',
|
||||
'src/renderer/src/components/right-sidebar/FileExplorerRow.tsx',
|
||||
'tests/e2e/helpers/browser-split-guest-probes.ts'
|
||||
]) {
|
||||
expect(selectPrE2eSpecs([source]), source).toContain(spec)
|
||||
}
|
||||
})
|
||||
|
||||
it('skips the general consumer only when every requested spec has a dedicated owner', () => {
|
||||
for (const spec of DEDICATED_E2E_SPECS) {
|
||||
expect(classify([spec]).e2e_run_changed, spec).toBe(false)
|
||||
|
||||
@@ -134,6 +134,18 @@ export const PR_E2E_SOURCE_ROUTES = [
|
||||
file
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'ssh.orcad-browser-drop-owner',
|
||||
specs: ['tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'],
|
||||
matches: (file) =>
|
||||
/^tests\/e2e\/helpers\/(?:orcad-convert-(?:flow|host)|orcad-upgrade-profile|docker-ssh-relay-target|browser-split-(?:guest-probes|page-server))\.ts$/.test(
|
||||
file
|
||||
) ||
|
||||
(isProductSource(file) &&
|
||||
/^src\/renderer\/src\/(?:lib\/(?:file-preview|workspace-file-drag|worktree-runtime-owner|connection-context)\.ts|components\/(?:right-sidebar\/FileExplorer(?:Row|VirtualRows)\.tsx|browser-pane\/(?:navigate\/use-browser-page-navigation-downloads\.ts|host-guest\/attach-browser-page-webview\.ts)))$/.test(
|
||||
file
|
||||
))
|
||||
},
|
||||
{
|
||||
id: 'ssh.orcad-open-in-owner',
|
||||
specs: ['tests/e2e/ssh-orcad-open-in-owner.spec.ts'],
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useBrowserPageNavigationDownloads } from './use-browser-page-navigation-downloads'
|
||||
import {
|
||||
WORKSPACE_FILE_DRAG_SOURCE_MIME,
|
||||
WORKSPACE_FILE_PATH_MIME
|
||||
} from '@/lib/workspace-file-drag'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ notice: vi.fn(), target: vi.fn(), navigate: vi.fn() }))
|
||||
vi.mock('@/lib/file-preview', () => ({
|
||||
REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE:
|
||||
'Open in Orca Browser is only available for local files.',
|
||||
getWorkspaceFileBrowserOpenTarget: mocks.target
|
||||
}))
|
||||
vi.mock('./navigate-browser-page-url', () => ({ navigateBrowserPageToUrl: mocks.navigate }))
|
||||
vi.mock('./workspace-doc-address-submission', () => ({
|
||||
routeWorkspaceDocAddressSubmission: vi.fn()
|
||||
}))
|
||||
vi.mock('./use-browser-page-download-events', () => ({
|
||||
useBrowserPageDownloadEvents: () => ({ downloadStates: [], setDownloadStates: vi.fn() })
|
||||
}))
|
||||
|
||||
function DropSurface() {
|
||||
const nav = useBrowserPageNavigationDownloads({
|
||||
browserTabId: 'page-local',
|
||||
worktreeId: 'global-floating-terminal',
|
||||
webviewRef: { current: null },
|
||||
activeLoadFailureRef: { current: null },
|
||||
lastKnownWebviewUrlRef: { current: null },
|
||||
trackNextLoadingEventRef: { current: false },
|
||||
recoveryNavigationValidationRef: { current: null },
|
||||
onSetUrlRef: { current: vi.fn() },
|
||||
onUpdatePageStateRef: { current: vi.fn() },
|
||||
keepAddressBarFocusRef: { current: false },
|
||||
focusWebviewNow: vi.fn(() => false),
|
||||
setResourceNotice: mocks.notice,
|
||||
addressBarValueRef: { current: 'about:blank' },
|
||||
addressBarInputRef: { current: null },
|
||||
browserTabUrl: 'about:blank'
|
||||
})
|
||||
return <div data-testid="drop" onDrop={(event) => nav.handleInternalFileDropRef.current(event)} />
|
||||
}
|
||||
|
||||
function dropPayload(source: string | null) {
|
||||
const values = new Map([[WORKSPACE_FILE_PATH_MIME, '/same/path/report.html']])
|
||||
if (source !== null) {
|
||||
values.set(WORKSPACE_FILE_DRAG_SOURCE_MIME, source)
|
||||
}
|
||||
return { types: [...values.keys()], getData: (type: string) => values.get(type) ?? '' }
|
||||
}
|
||||
|
||||
describe('browser file drops preserve source ownership', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.target.mockReturnValue({ status: 'ready', url: 'file:///same/path/report.html' })
|
||||
})
|
||||
afterEach(cleanup)
|
||||
|
||||
it.each(['runtime:host-a', 'ssh:host-a', 'runtime:unresolved-owner'])(
|
||||
'rejects a %s source before local URL resolution',
|
||||
(executionHostId) => {
|
||||
const view = render(<DropSurface />)
|
||||
fireEvent.drop(view.getByTestId('drop'), {
|
||||
dataTransfer: dropPayload(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
executionHostId,
|
||||
workspaceId: 'source-workspace'
|
||||
})
|
||||
)
|
||||
})
|
||||
expect(mocks.target).not.toHaveBeenCalled()
|
||||
expect(mocks.navigate).not.toHaveBeenCalled()
|
||||
expect(mocks.notice).toHaveBeenCalledWith(
|
||||
'Open in Orca Browser is only available for local files.'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([null, '{broken', '{"version":1,"executionHostId":"local"}'])(
|
||||
'rejects an unresolved source %s',
|
||||
(source) => {
|
||||
const view = render(<DropSurface />)
|
||||
fireEvent.drop(view.getByTestId('drop'), { dataTransfer: dropPayload(source) })
|
||||
expect(mocks.target).not.toHaveBeenCalled()
|
||||
expect(mocks.navigate).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('accepts a desktop source from another workspace into the floating browser', () => {
|
||||
const view = render(<DropSurface />)
|
||||
fireEvent.drop(view.getByTestId('drop'), {
|
||||
dataTransfer: dropPayload(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
executionHostId: 'local',
|
||||
workspaceId: 'source-workspace'
|
||||
})
|
||||
)
|
||||
})
|
||||
expect(mocks.target).toHaveBeenCalledWith({
|
||||
filePath: '/same/path/report.html',
|
||||
worktreeId: 'global-floating-terminal'
|
||||
})
|
||||
expect(mocks.notice).toHaveBeenCalledWith('Browser page is not ready for file drops.')
|
||||
})
|
||||
})
|
||||
+12
-1
@@ -10,11 +10,15 @@ import {
|
||||
type SetStateAction
|
||||
} from 'react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getWorkspaceFileBrowserOpenTarget } from '@/lib/file-preview'
|
||||
import {
|
||||
getWorkspaceFileBrowserOpenTarget,
|
||||
REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
|
||||
} from '@/lib/file-preview'
|
||||
import { routeWorkspaceDocAddressSubmission } from './workspace-doc-address-submission'
|
||||
import {
|
||||
getWorkspaceFileDragRejectionMessage,
|
||||
readWorkspaceFileDragPaths,
|
||||
readWorkspaceFileDragSource,
|
||||
WORKSPACE_FILE_PATH_MIME
|
||||
} from '@/lib/workspace-file-drag'
|
||||
import type { BrowserLoadError } from '../../../../../shared/browser-workspace-types'
|
||||
@@ -173,6 +177,13 @@ export function useBrowserPageNavigationDownloads({
|
||||
return
|
||||
}
|
||||
|
||||
// A file URL reads this machine, regardless of which workspace owns the browser.
|
||||
const source = readWorkspaceFileDragSource(event.dataTransfer)
|
||||
if (source?.executionHostId !== 'local') {
|
||||
setResourceNotice(REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
const target = getWorkspaceFileBrowserOpenTarget({ filePath, worktreeId })
|
||||
if (target.status === 'unsupported') {
|
||||
setResourceNotice(target.message)
|
||||
|
||||
@@ -481,6 +481,20 @@ describe('getWorkspaceFileBrowserOpenTarget', () => {
|
||||
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses paired files even when their connection ID is null', () => {
|
||||
mocks.environmentId = 'managed-host'
|
||||
expect(
|
||||
getWorkspaceFileBrowserOpenTarget({
|
||||
filePath: '/srv/repo/report.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
).toEqual({
|
||||
status: 'unsupported',
|
||||
reason: 'remote-worktree',
|
||||
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Why the reuse case is pinned here too: the address bar's way into a document must obey the same
|
||||
|
||||
@@ -147,7 +147,10 @@ export function getWorkspaceFileBrowserOpenTarget(params: {
|
||||
filePath: string
|
||||
worktreeId: string
|
||||
}): WorkspaceFileBrowserOpenTarget {
|
||||
if (getConnectionIdForFile(params.worktreeId, params.filePath) !== null) {
|
||||
if (
|
||||
getConnectionIdForFile(params.worktreeId, params.filePath) !== null ||
|
||||
getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), params.worktreeId)
|
||||
) {
|
||||
// Why: Chromium resolves file:// URLs on the local machine. Remote files
|
||||
// need an Orca-served URL before the browser can render them correctly.
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { createRestartSession } from './helpers/orca-restart'
|
||||
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
|
||||
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
|
||||
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
|
||||
import { openFileExplorer } from './helpers/file-explorer'
|
||||
import { shellQuote } from './helpers/docker-ssh-relay-target'
|
||||
import { startBrowserSplitPageServer } from './helpers/browser-split-page-server'
|
||||
import {
|
||||
navigateGuest,
|
||||
waitForGuestUrl,
|
||||
waitForGuestIdle
|
||||
} from './helpers/browser-split-guest-probes'
|
||||
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
|
||||
import { getWorktreeHostIdentity } from '../../src/shared/worktree/host-qualified-identity'
|
||||
|
||||
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
|
||||
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs owned Docker host')
|
||||
test.skip(process.platform === 'win32', 'Owned POSIX host collision fixture')
|
||||
|
||||
async function dropExplorerFile(page: Page, name: string, tabId: string) {
|
||||
const row = page.locator('[data-file-explorer-row]').filter({ hasText: name })
|
||||
await expect(row).toBeVisible()
|
||||
return row.evaluate((element, browserTabId) => {
|
||||
const webview = document.querySelector<Electron.WebviewTag>(
|
||||
`[data-browser-overlay-tab-id="${browserTabId}"] webview`
|
||||
)
|
||||
if (!webview || !webview.parentElement) {
|
||||
throw new Error('Missing actual guest container')
|
||||
}
|
||||
const dataTransfer = new DataTransfer()
|
||||
element.dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer }))
|
||||
const payload = Object.fromEntries(
|
||||
[...dataTransfer.types].map((type) => [type, dataTransfer.getData(type)])
|
||||
)
|
||||
const rect = webview.getBoundingClientRect()
|
||||
if (rect.width < 1 || rect.height < 1) {
|
||||
throw new Error('Actual guest has no drop surface')
|
||||
}
|
||||
webview.parentElement.dispatchEvent(
|
||||
new DragEvent('drop', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: rect.left + rect.width / 2,
|
||||
clientY: rect.top + rect.height / 2
|
||||
})
|
||||
)
|
||||
element.dispatchEvent(new DragEvent('dragend', { bubbles: true, dataTransfer }))
|
||||
return payload
|
||||
}, tabId)
|
||||
}
|
||||
|
||||
async function guestText(page: Page, tabId: string) {
|
||||
return page.evaluate(async (id) => {
|
||||
const webview = document.querySelector<Electron.WebviewTag>(
|
||||
`[data-browser-overlay-tab-id="${id}"] webview`
|
||||
)
|
||||
if (!webview) {
|
||||
throw new Error('Missing actual guest')
|
||||
}
|
||||
return {
|
||||
url: webview.getURL(),
|
||||
text: await webview.executeJavaScript('document.body.innerText')
|
||||
}
|
||||
}, tabId)
|
||||
}
|
||||
|
||||
test('managed Files drag cannot load a desktop same-path HTML file', async ({
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
test.setTimeout(4 * 60_000)
|
||||
const fileName = 'DROP_OWNER.html'
|
||||
const filePath = path.join(testRepoPath, fileName)
|
||||
writeFileSync(filePath, '<html><body>DESKTOP_BROWSER_DROP_OWNER</body></html>')
|
||||
const host = startOrcadConvertHost('docker', testInfo)
|
||||
const server = await startBrowserSplitPageServer()
|
||||
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
|
||||
let app: ElectronApplication | null = null
|
||||
try {
|
||||
if (!host.exec) {
|
||||
throw new Error('Missing owned host control')
|
||||
}
|
||||
host.exec(
|
||||
`mkdir -p ${shellQuote(path.dirname(testRepoPath))} && git clone --quiet ${shellQuote(host.remoteRepoPath)} ${shellQuote(testRepoPath)} && printf '%s' '<html><body>REMOTE_BROWSER_DROP_OWNER</body></html>' > ${shellQuote(filePath)}`
|
||||
)
|
||||
host.exec(
|
||||
`printf '%s' REMOTE_DROP_READY > ${shellQuote(path.join(testRepoPath, 'REMOTE_DROP_READY.txt'))}`
|
||||
)
|
||||
const first = await session.launch()
|
||||
app = first.app
|
||||
await waitForSessionReady(first.page)
|
||||
const nativeId = await first.page.evaluate(async (repoPath) => {
|
||||
const result = await window.api.repos.add({ path: repoPath })
|
||||
if ('error' in result) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
return `${result.repo.id}::${repoPath}`
|
||||
}, testRepoPath)
|
||||
await session.close(app)
|
||||
app = null
|
||||
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
|
||||
repoPath: testRepoPath,
|
||||
folderPath: host.remoteFolderPath
|
||||
})
|
||||
const launched = await session.launch()
|
||||
app = launched.app
|
||||
const page = launched.page
|
||||
await waitForSessionReady(page)
|
||||
await convertAndRetain(page, session.userDataDir, seeded)
|
||||
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
|
||||
(entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId
|
||||
)
|
||||
if (!environment) {
|
||||
throw new Error('Missing actual managed environment')
|
||||
}
|
||||
await dismissTransientAnnouncement(page)
|
||||
const nativeIdentity = getWorktreeHostIdentity({ id: nativeId, hostId: 'local' })
|
||||
await page.locator(`[data-worktree-host-identity="${nativeIdentity}"]:visible`).click()
|
||||
await openFileExplorer(page)
|
||||
const floatingId = await page.evaluate(
|
||||
(url) => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
if (!store || !state?.settings) {
|
||||
throw new Error('Missing ready settings')
|
||||
}
|
||||
store.setState({ settings: { ...state.settings, floatingTerminalEnabled: true } })
|
||||
const tab = store.getState().createBrowserTab('global-floating-terminal', url, {
|
||||
activate: true,
|
||||
focusAddressBar: false,
|
||||
targetGroupId: state.ensureWorktreeRootGroup('global-floating-terminal'),
|
||||
browserRuntimeEnvironmentId: null
|
||||
})
|
||||
return tab.id
|
||||
},
|
||||
server.pageUrl('float', 1)
|
||||
)
|
||||
await expect(page.locator('[data-floating-terminal-panel]')).toHaveCount(1)
|
||||
if ((await page.locator('[data-floating-terminal-panel][aria-hidden="false"]').count()) === 0) {
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('orca-toggle-floating-terminal')))
|
||||
}
|
||||
await expect(page.locator(`[data-browser-overlay-tab-id="${floatingId}"]`)).toBeVisible()
|
||||
await waitForGuestUrl(page, floatingId, server.pageUrl('float', 1))
|
||||
console.log('[native-actual-drag-payload]', await dropExplorerFile(page, fileName, floatingId))
|
||||
await expect
|
||||
.poll(async () => (await guestText(page, floatingId)).text)
|
||||
.toContain('DESKTOP_BROWSER_DROP_OWNER')
|
||||
console.log('[native-positive-actual-guest]', await guestText(page, floatingId))
|
||||
await navigateGuest(page, floatingId, server.pageUrl('float', 2))
|
||||
await waitForGuestIdle(page, floatingId)
|
||||
const remoteIdentity = getWorktreeHostIdentity({
|
||||
id: seeded.worktreeId,
|
||||
hostId: toRuntimeExecutionHostId(environment.id)
|
||||
})
|
||||
await page.locator(`[data-worktree-host-identity="${remoteIdentity}"]:visible`).click()
|
||||
await openFileExplorer(page)
|
||||
await page.getByRole('button', { name: 'Refresh Explorer', exact: true }).click()
|
||||
await expect(
|
||||
page.locator('[data-file-explorer-row]').filter({ hasText: 'REMOTE_DROP_READY.txt' })
|
||||
).toBeVisible()
|
||||
expect(
|
||||
await page.evaluate(() => window.__store?.getState().activeWorkspaceExecutionHostId)
|
||||
).toBe(toRuntimeExecutionHostId(environment.id))
|
||||
console.log(
|
||||
'[independent-managed-source]',
|
||||
await serverCall(page, environment.id, 'files.read', {
|
||||
worktree: `id:${seeded.worktreeId}`,
|
||||
relativePath: fileName
|
||||
})
|
||||
)
|
||||
const remotePayload = await dropExplorerFile(page, fileName, floatingId)
|
||||
console.log('[managed-actual-drag-payload]', remotePayload)
|
||||
expect(
|
||||
JSON.parse(remotePayload['application/x-orca-workspace-file-source'] ?? '{}')
|
||||
).toMatchObject({
|
||||
executionHostId: toRuntimeExecutionHostId(environment.id),
|
||||
workspaceId: seeded.worktreeId
|
||||
})
|
||||
await waitForGuestIdle(page, floatingId)
|
||||
console.log('[managed-drop-actual-guest]', await guestText(page, floatingId))
|
||||
await page.screenshot({ path: testInfo.outputPath('managed-browser-drop-owner.png') })
|
||||
expect((await guestText(page, floatingId)).text).not.toContain('DESKTOP_BROWSER_DROP_OWNER')
|
||||
await expect(
|
||||
page.getByText('Open in Orca Browser is only available for local files.', { exact: true })
|
||||
).toBeVisible()
|
||||
expect((await guestText(page, floatingId)).url).toBe(server.pageUrl('float', 2))
|
||||
} finally {
|
||||
try {
|
||||
if (app) {
|
||||
await session.close(app)
|
||||
}
|
||||
await session.dispose()
|
||||
await server.close()
|
||||
} finally {
|
||||
host.cleanup()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -65,23 +65,7 @@ test('a managed orcad stops after idling and starts again on the next connect',
|
||||
app = first.app
|
||||
await waitForSessionReady(first.page)
|
||||
// A managed host is reached through its server, not a relay, so no relay repo is added.
|
||||
const remote = await first.page.evaluate(
|
||||
async (input) => {
|
||||
const { target: created } = await window.api.ssh.addTarget({ target: input })
|
||||
const state = await window.api.ssh.connect({ targetId: created.id })
|
||||
return { targetId: created.id, managedServer: state?.managedServer ?? null }
|
||||
},
|
||||
{
|
||||
label: `orcad idle E2E ${Date.now()}`,
|
||||
host: target.host,
|
||||
port: target.port,
|
||||
username: 'root',
|
||||
identityFile: target.identityFile,
|
||||
identitiesOnly: true,
|
||||
relayGracePeriodSeconds: 1
|
||||
}
|
||||
)
|
||||
expect(remote.managedServer).toMatchObject({ kind: 'managed' })
|
||||
const remote = await connectManagedHost(first.page, target)
|
||||
expect(runningOrcadPids(target)).toHaveLength(1)
|
||||
|
||||
// While the client is connected the server stays up past its quiet period.
|
||||
@@ -112,6 +96,13 @@ test('a managed orcad stops after idling and starts again on the next connect',
|
||||
// A start inherited from the launch-time connect this reconnect dropped would report `serving`.
|
||||
expect(JSON.parse(connected)).toMatchObject({ kind: 'managed' })
|
||||
expect(JSON.parse(connected)).not.toHaveProperty('serving')
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await callEnvironment(second.page, remote.environmentId, 'terminal.list', {})).ok,
|
||||
{ timeout: 60_000 }
|
||||
)
|
||||
.toBe(true)
|
||||
expect(runningOrcadPids(target)).toHaveLength(1)
|
||||
// The restarted server read the record, so a later crash cannot be mistaken for an idle stop.
|
||||
expect(readIdleStopRecord(target)).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user