diff --git a/package-lock.json b/package-lock.json
index 6786680f70..6cf68d8766 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,12 +39,14 @@
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
+ "@zip.js/zip.js": "^2.3.17",
"ansi-to-html": "^0.7.1",
"babel-loader": "^8.2.2",
"chokidar": "^3.5.0",
"chromedriver": "^94.0.0",
"commonmark": "^0.29.1",
"concurrently": "^6.2.1",
+ "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.2",
"css-loader": "^5.2.6",
"electron": "^12.2.1",
@@ -940,6 +942,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@gar/promisify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
+ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==",
+ "dev": true
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -1094,6 +1102,74 @@
"node": ">= 8"
}
},
+ "node_modules/@npmcli/fs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.0.0.tgz",
+ "integrity": "sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ==",
+ "dev": true,
+ "dependencies": {
+ "@gar/promisify": "^1.0.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/@npmcli/move-file": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
+ "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
+ "dev": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@playwright/test": {
"resolved": "packages/playwright-test",
"link": true
@@ -1228,9 +1304,9 @@
}
},
"node_modules/@types/json-schema": {
- "version": "7.0.7",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
- "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
+ "version": "7.0.9",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
+ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"node_modules/@types/mime": {
@@ -1915,6 +1991,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "node_modules/@zip.js/zip.js": {
+ "version": "2.3.17",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.3.17.tgz",
+ "integrity": "sha512-ktTJ8dvbiIu4MAlioJo/475QtTsbOBK5YmBbvRJaduCeEKOof/ZTY2H7DPwiC0pC9dDfEo+uFsqMVI1J4HLl5g==",
+ "dev": true
+ },
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -3289,6 +3371,171 @@
"node": ">=0.10.0"
}
},
+ "node_modules/copy-webpack-plugin": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz",
+ "integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==",
+ "dev": true,
+ "dependencies": {
+ "cacache": "^15.0.5",
+ "fast-glob": "^3.2.4",
+ "find-cache-dir": "^3.3.1",
+ "glob-parent": "^5.1.1",
+ "globby": "^11.0.1",
+ "loader-utils": "^2.0.0",
+ "normalize-path": "^3.0.0",
+ "p-limit": "^3.0.2",
+ "schema-utils": "^3.0.0",
+ "serialize-javascript": "^5.0.1",
+ "webpack-sources": "^1.4.3"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.37.0 || ^5.0.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/cacache": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
+ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/fs": "^1.0.0",
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.1",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/loader-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+ "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/schema-utils": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+ "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+ "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/ssri": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
+ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
"node_modules/core-js": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz",
@@ -5083,6 +5330,18 @@
"node": ">=6 <7 || >=8"
}
},
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -7085,6 +7344,79 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
+ "node_modules/minipass": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz",
+ "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
"node_modules/mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
@@ -9642,6 +9974,50 @@
"node": ">=6"
}
},
+ "node_modules/tar": {
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+ "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/tar/node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
"node_modules/tcp-port-used": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
@@ -10927,6 +11303,18 @@
"buffer-crc32": "~0.2.3"
}
},
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"packages/create-playwright": {
"version": "0.1.7",
"license": "MIT",
@@ -10994,6 +11382,9 @@
"bin": {
"playwright": "cli.js"
},
+ "devDependencies": {
+ "@zip.js/zip.js": "^2.3.17"
+ },
"engines": {
"node": ">=12"
}
@@ -11679,6 +12070,12 @@
}
}
},
+ "@gar/promisify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
+ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==",
+ "dev": true
+ },
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -11799,6 +12196,60 @@
"fastq": "^1.6.0"
}
},
+ "@npmcli/fs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.0.0.tgz",
+ "integrity": "sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ==",
+ "dev": true,
+ "requires": {
+ "@gar/promisify": "^1.0.1",
+ "semver": "^7.3.5"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+ },
+ "@npmcli/move-file": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
+ "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ }
+ }
+ },
"@playwright/test": {
"version": "file:packages/playwright-test",
"requires": {
@@ -11971,9 +12422,9 @@
}
},
"@types/json-schema": {
- "version": "7.0.7",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
- "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
+ "version": "7.0.9",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
+ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/mime": {
@@ -12564,6 +13015,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "@zip.js/zip.js": {
+ "version": "2.3.17",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.3.17.tgz",
+ "integrity": "sha512-ktTJ8dvbiIu4MAlioJo/475QtTsbOBK5YmBbvRJaduCeEKOof/ZTY2H7DPwiC0pC9dDfEo+uFsqMVI1J4HLl5g==",
+ "dev": true
+ },
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -13677,6 +14134,129 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true
},
+ "copy-webpack-plugin": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz",
+ "integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==",
+ "dev": true,
+ "requires": {
+ "cacache": "^15.0.5",
+ "fast-glob": "^3.2.4",
+ "find-cache-dir": "^3.3.1",
+ "glob-parent": "^5.1.1",
+ "globby": "^11.0.1",
+ "loader-utils": "^2.0.0",
+ "normalize-path": "^3.0.0",
+ "p-limit": "^3.0.2",
+ "schema-utils": "^3.0.0",
+ "serialize-javascript": "^5.0.1",
+ "webpack-sources": "^1.4.3"
+ },
+ "dependencies": {
+ "cacache": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
+ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
+ "dev": true,
+ "requires": {
+ "@npmcli/fs": "^1.0.0",
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.1",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ }
+ },
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true
+ },
+ "loader-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+ "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "schema-utils": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+ "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ }
+ },
+ "serialize-javascript": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+ "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "ssri": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
+ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.1.1"
+ }
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+ },
"core-js": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz",
@@ -15086,6 +15666,15 @@
"universalify": "^0.1.0"
}
},
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -16600,6 +17189,68 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
+ "minipass": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz",
+ "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+ },
+ "minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+ },
"mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
@@ -17297,6 +17948,7 @@
"playwright-core": {
"version": "file:packages/playwright-core",
"requires": {
+ "@zip.js/zip.js": "^2.3.17",
"commander": "^8.2.0",
"debug": "^4.1.1",
"extract-zip": "^2.0.1",
@@ -18680,6 +19332,40 @@
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
"dev": true
},
+ "tar": {
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+ "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+ },
"tcp-port-used": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
@@ -19706,6 +20392,12 @@
"requires": {
"buffer-crc32": "~0.2.3"
}
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
}
}
}
diff --git a/package.json b/package.json
index 28d1781e5e..0fcaf3ffec 100644
--- a/package.json
+++ b/package.json
@@ -68,12 +68,14 @@
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
+ "@zip.js/zip.js": "^2.3.17",
"ansi-to-html": "^0.7.1",
"babel-loader": "^8.2.2",
"chokidar": "^3.5.0",
"chromedriver": "^94.0.0",
"commonmark": "^0.29.1",
"concurrently": "^6.2.1",
+ "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.2",
"css-loader": "^5.2.6",
"electron": "^12.2.1",
diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json
index 936d72f278..e1a15856d2 100644
--- a/packages/playwright-core/package.json
+++ b/packages/playwright-core/package.json
@@ -40,7 +40,7 @@
"rimraf": "^3.0.2",
"stack-utils": "^2.0.3",
"ws": "^7.4.6",
- "yazl": "^2.5.1",
- "yauzl": "^2.10.0"
+ "yauzl": "^2.10.0",
+ "yazl": "^2.5.1"
}
}
diff --git a/packages/playwright-core/src/server/snapshot/snapshotServer.ts b/packages/playwright-core/src/server/snapshot/snapshotServer.ts
deleted file mode 100644
index 7587276ffb..0000000000
--- a/packages/playwright-core/src/server/snapshot/snapshotServer.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import http from 'http';
-import path from 'path';
-import { HttpServer } from '../../utils/httpServer';
-import type { ResourceSnapshot } from './snapshotTypes';
-import { SnapshotStorage } from './snapshotStorage';
-import type { Point } from '../../common/types';
-import { URLSearchParams } from 'url';
-
-export class SnapshotServer {
- private _snapshotStorage: SnapshotStorage;
-
- constructor(server: HttpServer, snapshotStorage: SnapshotStorage) {
- this._snapshotStorage = snapshotStorage;
-
- server.routePrefix('/snapshot/sw.bundle.js', (request, response) => {
- server.serveFile(response, path.join(__dirname, '..', '..', 'web', 'traceViewer', 'sw.bundle.js'));
- return true;
- });
- server.routePrefix('/snapshot/', this._serveSnapshot.bind(this));
- server.routePrefix('/snapshotSize/', this._serveSnapshotSize.bind(this));
- server.routePrefix('/resources/', this._serveResource.bind(this));
- }
-
- private _serveSnapshotRoot(request: http.IncomingMessage, response: http.ServerResponse): boolean {
- response.statusCode = 200;
- response.setHeader('Cache-Control', 'public, max-age=31536000');
- response.setHeader('Content-Type', 'text/html');
- response.end(`
-
-
-
-
- `);
- return true;
- }
-
- private _serveSnapshot(request: http.IncomingMessage, response: http.ServerResponse): boolean {
- const { pathname, searchParams } = new URL('http://localhost' + request.url);
- if (pathname.endsWith('/snapshot/'))
- return this._serveSnapshotRoot(request, response);
- const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
- this._respondWithJson(response, snapshot ? snapshot.render() : { html: '' });
- return true;
- }
-
- private _serveSnapshotSize(request: http.IncomingMessage, response: http.ServerResponse): boolean {
- const { pathname, searchParams } = new URL('http://localhost' + request.url);
- const snapshot = this._snapshot(pathname.substring('/snapshotSize'.length), searchParams);
- this._respondWithJson(response, snapshot ? snapshot.viewport() : {});
- return true;
- }
-
- private _snapshot(pathname: string, params: URLSearchParams) {
- const name = params.get('name')!;
- return this._snapshotStorage.snapshotByName(pathname.slice(1), name);
- }
-
- private _respondWithJson(response: http.ServerResponse, object: any) {
- response.statusCode = 200;
- response.setHeader('Cache-Control', 'public, max-age=31536000');
- response.setHeader('Content-Type', 'application/json');
- response.end(JSON.stringify(object));
- }
-
- private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean {
- const { frameId, index, url } = JSON.parse(Buffer.from(request.url!.substring('/resources/'.length), 'base64').toString());
- const snapshot = this._snapshotStorage.snapshotByIndex(frameId, index);
- const resource = snapshot?.resourceByUrl(url);
- if (!resource)
- return false;
-
- const sha1 = resource.response.content._sha1;
- if (!sha1)
- return false;
- (async () => {
- this._innerServeResource(sha1, resource, response);
- })().catch(() => {});
- return true;
- }
-
- private async _innerServeResource(sha1: string, resource: ResourceSnapshot, response: http.ServerResponse) {
- const content = await this._snapshotStorage.resourceContent(sha1);
- if (!content) {
- response.statusCode = 404;
- response.end();
- return;
- }
- response.statusCode = 200;
- let contentType = resource.response.content.mimeType;
- const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);
- if (isTextEncoding && !contentType.includes('charset'))
- contentType = `${contentType}; charset=utf-8`;
- response.setHeader('Content-Type', contentType);
- for (const { name, value } of resource.response.headers) {
- try {
- response.setHeader(name, value.split('\n'));
- } catch (e) {
- // Browser is able to handle the header, but Node is not.
- // Swallow the error since we cannot do anything meaningful.
- }
- }
-
- response.removeHeader('Content-Encoding');
- response.removeHeader('Access-Control-Allow-Origin');
- response.setHeader('Access-Control-Allow-Origin', '*');
- response.removeHeader('Content-Length');
- response.setHeader('Content-Length', content.byteLength);
- response.setHeader('Cache-Control', 'public, max-age=31536000');
- response.end(content);
- }
-}
-
-declare global {
- interface Window {
- showSnapshot: (url: string, point?: Point) => Promise;
- }
-}
-function rootScript() {
- if (window.location.href.endsWith('serviceWorkerForTest'))
- navigator.serviceWorker.register('sw.bundle.js');
- let showPromise = Promise.resolve();
- if (!navigator.serviceWorker.controller) {
- showPromise = new Promise(resolve => {
- navigator.serviceWorker.oncontrollerchange = () => resolve();
- });
- }
-
- const pointElement = document.createElement('div');
- pointElement.style.position = 'fixed';
- pointElement.style.backgroundColor = 'red';
- pointElement.style.width = '20px';
- pointElement.style.height = '20px';
- pointElement.style.borderRadius = '10px';
- pointElement.style.margin = '-10px 0 0 -10px';
- pointElement.style.zIndex = '2147483647';
-
- const iframe = document.createElement('iframe');
- document.body.appendChild(iframe);
- (window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => {
- await showPromise;
- iframe.src = url;
- if (options.point) {
- pointElement.style.left = options.point.x + 'px';
- pointElement.style.top = options.point.y + 'px';
- document.documentElement.appendChild(pointElement);
- } else {
- pointElement.remove();
- }
- };
- window.addEventListener('message', event => {
- window.showSnapshot(window.location.href + event.data.snapshotUrl);
- }, false);
-}
diff --git a/packages/playwright-core/src/server/snapshot/snapshotTypes.ts b/packages/playwright-core/src/server/trace/common/snapshotTypes.ts
similarity index 96%
rename from packages/playwright-core/src/server/snapshot/snapshotTypes.ts
rename to packages/playwright-core/src/server/trace/common/snapshotTypes.ts
index 040997917d..3863e613c6 100644
--- a/packages/playwright-core/src/server/snapshot/snapshotTypes.ts
+++ b/packages/playwright-core/src/server/trace/common/snapshotTypes.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Entry as HAREntry } from '../supplements/har/har';
+import { Entry as HAREntry } from '../../supplements/har/har';
export type ResourceSnapshot = HAREntry;
diff --git a/packages/playwright-core/src/server/trace/common/traceEvents.ts b/packages/playwright-core/src/server/trace/common/traceEvents.ts
index 8bb3a21365..7dbf3a70a7 100644
--- a/packages/playwright-core/src/server/trace/common/traceEvents.ts
+++ b/packages/playwright-core/src/server/trace/common/traceEvents.ts
@@ -14,15 +14,24 @@
* limitations under the License.
*/
-import { CallMetadata } from '../../instrumentation';
-import { FrameSnapshot, ResourceSnapshot } from '../../snapshot/snapshotTypes';
-import { BrowserContextOptions } from '../../types';
+import type { Size } from '../../../common/types';
+import type { CallMetadata } from '../../instrumentation';
+import type { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
+
+export const VERSION = 3;
+
+export type BrowserContextEventOptions = {
+ viewport?: Size,
+ deviceScaleFactor?: number,
+ isMobile?: boolean,
+ _debugName?: string,
+};
export type ContextCreatedTraceEvent = {
version: number,
type: 'context-options',
browserName: string,
- options: BrowserContextOptions
+ options: BrowserContextEventOptions
};
export type ScreencastFrameTraceEvent = {
diff --git a/packages/playwright-core/src/server/snapshot/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts
similarity index 93%
rename from packages/playwright-core/src/server/snapshot/snapshotter.ts
rename to packages/playwright-core/src/server/trace/recorder/snapshotter.ts
index b38fdb1738..fe78974b2e 100644
--- a/packages/playwright-core/src/server/snapshot/snapshotter.ts
+++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts
@@ -14,15 +14,15 @@
* limitations under the License.
*/
-import { BrowserContext } from '../browserContext';
-import { Page } from '../page';
-import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
-import { debugLogger } from '../../utils/debugLogger';
-import { Frame } from '../frames';
+import { BrowserContext } from '../../browserContext';
+import { Page } from '../../page';
+import { eventsHelper, RegisteredListener } from '../../../utils/eventsHelper';
+import { debugLogger } from '../../../utils/debugLogger';
+import { Frame } from '../../frames';
import { frameSnapshotStreamer, SnapshotData } from './snapshotterInjected';
-import { calculateSha1, createGuid, monotonicTime } from '../../utils/utils';
-import { FrameSnapshot } from './snapshotTypes';
-import { ElementHandle } from '../dom';
+import { calculateSha1, createGuid, monotonicTime } from '../../../utils/utils';
+import { FrameSnapshot } from '../common/snapshotTypes';
+import { ElementHandle } from '../../dom';
import * as mime from 'mime';
export type SnapshotterBlob = {
diff --git a/packages/playwright-core/src/server/snapshot/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
similarity index 99%
rename from packages/playwright-core/src/server/snapshot/snapshotterInjected.ts
rename to packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
index fb858a9195..66b3a74aed 100644
--- a/packages/playwright-core/src/server/snapshot/snapshotterInjected.ts
+++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { NodeSnapshot } from './snapshotTypes';
+import { NodeSnapshot } from '../common/snapshotTypes';
export type SnapshotData = {
doctype?: string,
diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts
index cd79015903..3c0a549b09 100644
--- a/packages/playwright-core/src/server/trace/recorder/tracing.ts
+++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts
@@ -27,10 +27,11 @@ import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrume
import { Page } from '../../page';
import * as trace from '../common/traceEvents';
import { commandsWithTracingSnapshots } from '../../../protocol/channels';
-import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from '../../snapshot/snapshotter';
-import { FrameSnapshot } from '../../snapshot/snapshotTypes';
+import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
+import { FrameSnapshot } from '../common/snapshotTypes';
import { HarTracer, HarTracerDelegate } from '../../supplements/har/harTracer';
import * as har from '../../supplements/har/har';
+import { VERSION } from '../common/traceEvents';
export type TracerOptions = {
name?: string;
@@ -38,8 +39,6 @@ export type TracerOptions = {
screenshots?: boolean;
};
-export const VERSION = 3;
-
type RecordingState = {
options: TracerOptions,
traceName: string,
diff --git a/packages/playwright-core/src/server/trace/viewer/traceModel.ts b/packages/playwright-core/src/server/trace/viewer/traceModel.ts
deleted file mode 100644
index 234f6e19d6..0000000000
--- a/packages/playwright-core/src/server/trace/viewer/traceModel.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import * as trace from '../common/traceEvents';
-import { ResourceSnapshot } from '../../snapshot/snapshotTypes';
-import { BaseSnapshotStorage } from '../../snapshot/snapshotStorage';
-import { BrowserContextOptions } from '../../types';
-import { shouldCaptureSnapshot, VERSION } from '../recorder/tracing';
-import { VirtualFileSystem } from '../../../utils/vfs';
-export * as trace from '../common/traceEvents';
-
-export class TraceModel {
- contextEntry: ContextEntry;
- pageEntries = new Map();
- private _snapshotStorage: PersistentSnapshotStorage;
- private _version: number | undefined;
-
- constructor(snapshotStorage: PersistentSnapshotStorage) {
- this._snapshotStorage = snapshotStorage;
- this.contextEntry = {
- startTime: Number.MAX_VALUE,
- endTime: Number.MIN_VALUE,
- browserName: '',
- options: { },
- pages: [],
- resources: [],
- };
- }
-
- build() {
- for (const page of this.contextEntry!.pages)
- page.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
- this.contextEntry!.resources = this._snapshotStorage.resources();
- }
-
- private _pageEntry(pageId: string): PageEntry {
- let pageEntry = this.pageEntries.get(pageId);
- if (!pageEntry) {
- pageEntry = {
- actions: [],
- events: [],
- objects: {},
- screencastFrames: [],
- };
- this.pageEntries.set(pageId, pageEntry);
- this.contextEntry.pages.push(pageEntry);
- }
- return pageEntry;
- }
-
- appendEvent(line: string) {
- const event = this._modernize(JSON.parse(line));
- switch (event.type) {
- case 'context-options': {
- this._version = event.version || 0;
- this.contextEntry.browserName = event.browserName;
- this.contextEntry.options = event.options;
- break;
- }
- case 'screencast-frame': {
- this._pageEntry(event.pageId).screencastFrames.push(event);
- break;
- }
- case 'action': {
- const metadata = event.metadata;
- const include = event.hasSnapshot;
- if (include && metadata.pageId)
- this._pageEntry(metadata.pageId).actions.push(event);
- break;
- }
- case 'event': {
- const metadata = event.metadata;
- if (metadata.pageId) {
- if (metadata.method === '__create__')
- this._pageEntry(metadata.pageId).objects[metadata.params.guid] = metadata.params.initializer;
- else
- this._pageEntry(metadata.pageId).events.push(event);
- }
- break;
- }
- case 'resource-snapshot':
- this._snapshotStorage.addResource(event.snapshot);
- break;
- case 'frame-snapshot':
- this._snapshotStorage.addFrameSnapshot(event.snapshot);
- break;
- }
- if (event.type === 'action' || event.type === 'event') {
- this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.metadata.startTime);
- this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.metadata.endTime);
- }
- }
-
- private _modernize(event: any): trace.TraceEvent {
- if (this._version === undefined)
- return event;
- for (let version = this._version; version < VERSION; ++version)
- event = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, event);
- return event;
- }
-
- _modernize_0_to_1(event: any): any {
- if (event.type === 'action') {
- if (typeof event.metadata.error === 'string')
- event.metadata.error = { error: { name: 'Error', message: event.metadata.error } };
- if (event.metadata && typeof event.hasSnapshot !== 'boolean')
- event.hasSnapshot = shouldCaptureSnapshot(event.metadata);
- }
- return event;
- }
-
- _modernize_1_to_2(event: any): any {
- if (event.type === 'frame-snapshot' && event.snapshot.isMainFrame) {
- // Old versions had completely wrong viewport.
- event.snapshot.viewport = this.contextEntry.options.viewport || { width: 1280, height: 720 };
- }
- return event;
- }
-
- _modernize_2_to_3(event: any): any {
- if (event.type === 'resource-snapshot' && !event.snapshot.request) {
- // Migrate from old ResourceSnapshot to new har entry format.
- const resource = event.snapshot;
- event.snapshot = {
- _frameref: resource.frameId,
- request: {
- url: resource.url,
- method: resource.method,
- headers: resource.requestHeaders,
- postData: resource.requestSha1 ? { _sha1: resource.requestSha1 } : undefined,
- },
- response: {
- status: resource.status,
- headers: resource.responseHeaders,
- content: {
- mimeType: resource.contentType,
- _sha1: resource.responseSha1,
- },
- },
- _monotonicTime: resource.timestamp,
- };
- }
- return event;
- }
-}
-
-export type ContextEntry = {
- startTime: number;
- endTime: number;
- browserName: string;
- options: BrowserContextOptions;
- pages: PageEntry[];
- resources: ResourceSnapshot[];
-};
-
-export type PageEntry = {
- actions: trace.ActionTraceEvent[];
- events: trace.ActionTraceEvent[];
- objects: { [key: string]: any };
- screencastFrames: {
- sha1: string,
- timestamp: number,
- width: number,
- height: number,
- }[];
-};
-
-export class PersistentSnapshotStorage extends BaseSnapshotStorage {
- private _loader: VirtualFileSystem;
- constructor(loader: VirtualFileSystem) {
- super();
- this._loader = loader;
- }
-
- async resourceContent(sha1: string): Promise {
- return this._loader.read('resources/' + sha1);
- }
-}
diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
index 54813458e2..96cc10bd0c 100644
--- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
+++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
@@ -14,199 +14,69 @@
* limitations under the License.
*/
-import fs from 'fs';
-import readline from 'readline';
-import os from 'os';
import path from 'path';
-import rimraf from 'rimraf';
-import stream from 'stream';
-import { createPlaywright } from '../../playwright';
-import { PersistentSnapshotStorage, TraceModel } from './traceModel';
-import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
-import { SnapshotServer } from '../../snapshot/snapshotServer';
import * as consoleApiSource from '../../../generated/consoleApiSource';
-import { isUnderTest, download } from '../../../utils/utils';
-import { internalCallMetadata } from '../../instrumentation';
-import { ProgressController } from '../../progress';
-import { BrowserContext } from '../../browserContext';
+import { HttpServer } from '../../../utils/httpServer';
import { findChromiumChannel } from '../../../utils/registry';
+import { isUnderTest } from '../../../utils/utils';
+import { BrowserContext } from '../../browserContext';
import { installAppIcon } from '../../chromium/crApp';
-import { debugLogger } from '../../../utils/debugLogger';
-import { VirtualFileSystem, RealFileSystem, ZipFileSystem } from '../../../utils/vfs';
+import { internalCallMetadata } from '../../instrumentation';
+import { createPlaywright } from '../../playwright';
+import { ProgressController } from '../../progress';
-export class TraceViewer {
- private _vfs: VirtualFileSystem;
- private _server: HttpServer;
- private _browserName: string;
-
- constructor(vfs: VirtualFileSystem, browserName: string) {
- this._vfs = vfs;
- this._browserName = browserName;
- this._server = new HttpServer();
- }
-
- async init() {
- // Served by TraceServer
- // - "/tracemodel" - json with trace model.
- //
- // Served by TraceViewer
- // - "/" - our frontend.
- // - "/file?filePath" - local files, used by sources tab.
- // - "/sha1/" - trace resource bodies, used by network previews.
- //
- // Served by SnapshotServer
- // - "/resources/" - network resources from the trace.
- // - "/snapshot/" - root for snapshot frame.
- // - "/snapshot/pageId/..." - actual snapshot html.
- // and translates them into network requests.
- const entries = await this._vfs.entries();
- const debugNames = entries.filter(name => name.endsWith('.trace')).map(name => {
- return name.substring(0, name.indexOf('.trace'));
- });
-
- const traceListHandler: ServerRouteHandler = (request, response) => {
- response.statusCode = 200;
- response.setHeader('Content-Type', 'application/json');
- response.end(JSON.stringify(debugNames));
- return true;
- };
- this._server.routePath('/contexts', traceListHandler);
- const snapshotStorage = new PersistentSnapshotStorage(this._vfs);
- new SnapshotServer(this._server, snapshotStorage);
-
- const traceModelHandler: ServerRouteHandler = (request, response) => {
- const debugName = request.url!.substring('/context/'.length);
- snapshotStorage.clear();
- response.statusCode = 200;
- response.setHeader('Content-Type', 'application/json');
- (async () => {
- const traceFile = await this._vfs.readStream(debugName + '.trace');
- const match = debugName.match(/^(.*)-\d+$/);
- const networkFile = await this._vfs.readStream((match ? match[1] : debugName) + '.network').catch(() => undefined);
- const model = new TraceModel(snapshotStorage);
- await appendTraceEvents(model, traceFile);
- if (networkFile)
- await appendTraceEvents(model, networkFile);
- model.build();
- response.end(JSON.stringify(model.contextEntry));
- })().catch(e => console.error(e));
- return true;
- };
- this._server.routePrefix('/context/', traceModelHandler);
-
- const fileHandler: ServerRouteHandler = (request, response) => {
- try {
- const url = new URL('http://localhost' + request.url!);
- const search = url.search;
- if (search[0] !== '?')
- return false;
- return this._server.serveFile(response, search.substring(1));
- } catch (e) {
- return false;
- }
- };
- this._server.routePath('/file', fileHandler);
-
- const sha1Handler: ServerRouteHandler = (request, response) => {
- const sha1 = request.url!.substring('/sha1/'.length);
- if (sha1.includes('/'))
- return false;
- this._server.serveVirtualFile(response, this._vfs, 'resources/' + sha1).catch(() => {});
- return true;
- };
- this._server.routePrefix('/sha1/', sha1Handler);
-
- const traceViewerHandler: ServerRouteHandler = (request, response) => {
- const relativePath = request.url!;
- const absolutePath = path.join(__dirname, '..', '..', '..', 'web', 'traceViewer', ...relativePath.split('/'));
- return this._server.serveFile(response, absolutePath);
- };
- this._server.routePrefix('/', traceViewerHandler);
- }
-
- async show(headless: boolean): Promise {
- const urlPrefix = await this._server.start();
-
- const traceViewerPlaywright = createPlaywright('javascript', true);
- const traceViewerBrowser = isUnderTest() ? 'chromium' : this._browserName;
- const args = traceViewerBrowser === 'chromium' ? [
- '--app=data:text/html,',
- '--window-size=1280,800'
- ] : [];
- if (isUnderTest())
- args.push(`--remote-debugging-port=0`);
-
- const context = await traceViewerPlaywright[traceViewerBrowser as 'chromium'].launchPersistentContext(internalCallMetadata(), '', {
- // TODO: store language in the trace.
- channel: findChromiumChannel(traceViewerPlaywright.options.sdkLanguage),
- args,
- noDefaultViewport: true,
- headless,
- useWebSocket: isUnderTest()
- });
-
- const controller = new ProgressController(internalCallMetadata(), context._browser);
- await controller.run(async progress => {
- await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
- });
- await context.extendInjectedScript(consoleApiSource.source);
- const [page] = context.pages();
-
- if (traceViewerBrowser === 'chromium')
- await installAppIcon(page);
-
- if (isUnderTest())
- page.on('close', () => context.close(internalCallMetadata()).catch(() => {}));
- else
- page.on('close', () => process.exit());
-
- await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/index.html');
- return context;
- }
-}
-
-async function appendTraceEvents(model: TraceModel, input: stream.Readable) {
- const rl = readline.createInterface({
- input,
- crlfDelay: Infinity
- });
- for await (const line of rl as any)
- model.appendEvent(line);
-}
-
-export async function showTraceViewer(tracePath: string, browserName: string, headless = false): Promise {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`));
- process.on('exit', () => rimraf.sync(dir));
-
- if (/^https?:\/\//i.test(tracePath)){
- const downloadZipPath = path.join(dir, 'trace.zip');
+export async function showTraceViewer(traceUrl: string, browserName: string, headless = false): Promise {
+ const server = new HttpServer();
+ server.routePath('/file', (request, response) => {
try {
- await download(tracePath, downloadZipPath, {
- progressBarName: tracePath,
- log: debugLogger.log.bind(debugLogger, 'download')
- });
- } catch (error) {
- console.log(`${error?.message || ''}`); // eslint-disable-line no-console
- return;
+ const path = new URL('http://localhost' + request.url!).searchParams.get('path')!;
+ return server.serveFile(response, path);
+ } catch (e) {
+ return false;
}
- tracePath = downloadZipPath;
- }
+ });
- let stat;
- try {
- stat = fs.statSync(tracePath);
- } catch (e) {
- console.log(`No such file or directory: ${tracePath}`); // eslint-disable-line no-console
- return;
- }
+ server.routePrefix('/', (request, response) => {
+ const relativePath = new URL('http://localhost' + request.url!).pathname;
+ const absolutePath = path.join(__dirname, '..', '..', '..', 'web', 'traceViewer', ...relativePath.split('/'));
+ return server.serveFile(response, absolutePath);
+ });
- if (stat.isDirectory()) {
- const traceViewer = new TraceViewer(new RealFileSystem(tracePath), browserName);
- await traceViewer.init();
- return await traceViewer.show(headless);
- }
+ const urlPrefix = await server.start();
- const traceViewer = new TraceViewer(new ZipFileSystem(tracePath), browserName);
- await traceViewer.init();
- return await traceViewer.show(headless);
+ const traceViewerPlaywright = createPlaywright('javascript', true);
+ const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
+ const args = traceViewerBrowser === 'chromium' ? [
+ '--app=data:text/html,',
+ '--window-size=1280,800'
+ ] : [];
+ if (isUnderTest())
+ args.push(`--remote-debugging-port=0`);
+
+ const context = await traceViewerPlaywright[traceViewerBrowser as 'chromium'].launchPersistentContext(internalCallMetadata(), '', {
+ // TODO: store language in the trace.
+ channel: findChromiumChannel(traceViewerPlaywright.options.sdkLanguage),
+ args,
+ noDefaultViewport: true,
+ headless,
+ useWebSocket: isUnderTest()
+ });
+
+ const controller = new ProgressController(internalCallMetadata(), context._browser);
+ await controller.run(async progress => {
+ await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
+ });
+ await context.extendInjectedScript(consoleApiSource.source);
+ const [page] = context.pages();
+
+ if (traceViewerBrowser === 'chromium')
+ await installAppIcon(page);
+
+ if (isUnderTest())
+ page.on('close', () => context.close(internalCallMetadata()).catch(() => {}));
+ else
+ page.on('close', () => process.exit());
+
+ await page.mainFrame().goto(internalCallMetadata(), urlPrefix + `/index.html?trace=${traceUrl}`);
+ return context;
}
diff --git a/packages/playwright-core/src/server/snapshot/inMemorySnapshotter.ts b/packages/playwright-core/src/web/traceViewer/inMemorySnapshotter.ts
similarity index 79%
rename from packages/playwright-core/src/server/snapshot/inMemorySnapshotter.ts
rename to packages/playwright-core/src/web/traceViewer/inMemorySnapshotter.ts
index 9644eb279b..5b32775b80 100644
--- a/packages/playwright-core/src/server/snapshot/inMemorySnapshotter.ts
+++ b/packages/playwright-core/src/web/traceViewer/inMemorySnapshotter.ts
@@ -14,37 +14,31 @@
* limitations under the License.
*/
-import { HttpServer } from '../../utils/httpServer';
-import { BrowserContext } from '../browserContext';
+import { BrowserContext } from '../../server/browserContext';
import { eventsHelper } from '../../utils/eventsHelper';
-import { Page } from '../page';
-import { FrameSnapshot } from './snapshotTypes';
+import { Page } from '../../server/page';
+import { FrameSnapshot } from '../../server/trace/common/snapshotTypes';
import { SnapshotRenderer } from './snapshotRenderer';
-import { SnapshotServer } from './snapshotServer';
import { BaseSnapshotStorage } from './snapshotStorage';
-import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
-import { ElementHandle } from '../dom';
-import { HarTracer, HarTracerDelegate } from '../supplements/har/harTracer';
-import * as har from '../supplements/har/har';
+import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from '../../server/trace/recorder/snapshotter';
+import { ElementHandle } from '../../server/dom';
+import { HarTracer, HarTracerDelegate } from '../../server/supplements/har/harTracer';
+import * as har from '../../server/supplements/har/har';
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate, HarTracerDelegate {
private _blobs = new Map();
- private _server: HttpServer;
private _snapshotter: Snapshotter;
private _harTracer: HarTracer;
constructor(context: BrowserContext) {
super();
- this._server = new HttpServer();
- new SnapshotServer(this._server, this);
this._snapshotter = new Snapshotter(context, this);
this._harTracer = new HarTracer(context, this, { content: 'sha1', waitForContentOnStop: false, skipScripts: true });
}
- async initialize(): Promise {
+ async initialize(): Promise {
await this._snapshotter.start();
this._harTracer.start();
- return await this._server.start();
}
async reset() {
@@ -59,7 +53,6 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
this._snapshotter.dispose();
await this._harTracer.flush();
this._harTracer.stop();
- await this._server.stop();
}
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle): Promise {
@@ -96,7 +89,11 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
this.addFrameSnapshot(snapshot);
}
- async resourceContent(sha1: string): Promise {
+ async resourceContent(sha1: string): Promise {
+ throw new Error('Not implemented');
+ }
+
+ async resourceContentForTest(sha1: string): Promise {
return this._blobs.get(sha1);
}
}
diff --git a/packages/playwright-core/src/web/traceViewer/index.tsx b/packages/playwright-core/src/web/traceViewer/index.tsx
index 5ee2fcdb29..d2b3f6254e 100644
--- a/packages/playwright-core/src/web/traceViewer/index.tsx
+++ b/packages/playwright-core/src/web/traceViewer/index.tsx
@@ -24,6 +24,10 @@ import '../common.css';
(async () => {
applyTheme();
navigator.serviceWorker.register('sw.bundle.js');
- const debugNames = await fetch('/contexts').then(response => response.json());
- ReactDOM.render(, document.querySelector('#root'));
+ if (!navigator.serviceWorker.controller) {
+ await new Promise(f => {
+ navigator.serviceWorker.oncontrollerchange = () => f();
+ });
+ }
+ ReactDOM.render(, document.querySelector('#root'));
})();
diff --git a/packages/playwright-core/src/server/snapshot/snapshotRenderer.ts b/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts
similarity index 98%
rename from packages/playwright-core/src/server/snapshot/snapshotRenderer.ts
rename to packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts
index 16f051606a..e0b93d1a49 100644
--- a/packages/playwright-core/src/server/snapshot/snapshotRenderer.ts
+++ b/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-import { FrameSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot } from './snapshotTypes';
+import { FrameSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
export class SnapshotRenderer {
private _snapshots: FrameSnapshot[];
private _index: number;
readonly snapshotName: string | undefined;
- private _resources: ResourceSnapshot[];
+ _resources: ResourceSnapshot[];
private _snapshot: FrameSnapshot;
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) {
diff --git a/packages/playwright-core/src/web/traceViewer/snapshotServer.ts b/packages/playwright-core/src/web/traceViewer/snapshotServer.ts
new file mode 100644
index 0000000000..bb2eb0fdde
--- /dev/null
+++ b/packages/playwright-core/src/web/traceViewer/snapshotServer.ts
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
+import { SnapshotStorage } from './snapshotStorage';
+import type { Point } from '../../common/types';
+import { URLSearchParams } from 'url';
+import { SnapshotRenderer } from './snapshotRenderer';
+
+const kBlobUrlPrefix = 'http://playwright.bloburl/#';
+
+export class SnapshotServer {
+ private _snapshotStorage: SnapshotStorage;
+ private _snapshotIds = new Map();
+
+ constructor(snapshotStorage: SnapshotStorage) {
+ this._snapshotStorage = snapshotStorage;
+ }
+
+ static serveSnapshotRoot(): Response {
+ return new Response(`
+
+
+
+
+ `, {
+ status: 200,
+ headers: {
+ 'Cache-Control': 'public, max-age=31536000',
+ 'Content-Type': 'text/html'
+ }
+ });
+ }
+
+ serveSnapshot(pathname: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
+ const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
+ if (!snapshot)
+ return new Response(null, { status: 404 });
+ const renderedSnapshot = snapshot.render();
+ this._snapshotIds.set(snapshotUrl, snapshot);
+ return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html' } });
+ }
+
+ serveSnapshotSize(pathname: string, searchParams: URLSearchParams): Response {
+ const snapshot = this._snapshot(pathname.substring('/snapshotSize'.length), searchParams);
+ return this._respondWithJson(snapshot ? snapshot.viewport() : {});
+ }
+
+ private _snapshot(pathname: string, params: URLSearchParams) {
+ const name = params.get('name')!;
+ return this._snapshotStorage.snapshotByName(pathname.slice(1), name);
+ }
+
+ private _respondWithJson(object: any): Response {
+ return new Response(JSON.stringify(object), {
+ status: 200,
+ headers: {
+ 'Cache-Control': 'public, max-age=31536000',
+ 'Content-Type': 'application/json'
+ }
+ });
+ }
+
+ async serveResource(requestUrl: string, snapshotUrl: string): Promise {
+ const snapshot = this._snapshotIds.get(snapshotUrl)!;
+ const url = requestUrl.startsWith(kBlobUrlPrefix) ? requestUrl.substring(kBlobUrlPrefix.length) : removeHash(requestUrl);
+ const resource = snapshot?.resourceByUrl(url);
+ if (!resource)
+ return new Response(null, { status: 404 });
+
+ const sha1 = resource.response.content._sha1;
+ if (!sha1)
+ return new Response(null, { status: 404 });
+ return this._innerServeResource(sha1, resource);
+ }
+
+ private async _innerServeResource(sha1: string, resource: ResourceSnapshot): Promise {
+ const content = await this._snapshotStorage.resourceContent(sha1);
+ if (!content)
+ return new Response(null, { status: 404 });
+
+ let contentType = resource.response.content.mimeType;
+ const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);
+ if (isTextEncoding && !contentType.includes('charset'))
+ contentType = `${contentType}; charset=utf-8`;
+
+ const headers = new Headers();
+ headers.set('Content-Type', contentType);
+ for (const { name, value } of resource.response.headers)
+ headers.set(name, value);
+ headers.delete('Content-Encoding');
+ headers.delete('Access-Control-Allow-Origin');
+ headers.set('Access-Control-Allow-Origin', '*');
+ headers.delete('Content-Length');
+ headers.set('Content-Length', String(content.size));
+ headers.set('Cache-Control', 'public, max-age=31536000');
+ return new Response(content, { headers });
+ }
+}
+
+declare global {
+ interface Window {
+ showSnapshot: (url: string, point?: Point) => Promise;
+ }
+}
+
+function rootScript() {
+ const pointElement = document.createElement('div');
+ pointElement.style.position = 'fixed';
+ pointElement.style.backgroundColor = 'red';
+ pointElement.style.width = '20px';
+ pointElement.style.height = '20px';
+ pointElement.style.borderRadius = '10px';
+ pointElement.style.margin = '-10px 0 0 -10px';
+ pointElement.style.zIndex = '2147483647';
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ (window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => {
+ iframe.src = url;
+ if (options.point) {
+ pointElement.style.left = options.point.x + 'px';
+ pointElement.style.top = options.point.y + 'px';
+ document.documentElement.appendChild(pointElement);
+ } else {
+ pointElement.remove();
+ }
+ };
+ window.addEventListener('message', event => {
+ window.showSnapshot(window.location.href + event.data.snapshotUrl);
+ }, false);
+}
+
+function removeHash(url: string) {
+ try {
+ const u = new URL(url);
+ u.hash = '';
+ return u.toString();
+ } catch (e) {
+ return url;
+ }
+}
diff --git a/packages/playwright-core/src/server/snapshot/snapshotStorage.ts b/packages/playwright-core/src/web/traceViewer/snapshotStorage.ts
similarity index 91%
rename from packages/playwright-core/src/server/snapshot/snapshotStorage.ts
rename to packages/playwright-core/src/web/traceViewer/snapshotStorage.ts
index 5af4cfcd33..3f3310fb72 100644
--- a/packages/playwright-core/src/server/snapshot/snapshotStorage.ts
+++ b/packages/playwright-core/src/web/traceViewer/snapshotStorage.ts
@@ -15,12 +15,12 @@
*/
import { EventEmitter } from 'events';
-import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
+import { FrameSnapshot, ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
import { SnapshotRenderer } from './snapshotRenderer';
export interface SnapshotStorage {
resources(): ResourceSnapshot[];
- resourceContent(sha1: string): Promise;
+ resourceContent(sha1: string): Promise;
snapshotByName(pageOrFrameId: string, snapshotName: string): SnapshotRenderer | undefined;
snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined;
}
@@ -58,7 +58,7 @@ export abstract class BaseSnapshotStorage extends EventEmitter implements Snapsh
this.emit('snapshot', renderer);
}
- abstract resourceContent(sha1: string): Promise;
+ abstract resourceContent(sha1: string): Promise;
resources(): ResourceSnapshot[] {
return this._resources.slice();
@@ -73,5 +73,4 @@ export abstract class BaseSnapshotStorage extends EventEmitter implements Snapsh
const snapshot = this._frameSnapshots.get(frameId);
return snapshot?.renderer[index];
}
-
}
diff --git a/packages/playwright-core/src/web/traceViewer/sw.ts b/packages/playwright-core/src/web/traceViewer/sw.ts
index d0aa2ab75e..3811c3bb0e 100644
--- a/packages/playwright-core/src/web/traceViewer/sw.ts
+++ b/packages/playwright-core/src/web/traceViewer/sw.ts
@@ -14,75 +14,67 @@
* limitations under the License.
*/
-import type { RenderedFrameSnapshot } from '../../server/snapshot/snapshotTypes';
+import { SnapshotServer } from './snapshotServer';
+import { TraceModel } from './traceModel';
// @ts-ignore
declare const self: ServiceWorkerGlobalScope;
-const kBlobUrlPrefix = 'http://playwright.bloburl/#';
-const snapshotIds = new Map();
-
-self.addEventListener('install', function(event: any) {
-});
+self.addEventListener('install', function(event: any) {});
self.addEventListener('activate', function(event: any) {
event.waitUntil(self.clients.claim());
});
-function respondNotAvailable(): Response {
- return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } });
+let traceModel: TraceModel | undefined;
+let snapshotServer: SnapshotServer | undefined;
+
+async function loadTrace(trace: string): Promise {
+ const traceModel = new TraceModel();
+ const url = trace.startsWith('http') ? trace : `/file?path=${trace}`;
+ await traceModel.load(url);
+ return traceModel;
}
-function removeHash(url: string) {
- try {
- const u = new URL(url);
- u.hash = '';
- return u.toString();
- } catch (e) {
- return url;
- }
-}
-
-async function doFetch(event: any /* FetchEvent */): Promise {
+// @ts-ignore
+async function doFetch(event: FetchEvent): Promise {
const request = event.request;
- const pathname = new URL(request.url).pathname;
- const isSnapshotUrl = pathname !== '/snapshot/' && pathname.startsWith('/snapshot/');
- if (request.url.startsWith(self.location.origin) && !isSnapshotUrl)
- return fetch(event.request);
-
+ const { pathname, searchParams } = new URL(request.url);
const snapshotUrl = request.mode === 'navigate' ?
request.url : (await self.clients.get(event.clientId))!.url;
- if (request.mode === 'navigate') {
- const htmlResponse = await fetch(request);
- const { html, frameId, index }: RenderedFrameSnapshot = await htmlResponse.json();
- if (!html)
- return respondNotAvailable();
- snapshotIds.set(snapshotUrl, { frameId, index });
- const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } });
- return response;
+ if (request.url.startsWith(self.location.origin)) {
+ if (pathname === '/context') {
+ const trace = searchParams.get('trace')!;
+ traceModel = await loadTrace(trace);
+ snapshotServer = new SnapshotServer(traceModel.storage());
+ return new Response(JSON.stringify(traceModel!.contextEntry), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ if (pathname === '/snapshot/')
+ return SnapshotServer.serveSnapshotRoot();
+ if (pathname.startsWith('/snapshotSize/'))
+ return snapshotServer!.serveSnapshotSize(pathname, searchParams);
+ if (pathname.startsWith('/snapshot/'))
+ return snapshotServer!.serveSnapshot(pathname, searchParams, snapshotUrl);
+ if (pathname.startsWith('/sha1/')) {
+ const blob = await traceModel!.resourceForSha1(pathname.slice('/sha1/'.length));
+ if (blob)
+ return new Response(blob, { status: 200 });
+ else
+ return new Response(null, { status: 404 });
+ }
+ return fetch(event.request);
}
- const { frameId, index } = snapshotIds.get(snapshotUrl)!;
- const url = request.url.startsWith(kBlobUrlPrefix) ? request.url.substring(kBlobUrlPrefix.length) : removeHash(request.url);
- const complexUrl = btoa(JSON.stringify({ frameId, index, url }));
- const fetchUrl = `/resources/${complexUrl}`;
- const fetchedResponse = await fetch(fetchUrl);
- // We make a copy of the response, instead of just forwarding,
- // so that response url is not inherited as "/resources/...", but instead
- // as the original request url.
-
- // Response url turns into resource base uri that is used to resolve
- // relative links, e.g. url(/foo/bar) in style sheets.
- const headers = new Headers(fetchedResponse.headers);
- const response = new Response(fetchedResponse.body, {
- status: fetchedResponse.status,
- statusText: fetchedResponse.statusText,
- headers,
- });
- return response;
+ if (!snapshotServer)
+ return new Response(null, { status: 404 });
+ return snapshotServer!.serveResource(request.url, snapshotUrl);
}
-self.addEventListener('fetch', function(event: any) {
+// @ts-ignore
+self.addEventListener('fetch', function(event: FetchEvent) {
event.respondWith(doFetch(event));
});
diff --git a/packages/playwright-core/src/web/traceViewer/traceModel.ts b/packages/playwright-core/src/web/traceViewer/traceModel.ts
new file mode 100644
index 0000000000..608d67edeb
--- /dev/null
+++ b/packages/playwright-core/src/web/traceViewer/traceModel.ts
@@ -0,0 +1,334 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as trace from '../../server/trace/common/traceEvents';
+import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
+import { BaseSnapshotStorage } from './snapshotStorage';
+
+import type zip from '@zip.js/zip.js';
+// @ts-ignore
+self.importScripts('zip.min.js');
+
+const zipjs = (self as any).zip;
+
+export class TraceModel {
+ contextEntry: ContextEntry;
+ pageEntries = new Map();
+ private _snapshotStorage: PersistentSnapshotStorage | undefined;
+ private _entries = new Map();
+ private _version: number | undefined;
+
+ constructor() {
+ this.contextEntry = {
+ startTime: Number.MAX_VALUE,
+ endTime: Number.MIN_VALUE,
+ browserName: '',
+ options: { },
+ pages: [],
+ resources: [],
+ };
+ }
+
+ async load(traceURL: string) {
+ const response = await fetch(traceURL);
+ const blob = await response.blob();
+ const zipReader = new zipjs.ZipReader(new zipjs.BlobReader(blob), { useWebWorkers: false }) as zip.ZipReader;
+ let traceEntry: zip.Entry | undefined;
+ let networkEntry: zip.Entry | undefined;
+ for (const entry of await zipReader.getEntries()) {
+ if (entry.filename.endsWith('.trace'))
+ traceEntry = entry;
+ if (entry.filename.endsWith('.network'))
+ networkEntry = entry;
+ this._entries.set(entry.filename, entry);
+ }
+ this._snapshotStorage = new PersistentSnapshotStorage(this._entries);
+
+ const traceWriter = new zipjs.TextWriter() as zip.TextWriter;
+ await traceEntry!.getData!(traceWriter);
+ for (const line of (await traceWriter.getData()).split('\n'))
+ this.appendEvent(line);
+
+ if (networkEntry) {
+ const networkWriter = new zipjs.TextWriter();
+ await networkEntry.getData!(networkWriter);
+ for (const line of (await networkWriter.getData()).split('\n'))
+ this.appendEvent(line);
+ }
+ this._build();
+ }
+
+ async resourceForSha1(sha1: string): Promise {
+ const entry = this._entries.get('resources/' + sha1);
+ if (!entry)
+ return;
+ const blobWriter = new zipjs.BlobWriter() as zip.BlobWriter;
+ await entry!.getData!(blobWriter);
+ return await blobWriter.getData();
+ }
+
+ storage(): PersistentSnapshotStorage {
+ return this._snapshotStorage!;
+ }
+
+ private _build() {
+ for (const page of this.contextEntry!.pages)
+ page.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
+ this.contextEntry!.resources = this._snapshotStorage!.resources();
+ }
+
+ private _pageEntry(pageId: string): PageEntry {
+ let pageEntry = this.pageEntries.get(pageId);
+ if (!pageEntry) {
+ pageEntry = {
+ actions: [],
+ events: [],
+ objects: {},
+ screencastFrames: [],
+ };
+ this.pageEntries.set(pageId, pageEntry);
+ this.contextEntry.pages.push(pageEntry);
+ }
+ return pageEntry;
+ }
+
+ appendEvent(line: string) {
+ if (!line)
+ return;
+ const event = this._modernize(JSON.parse(line));
+ switch (event.type) {
+ case 'context-options': {
+ this.contextEntry.browserName = event.browserName;
+ this.contextEntry.options = event.options;
+ break;
+ }
+ case 'screencast-frame': {
+ this._pageEntry(event.pageId).screencastFrames.push(event);
+ break;
+ }
+ case 'action': {
+ const metadata = event.metadata;
+ const include = event.hasSnapshot;
+ if (include && metadata.pageId)
+ this._pageEntry(metadata.pageId).actions.push(event);
+ break;
+ }
+ case 'event': {
+ const metadata = event.metadata;
+ if (metadata.pageId) {
+ if (metadata.method === '__create__')
+ this._pageEntry(metadata.pageId).objects[metadata.params.guid] = metadata.params.initializer;
+ else
+ this._pageEntry(metadata.pageId).events.push(event);
+ }
+ break;
+ }
+ case 'resource-snapshot':
+ this._snapshotStorage!.addResource(event.snapshot);
+ break;
+ case 'frame-snapshot':
+ this._snapshotStorage!.addFrameSnapshot(event.snapshot);
+ break;
+ }
+ if (event.type === 'action' || event.type === 'event') {
+ this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.metadata.startTime);
+ this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.metadata.endTime);
+ }
+ }
+
+ private _modernize(event: any): trace.TraceEvent {
+ if (this._version === undefined)
+ return event;
+ for (let version = this._version; version < trace.VERSION; ++version)
+ event = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, event);
+ return event;
+ }
+
+ _modernize_0_to_1(event: any): any {
+ if (event.type === 'action') {
+ if (typeof event.metadata.error === 'string')
+ event.metadata.error = { error: { name: 'Error', message: event.metadata.error } };
+ if (event.metadata && typeof event.hasSnapshot !== 'boolean')
+ event.hasSnapshot = commandsWithTracingSnapshots.has(event.metadata);
+ }
+ return event;
+ }
+
+ _modernize_1_to_2(event: any): any {
+ if (event.type === 'frame-snapshot' && event.snapshot.isMainFrame) {
+ // Old versions had completely wrong viewport.
+ event.snapshot.viewport = this.contextEntry.options.viewport || { width: 1280, height: 720 };
+ }
+ return event;
+ }
+
+ _modernize_2_to_3(event: any): any {
+ if (event.type === 'resource-snapshot' && !event.snapshot.request) {
+ // Migrate from old ResourceSnapshot to new har entry format.
+ const resource = event.snapshot;
+ event.snapshot = {
+ _frameref: resource.frameId,
+ request: {
+ url: resource.url,
+ method: resource.method,
+ headers: resource.requestHeaders,
+ postData: resource.requestSha1 ? { _sha1: resource.requestSha1 } : undefined,
+ },
+ response: {
+ status: resource.status,
+ headers: resource.responseHeaders,
+ content: {
+ mimeType: resource.contentType,
+ _sha1: resource.responseSha1,
+ },
+ },
+ _monotonicTime: resource.timestamp,
+ };
+ }
+ return event;
+ }
+}
+
+export type ContextEntry = {
+ startTime: number;
+ endTime: number;
+ browserName: string;
+ options: trace.BrowserContextEventOptions;
+ pages: PageEntry[];
+ resources: ResourceSnapshot[];
+};
+
+export type PageEntry = {
+ actions: trace.ActionTraceEvent[];
+ events: trace.ActionTraceEvent[];
+ objects: { [key: string]: any };
+ screencastFrames: {
+ sha1: string,
+ timestamp: number,
+ width: number,
+ height: number,
+ }[];
+};
+
+export class PersistentSnapshotStorage extends BaseSnapshotStorage {
+ private _entries: Map;
+
+ constructor(entries: Map) {
+ super();
+ this._entries = entries;
+ }
+
+ async resourceContent(sha1: string): Promise {
+ const entry = this._entries.get('resources/' + sha1)!;
+ const writer = new zipjs.BlobWriter();
+ await entry.getData!(writer);
+ return writer.getData();
+ }
+}
+
+// Prior to version 2 we did not have a hasSnapshot bit on.
+export const commandsWithTracingSnapshots = new Set([
+ 'EventTarget.waitForEventInfo',
+ 'BrowserContext.waitForEventInfo',
+ 'Page.waitForEventInfo',
+ 'WebSocket.waitForEventInfo',
+ 'ElectronApplication.waitForEventInfo',
+ 'AndroidDevice.waitForEventInfo',
+ 'Page.goBack',
+ 'Page.goForward',
+ 'Page.reload',
+ 'Page.setViewportSize',
+ 'Page.keyboardDown',
+ 'Page.keyboardUp',
+ 'Page.keyboardInsertText',
+ 'Page.keyboardType',
+ 'Page.keyboardPress',
+ 'Page.mouseMove',
+ 'Page.mouseDown',
+ 'Page.mouseUp',
+ 'Page.mouseClick',
+ 'Page.mouseWheel',
+ 'Page.touchscreenTap',
+ 'Frame.evalOnSelector',
+ 'Frame.evalOnSelectorAll',
+ 'Frame.addScriptTag',
+ 'Frame.addStyleTag',
+ 'Frame.check',
+ 'Frame.click',
+ 'Frame.dragAndDrop',
+ 'Frame.dblclick',
+ 'Frame.dispatchEvent',
+ 'Frame.evaluateExpression',
+ 'Frame.evaluateExpressionHandle',
+ 'Frame.fill',
+ 'Frame.focus',
+ 'Frame.getAttribute',
+ 'Frame.goto',
+ 'Frame.hover',
+ 'Frame.innerHTML',
+ 'Frame.innerText',
+ 'Frame.inputValue',
+ 'Frame.isChecked',
+ 'Frame.isDisabled',
+ 'Frame.isEnabled',
+ 'Frame.isHidden',
+ 'Frame.isVisible',
+ 'Frame.isEditable',
+ 'Frame.press',
+ 'Frame.selectOption',
+ 'Frame.setContent',
+ 'Frame.setInputFiles',
+ 'Frame.tap',
+ 'Frame.textContent',
+ 'Frame.type',
+ 'Frame.uncheck',
+ 'Frame.waitForTimeout',
+ 'Frame.waitForFunction',
+ 'Frame.waitForSelector',
+ 'Frame.expect',
+ 'JSHandle.evaluateExpression',
+ 'ElementHandle.evaluateExpression',
+ 'JSHandle.evaluateExpressionHandle',
+ 'ElementHandle.evaluateExpressionHandle',
+ 'ElementHandle.evalOnSelector',
+ 'ElementHandle.evalOnSelectorAll',
+ 'ElementHandle.check',
+ 'ElementHandle.click',
+ 'ElementHandle.dblclick',
+ 'ElementHandle.dispatchEvent',
+ 'ElementHandle.fill',
+ 'ElementHandle.hover',
+ 'ElementHandle.innerHTML',
+ 'ElementHandle.innerText',
+ 'ElementHandle.inputValue',
+ 'ElementHandle.isChecked',
+ 'ElementHandle.isDisabled',
+ 'ElementHandle.isEditable',
+ 'ElementHandle.isEnabled',
+ 'ElementHandle.isHidden',
+ 'ElementHandle.isVisible',
+ 'ElementHandle.press',
+ 'ElementHandle.scrollIntoViewIfNeeded',
+ 'ElementHandle.selectOption',
+ 'ElementHandle.selectText',
+ 'ElementHandle.setInputFiles',
+ 'ElementHandle.tap',
+ 'ElementHandle.textContent',
+ 'ElementHandle.type',
+ 'ElementHandle.uncheck',
+ 'ElementHandle.waitForElementState',
+ 'ElementHandle.waitForSelector'
+]);
diff --git a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx
index e685d0877a..497c214db6 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx
+++ b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx
@@ -19,7 +19,7 @@ import { Boundaries, Size } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
import { upperBound } from '../../uiUtils';
-import { ContextEntry, PageEntry } from '../../../server/trace/viewer/traceModel';
+import { ContextEntry, PageEntry } from '../traceModel';
const tileSize = { width: 200, height: 45 };
diff --git a/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts b/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts
index ee0471dfa0..e14e87f7dc 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts
+++ b/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-import { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
+import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
-import { ContextEntry, PageEntry } from '../../../server/trace/viewer/traceModel';
+import { ContextEntry, PageEntry } from '../traceModel';
const contextSymbol = Symbol('context');
const pageSymbol = Symbol('context');
diff --git a/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.css b/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.css
index 789adf5265..989d67c679 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.css
+++ b/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.css
@@ -43,12 +43,12 @@
.network-request-title-status,
.network-request-title-method {
- margin-right: 5px;
+ padding-right: 5px;
}
.network-request-title-status.status-failure {
- color: var(--red);
- font-weight: bold;
+ background-color: var(--red);
+ color: var(--white);
}
.network-request-title-status.status-neutral {
diff --git a/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.tsx b/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.tsx
index 431e98ae80..c7bca4da05 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.tsx
+++ b/packages/playwright-core/src/web/traceViewer/ui/networkResourceDetails.tsx
@@ -16,7 +16,7 @@
import './networkResourceDetails.css';
import * as React from 'react';
-import type { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
+import type { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
import { Expandable } from '../../components/expandable';
export const NetworkResourceDetails: React.FunctionComponent<{
diff --git a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx
index e9ea79280d..ac33873f3e 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx
+++ b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx
@@ -73,7 +73,7 @@ export const SnapshotTab: React.FunctionComponent<{
})();
}, [iframeRef, snapshotUrl, snapshotSizeUrl, pointX, pointY]);
- const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
+ const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height, 1);
const scaledSize = {
width: snapshotSize.width * scale,
height: snapshotSize.height * scale,
diff --git a/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx b/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx
index eaa7f5ef50..d984b1d4b1 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx
+++ b/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx
@@ -16,7 +16,7 @@
*/
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
-import { ContextEntry } from '../../../server/trace/viewer/traceModel';
+import { ContextEntry } from '../traceModel';
import './timeline.css';
import { Boundaries } from '../geometry';
import * as React from 'react';
diff --git a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx
index 9e55158b21..a6d759d126 100644
--- a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx
+++ b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx
@@ -15,47 +15,45 @@
*/
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
-import { ContextEntry } from '../../../server/trace/viewer/traceModel';
+import { ContextEntry } from '../traceModel';
import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline';
import './workbench.css';
import * as React from 'react';
-import { ContextSelector } from './contextSelector';
import { NetworkTab } from './networkTab';
import { SourceTab } from './sourceTab';
import { SnapshotTab } from './snapshotTab';
import { CallTab } from './callTab';
import { SplitView } from '../../components/splitView';
-import { useAsyncMemo } from './helpers';
import { ConsoleTab } from './consoleTab';
import * as modelUtil from './modelUtil';
export const Workbench: React.FunctionComponent<{
- debugNames: string[],
-}> = ({ debugNames }) => {
- const [debugName, setDebugName] = React.useState(debugNames[0]);
+}> = () => {
+ const [contextEntry, setContextEntry] = React.useState(emptyContext);
const [selectedAction, setSelectedAction] = React.useState();
const [highlightedAction, setHighlightedAction] = React.useState();
const [selectedTab, setSelectedTab] = React.useState('logs');
+ const trace = new URL(window.location.href).searchParams.get('trace');
- const context = useAsyncMemo(async () => {
- if (!debugName)
- return emptyContext;
- const context = (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry;
- modelUtil.indexModel(context);
- return context;
- }, [debugName], emptyContext);
+ React.useEffect(() => {
+ (async () => {
+ const contextEntry = (await fetch(`/context?trace=${trace}`).then(response => response.json())) as ContextEntry;
+ modelUtil.indexModel(contextEntry);
+ setContextEntry(contextEntry);
+ })();
+ }, [trace]);
const actions = React.useMemo(() => {
const actions: ActionTraceEvent[] = [];
- for (const page of context.pages)
+ for (const page of contextEntry.pages)
actions.push(...page.actions);
return actions;
- }, [context]);
+ }, [contextEntry]);
- const defaultSnapshotSize = context.options.viewport || { width: 1280, height: 720 };
- const boundaries = { minimum: context.startTime, maximum: context.endTime };
+ const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 };
+ const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
// Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
@@ -68,18 +66,10 @@ export const Workbench: React.FunctionComponent<{
ðŸŽ
Playwright
- {
- setDebugName(debugName);
- setSelectedAction(undefined);
- }}
- />
Promise }>({
- snapshotPort: async ({}, run, testInfo) => {
- await run(11000 + testInfo.workerIndex);
- },
-
- snapshotter: async ({ mode, toImpl, context, snapshotPort }, run, testInfo) => {
+const it = contextTest.extend<{ snapshotter: InMemorySnapshotter }>({
+ snapshotter: async ({ mode, toImpl, context }, run, testInfo) => {
testInfo.skip(mode !== 'default');
const snapshotter = new InMemorySnapshotter(toImpl(context));
await snapshotter.initialize();
- const httpServer = new HttpServer();
- httpServer.routePath('/snapshot/sw.js', (request, response) => {
- return httpServer.serveFile(response, path.join(__dirname, 'playwright-core/lib/web/traceViewer/sw.js'));
- });
- new SnapshotServer(httpServer, snapshotter);
- await httpServer.start(snapshotPort);
await run(snapshotter);
await snapshotter.dispose();
- await httpServer.stop();
- },
-
- showSnapshot: async ({ contextFactory, snapshotPort }, use) => {
- await use(async (snapshot: any) => {
- const previewContext = await contextFactory();
- const previewPage = await previewContext.newPage();
- previewPage.on('console', console.log);
- await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/?serviceWorkerForTest`);
- const frameSnapshot = snapshot.snapshot();
- await previewPage.evaluate(snapshotId => {
- (window as any).showSnapshot(snapshotId);
- }, `${frameSnapshot.pageId}?name=${frameSnapshot.snapshotName}`);
- // wait for the render frame to load
- while (previewPage.frames().length < 2)
- await new Promise(f => previewPage.once('frameattached', f));
- const frame = previewPage.frames()[1];
- await frame.waitForLoadState();
- return frame;
- });
},
});
@@ -150,10 +116,10 @@ it.describe('snapshots', () => {
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
const resource = snapshot2.resourceByUrl(`http://localhost:${server.PORT}/style.css`);
- expect((await snapshotter.resourceContent(resource.response.content._sha1)).toString()).toBe('button { color: blue; }');
+ expect((await snapshotter.resourceContentForTest(resource.response.content._sha1)).toString()).toBe('button { color: blue; }');
});
- it('should capture iframe', async ({ page, server, toImpl, browserName, snapshotter, showSnapshot }) => {
+ it('should capture iframe', async ({ page, server, toImpl, browserName, snapshotter }) => {
it.skip(browserName === 'firefox');
await page.route('**/empty.html', route => {
@@ -180,13 +146,6 @@ it.describe('snapshots', () => {
break;
await page.waitForTimeout(250);
}
-
- // Render snapshot, check expectations.
- const frame = await showSnapshot(snapshot);
- while (frame.childFrames().length < 1)
- await new Promise(f => frame.page().once('frameattached', f));
- const button = await frame.childFrames()[0].waitForSelector('button');
- expect(await button.textContent()).toBe('Hello iframe');
});
it('should capture snapshot target', async ({ page, toImpl, snapshotter }) => {
@@ -221,189 +180,6 @@ it.describe('snapshots', () => {
expect(distillSnapshot(snapshot)).toBe('');
}
});
-
- it('should contain adopted style sheets', async ({ page, toImpl, showSnapshot, snapshotter, browserName }) => {
- it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
- await page.setContent('');
- await page.evaluate(() => {
- const sheet = new CSSStyleSheet();
- sheet.addRule('button', 'color: red');
- (document as any).adoptedStyleSheets = [sheet];
-
- const sheet2 = new CSSStyleSheet();
- sheet2.addRule(':host', 'color: blue');
-
- for (const element of [document.createElement('div'), document.createElement('span')]) {
- const root = element.attachShadow({
- mode: 'open'
- });
- root.append('foo');
- (root as any).adoptedStyleSheets = [sheet2];
- document.body.appendChild(element);
- }
- });
- const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
-
- const frame = await showSnapshot(snapshot1);
- await frame.waitForSelector('button');
- const buttonColor = await frame.$eval('button', button => {
- return window.getComputedStyle(button).color;
- });
- expect(buttonColor).toBe('rgb(255, 0, 0)');
- const divColor = await frame.$eval('div', div => {
- return window.getComputedStyle(div).color;
- });
- expect(divColor).toBe('rgb(0, 0, 255)');
- const spanColor = await frame.$eval('span', span => {
- return window.getComputedStyle(span).color;
- });
- expect(spanColor).toBe('rgb(0, 0, 255)');
- });
-
- it('should work with adopted style sheets and replace/replaceSync', async ({ page, toImpl, showSnapshot, snapshotter, browserName }) => {
- it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
- await page.setContent('');
- await page.evaluate(() => {
- const sheet = new CSSStyleSheet();
- sheet.addRule('button', 'color: red');
- (document as any).adoptedStyleSheets = [sheet];
- });
- const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
- await page.evaluate(() => {
- const [sheet] = (document as any).adoptedStyleSheets;
- sheet.replaceSync(`button { color: blue }`);
- });
- const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
- await page.evaluate(() => {
- const [sheet] = (document as any).adoptedStyleSheets;
- sheet.replace(`button { color: #0F0 }`);
- });
- const snapshot3 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot3');
-
- {
- const frame = await showSnapshot(snapshot1);
- await frame.waitForSelector('button');
- const buttonColor = await frame.$eval('button', button => {
- return window.getComputedStyle(button).color;
- });
- expect(buttonColor).toBe('rgb(255, 0, 0)');
- }
- {
- const frame = await showSnapshot(snapshot2);
- await frame.waitForSelector('button');
- const buttonColor = await frame.$eval('button', button => {
- return window.getComputedStyle(button).color;
- });
- expect(buttonColor).toBe('rgb(0, 0, 255)');
- }
- {
- const frame = await showSnapshot(snapshot3);
- await frame.waitForSelector('button');
- const buttonColor = await frame.$eval('button', button => {
- return window.getComputedStyle(button).color;
- });
- expect(buttonColor).toBe('rgb(0, 255, 0)');
- }
- });
-
- it('should restore scroll positions', async ({ page, showSnapshot, toImpl, snapshotter, browserName }) => {
- it.skip(browserName === 'firefox');
-
- await page.setContent(`
-
-
-
- - Item 1
- - Item 2
- - Item 3
- - Item 4
- - Item 5
- - Item 6
- - Item 7
- - Item 8
- - Item 9
- - Item 10
-
-
- `);
-
- await (await page.$('text=Item 8')).scrollIntoViewIfNeeded();
- const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'scrolled');
-
- // Render snapshot, check expectations.
- const frame = await showSnapshot(snapshot);
- const div = await frame.waitForSelector('div');
- expect(await div.evaluate(div => div.scrollTop)).toBe(136);
- });
-
- it('should work with meta CSP', async ({ page, showSnapshot, toImpl, snapshotter, browserName }) => {
- it.skip(browserName === 'firefox');
-
- await page.setContent(`
-
-
-
-
- Hello
-
- `);
- await page.$eval('div', div => {
- const shadow = div.attachShadow({ mode: 'open' });
- const span = document.createElement('span');
- span.textContent = 'World';
- shadow.appendChild(span);
- });
-
- const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'meta');
-
- // Render snapshot, check expectations.
- const frame = await showSnapshot(snapshot);
- await frame.waitForSelector('div');
- // Should render shadow dom with post-processing script.
- expect(await frame.textContent('span')).toBe('World');
- });
-
- it('should handle multiple headers', async ({ page, server, showSnapshot, toImpl, snapshotter, browserName }) => {
- it.skip(browserName === 'firefox');
-
- server.setRoute('/foo.css', (req, res) => {
- res.statusCode = 200;
- res.setHeader('vary', ['accepts-encoding', 'accepts-encoding']);
- res.end('body { padding: 42px }');
- });
-
- await page.goto(server.EMPTY_PAGE);
- await page.setContent(`Hello
`);
- const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
- const frame = await showSnapshot(snapshot);
- await frame.waitForSelector('div');
- const padding = await frame.$eval('body', body => window.getComputedStyle(body).paddingLeft);
- expect(padding).toBe('42px');
- });
-
- it('should handle src=blob', async ({ page, server, showSnapshot, toImpl, snapshotter, browserName }) => {
- it.skip(browserName === 'firefox');
-
- await page.goto(server.EMPTY_PAGE);
- await page.evaluate(async () => {
- const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAASCAQAAADIvofAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfhBhAPKSstM+EuAAAAvUlEQVQY05WQIW4CYRgF599gEZgeoAKBWIfCNSmVvQMe3wv0ChhIViKwtTQEAYJwhgpISBA0JSxNIdlB7LIGTJ/8kpeZ7wW5TcT9o/QNBtvOrrWMrtg0sSGOFeELbHlCDsQ+ukeYiHNFJPHBDRKlQKVEbFkLUT3AiAxI6VGCXsWXAoQLBUl5E7HjUFwiyI4zf/wWoB3CFnxX5IeGdY8IGU/iwE9jcZrLy4pnEat+FL4hf/cbqREKo/Cf6W5zASVMeh234UtGAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA2LTE2VDE1OjQxOjQzLTA3OjAwd1xNIQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNi0xNlQxNTo0MTo0My0wNzowMAYB9Z0AAAAASUVORK5CYII=';
- const blob = await fetch(dataUrl).then(res => res.blob());
- const url = window.URL.createObjectURL(blob);
- const img = document.createElement('img');
- img.src = url;
- const loaded = new Promise(f => img.onload = f);
- document.body.appendChild(img);
- await loaded;
- });
-
- const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
- const frame = await showSnapshot(snapshot);
- const img = await frame.waitForSelector('img');
- expect(await img.screenshot()).toMatchSnapshot('blob-src.png');
- });
});
function distillSnapshot(snapshot) {
diff --git a/tests/snapshotter.spec.ts-snapshots/blob-src-chromium.png b/tests/snapshotter.spec.ts-snapshots/blob-src-chromium.png
deleted file mode 100644
index f5730831d2..0000000000
Binary files a/tests/snapshotter.spec.ts-snapshots/blob-src-chromium.png and /dev/null differ
diff --git a/tests/snapshotter.spec.ts-snapshots/blob-src-webkit.png b/tests/snapshotter.spec.ts-snapshots/blob-src-webkit.png
deleted file mode 100644
index d834e92f04..0000000000
Binary files a/tests/snapshotter.spec.ts-snapshots/blob-src-webkit.png and /dev/null differ
diff --git a/tests/trace-viewer/trace-viewer.spec.ts b/tests/trace-viewer/trace-viewer.spec.ts
index eaf9674915..95b81d49d9 100644
--- a/tests/trace-viewer/trace-viewer.spec.ts
+++ b/tests/trace-viewer/trace-viewer.spec.ts
@@ -15,7 +15,7 @@
*/
import path from 'path';
-import type { Browser, Locator, Page } from 'playwright-core';
+import type { Browser, Frame, Locator, Page } from 'playwright-core';
import { showTraceViewer } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import { playwrightTest, expect } from '../config/browserTest';
@@ -50,8 +50,8 @@ class TraceViewerPage {
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
}
- async selectAction(title: string) {
- await this.page.click(`.action-title:has-text("${title}")`);
+ async selectAction(title: string, ordinal: number = 0) {
+ await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
}
async selectSnapshot(name: string) {
@@ -81,9 +81,16 @@ class TraceViewerPage {
const result = [...set];
return result.sort();
}
+
+ async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise {
+ await this.selectAction(actionName, ordinal);
+ while (this.page.frames().length < (hasSubframe ? 4 : 3))
+ await this.page.waitForEvent('frameattached');
+ return this.page.mainFrame().childFrames()[0].childFrames()[0];
+ }
}
-const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise }>({
+const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise, runAndTrace: (body: () => Promise) => Promise }>({
showTraceViewer: async ({ playwright, browserName, headless }, use) => {
let browser: Browser;
let contextImpl: any;
@@ -94,6 +101,16 @@ const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise
});
await browser.close();
await contextImpl._browser.close();
+ },
+
+ runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
+ await use(async (body: () => Promise) => {
+ const traceFile = testInfo.outputPath('trace.zip');
+ await context.tracing.start({ snapshots: true, screenshots: true });
+ await body();
+ await context.tracing.stop({ path: traceFile });
+ return showTraceViewer(traceFile);
+ });
}
});
@@ -256,3 +273,225 @@ test('should have network requests', async ({ showTraceViewer }) => {
'200GETscript.jsapplication/javascript',
]);
});
+
+test('should capture iframe', async ({ page, server, browserName, runAndTrace }) => {
+ test.skip(browserName === 'firefox');
+
+ await page.route('**/empty.html', route => {
+ route.fulfill({
+ body: '',
+ contentType: 'text/html'
+ }).catch(() => {});
+ });
+ await page.route('**/iframe.html', route => {
+ route.fulfill({
+ body: '',
+ contentType: 'text/html'
+ }).catch(() => {});
+ });
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.goto(server.EMPTY_PAGE);
+ if (page.frames().length < 2)
+ await page.waitForEvent('frameattached');
+ await page.frames()[1].waitForSelector('button');
+ // Force snapshot.
+ await page.evaluate('2+2');
+ });
+
+ // Render snapshot, check expectations.
+ const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate', 0, true);
+ const button = await snapshotFrame.childFrames()[0].waitForSelector('button');
+ expect(await button.textContent()).toBe('Hello iframe');
+});
+
+test('should contain adopted style sheets', async ({ page, runAndTrace, browserName }) => {
+ test.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.setContent('');
+ await page.evaluate(() => {
+ const sheet = new CSSStyleSheet();
+ sheet.addRule('button', 'color: red');
+ (document as any).adoptedStyleSheets = [sheet];
+
+ const sheet2 = new CSSStyleSheet();
+ sheet2.addRule(':host', 'color: blue');
+
+ for (const element of [document.createElement('div'), document.createElement('span')]) {
+ const root = element.attachShadow({
+ mode: 'open'
+ });
+ root.append('foo');
+ (root as any).adoptedStyleSheets = [sheet2];
+ document.body.appendChild(element);
+ }
+ });
+ });
+
+ const frame = await traceViewer.snapshotFrame('page.evaluate');
+ await frame.waitForSelector('button');
+ const buttonColor = await frame.$eval('button', button => {
+ return window.getComputedStyle(button).color;
+ });
+ expect(buttonColor).toBe('rgb(255, 0, 0)');
+ const divColor = await frame.$eval('div', div => {
+ return window.getComputedStyle(div).color;
+ });
+ expect(divColor).toBe('rgb(0, 0, 255)');
+ const spanColor = await frame.$eval('span', span => {
+ return window.getComputedStyle(span).color;
+ });
+ expect(spanColor).toBe('rgb(0, 0, 255)');
+});
+
+test('should work with adopted style sheets and replace/replaceSync', async ({ page, runAndTrace, browserName }) => {
+ test.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.setContent('');
+ await page.evaluate(() => {
+ const sheet = new CSSStyleSheet();
+ sheet.addRule('button', 'color: red');
+ (document as any).adoptedStyleSheets = [sheet];
+ });
+ await page.evaluate(() => {
+ const [sheet] = (document as any).adoptedStyleSheets;
+ sheet.replaceSync(`button { color: blue }`);
+ });
+ await page.evaluate(() => {
+ const [sheet] = (document as any).adoptedStyleSheets;
+ sheet.replace(`button { color: #0F0 }`);
+ });
+ });
+
+ {
+ const frame = await traceViewer.snapshotFrame('page.evaluate', 0);
+ await frame.waitForSelector('button');
+ const buttonColor = await frame.$eval('button', button => {
+ return window.getComputedStyle(button).color;
+ });
+ expect(buttonColor).toBe('rgb(255, 0, 0)');
+ }
+ {
+ const frame = await traceViewer.snapshotFrame('page.evaluate', 1);
+ await frame.waitForSelector('button');
+ const buttonColor = await frame.$eval('button', button => {
+ return window.getComputedStyle(button).color;
+ });
+ expect(buttonColor).toBe('rgb(0, 0, 255)');
+ }
+ {
+ const frame = await traceViewer.snapshotFrame('page.evaluate', 2);
+ await frame.waitForSelector('button');
+ const buttonColor = await frame.$eval('button', button => {
+ return window.getComputedStyle(button).color;
+ });
+ expect(buttonColor).toBe('rgb(0, 255, 0)');
+ }
+});
+
+test('should restore scroll positions', async ({ page, runAndTrace, browserName }) => {
+ test.skip(browserName === 'firefox');
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.setContent(`
+
+
+
+ - Item 1
+ - Item 2
+ - Item 3
+ - Item 4
+ - Item 5
+ - Item 6
+ - Item 7
+ - Item 8
+ - Item 9
+ - Item 10
+
+
+ `);
+
+ await (await page.$('text=Item 8')).scrollIntoViewIfNeeded();
+ });
+
+ // Render snapshot, check expectations.
+ const frame = await traceViewer.snapshotFrame('scrollIntoViewIfNeeded');
+ const div = await frame.waitForSelector('div');
+ expect(await div.evaluate(div => div.scrollTop)).toBe(136);
+});
+
+test('should work with meta CSP', async ({ page, runAndTrace, browserName }) => {
+ test.skip(browserName === 'firefox');
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.setContent(`
+
+
+
+
+ Hello
+
+ `);
+ await page.$eval('div', div => {
+ const shadow = div.attachShadow({ mode: 'open' });
+ const span = document.createElement('span');
+ span.textContent = 'World';
+ shadow.appendChild(span);
+ });
+ });
+
+ // Render snapshot, check expectations.
+ const frame = await traceViewer.snapshotFrame('$eval');
+ await frame.waitForSelector('div');
+ // Should render shadow dom with post-processing script.
+ expect(await frame.textContent('span')).toBe('World');
+});
+
+test('should handle multiple headers', async ({ page, server, runAndTrace, browserName }) => {
+ test.skip(browserName === 'firefox');
+
+ server.setRoute('/foo.css', (req, res) => {
+ res.statusCode = 200;
+ res.setHeader('vary', ['accepts-encoding', 'accepts-encoding']);
+ res.end('body { padding: 42px }');
+ });
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`Hello
`);
+ });
+
+ const frame = await traceViewer.snapshotFrame('setContent');
+ await frame.waitForSelector('div');
+ const padding = await frame.$eval('body', body => window.getComputedStyle(body).paddingLeft);
+ expect(padding).toBe('42px');
+});
+
+test('should handle src=blob', async ({ page, server, runAndTrace, browserName }) => {
+ test.skip(browserName === 'firefox');
+
+ const traceViewer = await runAndTrace(async () => {
+ await page.setViewportSize({ width: 300, height: 300 });
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(async () => {
+ const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAASCAQAAADIvofAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfhBhAPKSstM+EuAAAAvUlEQVQY05WQIW4CYRgF599gEZgeoAKBWIfCNSmVvQMe3wv0ChhIViKwtTQEAYJwhgpISBA0JSxNIdlB7LIGTJ/8kpeZ7wW5TcT9o/QNBtvOrrWMrtg0sSGOFeELbHlCDsQ+ukeYiHNFJPHBDRKlQKVEbFkLUT3AiAxI6VGCXsWXAoQLBUl5E7HjUFwiyI4zf/wWoB3CFnxX5IeGdY8IGU/iwE9jcZrLy4pnEat+FL4hf/cbqREKo/Cf6W5zASVMeh234UtGAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA2LTE2VDE1OjQxOjQzLTA3OjAwd1xNIQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNi0xNlQxNTo0MTo0My0wNzowMAYB9Z0AAAAASUVORK5CYII=';
+ const blob = await fetch(dataUrl).then(res => res.blob());
+ const url = window.URL.createObjectURL(blob);
+ const img = document.createElement('img');
+ img.src = url;
+ const loaded = new Promise(f => img.onload = f);
+ document.body.appendChild(img);
+ await loaded;
+ });
+ });
+
+ const frame = await traceViewer.snapshotFrame('page.evaluate');
+ const img = await frame.waitForSelector('img');
+ const size = await img.evaluate(e => (e as HTMLImageElement).naturalWidth);
+ expect(size).toBe(10);
+});
diff --git a/utils/check_deps.js b/utils/check_deps.js
index 2e7d34fa4a..70ed075032 100644
--- a/utils/check_deps.js
+++ b/utils/check_deps.js
@@ -175,9 +175,10 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inProcessFactory.ts'] = DEPS['src/browserS
// Tracing is a client/server plugin, nothing should depend on it.
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts'];
-DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/'];
-DEPS['src/web/traceViewer/sw.ts'] = ['src/server/snapshot/snapshotTypes.ts'];
+DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/', 'src/server/trace/common/'];
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/protocol/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
+DEPS['src/web/traceViewer/inMemorySnapshotter.ts'] = ['src/**'];
+
// The service is a cross-cutting feature, and so it depends on a bunch of things.
DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/server/trace/', 'src/utils/**'];