Skip to content

Provides device emulation via -d/--device flag using playwright#80

Open
MildMax wants to merge 12 commits intomainfrom
mildmax/mobile-emulation
Open

Provides device emulation via -d/--device flag using playwright#80
MildMax wants to merge 12 commits intomainfrom
mildmax/mobile-emulation

Conversation

@MildMax
Copy link

@MildMax MildMax commented Jan 9, 2026

  • Provides new flag -d/--device to allow device emulation
  • Users are allowed to provide custom viewports that override device defaults via --width and --height CLI args

@MildMax MildMax changed the title Provides mobile emulation via -m/--mobile flag using playwright Provides devi e emulation via -d/--device flag using playwright Jan 12, 2026
@MildMax MildMax marked this pull request as ready for review January 12, 2026 17:28
@MildMax MildMax requested a review from a team January 12, 2026 17:28
Copy link
Member

@sergeychernyshev sergeychernyshev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why, but npm test prog has some outstanding promises.

@sergeychernyshev sergeychernyshev changed the title Provides devi e emulation via -d/--device flag using playwright Provides device emulation via -d/--device flag using playwright Jan 16, 2026
@MildMax MildMax force-pushed the mildmax/mobile-emulation branch from c92a410 to 5177303 Compare January 23, 2026 16:31
@MildMax MildMax force-pushed the mildmax/mobile-emulation branch from 441c132 to f2ed6ba Compare January 23, 2026 21:07
@MildMax MildMax force-pushed the mildmax/mobile-emulation branch from 20e81b0 to 07e2d02 Compare January 23, 2026 21:33
@mjkozicki mjkozicki linked an issue Feb 9, 2026 that may be closed by this pull request
Comment on lines +1 to +68
import { launchTest } from '../index.js';
import fs from 'fs';
import { devices } from 'playwright/test';

import { BrowserConfig } from '../lib/browsers.js';
import { normalizeCLIConfig } from '../lib/config.js';
const browsers = BrowserConfig.getBrowsers();

const device = 'Nexus 10'
const browser = 'chrome'



test('Setting device emulation updates the config', async () => {
let options = {
browser: 'chrome',
device: device,
url: 'https://www.example.com',
};
var config = normalizeCLIConfig(options);

const result = await launchTest(config);

expect(result).toHaveProperty('success');
expect(result).toHaveProperty('testId');
expect(result).toHaveProperty('resultsPath');
expect(result.success).toBe(true);
expect(fs.existsSync(result.resultsPath)).toBe(true);
});


// describe.each(browsers)('Programmatic API (%s)', browser => {
// describe.each(Object.keys(devices))(
// 'launchTest executes and returns result object when emulating device: %s',
// device => {
// test('Setting device emulation updates the config', async () => {
// console.log(devices)
// let options = {
// browser,
// device: device,
// url: '../tests/sandbox/index.html',
// };
// options = normalizeCLIConfig(options);
// let config = new BrowserConfig().getBrowserConfig(browser, options);
// expect(config && typeof config === 'object').toBe(true);
// // test collecting device data for desktop devices
// if (device.toLowerCase().includes('desktop')) {
// expect(config.isMobile).toBe(false);
// expect(config.hasTouch).toBe(false);
// }
// // test collecting device data for non-desktop devices
// else {
// expect(config.isMobile).toBe(true);
// expect(config.hasTouch).toBe(true);
// expect(config.deviceScaleFactor).toBeDefined();
// }

// const result = await launchTest(options);

