mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
a707348b9b
* feat: add cloud webdriver artifacts * fix: clean up local session after provider release failure * fix: tag cloud webdriver provider requests * feat: connect hosted webdriver providers * docs: document hosted provider credentials * refactor: tighten cloud webdriver provider internals * refactor: consolidate cloud webdriver provider definitions * refactor: collapse hosted webdriver runtime wrapper * refactor: reduce cloud webdriver smell surface * docs: clarify hosted provider interfaces * fix: avoid regex slash trimming in webdriver urls * docs: rename hosted providers to device clouds * fix: align provider profile imports with remote modules * fix: skip local android recovery for provider devices * test: classify cloud provider integration flags * fix: close active cloud connection session * test: cover provider disconnect cli flow * fix: make cloud webdriver sessions launchable * fix: align cloud webdriver input gestures * fix: avoid keyboard input during cloud scroll * fix: constrain cloud webdriver scroll gestures * refactor: isolate cloud webdriver scroll frame * refactor: deduplicate cloud webdriver helpers * refactor: tighten cloud webdriver action types * fix: harden cloud webdriver release * fix: polish provider disconnect diagnostics * refactor: group connection profile helpers * fix: repair rebased internal paths * fix: satisfy cloud webdriver CI guards
568 lines
19 KiB
TypeScript
568 lines
19 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import type { ServerResponse } from 'node:http';
|
|
import { test } from 'vitest';
|
|
import { createCloudWebDriverRuntime } from '../../../src/cloud-webdriver/runtime.ts';
|
|
import { createDefaultCloudWebDriverProviderRuntimes } from '../../../src/cloud-webdriver/provider-runtimes.ts';
|
|
import { scrollFrameFromWebDriverSource } from '../../../src/cloud-webdriver/webdriver-scroll-frame.ts';
|
|
import { parseWebDriverSource } from '../../../src/cloud-webdriver/webdriver-source.ts';
|
|
import { CLOUD_WEBDRIVER_PROVIDERS } from '../../../src/cloud-webdriver/providers.ts';
|
|
import type { CloudArtifact } from '../../../src/cloud-artifacts.ts';
|
|
import { createProviderDeviceRuntimeRequestProviders } from '../../../src/provider-device-runtime.ts';
|
|
import type { DeviceLease } from '../../../src/daemon/lease-registry.ts';
|
|
import type { DaemonRequest } from '../../../src/daemon/types.ts';
|
|
import { assertRpcError, assertRpcOk } from './assertions.ts';
|
|
import {
|
|
createProviderScenarioHarness,
|
|
withProviderScenarioResource,
|
|
withProviderScenarioTempDir,
|
|
} from './harness.ts';
|
|
import { runProviderScenario, type ProviderScenarioStep } from './scenario.ts';
|
|
import {
|
|
CloudWebDriverTestServer,
|
|
type CloudWebDriverHttpCall,
|
|
startCloudWebDriverTestServer,
|
|
type StartedCloudWebDriverTestServer,
|
|
writeCloudWebDriverTestJson,
|
|
} from './cloud-webdriver-test-server.ts';
|
|
|
|
const WEBDRIVER_PROVIDER = 'webdriver-fake';
|
|
|
|
test('Cloud WebDriver runtime drives provider devices through daemon commands', async () => {
|
|
await withProviderScenarioResource(createCloudWebDriverWorld, async (world) => {
|
|
const { daemon, server } = world;
|
|
await withProviderScenarioTempDir('agent-device-cloud-webdriver-', async (tempDir) => {
|
|
const appPath = path.join(tempDir, 'demo.apk');
|
|
fs.writeFileSync(appPath, 'fake apk');
|
|
const lease = await allocateWebDriverLease(daemon);
|
|
const steps = cloudWebDriverScenarioSteps(appPath, lease);
|
|
const releaseStep = steps.at(-1);
|
|
assert.ok(releaseStep);
|
|
await runProviderScenario(daemon, steps.slice(0, -1), {
|
|
flags: leaseFlags(lease.leaseId),
|
|
meta: leaseMeta(lease.leaseId),
|
|
});
|
|
const inferredArtifacts = await daemon.callCommand('artifacts');
|
|
const inferredData = assertRpcOk<{
|
|
provider?: string;
|
|
status?: string;
|
|
providerSessionId?: string;
|
|
}>(inferredArtifacts);
|
|
assert.equal(inferredData.provider, WEBDRIVER_PROVIDER);
|
|
assert.equal(inferredData.status, 'ready');
|
|
assert.equal(inferredData.providerSessionId, 'wd-1');
|
|
|
|
world.failNextArtifactLookup();
|
|
const unavailableArtifacts = await daemon.callCommand('artifacts');
|
|
const unavailableData = assertRpcOk<{
|
|
provider?: string;
|
|
status?: string;
|
|
providerSessionId?: string;
|
|
cloudArtifacts?: CloudArtifact[];
|
|
}>(unavailableArtifacts);
|
|
assert.equal(unavailableData.provider, WEBDRIVER_PROVIDER);
|
|
assert.equal(unavailableData.status, 'unavailable');
|
|
assert.equal(unavailableData.providerSessionId, 'wd-1');
|
|
assert.deepEqual(unavailableData.cloudArtifacts, []);
|
|
|
|
await runProviderScenario(daemon, [releaseStep], {
|
|
flags: leaseFlags(lease.leaseId),
|
|
meta: leaseMeta(lease.leaseId),
|
|
});
|
|
assertWebDriverCalls(server.calls, lease.leaseId, appPath);
|
|
});
|
|
});
|
|
}, 15_000);
|
|
|
|
test('Cloud WebDriver release still returns artifacts when WebDriver session delete fails', async () => {
|
|
await withProviderScenarioResource(createCloudWebDriverWorld, async (world) => {
|
|
const { daemon, server } = world;
|
|
const lease = await allocateWebDriverLease(daemon);
|
|
server.sessionDeleteFailuresRemaining = 2;
|
|
|
|
const release = await daemon.callCommand('lease_release', [], leaseFlags(lease.leaseId), {
|
|
meta: leaseMeta(lease.leaseId),
|
|
});
|
|
|
|
const data = assertRpcOk<{
|
|
released?: boolean;
|
|
provider?: {
|
|
provider?: string;
|
|
providerSessionId?: string;
|
|
warnings?: Array<{ code?: string; message?: string }>;
|
|
cloudArtifacts?: {
|
|
status?: string;
|
|
cloudArtifacts?: Array<{ kind?: string }>;
|
|
};
|
|
};
|
|
}>(release);
|
|
assert.equal(data.released, true);
|
|
assert.equal(data.provider?.provider, WEBDRIVER_PROVIDER);
|
|
assert.equal(data.provider?.providerSessionId, 'wd-1');
|
|
assert.equal(data.provider?.warnings?.[0]?.code, 'WEBDRIVER_SESSION_DELETE_FAILED');
|
|
assert.match(data.provider?.warnings?.[0]?.message ?? '', /stale webdriver session/);
|
|
assert.equal(data.provider?.cloudArtifacts?.status, 'ready');
|
|
assert.equal(data.provider?.cloudArtifacts?.cloudArtifacts?.[0]?.kind, 'video');
|
|
});
|
|
}, 15_000);
|
|
|
|
test('Cloud WebDriver allocation preserves create-session failure when cleanup fails', async () => {
|
|
let cleanupCalled = false;
|
|
await withProviderScenarioResource(
|
|
() =>
|
|
createCloudWebDriverWorld({
|
|
cleanup: async () => {
|
|
cleanupCalled = true;
|
|
throw new Error('provider cleanup failed');
|
|
},
|
|
}),
|
|
async (world) => {
|
|
const { daemon, server } = world;
|
|
server.createSessionFailuresRemaining = 2;
|
|
|
|
const allocate = await daemon.callCommand('lease_allocate', [], leaseFlags(), {
|
|
meta: leaseMeta(),
|
|
});
|
|
|
|
const error = assertRpcError(allocate, 'COMMAND_FAILED', /create session failed/) as {
|
|
details?: { cleanupError?: unknown };
|
|
};
|
|
assert.equal(error.details?.cleanupError, 'provider cleanup failed');
|
|
assert.equal(cleanupCalled, true);
|
|
},
|
|
);
|
|
}, 15_000);
|
|
|
|
test('default BrowserStack provider runtime builds sessions from daemon request profile flags', async () => {
|
|
const server = await FakeWebDriverServer.start();
|
|
const runtimes = createDefaultCloudWebDriverProviderRuntimes({
|
|
BROWSERSTACK_USERNAME: 'browser-user',
|
|
BROWSERSTACK_ACCESS_KEY: 'browser-key',
|
|
BROWSERSTACK_WEBDRIVER_ENDPOINT: `${server.url}/wd/hub/`,
|
|
});
|
|
const providers = createProviderDeviceRuntimeRequestProviders(runtimes);
|
|
const daemon = await createProviderScenarioHarness({
|
|
...providers,
|
|
deviceInventoryProvider: providers.deviceInventoryProvider!,
|
|
});
|
|
try {
|
|
const allocate = await daemon.callCommand(
|
|
'lease_allocate',
|
|
[],
|
|
{
|
|
tenant: 'team-a',
|
|
runId: 'run-a',
|
|
platform: 'android',
|
|
device: 'Google Pixel 8',
|
|
providerApp: 'bs://app-id',
|
|
providerOsVersion: '14.0',
|
|
providerProject: 'agent-device',
|
|
providerBuild: 'build-a',
|
|
providerSessionName: 'session-a',
|
|
},
|
|
{
|
|
meta: {
|
|
tenantId: 'team-a',
|
|
runId: 'run-a',
|
|
leaseBackend: 'android-instance',
|
|
leaseProvider: CLOUD_WEBDRIVER_PROVIDERS.browserStack,
|
|
clientId: 'client-a',
|
|
},
|
|
},
|
|
);
|
|
const data = assertRpcOk<{
|
|
lease?: DeviceLease;
|
|
provider?: {
|
|
provider?: string;
|
|
sessionId?: string;
|
|
providerSessionId?: string;
|
|
};
|
|
}>(allocate);
|
|
assert.equal(data.provider?.provider, CLOUD_WEBDRIVER_PROVIDERS.browserStack);
|
|
assert.equal(data.provider?.providerSessionId, 'wd-1');
|
|
assert.deepEqual(server.calls[0]?.body, {
|
|
capabilities: {
|
|
alwaysMatch: {
|
|
platformName: 'Android',
|
|
'appium:deviceName': 'Google Pixel 8',
|
|
device: 'Google Pixel 8',
|
|
os_version: '14.0',
|
|
app: 'bs://app-id',
|
|
'bstack:options': {
|
|
projectName: 'agent-device',
|
|
buildName: 'build-a',
|
|
sessionName: 'session-a',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(
|
|
server.calls[0]?.headers.authorization,
|
|
`Basic ${Buffer.from('browser-user:browser-key').toString('base64')}`,
|
|
);
|
|
} finally {
|
|
await daemon.close();
|
|
await Promise.allSettled(runtimes.map(async (runtime) => await runtime.shutdown()));
|
|
await server.close();
|
|
}
|
|
}, 15_000);
|
|
|
|
test('WebDriver source parser reuses hardened XML parsing', () => {
|
|
const nodes = parseWebDriverSource(
|
|
'<hierarchy><node text="A > B" resource-id="login" bounds="[0,0][10,10]" displayed="true" /></hierarchy>',
|
|
);
|
|
|
|
assert.equal(nodes[0]?.label, 'A > B');
|
|
assert.equal(nodes[0]?.identifier, 'login');
|
|
assert.deepEqual(nodes[0]?.rect, { x: 0, y: 0, width: 10, height: 10 });
|
|
assert.throws(
|
|
() => parseWebDriverSource('<node __proto__="polluted" text="x" />'),
|
|
/Unsupported XML attribute name "__proto__"/,
|
|
);
|
|
});
|
|
|
|
test('WebDriver scroll frame prefers visible scrollable containers', () => {
|
|
assert.deepEqual(
|
|
scrollFrameFromWebDriverSource(
|
|
'<hierarchy>' +
|
|
'<android.widget.FrameLayout bounds="[0,0][1080,2400]" displayed="true" />' +
|
|
'<android.widget.ListView bounds="[0,393][1080,1496]" displayed="true" />' +
|
|
'<android.support.v7.widget.RecyclerView bounds="[18,597][1062,1196]" displayed="false" />' +
|
|
'</hierarchy>',
|
|
),
|
|
{ x: 0, y: 393, width: 1080, height: 1103 },
|
|
);
|
|
});
|
|
|
|
async function createCloudWebDriverWorld(
|
|
options: { cleanup?: () => Promise<Record<string, unknown> | undefined> } = {},
|
|
) {
|
|
const server = await FakeWebDriverServer.start();
|
|
let artifactFailuresRemaining = 0;
|
|
const runtime = createCloudWebDriverRuntime({
|
|
provider: WEBDRIVER_PROVIDER,
|
|
endpoint: `${server.url}/wd/hub/`,
|
|
platform: 'android',
|
|
deviceName: 'BrowserStack Google Pixel 8',
|
|
webdriverCapabilities: (lease) => ({
|
|
'appium:automationName': 'UiAutomator2',
|
|
'bstack:options': {
|
|
buildName: lease.runId,
|
|
sessionName: lease.leaseId,
|
|
},
|
|
}),
|
|
prepareSession: options.cleanup
|
|
? async ({ base }) => ({ ...base, cleanup: options.cleanup })
|
|
: undefined,
|
|
listArtifacts: async ({ provider, providerSessionId }) => {
|
|
if (artifactFailuresRemaining > 0) {
|
|
artifactFailuresRemaining -= 1;
|
|
throw new Error('provider artifact lookup failed');
|
|
}
|
|
return {
|
|
provider,
|
|
providerSessionId,
|
|
status: 'ready',
|
|
cloudArtifacts: [
|
|
{
|
|
provider,
|
|
providerSessionId,
|
|
kind: 'video',
|
|
name: 'Session video',
|
|
url: 'https://provider.example/video.mp4',
|
|
availability: 'ready',
|
|
},
|
|
],
|
|
};
|
|
},
|
|
});
|
|
const providers = createProviderDeviceRuntimeRequestProviders([runtime]);
|
|
const daemon = await createProviderScenarioHarness({
|
|
...providers,
|
|
deviceInventoryProvider: providers.deviceInventoryProvider!,
|
|
});
|
|
return {
|
|
daemon,
|
|
server,
|
|
failNextArtifactLookup: () => {
|
|
artifactFailuresRemaining += 1;
|
|
},
|
|
close: async () => {
|
|
await runtime.shutdown();
|
|
await daemon.close();
|
|
await server.close();
|
|
},
|
|
};
|
|
}
|
|
|
|
async function allocateWebDriverLease(
|
|
daemon: Awaited<ReturnType<typeof createProviderScenarioHarness>>,
|
|
): Promise<DeviceLease> {
|
|
const allocate = await daemon.callCommand('lease_allocate', [], leaseFlags(), {
|
|
meta: leaseMeta(),
|
|
});
|
|
const data = assertRpcOk<{
|
|
lease: DeviceLease;
|
|
provider?: {
|
|
capabilities?: { operations?: { snapshot?: { support?: string } } };
|
|
};
|
|
}>(allocate);
|
|
assert.equal(data.provider?.capabilities?.operations?.snapshot?.support, 'partial');
|
|
return data.lease;
|
|
}
|
|
|
|
function cloudWebDriverScenarioSteps(appPath: string, lease: DeviceLease): ProviderScenarioStep[] {
|
|
return [
|
|
{
|
|
name: 'heartbeat',
|
|
command: 'lease_heartbeat',
|
|
expectData: { provider: { provider: WEBDRIVER_PROVIDER } },
|
|
},
|
|
{
|
|
name: 'install',
|
|
command: 'install',
|
|
positionals: ['com.example.demo', appPath],
|
|
expectData: {
|
|
platform: 'android',
|
|
packageName: 'com.example.demo',
|
|
},
|
|
},
|
|
{
|
|
name: 'open',
|
|
command: 'open',
|
|
positionals: ['com.example.demo'],
|
|
expectData: {
|
|
platform: 'android',
|
|
id: `webdriver-fake:android:${lease.leaseId}`,
|
|
serial: `webdriver-fake:android:${lease.leaseId}`,
|
|
},
|
|
},
|
|
{ name: 'click', command: 'click', positionals: ['10', '20'], expectData: { x: 10, y: 20 } },
|
|
{
|
|
name: 'fill',
|
|
command: 'fill',
|
|
positionals: ['12', '24', 'hello cloud'],
|
|
expectData: { x: 12, y: 24, text: 'hello cloud' },
|
|
},
|
|
{
|
|
name: 'snapshot',
|
|
command: 'snapshot',
|
|
assert: (response) => {
|
|
const data = assertRpcOk<{
|
|
nodes?: Array<{
|
|
label?: string;
|
|
identifier?: string;
|
|
depth?: number;
|
|
parentIndex?: number;
|
|
hittable?: boolean;
|
|
}>;
|
|
}>(response);
|
|
assert.equal(data.nodes?.[1]?.label, 'Login');
|
|
assert.equal(data.nodes?.[1]?.identifier, 'com.example:id/login');
|
|
assert.equal(data.nodes?.[1]?.depth, 1);
|
|
assert.equal(data.nodes?.[1]?.parentIndex, 0);
|
|
assert.equal(data.nodes?.[1]?.hittable, true);
|
|
},
|
|
},
|
|
{
|
|
name: 'scroll',
|
|
command: 'scroll',
|
|
positionals: ['down'],
|
|
flags: { pixels: 200 },
|
|
expectData: { direction: 'down', distance: 200 },
|
|
},
|
|
{
|
|
name: 'artifacts',
|
|
command: 'artifacts',
|
|
expectData: {
|
|
provider: WEBDRIVER_PROVIDER,
|
|
status: 'ready',
|
|
providerSessionId: 'wd-1',
|
|
},
|
|
},
|
|
{
|
|
name: 'release',
|
|
command: 'lease_release',
|
|
assert: (response) => {
|
|
const data = assertRpcOk<{
|
|
released?: boolean;
|
|
provider?: {
|
|
provider?: string;
|
|
providerSessionId?: string;
|
|
cloudArtifacts?: {
|
|
status?: string;
|
|
cloudArtifacts?: Array<{ kind?: string }>;
|
|
};
|
|
};
|
|
}>(response);
|
|
assert.equal(data.released, true);
|
|
assert.equal(data.provider?.provider, WEBDRIVER_PROVIDER);
|
|
assert.equal(data.provider?.providerSessionId, 'wd-1');
|
|
assert.equal(data.provider?.cloudArtifacts?.status, 'ready');
|
|
assert.equal(data.provider?.cloudArtifacts?.cloudArtifacts?.[0]?.kind, 'video');
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
function assertWebDriverCalls(
|
|
calls: readonly CloudWebDriverHttpCall[],
|
|
leaseId: string,
|
|
appPath: string,
|
|
): void {
|
|
assert.deepEqual(
|
|
calls.map((call) => `${call.method} ${call.path}`),
|
|
[
|
|
'POST /wd/hub/session',
|
|
'POST /wd/hub/session/wd-1/appium/device/install_app',
|
|
'POST /wd/hub/session/wd-1/appium/device/activate_app',
|
|
'POST /wd/hub/session/wd-1/actions',
|
|
'DELETE /wd/hub/session/wd-1/actions',
|
|
'POST /wd/hub/session/wd-1/actions',
|
|
'DELETE /wd/hub/session/wd-1/actions',
|
|
'POST /wd/hub/session/wd-1/keys',
|
|
'GET /wd/hub/session/wd-1/source',
|
|
'POST /wd/hub/session/wd-1/appium/device/hide_keyboard',
|
|
'GET /wd/hub/session/wd-1/source',
|
|
'POST /wd/hub/session/wd-1/actions',
|
|
'DELETE /wd/hub/session/wd-1/actions',
|
|
'DELETE /wd/hub/session/wd-1',
|
|
],
|
|
);
|
|
assert.deepEqual(calls[0]?.body, {
|
|
capabilities: {
|
|
alwaysMatch: {
|
|
platformName: 'Android',
|
|
'appium:deviceName': 'BrowserStack Google Pixel 8',
|
|
'appium:automationName': 'UiAutomator2',
|
|
'bstack:options': {
|
|
buildName: 'run-a',
|
|
sessionName: leaseId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.deepEqual(calls[1]?.body, { appPath });
|
|
assert.deepEqual(calls[2]?.body, { appId: 'com.example.demo' });
|
|
assert.deepEqual(calls[7]?.body, { value: Array.from('hello cloud') });
|
|
assert.equal(calls[9]?.body, undefined);
|
|
assert.deepEqual(calls[11]?.body, {
|
|
actions: [
|
|
{
|
|
type: 'pointer',
|
|
id: 'swipe',
|
|
parameters: { pointerType: 'touch' },
|
|
actions: [
|
|
{ type: 'pointerMove', duration: 0, x: 540, y: 988 },
|
|
{ type: 'pointerDown', button: 0 },
|
|
{ type: 'pointerMove', duration: 350, x: 540, y: 788 },
|
|
{ type: 'pointerUp', button: 0 },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
for (const call of calls) {
|
|
assert.equal(call.headers['x-agent-device-client'], 'agent-device-cli');
|
|
assert.equal(typeof call.headers['x-agent-device-version'], 'string');
|
|
assert.notEqual(call.headers['x-agent-device-version'], '');
|
|
}
|
|
}
|
|
|
|
class FakeWebDriverServer extends CloudWebDriverTestServer {
|
|
createSessionFailuresRemaining = 0;
|
|
sessionDeleteFailuresRemaining = 0;
|
|
|
|
static async start(): Promise<StartedCloudWebDriverTestServer<FakeWebDriverServer>> {
|
|
return await startCloudWebDriverTestServer(new FakeWebDriverServer());
|
|
}
|
|
|
|
protected respond(call: CloudWebDriverHttpCall, res: ServerResponse): void {
|
|
respondToFakeWebDriverCall(this, call, res);
|
|
}
|
|
}
|
|
|
|
function respondToFakeWebDriverCall(
|
|
server: FakeWebDriverServer,
|
|
call: CloudWebDriverHttpCall,
|
|
res: ServerResponse,
|
|
): void {
|
|
switch (`${call.method} ${call.path}`) {
|
|
case 'POST /wd/hub/session':
|
|
writeFakeCreateSessionResponse(server, res);
|
|
return;
|
|
case 'GET /wd/hub/session/wd-1/source':
|
|
writeCloudWebDriverTestJson(res, { value: fakeWebDriverSource() });
|
|
return;
|
|
case 'GET /wd/hub/session/wd-1/window/rect':
|
|
writeCloudWebDriverTestJson(res, { value: { x: 0, y: 0, width: 1080, height: 1920 } });
|
|
return;
|
|
case 'DELETE /wd/hub/session/wd-1/actions':
|
|
writeCloudWebDriverTestJson(
|
|
res,
|
|
{ value: { message: 'The requested resource could not be found.' } },
|
|
500,
|
|
);
|
|
return;
|
|
case 'DELETE /wd/hub/session/wd-1':
|
|
writeFakeDeleteSessionResponse(server, res);
|
|
return;
|
|
default:
|
|
writeCloudWebDriverTestJson(res, { value: null });
|
|
}
|
|
}
|
|
|
|
function writeFakeCreateSessionResponse(server: FakeWebDriverServer, res: ServerResponse): void {
|
|
if (server.createSessionFailuresRemaining > 0) {
|
|
server.createSessionFailuresRemaining -= 1;
|
|
writeCloudWebDriverTestJson(res, { value: { message: 'create session failed' } }, 500);
|
|
return;
|
|
}
|
|
writeCloudWebDriverTestJson(res, {
|
|
value: {
|
|
sessionId: 'wd-1',
|
|
capabilities: { platformName: 'Android' },
|
|
},
|
|
});
|
|
}
|
|
|
|
function writeFakeDeleteSessionResponse(server: FakeWebDriverServer, res: ServerResponse): void {
|
|
if (server.sessionDeleteFailuresRemaining > 0) {
|
|
server.sessionDeleteFailuresRemaining -= 1;
|
|
writeCloudWebDriverTestJson(res, { value: { message: 'stale webdriver session' } }, 500);
|
|
return;
|
|
}
|
|
writeCloudWebDriverTestJson(res, { value: null });
|
|
}
|
|
|
|
function fakeWebDriverSource(): string {
|
|
return (
|
|
'<hierarchy><node text="Root" bounds="[0,0][100,40]" displayed="true">' +
|
|
'<node text="Login" resource-id="com.example:id/login" bounds="[10,20][110,70]" displayed="true" />' +
|
|
'<android.widget.ListView resource-id="com.example:id/results" bounds="[0,279][1080,1496]" displayed="true" />' +
|
|
'</node></hierarchy>'
|
|
);
|
|
}
|
|
|
|
function leaseFlags(leaseId?: string): DaemonRequest['flags'] {
|
|
return {
|
|
platform: 'android',
|
|
tenant: 'team-a',
|
|
runId: 'run-a',
|
|
leaseId,
|
|
leaseProvider: WEBDRIVER_PROVIDER,
|
|
};
|
|
}
|
|
|
|
function leaseMeta(leaseId?: string): DaemonRequest['meta'] {
|
|
return {
|
|
tenantId: 'team-a',
|
|
runId: 'run-a',
|
|
leaseId,
|
|
leaseBackend: 'android-instance',
|
|
leaseProvider: WEBDRIVER_PROVIDER,
|
|
deviceKey: 'webdriver-android-a',
|
|
clientId: 'client-a',
|
|
};
|
|
}
|