diff --git a/src/Common/Version.props b/src/Common/Version.props index 7066e2115b..d5fbd4298d 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.22.0 $(AssemblyVersion) - 1.23.0-beta-1656330747000 + 1.23.0 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.Tests.TestServer/assets/har-fulfill.har b/src/Playwright.Tests.TestServer/assets/har-fulfill.har new file mode 100644 index 0000000000..5b679098c8 --- /dev/null +++ b/src/Playwright.Tests.TestServer/assets/har-fulfill.har @@ -0,0 +1,366 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "111" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 111, + "mimeType": "text/html", + "compression": 0, + "text": "Hey
hello
" + }, + "headersSize": 65, + "bodySize": 170, + "redirectURL": "", + "_transferSize": 170 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.172Z", + "time": 7.132, + "request": { + "method": "POST", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background:cyan }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.174Z", + "time": 8.132, + "request": { + "method": "GET", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background: red }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572175.042, + "startedDateTime": "2022-06-10T04:27:32.175Z", + "time": 15.997, + "request": { + "method": "GET", + "url": "http://no.playwright/script.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 205, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "location", + "value": "http://no.playwright/script2.js" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 77, + "bodySize": 0, + "redirectURL": "http://no.playwright/script2.js", + "_transferSize": 77 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 7.673, + "receive": 8.324 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572181.822, + "startedDateTime": "2022-06-10T04:27:32.182Z", + "time": 6.735, + "request": { + "method": "GET", + "url": "http://no.playwright/script2.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 206, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "18" + }, + { + "name": "content-type", + "value": "text/javascript" + } + ], + "content": { + "size": 18, + "mimeType": "text/javascript", + "compression": 0, + "text": "window.value='foo'" + }, + "headersSize": 70, + "bodySize": 82, + "redirectURL": "", + "_transferSize": 82 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 6.735, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} \ No newline at end of file diff --git a/src/Playwright.Tests.TestServer/assets/har-redirect.har b/src/Playwright.Tests.TestServer/assets/har-redirect.har new file mode 100644 index 0000000000..5b50e7bffd --- /dev/null +++ b/src/Playwright.Tests.TestServer/assets/har-redirect.har @@ -0,0 +1,620 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.42" + }, + "pages": [ + { + "startedDateTime": "2022-06-16T21:41:23.901Z", + "id": "page@8f314969edc000996eb5c2ab22f0e6b3", + "title": "Microsoft", + "pageTimings": { + "onContentLoad": 8363, + "onLoad": 8896 + } + } + ], + "entries": [ + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928357.437, + "startedDateTime": "2022-06-16T21:41:23.951Z", + "time": 93.99, + "request": { + "method": "GET", + "url": "https://theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": ":authority", + "value": "theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 644, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "content-length", + "value": "0" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "location", + "value": "http://www.theverge.com/" + }, + { + "name": "retry-after", + "value": "0" + }, + { + "name": "server", + "value": "Varnish" + }, + { + "name": "set-cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "0" + }, + { + "name": "x-served-by", + "value": "cache-pao17442-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.005867,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 425, + "bodySize": 0, + "redirectURL": "http://www.theverge.com/", + "_transferSize": 425 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0, + "connect": 34.151, + "ssl": 28.074, + "send": 0, + "wait": 27.549, + "receive": 4.216 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.65.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928427.603, + "startedDateTime": "2022-06-16T21:41:24.022Z", + "time": 44.39499999999999, + "request": { + "method": "GET", + "url": "http://www.theverge.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "www.theverge.com" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 423, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "4e0c1265-10f8-4cb1-a5de-1c3cf70b531c", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "Accept-Ranges", + "value": "bytes" + }, + { + "name": "Age", + "value": "2615" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "0" + }, + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "Location", + "value": "https://www.theverge.com/" + }, + { + "name": "Server", + "value": "nginx" + }, + { + "name": "Set-Cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "Set-Cookie", + "value": "vmidv1=4e0c1265-10f8-4cb1-a5de-1c3cf70b531c;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "Vary", + "value": "X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Accept-Encoding" + }, + { + "name": "Via", + "value": "1.1 varnish" + }, + { + "name": "X-Cache", + "value": "HIT" + }, + { + "name": "X-Cache-Hits", + "value": "2" + }, + { + "name": "X-Served-By", + "value": "cache-pao17450-PAO" + }, + { + "name": "X-Timer", + "value": "S1655415684.035748,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "text/html", + "compression": 0 + }, + "headersSize": 731, + "bodySize": 0, + "redirectURL": "https://www.theverge.com/", + "_transferSize": 731 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 2.742, + "connect": 10.03, + "ssl": 14.123, + "send": 0, + "wait": 15.023, + "receive": 2.477 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 80, + "_securityDetails": {} + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928455.901, + "startedDateTime": "2022-06-16T21:41:24.050Z", + "time": 50.29199999999999, + "request": { + "method": "GET", + "url": "https://www.theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41" + }, + { + "name": "_chorus_geoip_continent", + "value": "NA" + } + ], + "headers": [ + { + "name": ":authority", + "value": "www.theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41; _chorus_geoip_continent=NA" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 729, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "40d8fd14-5ac3-4757-9e9c-efb106e82d3a", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "age", + "value": "263" + }, + { + "name": "cache-control", + "value": "max-age=0, public, must-revalidate" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-length", + "value": "14" + }, + { + "name": "content-security-policy", + "value": "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob: ; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests" + }, + { + "name": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "etag", + "value": "W/\"d498ef668223d015000070a66a181e85\"" + }, + { + "name": "link", + "value": "; rel=preload; as=fetch; crossorigin" + }, + { + "name": "referrer-policy", + "value": "strict-origin-when-cross-origin" + }, + { + "name": "server", + "value": "nginx" + }, + { + "name": "set-cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "set-cookie", + "value": "vmidv1=40d8fd14-5ac3-4757-9e9c-efb106e82d3a;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "strict-transport-security", + "value": "max-age=31556952; preload" + }, + { + "name": "vary", + "value": "Accept-Encoding, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Origin, X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "1" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-download-options", + "value": "noopen" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-permitted-cross-domain-policies", + "value": "none" + }, + { + "name": "x-request-id", + "value": "97363ad70e272e63641c0bb784fa06a01b848dfd" + }, + { + "name": "x-runtime", + "value": "0.257911" + }, + { + "name": "x-served-by", + "value": "cache-pao17436-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.075077,VS0,VE1" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + } + ], + "content": { + "size": 14, + "mimeType": "text/html", + "compression": 0, + "text": "

