Skip to content

Commit 6975bcc

Browse files
feat: emulate geolocation positionUnavailable (#3359)
Spec is landed in w3c/webdriver-bidi#911
1 parent 7909fb6 commit 6975bcc

File tree

8 files changed

+174
-34
lines changed

8 files changed

+174
-34
lines changed

src/bidiMapper/modules/browser/UserContextConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export class UserContextConfig {
2828
readonly userContextId: string;
2929
viewport?: BrowsingContext.Viewport | null;
3030
devicePixelRatio?: number | null;
31-
emulatedGeolocation?: Emulation.GeolocationCoordinates | null;
31+
geolocation?:
32+
| Emulation.GeolocationCoordinates
33+
| Emulation.GeolocationPositionError
34+
| null;
3235

3336
constructor(userContextId: string) {
3437
this.userContextId = userContextId;

src/bidiMapper/modules/cdp/CdpTarget.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type ChromiumBidi,
2525
type Emulation,
2626
Session,
27+
UnknownErrorException,
2728
UnsupportedOperationException,
2829
} from '../../../protocol/protocol.js';
2930
import {Deferred} from '../../../utils/Deferred.js';
@@ -636,13 +637,11 @@ export class CdpTarget {
636637
}
637638

638639
if (
639-
this.#userContextConfig.emulatedGeolocation !== undefined &&
640-
this.#userContextConfig.emulatedGeolocation !== null
640+
this.#userContextConfig.geolocation !== undefined &&
641+
this.#userContextConfig.geolocation !== null
641642
) {
642643
promises.push(
643-
this.setGeolocationOverride(
644-
this.#userContextConfig.emulatedGeolocation,
645-
),
644+
this.setGeolocationOverride(this.#userContextConfig.geolocation),
646645
);
647646
}
648647

@@ -672,21 +671,38 @@ export class CdpTarget {
672671
}
673672

674673
async setGeolocationOverride(
675-
coordinates: Emulation.GeolocationCoordinates | null,
674+
geolocation:
675+
| Emulation.GeolocationCoordinates
676+
| Emulation.GeolocationPositionError
677+
| null,
676678
): Promise<void> {
677-
if (coordinates === null) {
679+
if (geolocation === null) {
678680
await this.cdpClient.sendCommand('Emulation.clearGeolocationOverride');
679-
} else {
681+
} else if ('type' in geolocation) {
682+
if (geolocation.type !== 'positionUnavailable') {
683+
// Unreachable. Handled by params parser.
684+
throw new UnknownErrorException(
685+
`Unknown geolocation error ${geolocation.type}`,
686+
);
687+
}
688+
// Omitting latitude, longitude or accuracy emulates position unavailable.
689+
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {});
690+
} else if ('latitude' in geolocation) {
680691
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {
681-
latitude: coordinates.latitude,
682-
longitude: coordinates.longitude,
683-
accuracy: coordinates.accuracy ?? 1,
692+
latitude: geolocation.latitude,
693+
longitude: geolocation.longitude,
694+
accuracy: geolocation.accuracy ?? 1,
684695
// `null` value is treated as "missing".
685-
altitude: coordinates.altitude ?? undefined,
686-
altitudeAccuracy: coordinates.altitudeAccuracy ?? undefined,
687-
heading: coordinates.heading ?? undefined,
688-
speed: coordinates.speed ?? undefined,
696+
altitude: geolocation.altitude ?? undefined,
697+
altitudeAccuracy: geolocation.altitudeAccuracy ?? undefined,
698+
heading: geolocation.heading ?? undefined,
699+
speed: geolocation.speed ?? undefined,
689700
});
701+
} else {
702+
// Unreachable. Handled by params parser.
703+
throw new UnknownErrorException(
704+
'Unexpected geolocation coordinates value',
705+
);
690706
}
691707
}
692708
}

src/bidiMapper/modules/emulation/EmulationProcessor.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,42 @@ export class EmulationProcessor {
3939
async setGeolocationOverride(
4040
params: Emulation.SetGeolocationOverrideParameters,
4141
): Promise<EmptyResult> {
42-
if (
43-
(params.coordinates?.altitude ?? null) === null &&
44-
(params.coordinates?.altitudeAccuracy ?? null) !== null
45-
) {
42+
if ('coordinates' in params && 'error' in params) {
43+
// Unreachable. Handled by params parser.
4644
throw new InvalidArgumentException(
47-
'Geolocation altitudeAccuracy can be set only with altitude',
45+
'Coordinates and error cannot be set at the same time',
4846
);
4947
}
5048

49+
let geolocation:
50+
| Emulation.GeolocationCoordinates
51+
| Emulation.GeolocationPositionError
52+
| null = null;
53+
54+
if ('coordinates' in params) {
55+
if (
56+
(params.coordinates?.altitude ?? null) === null &&
57+
(params.coordinates?.altitudeAccuracy ?? null) !== null
58+
) {
59+
throw new InvalidArgumentException(
60+
'Geolocation altitudeAccuracy can be set only with altitude',
61+
);
62+
}
63+
64+
geolocation = params.coordinates;
65+
} else if ('error' in params) {
66+
if (params.error.type !== 'positionUnavailable') {
67+
// Unreachable. Handled by params parser.
68+
throw new InvalidArgumentException(
69+
`Unknown geolocation error ${params.error.type}`,
70+
);
71+
}
72+
geolocation = params.error;
73+
} else {
74+
// Unreachable. Handled by params parser.
75+
throw new InvalidArgumentException(`Coordinates or error should be set`);
76+
}
77+
5178
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(
5279
params.contexts,
5380
params.userContexts,
@@ -56,13 +83,13 @@ export class EmulationProcessor {
5683
for (const userContextId of params.userContexts ?? []) {
5784
const userContextConfig =
5885
this.#userContextStorage.getConfig(userContextId);
59-
userContextConfig.emulatedGeolocation = params.coordinates;
86+
userContextConfig.geolocation = geolocation;
6087
}
6188

6289
await Promise.all(
6390
browsingContexts.map(
6491
async (context) =>
65-
await context.cdpTarget.setGeolocationOverride(params.coordinates),
92+
await context.cdpTarget.setGeolocationOverride(geolocation),
6693
),
6794
);
6895
return {};

src/protocol-parser/generated/webdriver-bidi.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,14 +1176,27 @@ export namespace Emulation {
11761176
}
11771177
export namespace Emulation {
11781178
export const SetGeolocationOverrideParametersSchema = z.lazy(() =>
1179-
z.object({
1180-
coordinates: z.union([Emulation.GeolocationCoordinatesSchema, z.null()]),
1181-
contexts: z
1182-
.array(BrowsingContext.BrowsingContextSchema)
1183-
.min(1)
1184-
.optional(),
1185-
userContexts: z.array(Browser.UserContextSchema).min(1).optional(),
1186-
}),
1179+
z
1180+
.union([
1181+
z.object({
1182+
coordinates: z.union([
1183+
Emulation.GeolocationCoordinatesSchema,
1184+
z.null(),
1185+
]),
1186+
}),
1187+
z.object({
1188+
error: Emulation.GeolocationPositionErrorSchema,
1189+
}),
1190+
])
1191+
.and(
1192+
z.object({
1193+
contexts: z
1194+
.array(BrowsingContext.BrowsingContextSchema)
1195+
.min(1)
1196+
.optional(),
1197+
userContexts: z.array(Browser.UserContextSchema).min(1).optional(),
1198+
}),
1199+
),
11871200
);
11881201
}
11891202
export namespace Emulation {
@@ -1203,6 +1216,13 @@ export namespace Emulation {
12031216
}),
12041217
);
12051218
}
1219+
export namespace Emulation {
1220+
export const GeolocationPositionErrorSchema = z.lazy(() =>
1221+
z.object({
1222+
type: z.literal('positionUnavailable'),
1223+
}),
1224+
);
1225+
}
12061226
export const NetworkCommandSchema = z.lazy(() =>
12071227
z.union([
12081228
Network.AddInterceptSchema,

src/protocol-parser/protocol-parser.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ export namespace Session {
297297

298298
export namespace Emulation {
299299
export function parseSetGeolocationOverrideParams(params: unknown) {
300+
if ('coordinates' in (params as object) && 'error' in (params as object)) {
301+
// Zod picks the first matching parameter omitting the other. In this case, the
302+
// `parseObject` will remove `error` from the params. However, specification
303+
// requires to throw an exception.
304+
throw new InvalidArgumentException(
305+
'Coordinates and error cannot be set at the same time',
306+
);
307+
}
300308
return parseObject(
301309
params,
302310
WebDriverBidi.Emulation.SetGeolocationOverrideParametersSchema,

src/protocol/generated/webdriver-bidi.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -949,8 +949,14 @@ export namespace Emulation {
949949
};
950950
}
951951
export namespace Emulation {
952-
export type SetGeolocationOverrideParameters = {
953-
coordinates: Emulation.GeolocationCoordinates | null;
952+
export type SetGeolocationOverrideParameters = (
953+
| {
954+
coordinates: Emulation.GeolocationCoordinates | null;
955+
}
956+
| {
957+
error: Emulation.GeolocationPositionError;
958+
}
959+
) & {
954960
contexts?: [
955961
BrowsingContext.BrowsingContext,
956962
...BrowsingContext.BrowsingContext[],
@@ -998,6 +1004,11 @@ export namespace Emulation {
9981004
speed?: number | null;
9991005
};
10001006
}
1007+
export namespace Emulation {
1008+
export type GeolocationPositionError = {
1009+
type: 'positionUnavailable';
1010+
};
1011+
}
10011012
export type NetworkCommand =
10021013
| Network.AddIntercept
10031014
| Network.ContinueRequest

tests/emulation/__snapshots__/test_geolocation.ambr

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
# serializer version: 1
2+
# name: test_geolocation_emulate_unavailable
3+
dict({
4+
'type': 'object',
5+
'value': list([
6+
list([
7+
'code',
8+
dict({
9+
'type': 'number',
10+
'value': 2,
11+
}),
12+
]),
13+
]),
14+
})
15+
# ---
216
# name: test_geolocation_per_user_context
317
dict({
418
'type': 'object',

tests/emulation/test_geolocation.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def get_geolocation(websocket, context_id):
5858
new Promise(
5959
resolve => window.navigator.geolocation.getCurrentPosition(
6060
position => resolve(position.coords.toJSON()),
61-
error => resolve({code: error.code, message: error.message}),
61+
error => resolve({code: error.code}),
6262
{timeout: 200}
6363
))
6464
""",
@@ -123,6 +123,47 @@ async def test_geolocation_set_and_clear(websocket, context_id, url_example,
123123
assert initial_geolocation == await get_geolocation(websocket, context_id)
124124

125125

126+
@pytest.mark.asyncio
127+
async def test_geolocation_emulate_unavailable(websocket, context_id,
128+
url_example, snapshot):
129+
await goto_url(websocket, context_id, url_example)
130+
131+
await set_permission(websocket, get_origin(url_example),
132+
{'name': 'geolocation'}, 'granted')
133+
134+
initial_geolocation = await get_geolocation(websocket, context_id)
135+
136+
await execute_command(
137+
websocket, {
138+
'method': 'emulation.setGeolocationOverride',
139+
'params': {
140+
'contexts': [context_id],
141+
'error': {
142+
'type': 'positionUnavailable'
143+
}
144+
}
145+
})
146+
147+
emulated_geolocation = await get_geolocation(websocket, context_id)
148+
149+
assert initial_geolocation != emulated_geolocation, "Geolocation should have changed"
150+
assert emulated_geolocation == snapshot(
151+
), "New geolocation should match snapshot"
152+
153+
# Clear geolocation override.
154+
await execute_command(
155+
websocket, {
156+
'method': 'emulation.setGeolocationOverride',
157+
'params': {
158+
'contexts': [context_id],
159+
'coordinates': None
160+
}
161+
})
162+
163+
# Assert the geolocation has returned to the original state.
164+
assert initial_geolocation == await get_geolocation(websocket, context_id)
165+
166+
126167
@pytest.mark.asyncio
127168
async def test_geolocation_per_user_context(websocket, url_example,
128169
url_example_another_origin,

0 commit comments

Comments
 (0)