Skip to content

Commit

Permalink
feat: send browsingContext.contextCreated event while subscribing (#…
Browse files Browse the repository at this point in the history
…2255)

Subscribing to `browsingContext.contextCreated` should emit events for
the exiting contexts.

This required to add subscribe hooks to event manager. Hooks are engaged
when the subscription is done for the required method.

Spec:
https://w3c.github.io/webdriver-bidi/#ref-for-event-remote-end-subscribe-steps%E2%91%A1

Addressing WPT test
[**webdriver/tests/bidi/browsing_context/context_created/context_created.py:test_existing_context**](https://wpt.fyi/results/webdriver/tests/bidi/browsing_context/context_created/context_created.py?q=label%3Achromium-bidi-2023&run_id=6271852771278848&run_id=5105860015816704).

---------

Signed-off-by: Browser Automation Bot <browser-automation-bot@google.com>
Co-authored-by: Browser Automation Bot <browser-automation-bot@google.com>
  • Loading branch information
sadym-chromium and browser-automation-bot authored May 31, 2024
1 parent 4c3ce87 commit 592c839
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 47 deletions.
3 changes: 2 additions & 1 deletion src/bidiMapper/CommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
this.#browserProcessor = new BrowserProcessor(browserCdpClient);
this.#browsingContextProcessor = new BrowsingContextProcessor(
browserCdpClient,
browsingContextStorage
browsingContextStorage,
eventManager
);
this.#cdpProcessor = new CdpProcessor(
browsingContextStorage,
Expand Down
32 changes: 31 additions & 1 deletion src/bidiMapper/modules/context/BrowsingContextProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,35 @@ import type {Protocol} from 'devtools-protocol';
import type {CdpClient} from '../../../cdp/CdpClient.js';
import {
BrowsingContext,
ChromiumBidi,
InvalidArgumentException,
type EmptyResult,
NoSuchUserContextException,
NoSuchAlertException,
} from '../../../protocol/protocol.js';
import {CdpErrorConstants} from '../../../utils/CdpErrorConstants.js';
import type {EventManager} from '../session/EventManager.js';

import type {BrowsingContextImpl} from './BrowsingContextImpl.js';
import type {BrowsingContextStorage} from './BrowsingContextStorage.js';

export class BrowsingContextProcessor {
readonly #browserCdpClient: CdpClient;
readonly #browsingContextStorage: BrowsingContextStorage;
readonly #eventManager: EventManager;

constructor(
browserCdpClient: CdpClient,
browsingContextStorage: BrowsingContextStorage
browsingContextStorage: BrowsingContextStorage,
eventManager: EventManager
) {
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
this.#eventManager.addSubscribeHook(
ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
this.#onContextCreatedSubscribeHook.bind(this)
);
}

getTree(
Expand Down Expand Up @@ -285,4 +294,25 @@ export class BrowsingContextProcessor {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.locateNodes(params);
}

#onContextCreatedSubscribeHook(
contextId: BrowsingContext.BrowsingContext
): Promise<void> {
const context = this.#browsingContextStorage.getContext(contextId);
const contextsToReport = [
context,
...this.#browsingContextStorage.getContext(contextId).allChildren,
];
contextsToReport.forEach((context) => {
this.#eventManager.registerEvent(
{
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
params: context.serializeToBidiValue(),
},
context.id
);
});
return Promise.resolve();
}
}
42 changes: 41 additions & 1 deletion src/bidiMapper/modules/session/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../../../protocol/protocol.js';
import {Buffer} from '../../../utils/Buffer.js';
import {DefaultMap} from '../../../utils/DefaultMap.js';
import {distinctValues} from '../../../utils/DistinctValues.js';
import {EventEmitter} from '../../../utils/EventEmitter.js';
import {IdWrapper} from '../../../utils/IdWrapper.js';
import type {Result} from '../../../utils/result.js';
Expand Down Expand Up @@ -74,6 +75,14 @@ const eventBufferLength: ReadonlyMap<ChromiumBidi.EventNames, number> = new Map(
[[ChromiumBidi.Log.EventNames.LogEntryAdded, 100]]
);