// expect(result).toHaveProperty('success');
// expect(result).toHaveProperty('testId');
// expect(result).toHaveProperty('resultsPath');
// expect(result.success).toBe(true);
// expect(fs.existsSync(result.resultsPath)).toBe(true);
// });
// },
// );
// });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { launchTest } from '../index.js';
import fs from 'fs';
import { devices } from 'playwright/test';
import { BrowserConfig } from '../lib/browsers.js';
import { normalizeCLIConfig } from '../lib/config.js';
const browsers = BrowserConfig.getBrowsers();
const device = 'Nexus 10'
const browser = 'chrome'
test('Setting device emulation updates the config', async () => {
let options = {
browser: 'chrome',
device: device,
url: 'https://www.example.com',
};
var config = normalizeCLIConfig(options);
const result = await launchTest(config);
expect(result).toHaveProperty('success');
expect(result).toHaveProperty('testId');
expect(result).toHaveProperty('resultsPath');
expect(result.success).toBe(true);
expect(fs.existsSync(result.resultsPath)).toBe(true);
});
// describe.each(browsers)('Programmatic API (%s)', browser => {
// describe.each(Object.keys(devices))(
// 'launchTest executes and returns result object when emulating device: %s',
// device => {
// test('Setting device emulation updates the config', async () => {
// console.log(devices)
// let options = {
// browser,
// device: device,
// url: '../tests/sandbox/index.html',
// };
// options = normalizeCLIConfig(options);
// let config = new BrowserConfig().getBrowserConfig(browser, options);
// expect(config && typeof config === 'object').toBe(true);
// // test collecting device data for desktop devices
// if (device.toLowerCase().includes('desktop')) {
// expect(config.isMobile).toBe(false);
// expect(config.hasTouch).toBe(false);
// }
// // test collecting device data for non-desktop devices
// else {
// expect(config.isMobile).toBe(true);
// expect(config.hasTouch).toBe(true);
// expect(config.deviceScaleFactor).toBeDefined();
// }
// const result = await launchTest(options);
// expect(result).toHaveProperty('success');
// expect(result).toHaveProperty('testId');
// expect(result).toHaveProperty('resultsPath');
// expect(result.success).toBe(true);
// expect(fs.existsSync(result.resultsPath)).toBe(true);
// });
// },
// );
// });
import { launchTest } from '../index.js';
import { BrowserConfig } from '../lib/browsers.js';
import { devices } from 'playwright/test';
import { normalizeCLIConfig } from '../lib/config.js';
const browsers = BrowserConfig.getBrowsers();
const availableDevices = devices.filter(device => browsers.contains(device.defaultBrowserType));
describe.each(availableDevices)('Setting device emulation updates the config: %s', device => {
test('Setting device emulation updates the config', () => {
let options = {
browser: device.defaultBrowserType,
device: device.name,
url: '../tests/sandbox/index.html',
};
options = normalizeCLIConfig(options);
let config = new BrowserConfig().getBrowserConfig(device.defaultBrowserType, options);
expect(config && typeof config === 'object').toBe(true);
expect(config.isMobile).toBe(device.isMobile);
expect(config.hasTouch).toBe(device.hasTouch);
expect(config.width).toBe(device.viewport.width);
expect(config.height).toBe(device.viewport.height);
expect(config.deviceScaleFactor).toBeDefined();
});
});
describe.each(availableDevices)('Setting device emulation with overrides updates the config: %s', device => {
test('Setting device emulation updates the config', () => {
let options = {
browser: device.defaultBrowserType,
device: device.name,
width: 9999,
height: 9999,
url: '../tests/sandbox/index.html',
};
options = normalizeCLIConfig(options);
let config = new BrowserConfig().getBrowserConfig(device.defaultBrowserType, options);
expect(config && typeof config === 'object').toBe(true);
expect(config.isMobile).toBe(device.isMobile);
expect(config.hasTouch).toBe(device.hasTouch);
expect(config.width).toBe(9999);
expect(config.height).toBe(9999);
});
});
describe.each(availableDevices)('Device emulation: %s', device => {
test('Device emulation successfully runs tests', async () => {
let options = {
browser: device.defaultBrowserType,
device: device.name,
url: '../tests/sandbox/index.html',
};
options = normalizeCLIConfig(options);
let config = new BrowserConfig().getBrowserConfig(device.defaultBrowserType, options);
const result = await launchTest(config);
expect(result).toHaveProperty('success');
expect(result).toHaveProperty('testId');
expect(result).toHaveProperty('resultsPath');
expect(result.success).toBe(true);
expect(fs.existsSync(result.resultsPath)).toBe(true);
});
});

