refactor: sync emulation state with secondary clients (#10849)
This commit is contained in:
parent
e92e4b2c82
commit
72175906a5
@ -17,10 +17,24 @@ import {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {GeolocationOptions, MediaFeature} from '../api/Page.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
|
||||
import {Viewport} from './PuppeteerViewport.js';
|
||||
import {debugError} from './util.js';
|
||||
|
||||
interface ViewportState {
|
||||
viewport?: Viewport;
|
||||
}
|
||||
|
||||
interface IdleOverridesState {
|
||||
overrides?: {
|
||||
isUserActive: boolean;
|
||||
isScreenUnlocked: boolean;
|
||||
};
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -31,7 +45,10 @@ export class EmulationManager {
|
||||
#hasTouch = false;
|
||||
#javascriptEnabled = true;
|
||||
|
||||
#viewport?: Viewport;
|
||||
#viewportState: ViewportState = {};
|
||||
#idleOverridesState: IdleOverridesState = {
|
||||
active: false,
|
||||
};
|
||||
#secondaryClients = new Set<CDPSession>();
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
@ -45,10 +62,11 @@ export class EmulationManager {
|
||||
|
||||
async registerSpeculativeSession(client: CDPSession): Promise<void> {
|
||||
this.#secondaryClients.add(client);
|
||||
await this.#applyViewport(client);
|
||||
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
||||
return this.#secondaryClients.delete(client);
|
||||
});
|
||||
void this.#syncViewport().catch(debugError);
|
||||
void this.#syncIdleState().catch(debugError);
|
||||
}
|
||||
|
||||
get javascriptEnabled(): boolean {
|
||||
@ -56,9 +74,11 @@ export class EmulationManager {
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
||||
this.#viewport = viewport;
|
||||
this.#viewportState = {
|
||||
viewport,
|
||||
};
|
||||
|
||||
await this.#applyViewport(this.#client);
|
||||
await this.#syncViewport();
|
||||
|
||||
const mobile = viewport.isMobile || false;
|
||||
const hasTouch = viewport.hasTouch || false;
|
||||
@ -67,22 +87,27 @@ export class EmulationManager {
|
||||
this.#emulatingMobile = mobile;
|
||||
this.#hasTouch = hasTouch;
|
||||
|
||||
if (!reloadNeeded) {
|
||||
// If the page will be reloaded, no need to adjust secondary clients.
|
||||
await Promise.all(
|
||||
Array.from(this.#secondaryClients).map(client => {
|
||||
return this.#applyViewport(client);
|
||||
})
|
||||
);
|
||||
}
|
||||
return reloadNeeded;
|
||||
}
|
||||
|
||||
async #applyViewport(client: CDPSession): Promise<void> {
|
||||
const viewport = this.#viewport;
|
||||
if (!viewport) {
|
||||
async #syncViewport() {
|
||||
await Promise.all([
|
||||
this.#applyViewport(this.#client, this.#viewportState),
|
||||
...Array.from(this.#secondaryClients).map(client => {
|
||||
return this.#applyViewport(client, this.#viewportState);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #applyViewport(
|
||||
client: CDPSession,
|
||||
viewportState: ViewportState
|
||||
): Promise<void> {
|
||||
if (!viewportState.viewport) {
|
||||
return;
|
||||
}
|
||||
const {viewport} = viewportState;
|
||||
const mobile = viewport.isMobile || false;
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
@ -111,13 +136,37 @@ export class EmulationManager {
|
||||
isUserActive: boolean;
|
||||
isScreenUnlocked: boolean;
|
||||
}): Promise<void> {
|
||||
if (overrides) {
|
||||
await this.#client.send('Emulation.setIdleOverride', {
|
||||
isUserActive: overrides.isUserActive,
|
||||
isScreenUnlocked: overrides.isScreenUnlocked,
|
||||
this.#idleOverridesState = {
|
||||
active: true,
|
||||
overrides,
|
||||
};
|
||||
await this.#syncIdleState();
|
||||
}
|
||||
|
||||
async #syncIdleState() {
|
||||
await Promise.all([
|
||||
this.#emulateIdleState(this.#client, this.#idleOverridesState),
|
||||
...Array.from(this.#secondaryClients).map(client => {
|
||||
return this.#emulateIdleState(client, this.#idleOverridesState);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateIdleState(
|
||||
client: CDPSession,
|
||||
idleStateState: IdleOverridesState
|
||||
): Promise<void> {
|
||||
if (!idleStateState.active) {
|
||||
return;
|
||||
}
|
||||
if (idleStateState.overrides) {
|
||||
await client.send('Emulation.setIdleOverride', {
|
||||
isUserActive: idleStateState.overrides.isUserActive,
|
||||
isScreenUnlocked: idleStateState.overrides.isScreenUnlocked,
|
||||
});
|
||||
} else {
|
||||
await this.#client.send('Emulation.clearIdleOverride');
|
||||
await client.send('Emulation.clearIdleOverride');
|
||||
}
|
||||
}
|
||||
|
||||
|
89
packages/puppeteer-core/src/util/decorators.test.ts
Normal file
89
packages/puppeteer-core/src/util/decorators.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright 2023 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import expect from 'expect';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {invokeAtMostOnceForArguments} from './decorators.js';
|
||||
|
||||
describe('decorators', function () {
|
||||
describe('invokeAtMostOnceForArguments', () => {
|
||||
it('should delegate calls', () => {
|
||||
const spy = sinon.spy();
|
||||
class Test {
|
||||
@invokeAtMostOnceForArguments
|
||||
test(obj1: object, obj2: object) {
|
||||
spy(obj1, obj2);
|
||||
}
|
||||
}
|
||||
const t = new Test();
|
||||
expect(spy.callCount).toBe(0);
|
||||
const obj1 = {};
|
||||
const obj2 = {};
|
||||
t.test(obj1, obj2);
|
||||
expect(spy.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should prevent repeated calls', () => {
|
||||
const spy = sinon.spy();
|
||||
class Test {
|
||||
@invokeAtMostOnceForArguments
|
||||
test(obj1: object, obj2: object) {
|
||||
spy(obj1, obj2);
|
||||
}
|
||||
}
|
||||
const t = new Test();
|
||||
expect(spy.callCount).toBe(0);
|
||||
const obj1 = {};
|
||||
const obj2 = {};
|
||||
t.test(obj1, obj2);
|
||||
expect(spy.callCount).toBe(1);
|
||||
expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy();
|
||||
t.test(obj1, obj2);
|
||||
expect(spy.callCount).toBe(1);
|
||||
expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy();
|
||||
const obj3 = {};
|
||||
t.test(obj1, obj3);
|
||||
expect(spy.callCount).toBe(2);
|
||||
expect(spy.lastCall.calledWith(obj1, obj3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should throw an error for dynamic argumetns', () => {
|
||||
class Test {
|
||||
@invokeAtMostOnceForArguments
|
||||
test(..._args: unknown[]) {}
|
||||
}
|
||||
const t = new Test();
|
||||
t.test({});
|
||||
expect(() => {
|
||||
t.test({}, {});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error for non object arguments', () => {
|
||||
class Test {
|
||||
@invokeAtMostOnceForArguments
|
||||
test(..._args: unknown[]) {}
|
||||
}
|
||||
const t = new Test();
|
||||
expect(() => {
|
||||
t.test(1);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
@ -72,3 +72,44 @@ export function throwIfDisposed<This extends Disposed>(
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The decorator only invokes the target if the target has not been invoked with
|
||||
* the same arguments before. The decorated method throws an error if it's
|
||||
* invoked with a different number of elements: if you decorate a method, it
|
||||
* should have the same number of arguments
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function invokeAtMostOnceForArguments(
|
||||
target: (this: unknown, ...args: any[]) => any,
|
||||
_: unknown
|
||||
): typeof target {
|
||||
const cache = new WeakMap();
|
||||
let cacheDepth = -1;
|
||||
return function (this: unknown, ...args: unknown[]) {
|
||||
if (cacheDepth === -1) {
|
||||
cacheDepth = args.length;
|
||||
}
|
||||
if (cacheDepth !== args.length) {
|
||||
throw new Error(
|
||||
'Memoized method was called with the wrong number of arguments'
|
||||
);
|
||||
}
|
||||
let freshArguments = false;
|
||||
let cacheIterator = cache;
|
||||
for (const arg of args) {
|
||||
if (cacheIterator.has(arg as object)) {
|
||||
cacheIterator = cacheIterator.get(arg as object)!;
|
||||
} else {
|
||||
freshArguments = true;
|
||||
cacheIterator.set(arg as object, new WeakMap());
|
||||
cacheIterator = cacheIterator.get(arg as object)!;
|
||||
}
|
||||
}
|
||||
if (!freshArguments) {
|
||||
return;
|
||||
}
|
||||
return target.call(this, ...args);
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user