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