Skip to content

Commit c616413

Browse files
authored
Fix viewport settings not persisting when opening new tabs (#2759)
Fixes #2733 ## Problem When setViewPort() was called and then a new tab was opened with openTab(), the viewport settings were not preserved in the new tab. The viewport would reset to the browser's default resolution, breaking automated visual testing tools that rely on consistent viewport dimensions. ## Solution This PR introduces comprehensive viewport state management: 1. **Viewport persistence across sessions** - Store and automatically reapply viewport settings when opening new tabs 2. **Race condition fix** - Added `handlerActingOnNewSession` synchronization to ensure viewport is fully applied before continuing (fixes Windows CI flakiness) 3. **State cleanup** - Reset viewport settings on browser close to prevent leakage between test runs 4. **Test coverage** - Added unit tests for viewport persistence and proper mocking ## Changes - `lib/handlers/emulationHandler.js`: Store/reapply viewport settings with synchronization, add `resetViewportSettings()` - `lib/taiko.js`: Call `emulationHandler.resetViewportSettings()` on browser close - `test/unit-tests/handlers/emulationHandler.test.js`: Add tests for viewport persistence and state reset. --------- Signed-off-by: winst0niuss <chumachenko.vadym@gmail.com>
1 parent c5942bd commit c616413

File tree

4 files changed

+141
-8
lines changed

4 files changed

+141
-8
lines changed

lib/handlers/emulationHandler.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
const { eventHandler } = require("../eventBus");
22
const networkHandler = require("./networkHandler");
33
let emulation;
4+
let currentViewportSettings = null;
5+
6+
const createdSessionListener = async (client) => {
7+
let resolve;
8+
eventHandler.emit(
9+
"handlerActingOnNewSession",
10+
new Promise((r) => {
11+
resolve = r;
12+
}),
13+
);
414

5-
const createdSessionListener = (client) => {
615
emulation = client.Emulation;
16+
17+
// Reapply viewport settings if they were previously set
18+
if (currentViewportSettings) {
19+
await applyViewportSettings(currentViewportSettings);
20+
}
21+
22+
resolve();
723
};
824
eventHandler.on("createdSession", createdSessionListener);
925

26+
const applyViewportSettings = async (options) => {
27+
const hasTouch = options.hasTouch || false;
28+
await Promise.all([
29+
emulation.setDeviceMetricsOverride(options),
30+
emulation.setTouchEmulationEnabled({ enabled: hasTouch }),
31+
]);
32+
};
33+
1034
const setViewport = async (options) => {
1135
if (options.height === undefined || options.width === undefined) {
1236
throw new Error("No height and width provided");
@@ -17,11 +41,11 @@ const setViewport = async (options) => {
1741
options.screenOrientation || options.isLandscape
1842
? { angle: 90, type: "landscapePrimary" }
1943
: { angle: 0, type: "portraitPrimary" };
20-
const hasTouch = options.hasTouch || false;
21-
await Promise.all([
22-
emulation.setDeviceMetricsOverride(options),
23-
emulation.setTouchEmulationEnabled({ enabled: hasTouch }),
24-
]);
44+
45+
// Store viewport settings for reapplication when new tabs are created
46+
currentViewportSettings = options;
47+
48+
await applyViewportSettings(options);
2549
};
2650

2751
const setTimeZone = async (timezone) => {
@@ -74,4 +98,14 @@ const emulateDevice = async (deviceModel) => {
7498
]);
7599
};
76100

77-
module.exports = { setLocation, setViewport, setTimeZone, emulateDevice };
101+
const resetViewportSettings = () => {
102+
currentViewportSettings = null;
103+
};
104+
105+
module.exports = {
106+
setLocation,
107+
setViewport,
108+
setTimeZone,
109+
emulateDevice,
110+
resetViewportSettings,
111+
};

lib/taiko.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ module.exports.closeBrowser = async () => {
147147

148148
const _closeBrowser = async () => {
149149
fetchHandler.resetInterceptors();
150+
emulationHandler.resetViewportSettings();
150151
await closeConnection(promisesToBeResolvedBeforeCloseBrowser);
151152
};
152153

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "http://json.schemastore.org/package",
33
"name": "taiko",
4-
"version": "1.4.5",
4+
"version": "1.4.6",
55
"description": "Taiko is a Node.js library for automating Chromium based browsers",
66
"main": "bin/taiko.js",
77
"bin": {

test/unit-tests/handlers/emulationHandler.test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,102 @@ describe("emulationHandler", () => {
107107
deviceScaleFactor: 1,
108108
});
109109
});
110+
111+
it(".setViewport should reapply viewport settings when createdSession event is emitted", async () => {
112+
// Set viewport first
113+
await emulationHandler.setViewport({ height: 720, width: 1280 });
114+
115+
// Reset calledWith to verify reapplication
116+
calledWith = {};
117+
calledWithTouch = {};
118+
119+
// Create a new mock emulation for the new session
120+
const newCalledWith = {};
121+
const newCalledWithTouch = {};
122+
const newMockEmulation = {
123+
setDeviceMetricsOverride: async (param) => {
124+
Object.assign(newCalledWith, param);
125+
},
126+
setTouchEmulationEnabled: async (param) => {
127+
Object.assign(newCalledWithTouch, param);
128+
},
129+
};
130+
131+
// Mock Network to prevent errors from networkHandler
132+
const newMockNetwork = {
133+
requestWillBeSent: () => {},
134+
loadingFinished: () => {},
135+
loadingFailed: () => {},
136+
responseReceived: () => {},
137+
setCacheDisabled: async () => {},
138+
};
139+
140+
const mockClient = {
141+
Emulation: newMockEmulation,
142+
Network: newMockNetwork,
143+
};
144+
145+
// Emit createdSession event (simulating opening a new tab)
146+
const eventHandler = emulationHandler.__get__("eventHandler");
147+
await eventHandler.emit("createdSession", mockClient);
148+
149+
// Wait for async event handler to complete
150+
await new Promise((resolve) => setTimeout(resolve, 50));
151+
152+
// Verify viewport settings were reapplied
153+
expect(newCalledWith).to.be.eql({
154+
height: 720,
155+
width: 1280,
156+
mobile: false,
157+
screenOrientation: { angle: 0, type: "portraitPrimary" },
158+
deviceScaleFactor: 1,
159+
});
160+
expect(newCalledWithTouch).to.be.eql({ enabled: false });
161+
});
162+
163+
it("should not reapply viewport if setViewport was never called", async () => {
164+
// Remove old listener before reloading
165+
const oldEventHandler = emulationHandler.__get__("eventHandler");
166+
const oldListener = emulationHandler.__get__("createdSessionListener");
167+
oldEventHandler.removeListener("createdSession", oldListener);
168+
169+
// Reload the module to reset state
170+
emulationHandler = rewire("../../../lib/handlers/emulationHandler");
171+
172+
const newCalledWith = {};
173+
const newCalledWithTouch = {};
174+
const newMockEmulation = {
175+
setDeviceMetricsOverride: async (param) => {
176+
Object.assign(newCalledWith, param);
177+
},
178+
setTouchEmulationEnabled: async (param) => {
179+
Object.assign(newCalledWithTouch, param);
180+
},
181+
};
182+
183+
// Mock Network to prevent errors from networkHandler
184+
const newMockNetwork = {
185+
requestWillBeSent: () => {},
186+
loadingFinished: () => {},
187+
loadingFailed: () => {},
188+
responseReceived: () => {},
189+
setCacheDisabled: async () => {},
190+
};
191+
192+
const mockClient = {
193+
Emulation: newMockEmulation,
194+
Network: newMockNetwork,
195+
};
196+
197+
// Emit createdSession without setting viewport first
198+
const eventHandler = emulationHandler.__get__("eventHandler");
199+
await eventHandler.emit("createdSession", mockClient);
200+
201+
// Wait for async event handler to complete
202+
await new Promise((resolve) => setTimeout(resolve, 50));
203+
204+
// Verify viewport settings were NOT applied
205+
expect(Object.keys(newCalledWith).length).to.equal(0);
206+
expect(Object.keys(newCalledWithTouch).length).to.equal(0);
207+
});
110208
});

0 commit comments

Comments
 (0)