Comment on lines +83 to +108
describe.each(Object.keys(devices))(
'Setting device emulation updates the config for device: %s',
device => {
test('Setting device emulation updates the config', () => {
let options = {
browser,
device: device,
url: '../tests/sandbox/index.html',
};
options = normalizeCLIConfig(options);
let config = new BrowserConfig().getBrowserConfig(browser, options);
expect(config && typeof config === 'object').toBe(true);
// test collecting device data for desktop devices
if (device.toLowerCase().includes('desktop')) {
expect(config.isMobile).toBe(false);
expect(config.hasTouch).toBe(false);
}
// test collecting device data for non-desktop devices
else {
expect(config.isMobile).toBe(true);
expect(config.hasTouch).toBe(true);
expect(config.deviceScaleFactor).toBeDefined();
}
});
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emulation.test.js can cover launchTest

Suggested change
describe.each(Object.keys(devices))(
'Setting device emulation updates the config for device: %s',
device => {
test('Setting device emulation updates the config', () => {
let options = {
browser,
device: device,
url: '../tests/sandbox/index.html',
};
options = normalizeCLIConfig(options);
let config = new BrowserConfig().getBrowserConfig(browser, options);
expect(config && typeof config === 'object').toBe(true);
// test collecting device data for desktop devices
if (device.toLowerCase().includes('desktop')) {
expect(config.isMobile).toBe(false);
expect(config.hasTouch).toBe(false);
}
// test collecting device data for non-desktop devices
else {
expect(config.isMobile).toBe(true);
expect(config.hasTouch).toBe(true);
expect(config.deviceScaleFactor).toBeDefined();
}
});
},
);

Comment on lines +96 to +101
// `npm test emlation` fails to finish
browserConfig.isMobile = true

// `npm test emulation` succeeds and finishes
// browserConfig.isMobile = false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needed?

String(DEFAULT_OPTIONS.height),
),
)
.addOption(new Option('--width <int>', 'Viewport width, in pixels'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.addOption(new Option('--width <int>', 'Viewport width, in pixels'))
.addOption(new Option('--width <int>', 'Viewport width, in pixels. If both width and device are provided, the width value will override device emulation viewport width.')).default(
String(DEFAULT_OPTIONS.width),
),

),
)
.addOption(new Option('--width <int>', 'Viewport width, in pixels'))
.addOption(new Option('--height <int>', 'Viewport height, in pixels'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.addOption(new Option('--height <int>', 'Viewport height, in pixels'))
.addOption(new Option('--height <int>', 'Viewport height, in pixels. If both height and device are provided, the height value will override device emulation viewport height.')).default(
String(DEFAULT_OPTIONS.height),
),

'--dry',
'Dry run (do not run test, just save config and cleanup)',
).default(DEFAULT_OPTIONS.dry),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readd the default option for dry.

new Option(
'-d, --device <string>',
'Device to emulate; devices are based on the Playwright device list (see https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json)',
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add default option for device.

Comment on lines +42 to +53
// set width and height from options before assigning device viewport or defaults
if (options.width) {
config.width = parseInt(options.width);
} else if (
config.device &&
config.device.viewport &&
config.device.viewport.width
) {
config.width = config.device.viewport.width;
} else {
config.width = DEFAULT_OPTIONS.width;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the config.width need to be set if there is no override for the config.device.viewport.width value?

Suggested change
// set width and height from options before assigning device viewport or defaults
if (options.width) {
config.width = parseInt(options.width);
} else if (
config.device &&
config.device.viewport &&
config.device.viewport.width
) {
config.width = config.device.viewport.width;
} else {
config.width = DEFAULT_OPTIONS.width;
}
// set width and height from options before assigning device viewport or defaults
if (options.width) {
config.width = parseInt(options.width);
}

Comment on lines +55 to +65
if (options.height) {
config.height = parseInt(options.height);
} else if (
config.device &&
config.device.viewport &&
config.device.viewport.height
) {
config.height = config.device.viewport.height;
} else {
config.height = DEFAULT_OPTIONS.height;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the config.height need to be set if there is no override for the config.device.viewport.height value?

Suggested change
if (options.height) {
config.height = parseInt(options.height);
} else if (
config.device &&
config.device.viewport &&
config.device.viewport.height
) {
config.height = config.device.viewport.height;
} else {
config.height = DEFAULT_OPTIONS.height;
}
if (options.height) {
config.height = parseInt(options.height);
}

// Dry run (false = no dry run)
dry: false,
// Device to emulate (empty string = no emulation)
device: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be clearer is device defaulted to null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: 📱Mobile Emulation

3 participants