chore(trace): make trace viewer a pwa (#9438)

This commit is contained in:
Pavel Feldman 2021-10-12 13:42:50 -08:00 committed by GitHub
parent bcfd47343c
commit c0945d9d00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1639 additions and 932 deletions

704
package-lock.json generated
View File

@ -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
}
}
}

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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 = {

View File

@ -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 = {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { NodeSnapshot } from './snapshotTypes';
import { NodeSnapshot } from '../common/snapshotTypes';
export type SnapshotData = {
doctype?: string,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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'));
})();

View File

@ -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) {

View 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;
}
}

View File

@ -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];
}
}

View File

@ -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));
});

View 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'
]);

View File

@ -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 };

View File

@ -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');

View File

@ -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 {

View File

@ -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<{

View File

@ -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,

View File

@ -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';

View File

@ -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}

View File

@ -23,7 +23,6 @@ module.exports = {
options: {
presets: [
"@babel/preset-typescript",
"@babel/preset-react"
]
},
exclude: /node_modules/

View File

@ -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'),

View File

@ -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

View File

@ -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);
});

View File

@ -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/**'];