diff --git a/examples/recorder-crx/options.html b/examples/recorder-crx/options.html
new file mode 100644
index 000000000..f4a922000
--- /dev/null
+++ b/examples/recorder-crx/options.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Playwright CRX - Configuration
+
+
+
+
+
+
diff --git a/examples/recorder-crx/public/manifest.json b/examples/recorder-crx/public/manifest.json
index fe698f7a5..4d2aafb08 100644
--- a/examples/recorder-crx/public/manifest.json
+++ b/examples/recorder-crx/public/manifest.json
@@ -19,5 +19,9 @@
},
"default_title": "Record"
},
- "permissions": ["debugger", "tabs", "contextMenus"]
+ "options_ui": {
+ "page": "options.html",
+ "open_in_tab": false
+ },
+ "permissions": ["debugger", "tabs", "contextMenus", "storage"]
}
diff --git a/examples/recorder-crx/src/background.ts b/examples/recorder-crx/src/background.ts
index 05b37e4d2..392bbeebb 100644
--- a/examples/recorder-crx/src/background.ts
+++ b/examples/recorder-crx/src/background.ts
@@ -15,7 +15,7 @@
*/
import type { CrxApplication } from 'playwright-crx';
-import { crx, _debug, _setUnderTest } from 'playwright-crx';
+import playwright, { crx, _debug, _setUnderTest } from 'playwright-crx';
type Mode = 'none' | 'recording' | 'inspecting' | 'assertingText' | 'recording-inspecting' | 'standby' | 'assertingVisibility' | 'assertingValue';
@@ -106,6 +106,10 @@ async function attach(tab: chrome.tabs.Tab) {
}
}
+async function setTestIdAttributeName(testIdAttributeName: string) {
+ playwright.selectors.setTestIdAttribute(testIdAttributeName);
+}
+
chrome.action.onClicked.addListener(attach);
chrome.contextMenus.create({
@@ -118,5 +122,11 @@ chrome.contextMenus.onClicked.addListener(async (_, tab) => {
if (tab) await attach(tab);
});
+chrome.storage.sync.onChanged.addListener(async ({ testIdAttributeName }) => {
+ if (!testIdAttributeName) return;
+
+ await setTestIdAttributeName(testIdAttributeName.newValue);
+});
+
// for testing
-Object.assign(self, { attach, _debug, _setUnderTest });
+Object.assign(self, { attach, setTestIdAttributeName, _debug, _setUnderTest });
diff --git a/examples/recorder-crx/src/options.css b/examples/recorder-crx/src/options.css
new file mode 100644
index 000000000..10ad21ef5
--- /dev/null
+++ b/examples/recorder-crx/src/options.css
@@ -0,0 +1,51 @@
+body {
+ font-family: sans-serif;
+ margin: 20px;
+}
+
+h1 {
+ text-align: center;
+ margin-bottom: 15px;
+}
+
+form {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+label {
+ font-weight: bold;
+}
+
+input[type="text"] {
+ padding: 5px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ width: 100%;
+}
+
+button[type="submit"] {
+ background-color: #4CAF50;
+ border: none;
+ color: white;
+ padding: 10px 20px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ margin: 4px 2px;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: background-color 0.2s ease-in-out;
+}
+
+button:hover {
+ background-color: #45A049;
+}
+
+button[type="submit"]:disabled {
+ background-color: #ccc;
+ color: #666;
+ cursor: not-allowed;
+}
diff --git a/examples/recorder-crx/src/options.ts b/examples/recorder-crx/src/options.ts
new file mode 100644
index 000000000..3ebe7ceb7
--- /dev/null
+++ b/examples/recorder-crx/src/options.ts
@@ -0,0 +1,31 @@
+import './options.css';
+
+async function initialize() {
+ const formElem = document.getElementById('options-form') as HTMLFormElement;
+ const testIdAttributeNameElem = document.getElementById('test-id') as HTMLInputElement;
+ const submitElem = document.getElementById('submit') as HTMLButtonElement;
+
+ if (!testIdAttributeNameElem || !formElem || !submitElem) return;
+
+ testIdAttributeNameElem.addEventListener('input', () => {
+ submitElem.disabled = false;
+ });
+
+ formElem.addEventListener('submit', (e) => {
+ if (!formElem.reportValidity()) return;
+
+ e.preventDefault();
+
+ submitElem.disabled = true;
+ const testIdAttributeName = testIdAttributeNameElem.value;
+ chrome.storage.sync.set({ testIdAttributeName }).catch(() => {});
+
+ return false;
+ });
+
+ submitElem.disabled = true;
+ const { testIdAttributeName } = await chrome.storage.sync.get('testIdAttributeName');
+ testIdAttributeNameElem.value = testIdAttributeName ?? 'data-testid';
+}
+
+initialize();
diff --git a/examples/recorder-crx/vite.config.ts b/examples/recorder-crx/vite.config.ts
index 0770a3d73..407a5092f 100644
--- a/examples/recorder-crx/vite.config.ts
+++ b/examples/recorder-crx/vite.config.ts
@@ -33,6 +33,7 @@ export default defineConfig({
plugins: [sourcemaps()],
input: {
'background': path.resolve(__dirname, 'src/background.ts'),
+ 'options': path.resolve(__dirname, 'options.html'),
},
output: {
entryFileNames: '[name].js',
diff --git a/tests/crx/recorder.spec.ts b/tests/crx/recorder.spec.ts
index e77f4395d..5336c856f 100644
--- a/tests/crx/recorder.spec.ts
+++ b/tests/crx/recorder.spec.ts
@@ -204,7 +204,7 @@ test('should record with all supported actions and assertions', async ({ context
});
const context = await browser.newContext();
const page = await context.newPage();
- await page.goto('http://127.0.0.1:3000/root.html');
+ await page.goto('${baseURL}/root.html');
await page.getByRole('checkbox').check();
await page.getByRole('button', { name: 'button' }).click();
await page.getByRole('checkbox').uncheck();
@@ -227,3 +227,40 @@ test('should record with all supported actions and assertions', async ({ context
await expect(recorderPage.locator('.CodeMirror-line')).toHaveText(code.split('\n'));
});
+
+test('should record with custom testid', async ({ page, attachRecorder, recordAction, baseURL, extensionServiceWorker }) => {
+ const recorderPage = await attachRecorder(page);
+ await recordAction(() => page.goto(`${baseURL}/empty.html`));
+ await page.setContent(`
+
+
+ `);
+ await recordAction(() => page.locator('button').first().click());
+ await extensionServiceWorker.evaluate(async () => {
+ await (globalThis as any).setTestIdAttributeName('data-foobar');
+ });
+ // injected recorder poll period
+ await page.waitForTimeout(1000);
+ await recordAction(() => page.locator('button').nth(1).click());
+
+ await recorderPage.getByTitle('Record').click();
+
+ const code = `const { chromium } = require('playwright');
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: false
+ });
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ await page.goto('${baseURL}/empty.html');
+ await page.getByTestId('btn-testid').click();
+ await page.getByTestId('btn-foobar').click();
+
+ // ---------------------
+ await context.close();
+ await browser.close();
+})();`;
+
+ await expect(recorderPage.locator('.CodeMirror-line')).toHaveText(code.split('\n'));
+});