UNPKG

electron-playwright-helpers

Version:

Helper functions for Electron end-to-end testing using Playwright

394 lines 16.5 kB
"use strict"; 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