/**
* Subscription item is a pair of event name and context id.
*/
export type SubscriptionItem = {
contextId: BrowsingContext.BrowsingContext;
event: ChromiumBidi.EventNames;
};

export class EventManager extends EventEmitter<EventManagerEventsMap> {
/**
* Maps event name to a set of contexts where this event already happened.
Expand All @@ -97,11 +106,19 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
#lastMessageSent = new Map<string, number>();
#subscriptionManager: SubscriptionManager;
#browsingContextStorage: BrowsingContextStorage;
/**
* Map of event name to hooks to be called when client is subscribed to the event.
*/
#subscribeHooks: DefaultMap<
ChromiumBidi.EventNames,
((contextId: BrowsingContext.BrowsingContext) => void)[]
>;

constructor(browsingContextStorage: BrowsingContextStorage) {
super();
this.#browsingContextStorage = browsingContextStorage;
this.#subscriptionManager = new SubscriptionManager(browsingContextStorage);
this.#subscribeHooks = new DefaultMap(() => []);
}

get subscriptionManager(): SubscriptionManager {
Expand All @@ -119,6 +136,13 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
return JSON.stringify({eventName, browsingContext, channel});
}

addSubscribeHook(
event: ChromiumBidi.EventNames,
hook: (contextId: BrowsingContext.BrowsingContext) => Promise<void>
): void {
this.#subscribeHooks.get(event).push(hook);
}

registerEvent(
event: ChromiumBidi.Event,
contextId: BrowsingContext.BrowsingContext | null
Expand Down Expand Up @@ -172,9 +196,17 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
}
}

// List of the subscription items that were actually added. Each contains a specific
// event and context. No domain event (like "network") or global context subscription
// (like null) are included.
const addedSubscriptionItems: SubscriptionItem[] = [];

for (const eventName of eventNames) {
for (const contextId of contextIds) {
this.#subscriptionManager.subscribe(eventName, contextId, channel);
addedSubscriptionItems.push(
...this.#subscriptionManager.subscribe(eventName, contextId, channel)
);

for (const eventWrapper of this.#getBufferedEvents(
eventName,
contextId,
Expand All @@ -193,6 +225,14 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
}
}

// Iterate over all new subscription items and call hooks if any. There can be
// duplicates, e.g. when subscribing to the whole domain and some specific event in
// the same time ("network", "network.responseCompleted"). `distinctValues` guarantees
// that hooks are called only once per pair event + context.
distinctValues(addedSubscriptionItems).forEach(({contextId, event}) => {
this.#subscribeHooks.get(event).forEach((hook) => hook(contextId));
});

await this.toggleModulesIfNeeded();
}

Expand Down
39 changes: 39 additions & 0 deletions src/bidiMapper/modules/session/SubscriptionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,48 @@ describe('SubscriptionManager', () => {
return null;
});

browsingContextStorage.getTopLevelContexts = sinon.stub().callsFake(() => {
return [{id: SOME_CONTEXT}, {id: ANOTHER_CONTEXT}];
});

subscriptionManager = new SubscriptionManager(browsingContextStorage);
});

describe('subscribe should return list of added subscriptions', () => {
describe('specific context', () => {
it('new subscription', () => {
expect(
subscriptionManager.subscribe(SOME_EVENT, SOME_CONTEXT, SOME_CHANNEL)
).to.deep.equal([{event: SOME_EVENT, contextId: SOME_CONTEXT}]);
});

it('existing subscription', () => {
subscriptionManager.subscribe(SOME_EVENT, SOME_CONTEXT, SOME_CHANNEL);
expect(
subscriptionManager.subscribe(SOME_EVENT, SOME_CONTEXT, SOME_CHANNEL)
).to.deep.equal([]);
});
});

describe('global', () => {
it('new subscription', () => {
expect(
subscriptionManager.subscribe(SOME_EVENT, null, SOME_CHANNEL)
).to.deep.equal([
{event: SOME_EVENT, contextId: SOME_CONTEXT},
{event: SOME_EVENT, contextId: ANOTHER_CONTEXT},
]);
});

it('existing subscription', () => {
subscriptionManager.subscribe(SOME_EVENT, SOME_CONTEXT, SOME_CHANNEL);
expect(
subscriptionManager.subscribe(SOME_EVENT, null, SOME_CHANNEL)
).to.deep.equal([{event: SOME_EVENT, contextId: ANOTHER_CONTEXT}]);
});
});
});

