/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import type {IncomingHttpHeaders} from 'node:http';
import {beforeEach, describe, it, mock} from 'node:test';
import {emulate} from '../../src/tools/emulation.js';
import {
geolocationTransform,
viewportTransform,
} from '../../src/tools/ToolDefinition.js';
import {serverHooks} from '../server.js';
import {html, withMcpContext} from '../utils.js';
describe('emulation', () => {
const server = serverHooks();
describe('transforms', () => {
describe('viewportTransform', () => {
it('returns undefined for undefined input', () => {
assert.strictEqual(viewportTransform(undefined), undefined);
});
it('parses basic dimensions', () => {
assert.deepStrictEqual(viewportTransform('800x600'), {
width: 800,
height: 600,
deviceScaleFactor: undefined,
isMobile: false,
isLandscape: false,
hasTouch: false,
});
});
it('parses dimensions with devicePixelRatio', () => {
assert.deepStrictEqual(viewportTransform('1024x768x2'), {
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: false,
isLandscape: false,
hasTouch: false,
});
});
it('parses mobile and touch tags', () => {
assert.deepStrictEqual(viewportTransform('375x667x2,mobile,touch'), {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
});
});
it('parses landscape tag', () => {
assert.deepStrictEqual(viewportTransform('1024x768x1,landscape'), {
width: 1024,
height: 768,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: true,
});
});
});
describe('geolocationTransform', () => {
it('returns undefined for undefined input', () => {
assert.strictEqual(geolocationTransform(undefined), undefined);
});
it('parses latitude and longitude', () => {
assert.deepStrictEqual(geolocationTransform('48.137154,11.576124'), {
latitude: 48.137154,
longitude: 11.576124,
});
});
});
});
describe('network', () => {
it('emulates offline network conditions', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
networkConditions: 'Offline',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
'Offline',
);
});
});
it('emulates network throttling when the throttling option is valid', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
networkConditions: 'Slow 3G',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
'Slow 3G',
);
});
});
it('disables network emulation', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
null,
);
});
});
it('does not set throttling when the network throttling is not one of the predefined options', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
networkConditions: 'Slow 11G',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
null,
);
});
});
it('report correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
networkConditions: 'Slow 3G',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
'Slow 3G',
);
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(
context.getSelectedMcpPage().networkConditions,
null,
);
});
});
});
describe('cpu', () => {
it('emulates cpu throttling when the rate is valid (1-20x)', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
cpuThrottlingRate: 4,
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().cpuThrottlingRate, 4);
});
});
it('applies cpu throttling to secondary session', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
const universe = context.getDevToolsUniverse(mcpPage);
assert.ok(universe);
const sendSpy = mock.method(universe.session, 'send');
await emulate.handler(
{
params: {
cpuThrottlingRate: 4,
},
page: mcpPage,
},
response,
context,
);
assert.ok(sendSpy.mock.calls.length > 0);
const cpuCall = sendSpy.mock.calls.find(
call => call.arguments[0] === 'Emulation.setCPUThrottlingRate',
);
assert.ok(cpuCall);
assert.deepStrictEqual(cpuCall.arguments[1], {rate: 4});
sendSpy.mock.restore();
});
});
it('disables cpu throttling', async () => {
await withMcpContext(async (response, context) => {
await context.getSelectedMcpPage().emulate({
cpuThrottlingRate: 4,
});
await emulate.handler(
{
params: {
cpuThrottlingRate: 1,
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().cpuThrottlingRate, 1);
});
});
it('report correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
cpuThrottlingRate: 4,
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().cpuThrottlingRate, 4);
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(context.getSelectedMcpPage().cpuThrottlingRate, 1);
});
});
});
describe('geolocation', () => {
it('emulates geolocation with latitude and longitude', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
geolocation: {
latitude: 48.137154,
longitude: 11.576124,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
const geolocation = context.getSelectedMcpPage().geolocation;
assert.strictEqual(geolocation?.latitude, 48.137154);
assert.strictEqual(geolocation?.longitude, 11.576124);
});
});
it('clears geolocation override when geolocation is set to null', async () => {
await withMcpContext(async (response, context) => {
// First set a geolocation
await emulate.handler(
{
params: {
geolocation: {
latitude: 48.137154,
longitude: 11.576124,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.notStrictEqual(context.getSelectedMcpPage().geolocation, null);
// Then clear it by setting geolocation to null
await emulate.handler(
{
params: {},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().geolocation, null);
});
});
it('reports correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
geolocation: {
latitude: 48.137154,
longitude: 11.576124,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
const geolocation = context.getSelectedMcpPage().geolocation;
assert.strictEqual(geolocation?.latitude, 48.137154);
assert.strictEqual(geolocation?.longitude, 11.576124);
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(context.getSelectedMcpPage().geolocation, null);
});
});
});
describe('viewport', () => {
beforeEach(() => {
server.addHtmlRoute('/viewport', html`Test page`);
});
it('emulates viewport', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
await page.goto(server.baseUrl + '/viewport');
await emulate.handler(
{
params: {
viewport: {
width: 400,
height: 400,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
const viewportData = await page.evaluate(() => {
return {
width: window.innerWidth,
height: window.innerHeight,
deviceScaleFactor: window.devicePixelRatio,
hasTouch: navigator.maxTouchPoints > 0,
};
});
assert.deepStrictEqual(viewportData, {
width: 400,
height: 400,
deviceScaleFactor: 2,
hasTouch: true,
});
});
});
it('clears viewport override when viewport is set to null', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
// First set a viewport
await emulate.handler(
{
params: {
viewport: {
width: 400,
height: 400,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
const viewportData = await page.evaluate(() => {
return {
width: window.innerWidth,
height: window.innerHeight,
};
});
assert.deepStrictEqual(viewportData, {
width: 400,
height: 400,
});
// Then clear it by setting viewport to null
await emulate.handler(
{
params: {},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().viewport, null);
// Somehow reset of the viewport seems to be async.
await context.getSelectedMcpPage().pptrPage.waitForFunction(() => {
return window.innerWidth !== 400 && window.innerHeight !== 400;
});
});
});
it('reports correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
viewport: {
width: 400,
height: 400,
},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.ok(context.getSelectedMcpPage().viewport);
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(context.getSelectedMcpPage().viewport, null);
assert.ok(
await context.getSelectedMcpPage().pptrPage.evaluate(() => {
return window.innerWidth !== 400 && window.innerHeight !== 400;
}),
);
});
});
});
describe('userAgent', () => {
it('emulates userAgent', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
userAgent: 'MyUA',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, 'MyUA');
const page = context.getSelectedMcpPage().pptrPage;
const ua = await page.evaluate(() => navigator.userAgent);
assert.strictEqual(ua, 'MyUA');
});
});
it('updates userAgent', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
userAgent: 'UA1',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, 'UA1');
await emulate.handler(
{
params: {
userAgent: 'UA2',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, 'UA2');
const page = context.getSelectedMcpPage().pptrPage;
const ua = await page.evaluate(() => navigator.userAgent);
assert.strictEqual(ua, 'UA2');
});
});
it('clears userAgent override when userAgent is set to null', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
userAgent: 'MyUA',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, 'MyUA');
await emulate.handler(
{
params: {},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, null);
const page = context.getSelectedMcpPage().pptrPage;
const ua = await page.evaluate(() => navigator.userAgent);
assert.notStrictEqual(ua, 'MyUA');
assert.ok(ua.length > 0);
});
});
it('reports correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
userAgent: 'MyUA',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().userAgent, 'MyUA');
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(context.getSelectedMcpPage().userAgent, null);
assert.ok(
await context.getSelectedMcpPage().pptrPage.evaluate(() => {
return navigator.userAgent !== 'MyUA';
}),
);
});
});
});
describe('extraHttpHeaders', () => {
it('sets extra headers on requests', async () => {
let receivedHeaders: IncomingHttpHeaders = {};
server.addRoute('/headers-test', async (req, res) => {
receivedHeaders = req.headers;
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Headers Test');
});
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
await emulate.handler(
{
params: {
extraHttpHeaders: {'X-Custom-Header': 'test-value'},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
await page.goto(server.getRoute('/headers-test'));
assert.strictEqual(receivedHeaders['x-custom-header'], 'test-value');
});
});
it('clears extra headers when null is passed', async () => {
let receivedHeaders: IncomingHttpHeaders = {};
server.addRoute('/headers-clear', async (req, res) => {
receivedHeaders = req.headers;
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Headers Clear');
});
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
// Set headers first
await emulate.handler(
{
params: {
extraHttpHeaders: {'X-To-Clear': 'value'},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
// Clear headers
await emulate.handler(
{
params: {
extraHttpHeaders: {},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
await page.goto(server.getRoute('/headers-clear'));
assert.strictEqual(receivedHeaders['x-to-clear'], undefined);
assert.strictEqual(
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
undefined,
);
});
});
it('headers persist across navigations', async () => {
const receivedHeaders: IncomingHttpHeaders[] = [];
server.addRoute('/persist-one', async (req, res) => {
receivedHeaders.push({...req.headers});
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Page One');
});
server.addRoute('/persist-two', async (req, res) => {
receivedHeaders.push({...req.headers});
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Page Two');
});
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
await emulate.handler(
{
params: {
extraHttpHeaders: {'X-Persist': 'yes'},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
await page.goto(server.getRoute('/persist-one'));
await page.goto(server.getRoute('/persist-two'));
assert.strictEqual(receivedHeaders[0]?.['x-persist'], 'yes');
assert.strictEqual(receivedHeaders[1]?.['x-persist'], 'yes');
});
});
it('does not affect other emulation settings', async () => {
await withMcpContext(async (response, context) => {
// Set userAgent first
await emulate.handler(
{
params: {
userAgent: 'MyUA',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
// Set extraHTTPHeaders separately
await emulate.handler(
{
params: {
extraHttpHeaders: {'X-Test': 'value'},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
const settings = context.getSelectedMcpPage().emulationSettings;
assert.deepStrictEqual(settings.extraHttpHeaders, {
'X-Test': 'value',
});
});
});
it('reports correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
extraHttpHeaders: {'X-Page': 'one'},
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.deepStrictEqual(
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
{'X-Page': 'one'},
);
const page = await context.newPage();
context.selectPage(page);
assert.strictEqual(
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
undefined,
);
});
});
});
describe('colorScheme', () => {
it('emulates color scheme', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
colorScheme: 'dark',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().colorScheme, 'dark');
const page = context.getSelectedMcpPage().pptrPage;
const scheme = await page.evaluate(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
);
assert.strictEqual(scheme, 'dark');
});
});
it('updates color scheme', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
colorScheme: 'dark',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().colorScheme, 'dark');
await emulate.handler(
{
params: {
colorScheme: 'light',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().colorScheme, 'light');
const page = context.getSelectedMcpPage().pptrPage;
const scheme = await page.evaluate(() =>
window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark',
);
assert.strictEqual(scheme, 'light');
});
});
it('resets color scheme when set to auto', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
const initial = await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
);
await emulate.handler(
{
params: {
colorScheme: 'dark',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().colorScheme, 'dark');
// Check manually that it is dark
assert.strictEqual(
await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
),
true,
);
await emulate.handler(
{
params: {
colorScheme: 'auto',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(context.getSelectedMcpPage().colorScheme, null);
assert.strictEqual(
await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
),
initial,
);
});
});
});
});