Skip to content

Commit 6f17ac3

Browse files
committedJan 24, 2024
Bug 1870848 - [remote] Introduce shared webdriver helper UserContextManager r=webdriver-reviewers,whimboo
Differential Revision: https://phabricator.services.mozilla.com/D198946
1 parent 9c44ff4 commit 6f17ac3

8 files changed

+549
-2
lines changed
 

‎remote/jar.mn

+2
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ remote.jar:
3131
content/shared/Stack.sys.mjs (shared/Stack.sys.mjs)
3232
content/shared/Sync.sys.mjs (shared/Sync.sys.mjs)
3333
content/shared/TabManager.sys.mjs (shared/TabManager.sys.mjs)
34+
content/shared/UserContextManager.sys.mjs (shared/UserContextManager.sys.mjs)
3435
content/shared/UUID.sys.mjs (shared/UUID.sys.mjs)
3536
content/shared/WebSocketConnection.sys.mjs (shared/WebSocketConnection.sys.mjs)
3637
content/shared/WindowManager.sys.mjs (shared/WindowManager.sys.mjs)
3738
content/shared/listeners/BrowsingContextListener.sys.mjs (shared/listeners/BrowsingContextListener.sys.mjs)
3839
content/shared/listeners/ConsoleAPIListener.sys.mjs (shared/listeners/ConsoleAPIListener.sys.mjs)
3940
content/shared/listeners/ConsoleListener.sys.mjs (shared/listeners/ConsoleListener.sys.mjs)
41+
content/shared/listeners/ContextualIdentityListener.sys.mjs (shared/listeners/ContextualIdentityListener.sys.mjs)
4042
content/shared/listeners/LoadListener.sys.mjs (shared/listeners/LoadListener.sys.mjs)
4143
content/shared/listeners/NavigationListener.sys.mjs (shared/listeners/NavigationListener.sys.mjs)
4244
content/shared/listeners/NetworkEventRecord.sys.mjs (shared/listeners/NetworkEventRecord.sys.mjs)
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
const lazy = {};
6+
7+
ChromeUtils.defineESModuleGetters(lazy, {
8+
ContextualIdentityService:
9+
"resource://gre/modules/ContextualIdentityService.sys.mjs",
10+
11+
ContextualIdentityListener:
12+
"chrome://remote/content/shared/listeners/ContextualIdentityListener.sys.mjs",
13+
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
14+
});
15+
16+
/**
17+
* A UserContextManager instance keeps track of all public user contexts and
18+
* maps their internal platform.
19+
*
20+
* This class is exported for test purposes. Otherwise the UserContextManager
21+
* singleton should be used.
22+
*/
23+
export class UserContextManagerClass {
24+
#contextualIdentityListener;
25+
#userContextIds;
26+
27+
DEFAULT_CONTEXT_ID = "default";
28+
DEFAULT_INTERNAL_ID = 0;
29+
30+
constructor() {
31+
// Map from internal ids (numbers) from the ContextualIdentityService to
32+
// opaque UUIDs (string).
33+
this.#userContextIds = new Map();
34+
35+
// The default user context is always using 0 as internal user context id
36+
// and should be exposed as "default" instead of a randomly generated id.
37+
this.#userContextIds.set(this.DEFAULT_INTERNAL_ID, this.DEFAULT_CONTEXT_ID);
38+
39+
// Register other (non-default) public contexts.
40+
lazy.ContextualIdentityService.getPublicIdentities().forEach(identity =>
41+
this.#registerIdentity(identity)
42+
);
43+
44+
this.#contextualIdentityListener = new lazy.ContextualIdentityListener();
45+
this.#contextualIdentityListener.on("created", this.#onIdentityCreated);
46+
this.#contextualIdentityListener.on("deleted", this.#onIdentityDeleted);
47+
this.#contextualIdentityListener.startListening();
48+
}
49+
50+
destroy() {
51+
this.#contextualIdentityListener.off("created", this.#onIdentityCreated);
52+
this.#contextualIdentityListener.off("deleted", this.#onIdentityDeleted);
53+
this.#contextualIdentityListener.destroy();
54+
55+
this.#userContextIds = null;
56+
}
57+
58+
/**
59+
* Creates a new user context.
60+
*
61+
* @param {string} prefix
62+
* The prefix to use for the name of the user context.
63+
*
64+
* @returns {string}
65+
* The user context id of the new user context.
66+
*/
67+
createContext(prefix = "remote") {
68+
// Prepare the opaque id and name beforehand.
69+
const userContextId = lazy.generateUUID();
70+
const name = `${prefix}-${userContextId}`;
71+
72+
// Create the user context.
73+
const identity = lazy.ContextualIdentityService.create(name);
74+
const internalId = identity.userContextId;
75+
76+
// An id has been set already by the contextual-identity-created observer.
77+
// Override it with `userContextId` to match the container name.
78+
this.#userContextIds.set(internalId, userContextId);
79+
80+
return userContextId;
81+
}
82+
83+
/**
84+
* Retrieve the user context id corresponding to the provided internal id.
85+
*
86+
* @param {number} internalId
87+
* The internal user context id.
88+
*
89+
* @returns {string|null}
90+
* The corresponding user context id or null if the user context does not
91+
* exist.
92+
*/
93+
getIdByInternalId(internalId) {
94+
if (this.#userContextIds.has(internalId)) {
95+
return this.#userContextIds.get(internalId);
96+
}
97+
return null;
98+
}
99+
100+
/**
101+
* Retrieve the internal id corresponding to the provided user
102+
* context id.
103+
*
104+
* @param {string} userContextId
105+
* The user context id.
106+
*
107+
* @returns {number|null}
108+
* The internal user context id or null if the user context does not
109+
* exist.
110+
*/
111+
getInternalIdById(userContextId) {
112+
for (const [internalId, id] of this.#userContextIds) {
113+
if (userContextId == id) {
114+
return internalId;
115+
}
116+
}
117+
return null;
118+
}
119+
120+
/**
121+
* Returns an array of all known user context ids.
122+
*
123+
* @returns {Array<string>}
124+
* The array of user context ids.
125+
*/
126+
getUserContextIds() {
127+
return Array.from(this.#userContextIds.values());
128+
}
129+
130+
/**
131+
* Checks if the provided user context id is known by this UserContextManager.
132+
*
133+
* @param {string} userContextId
134+
* The id of the user context to check.
135+
*/
136+
hasUserContextId(userContextId) {
137+
return this.getUserContextIds().includes(userContextId);
138+
}
139+
140+
/**
141+
* Removes a user context and closes all related container tabs.
142+
*
143+
* @param {string} userContextId
144+
* The id of the user context to remove.
145+
* @param {object=} options
146+
* @param {boolean=} options.closeContextTabs
147+
* Pass true if the tabs owned by the user context should also be closed.
148+
* Defaults to false.
149+
*/
150+
removeUserContext(userContextId, options = {}) {
151+
const { closeContextTabs = false } = options;
152+
153+
if (!this.hasUserContextId(userContextId)) {
154+
return;
155+
}
156+
157+
const internalId = this.getInternalIdById(userContextId);
158+
if (closeContextTabs) {
159+
lazy.ContextualIdentityService.closeContainerTabs(internalId);
160+
}
161+
lazy.ContextualIdentityService.remove(internalId);
162+
}
163+
164+
#onIdentityCreated = (eventName, data) => {
165+
this.#registerIdentity(data.identity);
166+
};
167+
168+
#onIdentityDeleted = (eventName, data) => {
169+
this.#userContextIds.delete(data.identity.userContextId);
170+
};
171+
172+
#registerIdentity(identity) {
173+
// Note: the id for identities created via UserContextManagerClass.createContext
174+
// are overridden in createContext.
175+
this.#userContextIds.set(identity.userContextId, lazy.generateUUID());
176+
}
177+
}
178+
179+
// Expose a shared singleton.
180+
export const UserContextManager = new UserContextManagerClass();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
const lazy = {};
6+
7+
ChromeUtils.defineESModuleGetters(lazy, {
8+
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
9+
});
10+
11+
const OBSERVER_TOPIC_CREATED = "contextual-identity-created";
12+
const OBSERVER_TOPIC_DELETED = "contextual-identity-deleted";
13+
14+
/**
15+
* The ContextualIdentityListener can be used to listen for notifications about
16+
* contextual identities (containers) being created or deleted.
17+
*
18+
* Example:
19+
* ```
20+
* const listener = new ContextualIdentityListener();
21+
* listener.on("created", onCreated);
22+
* listener.startListening();
23+
*
24+
* const onCreated = (eventName, data = {}) => {
25+
* const { identity } = data;
26+
* ...
27+
* };
28+
* ```
29+
*
30+
* @fires message
31+
* The ContextualIdentityListener emits "created" and "deleted" events,
32+
* with the following object as payload:
33+
* - {object} identity
34+
* The contextual identity which was created or deleted.
35+
*/
36+
export class ContextualIdentityListener {
37+
#listening;
38+
39+
/**
40+
* Create a new BrowsingContextListener instance.
41+
*/
42+
constructor() {
43+
lazy.EventEmitter.decorate(this);
44+
45+
this.#listening = false;
46+
}
47+
48+
destroy() {
49+
this.stopListening();
50+
}
51+
52+
observe(subject, topic, data) {
53+
switch (topic) {
54+
case OBSERVER_TOPIC_CREATED:
55+
this.emit("created", { identity: subject.wrappedJSObject });
56+
break;
57+
58+
case OBSERVER_TOPIC_DELETED:
59+
this.emit("deleted", { identity: subject.wrappedJSObject });
60+
break;
61+
}
62+
}
63+
64+
startListening() {
65+
if (this.#listening) {
66+
return;
67+
}
68+
69+
Services.obs.addObserver(this, OBSERVER_TOPIC_CREATED);
70+
Services.obs.addObserver(this, OBSERVER_TOPIC_DELETED);
71+
72+
this.#listening = true;
73+
}
74+
75+
stopListening() {
76+
if (!this.#listening) {
77+
return;
78+
}
79+
80+
Services.obs.removeObserver(this, OBSERVER_TOPIC_CREATED);
81+
Services.obs.removeObserver(this, OBSERVER_TOPIC_DELETED);
82+
83+
this.#listening = false;
84+
}
85+
}

