diff --git a/package-lock.json b/package-lock.json
index c216b1281c..093e08df01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"@types/babel__core": "^7.20.2",
"@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4",
+ "@types/http-proxy": "^1.17.15",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39",
"@types/react": "^18.0.12",
@@ -53,6 +54,7 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"formidable": "^2.1.1",
+ "http-proxy": "^1.18.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1",
"mime": "^3.0.0",
@@ -1831,6 +1833,16 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true
},
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.15",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz",
+ "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/immutable": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/@types/immutable/-/immutable-3.8.7.tgz",
@@ -3908,6 +3920,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -4046,6 +4065,27 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -4527,6 +4567,21 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -6107,6 +6162,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
diff --git a/package.json b/package.json
index f36e9039e3..f019f6eacc 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"@types/babel__core": "^7.20.2",
"@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4",
+ "@types/http-proxy": "^1.17.15",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39",
"@types/react": "^18.0.12",
@@ -92,6 +93,7 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"formidable": "^2.1.1",
+ "http-proxy": "^1.18.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1",
"mime": "^3.0.0",
diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts
index eb56204813..863a283d2f 100644
--- a/tests/playwright-test/ui-mode-trace.spec.ts
+++ b/tests/playwright-test/ui-mode-trace.spec.ts
@@ -16,6 +16,9 @@
import { createImage } from './playwright-test-fixtures';
import { test, expect, retries } from './ui-mode-fixtures';
+import http from 'node:http';
+import httpProxy from 'http-proxy';
+import { ManualPromise } from 'packages/playwright-core/lib/utils/manualPromise';
test.describe.configure({ mode: 'parallel', retries });
@@ -339,3 +342,55 @@ test('should show request source context id', async ({ runUITest, server }) => {
await expect(page.getByText('page#2')).toBeVisible();
await expect(page.getByText('api#1')).toBeVisible();
});
+
+test('should work behind proxy', async ({ runUITest }, testInfo) => {
+ const { page } = await runUITest({
+ 'a.test.ts': `
+ import { test, expect } from '@playwright/test';
+ test('trace test', async ({ page }) => {
+ await page.setContent('');
+ await page.getByRole('button').click();
+ expect(1).toBe(1);
+ });
+ `,
+ });
+
+ const prefix = '/subdir';
+ const origin = new URL(page.url());
+ const proxy = httpProxy.createProxy({ target: { port: origin.port }, ws: true });
+
+ const proxyServer = http.createServer((req, res) => {
+ req.url = req.url!.replace(prefix, '');
+ proxy.web(req, res);
+ });
+ proxyServer.on('upgrade', (req, socket, head) => {
+ req.url = req.url!.replace(prefix, '');
+ proxy.ws(req, socket, head);
+ });
+
+ const proxyPort = 9010 + testInfo.workerIndex * 4;
+ await new Promise(resolve => proxyServer.listen(proxyPort, resolve));
+
+ const proxyURL = new URL(origin);
+ proxyURL.host = `localhost:${proxyPort}`;
+ proxyURL.pathname = prefix + proxyURL.pathname;
+
+ await page.goto(proxyURL.toString());
+
+ await page.getByText('trace test').dblclick();
+
+ await expect(page.getByTestId('actions-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem /Before Hooks \\d+[hmsp]+/
+ - treeitem /page\\.setContent \\d+[hmsp]+/
+ - treeitem /locator\\.clickgetByRole\\('button'\\) \\d+[hmsp]+/
+ - treeitem /expect\\.toBe \\d+[hmsp]+/ [selected]
+ - treeitem /After Hooks \\d+[hmsp]+/
+ `);
+
+ await expect(
+ page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('button'),
+ ).toHaveText('Submit');
+
+ proxyServer.close();
+});