it('should subscribe twice to global and specific event in proper order', () => {
subscriptionManager.subscribe(SOME_EVENT, null, SOME_CHANNEL);
subscriptionManager.subscribe(SOME_EVENT, null, ANOTHER_CHANNEL);
Expand Down
71 changes: 49 additions & 22 deletions src/bidiMapper/modules/session/SubscriptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@

import type {BidiPlusChannel} from '../../../protocol/chromium-bidi.js';
import {
type BrowsingContext,
ChromiumBidi,
InvalidArgumentException,
type BrowsingContext,
} from '../../../protocol/protocol.js';
import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js';

import type {SubscriptionItem} from './EventManager.js';
import {isCdpEvent} from './events.js';

/**
Expand Down Expand Up @@ -204,36 +205,50 @@ export class SubscriptionManager {
return false;
}

/**
* Subscribes to event in the given context and channel.
* @param {EventNames} event
* @param {BrowsingContext.BrowsingContext | null} contextId
* @param {BidiPlusChannel} channel
* @return {SubscriptionItem[]} List of
* subscriptions. If the event is a whole module, it will return all the specific
* events. If the contextId is null, it will return all the top-level contexts which were
* not subscribed before the command.
*/
subscribe(
event: ChromiumBidi.EventNames,
contextId: BrowsingContext.BrowsingContext | null,
channel: BidiPlusChannel
): void {
): SubscriptionItem[] {
// All the subscriptions are handled on the top-level contexts.
contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);

// Check if subscribed event is a whole module
switch (event) {
case ChromiumBidi.BiDiModule.BrowsingContext:
Object.values(ChromiumBidi.BrowsingContext.EventNames).map(
(specificEvent) => this.subscribe(specificEvent, contextId, channel)
);
return;
return Object.values(ChromiumBidi.BrowsingContext.EventNames)
.map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
)
.flat();
case ChromiumBidi.BiDiModule.Log:
Object.values(ChromiumBidi.Log.EventNames).map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
);
return;
return Object.values(ChromiumBidi.Log.EventNames)
.map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
)
.flat();
case ChromiumBidi.BiDiModule.Network:
Object.values(ChromiumBidi.Network.EventNames).map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
);
return;
return Object.values(ChromiumBidi.Network.EventNames)
.map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
)
.flat();
case ChromiumBidi.BiDiModule.Script:
Object.values(ChromiumBidi.Script.EventNames).map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
);
return;
return Object.values(ChromiumBidi.Script.EventNames)
.map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
)
.flat();
default:
// Intentionally left empty.
}
Expand All @@ -248,12 +263,24 @@ export class SubscriptionManager {
}
const eventMap = contextToEventMap.get(contextId)!;

// Do not re-subscribe to events to keep the priority.
if (eventMap.has(event)) {
return;
const affectedContextIds = (
contextId === null
? this.#browsingContextStorage.getTopLevelContexts().map((c) => c.id)
: [contextId]
)
// There can be contexts that are already subscribed to the event. Do not include
// them to the output.
.filter((contextId) => !this.isSubscribedTo(event, contextId));

if (!eventMap.has(event)) {
// Add subscription only if it's not already subscribed.
eventMap.set(event, this.#subscriptionPriority++);
}

eventMap.set(event, this.#subscriptionPriority++);
return affectedContextIds.map((contextId) => ({
event,
contextId,
}));
}

/**
Expand Down
Loading

0 comments on commit 592c839

Please sign in to comment.