mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
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 {GeolocationOptions, MediaFeature} from '../api/Page.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
|
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
|
|
||||||
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
|
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
|
||||||
import {Viewport} from './PuppeteerViewport.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
|
* @internal
|
||||||
@ -31,7 +45,10 @@ export class EmulationManager {
|
|||||||
#hasTouch = false;
|
#hasTouch = false;
|
||||||
#javascriptEnabled = true;
|
#javascriptEnabled = true;
|
||||||
|
|
||||||
#viewport?: Viewport;
|
#viewportState: ViewportState = {};
|
||||||
|
#idleOverridesState: IdleOverridesState = {
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
#secondaryClients = new Set<CDPSession>();
|
#secondaryClients = new Set<CDPSession>();
|
||||||
|
|
||||||
constructor(client: CDPSession) {
|
constructor(client: CDPSession) {
|
||||||
@ -45,10 +62,11 @@ export class EmulationManager {
|
|||||||
|
|
||||||
async registerSpeculativeSession(client: CDPSession): Promise<void> {
|
async registerSpeculativeSession(client: CDPSession): Promise<void> {
|
||||||
this.#secondaryClients.add(client);
|
this.#secondaryClients.add(client);
|
||||||
await this.#applyViewport(client);
|
|
||||||
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
||||||
return this.#secondaryClients.delete(client);
|
return this.#secondaryClients.delete(client);
|
||||||
});
|
});
|
||||||
|
void this.#syncViewport().catch(debugError);
|
||||||
|
void this.#syncIdleState().catch(debugError);
|
||||||
}
|
}
|
||||||
|
|
||||||
get javascriptEnabled(): boolean {
|
get javascriptEnabled(): boolean {
|
||||||
@ -56,9 +74,11 @@ export class EmulationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
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 mobile = viewport.isMobile || false;
|
||||||
const hasTouch = viewport.hasTouch || false;
|
const hasTouch = viewport.hasTouch || false;
|
||||||
@ -67,22 +87,27 @@ export class EmulationManager {
|
|||||||
this.#emulatingMobile = mobile;
|
this.#emulatingMobile = mobile;
|
||||||
this.#hasTouch = hasTouch;
|
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;
|
return reloadNeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #applyViewport(client: CDPSession): Promise<void> {
|
async #syncViewport() {
|
||||||
const viewport = this.#viewport;
|
await Promise.all([
|
||||||
if (!viewport) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const {viewport} = viewportState;
|
||||||
const mobile = viewport.isMobile || false;
|
const mobile = viewport.isMobile || false;
|
||||||
const width = viewport.width;
|
const width = viewport.width;
|
||||||
const height = viewport.height;
|
const height = viewport.height;
|
||||||
@ -111,13 +136,37 @@ export class EmulationManager {
|
|||||||
isUserActive: boolean;
|
isUserActive: boolean;
|
||||||
isScreenUnlocked: boolean;
|
isScreenUnlocked: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (overrides) {
|
this.#idleOverridesState = {
|
||||||
await this.#client.send('Emulation.setIdleOverride', {
|
active: true,
|
||||||
isUserActive: overrides.isUserActive,
|
overrides,
|
||||||
isScreenUnlocked: overrides.isScreenUnlocked,
|
};
|
||||||
|
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 {
|
} 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