What is Puppeteer?
Puppeteer is a Node library that can be used for the automated testing of chrome extensions. It provides a high-level API control over Chrome or Chromium via the DevTools protocol. One great advantage of this library is that it can be used to do most of the things you manually do in the browser.
When testing chrome extensions, there are two major test scenarios, which are testing the compatibility of the extension and testing functionality of the extension. You can test the compatibility of a chromium-based browser extension by loading that extension on each browser variant. When it comes to testing the functionality, the puppeteer library provides various methods to imitate user interactions. This allows developers to create automated test cases to inspect and analyze the behavior of an extension.
Testing Extension Compatibility
Chrome extensions are inherently cross-browser compatible. Most chrome extensions can also be used in other chromium-based browsers such as MS Edge (Chromium), Vivaldi, Brave, etc. Puppeteer provides users the ability to specify the browser in the puppeteer class. In the following section, you will see how to load a browser extension in different browsers.
When using the normal puppeteer library, it will install the latest Chromium build, and therefore it is not required to provide the location of the browser and get called directly.
const puppeteer = require('puppeteer');
// Calling the default chromium installation
const browser = await puppeteer.launch();
However, if you are using the lightweight puppeteer-core or defining a different browser, you need to provide the executablePath option in the puppeteer.launch method. The below code demonstrates how to test the installation of an extension on MS Edge (Chromium)
browser_test.js
const puppeteer = require('puppeteer');
(async () => {
// Path to extension folder
const paths = 'F:\\puppeteer\\ext\\cplklnmnlbnpmjogncfgfijoopmnlemp\\';
try {
console.log('==>Open Browser');
const browser = await puppeteer.launch({
// Define the browser location
executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge Beta\\Application\\msedge.exe',
// Disable headless mode
headless: false,
// Pass the options to install the extension
args: [
`--disable-extensions-except=${paths}`,
`--load-extension=${paths}`,
`--window-size=800,600`
]
});
console.log('==>Navigate to Extension');
const page = await browser.newPage();
// Navigate to extension page
await page.goto('chrome-extension://fgbekoibaojaiamoheiklljjiihibdcb/panel.html');
// Take a screenshot of the extension page
console.log('==>Take Screenshot');
await page.screenshot({path: 'msedge-extension.png'});
console.log('==>Close Browser');
await browser.close();
}
catch (err) {
console.error(err);
}
})();
The following code-block will open a new MS Edge (Chromium) browser and install the chrome extension using the provided optional arguments.
--disable-extensions-except=<Extension Folder Path>
--load-extension=<Extention Folder Path>
The load-extension option will load the plugin while the --disable-extensions-except option will disable other extensions that might interfere with the testing.
A new browser page (tab) will then be created, and you will navigate to the extension URL and take a screenshot. The extension URL can be defined in the below format.
<Extension Protocol>://<Extension ID>/<Extension Page>
\
Additionally, the code is encapsulated within an async function to enforce an asynchronous operation in each stage of the puppeteer process.
You can make sure that the test is successful by checking the terminal output and the captured screenshot.
Terminal
Captured Screenshot (msedge-extension.png)
You can define the location for any other chromium-based browser in the same way that is used to define the location for MS Edge (Chromium).
Testing the Extension Functionality
Now you know how to load an extension to a browser, and next comes the functionality testing of the extension. But before that, you have to tackle a major obstacle in defining the extension URL. Whenever a new browser instance is loaded, it will create a new extension ID, making it difficult to define a static path for the extension.
However, Puppeteer provides a way to query for the extension ID by utilizing a background script. There, the extension ID can be extracted using the targets method in the Browser class, as shown below.
find-extensionid.js
const puppeteer = require('puppeteer');
(async () => {
// Define the extension path
const paths = 'F:\\puppeteer\\ext\\cplklnmnlbnpmjogncfgfijoopmnlemp\\';
try {
console.log('==>Open Browser');
// Configure the browser (Default Chromium Installation)
const browser = await puppeteer.launch({
headless: false,
// Chrome options
args: [
`--disable-extensions-except=${paths}`,
`--load-extension=${paths}`,
`--window-size=800,600`
]
});
// Name of the extension
const extensionName = 'iMacros for Chrome';
// Find the extension
const targets = await browser.targets();
const extensionTarget = targets.find(({ _targetInfo }) => {
return _targetInfo.title === extensionName && _targetInfo.type === 'background_page';
});
// Extract the URL
const extensionURL = extensionTarget._targetInfo.url;
console.log("\nExtracted URL ==>" + extensionURL);
const urlSplit = extensionURL.split('/');
console.log("Split URL ==>");
console.log(urlSplit);
const extensionID = urlSplit[2];
console.log("Extension ID ==>" + extensionID +"\n");
// Define the extension page
const extensionEndURL = 'panel.html';
//Navigate to the page
console.log('==>Navigate to Extension');
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
console.log('==>Take Screenshot');
await page.screenshot({path: 'chromium-extension.png'});
console.log('==>Close Browser');
await browser.close();
}
catch (err) {
console.error(err);
}
})();
In the above code block, you are querying for the chrome extension using the targets method. The targets method can be used to access the target of any page managed by the browser. In this instance, the target is specified using the extension name (This refers to the extension title defined in the manifest.json file of the extension).
Once the extension is located, you can extract the complete URL and then split it to retrieve the extension ID. You can observe this process using the console.log() outputs as shown in the following screenshot.
The last step is to create a variable to pass the necessary page with the extension. You have created a new variable called extensionEndURL to indicate the page and finally pass both the extension ID and the necessary page to the page.goto() method to direct the browser to the extension location.
User Interactions
Now, you have launched the browser and obtained the extension programmatically. The only remaining task is to interact with the extension. Puppeteer provides multiple ways to find elements within a webpage and interact with them. Let us go through the following code sample to see them in action.
google-search.js
const puppeteer = require('puppeteer');
(async () => {
try {
// Calling the default chromium installation
const browser = await puppeteer.launch({headless:false, defaultViewport: null});
const page = await browser.newPage();
await page.goto('https://www.google.com');
// Setup Viewport
await page.setViewport({width: 1280, height: 800})
// Interact with webpage
// Search Google
await page.type('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input', 'Puppeteer');
await page.$eval('input[name=btnK]', el => el.click());
// Wait for results
await page.waitForNavigation();
// Take screenshot
await page.screenshot({path: 'google-search.png'});
await browser.close();
}
catch (err) {
console.error(err);
}
})();
Above, you have defined a simple script to navigate to Google and perform a search. You are providing input to the search field using the page.type() method and CSS selector to indicate the element and enter the search term. Then, using the page.$eval() method, you are querying for the search button and clicking on that button. This method will use the document.querySelector function to search for the specific element. Then you have to wait for the page to load and take the screenshot.
All these interactions are based on DOM object querying. The Document Object Model is queried by using the $ (querySelector) and $$ (querySelectorAll) APIs provided by Puppeteer.
When it comes to testing a Chrome extension, you can use the same methods to interact with the extension. The following code-block illustrates how to interact with the “iMacros for Chrome” extension using the methods mentioned above. When the extension loads, you will click on the RECORD tab and navigate to the macro recording section.
//Navigate to the page
console.log('==>Navigate to Extension');
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
console.log('==>Click on RECORD tab')
await page.$eval('#record-tab-label', el => el.click());
console.log('==>Take Screenshot');
await page.screenshot({path: 'chromium-extension.png'});
RESULT
Bonus Script
The following script includes the things you have learned up to now and creates a script that will execute in multiple browsers like Chromium, MS Edge (Chromium), Vivaldi, and Google Chrome. It will also test the extension loading and functionality and then end the program by taking a screenshot. This script will run in all the browsers simultaneously while executing each action within the browsers in an asynchronous manner.
ext_test.js
const puppeteer = require('puppeteer');
const path = require('path');
// Required global variables
const ext_install_path = path.join(__dirname, '\\ext\\cplklnmnlbnpmjogncfgfijoopmnlemp\\');
const image_path = path.join(__dirname, '\\img\\');
const extensionEndURL = 'panel.html'
// Function to obtain extension ID
async function getID(browserObj) {
// Name of the extension
const extensionName = 'iMacros for Chrome';
// Find the extension
const targets = await browserObj.targets();
const extensionTarget = targets.find(({ _targetInfo }) => {
return _targetInfo.title === extensionName && _targetInfo.type === 'background_page';
});
// Extract the URL
const extensionURL = extensionTarget._targetInfo.url;
const urlSplit = extensionURL.split('/');
const extID = urlSplit[2];
return extID;
}
// Testing Built-in Chromium
(async () => {
console.log("===> Testing Chromium")
const browser = await puppeteer.launch({
headless: false,
// Chrome options
args: [
`--disable-extensions-except=${ext_install_path}`,
`--load-extension=${ext_install_path}`,
]
});
const extensionID = await getID(browser);
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
await page.$eval('#record-tab-label', el => el.click());
await page.screenshot({path: path.join(image_path, 'default-chromium.png')});
await browser.close();
})();
// Testing MS Edge (Chromium)
(async () => {
console.log("===> Testing MS Edge (Chromium)")
const browser_location = 'C:\\Program Files (x86)\\Microsoft\\Edge Beta\\Application\\msedge.exe'
const browser = await puppeteer.launch({
executablePath: browser_location,
headless: false,
args: [
`--disable-extensions-except=${ext_install_path}`,
`--load-extension=${ext_install_path}`,
`--window-size=800,600`
]
});
const extensionID = await getID(browser);
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
await page.$eval('#record-tab-label', el => el.click());
await page.screenshot({path: path.join(image_path, 'msedge-extension.png')});
await browser.close();
})();
// Testing Vivaldi
(async () => {
console.log("===> Testing Vivaldi")
const browser_location = 'C:\\Program Files\\Vivaldi\\Application\\vivaldi.exe';
const browser = await puppeteer.launch({
executablePath: browser_location,
headless : false,
args: [
`--disable-extensions-except=${ext_install_path}`,
`--load-extension=${ext_install_path}`,
]
});
const extensionID = await getID(browser);
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
await page.$eval('#record-tab-label', el => el.click());
await page.screenshot({path: path.join(image_path, 'vivaldi.png')});
await browser.close();
})();
// Testing Google Chrome
(async () => {
console.log("===> Testing Google Chrome")
const browser_location = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
const browser = await puppeteer.launch({
executablePath: browser_location,
headless : false,
args: [
`--disable-extensions-except=${ext_install_path}`,
`--load-extension=${ext_install_path}`,
]
});
const extensionID = await getID(browser);
const page = await browser.newPage();
await page.setViewport({width: 1280, height: 800})
await page.goto(`chrome-extension://${extensionID}/${extensionEndURL}`);
await page.$eval('#record-tab-label', el => el.click());
await page.screenshot({path: path.join(image_path, 'google-chrome.png')});
await browser.close();
})();
Conclusion
In this article, we’ve learned about Puppeteer and how it can be used to test Chrome extensions. Puppeteer is a powerful library that can be customized according to the needs of the user. Puppeteer can also be integrated with other Javascript testing frameworks like Jest, Mocha, or Jasmine in order to create a complete test suite.
Writing a test for your Chrome extension? There's an easier way.
While the above is a good starting point for building Chrome Extension testing in-house, it's still quite a challenging process. If you're looking for an easier way to test your Chrome extension, check out walrus.ai!
- Tests can be written in plain english — no difficult setup required
- Chrome extensions can be tested in an actual browser, not just in a new window/tab
- Zero maintenance — walrus.ai handles test maintenance for you, so you never experience flakes
Can I get an example?
Imagine you wanted to test Zoom's Google calendar Chrome extension. Writing a test for that is as easy as writing 4 lines of code.
$ walrus -u https://zoom.us -i \
'Click on the Zoom Chrome Extension' \
'In the extension popup, click "Schedule a meeting"' \
'In the browser, click Save' \
'In the browser, verify a calendar event loads'
Want to learn more? Let us know.
Top comments (0)