electron-playwright-helpers
Version:
Helper functions for Electron end-to-end testing using Playwright
394 lines • 16.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.waitForMenuItemStatus = exports.waitForMenuItem = exports.findMenuItem = exports.getApplicationMenu = exports.getMenuItemById = exports.isSerializedNativeImageError = exports.isSerializedNativeImageSuccess = exports.getMenuItemAttribute = exports.clickMenuItem = exports.clickMenuItemById = void 0;
const general_helpers_1 = require("./general_helpers");
/**
* Execute the `.click()` method on the element with the given id.
* **NOTE:** All menu testing functions will only work with items in the
* [application menu](https://www.electronjs.org/docs/latest/api/menu#menusetapplicationmenumenu).
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param id {string} - the id of the MenuItem to click
* @returns {Promise<void>}
* @fulfil {void} resolves with the result of the `click()` method - probably `undefined`
*/
function clickMenuItemById(electronApp, id) {
return electronApp.evaluate(({ Menu }, menuId) => {
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
const menuItem = menu.getMenuItemById(menuId);
if (menuItem) {
return menuItem.click();
}
else {
throw new Error(`Menu item with id ${menuId} not found`);
}
}, id);
}
exports.clickMenuItemById = clickMenuItemById;
/**
* Click the first matching menu item by any of its properties. This is
* useful for menu items that don't have an id. HOWEVER, this is not as fast
* or reliable as using `clickMenuItemById()` if the menu item has an id.
*
* **NOTE:** All menu testing functions will only work with items in the
* [application menu](https://www.electronjs.org/docs/latest/api/menu#menusetapplicationmenumenu).
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param property {String} - a property of the MenuItem to search for
* @param value {String | Number | Boolean} - the value of the property to search for
* @returns {Promise<void>}
* @fulfil {void} resolves with the result of the `click()` method - probably `undefined`
*/
async function clickMenuItem(electronApp, property, value) {
const menuItem = await findMenuItem(electronApp, property, value);
if (!menuItem) {
throw new Error(`Menu item with ${property} = ${value} not found`);
}
if (menuItem.commandId === undefined) {
throw new Error(`Menu item with ${property} = ${value} has no commandId`);
}
return await electronApp.evaluate(async ({ Menu }, commandId) => {
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
// recurse through the menu to find menu item with matching commandId
function findMenuItem(menu, commandId) {
for (const item of menu.items) {
if (item.type === 'submenu' && item.submenu) {
const found = findMenuItem(item.submenu, commandId);
if (found) {
return found;
}
}
else if (item.commandId === commandId) {
return item;
}
}
}
const mI = findMenuItem(menu, commandId);
if (!mI) {
throw new Error(`Menu item with commandId ${commandId} not found`);
}
if (!mI.click) {
throw new Error(`Menu item has no click method`);
}
await mI.click();
}, menuItem.commandId);
}
exports.clickMenuItem = clickMenuItem;
/**
* Get a given attribute the MenuItem with the given id.
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param menuId {string} - the id of the MenuItem to retrieve the attribute from
* @param attribute {string} - the attribute to retrieve
* @returns {Promise<string>}
* @fulfil {string} resolves with the attribute value
*/
function getMenuItemAttribute(electronApp, menuId, attribute) {
const attr = attribute;
const resultPromise = electronApp.evaluate(({ Menu }, { menuId, attr }) => {
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
const menuItem = menu.getMenuItemById(menuId);
if (!menuItem) {
throw new Error(`Menu item with id "${menuId}" not found`);
}
else if (menuItem[attr] === undefined) {
throw new Error(`Menu item with id "${menuId}" has no attribute "${attr}"`);
}
else {
return menuItem[attr];
}
}, { menuId, attr });
return resultPromise;
}
exports.getMenuItemAttribute = getMenuItemAttribute;
/** Type guard to check if a SerializedNativeImage is a success case */
function isSerializedNativeImageSuccess(image) {
return 'dataURL' in image;
}
exports.isSerializedNativeImageSuccess = isSerializedNativeImageSuccess;
/** Type guard to check if a SerializedNativeImage is an error case */
function isSerializedNativeImageError(image) {
return 'error' in image;
}
exports.isSerializedNativeImageError = isSerializedNativeImageError;
/**
* Get information about the MenuItem with the given id. Returns serializable values including
* primitives, objects, arrays, and other non-recursive data structures.
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param menuId {string} - the id of the MenuItem to retrieve
* @returns {Promise<MenuItemPartial>}
* @fulfil {MenuItemPartial} the MenuItem with the given id
*/
function getMenuItemById(electronApp, menuId) {
return electronApp.evaluate(({ Menu }, { menuId }) => {
// we need this function to be in scope/context for the electronApp.evaluate
function cleanMenuItem(menuItem, visited = new WeakSet()) {
// Check for circular references
if (visited.has(menuItem)) {
return { id: menuItem.id, label: '[Circular Reference]' };
}
visited.add(menuItem);
const returnValue = {};
Object.entries(menuItem).forEach(([k, v]) => {
const key = k;
const value = v;
try {
if (value === null || value === undefined) {
returnValue[key] = value;
}
else if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
returnValue[key] = value;
}
else if (value instanceof Date) {
// Convert dates to ISO strings for serialization
returnValue[key] = value.toISOString();
}
else if (value &&
typeof value === 'object' &&
value.constructor &&
value.constructor.name === 'NativeImage') {
// Handle nativeImage objects by converting to data URL
try {
returnValue[key] = {
type: 'NativeImage',
dataURL: value.toDataURL(),
size: value.getSize(),
isEmpty: value.isEmpty(),
};
}
catch (imageError) {
returnValue[key] = {
type: 'NativeImage',
error: 'Failed to serialize image',
};
}
}
else if ((Array.isArray(value) && key !== 'submenu') ||
typeof value === 'object') {
returnValue[key] = JSON.parse(JSON.stringify(value));
}
// Skip functions and other non-serializable types
}
catch (error) {
// Skip properties that can't be accessed or serialized
}
});
if (menuItem.type === 'submenu' && menuItem.submenu) {
returnValue['submenu'] = menuItem.submenu.items.map((item) => cleanMenuItem(item, visited));
}
return returnValue;
}
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
const menuItem = menu.getMenuItemById(menuId);
if (menuItem) {
const visited = new WeakSet();
return cleanMenuItem(menuItem, visited);
}
else {
throw new Error(`Menu item with id ${menuId} not found`);
}
}, { menuId });
}
exports.getMenuItemById = getMenuItemById;
/**
* Get the current state of the application menu. Contains serializable values including
* primitives, objects, arrays, and other non-recursive data structures.
* Very similar to menu
* [construction template structure](https://www.electronjs.org/docs/latest/api/menu#examples)
* in Electron.
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @returns {Promise<MenuItemPartial[]>}
* @fulfil {MenuItemPartial[]} an array of MenuItem-like objects
*/
function getApplicationMenu(electronApp) {
const promise = electronApp.evaluate(({ Menu }) => {
// we need this function to be in scope/context for the electronApp.evaluate
function cleanMenuItem(menuItem, visited = new WeakSet()) {
// Check for circular references
if (visited.has(menuItem)) {
return { id: menuItem.id, label: '[Circular Reference]' };
}
visited.add(menuItem);
const returnValue = {};
Object.entries(menuItem).forEach(([k, v]) => {
const key = k;
const value = v;
try {
if (value === null || value === undefined) {
returnValue[key] = value;
}
else if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
returnValue[key] = value;
}
else if (value instanceof Date) {
// Convert dates to ISO strings for serialization
returnValue[key] = value.toISOString();
}
else if (value &&
typeof value === 'object' &&
value.constructor &&
value.constructor.name === 'NativeImage') {
// Handle nativeImage objects by converting to data URL
try {
returnValue[key] = {
type: 'NativeImage',
dataURL: value.toDataURL(),
size: value.getSize(),
isEmpty: value.isEmpty(),
};
}
catch (imageError) {
returnValue[key] = {
type: 'NativeImage',
error: 'Failed to serialize image',
};
}
}
else if ((Array.isArray(value) && key !== 'submenu') ||
typeof value === 'object') {
returnValue[key] = JSON.parse(JSON.stringify(value));
}
// Skip functions and other non-serializable types
}
catch (error) {
// Skip properties that can't be accessed or serialized
}
});
if (menuItem.type === 'submenu' && menuItem.submenu) {
returnValue['submenu'] = menuItem.submenu.items.map((item) => cleanMenuItem(item, visited));
}
return returnValue;
}
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
const cleanItems = menu.items.map((item) => cleanMenuItem(item));
return cleanItems;
});
return promise;
}
exports.getApplicationMenu = getApplicationMenu;
/**
* Find a MenuItem by any of its properties
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param property {string} - the property to search for
* @param value {string} - the value to search for
* @param menuItems {MenuItemPartial | MenuItemPartial[]} optional - single MenuItem or array - if not provided, will be retrieved from the application menu
* @returns {Promise<MenuItemPartial>}
* @fulfil {MenuItemPartial} the first MenuItem with the given property and value
*/
async function findMenuItem(electronApp, property, value, menuItems) {
if (property === 'role') {
// set the value to lowercase
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = value.toLowerCase();
}
if (!menuItems) {
const menu = await getApplicationMenu(electronApp);
return findMenuItem(electronApp, property, value, menu);
}
if (Array.isArray(menuItems)) {
for (const menuItem of menuItems) {
const found = await findMenuItem(electronApp, property, value, menuItem);
if (found) {
return found;
}
}
}
else {
if (menuItems[property] === value) {
return menuItems;
}
else if (menuItems.submenu) {
return findMenuItem(electronApp, property, value, menuItems.submenu);
}
}
}
exports.findMenuItem = findMenuItem;
/**
* Wait for a MenuItem to exist
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param id {string} - the id of the MenuItem to wait for
* @returns {Promise<void>}
* @fulfil {void} resolves when the MenuItem is found
*/
async function waitForMenuItem(electronApp, id) {
await (0, general_helpers_1.electronWaitForFunction)(electronApp, ({ Menu }, id) => {
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
return !!menu.getMenuItemById(id);
}, id);
}
exports.waitForMenuItem = waitForMenuItem;
/**
* Wait for a MenuItem to have a specific attribute value.
* For example, wait for a MenuItem to be enabled... or be visible.. etc
*
* @category Menu
*
* @param electronApp {ElectronApplication} - the Electron application object (from Playwright)
* @param id {string} - the id of the MenuItem to wait for
* @param property {string} - the property to search for
* @param value {string | number | boolean} - the value to search for
* @returns {Promise<void>}
* @fulfil {void} resolves when the MenuItem with correct status is found
*/
async function waitForMenuItemStatus(electronApp, id, property, value) {
if (property === 'role') {
// set the value to lowercase
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = value.toLowerCase();
}
await (0, general_helpers_1.electronWaitForFunction)(electronApp, ({ Menu }, { id, value, property }) => {
const menu = Menu.getApplicationMenu();
if (!menu) {
throw new Error('No application menu found');
}
const menuItem = menu.getMenuItemById(id);
if (!menuItem) {
throw new Error(`Menu item with id "${id}" not found`);
}
return menuItem[property] === value;
}, { id, value, property });
}
exports.waitForMenuItemStatus = waitForMenuItemStatus;
//# sourceMappingURL=menu_helpers.js.map