hello

" + }, + "headersSize": 1742, + "bodySize": 48716, + "redirectURL": "", + "_transferSize": 48716 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0.016, + "connect": 24.487, + "ssl": 17.406, + "send": 0, + "wait": 8.383, + "receive": -1 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + } + ] + } +} \ No newline at end of file diff --git a/src/Playwright.Tests.TestServer/assets/har-sha1-main-response.txt b/src/Playwright.Tests.TestServer/assets/har-sha1-main-response.txt new file mode 100644 index 0000000000..dbe9dba55e --- /dev/null +++ b/src/Playwright.Tests.TestServer/assets/har-sha1-main-response.txt @@ -0,0 +1 @@ +Hello, world \ No newline at end of file diff --git a/src/Playwright.Tests.TestServer/assets/har-sha1.har b/src/Playwright.Tests.TestServer/assets/har-sha1.har new file mode 100644 index 0000000000..d918acd028 --- /dev/null +++ b/src/Playwright.Tests.TestServer/assets/har-sha1.har @@ -0,0 +1,95 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "12" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 12, + "mimeType": "text/html", + "compression": 0, + "_file": "har-sha1-main-response.txt" + }, + "headersSize": 64, + "bodySize": 71, + "redirectURL": "", + "_transferSize": 71 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} \ No newline at end of file diff --git a/src/Playwright.Tests/BrowserContextHarTests.cs b/src/Playwright.Tests/BrowserContextHarTests.cs new file mode 100644 index 0000000000..e13040b5a1 --- /dev/null +++ b/src/Playwright.Tests/BrowserContextHarTests.cs @@ -0,0 +1,400 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace Microsoft.Playwright.Tests +{ + public class BrowserContextHarTests : ContextTestEx + { + [PlaywrightTest("browsercontext-har.spec.ts", "should context.routeFromHAR, matching the method and following redirects")] + public async Task ShouldContextRouteFromHarMatchingTheMethodAndFollowingRedirects() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + await Context.RouteFromHARAsync(path); + var page = await Context.NewPageAsync(); + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + // HAR contains a POST for the css file that should not be used. + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should page.routeFromHAR, matching the method and following redirects")] + public async Task ShouldPageRouteFromHarMatchingTheMethodAndFollowingRedirects() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + var page = await Context.NewPageAsync(); + await page.RouteFromHARAsync(path); + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + // HAR contains a POST for the css file that should not be used. + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "fallback:continue should continue when not found in har")] + public async Task FallbackContinueShouldContinueWhenNotFoundInHar() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + await Context.RouteFromHARAsync(path, new() { NotFound = HarNotFound.Fallback }); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.Prefix + "/one-style.html"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "by default should abort requests not found in har")] + public async Task ByDefaultshouldAbortRequestsNotFoundInHar() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + await Context.RouteFromHARAsync(path); + var page = await Context.NewPageAsync(); + + await PlaywrightAssert.ThrowsAsync(() + => page.GotoAsync(Server.EmptyPage)); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "fallback:continue should continue requests on bad har")] + public async Task FallbackContinueShouldContinueRequestsOnBadHar() + { + using var tmpDir = new TempDirectory(); + string path = Path.Combine(tmpDir.Path, "har.har"); + File.WriteAllText(path, "{ \"log\": { } }"); + + await Context.RouteFromHARAsync(path, new() { NotFound = HarNotFound.Fallback }); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.Prefix + "/one-style.html"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should only handle requests matching url filter")] + public async Task ShouldOnlyHandleRequestsMatchingUrlFilter() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + await Context.RouteFromHARAsync(path, new() { NotFound = HarNotFound.Fallback, UrlString = "**/*.js" }); + var page = await Context.NewPageAsync(); + await Context.RouteAsync("http://no.playwright/", async route => + { + Assert.AreEqual(route.Request.Url, "http://no.playwright/"); + await route.FulfillAsync(new() + { + Status = 200, + ContentType = "text/html", + Body = "
hello
" + }); + }); + + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgba(0, 0, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should only context.routeFromHAR requests matching url filter")] + public async Task ShouldOnlyContextRouteFromHarRequestsMatchingUrlFilter() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + await Context.RouteFromHARAsync(path, new() { UrlString = "**/*.js" }); + var page = await Context.NewPageAsync(); + await Context.RouteAsync("http://no.playwright/", async route => + { + Assert.AreEqual(route.Request.Url, "http://no.playwright/"); + await route.FulfillAsync(new() + { + Status = 200, + ContentType = "text/html", + Body = "
hello
" + }); + }); + + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgba(0, 0, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should only page.routeFromHAR requests matching url filter")] + public async Task ShouldOnlyPageRouteFromHarRequestsMatchingUrlFilter() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + var page = await Context.NewPageAsync(); + await page.RouteFromHARAsync(path, new() { UrlString = "**/*.js" }); + await Context.RouteAsync("http://no.playwright/", async route => + { + Assert.AreEqual(route.Request.Url, "http://no.playwright/"); + await route.FulfillAsync(new() + { + Status = 200, + ContentType = "text/html", + Body = "
hello
" + }); + }); + + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgba(0, 0, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should support regex filter")] + public async Task ShouldSupportRegexFilter() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + + await Context.RouteFromHARAsync(path, new() { UrlRegex = new Regex(@".*(\.js|.*\.css|no.playwright\/)$") }); + var page = await Context.NewPageAsync(); + await page.GotoAsync("http://no.playwright/"); + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 0, 0)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "newPage should fulfill from har, matching the method and following redirects")] + public async Task NewPageShouldFulfillFromHarMatchingTheMethodAndFollowingRedirects() + { + var path = TestUtils.GetAsset("har-fulfill.har"); + var page = await Browser.NewPageAsync(); + await page.RouteFromHARAsync(path); + await page.GotoAsync("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + Assert.AreEqual(await page.EvaluateAsync("window.value"), "foo"); + // HAR contains a POST for the css file that should not be used. + await Expect(page.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 0, 0)"); + await page.CloseAsync(); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should change document URL after redirected navigation")] + public async Task ShouldChangeDocumentUrlAfterRedirectedNavigation() + { + var path = TestUtils.GetAsset("har-redirect.har"); + await Context.RouteFromHARAsync(path); + var page = await Context.NewPageAsync(); + var waitForUrl = page.WaitForURLAsync("https://www.theverge.com/"); + var (response, _) = await TaskUtils.WhenAll( + page.WaitForNavigationAsync(), + page.GotoAsync("https://www.theverge.com/")); + await waitForUrl; + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + Assert.AreEqual(response.Request.Url, "https://www.theverge.com/"); + Assert.AreEqual(await page.EvaluateAsync("location.href"), "https://www.theverge.com/"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should change document URL after redirected navigation on click")] + public async Task ShouldChangeDocumentUrlAfterRedirectedNavigationOnClick() + { + var path = TestUtils.GetAsset("har-redirect.har"); + await Context.RouteFromHARAsync(path, new() { UrlRegex = new Regex(".*theverge.*") }); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.EmptyPage); + await page.SetContentAsync("click me"); + var responseTask = page.WaitForNavigationAsync(); + await page.ClickAsync("text=click me"); + var response = await responseTask; + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + Assert.AreEqual(response.Request.Url, "https://www.theverge.com/"); + Assert.AreEqual(await page.EvaluateAsync("location.href"), "https://www.theverge.com/"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should goBack to redirected navigation")] + public async Task ShouldGoBackToRedirectedNavigation() + { + var path = TestUtils.GetAsset("har-redirect.har"); + await Context.RouteFromHARAsync(path, new() { UrlRegex = new Regex(".*theverge.*") }); + var page = await Context.NewPageAsync(); + await page.GotoAsync("https://www.theverge.com/"); + await page.GotoAsync(Server.EmptyPage); + await Expect(page).ToHaveURLAsync(Server.EmptyPage); + var response = await page.GoBackAsync(); + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + Assert.AreEqual(response.Request.Url, "https://www.theverge.com/"); + Assert.AreEqual(await page.EvaluateAsync("location.href"), "https://www.theverge.com/"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should goForward to redirected navigation")] + // Flaky in firefox + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldGoForwardToRedirectedNavigation() + { + var path = TestUtils.GetAsset("har-redirect.har"); + await Context.RouteFromHARAsync(path, new() { UrlRegex = new Regex(".*theverge.*") }); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.EmptyPage); + await Expect(page).ToHaveURLAsync(Server.EmptyPage); + await page.GotoAsync("https://www.theverge.com/"); + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + await page.GoBackAsync(); + await Expect(page).ToHaveURLAsync(Server.EmptyPage); + var response = await page.GoForwardAsync(); + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + Assert.AreEqual(response.Request.Url, "https://www.theverge.com/"); + Assert.AreEqual(await page.EvaluateAsync("location.href"), "https://www.theverge.com/"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should reload redirected navigation")] + public async Task ShouldReloadRedirectedNavigation() + { + var path = TestUtils.GetAsset("har-redirect.har"); + await Context.RouteFromHARAsync(path, new() { UrlRegex = new Regex(".*theverge.*") }); + var page = await Context.NewPageAsync(); + await page.GotoAsync("https://www.theverge.com/"); + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + var response = await page.ReloadAsync(); + await Expect(page).ToHaveURLAsync("https://www.theverge.com/"); + Assert.AreEqual(response.Request.Url, "https://www.theverge.com/"); + Assert.AreEqual(await page.EvaluateAsync("location.href"), "https://www.theverge.com/"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should fulfill from har with content in a file")] + public async Task ShouldFulfillFromHarWithContentInAFile() + { + var path = TestUtils.GetAsset("har-sha1.har"); + await Context.RouteFromHARAsync(path); + var page = await Context.NewPageAsync(); + await page.GotoAsync("http://no.playwright/"); + Assert.AreEqual(await page.ContentAsync(), "Hello, world"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should round-trip har.zip")] + public async Task ShouldRoundTripHarZip() + { + using var tmpDir = new TempDirectory(); + var harPath = Path.Join(tmpDir.Path, "har.zip"); + var context1 = await Browser.NewContextAsync(new() { RecordHarMode = HarMode.Minimal, RecordHarPath = harPath }); + var page1 = await context1.NewPageAsync(); + await page1.GotoAsync(Server.Prefix + "/one-style.html"); + await context1.CloseAsync(); + + var context2 = await Browser.NewContextAsync(); + await context2.RouteFromHARAsync(harPath, new() { NotFound = HarNotFound.Abort }); + var page2 = await context2.NewPageAsync(); + await page2.GotoAsync(Server.Prefix + "/one-style.html"); + StringAssert.Contains("hello, world", await page2.ContentAsync()); + await Expect(page2.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should round-trip extracted har.zip")] + public async Task ShouldRoundTripExtractedHarZip() + { + using var tmpDir = new TempDirectory(); + var harPath = Path.Join(tmpDir.Path, "har.zip"); + var context1 = await Browser.NewContextAsync(new() { RecordHarMode = HarMode.Minimal, RecordHarPath = harPath }); + var page1 = await context1.NewPageAsync(); + await page1.GotoAsync(Server.Prefix + "/one-style.html"); + await context1.CloseAsync(); + + ZipFile.ExtractToDirectory(harPath, tmpDir.ToString()); + + var context2 = await Browser.NewContextAsync(); + await context2.RouteFromHARAsync(Path.Join(tmpDir.ToString(), "har.har")); + var page2 = await context2.NewPageAsync(); + await page2.GotoAsync(Server.Prefix + "/one-style.html"); + StringAssert.Contains("hello, world", await page2.ContentAsync()); + await Expect(page2.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should round-trip har with postData")] + public async Task ShouldRoundTripHarWithPostData() + { + Server.SetRoute("/echo", ctx => ctx.Request.Body.CopyToAsync(ctx.Response.Body)); + + using var tmpDir = new TempDirectory(); + var harPath = Path.Join(tmpDir.Path, "har.zip"); + var context1 = await Browser.NewContextAsync(new() { RecordHarMode = HarMode.Minimal, RecordHarPath = harPath }); + var page1 = await context1.NewPageAsync(); + await page1.GotoAsync(Server.EmptyPage); + + var fetchFunction = @"async (body) => { + const response = await fetch('/echo', { method: 'POST', body }); + return await response.text(); + };"; + + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "1"), "1"); + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "2"), "2"); + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "3"), "3"); + await context1.CloseAsync(); + + var context2 = await Browser.NewContextAsync(); + await context2.RouteFromHARAsync(harPath); + var page2 = await context2.NewPageAsync(); + await page2.GotoAsync(Server.EmptyPage); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "1"), "1"); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "2"), "2"); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "3"), "3"); + await PlaywrightAssert.ThrowsAsync(() => page2.EvaluateAsync(fetchFunction, "4")); + } + + [PlaywrightTest("browsercontext-har.spec.ts", "should disambiguate by header")] + public async Task ShouldDisambiguateByHeader() + { + Server.SetRoute("/echo", async ctx => + { + await ctx.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(ctx.Request.Headers["baz"][0])); + await ctx.Response.CompleteAsync(); + }); + + using var tmpDir = new TempDirectory(); + var harPath = Path.Join(tmpDir.Path, "har.zip"); + var context1 = await Browser.NewContextAsync(new() { RecordHarMode = HarMode.Minimal, RecordHarPath = harPath }); + var page1 = await context1.NewPageAsync(); + await page1.GotoAsync(Server.EmptyPage); + + var fetchFunction = @"async (bazValue) => { + const response = await fetch('/echo', { + method: 'POST', + body: '', + headers: { + foo: 'foo-value', + bar: 'bar-value', + baz: bazValue, + } + }); + return await response.text(); + }"; + + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "baz1"), "baz1"); + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "baz2"), "baz2"); + Assert.AreEqual(await page1.EvaluateAsync(fetchFunction, "baz3"), "baz3"); + await context1.CloseAsync(); + + var context2 = await Browser.NewContextAsync(); + await context2.RouteFromHARAsync(harPath); + var page2 = await context2.NewPageAsync(); + await page2.GotoAsync(Server.EmptyPage); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "baz1"), "baz1"); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "baz2"), "baz2"); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "baz3"), "baz3"); + Assert.AreEqual(await page2.EvaluateAsync(fetchFunction, "baz4"), "baz1"); + } + } +} diff --git a/src/Playwright/API/Generated/IRequest.cs b/src/Playwright/API/Generated/IRequest.cs index 52201e3ce8..88aa424d21 100644 --- a/src/Playwright/API/Generated/IRequest.cs +++ b/src/Playwright/API/Generated/IRequest.cs @@ -101,11 +101,12 @@ public partial interface IRequest /// /// - /// **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use instead. + /// An object with the request HTTP headers. The header names are lower-cased. Note + /// that this method does not return security-related headers, including cookie-related + /// ones. You can use for complete list of headers + /// that include cookie information. /// /// - [System.Obsolete] Dictionary Headers { get; } /// diff --git a/src/Playwright/API/Generated/IResponse.cs b/src/Playwright/API/Generated/IResponse.cs index c8dfb9da07..ee005bddae 100644 --- a/src/Playwright/API/Generated/IResponse.cs +++ b/src/Playwright/API/Generated/IResponse.cs @@ -64,11 +64,12 @@ public partial interface IResponse /// /// - /// **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use instead. + /// An object with the response HTTP headers. The header names are lower-cased. Note + /// that this method does not return security-related headers, including cookie-related + /// ones. You can use for complete list of headers + /// that include cookie information. /// /// - [System.Obsolete] Dictionary Headers { get; } /// diff --git a/src/Playwright/Core/Browser.cs b/src/Playwright/Core/Browser.cs index 6b99c96e67..aa08ef8c3d 100644 --- a/src/Playwright/Core/Browser.cs +++ b/src/Playwright/Core/Browser.cs @@ -104,8 +104,12 @@ public async Task NewContextAsync(BrowserNewContextOptions opti offline: options.Offline, permissions: options.Permissions, proxy: options.Proxy, + recordHarContent: options.RecordHarContent, + recordHarMode: options.RecordHarMode, recordHarOmitContent: options.RecordHarOmitContent, recordHarPath: options.RecordHarPath, + recordHarUrlFilterString: options.RecordHarUrlFilterString, + recordHarUrlFilterRegex: options.RecordHarUrlFilterRegex, recordVideo: GetVideoArgs(options.RecordVideoDir, options.RecordVideoSize), storageState: options.StorageState, storageStatePath: options.StorageStatePath, @@ -151,7 +155,11 @@ public async Task NewPageAsync(BrowserNewPageOptions options = default) ReducedMotion = options.ReducedMotion, ForcedColors = options.ForcedColors, RecordHarPath = options.RecordHarPath, + RecordHarContent = options.RecordHarContent, + RecordHarMode = options.RecordHarMode, RecordHarOmitContent = options.RecordHarOmitContent, + RecordHarUrlFilterString = options.RecordHarUrlFilterString, + RecordHarUrlFilterRegex = options.RecordHarUrlFilterRegex, RecordVideoDir = options.RecordVideoDir, RecordVideoSize = options.RecordVideoSize, Proxy = options.Proxy, diff --git a/src/Playwright/Core/BrowserContext.cs b/src/Playwright/Core/BrowserContext.cs index 5574374279..acb9e4482e 100644 --- a/src/Playwright/Core/BrowserContext.cs +++ b/src/Playwright/Core/BrowserContext.cs @@ -496,6 +496,15 @@ private Task ExposeBindingAsync(string name, Delegate callback, bool handle = fa return Channel.ExposeBindingAsync(name, handle); } - public Task RouteFromHARAsync(string har, BrowserContextRouteFromHAROptions options = null) => throw new NotImplementedException(); + public async Task RouteFromHARAsync(string har, BrowserContextRouteFromHAROptions options = null) + { + var harRouter = await HarRouter.CreateAsync(Channel.Connection.LocalUtils, har, options?.NotFound ?? HarNotFound.Abort, new() + { + UrlFunc = options?.UrlFunc, + UrlRegex = options?.UrlRegex, + UrlString = options?.UrlString, + }).ConfigureAwait(false); + await harRouter.AddContextRouteAsync(this).ConfigureAwait(false); + } } } diff --git a/src/Playwright/Core/BrowserType.cs b/src/Playwright/Core/BrowserType.cs index 778cd47c63..b2736100c8 100644 --- a/src/Playwright/Core/BrowserType.cs +++ b/src/Playwright/Core/BrowserType.cs @@ -120,8 +120,12 @@ public async Task LaunchPersistentContextAsync(string userDataD httpCredentials: options.HttpCredentials, colorScheme: options.ColorScheme, reducedMotion: options.ReducedMotion, + recordHarContent: options.RecordHarContent, + recordHarMode: options.RecordHarMode, recordHarPath: options.RecordHarPath, recordHarOmitContent: options.RecordHarOmitContent, + recordHarUrlFilterString: options.RecordHarUrlFilterString, + recordHarUrlFilterRegex: options.RecordHarUrlFilterRegex, recordVideo: Browser.GetVideoArgs(options.RecordVideoDir, options.RecordVideoSize), serviceWorkers: options.ServiceWorkers, ignoreDefaultArgs: options.IgnoreDefaultArgs, diff --git a/src/Playwright/Core/HarRouter.cs b/src/Playwright/Core/HarRouter.cs new file mode 100644 index 0000000000..67faf08ba5 --- /dev/null +++ b/src/Playwright/Core/HarRouter.cs @@ -0,0 +1,156 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Playwright.Helpers; + +namespace Microsoft.Playwright.Core +{ + internal class HarRouter + { + private readonly LocalUtils _localUtils; + + private readonly string _harId; + + private readonly HarRouterOptions _options; + + private readonly HarNotFound _notFoundAction; + + private HarRouter(LocalUtils localUtils, string harId, HarNotFound notFoundAction, HarRouterOptions options) + { + _localUtils = localUtils; + _harId = harId; + _options = options; + _notFoundAction = notFoundAction; + } + + internal static async Task CreateAsync(LocalUtils localUtils, string file, HarNotFound notFoundAction, HarRouterOptions options) + { + var (harId, error) = await localUtils.HarOpenAsync(file).ConfigureAwait(false); + if (!string.IsNullOrEmpty(error)) + { + throw new PlaywrightException(error); + } + return new HarRouter(localUtils, harId, notFoundAction, options); + } + + private async Task HandleAsync(Route route) + { + var request = route.Request; + var response = await _localUtils.HarLookupAsync( + harId: _harId, + url: request.Url, + method: request.Method, + headers: (await request.HeadersArrayAsync().ConfigureAwait(false)).ToList(), + postData: request.PostDataBuffer, + isNavigationRequest: request.IsNavigationRequest).ConfigureAwait(false); + + if (response.Action == "redirect") + { + await route.RedirectNavigationRequestAsync(response.RedirectURL).ConfigureAwait(false); + return; + } + if (response.Action == "fulfill") + { + await route.FulfillAsync(new() + { + Status = response.Status, + Headers = new RawHeaders(response.Headers).Headers, + BodyBytes = response.Body, + }).ConfigureAwait(false); + return; + } + if (response.Action == "error") + { + // Report the error, but fall through to the default handler. + } + + if (_notFoundAction == HarNotFound.Abort) + { + await route.AbortAsync().ConfigureAwait(false); + return; + } + + await route.FallbackAsync().ConfigureAwait(false); + } + + internal async Task AddContextRouteAsync(BrowserContext context) + { + if (!string.IsNullOrEmpty(_options.UrlString)) + { + await context.RouteAsync(_options.UrlString, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else if (_options.UrlRegex != null) + { + await context.RouteAsync(_options.UrlRegex, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else if (_options.UrlFunc != null) + { + await context.RouteAsync(_options.UrlFunc, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else + { + await context.RouteAsync("**/*", route => HandleAsync((Route)route)).ConfigureAwait(false); + } + + context.Close += (_, _) => Dispose(); + } + + internal async Task AddPageRouteAsync(Page page) + { + if (!string.IsNullOrEmpty(_options.UrlString)) + { + await page.RouteAsync(_options.UrlString, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else if (_options.UrlRegex != null) + { + await page.RouteAsync(_options.UrlRegex, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else if (_options.UrlFunc != null) + { + await page.RouteAsync(_options.UrlFunc, route => HandleAsync((Route)route)).ConfigureAwait(false); + } + else + { + await page.RouteAsync("**/*", route => HandleAsync((Route)route)).ConfigureAwait(false); + } + + page.Close += (_, _) => Dispose(); + } + + private void Dispose() => _localUtils.HarCloseAsync(_harId).IgnoreException(); + } + + internal class HarRouterOptions + { + internal string UrlString { get; set; } + + internal Regex UrlRegex { get; set; } + + internal Func UrlFunc { get; set; } + } +} diff --git a/src/Playwright/Core/LocalUtils.cs b/src/Playwright/Core/LocalUtils.cs index a1dfd658c5..18da30c53b 100644 --- a/src/Playwright/Core/LocalUtils.cs +++ b/src/Playwright/Core/LocalUtils.cs @@ -24,9 +24,7 @@ using System.Collections.Generic; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; -using Microsoft.Playwright.Helpers; using Microsoft.Playwright.Transport; using Microsoft.Playwright.Transport.Channels; using Microsoft.Playwright.Transport.Protocol; @@ -48,5 +46,35 @@ public LocalUtils(IChannelOwner parent, string guid, JsonElement? initializer) : internal Task ZipAsync(string zipFile, List entries) => _channel.ZipAsync(zipFile, entries); + + internal Task<(string HarId, string Error)> HarOpenAsync(string file) + => _channel.HarOpenAsync(file); + + internal Task HarLookupAsync( + string harId, + string url, + string method, + List
headers, + byte[] postData, + bool isNavigationRequest) + => _channel.HarLookupAsync(harId, url, method, headers, postData, isNavigationRequest); + + internal Task HarCloseAsync(string harId) + => _channel.HarCloseAsync(harId); + } + + internal class LocalUtilsHarLookupResult + { + public string Action { get; set; } + + public string Message { get; set; } + + public string RedirectURL { get; set; } + + public int Status { get; set; } + + public List Headers { get; set; } + + public byte[] Body { get; set; } } } diff --git a/src/Playwright/Core/Page.cs b/src/Playwright/Core/Page.cs index ddcc4b8179..45b9cfe28b 100644 --- a/src/Playwright/Core/Page.cs +++ b/src/Playwright/Core/Page.cs @@ -1153,6 +1153,15 @@ private FrameSetInputFilesOptions Map(PageSetInputFilesOptions options) }; } - public Task RouteFromHARAsync(string har, PageRouteFromHAROptions options = null) => throw new NotImplementedException(); + public async Task RouteFromHARAsync(string har, PageRouteFromHAROptions options = null) + { + var harRouter = await HarRouter.CreateAsync(Channel.Connection.LocalUtils, har, options?.NotFound ?? HarNotFound.Abort, new() + { + UrlFunc = options?.UrlFunc, + UrlRegex = options?.UrlRegex, + UrlString = options?.UrlString, + }).ConfigureAwait(false); + await harRouter.AddPageRouteAsync(this).ConfigureAwait(false); + } } } diff --git a/src/Playwright/Core/Route.cs b/src/Playwright/Core/Route.cs index 6b8796ffe1..58823cf146 100644 --- a/src/Playwright/Core/Route.cs +++ b/src/Playwright/Core/Route.cs @@ -218,6 +218,13 @@ public Task FallbackAsync(RouteFallbackOptions options = null) return Task.CompletedTask; } + internal async Task RedirectNavigationRequestAsync(string url) + { + CheckNotHandled(); + await RaceWithPageCloseAsync(_channel.RedirectNavigationRequestAsync(url)).ConfigureAwait(false); + ReportHandled(true); + } + internal Task StartHandlingAsync() { _handlingTask = new(); diff --git a/src/Playwright/Transport/Channels/BrowserChannel.cs b/src/Playwright/Transport/Channels/BrowserChannel.cs index 23a5976deb..3a366a1501 100644 --- a/src/Playwright/Transport/Channels/BrowserChannel.cs +++ b/src/Playwright/Transport/Channels/BrowserChannel.cs @@ -27,8 +27,10 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Playwright.Core; +using Microsoft.Playwright.Helpers; using Microsoft.Playwright.Transport.Protocol; namespace Microsoft.Playwright.Transport.Channels @@ -41,6 +43,49 @@ public BrowserChannel(string guid, Connection connection, Browser owner) : base( internal event EventHandler Closed; + internal static void ApplyHarOptions( + HarContentPolicy? recordHarContent, + HarMode? recordHarMode, + string recordHarPath, + bool? recordHarOmitContent, + string recordHarUrlFilterString, + Regex recordHarUrlFilterRegex, + Dictionary args) + { + if (string.IsNullOrEmpty(recordHarPath)) + { + return; + } + var recordHarArgs = new Dictionary(); + recordHarArgs["path"] = recordHarPath; + if (recordHarContent.HasValue) + { + recordHarArgs["content"] = recordHarContent; + } + else if (recordHarOmitContent == true) + { + recordHarArgs["content"] = HarContentPolicy.Omit; + } + if (!string.IsNullOrEmpty(recordHarUrlFilterString)) + { + recordHarArgs["urlGlob"] = recordHarUrlFilterString; + } + else if (recordHarUrlFilterRegex != null) + { + recordHarArgs["urlRegex"] = recordHarUrlFilterRegex; + recordHarArgs["urlRegexFlags"] = recordHarUrlFilterRegex.Options.GetInlineFlags(); + } + if (recordHarMode.HasValue) + { + recordHarArgs["mode"] = recordHarMode; + } + + if (recordHarArgs.Keys.Count > 0) + { + args.Add("recordHar", recordHarArgs); + } + } + internal override void OnMessage(string method, JsonElement? serverParams) { switch (method) @@ -69,8 +114,12 @@ internal Task NewContextAsync( bool? offline = null, IEnumerable permissions = null, Proxy proxy = null, - bool? recordHarOmitContent = null, - string recordHarPath = null, + HarContentPolicy? recordHarContent = default, + HarMode? recordHarMode = default, + string recordHarPath = default, + bool? recordHarOmitContent = default, + string recordHarUrlFilterString = default, + Regex recordHarUrlFilterRegex = default, Dictionary recordVideo = null, string storageState = null, string storageStatePath = null, @@ -110,14 +159,14 @@ internal Task NewContextAsync( args.Add("strictSelectors", strictSelectors); args.Add("forcedColors", forcedColors); - if (!string.IsNullOrEmpty(recordHarPath)) - { - args.Add("recordHar", new - { - Path = recordHarPath, - OmitContent = recordHarOmitContent, - }); - } + ApplyHarOptions( + recordHarContent: recordHarContent, + recordHarMode: recordHarMode, + recordHarPath: recordHarPath, + recordHarOmitContent: recordHarOmitContent, + recordHarUrlFilterString: recordHarUrlFilterString, + recordHarUrlFilterRegex: recordHarUrlFilterRegex, + args); if (recordVideo != null) { diff --git a/src/Playwright/Transport/Channels/BrowserTypeChannel.cs b/src/Playwright/Transport/Channels/BrowserTypeChannel.cs index 106f888af6..6a9ce5f92a 100644 --- a/src/Playwright/Transport/Channels/BrowserTypeChannel.cs +++ b/src/Playwright/Transport/Channels/BrowserTypeChannel.cs @@ -24,8 +24,8 @@ using System.Collections.Generic; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.Playwright.Core; using Microsoft.Playwright.Helpers; namespace Microsoft.Playwright.Transport.Channels @@ -121,8 +121,12 @@ internal Task LaunchPersistentContextAsync( ColorScheme? colorScheme = default, ReducedMotion? reducedMotion = default, ForcedColors? forcedColors = default, + HarContentPolicy? recordHarContent = default, + HarMode? recordHarMode = default, string recordHarPath = default, bool? recordHarOmitContent = default, + string recordHarUrlFilterString = default, + Regex recordHarUrlFilterRegex = default, Dictionary recordVideo = default, ServiceWorkerPolicy? serviceWorkers = default, IEnumerable ignoreDefaultArgs = default, @@ -155,6 +159,15 @@ internal Task LaunchPersistentContextAsync( { "serviceWorkers", serviceWorkers }, }; + BrowserChannel.ApplyHarOptions( + recordHarContent: recordHarContent, + recordHarMode: recordHarMode, + recordHarPath: recordHarPath, + recordHarOmitContent: recordHarOmitContent, + recordHarUrlFilterString: recordHarUrlFilterString, + recordHarUrlFilterRegex: recordHarUrlFilterRegex, + args: channelArgs); + if (viewportSize?.Width == -1) { channelArgs.Add("noDefaultViewport", true); @@ -180,15 +193,6 @@ internal Task LaunchPersistentContextAsync( channelArgs.Add("colorScheme", colorScheme); channelArgs.Add("reducedMotion", reducedMotion); - if (!string.IsNullOrEmpty(recordHarPath)) - { - channelArgs.Add("recordHar", new - { - Path = recordHarPath, - OmitContent = recordHarOmitContent.GetValueOrDefault(false), - }); - } - if (recordVideo != null) { channelArgs.Add("recordVideo", recordVideo); diff --git a/src/Playwright/Transport/Channels/LocalUtilsChannel.cs b/src/Playwright/Transport/Channels/LocalUtilsChannel.cs index 222b417a1a..7db64c364d 100644 --- a/src/Playwright/Transport/Channels/LocalUtilsChannel.cs +++ b/src/Playwright/Transport/Channels/LocalUtilsChannel.cs @@ -27,6 +27,7 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.Playwright.Core; +using Microsoft.Playwright.Helpers; using Microsoft.Playwright.Transport.Protocol; namespace Microsoft.Playwright.Transport.Channels @@ -43,5 +44,40 @@ internal Task ZipAsync(string zipFile, List entries) => { "zipFile", zipFile }, { "entries", entries }, }); + + internal async Task<(string HarId, string Error)> HarOpenAsync(string file) + { + var response = await Connection.SendMessageToServerAsync(Guid, "harOpen", new Dictionary + { + { "file", file }, + }).ConfigureAwait(false); + return (response.GetString("harId", true), response.GetString("error", true)); + } + + internal async Task HarLookupAsync( + string harId, + string url, + string method, + List
headers, + byte[] postData, + bool isNavigationRequest) + { + var response = await Connection.SendMessageToServerAsync(Guid, "harLookup", new Dictionary + { + { "harId", harId }, + { "url", url }, + { "method", method }, + { "headers", headers }, + { "postData", postData != null ? Convert.ToBase64String(postData) : null }, + { "isNavigationRequest", isNavigationRequest }, + }).ConfigureAwait(false); + return response; + } + + internal Task HarCloseAsync(string harId) => + Connection.SendMessageToServerAsync(Guid, "HarCloseAsync", new Dictionary + { + { "harId", harId }, + }); } } diff --git a/src/Playwright/Transport/Channels/RouteChannel.cs b/src/Playwright/Transport/Channels/RouteChannel.cs index 63f550d612..ab9089614b 100644 --- a/src/Playwright/Transport/Channels/RouteChannel.cs +++ b/src/Playwright/Transport/Channels/RouteChannel.cs @@ -74,5 +74,14 @@ public Task ContinueAsync(string url, string method, byte[] postData, IEnumerabl "continue", args); } + + internal Task RedirectNavigationRequestAsync(string url) => + Connection.SendMessageToServerAsync( + Guid, + "redirectNavigationRequest", + new Dictionary + { + ["url"] = url, + }); } }