‎remote/shared/listeners/test/browser/browser.toml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ prefs = ["remote.messagehandler.modulecache.useBrowserTestRoot=true"]
1414

1515
["browser_ConsoleListener_cached_messages.js"]
1616

17+
["browser_ContextualIdentityListener.js"]
18+
1719
["browser_NetworkListener.js"]
1820

1921
["browser_PromptListener.js"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
* You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
const { ContextualIdentityListener } = ChromeUtils.importESModule(
6+
"chrome://remote/content/shared/listeners/ContextualIdentityListener.sys.mjs"
7+
);
8+
9+
add_task(async function test_createdOnNewContextualIdentity() {
10+
const listener = new ContextualIdentityListener();
11+
const created = listener.once("created");
12+
13+
listener.startListening();
14+
15+
ContextualIdentityService.create("test_name");
16+
17+
const { identity } = await created;
18+
is(identity.name, "test_name", "Received expected identity");
19+
20+
listener.stopListening();
21+
22+
ContextualIdentityService.remove(identity.userContextId);
23+
});
24+
25+
add_task(async function test_deletedOnRemovedContextualIdentity() {
26+
const listener = new ContextualIdentityListener();
27+
const deleted = listener.once("deleted");
28+
29+
listener.startListening();
30+
31+
const testIdentity = ContextualIdentityService.create("test_name");
32+
ContextualIdentityService.remove(testIdentity.userContextId);
33+
34+
const { identity } = await deleted;
35+
is(identity.name, "test_name", "Received expected identity");
36+
37+
listener.stopListening();
38+
});

‎remote/shared/test/browser/browser.toml

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ support-files = ["head.js"]
1212
["browser_NavigationManager_notify.js"]
1313

1414
["browser_TabManager.js"]
15+
16+
["browser_UserContextManager.js"]

0 commit comments

Comments
 (0)