mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
chore(trace): make trace viewer a pwa (#9438)
This commit is contained in:
parent
bcfd47343c
commit
c0945d9d00
704
package-lock.json
generated
704
package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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(`
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<script>
|
||||
(${rootScript})();
|
||||
</script>
|
||||
</body>
|
||||
`);
|
||||
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<void>;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NodeSnapshot } from './snapshotTypes';
|
||||
import { NodeSnapshot } from '../common/snapshotTypes';
|
||||
|
||||
export type SnapshotData = {
|
||||
doctype?: string,
|
@ -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,
|
||||
|
@ -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<string, PageEntry>();
|
||||
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<Buffer | undefined> {
|
||||
return this._loader.read('resources/' + sha1);
|
||||
}
|
||||
}
|
@ -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/<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<BrowserContext> {
|
||||
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<BrowserContext | undefined> {
|
||||
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<BrowserContext | undefined> {
|
||||
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;
|
||||
}
|
||||
|
@ -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<string, Buffer>();
|
||||
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<string> {
|
||||
async initialize(): Promise<void> {
|
||||
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<SnapshotRenderer> {
|
||||
@ -96,7 +89,11 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
||||
this.addFrameSnapshot(snapshot);
|
||||
}
|
||||
|
||||
async resourceContent(sha1: string): Promise<Buffer | undefined> {
|
||||
async resourceContent(sha1: string): Promise<Blob | undefined> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async resourceContentForTest(sha1: string): Promise<Buffer | undefined> {
|
||||
return this._blobs.get(sha1);
|
||||
}
|
||||
}
|
@ -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(<Workbench debugNames={debugNames} />, document.querySelector('#root'));
|
||||
if (!navigator.serviceWorker.controller) {
|
||||
await new Promise<void>(f => {
|
||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||
});
|
||||
}
|
||||
ReactDOM.render(<Workbench/>, document.querySelector('#root'));
|
||||
})();
|
||||
|
@ -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) {
|
170
packages/playwright-core/src/web/traceViewer/snapshotServer.ts
Normal file
170
packages/playwright-core/src/web/traceViewer/snapshotServer.ts
Normal file
@ -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<string, SnapshotRenderer>();
|
||||
|
||||
constructor(snapshotStorage: SnapshotStorage) {
|
||||
this._snapshotStorage = snapshotStorage;
|
||||
}
|
||||
|
||||
static serveSnapshotRoot(): Response {
|
||||
return new Response(`
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<script>
|
||||
(${rootScript})();
|
||||
</script>
|
||||
</body>
|
||||
`, {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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<void>;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<Buffer | undefined>;
|
||||
resourceContent(sha1: string): Promise<Blob | undefined>;
|
||||
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<Buffer | undefined>;
|
||||
abstract resourceContent(sha1: string): Promise<Blob | undefined>;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
}
|
@ -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<string, { frameId: string, index: number }>();
|
||||
|
||||
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('<body style="background: #ddd"></body>', { status: 200, headers: { 'Content-Type': 'text/html' } });
|
||||
let traceModel: TraceModel | undefined;
|
||||
let snapshotServer: SnapshotServer | undefined;
|
||||
|
||||
async function loadTrace(trace: string): Promise<TraceModel> {
|
||||
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<Response> {
|
||||
// @ts-ignore
|
||||
async function doFetch(event: FetchEvent): Promise<Response> {
|
||||
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));
|
||||
});
|
||||
|
334
packages/playwright-core/src/web/traceViewer/traceModel.ts
Normal file
334
packages/playwright-core/src/web/traceViewer/traceModel.ts
Normal file
@ -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<string, PageEntry>();
|
||||
private _snapshotStorage: PersistentSnapshotStorage | undefined;
|
||||
private _entries = new Map<string, zip.Entry>();
|
||||
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<Blob | undefined> {
|
||||
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<string, zip.Entry>;
|
||||
|
||||
constructor(entries: Map<string, zip.Entry>) {
|
||||
super();
|
||||
this._entries = entries;
|
||||
}
|
||||
|
||||
async resourceContent(sha1: string): Promise<Blob | undefined> {
|
||||
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'
|
||||
]);
|
@ -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 };
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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 {
|
||||
|
@ -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<{
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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<ContextEntry>(emptyContext);
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedTab, setSelectedTab] = React.useState<string>('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<{
|
||||
<div className='logo'>🎭</div>
|
||||
<div className='product'>Playwright</div>
|
||||
<div className='spacer'></div>
|
||||
<ContextSelector
|
||||
debugNames={debugNames}
|
||||
debugName={debugName}
|
||||
onChange={debugName => {
|
||||
setDebugName(debugName);
|
||||
setSelectedAction(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none', borderBottom: '1px solid #ddd' }}>
|
||||
<Timeline
|
||||
context={context}
|
||||
context={contextEntry}
|
||||
boundaries={boundaries}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
|
@ -23,7 +23,6 @@ module.exports = {
|
||||
options: {
|
||||
presets: [
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
},
|
||||
exclude: /node_modules/
|
||||
|
@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
module.exports = {
|
||||
@ -40,6 +41,14 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, '../../../../../node_modules/@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'),
|
||||
to: path.resolve(__dirname, '../../../lib/web/traceViewer/zip.min.js')
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebPackPlugin({
|
||||
title: 'Playwright Trace Viewer',
|
||||
template: path.join(__dirname, 'index.html'),
|
||||
|
@ -15,49 +15,15 @@
|
||||
*/
|
||||
|
||||
import { contextTest, expect } from './config/browserTest';
|
||||
import { InMemorySnapshotter } from 'playwright-core/lib/server/snapshot/inMemorySnapshotter';
|
||||
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
|
||||
import { SnapshotServer } from 'playwright-core/lib/server/snapshot/snapshotServer';
|
||||
import type { Frame } from 'playwright-core';
|
||||
import path from 'path';
|
||||
import { InMemorySnapshotter } from 'playwright-core/lib/web/traceViewer/inMemorySnapshotter';
|
||||
|
||||
const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnapshotter, showSnapshot: (snapshot: any) => Promise<Frame> }>({
|
||||
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('<BUTTON data="two">Hello</BUTTON>');
|
||||
}
|
||||
});
|
||||
|
||||
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('<button>Hello</button>');
|
||||
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('<button>Hello</button>');
|
||||
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(`
|
||||
<style>
|
||||
li { height: 20px; margin: 0; padding: 0; }
|
||||
div { height: 60px; overflow-x: hidden; overflow-y: scroll; background: green; padding: 0; margin: 0; }
|
||||
</style>
|
||||
<div>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
<li>Item 4</li>
|
||||
<li>Item 5</li>
|
||||
<li>Item 6</li>
|
||||
<li>Item 7</li>
|
||||
<li>Item 8</li>
|
||||
<li>Item 9</li>
|
||||
<li>Item 10</li>
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'none'">
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello</div>
|
||||
</body>
|
||||
`);
|
||||
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(`<head><link rel=stylesheet href="/foo.css"></head><body><div>Hello</div></body>`);
|
||||
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) {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 301 B |
Binary file not shown.
Before Width: | Height: | Size: 407 B |
@ -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<Frame> {
|
||||
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<TraceViewerPage> }>({
|
||||
const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise<TraceViewerPage>, runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage> }>({
|
||||
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<void>) => {
|
||||
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: '<iframe src="iframe.html"></iframe>',
|
||||
contentType: 'text/html'
|
||||
}).catch(() => {});
|
||||
});
|
||||
await page.route('**/iframe.html', route => {
|
||||
route.fulfill({
|
||||
body: '<html><button>Hello iframe</button></html>',
|
||||
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('<button>Hello</button>');
|
||||
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('<button>Hello</button>');
|
||||
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(`
|
||||
<style>
|
||||
li { height: 20px; margin: 0; padding: 0; }
|
||||
div { height: 60px; overflow-x: hidden; overflow-y: scroll; background: green; padding: 0; margin: 0; }
|
||||
</style>
|
||||
<div>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
<li>Item 4</li>
|
||||
<li>Item 5</li>
|
||||
<li>Item 6</li>
|
||||
<li>Item 7</li>
|
||||
<li>Item 8</li>
|
||||
<li>Item 9</li>
|
||||
<li>Item 10</li>
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'none'">
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello</div>
|
||||
</body>
|
||||
`);
|
||||
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(`<head><link rel=stylesheet href="/foo.css"></head><body><div>Hello</div></body>`);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
@ -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/**'];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user