Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions contracts/fixtures/dispatch-disclosure.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,5 +474,11 @@
"producer": "webdriver",
"trigger": "a mutating POST the driver answered 503: it received the request, but not whether the mutation completed",
"dispatched": "unknown"
},
{
"id": "webdriver.orientation.every-route-unsupported",
"producer": "webdriver",
"trigger": "orientation refused by both the rotation route and the orientation route: each answer said the route is not implemented, so nothing ran",
"dispatched": "no"
}
]
1 change: 1 addition & 0 deletions packages/contracts/src/dispatch-disclosure.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const DISPATCH_DISCLOSURE_DRIVER_OWNERS: Readonly<Record<string, string>>
'android-helper.gesture-session.':
'packages/platform-android/src/__tests__/touch-helper-session.test.ts',
'webdriver.': 'packages/provider-webdriver/src/webdriver-transport.test.ts',
'webdriver.orientation.': 'packages/provider-webdriver/src/webdriver-orientation.test.ts',
'maestro-direct.':
'src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts',
};
Expand Down
38 changes: 38 additions & 0 deletions packages/provider-webdriver/src/webdriver-orientation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { AppError } from '@agent-device/kernel/errors';
import type { WebDriverClient } from './webdriver-client.ts';
import { setWebDriverOrientation } from './webdriver-orientation.ts';
import { WebDriverTransport } from './webdriver-transport.ts';
import fs from 'node:fs';
import {
assertDispatchDisclosureDriversMatchRows,
DISPATCH_DISCLOSURE_TABLE_PATH,
dispatchDisclosureRowsOwnedBy,
} from '@agent-device/contracts/dispatch-disclosure-fixtures';

type Call = { method: string; args: unknown[] };

Expand Down Expand Up @@ -181,6 +187,8 @@ test('exhausting both endpoints reports the rotation and each attempt', async ()
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.match(error.message, /landscape-left/);
assert.equal(error.details?.reason, 'webdriver_route_unsupported');
assert.equal(error.details?.dispatched, 'no');
assert.match(String(error.details?.hint), /--provider-device-orientation/);
const attempts = error.details?.attempts;
assert.ok(Array.isArray(attempts));
Expand All @@ -192,3 +200,33 @@ test('exhausting both endpoints reports the rotation and each attempt', async ()
},
);
});

const DRIVERS: Record<string, () => Promise<unknown>> = {
'webdriver.orientation.every-route-unsupported': async () =>
await setWebDriverOrientation(
makeClient({ reject: ['setRotation', 'setOrientation'] }).client,
'android',
'landscape-left',
),
};

const ROWS = dispatchDisclosureRowsOwnedBy(
import.meta.url,
fs.readFileSync(DISPATCH_DISCLOSURE_TABLE_PATH, 'utf8'),
);

test('every webdriver orientation dispatch-disclosure row has exactly one driver', () => {
assertDispatchDisclosureDriversMatchRows(ROWS, Object.keys(DRIVERS));
});

for (const row of ROWS) {
test(`${row.id}: ${row.trigger} → dispatched ${row.dispatched}`, async () => {
const drive = DRIVERS[row.id];
assert.ok(drive, `no driver for ${row.id}`);
await assert.rejects(drive(), (error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.details?.dispatched, row.dispatched);
return true;
});
});
}
3 changes: 2 additions & 1 deletion packages/provider-webdriver/src/webdriver-orientation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@agent-device/contracts/device';
import { AppError } from '@agent-device/kernel/errors';
import type { WebDriverClient } from './webdriver-client.ts';
import { isWebDriverRouteUnsupported } from './webdriver-transport.ts';
import { isWebDriverRouteUnsupported, unsupportedRouteRefusal } from './webdriver-transport.ts';

export type WebDriverOrientationBackend = 'android' | 'xctest';

Expand Down Expand Up @@ -58,6 +58,7 @@ export async function setWebDriverOrientation(
'COMMAND_FAILED',
`Could not set device orientation to ${rotation} on the hosted WebDriver session.`,
{
...unsupportedRouteRefusal(),
hint: 'The provider driver rejected both orientation endpoints. Set the orientation as a session capability instead, for example connect --provider-device-orientation portrait.',
rotation,
attempts,
Expand Down
14 changes: 12 additions & 2 deletions packages/provider-webdriver/src/webdriver-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,9 @@ function webdriverError(status: number, payload: unknown): AppError {
: `WebDriver request failed with HTTP ${status}.`;
if (isUnsupportedRouteAnswer(status, payload)) {
return new AppError('COMMAND_FAILED', message, {
reason: WEBDRIVER_ROUTE_UNSUPPORTED_REASON,
...unsupportedRouteRefusal(),
status,
response: payload,
...dispatchDisclosure('no'),
});
}
return new AppError('COMMAND_FAILED', message, {
Expand All @@ -352,6 +351,17 @@ function webdriverError(status: number, payload: unknown): AppError {
/** Machine-readable `details.reason` of a driver answer that it does not implement the route. */
const WEBDRIVER_ROUTE_UNSUPPORTED_REASON = 'webdriver_route_unsupported';

/**
* The details of a refusal the driver answered before running anything: it does not implement
* the route. A command that tried every route it has and was refused by each carries the same.
*/
export function unsupportedRouteRefusal(): {
reason: typeof WEBDRIVER_ROUTE_UNSUPPORTED_REASON;
dispatched: DispatchDisclosure;
} {
return { reason: WEBDRIVER_ROUTE_UNSUPPORTED_REASON, ...dispatchDisclosure('no') };
}

/** W3C `error` codes a driver answers with when it does not implement the route. */
const UNSUPPORTED_ROUTE_ERROR_CODES: ReadonlySet<string> = new Set([
'unknown command',
Expand Down
Loading