Testing WebUSB scenarios with Playwright
August 25, 2024 by Arjun Attam
Outline
- WebUSB is an upcoming standard that bolsters the web platform - with crypto wallets, printers etc
- Not covered in CDP - bunch of open bugs in Chromium
- As a result it is not covered in Playwright or Puppeteer
- The key issue is that WebUSB dialog to connect a USB device with the app (thrown by the
navigator.usb.requestDevice
API call) is not intercepted by the automation script - There’s a hack-y way to make it work via enterprise policies: which removes the dialog altogether
- Steps
- Create a policy on Chrome/Chromium
- macOS steps
- open
chrome://policy
to verify that the policy has been applied correctly
- Create a fixture/setup project in your code to create the policy file and tear it down after the tests have finished running
- Catch here: requires sudo
- If you are testing a web page - then fixture is appropriate
- If you are testing a chrome extension - maybe setup project is better
- Create a policy on Chrome/Chromium
Show me the code
In your test fixtures file,
import { writePolicy } from "../utils/policy";
import { test as setup } from "../fixtures";
setup.skip(process.env.CI === "true");
setup("chrome policy setup for WebUSB", async ({ extensionId }) => {
// Since WebUSB is not supported in CDP (and therefore Playwright), we need to use
// a workaround that involves Chrome enterprise policy to allow specific WebUSB devices.
// Chromium issue: https://issues.chromium.org/issues/40276988
await writePolicy(extensionId);
});
Utiltiies for changing Chrome policy
import sudo from "sudo-prompt";
import { exec } from "child_process";
import fs from "fs";
function chromePolicyContent(extensionId: string): string {
// Reference: https://chromeenterprise.google/policies/#WebUsbAllowDevicesForUrls
// The vendor id is hard coded for now
const vendorId = "0x2c97";
return `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>WebUsbAllowDevicesForUrls</key>
<array>
<dict>
<key>devices</key>
<array>
<dict>
<key>vendor_id</key>
<integer>${vendorId}</integer>
</dict>
</array>
<key>urls</key>
<array>
<string>chrome-extension://${extensionId}</string>
</array>
</dict>
</array>
</dict>
</plist>
`;
}
function getSystemUsername() {
return require("os").userInfo().username;
}
function getPolicyPath(withExtension: boolean) {
// Reference: https://www.chromium.org/administrators/mac-quick-start/
return `/Library/Managed Preferences/${getSystemUsername()}/org.chromium.Chromium${withExtension ? ".plist" : ""}`;
}
const SUDO_OPTIONS = {
name: "Playwright tests",
};
export async function writePolicy(extensionId: string) {
if (process.platform !== "darwin") {
// TODO: remove this
throw new Error(`Chrome policy is only supported on macOS`);
}
const tempFile = "temp.plist";
const fileContent = chromePolicyContent(extensionId);
fs.writeFileSync(tempFile, fileContent);
const mvCommand = `mv ${tempFile} "${getPolicyPath(true)}"`;
return new Promise((resolve, reject) => {
sudo.exec(mvCommand, SUDO_OPTIONS, (error) => {
if (error) {
reject(error);
}
// macOS can cache the policy file, so we need to read it to force a reload
exec(`defaults read "${getPolicyPath(false)}"`, (error) => {
if (error) {
reject(error);
}
resolve("done");
});
});
});
}
export async function cleanupPolicy() {
// TODO:
const rmCommand = `rm -rf "${getPolicyPath(true)}"`;
return new Promise((resolve, reject) => {
sudo.exec(rmCommand, SUDO_OPTIONS, (error) => {
if (error) {
reject(error);
}
// macOS can cache the policy file, so we need to read it to force a reload
exec(`defaults read "${getPolicyPath(false)}"`, () => {
// This will error out because the file is removed, so ignore the error
resolve("done");
});
});
});
}