From 787f20c920dade722b046e3199ea8fe38e3eae7f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 23 Aug 2024 16:26:39 +0200 Subject: [PATCH 01/47] chore: fix doclint (#32294) --- docs/src/network.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/network.md b/docs/src/network.md index c7e6bdca1c..152231556e 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -191,7 +191,7 @@ test('should use custom proxy on a new context', async ({ browser }) => { } }); const page = await context.newPage(); - + await context.close(); }); ``` From 0d4d5758c469187e268a431fdcf0588211f4813f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 23 Aug 2024 16:59:55 +0200 Subject: [PATCH 02/47] test: update Modernizer tests to Safari 18 (#32290) Fixes https://github.com/microsoft/playwright/issues/32288 --------- Signed-off-by: Max Schmitt Co-authored-by: Dmitry Gozman --- tests/assets/detect-touch.html | 12 - tests/assets/modernizr.html | 21 - tests/assets/modernizr.js | 5 - tests/assets/modernizr/README.md | 18 + tests/assets/modernizr/index.html | 26 + ...safari-14-1.json => mobile-safari-18.json} | 615 ++- tests/assets/modernizr/modernizr.js | 4147 +++++++++++++++++ tests/assets/modernizr/roll.sh | 18 + .../{safari-14-1.json => safari-18.json} | 625 ++- .../browsercontext-viewport-mobile.spec.ts | 11 +- tests/library/browsercontext-viewport.spec.ts | 3 - tests/library/modernizr.spec.ts | 84 +- 12 files changed, 4990 insertions(+), 595 deletions(-) delete mode 100644 tests/assets/detect-touch.html delete mode 100644 tests/assets/modernizr.html delete mode 100644 tests/assets/modernizr.js create mode 100644 tests/assets/modernizr/README.md create mode 100644 tests/assets/modernizr/index.html rename tests/assets/modernizr/{mobile-safari-14-1.json => mobile-safari-18.json} (67%) create mode 100644 tests/assets/modernizr/modernizr.js create mode 100644 tests/assets/modernizr/roll.sh rename tests/assets/modernizr/{safari-14-1.json => safari-18.json} (65%) diff --git a/tests/assets/detect-touch.html b/tests/assets/detect-touch.html deleted file mode 100644 index 80a4123fbd..0000000000 --- a/tests/assets/detect-touch.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - Detect Touch Test - - - - - - diff --git a/tests/assets/modernizr.html b/tests/assets/modernizr.html deleted file mode 100644 index 5096ffba74..0000000000 --- a/tests/assets/modernizr.html +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/tests/assets/modernizr.js b/tests/assets/modernizr.js deleted file mode 100644 index 1a03ac53b5..0000000000 --- a/tests/assets/modernizr.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! modernizr 3.6.0 (Custom Build) | MIT * - * https://modernizr.com/download/?-MessageChannel-adownload-ambientlight-animation-apng-appearance-applicationcache-arrow-atobbtoa-audio-audioloop-audiopreload-backdropfilter-backgroundblendmode-backgroundcliptext-backgroundsize-batteryapi-bdi-beacon-bgpositionshorthand-bgpositionxy-bgrepeatspace_bgrepeatround-bgsizecover-blobconstructor-bloburls-blobworkers-borderimage-borderradius-boxshadow-boxsizing-canvas-canvasblending-canvastext-canvaswinding-capture-checked-classlist-contains-contenteditable-contextmenu-cookies-cors-createelementattrs_createelement_attrs-cryptography-cssall-cssanimations-csscalc-csschunit-csscolumns-cssescape-cssexunit-cssfilters-cssgradients-cssgrid_cssgridlegacy-csshyphens_softhyphens_softhyphensfind-cssinvalid-cssmask-csspointerevents-csspositionsticky-csspseudoanimations-csspseudotransitions-cssreflections-cssremunit-cssresize-cssscrollbar-csstransforms-csstransforms3d-csstransformslevel2-csstransitions-cssvalid-cssvhunit-cssvmaxunit-cssvminunit-cssvwunit-cubicbezierrange-customelements-customevent-customprotocolhandler-dart-datachannel-datalistelem-dataset-datauri-dataview-dataworkers-details-devicemotion_deviceorientation-directory-display_runin-displaytable-documentfragment-ellipsis-emoji-es5-es5array-es5date-es5function-es5object-es5string-es5syntax-es5undefined-es6array-es6collections-es6math-es6number-es6object-es6string-eventlistener-eventsource-exiforientation-fetch-fileinput-filereader-filesystem-flash-flexbox-flexboxlegacy-flexboxtweener-flexwrap-focuswithin-fontface-forcetouch-formattribute-formvalidation-framed-fullscreen-gamepads-generatedcontent-generators-geolocation-getrandomvalues-getusermedia-hairline-hashchange-hidden-hiddenscroll-history-hovermq-hsla-htmlimports-ie8compat-imgcrossorigin-indexeddb-indexeddbblob-inlinesvg-input-inputformaction-inputformenctype-inputformmethod-inputformtarget-inputtypes-intl-jpeg2000-jpegxr-json-lastchild-ligatures-localizednumber-localstorage-lowbandwidth-lowbattery-matchmedia-mathml-mediaqueries-microdata-multiplebgs-mutationobserver-notification-nthchild-objectfit-olreversed-oninput-opacity-outputelem-overflowscrolling-pagevisibility-passiveeventlisteners-peerconnection-performance-picture-placeholder-pointerevents-pointerlock-pointermq-postmessage-preserve3d-progressbar_meter-promises-proximity-queryselector-quotamanagement-regions-requestanimationframe-requestautocomplete-rgba-ruby-sandbox-scriptasync-scriptdefer-scrollsnappoints-seamless-search-serviceworker-sessionstorage-shapes-sharedworkers-siblinggeneral-sizes-smil-speechrecognition-speechsynthesis-srcdoc-srcset-strictmode-stylescoped-subpixelfont-supports-svg-svgasimg-svgclippaths-svgfilters-svgforeignobject-target-template-templatestrings-textalignlast-textareamaxlength-textshadow-texttrackapi_track-time-todataurljpeg_todataurlpng_todataurlwebp-touchevents-transferables-typedarrays-unicode-unicoderange-unknownelements-urlparser-urlsearchparams-userdata-userselect-variablefonts-vibrate-video-videoautoplay-videocrossorigin-videoloop-videopreload-vml-webaudio-webgl-webglextensions-webintents-webp-webpalpha-webpanimation-webplossless_webp_lossless-websockets-websocketsbinary-websqldatabase-webworkers-willchange-wrapflow-xdomainrequest-xhr2-xhrresponsetype-xhrresponsetypearraybuffer-xhrresponsetypeblob-xhrresponsetypedocument-xhrresponsetypejson-xhrresponsetypetext-setclasses !*/ -!function(window,document,undefined){function is(A,e){return typeof A===e}function testRunner(){var A,e,t,n,r,o,i;for(var d in tests)if(tests.hasOwnProperty(d)){if(A=[],e=tests[d],e.name&&(A.push(e.name.toLowerCase()),e.options&&e.options.aliases&&e.options.aliases.length))for(t=0;td;d++)if(s=A[d],l=mStyle.style[s],contains(s,"-")&&(s=cssToDOM(s)),mStyle.style[s]!==undefined){if(n||is(t,"undefined"))return r(),"pfx"==e?s:!0;try{mStyle.style[s]=t}catch(u){}if(mStyle.style[s]!=l)return r(),"pfx"==e?s:!0}return r(),!1}function testPropsAll(A,e,t,n,r){var o=A.charAt(0).toUpperCase()+A.slice(1),i=(A+" "+cssomPrefixes.join(o+" ")+o).split(" ");return is(e,"string")||is(e,"undefined")?testProps(i,e,n,r):(i=(A+" "+domPrefixes.join(o+" ")+o).split(" "),testDOMProps(i,e,t))}function detectDeleteDatabase(A,e){var t=A.deleteDatabase(e);t.onsuccess=function(){addTest("indexeddb.deletedatabase",!0)},t.onerror=function(){addTest("indexeddb.deletedatabase",!1)}}function testAllProps(A,e,t){return testPropsAll(A,undefined,undefined,e,t)}var classes=[],tests=[],ModernizrProto={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(A,e){var t=this;setTimeout(function(){e(t[A])},0)},addTest:function(A,e,t){tests.push({name:A,fn:e,options:t})},addAsyncTest:function(A){tests.push({name:null,fn:A})}},Modernizr=function(){};Modernizr.prototype=ModernizrProto,Modernizr=new Modernizr,Modernizr.addTest("history",function(){var A=navigator.userAgent;return-1===A.indexOf("Android 2.")&&-1===A.indexOf("Android 4.0")||-1===A.indexOf("Mobile Safari")||-1!==A.indexOf("Chrome")||-1!==A.indexOf("Windows Phone")||"file:"===location.protocol?window.history&&"pushState"in window.history:!1}),Modernizr.addTest("ie8compat",!window.addEventListener&&!!document.documentMode&&7===document.documentMode),Modernizr.addTest("applicationcache","applicationCache"in window),Modernizr.addTest("blobconstructor",function(){try{return!!new Blob}catch(A){return!1}},{aliases:["blob-constructor"]}),Modernizr.addTest("cookies",function(){try{document.cookie="cookietest=1";var A=-1!=document.cookie.indexOf("cookietest=");return document.cookie="cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT",A}catch(e){return!1}}),Modernizr.addTest("cors","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest),Modernizr.addTest("customelements","customElements"in window),Modernizr.addTest("customprotocolhandler",function(){if(!navigator.registerProtocolHandler)return!1;try{navigator.registerProtocolHandler("thisShouldFail")}catch(A){return A instanceof TypeError}return!1}),Modernizr.addTest("customevent","CustomEvent"in window&&"function"==typeof window.CustomEvent),Modernizr.addTest("dataview","undefined"!=typeof DataView&&"getFloat64"in DataView.prototype),Modernizr.addTest("eventlistener","addEventListener"in window),Modernizr.addTest("geolocation","geolocation"in navigator),Modernizr.addTest("json","JSON"in window&&"parse"in JSON&&"stringify"in JSON),Modernizr.addTest("messagechannel","MessageChannel"in window),Modernizr.addTest("notification",function(){if(!window.Notification||!window.Notification.requestPermission)return!1;if("granted"===window.Notification.permission)return!0;try{new window.Notification("")}catch(A){if("TypeError"===A.name)return!1}return!0}),Modernizr.addTest("postmessage","postMessage"in window),Modernizr.addTest("queryselector","querySelector"in document&&"querySelectorAll"in document),Modernizr.addTest("serviceworker","serviceWorker"in navigator),Modernizr.addTest("svg",!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect),Modernizr.addTest("templatestrings",function(){var supports;try{eval("``"),supports=!0}catch(e){}return!!supports}),Modernizr.addTest("typedarrays","ArrayBuffer"in window);var supports=!1;try{supports="WebSocket"in window&&2===window.WebSocket.CLOSING}catch(e){}Modernizr.addTest("websockets",supports),Modernizr.addTest("xdomainrequest","XDomainRequest"in window),Modernizr.addTest("webaudio",function(){var A="webkitAudioContext"in window,e="AudioContext"in window;return Modernizr._config.usePrefixes?A||e:e});var CSS=window.CSS;Modernizr.addTest("cssescape",CSS?"function"==typeof CSS.escape:!1),Modernizr.addTest("focuswithin",function(){try{document.querySelector(":focus-within")}catch(A){return!1}return!0});var newSyntax="CSS"in window&&"supports"in window.CSS,oldSyntax="supportsCSS"in window;Modernizr.addTest("supports",newSyntax||oldSyntax),Modernizr.addTest("target",function(){var A=window.document;if(!("querySelectorAll"in A))return!1;try{return A.querySelectorAll(":target"),!0}catch(e){return!1}}),Modernizr.addTest("microdata","getItems"in document),Modernizr.addTest("mutationobserver",!!window.MutationObserver||!!window.WebKitMutationObserver),Modernizr.addTest("passiveeventlisteners",function(){var A=!1;try{var e=Object.defineProperty({},"passive",{get:function(){A=!0}});window.addEventListener("test",null,e)}catch(t){}return A}),Modernizr.addTest("picture","HTMLPictureElement"in window),Modernizr.addTest("es5array",function(){return!!(Array.prototype&&Array.prototype.every&&Array.prototype.filter&&Array.prototype.forEach&&Array.prototype.indexOf&&Array.prototype.lastIndexOf&&Array.prototype.map&&Array.prototype.some&&Array.prototype.reduce&&Array.prototype.reduceRight&&Array.isArray)}),Modernizr.addTest("es5date",function(){var A="2013-04-12T06:06:37.307Z",e=!1;try{e=!!Date.parse(A)}catch(t){}return!!(Date.now&&Date.prototype&&Date.prototype.toISOString&&Date.prototype.toJSON&&e)}),Modernizr.addTest("es5function",function(){return!(!Function.prototype||!Function.prototype.bind)}),Modernizr.addTest("beacon","sendBeacon"in navigator),Modernizr.addTest("lowbandwidth",function(){var A=navigator.connection||{type:0};return 3==A.type||4==A.type||/^[23]g$/.test(A.type)}),Modernizr.addTest("eventsource","EventSource"in window),Modernizr.addTest("fetch","fetch"in window),Modernizr.addTest("xhrresponsetype",function(){if("undefined"==typeof XMLHttpRequest)return!1;var A=new XMLHttpRequest;return A.open("get","/",!0),"response"in A}()),Modernizr.addTest("xhr2","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest),Modernizr.addTest("speechsynthesis","SpeechSynthesisUtterance"in window),Modernizr.addTest("localstorage",function(){var A="modernizr";try{return localStorage.setItem(A,A),localStorage.removeItem(A),!0}catch(e){return!1}}),Modernizr.addTest("sessionstorage",function(){var A="modernizr";try{return sessionStorage.setItem(A,A),sessionStorage.removeItem(A),!0}catch(e){return!1}}),Modernizr.addTest("websqldatabase","openDatabase"in window),Modernizr.addTest("es5object",function(){return!!(Object.keys&&Object.create&&Object.getPrototypeOf&&Object.getOwnPropertyNames&&Object.isSealed&&Object.isFrozen&&Object.isExtensible&&Object.getOwnPropertyDescriptor&&Object.defineProperty&&Object.defineProperties&&Object.seal&&Object.freeze&&Object.preventExtensions)}),Modernizr.addTest("svgfilters",function(){var A=!1;try{A="SVGFEColorMatrixElement"in window&&2==SVGFEColorMatrixElement.SVG_FECOLORMATRIX_TYPE_SATURATE}catch(e){}return A}),Modernizr.addTest("strictmode",function(){"use strict";return!this}()),Modernizr.addTest("es5string",function(){return!(!String.prototype||!String.prototype.trim)}),Modernizr.addTest("es5syntax",function(){var value,obj,stringAccess,getter,setter,reservedWords,zeroWidthChars;try{return stringAccess=eval('"foobar"[3] === "b"'),getter=eval("({ get x(){ return 1 } }).x === 1"),eval("({ set x(v){ value = v; } }).x = 1"),setter=1===value,eval("obj = ({ if: 1 })"),reservedWords=1===obj["if"],zeroWidthChars=eval("_‌‍ = true"),stringAccess&&getter&&setter&&reservedWords&&zeroWidthChars}catch(ignore){return!1}}),Modernizr.addTest("es5undefined",function(){var A,e;try{e=window.undefined,window.undefined=12345,A="undefined"==typeof window.undefined,window.undefined=e}catch(t){return!1}return A}),Modernizr.addTest("es5",function(){return!!(Modernizr.es5array&&Modernizr.es5date&&Modernizr.es5function&&Modernizr.es5object&&Modernizr.strictmode&&Modernizr.es5string&&Modernizr.json&&Modernizr.es5syntax&&Modernizr.es5undefined)}),Modernizr.addTest("es6array",!!(Array.prototype&&Array.prototype.copyWithin&&Array.prototype.fill&&Array.prototype.find&&Array.prototype.findIndex&&Array.prototype.keys&&Array.prototype.entries&&Array.prototype.values&&Array.from&&Array.of)),Modernizr.addTest("arrow",function(){try{eval("()=>{}")}catch(e){return!1}return!0}),Modernizr.addTest("es6collections",!!(window.Map&&window.Set&&window.WeakMap&&window.WeakSet)),Modernizr.addTest("generators",function(){try{new Function("function* test() {}")()}catch(A){return!1}return!0}),Modernizr.addTest("es6math",!!(Math&&Math.clz32&&Math.cbrt&&Math.imul&&Math.sign&&Math.log10&&Math.log2&&Math.log1p&&Math.expm1&&Math.cosh&&Math.sinh&&Math.tanh&&Math.acosh&&Math.asinh&&Math.atanh&&Math.hypot&&Math.trunc&&Math.fround)),Modernizr.addTest("es6number",!!(Number.isFinite&&Number.isInteger&&Number.isSafeInteger&&Number.isNaN&&Number.parseInt&&Number.parseFloat&&Number.isInteger(Number.MAX_SAFE_INTEGER)&&Number.isInteger(Number.MIN_SAFE_INTEGER)&&Number.isFinite(Number.EPSILON))),Modernizr.addTest("es6object",!!(Object.assign&&Object.is&&Object.setPrototypeOf)),Modernizr.addTest("promises",function(){return"Promise"in window&&"resolve"in window.Promise&&"reject"in window.Promise&&"all"in window.Promise&&"race"in window.Promise&&function(){var A;return new window.Promise(function(e){A=e}),"function"==typeof A}()}),Modernizr.addTest("es6string",!!(String.fromCodePoint&&String.raw&&String.prototype.codePointAt&&String.prototype.repeat&&String.prototype.startsWith&&String.prototype.endsWith&&String.prototype.includes)),Modernizr.addTest("devicemotion","DeviceMotionEvent"in window),Modernizr.addTest("deviceorientation","DeviceOrientationEvent"in window),Modernizr.addTest("filereader",!!(window.File&&window.FileList&&window.FileReader)),Modernizr.addTest("urlparser",function(){var A;try{return A=new URL("http://modernizr.com/"),"http://modernizr.com/"===A.href}catch(e){return!1}}),Modernizr.addTest("urlsearchparams","URLSearchParams"in window),Modernizr.addTest("framed",window.location!=top.location),Modernizr.addTest("webworkers","Worker"in window);var docElement=document.documentElement;Modernizr.addTest("contextmenu","contextMenu"in docElement&&"HTMLMenuItemElement"in window),Modernizr.addTest("cssall","all"in docElement.style),Modernizr.addTest("willchange","willChange"in docElement.style),Modernizr.addTest("classlist","classList"in docElement),Modernizr.addTest("documentfragment",function(){return"createDocumentFragment"in document&&"appendChild"in docElement}),Modernizr.addTest("contains",is(String.prototype.contains,"function"));var isSVG="svg"===docElement.nodeName.toLowerCase();Modernizr.addTest("audio",function(){var A=createElement("audio"),e=!1;try{e=!!A.canPlayType,e&&(e=new Boolean(e),e.ogg=A.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),e.mp3=A.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/,""),e.opus=A.canPlayType('audio/ogg; codecs="opus"')||A.canPlayType('audio/webm; codecs="opus"').replace(/^no$/,""),e.wav=A.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),e.m4a=(A.canPlayType("audio/x-m4a;")||A.canPlayType("audio/aac;")).replace(/^no$/,""))}catch(t){}return e}),Modernizr.addTest("canvas",function(){var A=createElement("canvas");return!(!A.getContext||!A.getContext("2d"))}),Modernizr.addTest("canvastext",function(){return Modernizr.canvas===!1?!1:"function"==typeof createElement("canvas").getContext("2d").fillText}),Modernizr.addTest("contenteditable",function(){if("contentEditable"in docElement){var A=createElement("div");return A.contentEditable=!0,"true"===A.contentEditable}}),Modernizr.addTest("emoji",function(){if(!Modernizr.canvastext)return!1;var A=window.devicePixelRatio||1,e=12*A,t=createElement("canvas"),n=t.getContext("2d");return n.fillStyle="#f00",n.textBaseline="top",n.font="32px Arial",n.fillText("🐨",0,0),0!==n.getImageData(e,e,1,1).data[0]}),Modernizr.addTest("olreversed","reversed"in createElement("ol")),Modernizr.addTest("userdata",!!createElement("div").addBehavior),Modernizr.addTest("video",function(){var A=createElement("video"),e=!1;try{e=!!A.canPlayType,e&&(e=new Boolean(e),e.ogg=A.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),e.h264=A.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),e.webm=A.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,""),e.vp9=A.canPlayType('video/webm; codecs="vp9"').replace(/^no$/,""),e.hls=A.canPlayType('application/x-mpegURL; codecs="avc1.42E01E"').replace(/^no$/,""))}catch(t){}return e}),Modernizr.addTest("vml",function(){var A,e=createElement("div"),t=!1;return isSVG||(e.innerHTML='',A=e.firstChild,"style"in A&&(A.style.behavior="url(#default#VML)"),t=A?"object"==typeof A.adj:!0),t}),Modernizr.addTest("webanimations","animate"in createElement("div")),Modernizr.addTest("webgl",function(){var A=createElement("canvas"),e="probablySupportsContext"in A?"probablySupportsContext":"supportsContext";return e in A?A[e]("webgl")||A[e]("experimental-webgl"):"WebGLRenderingContext"in window}),Modernizr.addTest("adownload",!window.externalHost&&"download"in createElement("a")),Modernizr.addTest("audioloop","loop"in createElement("audio")),Modernizr.addTest("canvasblending",function(){if(Modernizr.canvas===!1)return!1;var A=createElement("canvas").getContext("2d");try{A.globalCompositeOperation="screen"}catch(e){}return"screen"===A.globalCompositeOperation});var canvas=createElement("canvas");Modernizr.addTest("todataurljpeg",function(){return!!Modernizr.canvas&&0===canvas.toDataURL("image/jpeg").indexOf("data:image/jpeg")}),Modernizr.addTest("todataurlpng",function(){return!!Modernizr.canvas&&0===canvas.toDataURL("image/png").indexOf("data:image/png")}),Modernizr.addTest("todataurlwebp",function(){var A=!1;try{A=!!Modernizr.canvas&&0===canvas.toDataURL("image/webp").indexOf("data:image/webp")}catch(e){}return A}),Modernizr.addTest("canvaswinding",function(){if(Modernizr.canvas===!1)return!1;var A=createElement("canvas").getContext("2d");return A.rect(0,0,10,10),A.rect(2,2,6,6),A.isPointInPath(5,5,"evenodd")===!1}),Modernizr.addTest("bgpositionshorthand",function(){var A=createElement("a"),e=A.style,t="right 10px bottom 10px";return e.cssText="background-position: "+t+";",e.backgroundPosition===t}),Modernizr.addTest("multiplebgs",function(){var A=createElement("a").style;return A.cssText="background:url(https://),url(https://),red url(https://)",/(url\s*\(.*?){3}/.test(A.background)}),Modernizr.addTest("csspointerevents",function(){var A=createElement("a").style;return A.cssText="pointer-events:auto","auto"===A.pointerEvents}),Modernizr.addTest("cssremunit",function(){var A=createElement("a").style;try{A.fontSize="3rem"}catch(e){}return/rem/.test(A.fontSize)}),Modernizr.addTest("rgba",function(){var A=createElement("a").style;return A.cssText="background-color:rgba(150,255,150,.5)",(""+A.backgroundColor).indexOf("rgba")>-1}),Modernizr.addTest("preserve3d",function(){var A,e,t=window.CSS,n=!1;return t&&t.supports&&t.supports("(transform-style: preserve-3d)")?!0:(A=createElement("a"),e=createElement("a"),A.style.cssText="display: block; transform-style: preserve-3d; transform-origin: right; transform: rotateY(40deg);",e.style.cssText="display: block; width: 9px; height: 1px; background: #000; transform-origin: right; transform: rotateY(40deg);",A.appendChild(e),docElement.appendChild(A),n=e.getBoundingClientRect(),docElement.removeChild(A),n=n.width&&n.width<4)}),Modernizr.addTest("createelementattrs",function(){try{return"test"==createElement('').getAttribute("name")}catch(A){return!1}},{aliases:["createelement-attrs"]}),Modernizr.addTest("dataset",function(){var A=createElement("div");return A.setAttribute("data-a-b","c"),!(!A.dataset||"c"!==A.dataset.aB)}),Modernizr.addTest("hidden","hidden"in createElement("a")),Modernizr.addTest("outputelem","value"in createElement("output")),Modernizr.addTest("progressbar",createElement("progress").max!==undefined),Modernizr.addTest("meter",createElement("meter").max!==undefined),Modernizr.addTest("ruby",function(){function A(A,e){var t;return window.getComputedStyle?t=document.defaultView.getComputedStyle(A,null).getPropertyValue(e):A.currentStyle&&(t=A.currentStyle[e]),t}function e(){docElement.removeChild(t),t=null,n=null,r=null}var t=createElement("ruby"),n=createElement("rt"),r=createElement("rp"),o="display",i="fontSize";return t.appendChild(r),t.appendChild(n),docElement.appendChild(t),"none"==A(r,o)||"ruby"==A(t,o)&&"ruby-text"==A(n,o)||"6pt"==A(r,i)&&"6pt"==A(n,i)?(e(),!0):(e(),!1)}),Modernizr.addTest("template","content"in createElement("template")),Modernizr.addTest("srcset","srcset"in createElement("img")),Modernizr.addTest("time","valueAsDate"in createElement("time")),Modernizr.addTest("texttrackapi","function"==typeof createElement("video").addTextTrack),Modernizr.addTest("track","kind"in createElement("track")),Modernizr.addTest("unknownelements",function(){var A=createElement("a");return A.innerHTML="",1===A.childNodes.length}),Modernizr.addTest("inputformaction",!!("formAction"in createElement("input")),{aliases:["input-formaction"]}),Modernizr.addTest("inputformenctype",!!("formEnctype"in createElement("input")),{aliases:["input-formenctype"]}),Modernizr.addTest("inputformmethod",!!("formMethod"in createElement("input"))),Modernizr.addTest("inputformtarget",!!("formtarget"in createElement("input")),{aliases:["input-formtarget"]}),Modernizr.addTest("scriptasync","async"in createElement("script")),Modernizr.addTest("scriptdefer","defer"in createElement("script")),Modernizr.addTest("stylescoped","scoped"in createElement("style")),Modernizr.addTest("capture","capture"in createElement("input")),Modernizr.addTest("fileinput",function(){if(navigator.userAgent.match(/(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/))return!1;var A=createElement("input");return A.type="file",!A.disabled}),Modernizr.addTest("formattribute",function(){var A,e=createElement("form"),t=createElement("input"),n=createElement("div"),r="formtest"+(new Date).getTime(),o=!1;e.id=r;try{t.setAttribute("form",r)}catch(i){document.createAttribute&&(A=document.createAttribute("form"),A.nodeValue=r,t.setAttributeNode(A))}return n.appendChild(e),n.appendChild(t),docElement.appendChild(n),o=e.elements&&1===e.elements.length&&t.form==e,n.parentNode.removeChild(n),o}),Modernizr.addTest("placeholder","placeholder"in createElement("input")&&"placeholder"in createElement("textarea")),Modernizr.addTest("sandbox","sandbox"in createElement("iframe")),Modernizr.addTest("inlinesvg",function(){var A=createElement("div");return A.innerHTML="","http://www.w3.org/2000/svg"==("undefined"!=typeof SVGRect&&A.firstChild&&A.firstChild.namespaceURI)}),Modernizr.addTest("textareamaxlength",!!("maxLength"in createElement("textarea"))),Modernizr.addTest("videocrossorigin","crossOrigin"in createElement("video")),Modernizr.addAsyncTest(function(){if(Modernizr.webglextensions=!1,Modernizr.webgl){var A,e,t;try{A=createElement("canvas"),e=A.getContext("webgl")||A.getContext("experimental-webgl"),t=e.getSupportedExtensions()}catch(n){return}e!==undefined&&(Modernizr.webglextensions=new Boolean(!0));for(var r=-1,o=t.length;++r7}),Modernizr.addTest("inputsearchevent",hasEvent("search")),Modernizr.addTest("ambientlight",hasEvent("devicelight",window));var inputElem=createElement("input"),inputattrs="autocomplete autofocus list placeholder max min multiple pattern required step".split(" "),attrs={};Modernizr.input=function(A){for(var e=0,t=A.length;t>e;e++)attrs[A[e]]=!!(A[e]in inputElem);return attrs.list&&(attrs.list=!(!createElement("datalist")||!window.HTMLDataListElement)),attrs}(inputattrs),Modernizr.addTest("datalistelem",Modernizr.input.list);var inputtypes="search tel url email datetime date month week time datetime-local number range color".split(" "),inputs={};Modernizr.inputtypes=function(A){for(var e,t,n,r=A.length,o="1)",i=0;r>i;i++)inputElem.setAttribute("type",e=A[i]),n="text"!==inputElem.type&&"style"in inputElem,n&&(inputElem.value=o,inputElem.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(e)&&inputElem.style.WebkitAppearance!==undefined?(docElement.appendChild(inputElem),t=document.defaultView,n=t.getComputedStyle&&"textfield"!==t.getComputedStyle(inputElem,null).WebkitAppearance&&0!==inputElem.offsetHeight,docElement.removeChild(inputElem)):/^(search|tel)$/.test(e)||(n=/^(url|email)$/.test(e)?inputElem.checkValidity&&inputElem.checkValidity()===!1:inputElem.value!=o)),inputs[A[i]]=!!n;return inputs}(inputtypes),Modernizr.addTest("videoloop","loop"in createElement("video"));var prefixes=ModernizrProto._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];ModernizrProto._prefixes=prefixes,Modernizr.addTest("csscalc",function(){var A="width:",e="calc(10px);",t=createElement("a");return t.style.cssText=A+prefixes.join(e+A),!!t.style.length}),Modernizr.addTest("cubicbezierrange",function(){var A=createElement("a");return A.style.cssText=prefixes.join("transition-timing-function:cubic-bezier(1,0,0,1.1); "),!!A.style.length}),Modernizr.addTest("cssgradients",function(){for(var A,e="background-image:",t="gradient(linear,left top,right bottom,from(#9f9),to(white));",n="",r=0,o=prefixes.length-1;o>r;r++)A=0===r?"to ":"",n+=e+prefixes[r]+"linear-gradient("+A+"left top, #9f9, white);";Modernizr._config.usePrefixes&&(n+=e+"-webkit-"+t);var i=createElement("a"),d=i.style;return d.cssText=n,(""+d.backgroundImage).indexOf("gradient")>-1}),Modernizr.addTest("opacity",function(){var A=createElement("a").style;return A.cssText=prefixes.join("opacity:.55;"),/^0.55$/.test(A.opacity)}),Modernizr.addTest("csspositionsticky",function(){var A="position:",e="sticky",t=createElement("a"),n=t.style;return n.cssText=A+prefixes.join(e+";"+A).slice(0,-A.length),-1!==n.position.indexOf(e)});var modElem={elem:createElement("modernizr")};Modernizr._q.push(function(){delete modElem.elem}),Modernizr.addTest("csschunit",function(){var A,e=modElem.elem.style;try{e.fontSize="3ch",A=-1!==e.fontSize.indexOf("ch")}catch(t){A=!1}return A}),Modernizr.addTest("cssexunit",function(){var A,e=modElem.elem.style;try{e.fontSize="3ex",A=-1!==e.fontSize.indexOf("ex")}catch(t){A=!1}return A}),Modernizr.addTest("hsla",function(){var A=createElement("a").style;return A.cssText="background-color:hsla(120,40%,100%,.5)",contains(A.backgroundColor,"rgba")||contains(A.backgroundColor,"hsla")}),Modernizr.addTest("videopreload","preload"in createElement("video")),Modernizr.addTest("getUserMedia","mediaDevices"in navigator&&"getUserMedia"in navigator.mediaDevices),Modernizr.addTest("websocketsbinary",function(){var A,e="https:"==location.protocol?"wss":"ws";if("WebSocket"in window){if(A="binaryType"in WebSocket.prototype)return A;try{return!!new WebSocket(e+"://.").binaryType}catch(t){}}return!1}),Modernizr.addTest("atobbtoa","atob"in window&&"btoa"in window,{aliases:["atob-btoa"]}),Modernizr.addTest("sharedworkers","SharedWorker"in window),Modernizr.addTest("bdi",function(){var A=createElement("div"),e=createElement("bdi");e.innerHTML="إ",A.appendChild(e),docElement.appendChild(A);var t="rtl"===computedStyle(e,null,"direction");return docElement.removeChild(A),t});var testXhrType=function(A){if("undefined"==typeof XMLHttpRequest)return!1;var e=new XMLHttpRequest;e.open("get","/",!0);try{e.responseType=A}catch(t){return!1}return"response"in e&&e.responseType==A};Modernizr.addTest("xhrresponsetypearraybuffer",testXhrType("arraybuffer")),Modernizr.addTest("xhrresponsetypeblob",testXhrType("blob")),Modernizr.addTest("xhrresponsetypedocument",testXhrType("document")),Modernizr.addTest("xhrresponsetypejson",testXhrType("json")),Modernizr.addTest("xhrresponsetypetext",testXhrType("text"));var toStringFn={}.toString;Modernizr.addTest("svgclippaths",function(){return!!document.createElementNS&&/SVGClipPath/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","clipPath")))}),Modernizr.addTest("svgforeignobject",function(){return!!document.createElementNS&&/SVGForeignObject/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))}),Modernizr.addTest("smil",function(){return!!document.createElementNS&&/SVGAnimate/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","animate")))});var testStyles=ModernizrProto.testStyles=injectElementWithStyles;Modernizr.addTest("hiddenscroll",function(){return testStyles("#modernizr {width:100px;height:100px;overflow:scroll}",function(A){return A.offsetWidth===A.clientWidth})}),Modernizr.addTest("mathml",function(){var A;return testStyles("#modernizr{position:absolute;display:inline-block}",function(e){e.innerHTML+="xxyy",A=e.offsetHeight>e.offsetWidth}),A}),Modernizr.addTest("touchevents",function(){var A;if("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)A=!0;else{var e=["@media (",prefixes.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");testStyles(e,function(e){A=9===e.offsetTop})}return A}),Modernizr.addTest("unicoderange",function(){return Modernizr.testStyles('@font-face{font-family:"unicodeRange";src:local("Arial");unicode-range:U+0020,U+002E}#modernizr span{font-size:20px;display:inline-block;font-family:"unicodeRange",monospace}#modernizr .mono{font-family:monospace}',function(A){for(var e=[".",".","m","m"],t=0;t=9;return e||t}();blacklist?Modernizr.addTest("fontface",!1):testStyles('@font-face {font-family:"font";src:url("https://")}',function(A,e){var t=document.getElementById("smodernizr"),n=t.sheet||t.styleSheet,r=n?n.cssRules&&n.cssRules[0]?n.cssRules[0].cssText:n.cssText||"":"",o=/src/i.test(r)&&0===r.indexOf(e.split(" ")[0]);Modernizr.addTest("fontface",o); -}),testStyles('#modernizr{font:0/0 a}#modernizr:after{content:":)";visibility:hidden;font:7px/1 a}',function(A){Modernizr.addTest("generatedcontent",A.offsetHeight>=6)}),Modernizr.addTest("hairline",function(){return testStyles("#modernizr {border:.5px solid transparent}",function(A){return 1===A.offsetHeight})}),Modernizr.addTest("cssinvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:invalid{width:50px}",function(A){var e=createElement("input");return e.required=!0,A.appendChild(e),e.clientWidth>10})}),testStyles("#modernizr div {width:100px} #modernizr :last-child{width:200px;display:block}",function(A){Modernizr.addTest("lastchild",A.lastChild.offsetWidth>A.firstChild.offsetWidth)},2),testStyles("#modernizr div {width:1px} #modernizr div:nth-child(2n) {width:2px;}",function(A){for(var e=A.getElementsByTagName("div"),t=!0,n=0;5>n;n++)t=t&&e[n].offsetWidth===n%2+1;Modernizr.addTest("nthchild",t)},5),testStyles("#modernizr{overflow: scroll; width: 40px; height: 40px; }#"+prefixes.join("scrollbar{width:10px} #modernizr::").split("#").slice(1).join("#")+"scrollbar{width:10px}",function(A){Modernizr.addTest("cssscrollbar","scrollWidth"in A&&30==A.scrollWidth)}),Modernizr.addTest("siblinggeneral",function(){return testStyles("#modernizr div {width:100px} #modernizr div ~ div {width:200px;display:block}",function(A){return 200==A.lastChild.offsetWidth},2)}),testStyles("#modernizr{position: absolute; top: -10em; visibility:hidden; font: normal 10px arial;}#subpixel{float: left; font-size: 33.3333%;}",function(A){var e=A.firstChild;e.innerHTML="This is a text written in Arial",Modernizr.addTest("subpixelfont",window.getComputedStyle?"44px"!==window.getComputedStyle(e,null).getPropertyValue("width"):!1)},1,["subpixel"]),Modernizr.addTest("cssvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:valid{width:50px}",function(A){var e=createElement("input");return A.appendChild(e),e.clientWidth>10})}),testStyles("#modernizr { height: 50vh; }",function(A){var e=parseInt(window.innerHeight/2,10),t=parseInt(computedStyle(A,null,"height"),10);Modernizr.addTest("cssvhunit",roundedEquals(t,e))}),testStyles("#modernizr1{width: 50vmax}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(A){var e=A.childNodes[2],t=A.childNodes[1],n=A.childNodes[0],r=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=n.clientWidth/100,i=n.clientHeight/100,d=parseInt(50*Math.max(o,i),10),a=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvmaxunit",roundedEquals(d,a)||roundedEquals(d,a-r))},3),testStyles("#modernizr1{width: 50vm;width:50vmin}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(A){var e=A.childNodes[2],t=A.childNodes[1],n=A.childNodes[0],r=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=n.clientWidth/100,i=n.clientHeight/100,d=parseInt(50*Math.min(o,i),10),a=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvminunit",roundedEquals(d,a)||roundedEquals(d,a-r))},3),testStyles("#modernizr { width: 50vw; }",function(A){var e=parseInt(window.innerWidth/2,10),t=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvwunit",roundedEquals(t,e))}),Modernizr.addTest("details",function(){var A,e=createElement("details");return"open"in e?(testStyles("#modernizr details{display:block}",function(t){t.appendChild(e),e.innerHTML="ab",A=e.offsetHeight,e.open=!0,A=A!=e.offsetHeight}),A):!1}),Modernizr.addTest("oninput",function(){var A,e=createElement("input");if(e.setAttribute("oninput","return"),hasEvent("oninput",docElement)||"function"==typeof e.oninput)return!0;try{var t=document.createEvent("KeyboardEvent");A=!1;var n=function(e){A=!0,e.preventDefault(),e.stopPropagation()};t.initKeyEvent("keypress",!0,!0,window,!1,!1,!1,!1,0,"e".charCodeAt(0)),docElement.appendChild(e),e.addEventListener("input",n,!1),e.focus(),e.dispatchEvent(t),e.removeEventListener("input",n,!1),docElement.removeChild(e)}catch(r){A=!1}return A}),Modernizr.addTest("formvalidation",function(){var A=createElement("form");if(!("checkValidity"in A&&"addEventListener"in A))return!1;if("reportValidity"in A)return!0;var e,t=!1;return Modernizr.formvalidationapi=!0,A.addEventListener("submit",function(A){(!window.opera||window.operamini)&&A.preventDefault(),A.stopPropagation()},!1),A.innerHTML='',testStyles("#modernizr form{position:absolute;top:-99999em}",function(n){n.appendChild(A),e=A.getElementsByTagName("input")[0],e.addEventListener("invalid",function(A){t=!0,A.preventDefault(),A.stopPropagation()},!1),Modernizr.formvalidationmessage=!!e.validationMessage,A.getElementsByTagName("button")[0].click()}),t}),Modernizr.addTest("localizednumber",function(){if(!Modernizr.inputtypes.number)return!1;if(!Modernizr.formvalidation)return!1;var A,e=createElement("div"),t=getBody(),n=function(){return docElement.insertBefore(t,docElement.firstElementChild||docElement.firstChild)}();e.innerHTML='';var r=e.childNodes[0];n.appendChild(e),r.focus();try{document.execCommand("SelectAll",!1),document.execCommand("InsertText",!1,"1,1")}catch(o){}return A="number"===r.type&&1.1===r.valueAsNumber&&r.checkValidity(),n.removeChild(e),t.fake&&n.parentNode.removeChild(n),A});var mq=function(){var A=window.matchMedia||window.msMatchMedia;return A?function(e){var t=A(e);return t&&t.matches||!1}:function(A){var e=!1;return injectElementWithStyles("@media "+A+" { #modernizr { position: absolute; } }",function(A){e="absolute"==(window.getComputedStyle?window.getComputedStyle(A,null):A.currentStyle).position}),e}}();ModernizrProto.mq=mq,Modernizr.addTest("mediaqueries",mq("only all"));var hasOwnProp;!function(){var A={}.hasOwnProperty;hasOwnProp=is(A,"undefined")||is(A.call,"undefined")?function(A,e){return e in A&&is(A.constructor.prototype[e],"undefined")}:function(e,t){return A.call(e,t)}}(),ModernizrProto._l={},ModernizrProto.on=function(A,e){this._l[A]||(this._l[A]=[]),this._l[A].push(e),Modernizr.hasOwnProperty(A)&&setTimeout(function(){Modernizr._trigger(A,Modernizr[A])},0)},ModernizrProto._trigger=function(A,e){if(this._l[A]){var t=this._l[A];setTimeout(function(){var A,n;for(A=0;Ar?void(e=setTimeout(A,t)):(o.removeEventListener("playing",A,!1),addTest("videoautoplay",d),void(o.parentNode&&o.parentNode.removeChild(o)))}var e,t=200,n=5,r=0,o=createElement("video"),i=o.style;if(!(Modernizr.video&&"autoplay"in o))return void addTest("videoautoplay",!1);i.position="absolute",i.height=0,i.width=0;try{if(Modernizr.video.ogg)o.src="data:video/ogg;base64,T2dnUwACAAAAAAAAAABmnCATAAAAAHDEixYBKoB0aGVvcmEDAgEAAQABAAAQAAAQAAAAAAAFAAAAAQAAAAAAAAAAAGIAYE9nZ1MAAAAAAAAAAAAAZpwgEwEAAAACrA7TDlj///////////////+QgXRoZW9yYSsAAABYaXBoLk9yZyBsaWJ0aGVvcmEgMS4xIDIwMDkwODIyIChUaHVzbmVsZGEpAQAAABoAAABFTkNPREVSPWZmbXBlZzJ0aGVvcmEtMC4yOYJ0aGVvcmG+zSj3uc1rGLWpSUoQc5zmMYxSlKQhCDGMYhCEIQhAAAAAAAAAAAAAEW2uU2eSyPxWEvx4OVts5ir1aKtUKBMpJFoQ/nk5m41mUwl4slUpk4kkghkIfDwdjgajQYC8VioUCQRiIQh8PBwMhgLBQIg4FRba5TZ5LI/FYS/Hg5W2zmKvVoq1QoEykkWhD+eTmbjWZTCXiyVSmTiSSCGQh8PB2OBqNBgLxWKhQJBGIhCHw8HAyGAsFAiDgUCw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDAwPEhQUFQ0NDhESFRUUDg4PEhQVFRUOEBETFBUVFRARFBUVFRUVEhMUFRUVFRUUFRUVFRUVFRUVFRUVFRUVEAwLEBQZGxwNDQ4SFRwcGw4NEBQZHBwcDhATFhsdHRwRExkcHB4eHRQYGxwdHh4dGxwdHR4eHh4dHR0dHh4eHRALChAYKDM9DAwOExo6PDcODRAYKDlFOA4RFh0zV1A+EhYlOkRtZ00YIzdAUWhxXDFATldneXhlSFxfYnBkZ2MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEhIVGRoaGhoSFBYaGhoaGhUWGRoaGhoaGRoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhESFh8kJCQkEhQYIiQkJCQWGCEkJCQkJB8iJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQREhgvY2NjYxIVGkJjY2NjGBo4Y2NjY2MvQmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRISEhUXGBkbEhIVFxgZGxwSFRcYGRscHRUXGBkbHB0dFxgZGxwdHR0YGRscHR0dHhkbHB0dHR4eGxwdHR0eHh4REREUFxocIBERFBcaHCAiERQXGhwgIiUUFxocICIlJRcaHCAiJSUlGhwgIiUlJSkcICIlJSUpKiAiJSUlKSoqEBAQFBgcICgQEBQYHCAoMBAUGBwgKDBAFBgcICgwQEAYHCAoMEBAQBwgKDBAQEBgICgwQEBAYIAoMEBAQGCAgAfF5cdH1e3Ow/L66wGmYnfIUbwdUTe3LMRbqON8B+5RJEvcGxkvrVUjTMrsXYhAnIwe0dTJfOYbWrDYyqUrz7dw/JO4hpmV2LsQQvkUeGq1BsZLx+cu5iV0e0eScJ91VIQYrmqfdVSK7GgjOU0oPaPOu5IcDK1mNvnD+K8LwS87f8Jx2mHtHnUkTGAurWZlNQa74ZLSFH9oF6FPGxzLsjQO5Qe0edcpttd7BXBSqMCL4k/4tFrHIPuEQ7m1/uIWkbDMWVoDdOSuRQ9286kvVUlQjzOE6VrNguN4oRXYGkgcnih7t13/9kxvLYKQezwLTrO44sVmMPgMqORo1E0sm1/9SludkcWHwfJwTSybR4LeAz6ugWVgRaY8mV/9SluQmtHrzsBtRF/wPY+X0JuYTs+ltgrXAmlk10xQHmTu9VSIAk1+vcvU4ml2oNzrNhEtQ3CysNP8UeR35wqpKUBdGdZMSjX4WVi8nJpdpHnbhzEIdx7mwf6W1FKAiucMXrWUWVjyRf23chNtR9mIzDoT/6ZLYailAjhFlZuvPtSeZ+2oREubDoWmT3TguY+JHPdRVSLKxfKH3vgNqJ/9emeEYikGXDFNzaLjvTeGAL61mogOoeG3y6oU4rW55ydoj0lUTSR/mmRhPmF86uwIfzp3FtiufQCmppaHDlGE0r2iTzXIw3zBq5hvaTldjG4CPb9wdxAme0SyedVKczJ9AtYbgPOzYKJvZZImsN7ecrxWZg5dR6ZLj/j4qpWsIA+vYwE+Tca9ounMIsrXMB4Stiib2SPQtZv+FVIpfEbzv8ncZoLBXc3YBqTG1HsskTTotZOYTG+oVUjLk6zhP8bg4RhMUNtfZdO7FdpBuXzhJ5Fh8IKlJG7wtD9ik8rWOJxy6iQ3NwzBpQ219mlyv+FLicYs2iJGSE0u2txzed++D61ZWCiHD/cZdQVCqkO2gJpdpNaObhnDfAPrT89RxdWFZ5hO3MseBSIlANppdZNIV/Rwe5eLTDvkfWKzFnH+QJ7m9QWV1KdwnuIwTNtZdJMoXBf74OhRnh2t+OTGL+AVUnIkyYY+QG7g9itHXyF3OIygG2s2kud679ZWKqSFa9n3IHD6MeLv1lZ0XyduRhiDRtrNnKoyiFVLcBm0ba5Yy3fQkDh4XsFE34isVpOzpa9nR8iCpS4HoxG2rJpnRhf3YboVa1PcRouh5LIJv/uQcPNd095ickTaiGBnWLKVWRc0OnYTSyex/n2FofEPnDG8y3PztHrzOLK1xo6RAml2k9owKajOC0Wr4D5x+3nA0UEhK2m198wuBHF3zlWWVKWLN1CHzLClUfuoYBcx4b1llpeBKmbayaR58njtE9onD66lUcsg0Spm2snsb+8HaJRn4dYcLbCuBuYwziB8/5U1C1DOOz2gZjSZtrLJk6vrLF3hwY4Io9xuT/ruUFRSBkNtUzTOWhjh26irLEPx4jPZL3Fo3QrReoGTTM21xYTT9oFdhTUIvjqTkfkvt0bzgVUjq/hOYY8j60IaO/0AzRBtqkTS6R5ellZd5uKdzzhb8BFlDdAcrwkE0rbXTOPB+7Y0FlZO96qFL4Ykg21StJs8qIW7h16H5hGiv8V2Cflau7QVDepTAHa6Lgt6feiEvJDM21StJsmOH/hynURrKxvUpQ8BH0JF7BiyG2qZpnL/7AOU66gt+reLEXY8pVOCQvSsBtqZTNM8bk9ohRcwD18o/WVkbvrceVKRb9I59IEKysjBeTMmmbA21xu/6iHadLRxuIzkLpi8wZYmmbbWi32RVAUjruxWlJ//iFxE38FI9hNKOoCdhwf5fDe4xZ81lgREhK2m1j78vW1CqkuMu/AjBNK210kzRUX/B+69cMMUG5bYrIeZxVSEZISmkzbXOi9yxwIfPgdsov7R71xuJ7rFcACjG/9PzApqFq7wEgzNJm2suWESPuwrQvejj7cbnQxMkxpm21lUYJL0fKmogPPqywn7e3FvB/FCNxPJ85iVUkCE9/tLKx31G4CgNtWTTPFhMvlu8G4/TrgaZttTChljfNJGgOT2X6EqpETy2tYd9cCBI4lIXJ1/3uVUllZEJz4baqGF64yxaZ+zPLYwde8Uqn1oKANtUrSaTOPHkhvuQP3bBlEJ/LFe4pqQOHUI8T8q7AXx3fLVBgSCVpMba55YxN3rv8U1Dv51bAPSOLlZWebkL8vSMGI21lJmmeVxPRwFlZF1CpqCN8uLwymaZyjbXHCRytogPN3o/n74CNykfT+qqRv5AQlHcRxYrC5KvGmbbUwmZY/29BvF6C1/93x4WVglXDLFpmbapmF89HKTogRwqqSlGbu+oiAkcWFbklC6Zhf+NtTLFpn8oWz+HsNRVSgIxZWON+yVyJlE5tq/+GWLTMutYX9ekTySEQPLVNQQ3OfycwJBM0zNtZcse7CvcKI0V/zh16Dr9OSA21MpmmcrHC+6pTAPHPwoit3LHHqs7jhFNRD6W8+EBGoSEoaZttTCZljfduH/fFisn+dRBGAZYtMzbVMwvul/T/crK1NQh8gN0SRRa9cOux6clC0/mDLFpmbarmF8/e6CopeOLCNW6S/IUUg3jJIYiAcDoMcGeRbOvuTPjXR/tyo79LK3kqqkbxkkMRAOB0GODPItnX3Jnxro/25Ud+llbyVVSN4ySGIgHA6DHBnkWzr7kz410f7cqO/Syt5KqpFVJwn6gBEvBM0zNtZcpGOEPiysW8vvRd2R0f7gtjhqUvXL+gWVwHm4XJDBiMpmmZtrLfPwd/IugP5+fKVSysH1EXreFAcEhelGmbbUmZY4Xdo1vQWVnK19P4RuEnbf0gQnR+lDCZlivNM22t1ESmopPIgfT0duOfQrsjgG4tPxli0zJmF5trdL1JDUIUT1ZXSqQDeR4B8mX3TrRro/2McGeUvLtwo6jIEKMkCUXWsLyZROd9P/rFYNtXPBli0z398iVUlVKAjFlY437JXImUTm2r/4ZYtMy61hf16RPJIU9nZ1MABAwAAAAAAAAAZpwgEwIAAABhp658BScAAAAAAADnUFBQXIDGXLhwtttNHDhw5OcpQRMETBEwRPduylKVB0HRdF0A";else{ -if(!Modernizr.video.h264)return void addTest("videoautoplay",!1);o.src="data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAs1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0OCByMjYwMSBhMGNkN2QzIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTEgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAD2WIhAA3//728P4FNjuZQQAAAu5tb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAZAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACGHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAgAAAAIAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAGQAAAAAAAEAAAAAAZBtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAACgAAAAEAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAE7bWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAAA+3N0YmwAAACXc3RzZAAAAAAAAAABAAAAh2F2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAgACAEgAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAxYXZjQwFkAAr/4QAYZ2QACqzZX4iIhAAAAwAEAAADAFA8SJZYAQAGaOvjyyLAAAAAGHN0dHMAAAAAAAAAAQAAAAEAAAQAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAAsUAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjU2LjQwLjEwMQ=="}}catch(d){return void addTest("videoautoplay",!1)}o.setAttribute("autoplay",""),i.cssText="display:none",docElement.appendChild(o),setTimeout(function(){o.addEventListener("playing",A,!1),e=setTimeout(A,t)},0)});var omPrefixes="Moz O ms Webkit",domPrefixes=ModernizrProto._config.usePrefixes?omPrefixes.toLowerCase().split(" "):[];ModernizrProto._domPrefixes=domPrefixes,Modernizr.addTest("pointerevents",function(){var A=!1,e=domPrefixes.length;for(A=Modernizr.hasEvent("pointerdown");e--&&!A;)hasEvent(domPrefixes[e]+"pointerdown")&&(A=!0);return A}),Modernizr.addTest("fileinputdirectory",function(){var A=createElement("input"),e="directory";if(A.type="file",e in A)return!0;for(var t=0,n=domPrefixes.length;n>t;t++)if(domPrefixes[t]+e in A)return!0;return!1});var cssomPrefixes=ModernizrProto._config.usePrefixes?omPrefixes.split(" "):[];ModernizrProto._cssomPrefixes=cssomPrefixes;var atRule=function(A){var e,t=prefixes.length,n=window.CSSRule;if("undefined"==typeof n)return undefined;if(!A)return!1;if(A=A.replace(/^@/,""),e=A.replace(/-/g,"_").toUpperCase()+"_RULE",e in n)return"@"+A;for(var r=0;t>r;r++){var o=prefixes[r],i=o.toUpperCase()+"_"+e;if(i in n)return"@-"+o.toLowerCase()+"-"+A}return!1};ModernizrProto.atRule=atRule;var mStyle={style:modElem.elem.style};Modernizr._q.unshift(function(){delete mStyle.style});var testProp=ModernizrProto.testProp=function(A,e,t){return testProps([A],undefined,e,t)};Modernizr.addTest("textshadow",testProp("textShadow","1px 1px")),ModernizrProto.testAllProps=testPropsAll;var prefixed=ModernizrProto.prefixed=function(A,e,t){return 0===A.indexOf("@")?atRule(A):(-1!=A.indexOf("-")&&(A=cssToDOM(A)),e?testPropsAll(A,e,t):testPropsAll(A,"pfx"))};Modernizr.addAsyncTest(function(){var A;try{A=prefixed("indexedDB",window)}catch(e){}if(A){var t="modernizr-"+Math.random(),n=A.open(t);n.onerror=function(){n.error&&"InvalidStateError"===n.error.name?addTest("indexeddb",!1):(addTest("indexeddb",!0),detectDeleteDatabase(A,t))},n.onsuccess=function(){addTest("indexeddb",!0),detectDeleteDatabase(A,t)}}else addTest("indexeddb",!1)}),Modernizr.addAsyncTest(function(){var A,e,t,n,r="detect-blob-support",o=!1;try{A=prefixed("indexedDB",window)}catch(i){}if(!Modernizr.indexeddb||!Modernizr.indexeddb.deletedatabase)return!1;try{A.deleteDatabase(r).onsuccess=function(){e=A.open(r,1),e.onupgradeneeded=function(){e.result.createObjectStore("store")},e.onsuccess=function(){t=e.result;try{n=t.transaction("store","readwrite").objectStore("store").put(new Blob,"key"),n.onsuccess=function(){o=!0},n.onerror=function(){o=!1}}catch(i){o=!1}finally{addTest("indexeddbblob",o),t.close(),A.deleteDatabase(r)}}}}catch(i){addTest("indexeddbblob",!1)}}),Modernizr.addTest("batteryapi",!!prefixed("battery",navigator),{aliases:["battery-api"]});var crypto=prefixed("crypto",window);Modernizr.addTest("crypto",!!prefixed("subtle",crypto)),Modernizr.addTest("dart",!!prefixed("startDart",navigator)),Modernizr.addTest("forcetouch",function(){return hasEvent(prefixed("mouseforcewillbegin",window,!1),window)?MouseEvent.WEBKIT_FORCE_AT_MOUSE_DOWN&&MouseEvent.WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN:!1}),Modernizr.addTest("fullscreen",!(!prefixed("exitFullscreen",document,!1)&&!prefixed("cancelFullScreen",document,!1))),Modernizr.addTest("gamepads",!!prefixed("getGamepads",navigator)),Modernizr.addTest("intl",!!prefixed("Intl",window)),Modernizr.addTest("pagevisibility",!!prefixed("hidden",document,!1)),Modernizr.addTest("performance",!!prefixed("performance",window)),Modernizr.addTest("pointerlock",!!prefixed("exitPointerLock",document)),Modernizr.addTest("quotamanagement",function(){var A=prefixed("temporaryStorage",navigator),e=prefixed("persistentStorage",navigator);return!(!A||!e)}),Modernizr.addTest("requestanimationframe",!!prefixed("requestAnimationFrame",window),{aliases:["raf"]}),Modernizr.addTest("vibrate",!!prefixed("vibrate",navigator)),Modernizr.addTest("webintents",!!prefixed("startActivity",navigator)),Modernizr.addTest("lowbattery",function(){var A=.2,e=prefixed("battery",navigator);return!!(e&&!e.charging&&e.level<=A)});var crypto=prefixed("crypto",window),supportsGetRandomValues;if(crypto&&"getRandomValues"in crypto&&"Uint32Array"in window){var array=new Uint32Array(10),values=crypto.getRandomValues(array);supportsGetRandomValues=values&&is(values[0],"number")}Modernizr.addTest("getrandomvalues",!!supportsGetRandomValues),Modernizr.addTest("backgroundblendmode",prefixed("backgroundBlendMode","text")),Modernizr.addTest("objectfit",!!prefixed("objectFit"),{aliases:["object-fit"]}),Modernizr.addTest("regions",function(){if(isSVG)return!1;var A=prefixed("flowFrom"),e=prefixed("flowInto"),t=!1;if(!A||!e)return t;var n=createElement("iframe"),r=createElement("div"),o=createElement("div"),i=createElement("div"),d="modernizr_flow_for_regions_check";o.innerText="M",r.style.cssText="top: 150px; left: 150px; padding: 0px;",i.style.cssText="width: 50px; height: 50px; padding: 42px;",i.style[A]=d,r.appendChild(o),r.appendChild(i),docElement.appendChild(r);var a,s,l=o.getBoundingClientRect();return o.style[e]=d,a=o.getBoundingClientRect(),s=parseInt(a.left-l.left,10),docElement.removeChild(r),42==s?t=!0:(docElement.appendChild(n),l=n.getBoundingClientRect(),n.style[e]=d,a=n.getBoundingClientRect(),l.height>0&&l.height!==a.height&&0===a.height&&(t=!0)),o=i=r=n=undefined,t}),Modernizr.addTest("wrapflow",function(){var A=prefixed("wrapFlow");if(!A||isSVG)return!1;var e=A.replace(/([A-Z])/g,function(A,e){return"-"+e.toLowerCase()}).replace(/^ms-/,"-ms-"),t=createElement("div"),n=createElement("div"),r=createElement("span");n.style.cssText="position: absolute; left: 50px; width: 100px; height: 20px;"+e+":end;",r.innerText="X",t.appendChild(n),t.appendChild(r),docElement.appendChild(t);var o=r.offsetLeft;return docElement.removeChild(t),n=r=t=undefined,150==o}),Modernizr.addTest("speechrecognition",!!prefixed("SpeechRecognition",window)),Modernizr.addTest("filesystem",!!prefixed("requestFileSystem",window)),Modernizr.addTest("requestautocomplete",!!prefixed("requestAutocomplete",createElement("form")));var url=prefixed("URL",window,!1);url=url&&window[url],Modernizr.addTest("bloburls",url&&"revokeObjectURL"in url&&"createObjectURL"in url),Modernizr.addAsyncTest(function(){function A(){addTest("transferables",!1),e()}function e(){d&&URL.revokeObjectURL(d),a&&a.terminate(),r&&clearTimeout(r)}var t=!!(Modernizr.blobconstructor&&Modernizr.bloburls&&Modernizr.webworkers&&Modernizr.typedarrays);if(!t)return addTest("transferables",!1);try{var n,r,o='var hello = "world"',i=new Blob([o],{type:"text/javascript"}),d=URL.createObjectURL(i),a=new Worker(d);a.onerror=A,r=setTimeout(A,200),n=new ArrayBuffer(1),a.postMessage(n,[n]),addTest("transferables",0===n.byteLength),e()}catch(s){A()}}),Modernizr.addTest("peerconnection",!!prefixed("RTCPeerConnection",window)),Modernizr.addTest("datachannel",function(){if(!Modernizr.peerconnection)return!1;for(var A=0,e=domPrefixes.length;e>A;A++){var t=window[domPrefixes[A]+"RTCPeerConnection"];if(t){var n=new t(null);return"createDataChannel"in n}}return!1}),Modernizr.addTest("matchmedia",!!prefixed("matchMedia",window)),ModernizrProto.testAllProps=testAllProps,Modernizr.addTest("ligatures",testAllProps("fontFeatureSettings",'"liga" 1')),Modernizr.addTest("cssanimations",testAllProps("animationName","a",!0)),Modernizr.addTest("csspseudoanimations",function(){var A=!1;if(!Modernizr.cssanimations||!window.getComputedStyle)return A;var e=["@",Modernizr._prefixes.join("keyframes csspseudoanimations { from { font-size: 10px; } }@").replace(/\@$/,""),'#modernizr:before { content:" "; font-size:5px;',Modernizr._prefixes.join("animation:csspseudoanimations 1ms infinite;"),"}"].join("");return Modernizr.testStyles(e,function(e){A="10px"===window.getComputedStyle(e,":before").getPropertyValue("font-size")}),A}),Modernizr.addTest("appearance",testAllProps("appearance")),Modernizr.addTest("backdropfilter",testAllProps("backdropFilter")),Modernizr.addTest("backgroundcliptext",function(){return testAllProps("backgroundClip","text")}),Modernizr.addTest("bgpositionxy",function(){return testAllProps("backgroundPositionX","3px",!0)&&testAllProps("backgroundPositionY","5px",!0)}),Modernizr.addTest("bgrepeatround",testAllProps("backgroundRepeat","round")),Modernizr.addTest("bgrepeatspace",testAllProps("backgroundRepeat","space")),Modernizr.addTest("backgroundsize",testAllProps("backgroundSize","100%",!0)),Modernizr.addTest("bgsizecover",testAllProps("backgroundSize","cover")),Modernizr.addTest("borderimage",testAllProps("borderImage","url() 1",!0)),Modernizr.addTest("borderradius",testAllProps("borderRadius","0px",!0)),Modernizr.addTest("boxshadow",testAllProps("boxShadow","1px 1px",!0)),Modernizr.addTest("boxsizing",testAllProps("boxSizing","border-box",!0)&&(document.documentMode===undefined||document.documentMode>7)),function(){Modernizr.addTest("csscolumns",function(){var A=!1,e=testAllProps("columnCount");try{A=!!e,A&&(A=new Boolean(A))}catch(t){}return A});for(var A,e,t=["Width","Span","Fill","Gap","Rule","RuleColor","RuleStyle","RuleWidth","BreakBefore","BreakAfter","BreakInside"],n=0;n9)}),Modernizr.addTest("flexbox",testAllProps("flexBasis","1px",!0)),Modernizr.addTest("flexboxlegacy",testAllProps("boxDirection","reverse",!0)),Modernizr.addTest("flexboxtweener",testAllProps("flexAlign","end",!0)),Modernizr.addTest("flexwrap",testAllProps("flexWrap","wrap",!0)),Modernizr.addAsyncTest(function(){function A(){function t(){try{var A=createElement("div"),e=createElement("span"),t=A.style,n=0,r=0,o=!1,i=document.body.firstElementChild||document.body.firstChild;return A.appendChild(e),e.innerHTML="Bacon ipsum dolor sit amet jerky velit in culpa hamburger et. Laborum dolor proident, enim dolore duis commodo et strip steak. Salami anim et, veniam consectetur dolore qui tenderloin jowl velit sirloin. Et ad culpa, fatback cillum jowl ball tip ham hock nulla short ribs pariatur aute. Pig pancetta ham bresaola, ut boudin nostrud commodo flank esse cow tongue culpa. Pork belly bresaola enim pig, ea consectetur nisi. Fugiat officia turkey, ea cow jowl pariatur ullamco proident do laborum velit sausage. Magna biltong sint tri-tip commodo sed bacon, esse proident aliquip. Ullamco ham sint fugiat, velit in enim sed mollit nulla cow ut adipisicing nostrud consectetur. Proident dolore beef ribs, laborum nostrud meatball ea laboris rump cupidatat labore culpa. Shankle minim beef, velit sint cupidatat fugiat tenderloin pig et ball tip. Ut cow fatback salami, bacon ball tip et in shank strip steak bresaola. In ut pork belly sed mollit tri-tip magna culpa veniam, short ribs qui in andouille ham consequat. Dolore bacon t-bone, velit short ribs enim strip steak nulla. Voluptate labore ut, biltong swine irure jerky. Cupidatat excepteur aliquip salami dolore. Ball tip strip steak in pork dolor. Ad in esse biltong. Dolore tenderloin exercitation ad pork loin t-bone, dolore in chicken ball tip qui pig. Ut culpa tongue, sint ribeye dolore ex shank voluptate hamburger. Jowl et tempor, boudin pork chop labore ham hock drumstick consectetur tri-tip elit swine meatball chicken ground round. Proident shankle mollit dolore. Shoulder ut duis t-bone quis reprehenderit. Meatloaf dolore minim strip steak, laboris ea aute bacon beef ribs elit shank in veniam drumstick qui. Ex laboris meatball cow tongue pork belly. Ea ball tip reprehenderit pig, sed fatback boudin dolore flank aliquip laboris eu quis. Beef ribs duis beef, cow corned beef adipisicing commodo nisi deserunt exercitation. Cillum dolor t-bone spare ribs, ham hock est sirloin. Brisket irure meatloaf in, boudin pork belly sirloin ball tip. Sirloin sint irure nisi nostrud aliqua. Nostrud nulla aute, enim officia culpa ham hock. Aliqua reprehenderit dolore sunt nostrud sausage, ea boudin pork loin ut t-bone ham tempor. Tri-tip et pancetta drumstick laborum. Ham hock magna do nostrud in proident. Ex ground round fatback, venison non ribeye in.",document.body.insertBefore(A,i),t.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justification:newspaper;",n=e.offsetHeight,r=e.offsetWidth,t.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justification:newspaper;"+prefixes.join("hyphens:auto; "),o=e.offsetHeight!=n||e.offsetWidth!=r,document.body.removeChild(A),A.removeChild(e),o}catch(d){return!1}}function n(A,e){try{var t=createElement("div"),n=createElement("span"),r=t.style,o=0,i=!1,d=!1,a=!1,s=document.body.firstElementChild||document.body.firstChild;return r.cssText="position:absolute;top:0;left:0;overflow:visible;width:1.25em;",t.appendChild(n),document.body.insertBefore(t,s),n.innerHTML="mm",o=n.offsetHeight,n.innerHTML="m"+A+"m",d=n.offsetHeight>o,e?(n.innerHTML="m
m",o=n.offsetWidth,n.innerHTML="m"+A+"m",a=n.offsetWidth>o):a=!0,d===!0&&a===!0&&(i=!0),document.body.removeChild(t),t.removeChild(n),i}catch(l){return!1}}function r(A){try{var e,t=createElement("input"),n=createElement("div"),r="lebowski",o=!1,i=document.body.firstElementChild||document.body.firstChild;n.innerHTML=r+A+r,document.body.insertBefore(n,i),document.body.insertBefore(t,n),t.setSelectionRange?(t.focus(),t.setSelectionRange(0,0)):t.createTextRange&&(e=t.createTextRange(),e.collapse(!0),e.moveEnd("character",0),e.moveStart("character",0),e.select());try{window.find?o=window.find(r+r):(e=window.self.document.body.createTextRange(),o=e.findText(r+r))}catch(d){o=!1}return document.body.removeChild(n),document.body.removeChild(t),o}catch(d){return!1}}return document.body||document.getElementsByTagName("body")[0]?(addTest("csshyphens",function(){if(!testAllProps("hyphens","auto",!0))return!1;try{return t()}catch(A){return!1}}),addTest("softhyphens",function(){try{return n("­",!0)&&n("​",!1)}catch(A){return!1}}),void addTest("softhyphensfind",function(){try{return r("­")&&r("​")}catch(A){return!1}})):void setTimeout(A,e)}var e=300;setTimeout(A,e)}),Modernizr.addTest("cssmask",testAllProps("maskRepeat","repeat-x",!0)),Modernizr.addTest("overflowscrolling",testAllProps("overflowScrolling","touch",!0)),Modernizr.addTest("cssreflections",testAllProps("boxReflect","above",!0)),Modernizr.addTest("cssresize",testAllProps("resize","both",!0)),Modernizr.addTest("scrollsnappoints",testAllProps("scrollSnapType")),Modernizr.addTest("shapes",testAllProps("shapeOutside","content-box",!0)),Modernizr.addTest("textalignlast",testAllProps("textAlignLast")),Modernizr.addTest("csstransforms",function(){return-1===navigator.userAgent.indexOf("Android 2.")&&testAllProps("transform","scale(1)",!0)}),Modernizr.addTest("csstransforms3d",function(){return!!testAllProps("perspective","1px",!0)}),Modernizr.addTest("csstransformslevel2",function(){return testAllProps("translate","45px",!0)}),Modernizr.addTest("csstransitions",testAllProps("transition","all",!0)),Modernizr.addTest("csspseudotransitions",function(){var A=!1;if(!Modernizr.csstransitions||!window.getComputedStyle)return A;var e='#modernizr:before { content:" "; font-size:5px;'+Modernizr._prefixes.join("transition:0s 100s;")+"}#modernizr.trigger:before { font-size:10px; }";return Modernizr.testStyles(e,function(e){window.getComputedStyle(e,":before").getPropertyValue("font-size"),e.className+="trigger",A="5px"===window.getComputedStyle(e,":before").getPropertyValue("font-size")}),A}),Modernizr.addTest("userselect",testAllProps("userSelect","none",!0)),Modernizr.addTest("variablefonts",testAllProps("fontVariationSettings")),testRunner(),setClasses(classes),delete ModernizrProto.addTest,delete ModernizrProto.addAsyncTest;for(var i=0;i + + diff --git a/tests/assets/modernizr/mobile-safari-14-1.json b/tests/assets/modernizr/mobile-safari-18.json similarity index 67% rename from tests/assets/modernizr/mobile-safari-14-1.json rename to tests/assets/modernizr/mobile-safari-18.json index 4e959333fa..e86fc3de20 100644 --- a/tests/assets/modernizr/mobile-safari-14-1.json +++ b/tests/assets/modernizr/mobile-safari-18.json @@ -11,6 +11,249 @@ "required": true, "step": true }, + "adownload": true, + "aping": true, + "areaping": true, + "ambientlight": false, + "applicationcache": false, + "audio": { + "ogg": "", + "mp3": "probably", + "opus": "probably", + "wav": "probably", + "m4a": "maybe" + }, + "audioloop": true, + "webaudio": true, + "batteryapi": false, + "battery-api": false, + "lowbattery": false, + "blobconstructor": true, + "blob-constructor": true, + "broadcastchannel": true, + "canvas": true, + "canvasblending": true, + "todataurljpeg": true, + "todataurlpng": true, + "todataurlwebp": false, + "canvaswinding": true, + "canvastext": true, + "clipboard": { + "read": true, + "readtext": true, + "write": true, + "writetext": true + }, + "contenteditable": true, + "contextmenu": false, + "cors": true, + "crypto": true, + "getrandomvalues": true, + "cssall": true, + "cssanimations": true, + "appearance": true, + "aspectratio": true, + "backdropfilter": true, + "backgroundblendmode": true, + "backgroundcliptext": true, + "bgpositionshorthand": true, + "bgpositionxy": true, + "bgrepeatround": true, + "bgrepeatspace": true, + "backgroundsize": true, + "bgsizecover": true, + "borderimage": true, + "borderradius": true, + "boxdecorationbreak": true, + "boxshadow": true, + "boxsizing": true, + "csscalc": true, + "checked": true, + "csschunit": true, + "csscolumns": { + "width": true, + "span": true, + "fill": true, + "gap": true, + "rule": true, + "rulecolor": true, + "rulestyle": true, + "rulewidth": true, + "breakbefore": true, + "breakafter": true, + "breakinside": true + }, + "cssgridlegacy": false, + "cssgrid": true, + "cubicbezierrange": true, + "customproperties": true, + "displayrunin": false, + "display-runin": false, + "displaytable": true, + "display-table": true, + "ellipsis": true, + "cssescape": true, + "cssexunit": true, + "supports": true, + "cssfilters": true, + "flexbox": true, + "flexboxlegacy": true, + "flexboxtweener": false, + "flexgap": true, + "flexwrap": true, + "focusvisible": true, + "focuswithin": true, + "fontdisplay": true, + "fontface": true, + "generatedcontent": true, + "cssgradients": true, + "hairline": true, + "hsla": true, + "cssinvalid": true, + "lastchild": true, + "cssmask": true, + "mediaqueries": true, + "multiplebgs": true, + "nthchild": true, + "objectfit": true, + "object-fit": true, + "opacity": true, + "overflowscrolling": true, + "csspointerevents": true, + "csspositionsticky": true, + "csspseudoanimations": true, + "csstransitions": true, + "csspseudotransitions": true, + "cssreflections": true, + "regions": false, + "cssremunit": true, + "cssresize": true, + "rgba": true, + "cssscrollbar": false, + "scrollsnappoints": true, + "shapes": true, + "siblinggeneral": true, + "subpixelfont": true, + "target": true, + "textalignlast": true, + "textdecoration": { + "line": true, + "style": true, + "color": true, + "skip": true, + "skipink": true + }, + "textshadow": true, + "csstransforms": true, + "csstransforms3d": true, + "csstransformslevel2": true, + "preserve3d": true, + "userselect": true, + "cssvalid": true, + "variablefonts": true, + "cssvhunit": true, + "cssvmaxunit": false, + "cssvminunit": true, + "cssvwunit": true, + "willchange": true, + "wrapflow": false, + "customelements": true, + "customprotocolhandler": false, + "dart": false, + "dataview": true, + "classlist": true, + "createelementattrs": false, + "createelement-attrs": false, + "dataset": true, + "documentfragment": true, + "hidden": true, + "intersectionobserver": true, + "microdata": false, + "mutationobserver": true, + "passiveeventlisteners": true, + "shadowroot": true, + "shadowrootlegacy": false, + "bdi": true, + "details": true, + "outputelem": true, + "picture": true, + "progressbar": true, + "meter": true, + "ruby": true, + "template": true, + "time": false, + "texttrackapi": true, + "track": true, + "unknownelements": true, + "emoji": true, + "es5array": true, + "es5date": true, + "es5function": true, + "es5object": true, + "strictmode": true, + "es5string": true, + "json": true, + "es5syntax": true, + "es5undefined": true, + "es5": true, + "es6array": true, + "arrow": true, + "es6class": true, + "es6collections": true, + "generators": true, + "es6math": true, + "es6number": true, + "es6object": true, + "promises": true, + "restparameters": true, + "spreadarray": true, + "stringtemplate": true, + "es6string": true, + "es6symbol": true, + "es7array": true, + "restdestructuringarray": true, + "restdestructuringobject": true, + "spreadobject": true, + "es8object": true, + "customevent": true, + "devicemotion": true, + "deviceorientation": true, + "eventlistener": true, + "forcetouch": false, + "hashchange": true, + "oninput": true, + "pointerevents": true, + "proximity": false, + "filereader": true, + "filesystem": false, + "flash": false, + "fullscreen": false, + "gamepads": true, + "geolocation": true, + "hiddenscroll": true, + "history": true, + "htmlimports": false, + "ie8compat": false, + "sandbox": true, + "seamless": false, + "srcdoc": true, + "imgcrossorigin": true, + "lazyloading": true, + "sizes": true, + "srcset": true, + "capture": true, + "fileinput": true, + "fileinputdirectory": true, + "inputformaction": true, + "input-formaction": true, + "formattribute": true, + "inputformenctype": true, + "input-formenctype": true, + "inputformmethod": true, + "inputformnovalidate": true, + "input-formnovalidate": true, + "inputformtarget": true, + "input-formtarget": true, "inputtypes": { "search": true, "tel": true, @@ -26,278 +269,140 @@ "range": true, "color": true }, - "htmlimports": false, - "history": true, - "ie8compat": false, - "applicationcache": false, - "blobconstructor": true, - "blob-constructor": true, - "cookies": true, - "cors": true, - "customelements": true, - "customprotocolhandler": false, - "customevent": true, - "dataview": true, - "eventlistener": true, - "geolocation": true, - "json": true, + "formvalidation": true, + "localizednumber": false, + "inputsearchevent": false, + "placeholder": true, + "requestautocomplete": false, + "intl": true, + "ligatures": true, + "olreversed": true, + "mathml": true, + "mediasource": false, + "hovermq": false, + "pointermq": true, "messagechannel": true, - "notification": false, - "postmessage": true, - "queryselector": true, - "serviceworker": true, - "svg": true, - "templatestrings": true, - "typedarrays": true, - "websockets": true, - "xdomainrequest": false, - "webaudio": true, - "cssescape": true, - "focuswithin": true, - "supports": true, - "target": true, - "microdata": false, - "mutationobserver": true, - "passiveeventlisteners": true, - "picture": true, - "es5array": true, - "es5date": true, - "es5function": true, "beacon": true, + "effectivetype": false, "lowbandwidth": false, "eventsource": true, "fetch": true, - "xhrresponsetype": true, - "xhr2": true, - "speechsynthesis": true, - "localstorage": true, - "sessionstorage": true, - "websqldatabase": true, - "es5object": true, - "svgfilters": true, - "strictmode": true, - "es5string": true, - "es5syntax": true, - "es5undefined": true, - "es5": true, - "es6array": true, - "arrow": true, - "es6collections": true, - "generators": true, - "es6math": true, - "es6number": true, - "es6object": true, - "promises": true, - "es6string": true, - "devicemotion": true, - "devicemotion2": true, - "deviceorientation": true, - "deviceorientation2": true, - "deviceorientation3": true, - "filereader": true, - "urlparser": true, - "urlsearchparams": true, - "framed": false, - "webworkers": true, - "contextmenu": false, - "cssall": true, - "willchange": true, - "classlist": true, - "documentfragment": true, - "contains": false, - "audio": true, - "canvas": true, - "canvastext": true, - "contenteditable": true, - "emoji": false, - "olreversed": true, - "userdata": false, - "video": true, - "vml": false, - "webanimations": true, - "webgl": true, - "adownload": true, - "audioloop": true, - "canvasblending": true, - "todataurljpeg": true, - "todataurlpng": true, - "todataurlwebp": false, - "canvaswinding": true, - "bgpositionshorthand": true, - "multiplebgs": true, - "csspointerevents": true, - "cssremunit": true, - "rgba": true, - "preserve3d": true, - "createelementattrs": false, - "createelement-attrs": false, - "dataset": true, - "hidden": true, - "outputelem": true, - "progressbar": true, - "meter": true, - "ruby": true, - "template": true, - "srcset": true, - "time": false, - "texttrackapi": true, - "track": true, - "unknownelements": true, - "inputformaction": true, - "input-formaction": true, - "inputformenctype": true, - "input-formenctype": true, - "inputformmethod": true, - "inputformtarget": false, - "input-formtarget": false, - "scriptasync": true, - "scriptdefer": true, - "stylescoped": false, - "capture": true, - "fileinput": true, - "formattribute": true, - "placeholder": true, - "sandbox": true, - "inlinesvg": true, - "textareamaxlength": true, - "videocrossorigin": true, - "webglextensions": true, - "seamless": false, - "srcdoc": true, - "imgcrossorigin": true, - "hashchange": true, - "inputsearchevent": false, - "ambientlight": false, - "datalistelem": true, - "videoloop": true, - "csscalc": true, - "cubicbezierrange": true, - "cssgradients": true, - "opacity": true, - "csspositionsticky": true, - "csschunit": true, - "cssexunit": true, - "hsla": true, - "videopreload": true, - "getusermedia": true, - "websocketsbinary": true, - "atobbtoa": true, - "atob-btoa": true, - "sharedworkers": true, - "bdi": true, "xhrresponsetypearraybuffer": true, "xhrresponsetypeblob": true, "xhrresponsetypedocument": true, "xhrresponsetypejson": true, "xhrresponsetypetext": true, - "svgclippaths": true, - "svgforeignobject": true, - "smil": true, - "hiddenscroll": true, - "mathml": true, - "touchevents": true, - "unicoderange": true, - "unicode": true, - "checked": true, - "displaytable": true, - "display-table": true, - "fontface": true, - "generatedcontent": true, - "hairline": true, - "cssinvalid": true, - "lastchild": true, - "nthchild": true, - "cssscrollbar": false, - "siblinggeneral": true, - "subpixelfont": true, - "cssvalid": true, - "cssvhunit": false, - "cssvmaxunit": false, - "cssvminunit": true, - "cssvwunit": true, - "details": true, - "oninput": true, - "formvalidation": true, - "localizednumber": false, - "mediaqueries": true, - "flash": false, - "proximity": false, - "sizes": true, - "hovermq": false, - "pointermq": true, - "svgasimg": true, - "pointerevents": true, - "fileinputdirectory": true, - "textshadow": true, - "batteryapi": false, - "battery-api": false, - "crypto": true, - "dart": false, - "forcetouch": false, - "fullscreen": false, - "gamepads": true, - "intl": true, + "xhrresponsetype": true, + "xhr2": true, + "notification": false, "pagevisibility": true, "performance": true, "pointerlock": false, - "quotamanagement": false, + "postmessage": { + "structuredclones": true + }, + "proxy": true, + "queryselector": true, + "prefetch": false, "requestanimationframe": true, "raf": true, - "vibrate": false, - "webintents": false, - "lowbattery": false, - "getrandomvalues": true, - "backgroundblendmode": true, - "objectfit": true, - "object-fit": true, - "regions": false, - "wrapflow": false, + "scriptasync": true, + "scriptdefer": true, + "scrolltooptions": true, + "serviceworker": true, "speechrecognition": true, - "filesystem": false, - "requestautocomplete": false, + "speechsynthesis": true, + "cookies": true, + "localstorage": true, + "quotamanagement": false, + "sessionstorage": true, + "userdata": false, + "websqldatabase": true, + "stylescoped": false, + "svg": true, + "svgasimg": true, + "svgclippaths": true, + "svgfilters": true, + "svgforeignobject": true, + "inlinesvg": true, + "smil": true, + "textareamaxlength": true, + "textencoder": true, + "textdecoder": true, + "typedarrays": true, + "unicoderange": true, "bloburls": true, - "transferables": true, + "urlparser": true, + "urlsearchparams": true, + "vibrate": false, + "video": { + "ogg": "", + "h264": "probably", + "h265": "", + "webm": "probably", + "vp9": "probably", + "hls": "probably", + "av1": "" + }, + "videocrossorigin": true, + "videoloop": true, + "videopreload": true, + "vml": false, + "webintents": false, + "webanimations": true, + "publickeycredential": true, + "webgl": true, + "webglextensions": { + "ANGLE_instanced_arrays": true, + "EXT_blend_minmax": true, + "EXT_clip_control": true, + "EXT_color_buffer_half_float": true, + "EXT_depth_clamp": true, + "EXT_frag_depth": true, + "EXT_polygon_offset_clamp": true, + "EXT_shader_texture_lod": true, + "EXT_texture_filter_anisotropic": true, + "EXT_sRGB": true, + "KHR_parallel_shader_compile": true, + "OES_element_index_uint": true, + "OES_fbo_render_mipmap": true, + "OES_standard_derivatives": true, + "OES_texture_float": true, + "OES_texture_half_float": true, + "OES_texture_half_float_linear": true, + "OES_vertex_array_object": true, + "WEBGL_color_buffer_float": true, + "WEBGL_compressed_texture_astc": true, + "WEBGL_compressed_texture_etc": true, + "WEBGL_compressed_texture_etc1": true, + "WEBGL_compressed_texture_pvrtc": true, + "WEBKIT_WEBGL_compressed_texture_pvrtc": true, + "WEBGL_debug_renderer_info": true, + "WEBGL_debug_shaders": true, + "WEBGL_depth_texture": true, + "WEBGL_draw_buffers": true, + "WEBGL_lose_context": true, + "WEBGL_multi_draw": true, + "WEBGL_polygon_mode": true + }, "peerconnection": true, - "datachannel": false, + "datachannel": true, + "getusermedia": true, + "mediastream": true, + "websockets": true, + "websocketsbinary": true, + "atobbtoa": true, + "atob-btoa": true, + "framed": false, "matchmedia": true, - "ligatures": true, - "cssanimations": true, - "csspseudoanimations": true, - "appearance": true, - "backdropfilter": true, - "backgroundcliptext": true, - "bgpositionxy": true, - "bgrepeatround": true, - "bgrepeatspace": true, - "backgroundsize": true, - "bgsizecover": true, - "borderimage": true, - "borderradius": true, - "boxshadow": true, - "boxsizing": true, - "csscolumns": true, - "cssgridlegacy": false, - "cssgrid": true, - "displayrunin": false, - "display-runin": false, - "ellipsis": true, - "cssfilters": true, - "flexbox": true, - "flexboxlegacy": true, - "flexboxtweener": false, - "flexwrap": true, - "cssmask": true, - "overflowscrolling": true, - "cssreflections": true, - "cssresize": true, - "scrollsnappoints": true, - "shapes": true, - "textalignlast": true, - "csstransforms": true, - "csstransforms3d": true, - "csstransformslevel2": true, - "csstransitions": true, - "csspseudotransitions": true, - "userselect": true, - "variablefonts": true + "pushmanager": false, + "resizeobserver": true, + "workertypeoption": true, + "sharedworkers": true, + "webworkers": true, + "transferables": true, + "xdomainrequest": false, + "devicemotion2": true, + "deviceorientation2": true, + "deviceorientation3": true } \ No newline at end of file diff --git a/tests/assets/modernizr/modernizr.js b/tests/assets/modernizr/modernizr.js new file mode 100644 index 0000000000..b8f4e2e65e --- /dev/null +++ b/tests/assets/modernizr/modernizr.js @@ -0,0 +1,4147 @@ +(()=>{var j=(e,A)=>()=>(A||e((A={exports:{}}).exports,A),A.exports);var Y=j((exports,module)=>{(function(scriptGlobalObject,window,document,undefined){var tests=[],ModernizrProto={_version:"4.0.0-alpha",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,A){var t=this;setTimeout(function(){A(t[e])},0)},addTest:function(e,A,t){tests.push({name:e,fn:A,options:t})},addAsyncTest:function(e){tests.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=ModernizrProto,Modernizr=new Modernizr;var classes=[];function is(e,A){return typeof e===A}function testRunner(){var e,A,t,r,n,o,a;for(var l in tests)if(tests.hasOwnProperty(l)){if(e=[],A=tests[l],A.name&&(e.push(A.name.toLowerCase()),A.options&&A.options.aliases&&A.options.aliases.length))for(t=0;t0&&(A+=" "+t+e.join(" "+t)),isSVG?docElement.className.baseVal=A:docElement.className=A)}var hasOwnProp;(function(){var e={}.hasOwnProperty;!is(e,"undefined")&&!is(e.call,"undefined")?hasOwnProp=function(A,t){return e.call(A,t)}:hasOwnProp=function(A,t){return t in A&&is(A.constructor.prototype[t],"undefined")}})(),ModernizrProto._l={},ModernizrProto.on=function(e,A){this._l[e]||(this._l[e]=[]),this._l[e].push(A),Modernizr.hasOwnProperty(e)&&setTimeout(function(){Modernizr._trigger(e,Modernizr[e])},0)},ModernizrProto._trigger=function(e,A){if(this._l[e]){var t=this._l[e];setTimeout(function(){var r,n;for(r=0;r"u")return undefined;if(!e)return!1;if(e=e.replace(/^@/,""),r=e.replace(/-/g,"_").toUpperCase()+"_RULE",r in t)return"@"+e;for(var n=0;n",a="hidden"in d,u=d.childNodes.length==1||function(){A.createElement("a");var c=A.createDocumentFragment();return typeof c.cloneNode>"u"||typeof c.createDocumentFragment>"u"||typeof c.createElement>"u"}()}catch{a=!0,u=!0}})();function m(d,c){var p=d.createElement("p"),w=d.getElementsByTagName("head")[0]||d.documentElement;return p.innerHTML="x",w.insertBefore(p.lastChild,w.firstChild)}function g(){var d=h.elements;return typeof d=="string"?d.split(" "):d}function P(d,c){var p=h.elements;typeof p!="string"&&(p=p.join(" ")),typeof d!="string"&&(d=d.join(" ")),h.elements=p+" "+d,z(c)}function M(d){var c=f[d[l]];return c||(c={},s++,d[l]=s,f[s]=c),c}function b(d,c,p){if(c||(c=A),u)return c.createElement(d);p||(p=M(c));var w;return p.cache[d]?w=p.cache[d].cloneNode():o.test(d)?w=(p.cache[d]=p.createElem(d)).cloneNode():w=p.createElem(d),w.canHaveChildren&&!n.test(d)&&!w.tagUrn?p.frag.appendChild(w):w}function k(d,c){if(d||(d=A),u)return d.createDocumentFragment();c=c||M(d);for(var p=c.frag.cloneNode(),w=0,v=g(),E=v.length;w"u"||typeof A.parentWindow>"u"||typeof d.applyElement>"u"||typeof d.removeNode>"u"||typeof e.attachEvent>"u")}();function F(d){for(var c,p=d.getElementsByTagName("*"),w=p.length,v=RegExp("^(?:"+g().join("|")+")$","i"),E=[];w--;)c=p[w],v.test(c.nodeName)&&E.push(c.applyElement(W(c)));return E}function W(d){for(var c,p=d.attributes,w=p.length,v=d.ownerDocument.createElement(T+":"+d.nodeName);w--;)c=p[w],c.specified&&v.setAttribute(c.nodeName,c.nodeValue);return v.style.cssText=d.style.cssText,v}function G(d){for(var c,p=d.split("{"),w=p.length,v=RegExp("(^|[\\s,>+~])("+g().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),E="$1"+T+"\\:$2";w--;)c=p[w]=p[w].split("}"),c[c.length-1]=c[c.length-1].replace(v,E),p[w]=c.join("}");return p.join("{")}function Z(d){for(var c=d.length;c--;)d[c].removeNode()}function S(d){var c,p,w=M(d),v=d.namespaces,E=d.parentWindow;if(!V||d.printShived)return d;typeof v[T]>"u"&&v.add(T);function I(){clearTimeout(w._removeSheetTimer),c&&c.removeNode(!0),c=null}return E.attachEvent("onbeforeprint",function(){I();for(var Q,x,D,R=d.styleSheets,B=[],y=R.length,C=Array(y);y--;)C[y]=R[y];for(;D=C.pop();)if(!D.disabled&&U.test(D.media)){try{Q=D.imports,x=Q.length}catch{x=0}for(y=0;y7));Modernizr.addTest("csscalc",function(){var e="width:",A="calc(10px);",t=createElement("a");return t.style.cssText=e+prefixes.join(A+e),!!t.style.length});Modernizr.addTest("checked",function(){return testStyles("#modernizr {position:absolute} #modernizr input {margin-left:10px} #modernizr :checked {margin-left:20px;display:block}",function(e){var A=createElement("input");return A.setAttribute("type","checkbox"),A.setAttribute("checked","checked"),e.appendChild(A),A.offsetLeft===20})});Modernizr.addTest("csschunit",function(){var e=modElem.elem.style,A;try{e.fontSize="3ch",A=e.fontSize.indexOf("ch")!==-1}catch{A=!1}return A});(function(){Modernizr.addTest("csscolumns",function(){var n=!1,o=testAllProps("columnCount");try{n=!!o,n&&(n=new Boolean(n))}catch{}return n});for(var e=["Width","Span","Fill","Gap","Rule","RuleColor","RuleStyle","RuleWidth","BreakBefore","BreakAfter","BreakInside"],A,t,r=0;r9)});Modernizr.addTest("flexbox",testAllProps("flexBasis","1px",!0));Modernizr.addTest("flexboxlegacy",testAllProps("boxDirection","reverse",!0));Modernizr.addTest("flexboxtweener",testAllProps("flexAlign","end",!0));Modernizr.addTest("flexgap",function(){var e=createElement("div");e.style.display="flex",e.style.flexDirection="column",e.style.rowGap="1px",e.appendChild(createElement("div")),e.appendChild(createElement("div")),docElement.appendChild(e);var A=e.scrollHeight===1;return e.parentNode.removeChild(e),A});Modernizr.addTest("flexwrap",testAllProps("flexWrap","wrap",!0));Modernizr.addTest("focusvisible",function(){try{document.querySelector(":focus-visible")}catch{return!1}return!0});Modernizr.addTest("focuswithin",function(){try{document.querySelector(":focus-within")}catch{return!1}return!0});Modernizr.addTest("fontDisplay",testProp("font-display"));var unsupportedUserAgent=function(){var e=navigator.userAgent,A=e.match(/w(eb)?osbrowser/gi),t=e.match(/windows phone/gi)&&e.match(/iemobile\/([0-9])+/gi)&&parseFloat(RegExp.$1)>=9;return A||t}();unsupportedUserAgent?Modernizr.addTest("fontface",!1):testStyles('@font-face {font-family:"font";src:url("https://")}',function(e,A){var t=document.getElementById("smodernizr"),r=t.sheet||t.styleSheet,n=r?r.cssRules&&r.cssRules[0]?r.cssRules[0].cssText:r.cssText||"":"",o=/src/i.test(n)&&n.indexOf(A.split(" ")[0])===0;Modernizr.addTest("fontface",o)});testStyles('#modernizr{font:0/0 a}#modernizr:after{content:":)";visibility:hidden;font:7px/1 a}',function(e){Modernizr.addTest("generatedcontent",e.offsetHeight>=6)});Modernizr.addTest("cssgradients",function(){for(var e="background-image:",A="gradient(linear,left top,right bottom,from(#9f9),to(white));",t="",r,n=0,o=prefixes.length-1;n-1});Modernizr.addTest("hairline",function(){return testStyles("#modernizr {border:.5px solid transparent}",function(e){return e.offsetHeight===1})});Modernizr.addTest("hsla",function(){var e=createElement("a").style;return e.cssText="background-color:hsla(120,40%,100%,.5)",contains(e.backgroundColor,"rgba")||contains(e.backgroundColor,"hsla")});Modernizr.addAsyncTest(function(){var e=300;setTimeout(A,e);function A(){if(!document.body&&!document.getElementsByTagName("body")[0]){setTimeout(A,e);return}function t(){try{var o=createElement("div"),a=createElement("span"),l=o.style,s=0,f=0,u=!1,m=document.body.firstElementChild||document.body.firstChild;return o.lang="en",o.appendChild(a),a.innerHTML="Bacon ipsum dolor sit amet jerky velit in culpa hamburger et. Laborum dolor proident, enim dolore duis commodo et strip steak. Salami anim et, veniam consectetur dolore qui tenderloin jowl velit sirloin. Et ad culpa, fatback cillum jowl ball tip ham hock nulla short ribs pariatur aute. Pig pancetta ham bresaola, ut boudin nostrud commodo flank esse cow tongue culpa. Pork belly bresaola enim pig, ea consectetur nisi. Fugiat officia turkey, ea cow jowl pariatur ullamco proident do laborum velit sausage. Magna biltong sint tri-tip commodo sed bacon, esse proident aliquip. Ullamco ham sint fugiat, velit in enim sed mollit nulla cow ut adipisicing nostrud consectetur. Proident dolore beef ribs, laborum nostrud meatball ea laboris rump cupidatat labore culpa. Shankle minim beef, velit sint cupidatat fugiat tenderloin pig et ball tip. Ut cow fatback salami, bacon ball tip et in shank strip steak bresaola. In ut pork belly sed mollit tri-tip magna culpa veniam, short ribs qui in andouille ham consequat. Dolore bacon t-bone, velit short ribs enim strip steak nulla. Voluptate labore ut, biltong swine irure jerky. Cupidatat excepteur aliquip salami dolore. Ball tip strip steak in pork dolor. Ad in esse biltong. Dolore tenderloin exercitation ad pork loin t-bone, dolore in chicken ball tip qui pig. Ut culpa tongue, sint ribeye dolore ex shank voluptate hamburger. Jowl et tempor, boudin pork chop labore ham hock drumstick consectetur tri-tip elit swine meatball chicken ground round. Proident shankle mollit dolore. Shoulder ut duis t-bone quis reprehenderit. Meatloaf dolore minim strip steak, laboris ea aute bacon beef ribs elit shank in veniam drumstick qui. Ex laboris meatball cow tongue pork belly. Ea ball tip reprehenderit pig, sed fatback boudin dolore flank aliquip laboris eu quis. Beef ribs duis beef, cow corned beef adipisicing commodo nisi deserunt exercitation. Cillum dolor t-bone spare ribs, ham hock est sirloin. Brisket irure meatloaf in, boudin pork belly sirloin ball tip. Sirloin sint irure nisi nostrud aliqua. Nostrud nulla aute, enim officia culpa ham hock. Aliqua reprehenderit dolore sunt nostrud sausage, ea boudin pork loin ut t-bone ham tempor. Tri-tip et pancetta drumstick laborum. Ham hock magna do nostrud in proident. Ex ground round fatback, venison non ribeye in.",document.body.insertBefore(o,m),l.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justify:newspaper;",s=a.offsetHeight,f=a.offsetWidth,l.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justify:newspaper;"+prefixes.join("hyphens:auto; "),u=a.offsetHeight!==s||a.offsetWidth!==f,document.body.removeChild(o),o.removeChild(a),u}catch{return!1}}function r(o,a){try{var l=createElement("div"),s=createElement("span"),f=l.style,u=0,m=!1,g=!1,P=!1,M=document.body.firstElementChild||document.body.firstChild;return f.cssText="position:absolute;top:0;left:0;overflow:visible;width:1.25em;",l.appendChild(s),document.body.insertBefore(l,M),s.innerHTML="mm",u=s.offsetHeight,s.innerHTML="m"+o+"m",g=s.offsetHeight>u,a?(s.innerHTML="m
m",u=s.offsetWidth,s.innerHTML="m"+o+"m",P=s.offsetWidth>u):P=!0,g===!0&&P===!0&&(m=!0),document.body.removeChild(l),l.removeChild(s),m}catch{return!1}}function n(o){try{var a=createElement("input"),l=createElement("div"),s="lebowski",f=!1,u,m=document.body.firstElementChild||document.body.firstChild;a.style.cssText="position:fixed;top:0;",l.style.cssText="position:fixed;top:0;",l.innerHTML=s+o+s,document.body.insertBefore(l,m),document.body.insertBefore(a,l),a.setSelectionRange?(a.focus(),a.setSelectionRange(0,0)):a.createTextRange&&(u=a.createTextRange(),u.collapse(!0),u.moveEnd("character",0),u.moveStart("character",0),u.select());try{window.find?f=window.find(s+s):(u=window.self.document.body.createTextRange(),f=u.findText(s+s))}catch{f=!1}return document.body.removeChild(l),document.body.removeChild(a),f}catch{return!1}}addTest("csshyphens",function(){if(!testAllProps("hyphens","auto",!0))return!1;try{return t()}catch{return!1}}),addTest("softhyphens",function(){try{return r("­",!0)&&r("​",!1)}catch{return!1}}),addTest("softhyphensfind",function(){try{return n("­")&&n("​")}catch{return!1}})}});Modernizr.addTest("cssinvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:invalid{width:50px}",function(e){var A=createElement("input");return A.required=!0,e.appendChild(A),A.clientWidth>10})});testStyles("#modernizr div {width:100px} #modernizr :last-child{width:200px;display:block}",function(e){Modernizr.addTest("lastchild",e.lastChild.offsetWidth>e.firstChild.offsetWidth)},2);Modernizr.addTest("cssmask",testAllProps("maskRepeat","repeat-x",!0));Modernizr.addTest("mediaqueries",mq("only all"));Modernizr.addTest("multiplebgs",function(){var e=createElement("a").style;return e.cssText="background:url(https://),url(https://),red url(https://)",/(url\s*\(.*?){3}/.test(e.background)});testStyles("#modernizr div {width:1px} #modernizr div:nth-child(2n) {width:2px;}",function(e){var A=e.getElementsByTagName("div"),t=A[0].offsetWidth===A[2].offsetWidth&&A[1].offsetWidth===A[3].offsetWidth&&A[0].offsetWidth!==A[1].offsetWidth;Modernizr.addTest("nthchild",t)},4);Modernizr.addTest("objectfit",!!prefixed("objectFit"),{aliases:["object-fit"]});Modernizr.addTest("opacity",function(){var e=createElement("a").style;return e.cssText=prefixes.join("opacity:.55;"),/^0.55$/.test(e.opacity)});Modernizr.addTest("overflowscrolling",testAllProps("overflowScrolling","touch",!0));Modernizr.addTest("csspointerevents",function(){var e=createElement("a").style;return e.cssText="pointer-events:auto",e.pointerEvents==="auto"});Modernizr.addTest("csspositionsticky",function(){var e="position:",A="sticky",t=createElement("a"),r=t.style;return r.cssText=e+prefixes.join(A+";"+e).slice(0,-e.length),r.position.indexOf(A)!==-1});Modernizr.addTest("csspseudoanimations",function(){var e=!1;if(!Modernizr.cssanimations)return e;var A=["@",prefixes.join("keyframes csspseudoanimations { from { font-size: 10px; } }@").replace(/\@$/,""),'#modernizr:before { content:" "; font-size:5px;',prefixes.join("animation:csspseudoanimations 1ms infinite;"),"}"].join("");return testStyles(A,function(t){e=computedStyle(t,":before","font-size")==="10px"}),e});Modernizr.addTest("csstransitions",testAllProps("transition","all",!0));Modernizr.addTest("csspseudotransitions",function(){var e=!1;if(!Modernizr.csstransitions)return e;var A='#modernizr:before { content:" "; font-size:5px;'+prefixes.join("transition:0s 100s;")+"}#modernizr.trigger:before { font-size:10px; }";return testStyles(A,function(t){computedStyle(t,":before","font-size"),t.className+="trigger",e=computedStyle(t,":before","font-size")==="5px"}),e});Modernizr.addTest("cssreflections",testAllProps("boxReflect","above",!0));Modernizr.addTest("regions",function(){if(isSVG)return!1;var e=prefixed("flowFrom"),A=prefixed("flowInto"),t=!1;if(!e||!A)return t;var r=createElement("iframe"),n=createElement("div"),o=createElement("div"),a=createElement("div"),l="modernizr_flow_for_regions_check";o.innerText="M",n.style.cssText="top: 150px; left: 150px; padding: 0px;",a.style.cssText="width: 50px; height: 50px; padding: 42px;",a.style[e]=l,n.appendChild(o),n.appendChild(a),docElement.appendChild(n);var s,f,u=o.getBoundingClientRect();return o.style[A]=l,s=o.getBoundingClientRect(),f=parseInt(s.left-u.left,10),docElement.removeChild(n),f===42?t=!0:(docElement.appendChild(r),u=r.getBoundingClientRect(),r.style[A]=l,s=r.getBoundingClientRect(),u.height>0&&u.height!==s.height&&s.height===0&&(t=!0)),o=a=n=r=undefined,t});Modernizr.addTest("cssremunit",function(){var e=createElement("a").style;try{e.fontSize="3rem"}catch{}return/rem/.test(e.fontSize)});Modernizr.addTest("cssresize",testAllProps("resize","both",!0));Modernizr.addTest("rgba",function(){var e=createElement("a").style;return e.cssText="background-color:rgba(150,255,150,.5)",(""+e.backgroundColor).indexOf("rgba")>-1});testStyles("#modernizr{overflow: scroll; width: 40px; height: 40px; }#"+prefixes.join("scrollbar{width:10px} #modernizr::").split("#").slice(1).join("#")+"scrollbar{width:10px}",function(e){Modernizr.addTest("cssscrollbar","scrollWidth"in e&&e.scrollWidth===30)});Modernizr.addTest("scrollsnappoints",testAllProps("scrollSnapType"));Modernizr.addTest("shapes",testAllProps("shapeOutside","content-box",!0));Modernizr.addTest("siblinggeneral",function(){return testStyles("#modernizr div {width:100px} #modernizr div ~ div {width:200px;display:block}",function(e){return e.lastChild.offsetWidth===200},2)});testStyles("#modernizr{position: absolute; top: -10em; visibility:hidden; font: normal 10px arial;}#subpixel{float: left; font-size: 33.3333%;}",function(e){var A=e.firstChild;A.innerHTML="This is a text written in Arial",Modernizr.addTest("subpixelfont",computedStyle(A,null,"width")!=="44px")},1,["subpixel"]);Modernizr.addTest("target",function(){var e=window.document;if(!("querySelectorAll"in e))return!1;try{return e.querySelectorAll(":target"),!0}catch{return!1}});Modernizr.addTest("textalignlast",testAllProps("textAlignLast"));(function(){Modernizr.addTest("textdecoration",function(){var n=!1,o=testAllProps("textDecoration");try{n=!!o,n&&(n=new Boolean(n))}catch{}return n});for(var e=["Line","Style","Color","Skip","SkipInk"],A,t,r=0;r10})});Modernizr.addTest("variablefonts",testAllProps("fontVariationSettings"));testStyles("#modernizr { height: 50vh; max-height: 10px; }",function(e){var A=parseInt(computedStyle(e,null,"height"),10);Modernizr.addTest("cssvhunit",A===10)});function roundedEquals(e,A){return e-1===A||e===A||e+1===A}testStyles("#modernizr1{width: 50vmax}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(e){var A=e.childNodes[2],t=e.childNodes[1],r=e.childNodes[0],n=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=r.clientWidth/100,a=r.clientHeight/100,l=parseInt(Math.max(o,a)*50,10),s=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvmaxunit",roundedEquals(l,s)||roundedEquals(l,s-n))},3);testStyles("#modernizr1{width: 50vm;width:50vmin}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(e){var A=e.childNodes[2],t=e.childNodes[1],r=e.childNodes[0],n=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=r.clientWidth/100,a=r.clientHeight/100,l=parseInt(Math.min(o,a)*50,10),s=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvminunit",roundedEquals(l,s)||roundedEquals(l,s-n))},3);testStyles("#modernizr { width: 50vw; }",function(e){var A=parseInt(window.innerWidth/2,10),t=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvwunit",roundedEquals(t,A))});Modernizr.addTest("willchange","willChange"in docElement.style);Modernizr.addTest("wrapflow",function(){var e=prefixed("wrapFlow");if(!e||isSVG)return!1;var A=e.replace(/([A-Z])/g,function(a,l){return"-"+l.toLowerCase()}).replace(/^ms-/,"-ms-"),t=createElement("div"),r=createElement("div"),n=createElement("span");r.style.cssText="position: absolute; left: 50px; width: 100px; height: 20px;"+A+":end;",n.innerText="X",t.appendChild(r),t.appendChild(n),docElement.appendChild(t);var o=n.offsetLeft;return docElement.removeChild(t),r=n=t=undefined,o===150});Modernizr.addTest("customelements","customElements"in window);Modernizr.addTest("customprotocolhandler",function(){if(!navigator.registerProtocolHandler)return!1;try{navigator.registerProtocolHandler("thisShouldFail")}catch(e){return e instanceof TypeError}return!1});Modernizr.addTest("dart",!!prefixed("startDart",navigator));Modernizr.addTest("dataview",typeof DataView<"u"&&"getFloat64"in DataView.prototype);Modernizr.addTest("classlist","classList"in docElement);Modernizr.addTest("createelementattrs",function(){try{return createElement('').getAttribute("name")==="test"}catch{return!1}},{aliases:["createelement-attrs"]});Modernizr.addTest("dataset",function(){var e=createElement("div");return e.setAttribute("data-a-b","c"),!!(e.dataset&&e.dataset.aB==="c")});Modernizr.addTest("documentfragment",function(){return"createDocumentFragment"in document&&"appendChild"in docElement});Modernizr.addTest("hidden","hidden"in createElement("a"));Modernizr.addTest("intersectionobserver","IntersectionObserver"in window);Modernizr.addTest("microdata","getItems"in document);Modernizr.addTest("mutationobserver",!!window.MutationObserver||!!window.WebKitMutationObserver);Modernizr.addTest("passiveeventlisteners",function(){var e=!1;try{var A=Object.defineProperty({},"passive",{get:function(){e=!0}}),t=function(){};window.addEventListener("testPassiveEventSupport",t,A),window.removeEventListener("testPassiveEventSupport",t,A)}catch{}return e});Modernizr.addTest("shadowroot","attachShadow"in createElement("div"));Modernizr.addTest("shadowrootlegacy","createShadowRoot"in createElement("div"));Modernizr.addTest("bdi",function(){var e=createElement("div"),A=createElement("bdi");A.innerHTML="إ",e.appendChild(A),docElement.appendChild(e);var t=computedStyle(A,null,"direction")==="rtl";return docElement.removeChild(e),t});Modernizr.addTest("details",function(){var e=createElement("details"),A;return"open"in e?(testStyles("#modernizr details{display:block}",function(t){t.appendChild(e),e.innerHTML="ab",A=e.offsetHeight,e.open=!0,A=A!==e.offsetHeight}),A):!1});Modernizr.addTest("outputelem","value"in createElement("output"));Modernizr.addTest("picture","HTMLPictureElement"in window);Modernizr.addTest("progressbar",createElement("progress").max!==undefined),Modernizr.addTest("meter",createElement("meter").max!==undefined);Modernizr.addTest("ruby",function(){var e=createElement("ruby"),A=createElement("rt"),t=createElement("rp");if(e.appendChild(t),e.appendChild(A),docElement.appendChild(e),computedStyle(t,null,"display")==="none"||computedStyle(e,null,"display")==="ruby"&&computedStyle(A,null,"display")==="ruby-text"||computedStyle(t,null,"fontSize")==="6pt"&&computedStyle(A,null,"fontSize")==="6pt")return r(),!0;return r(),!1;function r(){docElement.removeChild(e),e=null,A=null,t=null}});Modernizr.addTest("template","content"in createElement("template"));Modernizr.addTest("time","valueAsDate"in createElement("time"));Modernizr.addTest("texttrackapi",typeof createElement("video").addTextTrack=="function"),Modernizr.addTest("track","kind"in createElement("track"));Modernizr.addTest("unknownelements",function(){var e=createElement("a");return e.innerHTML="",e.childNodes.length===1});Modernizr.addTest("emoji",function(){if(!Modernizr.canvastext)return!1;var e=createElement("canvas"),A=e.getContext("2d"),t=A.webkitBackingStorePixelRatio||A.mozBackingStorePixelRatio||A.msBackingStorePixelRatio||A.oBackingStorePixelRatio||A.backingStorePixelRatio||1,r=12*t;return A.fillStyle="#f00",A.textBaseline="top",A.font="32px Arial",A.fillText("\u{1F428}",0,0),A.getImageData(r,r,1,1).data[0]!==0});Modernizr.addTest("es5array",function(){return!!(Array.prototype&&Array.prototype.every&&Array.prototype.filter&&Array.prototype.forEach&&Array.prototype.indexOf&&Array.prototype.lastIndexOf&&Array.prototype.map&&Array.prototype.some&&Array.prototype.reduce&&Array.prototype.reduceRight&&Array.isArray)});Modernizr.addTest("es5date",function(){var e="2013-04-12T06:06:37.307Z",A=!1;try{A=!!Date.parse(e)}catch{}return!!(Date.now&&Date.prototype&&Date.prototype.toISOString&&Date.prototype.toJSON&&A)});Modernizr.addTest("es5function",function(){return!!(Function.prototype&&Function.prototype.bind)});Modernizr.addTest("es5object",function(){return!!(Object.keys&&Object.create&&Object.getPrototypeOf&&Object.getOwnPropertyNames&&Object.isSealed&&Object.isFrozen&&Object.isExtensible&&Object.getOwnPropertyDescriptor&&Object.defineProperty&&Object.defineProperties&&Object.seal&&Object.freeze&&Object.preventExtensions)});Modernizr.addTest("strictmode",function(){"use strict";return!this}());Modernizr.addTest("es5string",function(){return!!(String.prototype&&String.prototype.trim)});Modernizr.addTest("json","JSON"in window&&"parse"in JSON&&"stringify"in JSON);Modernizr.addTest("es5syntax",function(){var value,obj,stringAccess,getter,setter,reservedWords,zeroWidthChars;try{return stringAccess=eval('"foobar"[3] === "b"'),getter=eval("({ get x(){ return 1 } }).x === 1"),eval("({ set x(v){ value = v; } }).x = 1"),setter=value===1,eval("obj = ({ if: 1 })"),reservedWords=obj.if===1,zeroWidthChars=eval("_\u200C\u200D = true"),stringAccess&&getter&&setter&&reservedWords&&zeroWidthChars}catch(e){return!1}});Modernizr.addTest("es5undefined",function(){var e,A;try{A=window.undefined,window.undefined=12345,e=typeof window.undefined>"u",window.undefined=A}catch{return!1}return e});Modernizr.addTest("es5",function(){return!!(Modernizr.es5array&&Modernizr.es5date&&Modernizr.es5function&&Modernizr.es5object&&Modernizr.strictmode&&Modernizr.es5string&&Modernizr.json&&Modernizr.es5syntax&&Modernizr.es5undefined)});Modernizr.addTest("es6array",!!(Array.prototype&&Array.prototype.copyWithin&&Array.prototype.fill&&Array.prototype.find&&Array.prototype.findIndex&&Array.prototype.keys&&Array.prototype.entries&&Array.prototype.values&&Array.from&&Array.of));Modernizr.addTest("arrow",function(){try{eval("()=>{}")}catch(e){return!1}return!0});Modernizr.addTest("es6class",function(){try{eval("class A{}")}catch(e){return!1}return!0});Modernizr.addTest("es6collections",!!(window.Map&&window.Set&&window.WeakMap&&window.WeakSet));Modernizr.addTest("generators",function(){try{new Function("function* test() {}")()}catch{return!1}return!0});Modernizr.addTest("es6math",!!(Math&&Math.clz32&&Math.cbrt&&Math.imul&&Math.sign&&Math.log10&&Math.log2&&Math.log1p&&Math.expm1&&Math.cosh&&Math.sinh&&Math.tanh&&Math.acosh&&Math.asinh&&Math.atanh&&Math.hypot&&Math.trunc&&Math.fround));Modernizr.addTest("es6number",!!(Number.isFinite&&Number.isInteger&&Number.isSafeInteger&&Number.isNaN&&Number.parseInt&&Number.parseFloat&&Number.isInteger(Number.MAX_SAFE_INTEGER)&&Number.isInteger(Number.MIN_SAFE_INTEGER)&&Number.isFinite(Number.EPSILON)));Modernizr.addTest("es6object",!!(Object.assign&&Object.is&&Object.setPrototypeOf));Modernizr.addTest("promises",function(){return"Promise"in window&&"resolve"in window.Promise&&"reject"in window.Promise&&"all"in window.Promise&&"race"in window.Promise&&function(){var e;return new window.Promise(function(A){e=A}),typeof e=="function"}()});Modernizr.addTest("restparameters",function(){try{eval("function f(...rest) {}")}catch(e){return!1}return!0});Modernizr.addTest("spreadarray",function(){try{eval("(function f(){})(...[1])")}catch(e){return!1}return!0});Modernizr.addTest("stringtemplate",function(){try{return eval("(function(){var a=1; return `-${a}-`;})()")==="-1-"}catch(e){return!1}});Modernizr.addTest("es6string",!!(String.fromCodePoint&&String.raw&&String.prototype.codePointAt&&String.prototype.repeat&&String.prototype.startsWith&&String.prototype.endsWith&&String.prototype.includes));Modernizr.addTest("es6symbol",!!(typeof Symbol=="function"&&Symbol.for&&Symbol.hasInstance&&Symbol.isConcatSpreadable&&Symbol.iterator&&Symbol.keyFor&&Symbol.match&&Symbol.prototype&&Symbol.replace&&Symbol.search&&Symbol.species&&Symbol.split&&Symbol.toPrimitive&&Symbol.toStringTag&&Symbol.unscopables));Modernizr.addTest("es7array",!!(Array.prototype&&Array.prototype.includes));Modernizr.addTest("restdestructuringarray",function(){try{eval("var [...rest]=[1]")}catch(e){return!1}return!0}),Modernizr.addTest("restdestructuringobject",function(){try{eval("var {...rest}={a:1}")}catch(e){return!1}return!0});Modernizr.addTest("spreadobject",function(){try{eval("var a={...{b:1}}")}catch(e){return!1}return!0});Modernizr.addTest("es8object",!!(Object.entries&&Object.values));Modernizr.addTest("customevent","CustomEvent"in window&&typeof window.CustomEvent=="function");Modernizr.addTest("devicemotion","DeviceMotionEvent"in window),Modernizr.addTest("deviceorientation","DeviceOrientationEvent"in window);Modernizr.addTest("eventlistener","addEventListener"in window);Modernizr.addTest("forcetouch",function(){return hasEvent(prefixed("mouseforcewillbegin",window,!1),window)?MouseEvent.WEBKIT_FORCE_AT_MOUSE_DOWN&&MouseEvent.WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN:!1});Modernizr.addTest("hashchange",function(){return hasEvent("hashchange",window)===!1?!1:document.documentMode===undefined||document.documentMode>7});Modernizr.addTest("oninput",function(){var e=createElement("input"),A;if(e.setAttribute("oninput","return"),e.style.cssText="position:fixed;top:0;",hasEvent("oninput",docElement)||typeof e.oninput=="function")return!0;try{var t=document.createEvent("KeyboardEvent");A=!1;var r=function(n){A=!0,n.preventDefault(),n.stopPropagation()};t.initKeyEvent("keypress",!0,!0,window,!1,!1,!1,!1,0,"e".charCodeAt(0)),docElement.appendChild(e),e.addEventListener("input",r,!1),e.focus(),e.dispatchEvent(t),e.removeEventListener("input",r,!1),docElement.removeChild(e)}catch{A=!1}return A});var domPrefixesAll=[""].concat(domPrefixes);ModernizrProto._domPrefixesAll=domPrefixesAll;Modernizr.addTest("pointerevents",function(){for(var e=0,A=domPrefixesAll.length;e"u"?!1:(t.drawImage(e,0,0),t.getImageData(0,0,1,1).data[3]===0)})},e.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAAAAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("avif",e.width===1)},e.src="data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAAEcbWV0YQAAAAAAAABIaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGNhdmlmIC0gaHR0cHM6Ly9naXRodWIuY29tL2xpbmstdS9jYXZpZgAAAAAeaWxvYwAAAAAEQAABAAEAAAAAAUQAAQAAABcAAAAqaWluZgEAAAAAAAABAAAAGmluZmUCAAAAAAEAAGF2MDFJbWFnZQAAAAAOcGl0bQAAAAAAAQAAAHJpcHJwAAAAUmlwY28AAAAQcGFzcAAAAAEAAAABAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAQcGl4aQAAAAADCAgIAAAAFmF2MUOBAAwACggYAAYICGgIIAAAABhpcG1hAAAAAAAAAAEAAQUBAoMDhAAAAB9tZGF0CggYAAYICGgIIBoFHiAAAEQiBACwDoA="});Modernizr.addTest("imgcrossorigin","crossOrigin"in createElement("img"));Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("exiforientation",!1,{aliases:["exif-orientation"]})},e.onload=function(){addTest("exiforientation",e.width!==2,{aliases:["exif-orientation"]})},e.src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/iiiigD/2Q=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("jpeg2000",e.width===1)},e.src="data:image/jp2;base64,/0//UQAyAAAAAAABAAAAAgAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAEBwEBBwEBBwEBBwEB/1IADAAAAAEAAAQEAAH/XAAEQED/ZAAlAAFDcmVhdGVkIGJ5IE9wZW5KUEVHIHZlcnNpb24gMi4wLjD/kAAKAAAAAABYAAH/UwAJAQAABAQAAf9dAAUBQED/UwAJAgAABAQAAf9dAAUCQED/UwAJAwAABAQAAf9dAAUDQED/k8+kEAGvz6QQAa/PpBABr994EAk//9k="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("jpegxr",e.width===1,{aliases:["jpeg-xr"]})},e.src="data:image/vnd.ms-photo;base64,SUm8AQgAAAAFAAG8AQAQAAAASgAAAIC8BAABAAAAAQAAAIG8BAABAAAAAQAAAMC8BAABAAAAWgAAAMG8BAABAAAAHwAAAAAAAAAkw91vA07+S7GFPXd2jckNV01QSE9UTwAZAYBxAAAAABP/gAAEb/8AAQAAAQAAAA=="});Modernizr.addTest("lazyloading","loading"in HTMLImageElement.prototype);Modernizr.addAsyncTest(function(){var e,A,t,r=createElement("img"),n="sizes"in r;!n&&"srcset"in r?(A="data:image/gif;base64,R0lGODlhAgABAPAAAP///wAAACH5BAAAAAAALAAAAAACAAEAAAICBAoAOw==",e="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",t=function(){addTest("sizes",r.width===2)},r.onload=t,r.onerror=t,r.setAttribute("sizes","9px"),r.srcset=e+" 1w,"+A+" 8w",r.src=e):addTest("sizes",n)});Modernizr.addTest("srcset","srcset"in createElement("img"));Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webpalpha",!1,{aliases:["webp-alpha"]})},e.onload=function(){addTest("webpalpha",e.width===1,{aliases:["webp-alpha"]})},e.src="data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webpanimation",!1,{aliases:["webp-animation"]})},e.onload=function(){addTest("webpanimation",e.width===1,{aliases:["webp-animation"]})},e.src="data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"});Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webplossless",!1,{aliases:["webp-lossless"]})},e.onload=function(){addTest("webplossless",e.width===1,{aliases:["webp-lossless"]})},e.src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="});Modernizr.addAsyncTest(function(){var e=[{uri:"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=",name:"webp"},{uri:"data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA==",name:"webp.alpha"},{uri:"data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA",name:"webp.animation"},{uri:"data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=",name:"webp.lossless"}],A=e.shift();function t(r,n,o){var a=new Image;function l(s){var f=s&&s.type==="load"?a.width===1:!1,u=r==="webp";addTest(r,u&&f?new Boolean(f):f),o&&o(s)}a.onerror=l,a.onload=l,a.src=n}t(A.name,A.uri,function(r){if(r&&r.type==="load")for(var n=0;n',testStyles("#modernizr form{position:absolute;top:-99999em}",function(r){r.appendChild(e),t=e.getElementsByTagName("input")[0],t.addEventListener("invalid",function(n){A=!0,n.preventDefault(),n.stopPropagation()},!1),Modernizr.formvalidationmessage=!!t.validationMessage,e.getElementsByTagName("button")[0].click()}),A});Modernizr.addTest("localizednumber",function(){if(!Modernizr.inputtypes.number||!Modernizr.formvalidation)return!1;var e=getBody(),A=createElement("div"),t=e.firstElementChild||e.firstChild,r;e.insertBefore(A,t),A.innerHTML='';var n=A.childNodes[0];e.appendChild(A),n.focus();try{document.execCommand("SelectAll",!1),document.execCommand("InsertText",!1,"1,1")}catch{}return r=n.type==="number"&&n.valueAsNumber===1.1&&n.checkValidity(),e.removeChild(A),e.fake&&e.parentNode&&e.parentNode.removeChild(e),r});Modernizr.addTest("inputsearchevent",hasEvent("search"));Modernizr.addTest("placeholder","placeholder"in createElement("input")&&"placeholder"in createElement("textarea"));Modernizr.addTest("requestautocomplete",!!prefixed("requestAutocomplete",createElement("form")));Modernizr.addTest("intl",!!prefixed("Intl",window));Modernizr.addTest("ligatures",testAllProps("fontFeatureSettings",'"liga" 1'));Modernizr.addTest("olreversed","reversed"in createElement("ol"));Modernizr.addTest("mathml",function(){var e;return testStyles("#modernizr{position:absolute;display:inline-block}",function(A){A.innerHTML+="xxyy",e=A.offsetHeight>A.offsetWidth}),e});Modernizr.addTest("mediasource","MediaSource"in window);Modernizr.addTest("hovermq",mq("(hover)"));Modernizr.addTest("pointermq",mq("(pointer:coarse),(pointer:fine),(pointer:none)"));Modernizr.addTest("messagechannel","MessageChannel"in window);Modernizr.addTest("beacon","sendBeacon"in navigator);Modernizr.addTest("effectiveType",function(){var e=navigator.connection||{effectiveType:0};return e.effectiveType!==0});Modernizr.addTest("lowbandwidth",function(){var e=navigator.connection||{type:0,effectiveType:0};return e.type===3||e.type===4||/^[23]g$/.test(e.effectiveType)});Modernizr.addTest("eventsource","EventSource"in window);Modernizr.addTest("fetch","fetch"in window);var testXhrType=function(e){if(typeof XMLHttpRequest>"u")return!1;var A=new XMLHttpRequest;A.open("get","/",!0);try{A.responseType=e}catch{return!1}return"response"in A&&A.responseType===e};Modernizr.addTest("xhrresponsetypearraybuffer",testXhrType("arraybuffer"));Modernizr.addTest("xhrresponsetypeblob",testXhrType("blob"));Modernizr.addTest("xhrresponsetypedocument",testXhrType("document"));Modernizr.addTest("xhrresponsetypejson",testXhrType("json"));Modernizr.addTest("xhrresponsetypetext",testXhrType("text"));Modernizr.addTest("xhrresponsetype",function(){if(typeof XMLHttpRequest>"u")return!1;var e=new XMLHttpRequest;return e.open("get","/",!0),"response"in e}());Modernizr.addTest("xhr2","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest);Modernizr.addTest("notification",function(){if(!window.Notification||!window.Notification.requestPermission)return!1;if(window.Notification.permission==="granted")return!0;try{new window.Notification("")}catch(e){if(e.name==="TypeError")return!1}return!0});Modernizr.addTest("pagevisibility",!!prefixed("hidden",document,!1));Modernizr.addTest("performance",!!prefixed("performance",window));Modernizr.addTest("pointerlock",!!prefixed("exitPointerLock",document));var bool=!0;try{window.postMessage({toString:function(){bool=!1}},"*")}catch(e){}Modernizr.addTest("postmessage",new Boolean("postMessage"in window)),Modernizr.addTest("postmessage.structuredclones",bool);Modernizr.addTest("proxy","Proxy"in window);Modernizr.addTest("queryselector","querySelector"in document&&"querySelectorAll"in document);Modernizr.addTest("prefetch",function(){if(document.documentMode===11)return!0;var e=createElement("link").relList;return!e||!e.supports?!1:e.supports("prefetch")});Modernizr.addTest("requestanimationframe",!!prefixed("requestAnimationFrame",window),{aliases:["raf"]});Modernizr.addTest("scriptasync","async"in createElement("script"));Modernizr.addTest("scriptdefer","defer"in createElement("script"));Modernizr.addTest("scrolltooptions",function(){var e=getBody(),A=window.pageYOffset,t=e.clientHeight<=window.innerHeight;if(t){var r=createElement("div");r.style.height=window.innerHeight-e.clientHeight+1+"px",r.style.display="block",e.appendChild(r)}window.scrollTo({top:1});var n=window.pageYOffset!==0;return t&&e.removeChild(r),window.scrollTo(0,A),n});Modernizr.addTest("serviceworker","serviceWorker"in navigator);Modernizr.addTest("speechrecognition",function(){try{return!!prefixed("SpeechRecognition",window)}catch{return!1}});Modernizr.addTest("speechsynthesis",function(){try{return"SpeechSynthesisUtterance"in window}catch{return!1}});Modernizr.addTest("cookies",function(){try{document.cookie="cookietest=1";var e=document.cookie.indexOf("cookietest=")!==-1;return document.cookie="cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT",e}catch{return!1}});Modernizr.addAsyncTest(function(){var e;try{e=prefixed("indexedDB",window)}catch{}if(e){var A="modernizr-"+Math.random(),t;try{t=e.open(A)}catch{addTest("indexeddb",!1);return}t.onerror=function(r){t.error&&(t.error.name==="InvalidStateError"||t.error.name==="UnknownError")?(addTest("indexeddb",!1),r.preventDefault()):(addTest("indexeddb",!0),detectDeleteDatabase(e,A))},t.onsuccess=function(){addTest("indexeddb",!0),detectDeleteDatabase(e,A)}}else addTest("indexeddb",!1)});function detectDeleteDatabase(e,A){var t=e.deleteDatabase(A);t.onsuccess=function(){addTest("indexeddb.deletedatabase",!0)},t.onerror=function(){addTest("indexeddb.deletedatabase",!1)}}Modernizr.addAsyncTest(function(){var e,A="detect-blob-support",t=!1,r,n,o;try{e=prefixed("indexedDB",window)}catch{}if(!(Modernizr.indexeddb&&Modernizr.indexeddb.deletedatabase))return!1;try{e.deleteDatabase(A).onsuccess=function(){r=e.open(A,1),r.onupgradeneeded=function(){r.result.createObjectStore("store")},r.onsuccess=function(){n=r.result;try{o=n.transaction("store","readwrite").objectStore("store").put(new Blob,"key"),o.onsuccess=function(){t=!0},o.onerror=function(){t=!1}}catch{t=!1}finally{addTest("indexeddbblob",t),n.close(),e.deleteDatabase(A)}}}}catch{addTest("indexeddbblob",!1)}});Modernizr.addAsyncTest(function(){Modernizr.on("indexeddb",function(e){e&&addTest("indexeddb2","getAll"in IDBIndex.prototype)})});Modernizr.addTest("localstorage",function(){var e="modernizr";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch{return!1}});Modernizr.addTest("quotamanagement",function(){var e=prefixed("temporaryStorage",navigator),A=prefixed("persistentStorage",navigator);return!!(e&&A)});Modernizr.addTest("sessionstorage",function(){var e="modernizr";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch{return!1}});Modernizr.addTest("userdata",!!createElement("div").addBehavior);Modernizr.addTest("websqldatabase","openDatabase"in window);Modernizr.addTest("stylescoped","scoped"in createElement("style"));Modernizr.addTest("svg",!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect);Modernizr.addTest("svgasimg",document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1"));var toStringFn={}.toString;Modernizr.addTest("svgclippaths",function(){return!!document.createElementNS&&/SVGClipPath/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","clipPath")))});Modernizr.addTest("svgfilters",function(){var e=!1;try{e="SVGFEColorMatrixElement"in window&&SVGFEColorMatrixElement.SVG_FECOLORMATRIX_TYPE_SATURATE===2}catch{}return e});Modernizr.addTest("svgforeignobject",function(){return!!document.createElementNS&&/SVGForeignObject/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))});Modernizr.addTest("inlinesvg",function(){var e=createElement("div");return e.innerHTML="",(typeof SVGRect<"u"&&e.firstChild&&e.firstChild.namespaceURI)==="http://www.w3.org/2000/svg"});Modernizr.addTest("smil",function(){return!!document.createElementNS&&/SVGAnimate/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","animate")))});Modernizr.addTest("textareamaxlength","maxLength"in createElement("textarea"));Modernizr.addTest("textencoder",!!(window.TextEncoder&&window.TextEncoder.prototype.encode)),Modernizr.addTest("textdecoder",!!(window.TextDecoder&&window.TextDecoder.prototype.decode));Modernizr.addTest("typedarrays","ArrayBuffer"in window);Modernizr.addTest("unicoderange",function(){return testStyles('@font-face{font-family:"unicodeRange";src:local("Arial");unicode-range:U+0020,U+002E}#modernizr span{font-size:20px;display:inline-block;font-family:"unicodeRange",monospace}#modernizr .mono{font-family:monospace}',function(e){for(var A=[".",".","m","m"],t=0;t 1 due to Webkit [bug #45761](https://bugs.webkit.org/show_bug.cgi?id=45761)"], + "notes": [{ + "name": "Comprehensive Compat Chart", + "href": "https://muddledramblings.com/table-of-css3-border-radius-compliance/" + }] +} +!*/ +/*! +{ + "name": "CSS Custom Properties", + "property": "customproperties", + "caniuse": "css-variables", + "tags": ["css"], + "builderAliases": ["css_customproperties"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/--*" + }, { + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-variables/" + }] +} +!*/ +/*! +{ + "name": "CSS Display run-in", + "property": "display-runin", + "authors": ["alanhogan"], + "tags": ["css"], + "builderAliases": ["css_displayrunin"], + "notes": [{ + "name": "CSS Tricks Article", + "href": "https://web.archive.org/web/20111204150927/http://css-tricks.com:80/596-run-in/" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/198" + }] +} +!*/ +/*! +{ + "name": "CSS Display table", + "property": "displaytable", + "caniuse": "css-table", + "authors": ["scottjehl"], + "tags": ["css"], + "builderAliases": ["css_displaytable"], + "notes": [{ + "name": "Detects for all additional table display values", + "href": "https://pastebin.com/Gk9PeVaQ" + }] +} +!*/ +/*! +{ + "name": "CSS text-overflow ellipsis", + "property": "ellipsis", + "caniuse": "text-overflow", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS.escape()", + "property": "cssescape", + "polyfills": ["css-escape"], + "tags": ["css", "cssom"] +} +!*/ +/*! +{ + "name": "CSS Font ex Units", + "authors": ["Ron Waldon (@jokeyrhyme)"], + "property": "cssexunit", + "caniuse": "mdn-css_types_length_ex", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-values/#font-relative-lengths" + }] +} +!*/ +/*! +{ + "name": "CSS Supports", + "property": "supports", + "caniuse": "css-featurequeries", + "tags": ["css"], + "builderAliases": ["css_supports"], + "notes": [{ + "name": "W3C Spec (The @supports rule)", + "href": "https://dev.w3.org/csswg/css3-conditional/#at-supports" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/648" + }, { + "name": "W3C Spec (The CSSSupportsRule interface)", + "href": "https://dev.w3.org/csswg/css3-conditional/#the-csssupportsrule-interface" + }] +} +!*/ +/*! +{ + "name": "CSS Filters", + "property": "cssfilters", + "caniuse": "css-filters", + "polyfills": ["polyfilter"], + "tags": ["css"], + "builderAliases": ["css_filters"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/filter" + }] +} +!*/ +/*! +{ + "name": "Flexbox", + "property": "flexbox", + "caniuse": "flexbox", + "tags": ["css"], + "notes": [{ + "name": "The _new_ flexbox", + "href": "https://www.w3.org/TR/css-flexbox-1/" + }], + "warnings": [ + "A `true` result for this detect does not imply that the `flex-wrap` property is supported; see the `flexwrap` detect." + ] +} +!*/ +/*! +{ + "name": "Flexbox (legacy)", + "property": "flexboxlegacy", + "tags": ["css"], + "polyfills": ["flexie"], + "notes": [{ + "name": "The _old_ flexbox", + "href": "https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/" + }] +} +!*/ +/*! +{ + "name": "Flexbox (tweener)", + "property": "flexboxtweener", + "tags": ["css"], + "polyfills": ["flexie"], + "notes": [{ + "name": "The _inbetween_ flexbox", + "href": "https://www.w3.org/TR/2011/WD-css3-flexbox-20111129/" + }], + "warnings": ["This represents an old syntax, not the latest standard syntax."] +} +!*/ +/*! +{ + "name": "Flex Gap", + "property": "flexgap", + "caniuse": "flexbox-gap", + "tags": ["css", "flexbox"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-align-3/#gaps" + }], + "authors": ["Chris Smith (@chris13524)"] +} +!*/ +/*! +{ + "name": "Flex Line Wrapping", + "property": "flexwrap", + "tags": ["css", "flexbox"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-flexbox-1/" + }], + "warnings": [ + "Does not imply a modern implementation – see documentation." + ] +} +!*/ +/*! +{ + "name": "CSS :focus-visible pseudo-selector", + "caniuse": "css-focus-visible", + "property": "focusvisible", + "authors": ["@esaborit4code"], + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS :focus-within pseudo-selector", + "caniuse": "css-focus-within", + "property": "focuswithin", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "Font Display", + "property": "fontdisplay", + "authors": ["Patrick Kettner"], + "caniuse": "css-font-rendering-controls", + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-fonts-4/#font-display-desc" + }, { + "name": "`font-display` for the masses", + "href": "https://css-tricks.com/font-display-masses/" + }] +} +!*/ +/*! +{ + "name": "@font-face", + "property": "fontface", + "authors": ["Diego Perini", "Mat Marquis"], + "tags": ["css"], + "knownBugs": [ + "False Positive: WebOS https://github.com/Modernizr/Modernizr/issues/342", + "False Positive: WP7 https://github.com/Modernizr/Modernizr/issues/538" + ], + "notes": [{ + "name": "@font-face detection routine by Diego Perini", + "href": "http://javascript.nwbox.com/CSSSupport/" + }, { + "name": "Filament Group @font-face compatibility research", + "href": "https://docs.google.com/presentation/d/1n4NyG4uPRjAA8zn_pSQ_Ket0RhcWC6QlZ6LMjKeECo0/edit#slide=id.p" + }, { + "name": "Filament Grunticon/@font-face device testing results", + "href": "https://docs.google.com/spreadsheet/ccc?key=0Ag5_yGvxpINRdHFYeUJPNnZMWUZKR2ItMEpRTXZPdUE#gid=0" + }, { + "name": "CSS fonts on Android", + "href": "https://stackoverflow.com/questions/3200069/css-fonts-on-android" + }, { + "name": "@font-face and Android", + "href": "http://archivist.incutio.com/viewlist/css-discuss/115960" + }] +} +!*/ +/*! +{ + "name": "CSS Generated Content", + "property": "generatedcontent", + "tags": ["css"], + "warnings": ["Android may not return correct height for anything below 7px in old versions #738"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-selectors/#gen-content" + }, { + "name": "MDN Docs on :before", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/::before" + }, { + "name": "MDN Docs on :after", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/::after" + }] +} +!*/ +/*! +{ + "name": "CSS Gradients", + "caniuse": "css-gradients", + "property": "cssgradients", + "tags": ["css"], + "knownBugs": ["False-positives on webOS (https://github.com/Modernizr/Modernizr/issues/202)"], + "notes": [{ + "name": "Webkit Gradient Syntax", + "href": "https://webkit.org/blog/175/introducing-css-gradients/" + }, { + "name": "Linear Gradient Syntax", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient" + }, { + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-images-3/#gradients" + }] +} +!*/ +/*! { + "name": "CSS Hairline", + "property": "hairline", + "tags": ["css"], + "authors": ["strarsis"], + "notes": [{ + "name": "Blog post about CSS retina hairlines", + "href": "http://dieulot.net/css-retina-hairline" + }, { + "name": "Derived from", + "href": "https://gist.github.com/dieulot/520a49463f6058fbc8d1" + }] +} +!*/ +/*! +{ + "name": "CSS HSLA Colors", + "caniuse": "css3-colors", + "property": "hsla", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Hyphens", + "caniuse": "css-hyphens", + "property": ["csshyphens", "softhyphens", "softhyphensfind"], + "tags": ["css"], + "builderAliases": ["css_hyphens"], + "async": true, + "authors": ["David Newton"], + "warnings": [ + "These tests currently require document.body to be present", + "If loading Hyphenator.js via yepnope, be cautious of issue 158: https://github.com/mnater/hyphenator/issues/158", + "This is very large – only include it if you absolutely need it" + ], + "notes": [{ + "name": "The Current State of Hyphenation on the Web.", + "href": "https://davidnewton.ca/the-current-state-of-hyphenation-on-the-web" + }, { + "name": "Hyphenation Test Page", + "href": "https://web.archive.org/web/20150319125549/http://davidnewton.ca/demos/hyphenation/test.html" + }, { + "name": "Hyphenation is Language Specific", + "href": "https://code.google.com/p/hyphenator/source/diff?spec=svn975&r=975&format=side&path=/trunk/Hyphenator.js#sc_svn975_313" + }, { + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/312" + }] +} +!*/ +/*! +{ + "name": "CSS :invalid pseudo-class", + "property": "cssinvalid", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid" + }] +} +!*/ +/*! +{ + "name": "CSS :last-child pseudo-selector", + "caniuse": "css-sel3", + "property": "lastchild", + "tags": ["css"], + "builderAliases": ["css_lastchild"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/304" + }] +} +!*/ +/*! +{ + "name": "CSS Mask", + "caniuse": "css-masks", + "property": "cssmask", + "tags": ["css"], + "builderAliases": ["css_mask"], + "notes": [{ + "name": "Webkit blog on CSS Masks", + "href": "https://webkit.org/blog/181/css-masks/" + }, { + "name": "Safari Docs", + "href": "https://developer.apple.com/library/archive/documentation/InternetWeb/Conceptual/SafariVisualEffectsProgGuide/Masks/Masks.html" + }, { + "name": "CSS SVG mask", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/mask" + }, { + "name": "Combine with clippaths for awesomeness", + "href": "https://web.archive.org/web/20150508193041/http://generic.cx:80/for/webkit/test.html" + }] +} +!*/ +/*! +{ + "name": "CSS Media Queries", + "caniuse": "css-mediaqueries", + "property": "mediaqueries", + "tags": ["css"], + "builderAliases": ["css_mediaqueries"] +} +!*/ +/*! +{ + "name": "CSS Multiple Backgrounds", + "caniuse": "multibackgrounds", + "property": "multiplebgs", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS :nth-child pseudo-selector", + "caniuse": "css-sel3", + "property": "nthchild", + "tags": ["css"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/685" + }, { + "name": "Sitepoint :nth-child documentation", + "href": "https://www.sitepoint.com/atoz-css-screencast-nth-child/" + }], + "authors": ["@emilchristensen"], + "knownBugs": ["Known false negative in Safari 3.1 and Safari 3.2.2"] +} +!*/ +/*! +{ + "name": "CSS Object Fit", + "caniuse": "object-fit", + "property": "objectfit", + "tags": ["css"], + "builderAliases": ["css_objectfit"], + "notes": [{ + "name": "Opera Article on Object Fit", + "href": "https://dev.opera.com/articles/css3-object-fit-object-position/" + }] +} +!*/ +/*! +{ + "name": "CSS Opacity", + "caniuse": "css-opacity", + "property": "opacity", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Overflow Scrolling", + "property": "overflowscrolling", + "tags": ["css"], + "builderAliases": ["css_overflow_scrolling"], + "notes": [{ + "name": "Article on iOS overflow scrolling", + "href": "https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling" + }] +} +!*/ +/*! +{ + "name": "CSS Pointer Events", + "caniuse": "pointer-events", + "property": "csspointerevents", + "authors": ["ausi"], + "tags": ["css"], + "builderAliases": ["css_pointerevents"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events" + }, { + "name": "Test Project Page", + "href": "https://ausi.github.com/Feature-detection-technique-for-pointer-events/" + }, { + "name": "Test Project Wiki", + "href": "https://github.com/ausi/Feature-detection-technique-for-pointer-events/wiki" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/80" + }] +} +!*/ +/*! +{ + "name": "CSS position: sticky", + "property": "csspositionsticky", + "tags": ["css"], + "builderAliases": ["css_positionsticky"], + "notes": [{ + "name": "Chrome bug report", + "href": "https://bugs.chromium.org/p/chromium/issues/detail?id=322972" + }], + "warnings": ["using position:sticky on anything but top aligned elements is buggy in Chrome < 37 and iOS <=7+"] +} +!*/ +/*! +{ + "name": "CSS Generated Content Animations", + "property": "csspseudoanimations", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Transitions", + "property": "csstransitions", + "caniuse": "css-transitions", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Generated Content Transitions", + "property": "csspseudotransitions", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Reflections", + "caniuse": "css-reflections", + "property": "cssreflections", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Regions", + "caniuse": "css-regions", + "authors": ["Mihai Balan"], + "property": "regions", + "tags": ["css"], + "builderAliases": ["css_regions"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-regions/" + }] +} +!*/ +/*! +{ + "name": "CSS Font rem Units", + "caniuse": "rem", + "authors": ["nsfmc"], + "property": "cssremunit", + "tags": ["css"], + "builderAliases": ["css_remunit"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-values/#relative0" + }, { + "name": "Font Size with rem by Jonathan Snook", + "href": "https://snook.ca/archives/html_and_css/font-size-with-rem" + }] +} +!*/ +/*! +{ + "name": "CSS UI Resize", + "property": "cssresize", + "caniuse": "css-resize", + "tags": ["css"], + "builderAliases": ["css_resize"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-ui/#resize" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/CSS/resize" + }] +} +!*/ +/*! +{ + "name": "CSS rgba", + "caniuse": "css3-colors", + "property": "rgba", + "tags": ["css"], + "notes": [{ + "name": "CSSTricks Tutorial", + "href": "https://css-tricks.com/rgba-browser-support/" + }] +} +!*/ +/*! +{ + "name": "CSS Stylable Scrollbars", + "property": "cssscrollbar", + "tags": ["css"], + "builderAliases": ["css_scrollbars"] +} +!*/ +/*! +{ + "name": "Scroll Snap Points", + "property": "scrollsnappoints", + "caniuse": "css-snappoints", + "notes": [{ + "name": "Setting native-like scrolling offsets in CSS with Scrolling Snap Points", + "href": "http://generatedcontent.org/post/66817675443/setting-native-like-scrolling-offsets-in-css-with" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap_Points" + }], + "polyfills": ["scrollsnap"] +} +!*/ +/*! +{ + "name": "CSS Shapes", + "property": "shapes", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-shapes" + }, { + "name": "Examples from Adobe", + "href": "https://web.archive.org/web/20171230010236/http://webplatform.adobe.com:80/shapes" + }, { + "name": "Examples from CSS-Tricks", + "href": "https://css-tricks.com/examples/ShapesOfCSS/" + }] +} +!*/ +/*! +{ + "name": "CSS general sibling selector", + "caniuse": "css-sel3", + "property": "siblinggeneral", + "tags": ["css"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/889" + }] +} +!*/ +/*! +{ + "name": "CSS Subpixel Fonts", + "property": "subpixelfont", + "tags": ["css"], + "builderAliases": ["css_subpixelfont"], + "authors": ["@derSchepp", "@gerritvanaaken", "@rodneyrehm", "@yatil", "@ryanseddon"], + "notes": [{ + "name": "Origin Test", + "href": "https://github.com/gerritvanaaken/subpixeldetect" + }] +} +!*/ +/*! +{ + "name": "CSS :target pseudo-class", + "caniuse": "css-sel3", + "property": "target", + "tags": ["css"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:target" + }], + "authors": ["@zachleat"], + "warnings": ["Opera Mini supports :target but doesn't update the hash for anchor links."] +} +!*/ +/*! +{ + "name": "CSS text-align-last", + "property": "textalignlast", + "caniuse": "css-text-align-last", + "tags": ["css"], + "warnings": ["IE does not support the 'start' or 'end' values."], + "notes": [{ + "name": "Quirksmode", + "href": "https://www.quirksmode.org/css/text/textalignlast.html" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-align-last" + }] +} +!*/ +/*! +{ + "name": "CSS textDecoration", + "property": "textdecoration", + "caniuse": "text-decoration", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-text-decor-3/#line-decoration" + }] +} +!*/ +/*! +{ + "name": "CSS textshadow", + "property": "textshadow", + "caniuse": "css-textshadow", + "tags": ["css"], + "knownBugs": ["FF3.0 will false positive on this test"] +} +!*/ +/*! +{ + "name": "CSS Transforms", + "property": "csstransforms", + "caniuse": "transforms2d", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Transforms 3D", + "property": "csstransforms3d", + "caniuse": "transforms3d", + "tags": ["css"], + "knownBugs": [ + "Chrome may occasionally fail this test on some systems; more info: https://bugs.chromium.org/p/chromium/issues/detail?id=129004, however, the issue has since been closed (marked as fixed)." + ] +} +!*/ +/*! +{ + "name": "CSS Transforms Level 2", + "property": "csstransformslevel2", + "authors": ["rupl"], + "tags": ["css"], + "notes": [{ + "name": "CSSWG Draft Spec", + "href": "https://drafts.csswg.org/css-transforms-2/" + }] +} +!*/ +/*! +{ + "name": "CSS Transform Style preserve-3d", + "property": "preserve3d", + "authors": ["denyskoch", "aFarkas"], + "tags": ["css"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/transform-style" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/1748" + }] +} +!*/ +/*! +{ + "name": "CSS user-select", + "property": "userselect", + "caniuse": "user-select-none", + "authors": ["ryan seddon"], + "tags": ["css"], + "builderAliases": ["css_userselect"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/250" + }] +} +!*/ +/*! +{ + "name": "CSS :valid pseudo-class", + "property": "cssvalid", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:valid" + }] +} +!*/ +/*! +{ + "name": "Variable Open Type Fonts", + "property": "variablefonts", + "authors": ["Patrick Kettner"], + "tags": ["css"], + "notes": [{ + "name": "Variable fonts on the web", + "href": "https://webkit.org/blog/7051/variable-fonts-on-the-web/" + }, { + "name": "Variable fonts for responsive design", + "href": "https://alistapart.com/blog/post/variable-fonts-for-responsive-design" + }] +} +!*/ +/*! +{ + "name": "CSS vh unit", + "property": "cssvhunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vhunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "Similar JSFiddle", + "href": "https://jsfiddle.net/FWeinb/etnYC/" + }] +} +!*/ +/*! +{ + "name": "CSS vmax unit", + "property": "cssvmaxunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vmaxunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/glsee/JDsWQ/4/" + }] +} +!*/ +/*! +{ + "name": "CSS vmin unit", + "property": "cssvminunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vminunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/glsee/JRmdq/8/" + }] +} +!*/ +/*! +{ + "name": "CSS vw unit", + "property": "cssvwunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vwunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/FWeinb/etnYC/" + }] +} +!*/ +/*! +{ + "name": "will-change", + "property": "willchange", + "caniuse": "will-change", + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-will-change/" + }] +} +!*/ +/*! +{ + "name": "CSS wrap-flow", + "property": "wrapflow", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-exclusions" + }, { + "name": "Example by Louie Rootfield", + "href": "https://webdesign.tutsplus.com/tutorials/css-exclusions--cms-28087" + }] +} +!*/ +/*! +{ + "name": "Custom Elements API", + "property": "customelements", + "caniuse": "custom-elementsv1", + "tags": ["customelements"], + "polyfills": ["customelements"], + "notes": [{ + "name": "Specs for Custom Elements", + "href": "https://www.w3.org/TR/custom-elements/" + }] +} +!*/ +/*! +{ + "name": "Custom protocol handler", + "property": "customprotocolhandler", + "authors": ["Ben Schwarz"], + "builderAliases": ["custom_protocol_handler"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/dev/system-state.html#custom-handlers" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/navigator.registerProtocolHandler" + }] +} +!*/ +/*! +{ + "name": "Dart", + "property": "dart", + "authors": ["Theodoor van Donge"], + "notes": [{ + "name": "Language website", + "href": "https://www.dartlang.org/" + }] +} +!*/ +/*! +{ + "name": "DataView", + "property": "dataview", + "authors": ["Addy Osmani"], + "builderAliases": ["dataview_api"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/JavaScript_typed_arrays/DataView" + }], + "polyfills": ["jdataview"] +} +!*/ +/*! +{ + "name": "classList", + "caniuse": "classlist", + "property": "classlist", + "tags": ["dom"], + "builderAliases": ["dataview_api"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/DOM/element.classList" + }] +} +!*/ +/*! +{ + "name": "createElement with Attributes", + "property": ["createelementattrs", "createelement-attrs"], + "tags": ["dom"], + "builderAliases": ["dom_createElement_attrs"], + "authors": ["James A. Rosen"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/258" + }] +} +!*/ +/*! +{ + "name": "dataset API", + "caniuse": "dataset", + "property": "dataset", + "tags": ["dom"], + "builderAliases": ["dom_dataset"], + "authors": ["@phiggins42"] +} +!*/ +/*! +{ + "name": "Document Fragment", + "property": "documentfragment", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-B63ED1A3" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment" + }, { + "name": "QuirksMode Compatibility Tables", + "href": "https://www.quirksmode.org/m/w3c_core.html#t112" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "knownBugs": ["false-positive on Blackberry 9500, see QuirksMode note"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "[hidden] Attribute", + "property": "hidden", + "tags": ["dom"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/dev/interaction.html#the-hidden-attribute" + }, { + "name": "original implementation of detect code", + "href": "https://github.com/aFarkas/html5shiv/blob/bf4fcc4/src/html5shiv.js#L38" + }], + "polyfills": ["html5shiv"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Intersection Observer", + "property": "intersectionobserver", + "caniuse": "intersectionobserver", + "tags": ["dom"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/IntersectionObserver/" + }, { + "name": "IntersectionObserver polyfill", + "href": "https://github.com/w3c/IntersectionObserver/tree/master/polyfill" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/docs/Web/API/Intersection_Observer_API" + }] +} +!*/ +/*! +{ + "name": "microdata", + "property": "microdata", + "tags": ["dom"], + "builderAliases": ["dom_microdata"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/microdata/" + }] +} +!*/ +/*! +{ + "name": "DOM4 MutationObserver", + "property": "mutationobserver", + "caniuse": "mutationobserver", + "tags": ["dom"], + "authors": ["Karel Sedláček (@ksdlck)"], + "polyfills": ["mutationobservers"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver" + }] +} +!*/ +/*! +{ + "property": "passiveeventlisteners", + "caniuse": "passive-event-listener", + "tags": ["dom"], + "authors": ["Rick Byers"], + "name": "Passive event listeners", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://dom.spec.whatwg.org/#dom-addeventlisteneroptions-passive" + }, { + "name": "WICG explainer", + "href": "https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md" + }] +} +!*/ +/*! +{ + "name": "Shadow DOM API", + "property": "shadowroot", + "caniuse": "shadowdomv1", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot" + }], + "authors": ["Kevin Coyle (@kevin-coyle-unipro)", "Pascal Lim (@pascalim)"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "Shadow DOM API (Legacy)", + "property": "shadowrootlegacy", + "caniuse": "shadowdom", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Element/createShadowRoot" + }], + "authors": ["Kevin Coyle (@kevin-coyle-unipro)", "Pascal Lim (@pascalim)"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "bdi Element", + "property": "bdi", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdi" + }] +} +!*/ +/*! +{ + "name": "details Element", + "caniuse": "details", + "property": "details", + "tags": ["elem"], + "builderAliases": ["elem_details"], + "authors": ["@mathias"], + "notes": [{ + "name": "Mathias' Original", + "href": "https://mathiasbynens.be/notes/html5-details-jquery#comment-35" + }] +} +!*/ +/*! +{ + "name": "output Element", + "property": "outputelem", + "tags": ["elem"], + "builderAliases": ["elem_output"], + "notes": [{ + "name": "WhatWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element" + }] +} +!*/ +/*! +{ + "name": "picture Element", + "property": "picture", + "tags": ["elem"], + "authors": ["Scott Jehl", "Mat Marquis"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#embedded-content" + }, { + "name": "Relevant spec issue", + "href": "https://github.com/ResponsiveImagesCG/picture-element/issues/87" + }] +} +!*/ +/*! +{ + "name": "progress Element", + "caniuse": "progress", + "property": ["progressbar", "meter"], + "tags": ["elem"], + "builderAliases": ["elem_progress_meter"], + "authors": ["Stefan Wallin"] +} +!*/ +/*! +{ + "name": "ruby, rp, rt Elements", + "caniuse": "ruby", + "property": "ruby", + "tags": ["elem"], + "builderAliases": ["elem_ruby"], + "authors": ["Cătălin Mariș"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-ruby-element" + }] +} +!*/ +/*! +{ + "name": "Template Tag", + "property": "template", + "caniuse": "template", + "tags": ["elem"], + "notes": [{ + "name": "HTML5Rocks Article", + "href": "https://www.html5rocks.com/en/tutorials/webcomponents/template/" + }, { + "name": "W3C Spec", + "href": "https://web.archive.org/web/20171130222649/http://www.w3.org/TR/html5/scripting-1.html" + }] +} +!*/ +/*! +{ + "name": "time Element", + "property": "time", + "tags": ["elem"], + "builderAliases": ["elem_time"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-time-element" + }] +} +!*/ +/*! +{ + "name": "Track element and Timed Text Track", + "property": ["texttrackapi", "track"], + "tags": ["elem"], + "builderAliases": ["elem_track"], + "authors": ["Addy Osmani"], + "notes": [{ + "name": "W3C Spec (Track Element)", + "href": "https://web.archive.org/web/20121119095019/http://www.w3.org/TR/html5/the-track-element.html#the-track-element" + }, { + "name": "W3C Spec (Track API)", + "href": "https://web.archive.org/web/20121119094620/http://www.w3.org/TR/html5/media-elements.html#text-track-api" + }], + "warnings": ["While IE10 has implemented the track element, IE10 does not expose the underlying APIs to create timed text tracks by JS (really sad)"] +} +!*/ +/*! +{ + "name": "Unknown Elements", + "property": "unknownelements", + "tags": ["elem"], + "notes": [{ + "name": "The Story of the HTML5 Shiv", + "href": "https://www.paulirish.com/2011/the-history-of-the-html5-shiv/" + }, { + "name": "original implementation of detect code", + "href": "https://github.com/aFarkas/html5shiv/blob/bf4fcc4/src/html5shiv.js#L36" + }], + "polyfills": ["html5shiv"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Emoji", + "property": "emoji" +} +!*/ +/*! +{ + "name": "ES5 Array", + "property": "es5array", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Date", + "property": "es5date", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Function", + "property": "es5function", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Object", + "property": "es5object", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim", "es5sham"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Strict Mode", + "property": "strictmode", + "caniuse": "use-strict", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "authors": ["@kangax"], + "tags": ["es5"], + "builderAliases": ["es5_strictmode"] +} +!*/ +/*! +{ + "name": "ES5 String", + "property": "es5string", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "JSON", + "property": "json", + "caniuse": "json", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Glossary/JSON" + }], + "polyfills": ["json2"] +} +!*/ +/*! +{ + "name": "ES5 Syntax", + "property": "es5syntax", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }, { + "name": "original implementation of detect code", + "href": "https://kangax.github.io/compat-table/es5/" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "warnings": ["This detect uses `eval()`, so CSP may be a problem."], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Immutable Undefined", + "property": "es5undefined", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }, { + "name": "original implementation of detect code", + "href": "https://kangax.github.io/compat-table/es5/" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5", + "property": "es5", + "caniuse": "es5", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim", "es5sham"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES6 Array", + "property": "es6array", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Arrow Functions", + "property": "arrow", + "authors": ["Vincent Riemer"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Class", + "property": "es6class", + "notes": [{ + "name": "ECMAScript 6 language specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions" + }], + "caniuse": "es6-class", + "authors": ["dabretin"], + "tags": ["es6"], + "builderAliases": ["class"] +} +!*/ +/*! +{ + "name": "ES6 Collections", + "property": "es6collections", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim", "weakmap"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Generators", + "property": "generators", + "authors": ["Michael Kachanovskyi"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Math", + "property": "es6math", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Number", + "property": "es6number", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Object", + "property": "es6object", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Promises", + "property": "promises", + "caniuse": "promises", + "polyfills": ["es6promises"], + "authors": ["Krister Kari", "Jake Archibald"], + "tags": ["es6"], + "notes": [{ + "name": "The ES6 promises spec", + "href": "https://github.com/domenic/promises-unwrapping" + }, { + "name": "Chromium dashboard - ES6 Promises", + "href": "https://www.chromestatus.com/features/5681726336532480" + }, { + "name": "JavaScript Promises: an Introduction", + "href": "https://developers.google.com/web/fundamentals/primers/promises/" + }] +} +!*/ +/*! +{ + "name": "ES6 Rest parameters", + "property": "restparameters", + "notes": [{ + "name": "ECMAScript 6 language specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-function-definitions" + }], + "caniuse": "rest", + "authors": ["dabretin"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Spread array", + "property": "spreadarray", + "notes": [{ + "name": "ECMAScript Specification", + "href": "https://tc39.es/ecma262/#sec-array-initializer" + }, + { + "name": "Article", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax" + }], + "caniuse": "mdn-javascript_operators_spread_spread_in_arrays", + "authors": ["dabretin"], + "warnings": ["not for object literals (implemented in ES7)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Template Strings", + "property": "stringtemplate", + "caniuse": "template-literals", + "builderAliases": ["templatestrings"], + "notes": [{ + "name": "ECMAScript 6 draft specification", + "href": "https://tc39wiki.calculist.org/es6/template-strings/" + }], + "authors": ["dabretin"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 String", + "property": "es6string", + "notes": [{ + "name": "ECMAScript 6 Specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript Specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Symbol", + "property": "es6symbol", + "caniuse": "mdn-javascript_builtins_symbol", + "notes": [{ + "name": "Official ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-constructor" + },{ + "name": "MDN web docs", + "href": "https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Symbol" + }], + "polyfills": ["es6symbol"], + "authors": ["buhichan (@buhichan)"], + "tags": ["es6","symbol"] +} +!*/ +/*! +{ + "name": "ES7 Array", + "property": "es7array", + "notes": [{ + "name": "ECMAScript array Specification", + "href": "https://tc39.es/ecma262/#sec-array.prototype.includes" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES7 Rest destructuring", + "property": ["restdestructuringarray", "restdestructuringobject"], + "caniuse" : "destructuring%20assignment", + "notes": [{ + "name": "ECMAScript Destructuring Assignment Specification", + "href": "https://tc39.es/ecma262/#sec-destructuring-assignment" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES7 Spread object", + "property": "spreadobject", + "notes": [{ + "name": "ECMAScript array Specification", + "href": "http://www.ecma-international.org/ecma-262/#sec-object-initializer" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES8 Object", + "property": "es8object", + "notes": [{ + "name": "ECMAScript specification: Object.entries", + "href": "https://www.ecma-international.org/ecma-262/#sec-object.entries" + }, { + "name": "ECMAScript specification: Object.values", + "href": "https://www.ecma-international.org/ecma-262/#sec-object.values" + }], + "caniuse": "object-entries,object-values", + "authors": ["dabretin"], + "tags": ["es8"] +} +!*/ +/*! +{ + "name": "CustomEvent", + "property": "customevent", + "tags": ["customevent"], + "authors": ["Alberto Elias"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/DOM-Level-3-Events/#interface-CustomEvent" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/docs/Web/API/CustomEvent" + }], + "polyfills": ["eventlistener"] +} +!*/ +/*! +{ + "name": "Orientation and Motion Events", + "property": ["devicemotion", "deviceorientation"], + "caniuse": "deviceorientation", + "notes": [{ + "name": "W3C Editor's Draft Spec", + "href": "https://w3c.github.io/deviceorientation/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation" + }], + "authors": ["Shi Chuan"], + "tags": ["event"], + "builderAliases": ["event_deviceorientation_motion"] +} +!*/ +/*! +{ + "name": "Event Listener", + "property": "eventlistener", + "caniuse": "addeventlistener", + "authors": ["Andrew Betts (@triblondon)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Registration-interfaces" + }], + "polyfills": ["eventlistener"] +} +!*/ +/*! +{ + "name": "Force Touch Events", + "property": "forcetouch", + "authors": ["Kraig Walker"], + "notes": [{ + "name": "Responding to Force Touch Events from JavaScript", + "href": "https://developer.apple.com/library/archive/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html" + }] +} +!*/ +/*! +{ + "name": "Hashchange event", + "property": "hashchange", + "caniuse": "hashchange", + "tags": ["history"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange" + }], + "polyfills": [ + "jquery-hashchange", + "moo-historymanager", + "jquery-ajaxy", + "hasher", + "shistory" + ] +} +!*/ +/*! +{ + "name": "onInput Event", + "property": "oninput", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers.oninput" + }, { + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/input.html#common-input-element-attributes" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/210" + }], + "authors": ["Patrick Kettner"], + "tags": ["event"] +} +!*/ +/*! +{ + "name": "DOM Pointer Events API", + "property": "pointerevents", + "caniuse": "pointer", + "tags": ["input"], + "authors": ["Stu Cox"], + "notes": [{ + "name": "W3C Spec (Pointer Events)", + "href": "https://www.w3.org/TR/pointerevents/" + }, { + "name": "W3C Spec (Pointer Events Level 2)", + "href": "https://www.w3.org/TR/pointerevents2/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent" + }], + "warnings": ["This property name now refers to W3C DOM PointerEvents: https://github.com/Modernizr/Modernizr/issues/548#issuecomment-12812099"], + "polyfills": ["pep"] +} +!*/ +/*! +{ + "name": "Proximity API", + "property": "proximity", + "authors": ["Cătălin Mariș"], + "tags": ["events", "proximity"], + "caniuse": "proximity", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Proximity_Events" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/proximity/" + }] +} +!*/ +/*! +{ + "name": "File API", + "property": "filereader", + "caniuse": "fileapi", + "notes": [{ + "name": "W3C Working Draft Spec", + "href": "https://www.w3.org/TR/FileAPI/" + }], + "tags": ["file"], + "builderAliases": ["file_api"], + "knownBugs": ["Will fail in Safari 5 due to its lack of support for the standards defined FileReader object"] +} +!*/ +/*! +{ + "name": "Filesystem API", + "property": "filesystem", + "caniuse": "filesystem", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/file-system-api/" + }], + "authors": ["Eric Bidelman (@ebidel)"], + "tags": ["file"], + "builderAliases": ["file_filesystem"], + "knownBugs": ["The API will be present in Chrome incognito, but will throw an exception. See crbug.com/93417"] +} +!*/ +/*! +{ + "name": "Flash", + "property": "flash", + "tags": ["flash"], + "polyfills": ["shumway"] +} +!*/ +/*! +{ + "name": "Fullscreen API", + "property": "fullscreen", + "caniuse": "fullscreen", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/API/Fullscreen" + }], + "polyfills": ["screenfulljs"], + "builderAliases": ["fullscreen_api"] +} +!*/ +/*! +{ + "name": "GamePad API", + "property": "gamepads", + "caniuse": "gamepad", + "authors": ["Eric Bidelman"], + "tags": ["media"], + "warnings": ["In new browsers it may return false in non-HTTPS connections"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/gamepad/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/doodles/gamepad/#toc-featuredetect" + }] +} +!*/ +/*! +{ + "name": "Geolocation API", + "property": "geolocation", + "caniuse": "geolocation", + "tags": ["media"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation" + }], + "polyfills": [ + "joshuabell-polyfill", + "webshims", + "geo-location-javascript", + "geolocation-api-polyfill" + ] +} +!*/ +/*! +{ + "name": "Hidden Scrollbar", + "property": "hiddenscroll", + "authors": ["Oleg Korsunsky"], + "tags": ["overlay"], + "notes": [{ + "name": "Overlay Scrollbar description", + "href": "https://developer.apple.com/library/mac/releasenotes/MacOSX/WhatsNewInOSX/Articles/MacOSX10_7.html#//apple_ref/doc/uid/TP40010355-SW39" + }, { + "name": "Video example of overlay scrollbars", + "href": "https://gfycat.com/FoolishMeaslyAtlanticsharpnosepuffer" + }] +} +!*/ +/*! +{ + "name": "History API", + "property": "history", + "caniuse": "history", + "tags": ["history"], + "authors": ["Hay Kranen", "Alexander Farkas"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/html51/browsers.html#the-history-interface" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/window.history" + }], + "polyfills": ["historyjs", "html5historyapi"] +} +!*/ +/*! +{ + "name": "HTML Imports", + "property": "htmlimports", + "tags": ["html", "import"], + "polyfills": ["polymer-htmlimports"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/webcomponents/spec/imports/" + }, { + "name": "HTML Imports - #include for the web", + "href": "https://www.html5rocks.com/en/tutorials/webcomponents/imports/" + }] +} +!*/ +/*! +{ + "name": "IE8 compat mode", + "property": "ie8compat", + "authors": ["Erich Ocean"] +} +!*/ +/*! +{ + "name": "iframe[sandbox] Attribute", + "property": "sandbox", + "caniuse": "iframe-sandbox", + "tags": ["iframe"], + "builderAliases": ["iframe_sandbox"], + "notes": [ + { + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox" + }], + "knownBugs": ["False-positive on Firefox < 29"] +} +!*/ +/*! +{ + "name": "iframe[seamless] Attribute", + "property": "seamless", + "tags": ["iframe"], + "builderAliases": ["iframe_seamless"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-seamless" + }] +} +!*/ +/*! +{ + "name": "iframe[srcdoc] Attribute", + "property": "srcdoc", + "caniuse": "iframe-srcdoc", + "tags": ["iframe"], + "builderAliases": ["iframe_srcdoc"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-srcdoc" + }] +} +!*/ +/*! +{ + "name": "Animated PNG", + "async": true, + "property": "apng", + "caniuse": "apng", + "tags": ["image"], + "builderAliases": ["img_apng"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/APNG" + }] +} +!*/ +/*! +{ + "name": "AVIF", + "async": true, + "property": "avif", + "caniuse": "avif", + "tags": ["image"], + "authors": ["Markel Ferro (@MarkelFe)"], + "polyfills": ["avifjs"], + "notes": [{ + "name": "Avif Spec", + "href": "https://aomediacodec.github.io/av1-avif/" + }] +} +!*/ +/*! +{ + "name": "Image crossOrigin", + "property": "imgcrossorigin", + "tags": ["image"], + "notes": [{ + "name": "Cross Domain Images and the Tainted Canvas", + "href": "https://blog.codepen.io/2013/10/08/cross-domain-images-tainted-canvas/" + }] +} +!*/ +/*! +{ + "name": "EXIF Orientation", + "property": "exiforientation", + "tags": ["image"], + "builderAliases": ["exif_orientation"], + "async": true, + "authors": ["Paul Sayre"], + "notes": [{ + "name": "Article by Dave Perrett", + "href": "https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/" + }, { + "name": "Article by Calvin Hass", + "href": "https://www.impulseadventure.com/photo/exif-orientation.html" + }] +} +!*/ +/*! +{ + "name": "JPEG 2000", + "async": true, + "aliases": ["jpeg-2000", "jpg2"], + "property": "jpeg2000", + "caniuse": "jpeg2000", + "tags": ["image"], + "authors": ["@eric_wvgg"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/JPEG_2000" + }] +} +!*/ +/*! +{ + "name": "JPEG XR (extended range)", + "async": true, + "aliases": ["jpeg-xr"], + "property": "jpegxr", + "tags": ["image"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/JPEG_XR" + }] +} +!*/ +/*! +{ + "name": "image and iframe native lazy loading", + "property": "lazyloading", + "caniuse": "loading-lazy-attr", + "tags": ["image", "lazy", "loading"], + "notes": [{ + "name": "Native image lazy-loading for the web", + "href": "https://addyosmani.com/blog/lazy-loading/" + }] +} +!*/ +/*! +{ + "name": "sizes attribute", + "async": true, + "property": "sizes", + "tags": ["image"], + "authors": ["Mat Marquis"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element" + }, { + "name": "Srcset and sizes", + "href": "https://ericportis.com/posts/2014/srcset-sizes/" + }] +} +!*/ +/*! +{ + "name": "srcset attribute", + "property": "srcset", + "caniuse": "srcset", + "tags": ["image"], + "notes": [{ + "name": "Smashing Magazine Article", + "href": "https://www.smashingmagazine.com/2013/08/webkit-implements-srcset-and-why-its-a-good-thing/" + }, { + "name": "Generate multi-resolution images for srcset with Grunt", + "href": "https://addyosmani.com/blog/generate-multi-resolution-images-for-srcset-with-grunt/" + }] +} +!*/ +/*! +{ + "name": "Webp Alpha", + "async": true, + "property": "webpalpha", + "aliases": ["webp-alpha"], + "tags": ["image"], + "authors": ["Krister Kari", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "WebP Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Article about WebP support", + "href": "https://optimus.keycdn.com/support/webp-support/" + }, { + "name": "Chromium WebP announcement", + "href": "https://blog.chromium.org/2011/11/lossless-and-transparency-encoding-in.html?m=1" + }] +} +!*/ +/*! +{ + "name": "Webp Animation", + "async": true, + "property": "webpanimation", + "aliases": ["webp-animation"], + "tags": ["image"], + "authors": ["Krister Kari", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "WebP Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Chromium blog - Chrome 32 Beta: Animated WebP images and faster Chrome for Android touch input", + "href": "https://blog.chromium.org/2013/11/chrome-32-beta-animated-webp-images-and.html" + }] +} +!*/ +/*! +{ + "name": "Webp Lossless", + "async": true, + "property": ["webplossless", "webp-lossless"], + "tags": ["image"], + "authors": ["@amandeep", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "Webp Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Webp Lossless Spec", + "href": "https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification" + }] +} +!*/ +/*! +{ + "name": "Webp", + "async": true, + "property": "webp", + "caniuse": "webp", + "tags": ["image"], + "builderAliases": ["img_webp"], + "authors": ["Krister Kari", "@amandeep", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "Webp Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Chromium blog - Chrome 32 Beta: Animated WebP images and faster Chrome for Android touch input", + "href": "https://blog.chromium.org/2013/11/chrome-32-beta-animated-webp-images-and.html" + }, { + "name": "Webp Lossless Spec", + "href": "https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification" + }, { + "name": "Article about WebP support", + "href": "https://optimus.keycdn.com/support/webp-support/" + }, { + "name": "Chromium WebP announcement", + "href": "https://blog.chromium.org/2011/11/lossless-and-transparency-encoding-in.html?m=1" + }] +} +!*/ +/*! +{ + "name": "input[capture] Attribute", + "property": "capture", + "tags": ["video", "image", "audio", "media", "attribute"], + "notes": [{ + "name": "W3C Draft Spec", + "href": "https://www.w3.org/TR/html-media-capture/" + }] +} +!*/ +/*! +{ + "name": "input[file] Attribute", + "property": "fileinput", + "caniuse": "forms", + "tags": ["file", "forms", "input"], + "builderAliases": ["forms_fileinput"] +} +!*/ +/*! +{ + "name": "input[directory] Attribute", + "property": "directory", + "authors": ["silverwind"], + "tags": ["file", "input", "attribute"] +} +!*/ +/*! +{ + "name": "input formaction", + "property": "inputformaction", + "aliases": ["input-formaction"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formaction" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formaction-attribute/" + }], + "polyfills": ["webshims"] +} +!*/ +/*! +{ + "name": "input[form] Attribute", + "property": "formattribute", + "tags": ["attribute", "forms", "input"], + "builderAliases": ["forms_formattribute"] +} +!*/ +/*! +{ + "name": "input formenctype", + "property": "inputformenctype", + "aliases": ["input-formenctype"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formenctype" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formenctype-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "input formmethod", + "property": "inputformmethod", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formmethod" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formmethod-attribute/" + }], + "polyfills": ["webshims"] +} +!*/ +/*! +{ + "name": "input formnovalidate", + "property": "inputformnovalidate", + "aliases": ["input-formnovalidate"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formnovalidate" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formnovalidate-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "input formtarget", + "property": "inputformtarget", + "aliases": ["input-formtarget"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formtarget" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formtarget-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "Input attributes", + "property": "input", + "tags": ["forms"], + "authors": ["Mike Taylor"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/input.html#input-type-attr-summary" + }], + "knownBugs": ["Some blackberry devices report false positive for input.multiple"] +} +!*/ +/*! +{ + "name": "Form input types", + "property": "inputtypes", + "caniuse": "forms", + "tags": ["forms"], + "authors": ["Mike Taylor"], + "polyfills": [ + "jquerytools", + "webshims", + "h5f", + "webforms2", + "nwxforms", + "fdslider", + "html5slider", + "galleryhtml5forms", + "jscolor", + "html5formshim", + "selectedoptionsjs", + "formvalidationjs" + ] +} +!*/ +/*! +{ + "name": "Form Validation", + "property": "formvalidation", + "tags": ["forms", "validation", "attribute"], + "builderAliases": ["forms_validation"] +} +!*/ +/*! +{ + "name": "input[type=\"number\"] Localization", + "property": "localizednumber", + "tags": ["forms", "localization", "attribute"], + "authors": ["Peter Janes"], + "notes": [{ + "name": "Webkit Bug Tracker Listing", + "href": "https://bugs.webkit.org/show_bug.cgi?id=42484" + }, { + "name": "Based on This", + "href": "https://trac.webkit.org/browser/trunk/LayoutTests/fast/forms/script-tests/input-number-keyoperation.js?rev=80096#L9" + }], + "knownBugs": ["Only ever returns true if the browser/OS is configured to use comma as a decimal separator. This is probably fine for most use cases."] +} +!*/ +/*! +{ + "name": "input[search] search event", + "property": "inputsearchevent", + "tags": ["input","search"], + "authors": ["Calvin Webster"], + "notes": [{ + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/search-type/" + }, { + "name": "CSS Tricks", + "href": "https://css-tricks.com/webkit-html5-search-inputs/" + }] +} +!*/ +/*! +{ + "name": "placeholder attribute", + "property": "placeholder", + "tags": ["forms", "attribute"], + "builderAliases": ["forms_placeholder"] +} +!*/ +/*! +{ + "name": "form#requestAutocomplete()", + "property": "requestautocomplete", + "tags": ["form", "forms", "requestAutocomplete", "payments"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://wiki.whatwg.org/wiki/RequestAutocomplete" + }] +} +!*/ +/*! +{ + "name": "Internationalization API", + "property": "intl", + "caniuse": "internationalization", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl" + }, { + "name": "ECMAScript spec", + "href": "https://www.ecma-international.org/ecma-402/1.0/" + }] +} + !*/ +/*! +{ + "name": "Font Ligatures", + "property": "ligatures", + "caniuse": "font-feature", + "notes": [{ + "name": "Cross-browser Web Fonts", + "href": "https://www.sitepoint.com/cross-browser-web-fonts-part-3/" + }] +} +!*/ +/*! +{ + "name": "Reverse Ordered Lists", + "property": "olreversed", + "notes": [{ + "name": "Impressive Webs article", + "href": "https://www.impressivewebs.com/reverse-ordered-lists-html5/" + }], + "builderAliases": ["lists_reversed"] +} +!*/ +/*! +{ + "name": "MathML", + "property": "mathml", + "caniuse": "mathml", + "authors": ["Addy Osmani", "Davide P. Cervone", "David Carlisle"], + "knownBugs": ["Firefox < 4 will likely return a false, however it does support MathML inside XHTML documents"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/Math/" + }], + "polyfills": ["mathjax"] +} +!*/ +/*! +{ + "name": "Media Source Extensions API", + "caniuse": "mediasource", + "property": "mediasource", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API" + }], + "builderAliases": ["media_source_extension_api"] +} +!*/ +/*! +{ + "name": "Hover Media Query", + "property": "hovermq", + "tags": ["mediaquery"] +} +!*/ +/*! +{ + "name": "Pointer Media Query", + "property": "pointermq", + "tags": ["mediaquery"] +} +!*/ +/*! +{ + "name": "Message Channel", + "property": "messagechannel", + "authors": ["Raju Konga (@kongaraju)"], + "caniuse": "channel-messaging", + "tags": ["performance", "messagechannel"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2011/WD-webmessaging-20110317/#message-channels" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API/Using_channel_messaging" + }] +} +!*/ +/*! +{ + "name": "Beacon API", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/navigator.sendBeacon" + }, { + "name": "W3C Spec", + "href": "https://w3c.github.io/beacon/" + }], + "property": "beacon", + "caniuse": "beacon", + "tags": ["beacon", "network"], + "authors": ["Cătălin Mariș"] +} +!*/ +/*! +{ + "name": "Connection Effective Type", + "notes": [{ + "name": "MDN documentation", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType" + }], + "property": "connectioneffectivetype", + "builderAliases": ["network_connection"], + "tags": ["network"] +} +!*/ +/*! +{ + "name": "Low Bandwidth Connection", + "property": "lowbandwidth", + "tags": ["network"], + "builderAliases": ["network_connection"] +} +!*/ +/*! +{ + "name": "Server Sent Events", + "property": "eventsource", + "tags": ["network"], + "builderAliases": ["network_eventsource"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events" + }] +} +!*/ +/*! +{ + "name": "Fetch API", + "property": "fetch", + "tags": ["network"], + "caniuse": "fetch", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://fetch.spec.whatwg.org/" + }], + "polyfills": ["fetch"] +} +!*/ +/*! +{ + "name": "XHR responseType='arraybuffer'", + "property": "xhrresponsetypearraybuffer", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='blob'", + "property": "xhrresponsetypeblob", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='document'", + "property": "xhrresponsetypedocument", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='json'", + "property": "xhrresponsetypejson", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }, { + "name": "Explanation of xhr.responseType='json'", + "href": "https://mathiasbynens.be/notes/xhr-responsetype-json" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='text'", + "property": "xhrresponsetypetext", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType", + "property": "xhrresponsetype", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XML HTTP Request Level 2 XHR2", + "property": "xhr2", + "caniuse": "xhr2", + "tags": ["network"], + "builderAliases": ["network_xhr2"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/XMLHttpRequest2/" + }, { + "name": "Details on Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/385" + }] +} +!*/ +/*! +{ + "name": "Notification", + "property": "notification", + "caniuse": "notifications", + "authors": ["Theodoor van Donge", "Hendrik Beskow"], + "notes": [{ + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/notifications/quick/" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/notifications/" + }, { + "name": "Changes in Chrome to Notifications API due to Service Worker Push Notifications", + "href": "https://developers.google.com/web/updates/2015/05/Notifying-you-of-notificiation-changes" + }], + "knownBugs": ["Possibility of false-positive on Chrome for Android if permissions we're granted for a website prior to Chrome 44."], + "polyfills": ["desktop-notify", "html5-notifications"] +} +!*/ +/*! +{ + "name": "Page Visibility API", + "property": "pagevisibility", + "caniuse": "pagevisibility", + "tags": ["performance"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/DOM/Using_the_Page_Visibility_API" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2011/WD-page-visibility-20110602/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/pagevisibility/intro/" + }], + "polyfills": ["visibilityjs", "visiblyjs", "jquery-visibility"] +} +!*/ +/*! +{ + "name": "Navigation Timing API", + "property": "performance", + "caniuse": "nav-timing", + "tags": ["performance"], + "authors": ["Scott Murphy (@uxder)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/navigation-timing/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/webperformance/basics/" + }], + "polyfills": ["perfnow"] +} +!*/ +/*! +{ + "name": "Pointer Lock API", + "property": "pointerlock", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/API/Pointer_Lock_API" + }], + "builderAliases": ["pointerlock_api"] +} +!*/ +/*! +{ + "name": "postMessage", + "property": "postmessage", + "caniuse": "x-doc-messaging", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/webmessaging/#crossDocumentMessages" + }], + "polyfills": ["easyxdm", "postmessage-jquery"], + "knownBugs": [ + "structuredclones - Android 2&3 can not send a structured clone of dates, filelists or regexps.", + "Some old WebKit versions have bugs." + ], + "warnings": ["To be safe you should stick with object, array, number and pixeldata."] +} +!*/ +/*! +{ + "name": "Proxy Object", + "property": "proxy", + "caniuse": "proxy", + "authors": ["Brock Beaudry"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" + }], + "polyfills": [ + "harmony-reflect" + ] +} +!*/ +/*! +{ + "name": "QuerySelector", + "property": "queryselector", + "caniuse": "queryselector", + "tags": ["queryselector"], + "authors": ["Andrew Betts (@triblondon)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/selectors-api/#queryselectorall" + }], + "polyfills": ["css-selector-engine"] +} +!*/ +/*! +{ + "name": "rel=prefetch", + "property": "prefetch", + "caniuse": "link-rel-prefetch", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/resource-hints/#prefetch" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/2536" + }] +} +!*/ +/*! +{ + "name": "requestAnimationFrame", + "property": "requestanimationframe", + "aliases": ["raf"], + "caniuse": "requestanimationframe", + "tags": ["animation"], + "authors": ["Addy Osmani"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/animation-timing/" + }], + "polyfills": ["raf"] +} +!*/ +/*! +{ + "name": "script[async]", + "property": "scriptasync", + "caniuse": "script-async", + "tags": ["script"], + "builderAliases": ["script_async"], + "authors": ["Theodoor van Donge"] +} +!*/ +/*! +{ + "name": "script[defer]", + "property": "scriptdefer", + "caniuse": "script-defer", + "tags": ["script"], + "builderAliases": ["script_defer"], + "authors": ["Theodoor van Donge"], + "warnings": ["Browser implementation of the `defer` attribute vary: https://stackoverflow.com/questions/3952009/defer-attribute-chrome#answer-3982619"], + "knownBugs": ["False positive in Opera 12"] +} +!*/ +/*! +{ + "name": "scrollToOptions dictionary", + "property": "scrolltooptions", + "caniuse": "mdn-api_scrolltooptions", + "notes": [{ + "name": "MDN docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo" + }], + "authors": ["Oliver Tušla (@asmarcz)", "Chris Smith (@chris13524)"] +} +!*/ +/*! +{ + "name": "ServiceWorker API", + "property": "serviceworker", + "caniuse": "serviceworkers", + "notes": [{ + "name": "ServiceWorkers Explained", + "href": "https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md" + }] +} +!*/ +/*! +{ + "property": "speechrecognition", + "caniuse": "speech-recognition", + "tags": ["input", "speech"], + "authors": ["Cătălin Mariș"], + "name": "Speech Recognition API", + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/speech-api/speechapi.html#speechreco-section" + }, { + "name": "Introduction to the Web Speech API", + "href": "https://developers.google.com/web/updates/2013/01/Voice-Driven-Web-Apps-Introduction-to-the-Web-Speech-API" + }] +} +!*/ +/*! +{ + "property": "speechsynthesis", + "caniuse": "speech-synthesis", + "tags": ["input", "speech"], + "authors": ["Cătălin Mariș"], + "name": "Speech Synthesis API", + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/speech-api/speechapi.html#tts-section" + }] +} +!*/ +/*! +{ + "name": "Cookies", + "property": "cookies", + "tags": ["storage"], + "authors": ["tauren"] +} +!*/ +/*! +{ + "name": "IndexedDB", + "property": "indexeddb", + "caniuse": "indexeddb", + "tags": ["storage"], + "polyfills": ["indexeddb"], + "async": true +} +!*/ +/*! +{ + "name": "IndexedDB Blob", + "property": "indexeddbblob", + "tags": ["storage"] +} +!*/ +/*! +{ + "name": "IndexedDB 2.0", + "property": "indexeddb2", + "tags": ["storage"], + "caniuse": "indexeddb2", + "authors": ["Tan Zhen Yong (@Xenonym)"], + "polyfills": ["indexeddb"], + "async": true +} +!*/ +/*! +{ + "name": "Local Storage", + "property": "localstorage", + "caniuse": "namevalue-storage", + "tags": ["storage"], + "polyfills": [ + "joshuabell-polyfill", + "cupcake", + "storagepolyfill", + "amplifyjs", + "yui-cacheoffline" + ] +} +!*/ +/*! +{ + "name": "Quota Storage Management API", + "property": "quotamanagement", + "tags": ["storage"], + "builderAliases": ["quota_management_api"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/quota-api/" + }] +} +!*/ +/*! +{ + "name": "Session Storage", + "property": "sessionstorage", + "tags": ["storage"], + "polyfills": ["joshuabell-polyfill", "cupcake", "storagepolyfill"] +} +!*/ +/*! +{ + "name": "IE User Data API", + "property": "userdata", + "tags": ["storage"], + "authors": ["@stereobooster"], + "notes": [{ + "name": "MSDN Documentation", + "href": "https://msdn.microsoft.com/en-us/library/ms531424.aspx" + }] +} +!*/ +/*! +{ + "name": "Web SQL Database", + "property": "websqldatabase", + "caniuse": "sql-storage", + "tags": ["storage"] +} +!*/ +/*! +{ + "name": "style[scoped]", + "property": "stylescoped", + "caniuse": "style-scoped", + "tags": ["dom"], + "builderAliases": ["style_scoped"], + "authors": ["Cătălin Mariș"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/semantics.html#attr-style-scoped" + }], + "polyfills": ["scoped-styles"] +} +!*/ +/*! +{ + "name": "SVG", + "property": "svg", + "caniuse": "svg", + "tags": ["svg"], + "authors": ["Erik Dahlstrom"], + "polyfills": [ + "svgweb", + "raphael", + "canvg", + "svg-boilerplate", + "sie", + "fabricjs" + ] +} +!*/ +/*! +{ + "name": "SVG as an tag source", + "property": "svgasimg", + "caniuse": "svg-img", + "tags": ["svg"], + "aliases": ["svgincss"], + "authors": ["Chris Coyier"], + "notes": [{ + "name": "HTML5 Spec", + "href": "https://www.w3.org/TR/html5/embedded-content-0.html#the-img-element" + }] +} +!*/ +/*! +{ + "name": "SVG clip paths", + "property": "svgclippaths", + "tags": ["svg"], + "notes": [{ + "name": "Demo", + "href": "http://srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg" + }] +} +!*/ +/*! +{ + "name": "SVG filters", + "property": "svgfilters", + "caniuse": "svg-filters", + "tags": ["svg"], + "builderAliases": ["svg_filters"], + "authors": ["Erik Dahlstrom"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/SVG11/filters.html" + }] +} +!*/ +/*! +{ + "name": "SVG foreignObject", + "property": "svgforeignobject", + "tags": ["svg"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/SVG11/extend.html" + }] +} +!*/ +/*! +{ + "name": "Inline SVG", + "property": "inlinesvg", + "caniuse": "svg-html5", + "tags": ["svg"], + "notes": [{ + "name": "Test page", + "href": "https://paulirish.com/demo/inline-svg" + }, { + "name": "Test page and results", + "href": "https://codepen.io/eltonmesquita/full/GgXbvo/" + }], + "polyfills": ["inline-svg-polyfill"], + "knownBugs": ["False negative on some Chromia browsers."] +} +!*/ +/*! +{ + "name": "SVG SMIL animation", + "property": "smil", + "caniuse": "svg-smil", + "tags": ["svg"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/AudioVideo/" + }] +} +!*/ +/*! +{ + "name": "textarea maxlength", + "property": "textareamaxlength", + "aliases": ["textarea-maxlength"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea" + }], + "polyfills": ["maxlength"] +} +!*/ +/*! +{ + "name": "Text Encoding/Decoding", + "property": ["textencoder", "textdecoder"], + "caniuse" : "textencoder", + "notes": [{ + "name": "MDN TextEncoder Doc", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder" + }, { + "name": "MDN TextDecoder Doc", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder" + }], + "authors": ["dabretin"] +} +!*/ +/*! +{ + "name": "Typed arrays", + "property": "typedarrays", + "caniuse": "typedarrays", + "tags": ["js"], + "authors": ["Stanley Stuart (@fivetanley)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays" + }, { + "name": "Kronos spec", + "href": "http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects" + }], + "polyfills": ["joshuabell-polyfill"] +} +!*/ +/*! +{ + "name": "Unicode Range", + "property": "unicoderange", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2013/CR-css-fonts-3-20131003/#descdef-unicode-range" + }, { + "name": "24 Way article", + "href": "https://24ways.org/2011/creating-custom-font-stacks-with-unicode-range" + }] +} +!*/ +/*! +{ + "name": "Blob URLs", + "property": "bloburls", + "caniuse": "bloburls", + "notes": [{ + "name": "W3C Working Draft Spec", + "href": "https://www.w3.org/TR/FileAPI/#creating-revoking" + }], + "tags": ["file", "url"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Data URI", + "property": "datauri", + "caniuse": "datauri", + "tags": ["url"], + "builderAliases": ["url_data_uri"], + "async": true, + "notes": [{ + "name": "Wikipedia article", + "href": "https://en.wikipedia.org/wiki/Data_URI_scheme" + }], + "warnings": ["Support in Internet Explorer 8 is limited to images and linked resources like CSS files, not HTML files"] +} +!*/ +/*! +{ + "name": "URL parser", + "property": "urlparser", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://url.spec.whatwg.org/" + }], + "polyfills": ["urlparser"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["url"] +} +!*/ +/*! +{ + "property": "urlsearchparams", + "caniuse": "urlsearchparams", + "tags": ["querystring", "url"], + "authors": ["Cătălin Mariș"], + "name": "URLSearchParams API", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://url.spec.whatwg.org/#interface-urlsearchparams" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" + }] +} +!*/ +/*! +{ + "name": "Vibration API", + "property": "vibrate", + "caniuse": "vibration", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/DOM/window.navigator.mozVibrate" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/vibration/" + }] +} +!*/ +/*! +{ + "name": "HTML5 Video", + "property": "video", + "caniuse": "video", + "tags": ["html5", "video", "media"], + "knownBugs": ["Without QuickTime, `Modernizr.video.h264` will be `undefined`; https://github.com/Modernizr/Modernizr/issues/546"], + "polyfills": [ + "html5media", + "mediaelementjs", + "videojs", + "leanbackplayer", + "videoforeverybody" + ] +} +!*/ +/*! +{ + "name": "Video Autoplay", + "property": "videoautoplay", + "tags": ["video"], + "async": true, + "warnings": ["This test is very large – only include it if you absolutely need it"], + "knownBugs": ["crashes with an alert on iOS7 when added to homescreen"] +} +!*/ +/*! +{ + "name": "Video crossOrigin", + "property": "videocrossorigin", + "caniuse": "cors", + "authors": ["Florian Mailliet"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes" + }] +} +!*/ +/*! +{ + "name": "Video Loop Attribute", + "property": "videoloop", + "tags": ["video", "media"] +} +!*/ +/*! +{ + "name": "Video Preload Attribute", + "property": "videopreload", + "tags": ["video", "media"] +} +!*/ +/*! +{ + "name": "VML", + "property": "vml", + "tags": ["vml"], + "authors": ["Craig Andrews (@candrews)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/NOTE-VML" + }, { + "name": "MSDN Documentation", + "href": "https://docs.microsoft.com/en-us/windows/desktop/VML/msdn-online-vml-introduction" + }] +} +!*/ +/*! +{ + "name": "Web Intents", + "property": "webintents", + "authors": ["Eric Bidelman"], + "notes": [{ + "name": "Web Intents project site", + "href": "http://www.webintents.org/" + }], + "builderAliases": ["web_intents"] +} +!*/ +/*! +{ + "name": "Web Animation API", + "property": "webanimations", + "caniuse": "web-animation", + "tags": ["webanimations"], + "polyfills": ["webanimationsjs"], + "notes": [{ + "name": "Introducing Web Animations", + "href": "https://birtles.wordpress.com/2013/06/26/introducing-web-animations/" + }] +} +!*/ +/*! +{ + "name": "PublicKeyCredential", + "notes": [ + { + "name": "MDN Documentation", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential" + }, + { + "name": "Google Developers solution", + "href": "https://developers.google.com/web/updates/2018/03/webauthn-credential-management#the_solution" + } + ], + "property": "publickeycredential", + "tags": ["webauthn", "web authentication"], + "authors": ["Eric Delia"] +} +!*/ +/*! +{ + "name": "WebGL", + "property": "webgl", + "caniuse": "webgl", + "tags": ["webgl", "graphics"], + "polyfills": ["jebgl", "cwebgl", "iewebgl"] +} +!*/ +/*! +{ + "name": "WebGL Extensions", + "property": "webglextensions", + "tags": ["webgl", "graphics"], + "builderAliases": ["webgl_extensions"], + "async": true, + "authors": ["Ilmari Heikkinen"], + "notes": [{ + "name": "Kronos extensions registry", + "href": "https://www.khronos.org/registry/webgl/extensions/" + }] +} +!*/ +/*! +{ + "name": "RTC Peer Connection", + "property": "peerconnection", + "caniuse": "rtcpeerconnection", + "tags": ["webrtc"], + "authors": ["Ankur Oberoi"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/webrtc/" + }] +} +!*/ +/*! +{ + "name": "RTC Data Channel", + "property": "datachannel", + "notes": [{ + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/webrtc/datachannels/" + }] +} +!*/ +/*! +{ + "name": "getUserMedia", + "property": "getusermedia", + "caniuse": "stream", + "tags": ["webrtc"], + "authors": ["Eric Bidelman", "Masataka Yakura"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia" + }] +} +!*/ +/*! +{ + "name": "MediaStream Recording API", + "property": "mediarecorder", + "caniuse": "mediarecorder", + "tags": ["mediarecorder", "media"], + "authors": ["Onkar Dahale"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API" + }] +} +!*/ +/*! +{ + "name": "WebSockets Support", + "property": "websockets", + "authors": ["Phread (@fearphage)", "Mike Sherov (@mikesherov)", "Burak Yigit Kaya (@BYK)"], + "caniuse": "websockets", + "tags": ["html5"], + "knownBugs": ["This test will reject any old version of WebSockets even if it is not prefixed such as in Safari 5.1"], + "notes": [{ + "name": "CLOSING State and Spec", + "href": "https://www.w3.org/TR/websockets/#the-websocket-interface" + }], + "polyfills": [ + "sockjs", + "socketio", + "websocketjs", + "atmosphere", + "graceful-websocket", + "portal", + "datachannel" + ] +} +!*/ +/*! +{ + "name": "Binary WebSockets", + "property": "websocketsbinary", + "tags": ["websockets"], + "builderAliases": ["websockets_binary"] +} +!*/ +/*! +{ + "name": "Base 64 encoding/decoding", + "property": "atobbtoa", + "builderAliases": ["atob-btoa"], + "caniuse": "atob-btoa", + "tags": ["atob", "base64", "WindowBase64", "btoa"], + "authors": ["Christian Ulbrich"], + "notes": [{ + "name": "WindowBase64", + "href": "https://www.w3.org/TR/html5/webappapis.html#windowbase64" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob" + }], + "polyfills": ["base64js"] +} +!*/ +/*! +{ + "name": "Framed window", + "property": "framed", + "tags": ["window"], + "builderAliases": ["window_framed"] +} +!*/ +/*! +{ + "name": "matchMedia", + "property": "matchmedia", + "caniuse": "matchmedia", + "tags": ["matchmedia"], + "authors": ["Alberto Elias"], + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Window.matchMedia" + }], + "polyfills": ["matchmediajs"] +} +!*/ +/*! +{ + "name": "PushManager", + "property": "pushmanager", + "caniuse": "mdn-api_pushmanager", + "authors": ["Dawid Kulpa (@dawidkulpa)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PushManager" + }] +} +!*/ +/*! +{ + "name": "ResizeObserver", + "property": "resizeobserver", + "caniuse": "resizeobserver", + "tags": ["ResizeObserver"], + "authors": ["Christian Andersson"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/resize-observer/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver" + }, { + "name": "Web.dev Article", + "href": "https://web.dev/resize-observer/" + }, { + "name": "Digital Ocean tutorial", + "href": "https://www.digitalocean.com/community/tutorials/js-resize-observer" + }] +} +!*/ +/*! +{ + "name": "worker type option test", + "property": "workertypeoption", + "caniuse":"mdn-api_worker_worker_ecmascript_modules", + "tags": ["web worker type options", "web worker"], + "builderAliases": ["worker_type_options"], + "authors": ["Debadutta Panda"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker" + }] +} +!*/ +/*! +{ + "name": "Workers from Blob URIs", + "property": "blobworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_blobworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }], + "warnings": ["This test may output garbage to console."], + "authors": ["Jussi Kalliokoski"], + "async": true +} +!*/ +/*! +{ + "name": "Workers from Data URIs", + "property": "dataworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_dataworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }], + "warnings": ["This test may output garbage to console."], + "authors": ["Jussi Kalliokoski"], + "async": true +} +!*/ +/*! +{ + "name": "Shared Workers", + "property": "sharedworkers", + "caniuse": "sharedworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_sharedworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }] +} +!*/ +/*! +{ + "name": "Web Workers", + "property": "webworkers", + "caniuse": "webworkers", + "tags": ["performance", "workers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/workers/basics/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" + }], + "polyfills": ["fakeworker", "html5shims"] +} +!*/ +/*! +{ + "name": "Transferables Objects", + "property": "transferables", + "tags": ["performance", "workers"], + "builderAliases": ["transferables"], + "notes": [{ + "name": "Transferable Objects: Lightning Fast!", + "href": "https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast" + }], + "async": true +} +!*/ +/*! +{ + "name": "XDomainRequest", + "property": "xdomainrequest", + "tags": ["cors", "xdomainrequest", "ie9", "ie8"], + "authors": ["Ivan Pan (@hypotenuse)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/XDomainRequest" + }] +} +!*/ diff --git a/tests/assets/modernizr/roll.sh b/tests/assets/modernizr/roll.sh new file mode 100644 index 0000000000..8e6dd5c651 --- /dev/null +++ b/tests/assets/modernizr/roll.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +MODERNIZR_VERSION="44fa7b07c367a1814e8699e3a2f15c53fbe32df7" + +cd "$(dirname "$0")" + +rm -rf Modernizr +git clone https://github.com/Modernizr/Modernizr +cd Modernizr +git checkout $MODERNIZR_VERSION +npm ci + +# Modernizr minifier is not working, hence we minify with ESBuild. +./bin/modernizr --config lib/config-all.json +npx esbuild --bundle modernizr.js --minify --outfile=../modernizr.js + +cd .. +rm -rf Modernizr diff --git a/tests/assets/modernizr/safari-14-1.json b/tests/assets/modernizr/safari-18.json similarity index 65% rename from tests/assets/modernizr/safari-14-1.json rename to tests/assets/modernizr/safari-18.json index 5d39648297..7b901a600c 100644 --- a/tests/assets/modernizr/safari-14-1.json +++ b/tests/assets/modernizr/safari-18.json @@ -11,6 +11,249 @@ "required": true, "step": true }, + "adownload": true, + "aping": true, + "areaping": true, + "ambientlight": false, + "applicationcache": false, + "audio": { + "ogg": "", + "mp3": "probably", + "opus": "probably", + "wav": "probably", + "m4a": "maybe" + }, + "audioloop": true, + "webaudio": true, + "batteryapi": false, + "battery-api": false, + "lowbattery": false, + "blobconstructor": true, + "blob-constructor": true, + "broadcastchannel": true, + "canvas": true, + "canvasblending": true, + "todataurljpeg": true, + "todataurlpng": true, + "todataurlwebp": false, + "canvaswinding": true, + "canvastext": true, + "clipboard": { + "read": true, + "readtext": true, + "write": true, + "writetext": true + }, + "contenteditable": true, + "contextmenu": false, + "cors": true, + "crypto": true, + "getrandomvalues": true, + "cssall": true, + "cssanimations": true, + "appearance": true, + "aspectratio": true, + "backdropfilter": true, + "backgroundblendmode": true, + "backgroundcliptext": true, + "bgpositionshorthand": true, + "bgpositionxy": true, + "bgrepeatround": true, + "bgrepeatspace": true, + "backgroundsize": true, + "bgsizecover": true, + "borderimage": true, + "borderradius": true, + "boxdecorationbreak": true, + "boxshadow": true, + "boxsizing": true, + "csscalc": true, + "checked": true, + "csschunit": true, + "csscolumns": { + "width": true, + "span": true, + "fill": true, + "gap": true, + "rule": true, + "rulecolor": true, + "rulestyle": true, + "rulewidth": true, + "breakbefore": true, + "breakafter": true, + "breakinside": true + }, + "cssgridlegacy": false, + "cssgrid": true, + "cubicbezierrange": true, + "customproperties": true, + "displayrunin": false, + "display-runin": false, + "displaytable": true, + "display-table": true, + "ellipsis": true, + "cssescape": true, + "cssexunit": true, + "supports": true, + "cssfilters": true, + "flexbox": true, + "flexboxlegacy": true, + "flexboxtweener": false, + "flexgap": true, + "flexwrap": true, + "focusvisible": true, + "focuswithin": true, + "fontdisplay": true, + "fontface": true, + "generatedcontent": true, + "cssgradients": true, + "hairline": true, + "hsla": true, + "cssinvalid": true, + "lastchild": true, + "cssmask": true, + "mediaqueries": true, + "multiplebgs": true, + "nthchild": true, + "objectfit": true, + "object-fit": true, + "opacity": true, + "overflowscrolling": false, + "csspointerevents": true, + "csspositionsticky": true, + "csspseudoanimations": true, + "csstransitions": true, + "csspseudotransitions": true, + "cssreflections": true, + "regions": false, + "cssremunit": true, + "cssresize": true, + "rgba": true, + "cssscrollbar": true, + "scrollsnappoints": true, + "shapes": true, + "siblinggeneral": true, + "subpixelfont": true, + "target": true, + "textalignlast": true, + "textdecoration": { + "line": true, + "style": true, + "color": true, + "skip": true, + "skipink": true + }, + "textshadow": true, + "csstransforms": true, + "csstransforms3d": true, + "csstransformslevel2": true, + "preserve3d": true, + "userselect": true, + "cssvalid": true, + "variablefonts": true, + "cssvhunit": true, + "cssvmaxunit": true, + "cssvminunit": true, + "cssvwunit": true, + "willchange": true, + "wrapflow": false, + "customelements": true, + "customprotocolhandler": false, + "dart": false, + "dataview": true, + "classlist": true, + "createelementattrs": false, + "createelement-attrs": false, + "dataset": true, + "documentfragment": true, + "hidden": true, + "intersectionobserver": true, + "microdata": false, + "mutationobserver": true, + "passiveeventlisteners": true, + "shadowroot": true, + "shadowrootlegacy": false, + "bdi": true, + "details": true, + "outputelem": true, + "picture": true, + "progressbar": true, + "meter": true, + "ruby": true, + "template": true, + "time": false, + "texttrackapi": true, + "track": true, + "unknownelements": true, + "emoji": true, + "es5array": true, + "es5date": true, + "es5function": true, + "es5object": true, + "strictmode": true, + "es5string": true, + "json": true, + "es5syntax": true, + "es5undefined": true, + "es5": true, + "es6array": true, + "arrow": true, + "es6class": true, + "es6collections": true, + "generators": true, + "es6math": true, + "es6number": true, + "es6object": true, + "promises": true, + "restparameters": true, + "spreadarray": true, + "stringtemplate": true, + "es6string": true, + "es6symbol": true, + "es7array": true, + "restdestructuringarray": true, + "restdestructuringobject": true, + "spreadobject": true, + "es8object": true, + "customevent": true, + "devicemotion": true, + "deviceorientation": true, + "eventlistener": true, + "forcetouch": false, + "hashchange": true, + "oninput": true, + "pointerevents": true, + "proximity": false, + "filereader": true, + "filesystem": false, + "flash": false, + "fullscreen": true, + "gamepads": true, + "geolocation": true, + "hiddenscroll": false, + "history": true, + "htmlimports": false, + "ie8compat": false, + "sandbox": true, + "seamless": false, + "srcdoc": true, + "imgcrossorigin": true, + "lazyloading": true, + "sizes": true, + "srcset": true, + "capture": false, + "fileinput": true, + "fileinputdirectory": true, + "inputformaction": true, + "input-formaction": true, + "formattribute": true, + "inputformenctype": true, + "input-formenctype": true, + "inputformmethod": true, + "inputformnovalidate": true, + "input-formnovalidate": true, + "inputformtarget": true, + "input-formtarget": true, "inputtypes": { "search": true, "tel": true, @@ -26,278 +269,148 @@ "range": true, "color": true }, - "htmlimports": false, - "history": true, - "ie8compat": false, - "applicationcache": false, - "blobconstructor": true, - "blob-constructor": true, - "cookies": true, - "cors": true, - "customelements": true, - "customprotocolhandler": false, - "customevent": true, - "dataview": true, - "eventlistener": true, - "geolocation": true, - "json": true, + "formvalidation": true, + "localizednumber": false, + "inputsearchevent": false, + "placeholder": true, + "requestautocomplete": false, + "intl": true, + "ligatures": true, + "olreversed": true, + "mathml": true, + "mediasource": true, + "hovermq": true, + "pointermq": true, "messagechannel": true, - "notification": true, - "postmessage": true, - "queryselector": true, - "serviceworker": true, - "svg": true, - "templatestrings": true, - "typedarrays": true, - "websockets": true, - "xdomainrequest": false, - "webaudio": true, - "cssescape": true, - "focuswithin": true, - "supports": true, - "target": true, - "microdata": false, - "mutationobserver": true, - "passiveeventlisteners": true, - "picture": true, - "es5array": true, - "es5date": true, - "es5function": true, "beacon": true, + "effectivetype": false, "lowbandwidth": false, "eventsource": true, "fetch": true, - "xhrresponsetype": true, - "xhr2": true, - "speechsynthesis": true, - "localstorage": true, - "sessionstorage": true, - "websqldatabase": true, - "es5object": true, - "svgfilters": true, - "strictmode": true, - "es5string": true, - "es5syntax": true, - "es5undefined": true, - "es5": true, - "es6array": true, - "arrow": true, - "es6collections": true, - "generators": true, - "es6math": true, - "es6number": true, - "es6object": true, - "promises": true, - "es6string": true, - "devicemotion": false, - "devicemotion2": false, - "deviceorientation": false, - "deviceorientation2": false, - "deviceorientation3": false, - "filereader": true, - "urlparser": true, - "urlsearchparams": true, - "framed": false, - "webworkers": true, - "contextmenu": false, - "cssall": true, - "willchange": true, - "classlist": true, - "documentfragment": true, - "contains": false, - "audio": true, - "canvas": true, - "canvastext": true, - "contenteditable": true, - "emoji": true, - "olreversed": true, - "userdata": false, - "video": true, - "vml": false, - "webanimations": true, - "webgl": true, - "adownload": true, - "audioloop": true, - "canvasblending": true, - "todataurljpeg": true, - "todataurlpng": true, - "todataurlwebp": false, - "canvaswinding": true, - "bgpositionshorthand": true, - "multiplebgs": true, - "csspointerevents": true, - "cssremunit": true, - "rgba": true, - "preserve3d": true, - "createelementattrs": false, - "createelement-attrs": false, - "dataset": true, - "hidden": true, - "outputelem": true, - "progressbar": true, - "meter": true, - "ruby": true, - "template": true, - "srcset": true, - "time": false, - "texttrackapi": true, - "track": true, - "unknownelements": true, - "inputformaction": true, - "input-formaction": true, - "inputformenctype": true, - "input-formenctype": true, - "inputformmethod": true, - "inputformtarget": false, - "input-formtarget": false, - "scriptasync": true, - "scriptdefer": true, - "stylescoped": false, - "capture": false, - "fileinput": true, - "formattribute": true, - "placeholder": true, - "sandbox": true, - "inlinesvg": true, - "textareamaxlength": true, - "videocrossorigin": true, - "webglextensions": true, - "seamless": false, - "srcdoc": true, - "imgcrossorigin": true, - "hashchange": true, - "inputsearchevent": false, - "ambientlight": false, - "datalistelem": true, - "videoloop": true, - "csscalc": true, - "cubicbezierrange": true, - "cssgradients": true, - "opacity": true, - "csspositionsticky": true, - "csschunit": true, - "cssexunit": true, - "hsla": true, - "videopreload": true, - "getusermedia": true, - "websocketsbinary": true, - "atobbtoa": true, - "atob-btoa": true, - "sharedworkers": true, - "bdi": true, "xhrresponsetypearraybuffer": true, "xhrresponsetypeblob": true, "xhrresponsetypedocument": true, "xhrresponsetypejson": true, "xhrresponsetypetext": true, - "svgclippaths": true, - "svgforeignobject": true, - "smil": true, - "hiddenscroll": true, - "mathml": true, - "touchevents": false, - "unicoderange": true, - "unicode": true, - "checked": true, - "displaytable": true, - "display-table": true, - "fontface": true, - "generatedcontent": true, - "hairline": true, - "cssinvalid": true, - "lastchild": true, - "nthchild": true, - "cssscrollbar": true, - "siblinggeneral": true, - "subpixelfont": true, - "cssvalid": true, - "cssvhunit": true, - "cssvmaxunit": true, - "cssvminunit": true, - "cssvwunit": true, - "details": true, - "oninput": true, - "formvalidation": true, - "localizednumber": false, - "mediaqueries": true, - "flash": false, - "proximity": false, - "sizes": true, - "hovermq": true, - "pointermq": true, - "svgasimg": true, - "pointerevents": true, - "fileinputdirectory": true, - "textshadow": true, - "batteryapi": false, - "battery-api": false, - "crypto": true, - "dart": false, - "forcetouch": false, - "fullscreen": true, - "gamepads": true, - "intl": true, + "xhrresponsetype": true, + "xhr2": true, + "notification": true, "pagevisibility": true, "performance": true, "pointerlock": true, - "quotamanagement": false, + "postmessage": { + "structuredclones": true + }, + "proxy": true, + "queryselector": true, + "prefetch": false, "requestanimationframe": true, "raf": true, - "vibrate": false, - "webintents": false, - "lowbattery": false, - "getrandomvalues": true, - "backgroundblendmode": true, - "objectfit": true, - "object-fit": true, - "regions": false, - "wrapflow": false, + "scriptasync": true, + "scriptdefer": true, + "scrolltooptions": false, + "serviceworker": true, "speechrecognition": true, - "filesystem": false, - "requestautocomplete": false, + "speechsynthesis": true, + "cookies": true, + "localstorage": true, + "quotamanagement": false, + "sessionstorage": true, + "userdata": false, + "websqldatabase": true, + "stylescoped": false, + "svg": true, + "svgasimg": true, + "svgclippaths": true, + "svgfilters": true, + "svgforeignobject": true, + "inlinesvg": true, + "smil": true, + "textareamaxlength": true, + "textencoder": true, + "textdecoder": true, + "typedarrays": true, + "unicoderange": true, "bloburls": true, - "transferables": true, + "urlparser": true, + "urlsearchparams": true, + "vibrate": false, + "video": { + "ogg": "", + "h264": "probably", + "h265": "", + "webm": "probably", + "vp9": "probably", + "hls": "probably", + "av1": "" + }, + "videocrossorigin": true, + "videoloop": true, + "videopreload": true, + "vml": false, + "webintents": false, + "webanimations": true, + "publickeycredential": true, + "webgl": true, + "webglextensions": { + "ANGLE_instanced_arrays": true, + "EXT_blend_minmax": true, + "EXT_clip_control": true, + "EXT_color_buffer_half_float": true, + "EXT_depth_clamp": true, + "EXT_float_blend": true, + "EXT_frag_depth": true, + "EXT_polygon_offset_clamp": true, + "EXT_shader_texture_lod": true, + "EXT_texture_compression_bptc": true, + "EXT_texture_compression_rgtc": true, + "EXT_texture_filter_anisotropic": true, + "EXT_texture_mirror_clamp_to_edge": true, + "EXT_sRGB": true, + "KHR_parallel_shader_compile": true, + "OES_element_index_uint": true, + "OES_fbo_render_mipmap": true, + "OES_standard_derivatives": true, + "OES_texture_float": true, + "OES_texture_float_linear": true, + "OES_texture_half_float": true, + "OES_texture_half_float_linear": true, + "OES_vertex_array_object": true, + "WEBGL_blend_func_extended": true, + "WEBGL_color_buffer_float": true, + "WEBGL_compressed_texture_astc": true, + "WEBGL_compressed_texture_etc": true, + "WEBGL_compressed_texture_etc1": true, + "WEBGL_compressed_texture_pvrtc": true, + "WEBKIT_WEBGL_compressed_texture_pvrtc": true, + "WEBGL_compressed_texture_s3tc": true, + "WEBGL_compressed_texture_s3tc_srgb": true, + "WEBGL_debug_renderer_info": true, + "WEBGL_debug_shaders": true, + "WEBGL_depth_texture": true, + "WEBGL_draw_buffers": true, + "WEBGL_lose_context": true, + "WEBGL_multi_draw": true, + "WEBGL_polygon_mode": true + }, "peerconnection": true, - "datachannel": false, + "datachannel": true, + "getusermedia": true, + "mediastream": true, + "websockets": true, + "websocketsbinary": true, + "atobbtoa": true, + "atob-btoa": true, + "framed": false, "matchmedia": true, - "ligatures": true, - "cssanimations": true, - "csspseudoanimations": true, - "appearance": true, - "backdropfilter": true, - "backgroundcliptext": true, - "bgpositionxy": true, - "bgrepeatround": true, - "bgrepeatspace": true, - "backgroundsize": true, - "bgsizecover": true, - "borderimage": true, - "borderradius": true, - "boxshadow": true, - "boxsizing": true, - "csscolumns": true, - "cssgridlegacy": false, - "cssgrid": true, - "displayrunin": false, - "display-runin": false, - "ellipsis": true, - "cssfilters": true, - "flexbox": true, - "flexboxlegacy": true, - "flexboxtweener": false, - "flexwrap": true, - "cssmask": true, - "overflowscrolling": false, - "cssreflections": true, - "cssresize": true, - "scrollsnappoints": true, - "shapes": true, - "textalignlast": true, - "csstransforms": true, - "csstransforms3d": true, - "csstransformslevel2": true, - "csstransitions": true, - "csspseudotransitions": true, - "userselect": true, - "variablefonts": true -} + "pushmanager": true, + "resizeobserver": true, + "workertypeoption": true, + "sharedworkers": true, + "webworkers": true, + "transferables": true, + "xdomainrequest": false, + "devicemotion2": true, + "deviceorientation2": false, + "deviceorientation3": true +} \ No newline at end of file diff --git a/tests/library/browsercontext-viewport-mobile.spec.ts b/tests/library/browsercontext-viewport-mobile.spec.ts index 1f897f60fa..2e0f90ceb0 100644 --- a/tests/library/browsercontext-viewport-mobile.spec.ts +++ b/tests/library/browsercontext-viewport-mobile.spec.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import os from 'os'; import { browserTest as it, expect } from '../config/browserTest'; it.describe('mobile viewport', () => { @@ -55,23 +54,19 @@ it.describe('mobile viewport', () => { } }); - it('should be detectable by Modernizr', async ({ playwright, browser, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); + it('should be detectable', async ({ playwright, browser, server, browserName, platform }) => { const iPhone = playwright.devices['iPhone 6']; const context = await browser.newContext({ ...iPhone }); const page = await context.newPage(); - await page.goto(server.PREFIX + '/detect-touch.html'); - expect(await page.evaluate(() => document.body.textContent!.trim())).toBe('YES'); + expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true); await context.close(); }); it('should detect touch when applying viewport with touches', async ({ browser, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true }); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); - await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' }); - expect(await page.evaluate(() => (window as any)['Modernizr'].touchevents)).toBe(true); + expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true); await context.close(); }); diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index 4ba1e2f52d..abb14b3a71 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -94,11 +94,8 @@ it('should emulate availWidth and availHeight', async ({ page }) => { }); it('should not have touch by default', async ({ page, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); - await page.goto(server.PREFIX + '/detect-touch.html'); - expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO'); }); it('should throw on tap if hasTouch is not enabled', async ({ page }) => { diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index 9db5b24aa5..df87081343 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -21,7 +21,7 @@ import os from 'os'; async function checkFeatures(name: string, context: any, server: any) { try { const page = await context.newPage(); - await page.goto(server.PREFIX + '/modernizr.html'); + await page.goto(server.PREFIX + '/modernizr/index.html'); const actual = await page.evaluate('window.report'); const expected = JSON.parse(fs.readFileSync(require.resolve(`../assets/modernizr/${name}.json`), 'utf-8')); return { actual, expected }; @@ -30,28 +30,39 @@ async function checkFeatures(name: string, context: any, server: any) { } } -it('safari-14-1', async ({ browser, browserName, platform, server, headless, isMac }) => { +it('Safari Desktop', async ({ browser, browserName, platform, server, headless, isMac }) => { it.skip(browserName !== 'webkit'); it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const context = await browser.newContext({ deviceScaleFactor: 2 }); - const { actual, expected } = await checkFeatures('safari-14-1', context, server); + const { actual, expected } = await checkFeatures('safari-18', context, server); + + expected.pushmanager = false; + expected.hiddenscroll = true; + expected.devicemotion2 = false; + expected.devicemotion = false; + expected.deviceorientation = false; + expected.deviceorientation3 = false; + + delete expected.webglextensions; + delete actual.webglextensions; + expected.audio = !!expected.audio; + actual.audio = !!actual.audio; + expected.video = !!expected.video; + actual.video = !!actual.video; if (platform === 'linux') { expected.subpixelfont = false; expected.speechrecognition = false; + expected.publickeycredential = false; + expected.mediastream = false; if (headless) expected.todataurljpeg = false; // GHA delete actual.variablefonts; delete expected.variablefonts; - - if (isDocker()) { - delete actual.unicode; - delete expected.unicode; - } } if (platform === 'win32') { @@ -61,30 +72,34 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM expected.speechrecognition = false; expected.speechsynthesis = false; expected.todataurljpeg = false; - expected.unicode = false; expected.webaudio = false; expected.gamepads = false; expected.input.list = false; + delete expected.datalistelem; + + expected.publickeycredential = false; + expected.mediastream = false; + expected.mediasource = false; + expected.datachannel = false; + expected.inputtypes.color = false; + expected.inputtypes.month = false; + expected.inputtypes.week = false; expected.inputtypes.date = false; expected.inputtypes['datetime-local'] = false; expected.inputtypes.time = false; } - if (isMac && parseInt(os.release(), 10) > 20) - expected.applicationcache = false; - expect(actual).toEqual(expected); }); -it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => { +it('Mobile Safari', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => { it.skip(browserName !== 'webkit'); - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen.'); it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const iPhone = playwright.devices['iPhone 12']; const context = await browser.newContext(iPhone); - const { actual, expected } = await checkFeatures('mobile-safari-14-1', context, server); + const { actual, expected } = await checkFeatures('mobile-safari-18', context, server); { // All platforms. @@ -93,22 +108,28 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.cssvhunit = true; expected.cssvmaxunit = true; expected.overflowscrolling = false; + expected.mediasource = true; + expected.scrolltooptions = false; + + delete expected.webglextensions; + delete actual.webglextensions; + expected.audio = !!expected.audio; + actual.audio = !!actual.audio; + expected.video = !!expected.video; + actual.video = !!actual.video; } if (platform === 'linux') { expected.subpixelfont = false; expected.speechrecognition = false; + expected.publickeycredential = false; + expected.mediastream = false; if (headless) expected.todataurljpeg = false; // GHA delete actual.variablefonts; delete expected.variablefonts; - - if (isDocker()) { - delete actual.unicode; - delete expected.unicode; - } } if (platform === 'win32') { @@ -118,32 +139,25 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.speechrecognition = false; expected.speechsynthesis = false; expected.todataurljpeg = false; - expected.unicode = false; expected.webaudio = false; expected.gamepads = false; expected.input.list = false; + + delete expected.datalistelem; + + expected.publickeycredential = false; + expected.mediastream = false; + expected.mediasource = false; + expected.datachannel = false; + expected.inputtypes.color = false; expected.inputtypes.month = false; expected.inputtypes.week = false; expected.inputtypes.date = false; - expected.inputtypes.time = false; expected.inputtypes['datetime-local'] = false; expected.inputtypes.time = false; } expect(actual).toEqual(expected); }); - -function isDocker() { - try { - fs.statSync('/.dockerenv'); - return true; - } catch { - } - try { - return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker'); - } catch { - } - return false; -} From 8703dd4f062c3f2f2aaec6158cf7fb9d1008b7c6 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 23 Aug 2024 08:57:18 -0700 Subject: [PATCH 03/47] feat(webkit): roll to r2063 (#32295) --- packages/playwright-core/browsers.json | 2 +- tests/page/page-goto.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 576b81035d..579f1ae013 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2062", + "revision": "2063", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 015fa0e4ef..944b7de87b 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -366,7 +366,7 @@ it('should fail when main resources failed to load', async ({ page, browserName, } else if (browserName === 'webkit' && isWindows && mode === 'service2') { expect(error.message).toContain(`proxy handshake error`); } else if (browserName === 'webkit' && isWindows) { - expect(error.message).toContain(`Couldn\'t connect to server`); + expect(error.message).toContain(`Could not connect to server`); } else if (browserName === 'webkit') { if (mode === 'service2') expect(error.message).toContain('Connection refused'); From 4edc076935e7103f7a908c17bb0723ee712221d0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 23 Aug 2024 10:19:36 -0700 Subject: [PATCH 04/47] chore: load env from playwright.env when running codegen (#32280) --- .gitignore | 3 +- package-lock.json | 10 +++--- package.json | 2 +- .../playwright-core/ThirdPartyNotices.txt | 31 ++++++++++++++++++- .../bundles/utils/package-lock.json | 17 ++++++++++ .../bundles/utils/package.json | 1 + .../bundles/utils/src/utilsBundleImpl.ts | 3 ++ packages/playwright-core/src/cli/program.ts | 3 +- packages/playwright-core/src/utilsBundle.ts | 1 + 9 files changed, 62 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 69d85e4975..aadc481067 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ test-results /tests/installation/output/ /tests/installation/.registry.json .cache/ -.eslintcache \ No newline at end of file +.eslintcache +playwright.env diff --git a/package-lock.json b/package-lock.json index 1c48c213d7..eeae134a6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.18.11", "eslint": "^8.55.0", @@ -3293,15 +3293,15 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/electron": { diff --git a/package.json b/package.json index 930fac8a80..6b2e043765 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.18.11", "eslint": "^8.55.0", diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 3c5a71e20f..0a3ca6a5f4 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina - concat-map@0.0.1 (https://github.com/substack/node-concat-map) - debug@4.3.4 (https://github.com/debug-js/debug) - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) +- dotenv@16.4.5 (https://github.com/motdotla/dotenv) - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - extract-zip@2.0.1 (https://github.com/maxogden/extract-zip) @@ -472,6 +473,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF define-lazy-prop@2.0.0 AND INFORMATION +%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dotenv@16.4.5 AND INFORMATION + %% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 45 +Total Packages: 46 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 66c4cdae12..eef68ef8ee 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -11,6 +11,7 @@ "colors": "1.4.0", "commander": "8.3.0", "debug": "^4.3.4", + "dotenv": "^16.4.5", "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", @@ -198,6 +199,17 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -560,6 +572,11 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 8ac0c112fe..a7c66192e0 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -12,6 +12,7 @@ "colors": "1.4.0", "commander": "8.3.0", "debug": "^4.3.4", + "dotenv": "^16.4.5", "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index 49dc61a05c..dcb3790629 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -20,6 +20,9 @@ export const colors = colorsLibrary; import debugLibrary from 'debug'; export const debug = debugLibrary; +import dotenvLibrary from 'dotenv'; +export const dotenv = dotenvLibrary; + export { getProxyForUrl } from 'proxy-from-env'; export { HttpsProxyAgent } from 'https-proxy-agent'; diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index ad943c049e..28cf15fddb 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import type { Command } from '../utilsBundle'; -import { program } from '../utilsBundle'; +import { program, dotenv } from '../utilsBundle'; export { program } from '../utilsBundle'; import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; @@ -561,6 +561,7 @@ async function open(options: Options, url: string | undefined, language: string) async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH); + dotenv.config({ path: 'playwright.env' }); await context._enableRecorder({ language, launchOptions, diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index bb037a1ca7..a2a62be867 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -19,6 +19,7 @@ import path from 'path'; export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors; export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug; +export const dotenv: typeof import('../bundles/utils/node_modules/dotenv') = require('./utilsBundleImpl').dotenv; export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl; export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent; export const jpegjs: typeof import('../bundles/utils/node_modules/jpeg-js') = require('./utilsBundleImpl').jpegjs; From 37eb66df10d962390cd4d5a74844c244702b591a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 23 Aug 2024 10:19:44 -0700 Subject: [PATCH 05/47] chore: extract performAction in recorder (#32279) --- .../src/server/injected/recorder/recorder.ts | 8 +- .../playwright-core/src/server/recorder.ts | 156 ++++++++++++------ .../src/server/recorder/recorderActions.ts | 1 + .../playwright-core/src/utils/expectUtils.ts | 29 ++++ packages/playwright-core/src/utils/index.ts | 1 + packages/playwright/src/matchers/matchers.ts | 36 ++-- .../playwright/src/matchers/toMatchText.ts | 13 -- 7 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 packages/playwright-core/src/utils/expectUtils.ts diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 3432f159dd..6e573c3c5a 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -23,7 +23,7 @@ import type { Highlight, HighlightOptions } from '../highlight'; import clipPaths from './clipPaths'; interface RecorderDelegate { - performAction?(action: actions.Action): Promise; + performAction?(action: actions.PerformOnRecordAction): Promise; recordAction?(action: actions.Action): Promise; setSelector?(selector: string): Promise; setMode?(mode: Mode): Promise; @@ -483,7 +483,7 @@ class RecordActionTool implements RecorderTool { return true; } - private async _performAction(action: actions.Action) { + private async _performAction(action: actions.PerformOnRecordAction) { this._hoveredElement = null; this._hoveredModel = null; this._activeModel = null; @@ -1361,7 +1361,7 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson): } interface Embedder { - __pw_recorderPerformAction(action: actions.Action): Promise; + __pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise; __pw_recorderRecordAction(action: actions.Action): Promise; __pw_recorderState(): Promise; __pw_recorderSetSelector(selector: string): Promise; @@ -1407,7 +1407,7 @@ export class PollingRecorder implements RecorderDelegate { this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); } - async performAction(action: actions.Action) { + async performAction(action: actions.PerformOnRecordAction) { await this._embedder.__pw_recorderPerformAction(action); } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 93f706581f..9e2174303b 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -36,7 +36,7 @@ import { RecorderApp } from './recorder/recorderApp'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; -import { createGuid, isUnderTest, monotonicTime } from '../utils'; +import { createGuid, isUnderTest, monotonicTime, serializeExpectedTextValues } from '../utils'; import { metadataToCallLog } from './recorder/recorderUtils'; import { Debugger } from './debugger'; import { EventEmitter } from 'events'; @@ -470,7 +470,7 @@ class ContextRecorder extends EventEmitter { // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. await this._context.exposeBinding('__pw_recorderPerformAction', false, - (source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)); + (source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(source.frame, action)); // Other non-essential actions are simply being recorded. await this._context.exposeBinding('__pw_recorderRecordAction', false, @@ -585,7 +585,7 @@ class ContextRecorder extends EventEmitter { return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; } - private async _performAction(frame: Frame, action: actions.Action) { + private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) { // Commit last action so that no further signals are added to it. this._generator.commitLastAction(); @@ -595,56 +595,13 @@ class ContextRecorder extends EventEmitter { action }; - const perform = async (action: string, params: any, cb: (callMetadata: CallMetadata) => Promise) => { - const callMetadata: CallMetadata = { - id: `call@${createGuid()}`, - apiName: 'frame.' + action, - objectId: frame.guid, - pageId: frame._page.guid, - frameId: frame.guid, - startTime: monotonicTime(), - endTime: 0, - type: 'Frame', - method: action, - params, - log: [], - }; - this._generator.willPerformAction(actionInContext); - - try { - await frame.instrumentation.onBeforeCall(frame, callMetadata); - await cb(callMetadata); - } catch (e) { - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - this._generator.performedActionFailed(actionInContext); - return; - } - - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - - this._setCommittedAfterTimeout(actionInContext); + this._generator.willPerformAction(actionInContext); + const success = await performAction(frame, action); + if (success) { this._generator.didPerformAction(actionInContext); - }; - - const kActionTimeout = 5000; - if (action.name === 'click') { - const { options } = toClickOptions(action); - await perform('click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); - } - if (action.name === 'press') { - const modifiers = toModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); - } - if (action.name === 'check') - await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'uncheck') - await perform('uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'select') { - const values = action.options.map(value => ({ value })); - await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); + this._setCommittedAfterTimeout(actionInContext); + } else { + this._generator.performedActionFailed(actionInContext); } } @@ -749,3 +706,98 @@ async function findFrameSelector(frame: Frame): Promise { } catch (e) { } } + +async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { + const callMetadata: CallMetadata = { + id: `call@${createGuid()}`, + apiName: 'frame.' + action, + objectId: frame.guid, + pageId: frame._page.guid, + frameId: frame.guid, + startTime: monotonicTime(), + endTime: 0, + type: 'Frame', + method: action, + params, + log: [], + }; + + try { + await frame.instrumentation.onBeforeCall(frame, callMetadata); + await cb(callMetadata); + } catch (e) { + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return false; + } + + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return true; +} + +async function performAction(frame: Frame, action: actions.Action): Promise { + const kActionTimeout = 5000; + if (action.name === 'click') { + const { options } = toClickOptions(action); + return await innerPerformAction(frame, 'click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); + } + if (action.name === 'press') { + const modifiers = toModifiers(action.modifiers); + const shortcut = [...modifiers, action.key].join('+'); + return await innerPerformAction(frame, 'press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'fill') + return await innerPerformAction(frame, 'fill', { selector: action.selector, text: action.text }, callMetadata => frame.fill(callMetadata, action.selector, action.text, { timeout: kActionTimeout, strict: true })); + if (action.name === 'setInputFiles') + return await innerPerformAction(frame, 'setInputFiles', { selector: action.selector, files: action.files }, callMetadata => frame.setInputFiles(callMetadata, action.selector, { selector: action.selector, payloads: [], timeout: kActionTimeout, strict: true })); + if (action.name === 'check') + return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'uncheck') + return await innerPerformAction(frame, 'uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'select') { + const values = action.options.map(value => ({ value })); + return await innerPerformAction(frame, 'selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'navigate') + return await innerPerformAction(frame, 'goto', { url: action.url }, callMetadata => frame.goto(callMetadata, action.url, { timeout: kActionTimeout })); + if (action.name === 'closePage') + return await innerPerformAction(frame, 'close', {}, callMetadata => frame._page.close(callMetadata)); + if (action.name === 'openPage') + throw Error('Not reached'); + if (action.name === 'assertChecked') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.be.checked', + isNot: !action.checked, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertText') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.have.text', + expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertValue') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.have.value', + expectedValue: action.value, + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertVisible') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.be.visible', + isNot: false, + timeout: kActionTimeout, + })); + } + throw new Error('Internal error: unexpected action ' + (action as any).name); +} diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 3c9720cbc4..295758aaeb 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -121,6 +121,7 @@ export type AssertVisibleAction = ActionBase & { export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction; +export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; // Signals. diff --git a/packages/playwright-core/src/utils/expectUtils.ts b/packages/playwright-core/src/utils/expectUtils.ts new file mode 100644 index 0000000000..0ae21e8602 --- /dev/null +++ b/packages/playwright-core/src/utils/expectUtils.ts @@ -0,0 +1,29 @@ +/** + * 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 { ExpectedTextValue } from '@protocol/channels'; +import { isRegExp, isString } from './rtti'; + +export function serializeExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { + return items.map(i => ({ + string: isString(i) ? i : undefined, + regexSource: isRegExp(i) ? i.source : undefined, + regexFlags: isRegExp(i) ? i.flags : undefined, + matchSubstring: options.matchSubstring, + ignoreCase: options.ignoreCase, + normalizeWhiteSpace: options.normalizeWhiteSpace, + })); +} diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index 372922ec59..0bc7a75b08 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -21,6 +21,7 @@ export * from './debug'; export * from './debugLogger'; export * from './env'; export * from './eventsHelper'; +export * from './expectUtils'; export * from './fileUtils'; export * from './headers'; export * from './hostPlatform'; diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 08ae8b6385..3ca9180ae2 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -20,8 +20,8 @@ import { colors } from 'playwright-core/lib/utilsBundle'; import { expectTypes, callLogText } from '../util'; import { toBeTruthy } from './toBeTruthy'; import { toEqual } from './toEqual'; -import { toExpectedTextValues, toMatchText } from './toMatchText'; -import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils'; +import { toMatchText } from './toMatchText'; +import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; import { currentTestInfo } from '../common/globals'; import { TestInfoImpl } from '../worker/testInfo'; import type { ExpectMatcherState } from '../../types/test'; @@ -163,12 +163,12 @@ export function toContainText( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, { ...options, contains: true }); } else { return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, options); } @@ -181,7 +181,7 @@ export function toHaveAccessibleDescription( options?: { timeout?: number, ignoreCase?: boolean }, ) { return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout }); }, expected, options); } @@ -193,7 +193,7 @@ export function toHaveAccessibleName( options?: { timeout?: number, ignoreCase?: boolean }, ) { return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); }, expected, options); } @@ -218,7 +218,7 @@ export function toHaveAttribute( }, options); } return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); }, expected as (string | RegExp), options); } @@ -231,12 +231,12 @@ export function toHaveClass( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected); + const expectedText = serializeExpectedTextValues(expected); return await locator._expect('to.have.class.array', { expectedText, isNot, timeout }); }, expected, options); } else { return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.class', { expectedText, isNot, timeout }); }, expected, options); } @@ -261,7 +261,7 @@ export function toHaveCSS( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); }, expected, options); } @@ -273,7 +273,7 @@ export function toHaveId( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.id', { expectedText, isNot, timeout }); }, expected, options); } @@ -299,7 +299,7 @@ export function toHaveRole( if (!isString(expected)) throw new Error(`"role" argument in toHaveRole must be a string`); return toMatchText.call(this, 'toHaveRole', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.role', { expectedText, isNot, timeout }); }, expected, options); } @@ -312,12 +312,12 @@ export function toHaveText( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } else { return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } @@ -330,7 +330,7 @@ export function toHaveValue( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.value', { expectedText, isNot, timeout }); }, expected, options); } @@ -342,7 +342,7 @@ export function toHaveValues( options?: { timeout?: number }, ) { return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected); + const expectedText = serializeExpectedTextValues(expected); return await locator._expect('to.have.values', { expectedText, isNot, timeout }); }, expected, options); } @@ -355,7 +355,7 @@ export function toHaveTitle( ) { const locator = page.locator(':root') as LocatorEx; return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true }); + const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true }); return await locator._expect('to.have.title', { expectedText, isNot, timeout }); }, expected, options); } @@ -370,7 +370,7 @@ export function toHaveURL( expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected; const locator = page.locator(':root') as LocatorEx; return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.url', { expectedText, isNot, timeout }); }, expected, options); } diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 790b402d2f..ebac8f8028 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -15,8 +15,6 @@ */ -import type { ExpectedTextValue } from '@protocol/channels'; -import { isRegExp, isString } from 'playwright-core/lib/utils'; import { expectTypes, callLogText } from '../util'; import { printReceivedStringContainExpectedResult, @@ -95,14 +93,3 @@ export async function toMatchText( timeout: timedOut ? timeout : undefined, }; } - -export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { - return items.map(i => ({ - string: isString(i) ? i : undefined, - regexSource: isRegExp(i) ? i.source : undefined, - regexFlags: isRegExp(i) ? i.flags : undefined, - matchSubstring: options.matchSubstring, - ignoreCase: options.ignoreCase, - normalizeWhiteSpace: options.normalizeWhiteSpace, - })); -} From 9d86bc53366ade74299de492c1b3b6e230a1697c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 23 Aug 2024 14:33:37 -0700 Subject: [PATCH 06/47] fix(dupe): render dupe test error indicator (#32303) Fixes https://github.com/microsoft/playwright/issues/32093 --- packages/trace-viewer/src/ui/workbench.tsx | 10 ++++++- .../ui-mode-test-source.spec.ts | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 01ae6142bd..2d6b0bd8bd 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -83,7 +83,7 @@ export const Workbench: React.FunctionComponent<{ setRevealedStack(action?.stack); }, [setSelectedActionImpl, setRevealedStack]); - const sources = React.useMemo(() => model?.sources || new Map(), [model]); + const sources = React.useMemo(() => model?.sources || new Map(), [model]); React.useEffect(() => { setSelectedTime(undefined); @@ -179,9 +179,17 @@ export const Workbench: React.FunctionComponent<{ selectPropertiesTab('source'); }} /> }; + + // Fallback location w/o action stands for file / test. + // Render error count on Source tab for that case. + let fallbackSourceErrorCount: number | undefined = undefined; + if (!selectedAction && fallbackLocation) + fallbackSourceErrorCount = fallbackLocation.source?.errors.length; + const sourceTab: TabbedPaneTabModel = { id: 'source', title: 'Source', + errorCount: fallbackSourceErrorCount, render: () => { /Missing semicolon./ ]); }); + +test('should load error (dupe tests) indicator on sources', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('first', () => {}); + test('first', () => {}); + `, + }); + await expect.poll(dumpTestTree(page)).toBe(` + ▼ ◯ a.test.ts + ◯ first + `); + + await page.getByTestId('test-tree').getByText('a.test.ts').click(); + await expect(page.getByText('Source1')).toBeVisible(); + + await expect( + page.locator('.CodeMirror .source-line-running'), + ).toHaveText(`4 test('first', () => {});`); + + await expect( + page.locator('.CodeMirror-linewidget') + ).toHaveText([ + '                              ', + /Error: duplicate test title "first", first declared in a.test.ts:3/ + ]); +}); From abe6c04a54d6231f64201011bff7bc45b7d2e053 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 23 Aug 2024 14:50:43 -0700 Subject: [PATCH 07/47] chore: remove `noWaitAfter` from selectOption (#32283) This follows removing this option from other methods in v1.46. The two methods still supporting `noWaitAfter` are `click` and `press`. --- docs/src/api/class-elementhandle.md | 2 +- docs/src/api/class-frame.md | 2 +- docs/src/api/class-locator.md | 2 +- docs/src/api/class-page.md | 2 +- packages/playwright-core/src/client/types.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 2 -- packages/playwright-core/src/server/dom.ts | 4 ++-- packages/playwright-core/src/server/frames.ts | 2 +- packages/playwright-core/types/types.d.ts | 24 +++++++------------ packages/protocol/src/channels.ts | 4 ---- packages/protocol/src/protocol.yml | 2 -- 11 files changed, 16 insertions(+), 32 deletions(-) diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index 1793798c8c..c8f54c7380 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] { ### option: ElementHandle.selectOption.force = %%-input-force-%% * since: v1.13 -### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: ElementHandle.selectOption.timeout = %%-input-timeout-%% diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 4da22fe989..f3f308622f 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" }) ### option: Frame.selectOption.force = %%-input-force-%% * since: v1.13 -### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: Frame.selectOption.strict = %%-input-strict-%% diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 8a148ddf0b..4df0035098 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" }); ### option: Locator.selectOption.force = %%-input-force-%% * since: v1.14 -### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 ### option: Locator.selectOption.timeout = %%-input-timeout-%% diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 78d38a6f7a..834c06ab10 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3742,7 +3742,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" }); ### option: Page.selectOption.force = %%-input-force-%% * since: v1.13 -### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: Page.selectOption.strict = %%-input-strict-%% diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 37d374e3ec..2e7f7e4107 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number }; export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string }; -export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean }; +export type SelectOptionOptions = { force?: boolean, timeout?: number }; export type FilePayload = { name: string, mimeType: string, buffer: Buffer }; export type StorageState = { cookies: channels.NetworkCookie[], diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 81755c79bc..e0b4a4d3df 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1637,7 +1637,6 @@ scheme.FrameSelectOptionParams = tObject({ }))), force: tOptional(tBoolean), timeout: tOptional(tNumber), - noWaitAfter: tOptional(tBoolean), }); scheme.FrameSelectOptionResult = tObject({ values: tArray(tString), @@ -2001,7 +2000,6 @@ scheme.ElementHandleSelectOptionParams = tObject({ }))), force: tOptional(tBoolean), timeout: tOptional(tNumber), - noWaitAfter: tOptional(tBoolean), }); scheme.ElementHandleSelectOptionResult = tObject({ values: tArray(tString), diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 60aa899412..175d2a0f4b 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -536,7 +536,7 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' }); } - async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise { + async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._selectOption(progress, elements, values, options); @@ -544,7 +544,7 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise { + async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { let resultingOptions: string[] = []; await this._retryAction(progress, 'select option', async () => { await progress.beforeInputAction(this); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 931ba8ef73..3a60e796c4 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1344,7 +1344,7 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } - async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions = {}): Promise { + async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options)); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 9c125fef1e..378c5a7151 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -3897,10 +3897,8 @@ export interface Page { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -7023,10 +7021,8 @@ export interface Frame { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -11136,10 +11132,8 @@ export interface ElementHandle extends JSHandle { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -13331,10 +13325,8 @@ export interface Locator { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index f3e0a2c35a..cc3d07ba57 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2939,7 +2939,6 @@ export type FrameSelectOptionParams = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type FrameSelectOptionOptions = { strict?: boolean, @@ -2952,7 +2951,6 @@ export type FrameSelectOptionOptions = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type FrameSelectOptionResult = { values: string[], @@ -3555,7 +3553,6 @@ export type ElementHandleSelectOptionParams = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type ElementHandleSelectOptionOptions = { elements?: ElementHandleChannel[], @@ -3567,7 +3564,6 @@ export type ElementHandleSelectOptionOptions = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type ElementHandleSelectOptionResult = { values: string[], diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4c25212c57..c0a8d09795 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2185,7 +2185,6 @@ Frame: index: number? force: boolean? timeout: number? - noWaitAfter: boolean? returns: values: type: array @@ -2741,7 +2740,6 @@ ElementHandle: index: number? force: boolean? timeout: number? - noWaitAfter: boolean? returns: values: type: array From 54c487c9396d7368eee78b4f19678427e8045124 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sat, 24 Aug 2024 11:49:18 +0200 Subject: [PATCH 08/47] test: unskip 'should use ipv6 proxy' for Docker --- tests/library/browsercontext-proxy.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index c2d9d5b31c..466e866e04 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -141,7 +141,6 @@ it.describe('should proxy local network requests', () => { it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => { it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST'); - it.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default'); proxyServer.forwardTo(server.PORT); const context = await contextFactory({ proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` } From 9c81eab3291f18eb361d0f8e6a6b04211ed062d3 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:56:08 -0700 Subject: [PATCH 09/47] feat(webkit): roll to r2064 (#32319) --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 579f1ae013..dcd73ae402 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2063", + "revision": "2064", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 1511d8643ea7bd2e72c4b80ff6f9b3b126d91e0f Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 26 Aug 2024 09:39:25 +0200 Subject: [PATCH 10/47] fix(test runner): expect.poll error reporting should handle non-expect errors (#32257) Closes https://github.com/microsoft/playwright/issues/32256 We were expecting all errors to be of type `ExpectError`, but apparently `expect` propagates rejections in the polling functions right through. So we also need to handle that case. I wonder if we have more cases of this. Would it make sense to enable `useUnknownInCatchVariables` in TypeScript? --- packages/playwright/src/matchers/expect.ts | 6 ++--- .../playwright/src/matchers/matcherHint.ts | 4 +++ packages/playwright/src/worker/testInfo.ts | 10 +++---- tests/playwright-test/expect-poll.spec.ts | 26 +++++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 3e49360eb7..ea796bfc72 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -60,7 +60,7 @@ import { } from '../common/expectBundle'; import { zones } from 'playwright-core/lib/utils'; import { TestInfoImpl } from '../worker/testInfo'; -import { ExpectError } from './matcherHint'; +import { ExpectError, isExpectError } from './matcherHint'; // #region // Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts @@ -289,8 +289,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const step = testInfo._addStep(stepInfo); - const reportStepError = (jestError: ExpectError) => { - const error = new ExpectError(jestError, customMessage, stackFrames); + const reportStepError = (jestError: Error | unknown) => { + const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError; step.complete({ error }); if (this._info.isSoft) testInfo._failWithError(error); diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index e8aba2bbff..8a78932c68 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -64,3 +64,7 @@ export class ExpectError extends Error { this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n'); } } + +export function isExpectError(e: unknown): e is ExpectError { + return e instanceof Error && 'matcherResult' in e; +} diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index e6b07be7c0..378b32524f 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -30,7 +30,7 @@ import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; export interface TestStepInternal { - complete(result: { error?: Error, attachments?: Attachment[] }): void; + complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; stepId: string; title: string; category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string; @@ -270,7 +270,7 @@ export class TestInfoImpl implements TestInfo { step.endWallTime = Date.now(); if (result.error) { - if (!(result.error as any)[stepSymbol]) + if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) (result.error as any)[stepSymbol] = step; const error = serializeError(result.error); if (data.boxedStack) @@ -327,13 +327,13 @@ export class TestInfoImpl implements TestInfo { this.status = 'interrupted'; } - _failWithError(error: Error) { + _failWithError(error: Error | unknown) { if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; const serialized = serializeError(error); - const step = (error as any)[stepSymbol] as TestStepInternal | undefined; + const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; if (step && step.boxedStack) - serialized.stack = `${error.name}: ${error.message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; + serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; this.errors.push(serialized); this._tracing.appendForError(serialized); } diff --git a/tests/playwright-test/expect-poll.spec.ts b/tests/playwright-test/expect-poll.spec.ts index e740fd5abe..aa8bbde79d 100644 --- a/tests/playwright-test/expect-poll.spec.ts +++ b/tests/playwright-test/expect-poll.spec.ts @@ -232,3 +232,29 @@ test('should show intermediate result for poll that spills over test time', asyn expect(result.output).toContain('Expected: 2'); expect(result.output).toContain('Received: 3'); }); + +test('should propagate promise rejections', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32256' } }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('should fail', async () => { + await expect.poll(() => Promise.reject('some error')).toBe({ foo: 'baz' }); + }); + ` + }); + + expect(result.output).toContain('some error'); +}); + +test('should propagate string exception from async arrow function', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32256' } }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('should fail', async () => { + await expect.poll(async () => { throw 'some error' }).toBe({ foo: 'baz' }); + }); + ` + }); + + expect(result.output).toContain('some error'); +}); \ No newline at end of file From 5acd2dbf4835de3a9c5293ede90437768661b66d Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 26 Aug 2024 01:25:59 -0700 Subject: [PATCH 11/47] feat(webkit): roll to r2065 (#32322) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- packages/playwright-core/src/server/webkit/protocol.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index dcd73ae402..43963417fc 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2064", + "revision": "2065", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 80b30f28fb..3ddfda4627 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -536,7 +536,7 @@ export module Protocol { /** * Pseudo-style identifier (see enum PseudoId in RenderStyleConstants.h). */ - export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner"; + export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"target-text"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner"; /** * Pseudo-style identifier (see enum PseudoId in RenderStyleConstants.h). */ From 596f497633c2d7719c7ad65b9ad6e050db8ad387 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 26 Aug 2024 17:27:21 +0200 Subject: [PATCH 12/47] fix: don't throw error on about:blank when blocking ServiceWorker (#32310) Fixes https://github.com/microsoft/playwright/issues/32292 --- packages/playwright-core/src/server/browserContext.ts | 2 +- .../library/browsercontext-service-worker-policy.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index db20728904..09b84b267f 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -142,7 +142,7 @@ export abstract class BrowserContext extends SdkObject { if (debugMode() === 'console') await this.extendInjectedScript(consoleApiSource.source); if (this._options.serviceWorkers === 'block') - await this.addInitScript(`\nnavigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); + await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); if (this._options.permissions) await this.grantPermissions(this._options.permissions); diff --git a/tests/library/browsercontext-service-worker-policy.spec.ts b/tests/library/browsercontext-service-worker-policy.spec.ts index 213cf1461a..1923a2dc4e 100644 --- a/tests/library/browsercontext-service-worker-policy.spec.ts +++ b/tests/library/browsercontext-service-worker-policy.spec.ts @@ -29,4 +29,12 @@ it.describe('block', () => { page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), ]); }); + + it('should not throw error on about:blank', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32292' }); + const errors = []; + page.on('pageerror', error => errors.push(error)); + await page.goto('about:blank'); + expect(errors).toEqual([]); + }); }); From 54709880c298ed41b9372391c8135d483138d126 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 26 Aug 2024 17:32:22 +0200 Subject: [PATCH 13/47] test: update Modernizir expectations (#32308) Looks like `hiddenscroll` was different when an external monitor was connected. --- tests/assets/modernizr/safari-18.json | 2 +- tests/library/modernizr.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/assets/modernizr/safari-18.json b/tests/assets/modernizr/safari-18.json index 7b901a600c..e4f4aa48b3 100644 --- a/tests/assets/modernizr/safari-18.json +++ b/tests/assets/modernizr/safari-18.json @@ -230,7 +230,7 @@ "fullscreen": true, "gamepads": true, "geolocation": true, - "hiddenscroll": false, + "hiddenscroll": true, "history": true, "htmlimports": false, "ie8compat": false, diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index df87081343..2fd4e5c955 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -39,7 +39,6 @@ it('Safari Desktop', async ({ browser, browserName, platform, server, headless, const { actual, expected } = await checkFeatures('safari-18', context, server); expected.pushmanager = false; - expected.hiddenscroll = true; expected.devicemotion2 = false; expected.devicemotion = false; expected.deviceorientation = false; From 67d3d5f203bcb89bc997197e3d013b4525b0b4c8 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 26 Aug 2024 18:26:38 +0200 Subject: [PATCH 14/47] fix(clock): don't throw for |null| or |undefined| callbacks (#32309) Fixes https://github.com/microsoft/playwright/issues/32293 This aligns it how Chromium and other browsers are doing it. --- .../playwright-core/src/server/injected/clock.ts | 7 ++++++- tests/library/clock.spec.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 48cc9276a2..414d23b958 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -239,7 +239,12 @@ export class ClockController { addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { this._replayLogOnce(); - if (options.func === undefined) + + if (options.type === TimerType.AnimationFrame && !options.func) + throw new Error('Callback must be provided to requestAnimationFrame calls'); + if (options.type === TimerType.IdleCallback && !options.func) + throw new Error('Callback must be provided to requestIdleCallback calls'); + if ([TimerType.Timeout, TimerType.Interval].includes(options.type) && !options.func && options.delay === undefined) throw new Error('Callback must be provided to timer calls'); let delay = options.delay ? +options.delay : 0; diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index 90279fd893..daad405e70 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -75,6 +75,14 @@ it.describe('setTimeout', () => { }).toThrow(); }); + it('does not throw if |undefined| or |null| is passed as a callback', async ({ clock }) => { + const timerId1 = clock.setTimeout(undefined, 10); + const timerId2 = clock.setTimeout(null, 10); + await clock.runFor(10); + expect(timerId1).toBeGreaterThan(0); + expect(timerId2).toBeGreaterThan(timerId1); + }); + it('returns numeric id or object with numeric id', async ({ clock }) => { const result = clock.setTimeout(() => { }, 10); expect(result).toEqual(expect.any(Number)); @@ -761,6 +769,14 @@ it.describe('setInterval', () => { }).toThrow(); }); + it('does not throw if |undefined| or |null| is passed as a callback', async ({ clock }) => { + const timerId1 = clock.setInterval(undefined, 10); + const timerId2 = clock.setInterval(null, 10); + await clock.runFor(10); + expect(timerId1).toBeGreaterThan(0); + expect(timerId2).toBeGreaterThan(timerId1); + }); + it('returns numeric id or object with numeric id', async ({ clock }) => { const result = clock.setInterval(() => {}, 10); expect(result).toBeGreaterThan(0); From 3d9342aa775a65950851dd54411569cad3f30579 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 26 Aug 2024 09:29:02 -0700 Subject: [PATCH 15/47] chore: update removeAllListeners docs (#32305) Closes https://github.com/microsoft/playwright/issues/31474 --- docs/src/api/class-browser.md | 4 +- docs/src/api/class-browsercontext.md | 4 +- docs/src/api/class-page.md | 17 ++++- packages/playwright-core/types/types.d.ts | 76 ++++++++++++++++++++--- utils/generate_types/overrides.d.ts | 30 ++++++++- 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/docs/src/api/class-browser.md b/docs/src/api/class-browser.md index 0c6fd67160..59cf4c99c0 100644 --- a/docs/src/api/class-browser.md +++ b/docs/src/api/class-browser.md @@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo ## async method: Browser.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. ### param: Browser.removeAllListeners.type * since: v1.47 diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index eed27fc1c1..d27afc9b58 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1048,8 +1048,10 @@ Returns all open pages in the context. ## async method: BrowserContext.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. ### param: BrowserContext.removeAllListeners.type * since: v1.47 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 834c06ab10..9a3a25b225 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3372,8 +3372,23 @@ By default, after calling the handler Playwright will wait until the overlay bec ## async method: Page.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. + +**Usage** + +```js +page.on('request', async request => { + const response = await request.response(); + const body = await response.body(); + console.log(body.byteLength); +}); +await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); +// Waits for all the reported 'request' events to resolve. +await page.removeAllListeners('request', { behavior: 'wait' }); +``` ### param: Page.removeAllListeners.type * since: v1.47 diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 378c5a7151..7f3c14eff3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -898,17 +898,55 @@ export interface Page { exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. + * + * **Usage** + * + * ```js + * page.on('request', async request => { + * const response = await request.response(); + * const body = await response.body(); + * console.log(body.byteLength); + * }); + * await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); + * // Waits for all the reported 'request' events to resolve. + * await page.removeAllListeners('request', { behavior: 'wait' }); + * ``` + * * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. + * + * **Usage** + * + * ```js + * page.on('request', async request => { + * const response = await request.response(); + * const body = await response.body(); + * console.log(body.byteLength); + * }); + * await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); + * // Waits for all the reported 'request' events to resolve. + * await page.removeAllListeners('request', { behavior: 'wait' }); + * ``` + * * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * Emitted when the page closes. */ @@ -7734,17 +7772,27 @@ export interface BrowserContext { addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * **NOTE** Only works with Chromium browser's persistent context. * @@ -9018,17 +9066,27 @@ export interface BrowserContext { */ export interface Browser { /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the * following: diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 578602959a..e679bbb9cb 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -64,7 +64,15 @@ export interface Page { exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Frame { @@ -106,12 +114,28 @@ export interface BrowserContext { addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Browser { removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Worker { From 4340d153df8d243829afb01ccfaad3999cd20ecc Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 26 Aug 2024 10:28:54 -0700 Subject: [PATCH 16/47] chore: deprecate locator.frameLocator() (#32306) --- docs/src/api/class-page.md | 1 + .../playwright-core/src/server/recorder.ts | 94 +++++----- .../src/server/recorder/codeGenerator.ts | 2 +- .../src/server/recorder/csharp.ts | 10 +- .../src/server/recorder/java.ts | 14 +- .../src/server/recorder/javascript.ts | 10 +- .../src/server/recorder/python.ts | 10 +- .../src/server/recorder/recorderActions.ts | 12 +- tests/library/inspector/cli-codegen-3.spec.ts | 160 +++++++++--------- 9 files changed, 133 insertions(+), 180 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 9a3a25b225..da5d48f906 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2195,6 +2195,7 @@ A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] ob ## method: Page.frameLocator * since: v1.17 +regular [`Locator`] instead. - returns: <[FrameLocator]> When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 9e2174303b..96e24e3210 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -191,15 +191,9 @@ export class Recorder implements InstrumentationListener { }); await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => { - const selectorPromises: Promise[] = []; - let currentFrame: Frame | null = frame; - while (currentFrame) { - selectorPromises.push(findFrameSelector(currentFrame)); - currentFrame = currentFrame.parentFrame(); - } - const fullSelector = (await Promise.all(selectorPromises)).filter(Boolean); - fullSelector.push(selector); - await this._recorderApp?.setSelector(fullSelector.join(' >> internal:control=enter-frame >> '), true); + const selectorChain = await generateFrameSelector(frame); + selectorChain.push(selector); + await this._recorderApp?.setSelector(selectorChain.join(' >> internal:control=enter-frame >> '), true); }); await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => { @@ -539,45 +533,14 @@ class ContextRecorder extends EventEmitter { private _describeMainFrame(page: Page): actions.FrameDescription { return { pageAlias: this._pageAliases.get(page)!, - isMainFrame: true, + framePath: [], }; } private async _describeFrame(frame: Frame): Promise { - const page = frame._page; - const pageAlias = this._pageAliases.get(page)!; - const chain: Frame[] = []; - for (let ancestor: Frame | null = frame; ancestor; ancestor = ancestor.parentFrame()) - chain.push(ancestor); - chain.reverse(); - - if (chain.length === 1) - return this._describeMainFrame(page); - - const selectorPromises: Promise[] = []; - for (let i = 0; i < chain.length - 1; i++) - selectorPromises.push(findFrameSelector(chain[i + 1])); - - const result = await raceAgainstDeadline(() => Promise.all(selectorPromises), monotonicTime() + 2000); - if (!result.timedOut && result.result.every(selector => !!selector)) { - return { - pageAlias, - isMainFrame: false, - selectorsChain: result.result as string[], - }; - } - // Best effort to find a selector for the frame. - const selectorsChain = []; - for (let i = 0; i < chain.length - 1; i++) { - if (chain[i].name()) - selectorsChain.push(`iframe[name=${quoteCSSAttributeValue(chain[i].name())}]`); - else - selectorsChain.push(`iframe[src=${quoteCSSAttributeValue(chain[i].url())}]`); - } return { - pageAlias, - isMainFrame: false, - selectorsChain, + pageAlias: this._pageAliases.get(frame._page)!, + framePath: await generateFrameSelector(frame), }; } @@ -691,20 +654,41 @@ function isScreenshotCommand(metadata: CallMetadata) { return metadata.method.toLowerCase().includes('screenshot'); } -async function findFrameSelector(frame: Frame): Promise { - try { +async function generateFrameSelector(frame: Frame): Promise { + const selectorPromises: Promise[] = []; + while (frame) { const parent = frame.parentFrame(); - const frameElement = await frame.frameElement(); - if (!frameElement || !parent) - return; - const utility = await parent._utilityContext(); - const injected = await utility.injectedScript(); - const selector = await injected.evaluate((injected, element) => { - return injected.generateSelectorSimple(element as Element, { testIdAttributeName: '', omitInternalEngines: true }); - }, frameElement); - return selector; - } catch (e) { + if (!parent) + break; + selectorPromises.push(generateFrameSelectorInParent(parent, frame)); + frame = parent; } + const result = await Promise.all(selectorPromises); + return result.reverse(); +} + +async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promise { + const result = await raceAgainstDeadline(async () => { + try { + const frameElement = await frame.frameElement(); + if (!frameElement || !parent) + return; + const utility = await parent._utilityContext(); + const injected = await utility.injectedScript(); + const selector = await injected.evaluate((injected, element) => { + return injected.generateSelectorSimple(element as Element); + }, frameElement); + return selector; + } catch (e) { + return e.toString(); + } + }, monotonicTime() + 2000); + if (!result.timedOut && result.result) + return result.result; + + if (frame.name()) + return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; + return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; } async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { diff --git a/packages/playwright-core/src/server/recorder/codeGenerator.ts b/packages/playwright-core/src/server/recorder/codeGenerator.ts index d3bb5f86d9..0185302a08 100644 --- a/packages/playwright-core/src/server/recorder/codeGenerator.ts +++ b/packages/playwright-core/src/server/recorder/codeGenerator.ts @@ -146,7 +146,7 @@ export class CodeGenerator extends EventEmitter { this.addAction({ frame: { pageAlias, - isMainFrame: true, + framePath: [], }, committed: true, action: { diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index 52460f8121..504995c9a8 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -72,14 +72,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/recorder/java.ts index 72d5d9a995..e6f0b3f0ed 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/recorder/java.ts @@ -63,16 +63,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - let inFrameLocator = false; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - inFrameLocator = true; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { @@ -82,7 +74,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { });`); } - let code = this._generateActionCall(subject, action, inFrameLocator); + let code = this._generateActionCall(subject, action, !!actionInContext.frame.framePath.length); if (signals.popup) { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/recorder/javascript.ts index 104b3bcd53..37fd3c25a9 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/recorder/javascript.ts @@ -52,14 +52,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts index d393fb38e4..3302089fbd 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/recorder/python.ts @@ -59,14 +59,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 295758aaeb..3ab7fb91ba 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -150,13 +150,7 @@ export type DialogSignal = BaseSignal & { export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; -type FrameDescriptionMainFrame = { - isMainFrame: true; +export type FrameDescription = { + pageAlias: string; + framePath: string[]; }; - -type FrameDescriptionChildFrame = { - isMainFrame: false; - selectorsChain: string[]; -}; - -export type FrameDescription = { pageAlias: string } & (FrameDescriptionMainFrame | FrameDescriptionChildFrame); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 424b2bacfe..6828576456 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -120,20 +120,20 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy frameHello1.click('text=Hello1'), ]); - expect(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').getByText('Hello1').click();`); + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().getByText('Hello1').click();`); - expect(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").getByText("Hello1").click();`); + expect.soft(sources.get('Java')!.text).toContain(` + page.locator("#frame1").contentFrame().getByText("Hello1").click();`); - expect(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").get_by_text("Hello1").click()`); + expect.soft(sources.get('Python')!.text).toContain(` + page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); - expect(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").get_by_text("Hello1").click()`); + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); - expect(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").GetByText("Hello1").ClickAsync();`); + expect.soft(sources.get('C#')!.text).toContain(` +await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`); [sources] = await Promise.all([ @@ -142,19 +142,19 @@ await page.FrameLocator("#frame1").GetByText("Hello1").ClickAsync();`); ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').getByText('Hello2').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().getByText('Hello2').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").getByText("Hello2").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").GetByText("Hello2").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().GetByText("Hello2").ClickAsync();`); [sources] = await Promise.all([ @@ -163,19 +163,19 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").GetByText("Hello2").Cl ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe[name="one"]').getByText('HelloNameOne').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe[name="one"]').contentFrame().getByText('HelloNameOne').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").frameLocator("iframe[name=\\"one\\"]").getByText("HelloNameOne").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe[name=\\"one\\"]").get_by_text("HelloNameOne").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe[name=\\"one\\"]").get_by_text("HelloNameOne").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe[name=\\"one\\"]").GetByText("HelloNameOne").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe[name=\\"one\\"]").ContentFrame().GetByText("HelloNameOne").ClickAsync();`); [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'), @@ -183,19 +183,19 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe[n ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe >> nth=2').getByText('HelloNameAnonymous').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe').nth(2).contentFrame().getByText('HelloNameAnonymous').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").frameLocator("iframe >> nth=2").getByText("HelloNameAnonymous").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe >> nth=2").get_by_text("HelloNameAnonymous").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe >> nth=2").get_by_text("HelloNameAnonymous").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe >> nth=2").GetByText("HelloNameAnonymous").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe").Nth(2).ContentFrame().GetByText("HelloNameAnonymous").ClickAsync();`); }); test('should generate frame locators with special characters in name attribute', async ({ page, openRecorder, server }) => { @@ -208,22 +208,22 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe > }); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('iframe[name="foo"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('iframe[name="foo"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('iframe[name="foo\\\\"]').getByRole('button', { name: 'Click me' }).click();`); + await page.locator('iframe[name="foo\\\\"]').contentFrame().getByRole('button', { name: 'Click me' }).click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("iframe[name=\\"foo\\\\\\"]").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`); + page.locator("iframe[name=\\"foo\\\\\\"]").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("iframe[name=\\"foo\\\\\\"]").get_by_role("button", name="Click me").click()`); + page.locator("iframe[name=\\"foo\\\\\\"]").content_frame().get_by_role("button", name="Click me").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("iframe[name=\\"foo\\\\\\"]").get_by_role("button", name="Click me").click()`); + await page.locator("iframe[name=\\"foo\\\\\\"]").content_frame().get_by_role("button", name="Click me").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("iframe[name=\\"foo\\\\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`); +await page.Locator("iframe[name=\\"foo\\\\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`); }); test('should generate frame locators with title attribute', async ({ page, openRecorder, server }) => { @@ -234,27 +234,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[title="hello world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[title="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('iframe[title="hello world"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('iframe[title="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"iframe[title=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"iframe[title=\\\"hello world\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"iframe[title=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("iframe[title=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("iframe[title=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("iframe[title=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -266,27 +266,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[name="hello world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[name="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('iframe[name="hello world"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('iframe[name="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"iframe[name=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"iframe[name=\\\"hello world\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"iframe[name=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("iframe[name=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("iframe[name=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("iframe[name=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -298,27 +298,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[id="hello-world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[id="hello-world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('#hello-world').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('#hello-world').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"#hello-world\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"#hello-world\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"#hello-world\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"#hello-world\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("#hello-world").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("#hello-world").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("#hello-world").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("#hello-world").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -330,27 +330,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'my-testid'), - page.frameLocator('iframe[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('iframe[data-testid="my-testid"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('[data-testid="my-testid"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"[data-testid=\\\"my-testid\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"[data-testid=\\\"my-testid\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"[data-testid=\\\"my-testid\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("[data-testid=\\\"my-testid\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("[data-testid=\\\"my-testid\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -365,19 +365,19 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').getByRole('button', { name: 'Submit' }).click();`); + await page.locator('#frame1').contentFrame().getByRole('button', { name: 'Submit' }).click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); + page.locator("#frame1").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); + page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); + await page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); +await page.Locator("#frame1").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); }); test('should generate getByTestId', async ({ page, openRecorder }) => { From 888a5b53e7972a65917fd8df81b9de537405b2b5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 26 Aug 2024 11:02:41 -0700 Subject: [PATCH 17/47] docs: avoid confustion with incognito mode (#32327) Fixes https://github.com/microsoft/playwright/issues/32321 --- docs/src/api/class-browsercontext.md | 2 +- packages/playwright-core/types/types.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index d27afc9b58..36f980e0ce 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -6,7 +6,7 @@ BrowserContexts provide a way to operate multiple independent browser sessions. If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser context. -Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser +Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser contexts don't write any browsing data to disk. ```js diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7f3c14eff3..d0424c3024 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -7605,9 +7605,9 @@ export interface Frame { * If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser * context. * - * Playwright allows creating "incognito" browser contexts with + * Playwright allows creating isolated non-persistent browser contexts with * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) method. - * "Incognito" browser contexts don't write any browsing data to disk. + * Non-persistent browser contexts don't write any browsing data to disk. * * ```js * // Create a new incognito browser context From 6f55b57e5a2a92e99fb706e46ff9d8dc77f7d310 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 26 Aug 2024 15:24:02 -0700 Subject: [PATCH 18/47] chore: move codegen into its own folder (#32330) --- packages/playwright-core/src/server/DEPS.list | 7 + .../src/server/codegen/DEPS.list | 3 + .../{recorder => codegen}/codeGenerator.ts | 9 +- .../server/{recorder => codegen}/csharp.ts | 27 ++-- .../src/server/{recorder => codegen}/java.ts | 30 ++-- .../{recorder => codegen}/javascript.ts | 27 ++-- .../src/server/{recorder => codegen}/jsonl.ts | 0 .../server/{recorder => codegen}/language.ts | 38 +++++- .../src/server/codegen/languages.ts | 37 +++++ .../server/{recorder => codegen}/python.ts | 27 ++-- .../playwright-core/src/server/recorder.ts | 129 ++---------------- .../src/server/recorder/recorderActions.ts | 5 - .../src/server/recorder/utils.ts | 51 ------- .../src/server/recorderRunner.ts | 116 ++++++++++++++++ 14 files changed, 246 insertions(+), 260 deletions(-) create mode 100644 packages/playwright-core/src/server/codegen/DEPS.list rename packages/playwright-core/src/server/{recorder => codegen}/codeGenerator.ts (97%) rename packages/playwright-core/src/server/{recorder => codegen}/csharp.ts (93%) rename packages/playwright-core/src/server/{recorder => codegen}/java.ts (91%) rename packages/playwright-core/src/server/{recorder => codegen}/javascript.ts (91%) rename packages/playwright-core/src/server/{recorder => codegen}/jsonl.ts (100%) rename packages/playwright-core/src/server/{recorder => codegen}/language.ts (66%) create mode 100644 packages/playwright-core/src/server/codegen/languages.ts rename packages/playwright-core/src/server/{recorder => codegen}/python.ts (92%) delete mode 100644 packages/playwright-core/src/server/recorder/utils.ts create mode 100644 packages/playwright-core/src/server/recorderRunner.ts diff --git a/packages/playwright-core/src/server/DEPS.list b/packages/playwright-core/src/server/DEPS.list index bc32bb8486..0e2b8301d6 100644 --- a/packages/playwright-core/src/server/DEPS.list +++ b/packages/playwright-core/src/server/DEPS.list @@ -20,3 +20,10 @@ ./electron/ ./firefox/ ./webkit/ + +[recorder.ts] +./codegen/codeGenerator.ts +./codegen/languages.ts + +[recorderRunner.ts] +./codegen/language.ts diff --git a/packages/playwright-core/src/server/codegen/DEPS.list b/packages/playwright-core/src/server/codegen/DEPS.list new file mode 100644 index 0000000000..58432390fd --- /dev/null +++ b/packages/playwright-core/src/server/codegen/DEPS.list @@ -0,0 +1,3 @@ +[*] +../../utils/ +../deviceDescriptors.ts diff --git a/packages/playwright-core/src/server/recorder/codeGenerator.ts b/packages/playwright-core/src/server/codegen/codeGenerator.ts similarity index 97% rename from packages/playwright-core/src/server/recorder/codeGenerator.ts rename to packages/playwright-core/src/server/codegen/codeGenerator.ts index 0185302a08..bfc640b38e 100644 --- a/packages/playwright-core/src/server/recorder/codeGenerator.ts +++ b/packages/playwright-core/src/server/codegen/codeGenerator.ts @@ -15,10 +15,15 @@ */ import { EventEmitter } from 'events'; -import type { BrowserContextOptions, LaunchOptions } from '../../..'; +import type { BrowserContextOptions, LaunchOptions } from '../../../types/types'; import type { Frame } from '../frames'; import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; -import type { Action, Signal, FrameDescription } from './recorderActions'; +import type { Action, Signal } from '../recorder/recorderActions'; + +export type FrameDescription = { + pageAlias: string; + framePath: string[]; +}; export type ActionInContext = { frame: FrameDescription; diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts similarity index 93% rename from packages/playwright-core/src/server/recorder/csharp.ts rename to packages/playwright-core/src/server/codegen/csharp.ts index 504995c9a8..41f91d259b 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { escapeWithQuotes, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -87,7 +84,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { } const lines: string[] = []; - lines.push(this._generateActionCall(subject, action)); + lines.push(this._generateActionCall(subject, actionInContext)); if (signals.download) { lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`); @@ -105,7 +102,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -115,16 +113,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { let method = 'Click'; if (action.clickCount === 2) method = 'DblClick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); if (!Object.entries(options).length) return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`; const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); @@ -139,7 +128,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`; } diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/codegen/java.ts similarity index 91% rename from packages/playwright-core/src/server/recorder/java.ts rename to packages/playwright-core/src/server/codegen/java.ts index e6f0b3f0ed..a59546f7ba 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; +import type * as types from '../types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -74,7 +72,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { });`); } - let code = this._generateActionCall(subject, action, !!actionInContext.frame.framePath.length); + let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length); if (signals.popup) { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { @@ -93,7 +91,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -103,16 +102,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsText = formatClickOptions(options); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`; } @@ -125,7 +115,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`; } @@ -271,7 +261,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: return lines.join('\n'); } -function formatClickOptions(options: MouseClickOptions) { +function formatClickOptions(options: types.MouseClickOptions) { const lines = []; if (options.button) lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`); diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts similarity index 91% rename from packages/playwright-core/src/server/recorder/javascript.ts rename to packages/playwright-core/src/server/codegen/javascript.ts index 37fd3c25a9..bc0f20e97a 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -68,7 +65,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (signals.download) formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); - formatter.add(this._generateActionCall(subject, action)); + formatter.add(this._generateActionCall(subject, actionInContext)); if (signals.popup) formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); @@ -78,7 +75,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -88,16 +86,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`; } @@ -110,7 +99,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`; } diff --git a/packages/playwright-core/src/server/recorder/jsonl.ts b/packages/playwright-core/src/server/codegen/jsonl.ts similarity index 100% rename from packages/playwright-core/src/server/recorder/jsonl.ts rename to packages/playwright-core/src/server/codegen/jsonl.ts diff --git a/packages/playwright-core/src/server/recorder/language.ts b/packages/playwright-core/src/server/codegen/language.ts similarity index 66% rename from packages/playwright-core/src/server/recorder/language.ts rename to packages/playwright-core/src/server/codegen/language.ts index cee2b22163..78414733d7 100644 --- a/packages/playwright-core/src/server/recorder/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -16,8 +16,9 @@ import type { BrowserContextOptions, LaunchOptions } from '../../..'; import type { Language } from '../../utils'; +import type * as actions from '../recorder/recorderActions'; +import type * as types from '../types'; import type { ActionInContext } from './codeGenerator'; -import type { Action, DialogSignal, DownloadSignal, PopupSignal } from './recorderActions'; export type { Language } from '../../utils'; export type LanguageGeneratorOptions = { @@ -51,10 +52,10 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption return cleanedOptions; } -export function toSignalMap(action: Action) { - let popup: PopupSignal | undefined; - let download: DownloadSignal | undefined; - let dialog: DialogSignal | undefined; +export function toSignalMap(action: actions.Action) { + let popup: actions.PopupSignal | undefined; + let download: actions.DownloadSignal | undefined; + let dialog: actions.DialogSignal | undefined; for (const signal of action.signals) { if (signal.name === 'popup') popup = signal; @@ -69,3 +70,30 @@ export function toSignalMap(action: Action) { dialog, }; } + +export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] { + const result: types.SmartKeyboardModifier[] = []; + if (modifiers & 1) + result.push('Alt'); + if (modifiers & 2) + result.push('ControlOrMeta'); + if (modifiers & 4) + result.push('ControlOrMeta'); + if (modifiers & 8) + result.push('Shift'); + return result; +} + +export function toClickOptions(action: actions.ClickAction): types.MouseClickOptions { + const modifiers = toKeyboardModifiers(action.modifiers); + const options: types.MouseClickOptions = {}; + if (action.button !== 'left') + options.button = action.button; + if (modifiers.length) + options.modifiers = modifiers; + if (action.clickCount > 2) + options.clickCount = action.clickCount; + if (action.position) + options.position = action.position; + return options; +} diff --git a/packages/playwright-core/src/server/codegen/languages.ts b/packages/playwright-core/src/server/codegen/languages.ts new file mode 100644 index 0000000000..d379be6be7 --- /dev/null +++ b/packages/playwright-core/src/server/codegen/languages.ts @@ -0,0 +1,37 @@ +/** + * 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 { JavaLanguageGenerator } from './java'; +import { JavaScriptLanguageGenerator } from './javascript'; +import { JsonlLanguageGenerator } from './jsonl'; +import { CSharpLanguageGenerator } from './csharp'; +import { PythonLanguageGenerator } from './python'; + +export function languageSet() { + return new Set([ + new JavaLanguageGenerator('junit'), + new JavaLanguageGenerator('library'), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), + new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), + new CSharpLanguageGenerator('mstest'), + new CSharpLanguageGenerator('nunit'), + new CSharpLanguageGenerator('library'), + new JsonlLanguageGenerator(), + ]); +} diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/codegen/python.ts similarity index 92% rename from packages/playwright-core/src/server/recorder/python.ts rename to packages/playwright-core/src/server/codegen/python.ts index 3302089fbd..98949320e7 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -66,7 +63,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { if (signals.dialog) formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); - let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`; + let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`; if (signals.popup) { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { @@ -87,7 +84,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -97,16 +95,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`; } @@ -119,7 +108,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`; } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 96e24e3210..e4e26b0e98 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -17,17 +17,11 @@ import * as fs from 'fs'; import type * as actions from './recorder/recorderActions'; import type * as channels from '@protocol/channels'; -import type { ActionInContext } from './recorder/codeGenerator'; -import { CodeGenerator } from './recorder/codeGenerator'; -import { toClickOptions, toModifiers } from './recorder/utils'; +import type { ActionInContext, FrameDescription } from './codegen/codeGenerator'; +import { CodeGenerator } from './codegen/codeGenerator'; import { Page } from './page'; import { Frame } from './frames'; import { BrowserContext } from './browserContext'; -import { JavaLanguageGenerator } from './recorder/java'; -import { JavaScriptLanguageGenerator } from './recorder/javascript'; -import { JsonlLanguageGenerator } from './recorder/jsonl'; -import { CSharpLanguageGenerator } from './recorder/csharp'; -import { PythonLanguageGenerator } from './recorder/python'; import * as recorderSource from '../generated/recorderSource'; import * as consoleApiSource from '../generated/consoleApiSource'; import { EmptyRecorderApp } from './recorder/recorderApp'; @@ -36,15 +30,17 @@ import { RecorderApp } from './recorder/recorderApp'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; -import { createGuid, isUnderTest, monotonicTime, serializeExpectedTextValues } from '../utils'; +import { isUnderTest, monotonicTime } from '../utils'; import { metadataToCallLog } from './recorder/recorderUtils'; import { Debugger } from './debugger'; import { EventEmitter } from 'events'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; -import type { Language, LanguageGenerator } from './recorder/language'; +import { type Language, type LanguageGenerator } from './codegen/language'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils'; import type { Dialog } from './dialog'; +import { performAction } from './recorderRunner'; +import { languageSet } from './codegen/languages'; type BindingSource = { frame: Frame, page: Page }; @@ -425,19 +421,7 @@ class ContextRecorder extends EventEmitter { } setOutput(codegenId: string, outputFile?: string) { - const languages = new Set([ - new JavaLanguageGenerator('junit'), - new JavaLanguageGenerator('library'), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), - new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), - new CSharpLanguageGenerator('mstest'), - new CSharpLanguageGenerator('nunit'), - new CSharpLanguageGenerator('library'), - new JsonlLanguageGenerator(), - ]); + const languages = languageSet(); const primaryLanguage = [...languages].find(l => l.id === codegenId); if (!primaryLanguage) throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); @@ -530,14 +514,14 @@ class ContextRecorder extends EventEmitter { } } - private _describeMainFrame(page: Page): actions.FrameDescription { + private _describeMainFrame(page: Page): FrameDescription { return { pageAlias: this._pageAliases.get(page)!, framePath: [], }; } - private async _describeFrame(frame: Frame): Promise { + private async _describeFrame(frame: Frame): Promise { return { pageAlias: this._pageAliases.get(frame._page)!, framePath: await generateFrameSelector(frame), @@ -690,98 +674,3 @@ async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promi return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; } - -async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { - const callMetadata: CallMetadata = { - id: `call@${createGuid()}`, - apiName: 'frame.' + action, - objectId: frame.guid, - pageId: frame._page.guid, - frameId: frame.guid, - startTime: monotonicTime(), - endTime: 0, - type: 'Frame', - method: action, - params, - log: [], - }; - - try { - await frame.instrumentation.onBeforeCall(frame, callMetadata); - await cb(callMetadata); - } catch (e) { - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - return false; - } - - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - return true; -} - -async function performAction(frame: Frame, action: actions.Action): Promise { - const kActionTimeout = 5000; - if (action.name === 'click') { - const { options } = toClickOptions(action); - return await innerPerformAction(frame, 'click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); - } - if (action.name === 'press') { - const modifiers = toModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - return await innerPerformAction(frame, 'press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); - } - if (action.name === 'fill') - return await innerPerformAction(frame, 'fill', { selector: action.selector, text: action.text }, callMetadata => frame.fill(callMetadata, action.selector, action.text, { timeout: kActionTimeout, strict: true })); - if (action.name === 'setInputFiles') - return await innerPerformAction(frame, 'setInputFiles', { selector: action.selector, files: action.files }, callMetadata => frame.setInputFiles(callMetadata, action.selector, { selector: action.selector, payloads: [], timeout: kActionTimeout, strict: true })); - if (action.name === 'check') - return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'uncheck') - return await innerPerformAction(frame, 'uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'select') { - const values = action.options.map(value => ({ value })); - return await innerPerformAction(frame, 'selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); - } - if (action.name === 'navigate') - return await innerPerformAction(frame, 'goto', { url: action.url }, callMetadata => frame.goto(callMetadata, action.url, { timeout: kActionTimeout })); - if (action.name === 'closePage') - return await innerPerformAction(frame, 'close', {}, callMetadata => frame._page.close(callMetadata)); - if (action.name === 'openPage') - throw Error('Not reached'); - if (action.name === 'assertChecked') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, - expression: 'to.be.checked', - isNot: !action.checked, - timeout: kActionTimeout, - })); - } - if (action.name === 'assertText') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, - expression: 'to.have.text', - expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), - isNot: false, - timeout: kActionTimeout, - })); - } - if (action.name === 'assertValue') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, - expression: 'to.have.value', - expectedValue: action.value, - isNot: false, - timeout: kActionTimeout, - })); - } - if (action.name === 'assertVisible') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, - expression: 'to.be.visible', - isNot: false, - timeout: kActionTimeout, - })); - } - throw new Error('Internal error: unexpected action ' + (action as any).name); -} diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 3ab7fb91ba..c048d21bd3 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -149,8 +149,3 @@ export type DialogSignal = BaseSignal & { }; export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; - -export type FrameDescription = { - pageAlias: string; - framePath: string[]; -}; diff --git a/packages/playwright-core/src/server/recorder/utils.ts b/packages/playwright-core/src/server/recorder/utils.ts deleted file mode 100644 index 883a8ab129..0000000000 --- a/packages/playwright-core/src/server/recorder/utils.ts +++ /dev/null @@ -1,51 +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 type { Frame } from '../frames'; -import type { SmartKeyboardModifier } from '../types'; -import type * as actions from './recorderActions'; - -export type MouseClickOptions = Parameters[2]; - -export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } { - let method: 'click' | 'dblclick' = 'click'; - if (action.clickCount === 2) - method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; - return { method, options }; -} - -export function toModifiers(modifiers: number): SmartKeyboardModifier[] { - const result: SmartKeyboardModifier[] = []; - if (modifiers & 1) - result.push('Alt'); - if (modifiers & 2) - result.push('ControlOrMeta'); - if (modifiers & 4) - result.push('ControlOrMeta'); - if (modifiers & 8) - result.push('Shift'); - return result; -} diff --git a/packages/playwright-core/src/server/recorderRunner.ts b/packages/playwright-core/src/server/recorderRunner.ts new file mode 100644 index 0000000000..4058ae4053 --- /dev/null +++ b/packages/playwright-core/src/server/recorderRunner.ts @@ -0,0 +1,116 @@ +/** + * 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 { createGuid, monotonicTime, serializeExpectedTextValues } from '../utils'; +import { toClickOptions, toKeyboardModifiers } from './codegen/language'; +import type { Frame } from './frames'; +import type { CallMetadata } from './instrumentation'; +import type * as actions from './recorder/recorderActions'; + +async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { + const callMetadata: CallMetadata = { + id: `call@${createGuid()}`, + apiName: 'frame.' + action, + objectId: frame.guid, + pageId: frame._page.guid, + frameId: frame.guid, + startTime: monotonicTime(), + endTime: 0, + type: 'Frame', + method: action, + params, + log: [], + }; + + try { + await frame.instrumentation.onBeforeCall(frame, callMetadata); + await cb(callMetadata); + } catch (e) { + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return false; + } + + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return true; +} + +export async function performAction(frame: Frame, action: actions.Action): Promise { + const kActionTimeout = 5000; + if (action.name === 'click') { + const options = toClickOptions(action); + return await innerPerformAction(frame, 'click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); + } + if (action.name === 'press') { + const modifiers = toKeyboardModifiers(action.modifiers); + const shortcut = [...modifiers, action.key].join('+'); + return await innerPerformAction(frame, 'press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'fill') + return await innerPerformAction(frame, 'fill', { selector: action.selector, text: action.text }, callMetadata => frame.fill(callMetadata, action.selector, action.text, { timeout: kActionTimeout, strict: true })); + if (action.name === 'setInputFiles') + return await innerPerformAction(frame, 'setInputFiles', { selector: action.selector, files: action.files }, callMetadata => frame.setInputFiles(callMetadata, action.selector, { selector: action.selector, payloads: [], timeout: kActionTimeout, strict: true })); + if (action.name === 'check') + return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'uncheck') + return await innerPerformAction(frame, 'uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'select') { + const values = action.options.map(value => ({ value })); + return await innerPerformAction(frame, 'selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'navigate') + return await innerPerformAction(frame, 'goto', { url: action.url }, callMetadata => frame.goto(callMetadata, action.url, { timeout: kActionTimeout })); + if (action.name === 'closePage') + return await innerPerformAction(frame, 'close', {}, callMetadata => frame._page.close(callMetadata)); + if (action.name === 'openPage') + throw Error('Not reached'); + if (action.name === 'assertChecked') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.be.checked', + isNot: !action.checked, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertText') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.have.text', + expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertValue') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.have.value', + expectedValue: action.value, + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertVisible') { + return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { + selector: action.selector, + expression: 'to.be.visible', + isNot: false, + timeout: kActionTimeout, + })); + } + throw new Error('Internal error: unexpected action ' + (action as any).name); +} From 177576a51bf236f80d5135b2fefa7f0811e9e08d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 26 Aug 2024 16:28:40 -0700 Subject: [PATCH 19/47] chore: add simple dom util (#32332) --- .../src/server/injected/simpleDom.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/playwright-core/src/server/injected/simpleDom.ts diff --git a/packages/playwright-core/src/server/injected/simpleDom.ts b/packages/playwright-core/src/server/injected/simpleDom.ts new file mode 100644 index 0000000000..0538dabc1e --- /dev/null +++ b/packages/playwright-core/src/server/injected/simpleDom.ts @@ -0,0 +1,82 @@ +/** + * 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 { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils'; +import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName } from './roleUtils'; +import { isElementVisible } from './domUtils'; + +const leafRoles = new Set([ + 'button', + 'checkbox', + 'combobox', + 'link', + 'textbox', +]); + +export function simpleDom(document: Document): { markup: string, elements: Map } { + const normalizeWhitespace = (text: string) => text.replace(/[\s\n]+/g, match => match.includes('\n') ? '\n' : ' '); + const tokens: string[] = []; + const idMap = new Map(); + let lastId = 0; + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + tokens.push(node.nodeValue!); + return; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + if (element.nodeName === 'SCRIPT' || element.nodeName === 'STYLE' || element.nodeName === 'NOSCRIPT') + return; + if (isElementVisible(element)) { + const role = getAriaRole(element) as string; + if (role && leafRoles.has(role)) { + let value: string | undefined; + if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') + value = (element as HTMLInputElement | HTMLTextAreaElement).value; + const name = getElementAccessibleName(element, false); + const structuralId = String(++lastId); + idMap.set(structuralId, element); + tokens.push(renderTag(role, name, structuralId, { value })); + return; + } + } + for (let child = element.firstChild; child; child = child.nextSibling) + visit(child); + } + }; + beginAriaCaches(); + try { + visit(document.body); + } finally { + endAriaCaches(); + } + return { + markup: normalizeWhitespace(tokens.join(' ')), + elements: idMap + }; +} + +function renderTag(role: string, name: string, id: string, params?: { value?: string }): string { + const escapedTextContent = escapeHTML(name); + const escapedValue = escapeHTMLAttribute(params?.value || ''); + switch (role) { + case 'button': return ``; + case 'link': return `${escapedTextContent}`; + case 'textbox': return ``; + } + return `
${escapedTextContent}
`; +} From 3f085d568935b9a4422f689185f720c1206bac84 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 26 Aug 2024 18:41:58 -0700 Subject: [PATCH 20/47] chore: remove same-site expectations for old browsers (#32334) --- tests/config/browserTest.ts | 12 ++++-------- tests/library/permissions.spec.ts | 7 +------ tests/page/jshandle-to-string.spec.ts | 4 ++-- tests/page/page-accessibility.spec.ts | 4 ++-- tests/page/page-network-response.spec.ts | 4 ++-- tests/page/page-route.spec.ts | 4 ++-- tests/page/wheel.spec.ts | 4 ++-- 7 files changed, 15 insertions(+), 24 deletions(-) diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 01b574f105..b5b09fbae7 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -59,10 +59,8 @@ const test = baseTest.extend }, { scope: 'worker' }], allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => { - if (browserName === 'firefox' && !channel) - await run(browserMajorVersion >= 103); - else if (browserName === 'firefox' && channel === 'firefox-beta') - await run(browserMajorVersion < 103 || browserMajorVersion >= 110); + if (browserName === 'firefox') + await run(true); else await run(false); }, { scope: 'worker' }], @@ -74,10 +72,8 @@ const test = baseTest.extend await run('Lax'); else if (browserName === 'webkit' && !isLinux) await run('None'); - else if (browserName === 'firefox' && channel === 'firefox-beta') - await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None'); - else if (browserName === 'firefox' && channel !== 'firefox-beta') - await run(browserMajorVersion >= 103 ? 'None' : 'Lax'); + else if (browserName === 'firefox') + await run('None'); else throw new Error('unknown browser - ' + browserName); }, { scope: 'worker' }], diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index 592ee80691..1064eaf61f 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -119,12 +119,7 @@ it.describe('permissions', () => { await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted']); await context.clearPermissions(); - - // Note: Chromium 110 stopped triggering "onchange" when clearing permissions. - expect(await page.evaluate(() => window['events'])).toEqual( - (browserName === 'chromium' && browserMajorVersion === 110) ? - ['prompt', 'denied', 'granted'] : - ['prompt', 'denied', 'granted', 'prompt']); + expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); }); it('should isolate permissions between browser contexts', async ({ server, browser }) => { diff --git a/tests/page/jshandle-to-string.spec.ts b/tests/page/jshandle-to-string.spec.ts index 930899ae3d..dc0a6695bb 100644 --- a/tests/page/jshandle-to-string.spec.ts +++ b/tests/page/jshandle-to-string.spec.ts @@ -56,7 +56,7 @@ it('should work for promises', async ({ page }) => { expect(bHandle.toString()).toBe('Promise'); }); -it('should work with different subtypes @smoke', async ({ page, browserName, browserMajorVersion }) => { +it('should work with different subtypes @smoke', async ({ page, browserName }) => { expect((await page.evaluateHandle('(function(){})')).toString()).toContain('function'); expect((await page.evaluateHandle('12')).toString()).toBe('12'); expect((await page.evaluateHandle('true')).toString()).toBe('true'); @@ -71,7 +71,7 @@ it('should work with different subtypes @smoke', async ({ page, browserName, bro expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('WeakMap'); expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('WeakSet'); expect((await page.evaluateHandle('new Error()')).toString()).toContain('Error'); - expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe((browserName === 'chromium' && browserMajorVersion >= 111) ? 'Proxy(Object)' : 'Proxy'); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe((browserName === 'chromium') ? 'Proxy(Object)' : 'Proxy'); }); it('should work with previewable subtypes', async ({ page, browserName }) => { diff --git a/tests/page/page-accessibility.spec.ts b/tests/page/page-accessibility.spec.ts index e6717c5e20..a85d3112bd 100644 --- a/tests/page/page-accessibility.spec.ts +++ b/tests/page/page-accessibility.spec.ts @@ -177,7 +177,7 @@ it('rich text editable fields should have children', async function({ page, brow expect(snapshot.children[0]).toEqual(golden); }); -it('rich text editable fields with role should have children', async function({ page, browserName, browserMajorVersion, browserVersion, isWebView2 }) { +it('rich text editable fields with role should have children', async function({ page, browserName, browserVersion, isWebView2 }) { it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy'); it.skip(isWebView2, 'WebView2 is missing a Chromium fix'); @@ -196,7 +196,7 @@ it('rich text editable fields with role should have children', async function({ } : { role: 'textbox', name: '', - multiline: (browserName === 'chromium' && browserMajorVersion >= 92) ? true : undefined, + multiline: (browserName === 'chromium') ? true : undefined, value: 'Edit this image: ', children: (chromiumVersionLessThan(browserVersion, '104.0.1293.1') && browserName === 'chromium') ? [{ role: 'text', diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index ecae86987a..41d7178208 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -270,14 +270,14 @@ it('should behave the same way for headers and allHeaders', async ({ page, serve expect(allHeaders['name-b']).toEqual('v4'); }); -it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, browserMajorVersion, mode }) => { +it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, mode }) => { it.skip(isAndroid, 'No files on Android'); it.skip(browserName === 'firefox', 'Firefox does return null for file:// URLs'); it.skip(mode.startsWith('service')); const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href; const response = await page.goto(fileurl); - if (isElectron || (browserName === 'chromium' && browserMajorVersion >= 99) || (browserName === 'webkit' && isWindows)) + if (isElectron || (browserName === 'chromium') || (browserName === 'webkit' && isWindows)) expect(response.status()).toBe(200); else expect(response.status()).toBe(0); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 911521018e..9bdb41f8d6 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -512,7 +512,7 @@ it('should work with badly encoded server', async ({ page, server }) => { expect(response.status()).toBe(200); }); -it('should work with encoded server - 2', async ({ page, server, browserName, browserMajorVersion }) => { +it('should work with encoded server - 2', async ({ page, server, browserName }) => { // The requestWillBeSent will report URL as-is, whereas interception will // report encoded URL for stylesheet. @see crbug.com/759388 const requests = []; @@ -522,7 +522,7 @@ it('should work with encoded server - 2', async ({ page, server, browserName, br }); const response = await page.goto(`data:text/html,`); expect(response).toBe(null); - if (browserName === 'firefox' && browserMajorVersion >= 97) + if (browserName === 'firefox') expect(requests.length).toBe(2); // Firefox DevTools report to navigations in this case as well. else expect(requests.length).toBe(1); diff --git a/tests/page/wheel.spec.ts b/tests/page/wheel.spec.ts index c8c634e16d..7a1fccc2aa 100644 --- a/tests/page/wheel.spec.ts +++ b/tests/page/wheel.spec.ts @@ -22,8 +22,8 @@ it.skip(({ isAndroid }) => { let ignoreDelta = false; -it.beforeAll(async ({ browserMajorVersion, browserName, isElectron, platform }) => { - if (((browserName === 'chromium' && browserMajorVersion >= 102) || isElectron) && platform === 'darwin') { +it.beforeAll(async ({ browserName, isElectron, platform }) => { + if (((browserName === 'chromium') || isElectron) && platform === 'darwin') { // Chromium reports deltaX/deltaY scaled by host device scale factor. // https://bugs.chromium.org/p/chromium/issues/detail?id=1324819 // https://github.com/microsoft/playwright/issues/7362 From bc87467b2558505941ed6fb6b6f28251e5f29685 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Aug 2024 11:52:14 -0700 Subject: [PATCH 21/47] chore: generate simple dom descriptions in codegen (#32333) --- .../src/server/codegen/codeGenerator.ts | 1 + .../src/server/codegen/javascript.ts | 8 +- packages/playwright-core/src/server/frames.ts | 2 +- .../src/server/injected/.eslintrc.js | 27 ++-- .../src/server/injected/clock.ts | 2 +- .../src/server/injected/injectedScript.ts | 48 ++++--- .../src/server/injected/recorder/DEPS.list | 2 +- .../src/server/injected/recorder/recorder.ts | 128 ++++++++++++------ .../src/server/injected/simpleDom.ts | 73 +++++++--- .../playwright-core/src/server/recorder.ts | 16 ++- packages/recorder/src/recorderTypes.ts | 1 + packages/trace-viewer/src/ui/snapshotTab.tsx | 1 + tests/library/role-utils.spec.ts | 12 +- 13 files changed, 215 insertions(+), 106 deletions(-) diff --git a/packages/playwright-core/src/server/codegen/codeGenerator.ts b/packages/playwright-core/src/server/codegen/codeGenerator.ts index bfc640b38e..0818b247e1 100644 --- a/packages/playwright-core/src/server/codegen/codeGenerator.ts +++ b/packages/playwright-core/src/server/codegen/codeGenerator.ts @@ -27,6 +27,7 @@ export type FrameDescription = { export type ActionInContext = { frame: FrameDescription; + description?: string; action: Action; committed?: boolean; }; diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index bc0f20e97a..1d1f82bae0 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -65,7 +65,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (signals.download) formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); - formatter.add(this._generateActionCall(subject, actionInContext)); + formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext))); if (signals.popup) formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); @@ -259,3 +259,9 @@ export class JavaScriptFormatter { function quote(text: string) { return escapeWithQuotes(text, '\''); } + +function wrapWithStep(description: string | undefined, body: string) { + return description ? `await test.step(\`${description}\`, async () => { +${body} +});` : body; +} diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 3a60e796c4..9ae6560a5f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -800,7 +800,7 @@ export class Frame extends SdkObject { const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { const elements = injected.querySelectorAll(info.parsed, root || document); const element: Element | undefined = elements[0]; - const visible = element ? injected.isVisible(element) : false; + const visible = element ? injected.utils.isElementVisible(element) : false; let log = ''; if (elements.length > 1) { if (info.strict) diff --git a/packages/playwright-core/src/server/injected/.eslintrc.js b/packages/playwright-core/src/server/injected/.eslintrc.js index e96e2a9f80..eccd5b787d 100644 --- a/packages/playwright-core/src/server/injected/.eslintrc.js +++ b/packages/playwright-core/src/server/injected/.eslintrc.js @@ -1,10 +1,21 @@ +const path = require('path'); + module.exports = { - rules: { - "no-restricted-globals": [ - "error", - { "name": "window" }, - { "name": "document" }, - { "name": "globalThis" }, - ] - } + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "notice"], + parserOptions: { + ecmaVersion: 9, + sourceType: "module", + project: path.join(__dirname, '../../../../../tsconfig.json'), + }, + rules: { + "no-restricted-globals": [ + "error", + { "name": "window" }, + { "name": "document" }, + { "name": "globalThis" }, + ], + '@typescript-eslint/no-floating-promises': 'error', + "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2, + }, }; diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 414d23b958..b2daf190f3 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -216,7 +216,7 @@ export class ClockController { const sinceLastSync = now - this._realTime!.lastSyncTicks; this._realTime!.lastSyncTicks = now; // eslint-disable-next-line no-console - this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); + void this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); }, callAt - this._now.ticks), }; } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 2323648bae..c78d8d4065 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -29,11 +29,12 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser'; import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator'; import type * as channels from '@protocol/channels'; import { Highlight } from './highlight'; -import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription } from './roleUtils'; +import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, beginAriaCaches, endAriaCaches } from './roleUtils'; import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils'; import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; -import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; +import { cacheNormalizedWhitespaces, escapeHTML, escapeHTMLAttribute, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; +import { generateSimpleDom, generateSimpleDomNode, selectorForSimpleDomNodeId } from './simpleDom'; export type FrameExpectParams = Omit & { expectedValue?: any }; @@ -66,7 +67,28 @@ export class InjectedScript { // eslint-disable-next-line no-restricted-globals readonly window: Window & typeof globalThis; readonly document: Document; - readonly utils = { isInsideScope, elementText, asLocator, normalizeWhiteSpace, cacheNormalizedWhitespaces }; + + // Recorder must use any external dependencies through InjectedScript. + // Otherwise it will end up with a copy of all modules it uses, and any + // module-level globals will be duplicated, which leads to subtle bugs. + readonly utils = { + asLocator, + beginAriaCaches, + cacheNormalizedWhitespaces, + elementText, + endAriaCaches, + escapeHTML, + escapeHTMLAttribute, + generateSimpleDom: generateSimpleDom.bind(undefined, this), + generateSimpleDomNode: generateSimpleDomNode.bind(undefined, this), + getAriaRole, + getElementAccessibleDescription, + getElementAccessibleName, + isElementVisible, + isInsideScope, + normalizeWhiteSpace, + selectorForSimpleDomNodeId: selectorForSimpleDomNodeId.bind(undefined, this), + }; // eslint-disable-next-line no-restricted-globals constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) { @@ -426,10 +448,6 @@ export class InjectedScript { return new constrFunction(this, params); } - isVisible(element: Element): boolean { - return isElementVisible(element); - } - async viewportRatio(element: Element): Promise { return await new Promise(resolve => { const observer = new IntersectionObserver(entries => { @@ -567,9 +585,9 @@ export class InjectedScript { } if (state === 'visible') - return this.isVisible(element); + return isElementVisible(element); if (state === 'hidden') - return !this.isVisible(element); + return !isElementVisible(element); const disabled = getAriaDisabled(element); if (state === 'disabled') @@ -1296,18 +1314,6 @@ export class InjectedScript { } throw this.createStacklessError('Unknown expect matcher: ' + expression); } - - getElementAccessibleName(element: Element, includeHidden?: boolean): string { - return getElementAccessibleName(element, !!includeHidden); - } - - getElementAccessibleDescription(element: Element, includeHidden?: boolean): string { - return getElementAccessibleDescription(element, !!includeHidden); - } - - getAriaRole(element: Element) { - return getAriaRole(element); - } } const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); diff --git a/packages/playwright-core/src/server/injected/recorder/DEPS.list b/packages/playwright-core/src/server/injected/recorder/DEPS.list index ee39467fea..1f58b3d5d0 100644 --- a/packages/playwright-core/src/server/injected/recorder/DEPS.list +++ b/packages/playwright-core/src/server/injected/recorder/DEPS.list @@ -1,4 +1,4 @@ -# Recorder must use any external dependencies through InjectedScript. +# Recorder must use any external dependencies through injectedScript.utils. # Otherwise it will end up with a copy of all modules it uses, and any # module-level globals will be duplicated, which leads to subtle bugs. [*] diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 6e573c3c5a..95885e22d3 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -21,10 +21,11 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes'; import type { ElementText } from '../selectorUtils'; import type { Highlight, HighlightOptions } from '../highlight'; import clipPaths from './clipPaths'; +import type { SimpleDomNode } from '../simpleDom'; interface RecorderDelegate { - performAction?(action: actions.PerformOnRecordAction): Promise; - recordAction?(action: actions.Action): Promise; + performAction?(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise; + recordAction?(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise; setSelector?(selector: string): Promise; setMode?(mode: Mode): Promise; setOverlayState?(state: OverlayState): Promise; @@ -168,7 +169,7 @@ class InspectTool implements RecorderTool { if (this._hoveredModel?.tooltipListItemSelected) this._reset(true); else if (this._assertVisibility) - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); } } @@ -182,15 +183,15 @@ class InspectTool implements RecorderTool { private _commit(selector: string) { if (this._assertVisibility) { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'assertVisible', selector, signals: [], }); - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); this._recorder.overlay?.flashToolSucceeded('assertingVisibility'); } else { - this._recorder.delegate.setSelector?.(selector); + this._recorder.setSelector(selector); } } @@ -338,7 +339,7 @@ class RecordActionTool implements RecorderTool { const target = this._recorder.deepEventTarget(event); if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'setInputFiles', selector: this._activeModel!.selector, signals: [], @@ -348,7 +349,7 @@ class RecordActionTool implements RecorderTool { } if (isRangeInput(target)) { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'fill', // must use hoveredModel instead of activeModel for it to work in webkit selector: this._hoveredModel!.selector, @@ -367,7 +368,7 @@ class RecordActionTool implements RecorderTool { // Non-navigating actions are simply recorded by Playwright. if (this._consumedDueWrongTarget(event)) return; - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'fill', selector: this._activeModel!.selector, signals: [], @@ -483,26 +484,27 @@ class RecordActionTool implements RecorderTool { return true; } - private async _performAction(action: actions.PerformOnRecordAction) { + private _performAction(action: actions.PerformOnRecordAction) { this._hoveredElement = null; this._hoveredModel = null; this._activeModel = null; this._recorder.updateHighlight(null, false); this._performingAction = true; - await this._recorder.delegate.performAction?.(action).catch(() => {}); - this._performingAction = false; + void this._recorder.performAction(action).then(() => { + this._performingAction = false; - // If that was a keyboard action, it similarly requires new selectors for active model. - this._onFocus(false); + // If that was a keyboard action, it similarly requires new selectors for active model. + this._onFocus(false); - if (this._recorder.injectedScript.isUnderTest) { - // Serialize all to string as we cannot attribute console message to isolated world - // in Firefox. - console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console - hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null, - active: this._activeModel ? (this._activeModel as any).selector : null, - })); - } + if (this._recorder.injectedScript.isUnderTest) { + // Serialize all to string as we cannot attribute console message to isolated world + // in Firefox. + console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console + hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null, + active: this._activeModel ? (this._activeModel as any).selector : null, + })); + } + }); } private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean { @@ -613,7 +615,7 @@ class TextAssertionTool implements RecorderTool { onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); consumeEvent(event); } @@ -680,8 +682,8 @@ class TextAssertionTool implements RecorderTool { if (!this._action || !this._dialog.isShowing()) return; this._dialog.close(); - this._recorder.delegate.recordAction?.(this._action); - this._recorder.delegate.setMode?.('recording'); + this._recorder.recordAction(this._action); + this._recorder.setMode('recording'); } private _showDialog() { @@ -726,8 +728,8 @@ class TextAssertionTool implements RecorderTool { const action = this._generateAction(); if (!action) return; - this._recorder.delegate.recordAction?.(action); - this._recorder.delegate.setMode?.('recording'); + this._recorder.recordAction(action); + this._recorder.setMode('recording'); this._recorder.overlay?.flashToolSucceeded('assertingValue'); } } @@ -799,7 +801,7 @@ class Overlay { this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } }; }), addEventListener(this._recordToggle, 'click', () => { - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby'); + this._recorder.setMode(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby'); }), addEventListener(this._pickLocatorToggle, 'click', () => { const newMode: Record = { @@ -812,19 +814,19 @@ class Overlay { 'assertingVisibility': 'recording-inspecting', 'assertingValue': 'recording-inspecting', }; - this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]); + this._recorder.setMode(newMode[this._recorder.state.mode]); }), addEventListener(this._assertVisibilityToggle, 'click', () => { if (!this._assertVisibilityToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility'); + this._recorder.setMode(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility'); }), addEventListener(this._assertTextToggle, 'click', () => { if (!this._assertTextToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); + this._recorder.setMode(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); }), addEventListener(this._assertValuesToggle, 'click', () => { if (!this._assertValuesToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); + this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); }), ]; } @@ -890,7 +892,7 @@ class Overlay { const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10; this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX)); this._updateVisualPosition(); - this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX }); + this._recorder.setOverlayState({ offsetX: this._offsetX }); consumeEvent(event); return true; } @@ -924,9 +926,15 @@ export class Recorder { readonly highlight: Highlight; readonly overlay: Overlay | undefined; private _stylesheet: CSSStyleSheet; - state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } }; + state: UIState = { + mode: 'none', + testIdAttributeName: 'data-testid', + language: 'javascript', + overlay: { offsetX: 0 }, + generateSimpleDom: false, + }; readonly document: Document; - delegate: RecorderDelegate = {}; + private _delegate: RecorderDelegate = {}; constructor(injectedScript: InjectedScript) { this.document = injectedScript.document; @@ -994,7 +1002,7 @@ export class Recorder { } setUIState(state: UIState, delegate: RecorderDelegate) { - this.delegate = delegate; + this._delegate = delegate; if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) { // All good. @@ -1155,7 +1163,7 @@ export class Recorder { tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector); this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText }); if (userGesture) - this.delegate.highlightUpdated?.(); + this._delegate.highlightUpdated?.(); } private _ignoreOverlayEvent(event: Event) { @@ -1172,6 +1180,40 @@ export class Recorder { } return event.composedPath()[0] as HTMLElement; } + + setMode(mode: Mode) { + void this._delegate.setMode?.(mode); + } + + async performAction(action: actions.PerformOnRecordAction) { + const simpleDomNode = this._generateSimpleDomNode(action); + await this._delegate.performAction?.(action, simpleDomNode).catch(() => {}); + } + + recordAction(action: actions.Action) { + const simpleDomNode = this._generateSimpleDomNode(action); + void this._delegate.recordAction?.(action, simpleDomNode); + } + + setOverlayState(state: { offsetX: number; }) { + void this._delegate.setOverlayState?.(state); + } + + setSelector(selector: string) { + void this._delegate.setSelector?.(selector); + } + + private _generateSimpleDomNode(action: actions.Action): SimpleDomNode | undefined { + if (!this.state.generateSimpleDom) + return; + if (!('selector' in action)) + return; + + const element = this.injectedScript.querySelector(this.injectedScript.parseSelector(action.selector), this.document.documentElement, true); + if (!element) + return; + return this.injectedScript.utils.generateSimpleDomNode(element); + } } class Dialog { @@ -1361,8 +1403,8 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson): } interface Embedder { - __pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise; - __pw_recorderRecordAction(action: actions.Action): Promise; + __pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise; + __pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise; __pw_recorderState(): Promise; __pw_recorderSetSelector(selector: string): Promise; __pw_recorderSetMode(mode: Mode): Promise; @@ -1407,12 +1449,12 @@ export class PollingRecorder implements RecorderDelegate { this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); } - async performAction(action: actions.PerformOnRecordAction) { - await this._embedder.__pw_recorderPerformAction(action); + async performAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { + await this._embedder.__pw_recorderPerformAction(action, simpleDomNode); } - async recordAction(action: actions.Action): Promise { - await this._embedder.__pw_recorderRecordAction(action); + async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise { + await this._embedder.__pw_recorderRecordAction(action, simpleDomNode); } async setSelector(selector: string): Promise { diff --git a/packages/playwright-core/src/server/injected/simpleDom.ts b/packages/playwright-core/src/server/injected/simpleDom.ts index 0538dabc1e..878b8021dd 100644 --- a/packages/playwright-core/src/server/injected/simpleDom.ts +++ b/packages/playwright-core/src/server/injected/simpleDom.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils'; -import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName } from './roleUtils'; -import { isElementVisible } from './domUtils'; +import type { InjectedScript } from './injectedScript'; const leafRoles = new Set([ 'button', @@ -26,11 +24,40 @@ const leafRoles = new Set([ 'textbox', ]); -export function simpleDom(document: Document): { markup: string, elements: Map } { +export type SimpleDom = { + markup: string; + elements: Map; +}; + +export type SimpleDomNode = { + dom: SimpleDom; + id: string; + tag: string; +}; + +let lastDom: SimpleDom | undefined; + +export function generateSimpleDom(injectedScript: InjectedScript): SimpleDom { + return generate(injectedScript).dom; +} + +export function generateSimpleDomNode(injectedScript: InjectedScript, target: Element): SimpleDomNode { + return generate(injectedScript, target).node!; +} + +export function selectorForSimpleDomNodeId(injectedScript: InjectedScript, id: string): string { + const element = lastDom?.elements.get(id); + if (!element) + throw new Error(`Internal error: element with id "${id}" not found`); + return injectedScript.generateSelectorSimple(element); +} + +function generate(injectedScript: InjectedScript, target?: Element): { dom: SimpleDom, node?: SimpleDomNode } { const normalizeWhitespace = (text: string) => text.replace(/[\s\n]+/g, match => match.includes('\n') ? '\n' : ' '); const tokens: string[] = []; - const idMap = new Map(); + const elements = new Map(); let lastId = 0; + let resultTarget: { tag: string, id: string } | undefined; const visit = (node: Node) => { if (node.nodeType === Node.TEXT_NODE) { tokens.push(node.nodeValue!); @@ -41,16 +68,19 @@ export function simpleDom(document: Document): { markup: string, elements: Map${escapedTextContent}`; case 'link': return `${escapedTextContent}`; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index e4e26b0e98..23c68b7297 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -41,6 +41,7 @@ import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '. import type { Dialog } from './dialog'; import { performAction } from './recorderRunner'; import { languageSet } from './codegen/languages'; +import type { SimpleDomNode } from './injected/simpleDom'; type BindingSource = { frame: Frame, page: Page }; @@ -182,6 +183,7 @@ export class Recorder implements InstrumentationListener { language: this._currentLanguage, testIdAttributeName: this._contextRecorder.testIdAttributeName(), overlay: this._overlayState, + generateSimpleDom: false, }; return uiState; }); @@ -448,11 +450,11 @@ class ContextRecorder extends EventEmitter { // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. await this._context.exposeBinding('__pw_recorderPerformAction', false, - (source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(source.frame, action)); + (source: BindingSource, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) => this._performAction(source.frame, action, simpleDomNode)); // Other non-essential actions are simply being recorded. await this._context.exposeBinding('__pw_recorderRecordAction', false, - (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); + (source: BindingSource, action: actions.Action, simpleDomNode?: SimpleDomNode) => this._recordAction(source.frame, action, simpleDomNode)); await this._context.extendInjectedScript(recorderSource.source); } @@ -532,14 +534,15 @@ class ContextRecorder extends EventEmitter { return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; } - private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) { + private async _performAction(frame: Frame, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { // Commit last action so that no further signals are added to it. this._generator.commitLastAction(); const frameDescription = await this._describeFrame(frame); const actionInContext: ActionInContext = { frame: frameDescription, - action + action, + description: undefined, // TODO: generate description based on simple dom node. }; this._generator.willPerformAction(actionInContext); @@ -552,14 +555,15 @@ class ContextRecorder extends EventEmitter { } } - private async _recordAction(frame: Frame, action: actions.Action) { + private async _recordAction(frame: Frame, action: actions.Action, simpleDomNode?: SimpleDomNode) { // Commit last action so that no further signals are added to it. this._generator.commitLastAction(); const frameDescription = await this._describeFrame(frame); const actionInContext: ActionInContext = { frame: frameDescription, - action + action, + description: undefined, // TODO: generate description based on simple dom node. }; this._setCommittedAfterTimeout(actionInContext); this._generator.addAction(actionInContext); diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index c56984ad6d..09cb02e3e2 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -51,6 +51,7 @@ export type UIState = { language: Language; testIdAttributeName: string; overlay: OverlayState; + generateSimpleDom: boolean; }; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 4faa668677..578f787f3e 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -254,6 +254,7 @@ export const InspectModeController: React.FunctionComponent<{ language: sdkLanguage, testIdAttributeName, overlay: { offsetX: 0 }, + generateSimpleDom: false, }, { async setSelector(selector: string) { setHighlightedLocator(asLocator(sdkLanguage, frameSelector + selector)); diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 6c45686f68..a02680ce86 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -22,8 +22,8 @@ test.skip(({ mode }) => mode !== 'default'); async function getNameAndRole(page: Page, selector: string) { return await page.$eval(selector, e => { - const name = (window as any).__injectedScript.getElementAccessibleName(e); - const role = (window as any).__injectedScript.getAriaRole(e); + const name = (window as any).__injectedScript.utils.getElementAccessibleName(e); + const role = (window as any).__injectedScript.utils.getAriaRole(e); return { name, role }; }); } @@ -89,7 +89,7 @@ for (let range = 0; range <= ranges.length; range++) { if (!element) throw new Error(`Unable to resolve "${step.selector}"`); const injected = (window as any).__injectedScript; - const received = step.property === 'name' ? injected.getElementAccessibleName(element) : injected.getElementAccessibleDescription(element); + const received = step.property === 'name' ? injected.utils.getElementAccessibleName(element) : injected.utils.getElementAccessibleDescription(element); result.push({ selector: step.selector, expected: step.value, received }); } return result; @@ -152,7 +152,7 @@ test('wpt accname non-manual', async ({ page, asset, server }) => { const injected = (window as any).__injectedScript; const title = element.getAttribute('data-testname'); const expected = element.getAttribute('data-expectedlabel'); - const received = injected.getElementAccessibleName(element); + const received = injected.utils.getElementAccessibleName(element); result.push({ title, expected, received }); } return result; @@ -180,7 +180,7 @@ test('axe-core implicit-role', async ({ page, asset, server }) => { const element = document.querySelector(selector); if (!element) throw new Error(`Unable to resolve "${selector}"`); - return (window as any).__injectedScript.getAriaRole(element); + return (window as any).__injectedScript.utils.getAriaRole(element); }, testCase.target); expect.soft(received, `checking ${JSON.stringify(testCase)}`).toBe(testCase.role); }); @@ -213,7 +213,7 @@ test('axe-core accessible-text', async ({ page, asset, server }) => { const element = injected.querySelector(injected.parseSelector('css=' + selector), document, false); if (!element) throw new Error(`Unable to resolve "${selector}"`); - return injected.getElementAccessibleName(element); + return injected.utils.getElementAccessibleName(element); }); }, targets); expect.soft(received, `checking ${JSON.stringify(testCase)}`).toEqual(expected); From a1df11011c57128c7f6b5d73a99ded4139a9617e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Aug 2024 14:10:21 -0700 Subject: [PATCH 22/47] chore: split recorder into files (#32345) --- packages/playwright-core/src/server/DEPS.list | 7 - .../src/server/codegen/csharp.ts | 3 +- .../src/server/codegen/java.ts | 3 +- .../src/server/codegen/javascript.ts | 3 +- .../src/server/codegen/jsonl.ts | 3 +- .../src/server/codegen/language.ts | 31 +- .../src/server/codegen/python.ts | 3 +- .../src/server/codegen/types.ts | 50 +++ .../playwright-core/src/server/recorder.ts | 347 +----------------- .../src/server/recorder/DEPS.list | 3 + .../src/server/recorder/contextRecorder.ts | 324 ++++++++++++++++ .../recorderCollection.ts} | 39 +- .../server/{ => recorder}/recorderRunner.ts | 10 +- .../src/server/recorder/throttledFile.ts | 43 +++ 14 files changed, 460 insertions(+), 409 deletions(-) create mode 100644 packages/playwright-core/src/server/codegen/types.ts create mode 100644 packages/playwright-core/src/server/recorder/contextRecorder.ts rename packages/playwright-core/src/server/{codegen/codeGenerator.ts => recorder/recorderCollection.ts} (75%) rename packages/playwright-core/src/server/{ => recorder}/recorderRunner.ts (95%) create mode 100644 packages/playwright-core/src/server/recorder/throttledFile.ts diff --git a/packages/playwright-core/src/server/DEPS.list b/packages/playwright-core/src/server/DEPS.list index 0e2b8301d6..bc32bb8486 100644 --- a/packages/playwright-core/src/server/DEPS.list +++ b/packages/playwright-core/src/server/DEPS.list @@ -20,10 +20,3 @@ ./electron/ ./firefox/ ./webkit/ - -[recorder.ts] -./codegen/codeGenerator.ts -./codegen/languages.ts - -[recorderRunner.ts] -./codegen/language.ts diff --git a/packages/playwright-core/src/server/codegen/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts index 41f91d259b..f11435a0c2 100644 --- a/packages/playwright-core/src/server/codegen/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -15,8 +15,7 @@ */ import type { BrowserContextOptions } from '../../../types/types'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { escapeWithQuotes, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; diff --git a/packages/playwright-core/src/server/codegen/java.ts b/packages/playwright-core/src/server/codegen/java.ts index a59546f7ba..47c6fa3619 100644 --- a/packages/playwright-core/src/server/codegen/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -16,8 +16,7 @@ import type { BrowserContextOptions } from '../../../types/types'; import type * as types from '../types'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index 1d1f82bae0..1c1ba3f1cb 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -15,8 +15,7 @@ */ import type { BrowserContextOptions } from '../../../types/types'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { escapeWithQuotes, asLocator } from '../../utils'; diff --git a/packages/playwright-core/src/server/codegen/jsonl.ts b/packages/playwright-core/src/server/codegen/jsonl.ts index 108d5eadc6..78485297b6 100644 --- a/packages/playwright-core/src/server/codegen/jsonl.ts +++ b/packages/playwright-core/src/server/codegen/jsonl.ts @@ -15,8 +15,7 @@ */ import { asLocator } from '../../utils'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; export class JsonlLanguageGenerator implements LanguageGenerator { id = 'jsonl'; diff --git a/packages/playwright-core/src/server/codegen/language.ts b/packages/playwright-core/src/server/codegen/language.ts index 78414733d7..72cfb9083d 100644 --- a/packages/playwright-core/src/server/codegen/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -14,32 +14,17 @@ * limitations under the License. */ -import type { BrowserContextOptions, LaunchOptions } from '../../..'; -import type { Language } from '../../utils'; +import type { BrowserContextOptions } from '../../..'; import type * as actions from '../recorder/recorderActions'; import type * as types from '../types'; -import type { ActionInContext } from './codeGenerator'; -export type { Language } from '../../utils'; +import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; -export type LanguageGeneratorOptions = { - browserName: string; - launchOptions: LaunchOptions; - contextOptions: BrowserContextOptions; - deviceName?: string; - saveStorage?: string; -}; - -export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text'; -export type LocatorBase = 'page' | 'locator' | 'frame-locator'; - -export interface LanguageGenerator { - id: string; - groupName: string; - name: string; - highlighter: Language; - generateHeader(options: LanguageGeneratorOptions): string; - generateAction(actionInContext: ActionInContext): string; - generateFooter(saveStorage: string | undefined): string; +export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { + const header = languageGenerator.generateHeader(options); + const footer = languageGenerator.generateFooter(options.saveStorage); + const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); + const text = [header, ...actionTexts, footer].join('\n'); + return { header, footer, actionTexts, text }; } export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions { diff --git a/packages/playwright-core/src/server/codegen/python.ts b/packages/playwright-core/src/server/codegen/python.ts index 98949320e7..6ed101bcf0 100644 --- a/packages/playwright-core/src/server/codegen/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -15,8 +15,7 @@ */ import type { BrowserContextOptions } from '../../../types/types'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; diff --git a/packages/playwright-core/src/server/codegen/types.ts b/packages/playwright-core/src/server/codegen/types.ts new file mode 100644 index 0000000000..96f2aa85d1 --- /dev/null +++ b/packages/playwright-core/src/server/codegen/types.ts @@ -0,0 +1,50 @@ +/** + * 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 { BrowserContextOptions, LaunchOptions } from '../../../types/types'; +import type * as actions from '../recorder/recorderActions'; +import type { Language } from '../../utils'; +export type { Language } from '../../utils'; + +export type LanguageGeneratorOptions = { + browserName: string; + launchOptions: LaunchOptions; + contextOptions: BrowserContextOptions; + deviceName?: string; + saveStorage?: string; +}; + +export type FrameDescription = { + pageAlias: string; + framePath: string[]; +}; + +export type ActionInContext = { + frame: FrameDescription; + description?: string; + action: actions.Action; + committed?: boolean; +}; + +export interface LanguageGenerator { + id: string; + groupName: string; + name: string; + highlighter: Language; + generateHeader(options: LanguageGeneratorOptions): string; + generateAction(actionInContext: ActionInContext): string; + generateFooter(saveStorage: string | undefined): string; +} diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 23c68b7297..17c38187e8 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -14,36 +14,21 @@ * limitations under the License. */ -import * as fs from 'fs'; -import type * as actions from './recorder/recorderActions'; import type * as channels from '@protocol/channels'; -import type { ActionInContext, FrameDescription } from './codegen/codeGenerator'; -import { CodeGenerator } from './codegen/codeGenerator'; -import { Page } from './page'; -import { Frame } from './frames'; -import { BrowserContext } from './browserContext'; -import * as recorderSource from '../generated/recorderSource'; -import * as consoleApiSource from '../generated/consoleApiSource'; -import { EmptyRecorderApp } from './recorder/recorderApp'; -import type { IRecorderApp } from './recorder/recorderApp'; -import { RecorderApp } from './recorder/recorderApp'; -import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; -import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; -import { isUnderTest, monotonicTime } from '../utils'; -import { metadataToCallLog } from './recorder/recorderUtils'; -import { Debugger } from './debugger'; -import { EventEmitter } from 'events'; -import { raceAgainstDeadline } from '../utils/timeoutRunner'; -import { type Language, type LanguageGenerator } from './codegen/language'; +import * as fs from 'fs'; +import type { Point } from '../common/types'; +import * as consoleApiSource from '../generated/consoleApiSource'; +import { isUnderTest } from '../utils'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; -import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils'; -import type { Dialog } from './dialog'; -import { performAction } from './recorderRunner'; -import { languageSet } from './codegen/languages'; -import type { SimpleDomNode } from './injected/simpleDom'; - -type BindingSource = { frame: Frame, page: Page }; +import { BrowserContext } from './browserContext'; +import { type Language } from './codegen/types'; +import { Debugger } from './debugger'; +import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; +import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; +import type { IRecorderApp } from './recorder/recorderApp'; +import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp'; +import { metadataToCallLog } from './recorder/recorderUtils'; const recorderSymbol = Symbol('recorderSymbol'); @@ -361,245 +346,8 @@ export class Recorder implements InstrumentationListener { } } -class ContextRecorder extends EventEmitter { - static Events = { - Change: 'change' - }; - - private _generator: CodeGenerator; - private _pageAliases = new Map(); - private _lastPopupOrdinal = 0; - private _lastDialogOrdinal = -1; - private _lastDownloadOrdinal = -1; - private _timers = new Set(); - private _context: BrowserContext; - private _params: channels.BrowserContextRecorderSupplementEnableParams; - private _recorderSources: Source[]; - private _throttledOutputFile: ThrottledFile | null = null; - private _orderedLanguages: LanguageGenerator[] = []; - private _listeners: RegisteredListener[] = []; - - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { - super(); - this._context = context; - this._params = params; - this._recorderSources = []; - const language = params.language || context.attribution.playwright.options.sdkLanguage; - this.setOutput(language, params.outputFile); - const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage); - generator.on('change', () => { - this._recorderSources = []; - for (const languageGenerator of this._orderedLanguages) { - const { header, footer, actions, text } = generator.generateStructure(languageGenerator); - const source: Source = { - isRecorded: true, - label: languageGenerator.name, - group: languageGenerator.groupName, - id: languageGenerator.id, - text, - header, - footer, - actions, - language: languageGenerator.highlighter, - highlight: [] - }; - source.revealLine = text.split('\n').length - 1; - this._recorderSources.push(source); - if (languageGenerator === this._orderedLanguages[0]) - this._throttledOutputFile?.setContent(source.text); - } - this.emit(ContextRecorder.Events.Change, { - sources: this._recorderSources, - primaryFileName: this._orderedLanguages[0].id - }); - }); - context.on(BrowserContext.Events.BeforeClose, () => { - this._throttledOutputFile?.flush(); - }); - this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { - this._throttledOutputFile?.flush(); - })); - this._generator = generator; - } - - setOutput(codegenId: string, outputFile?: string) { - const languages = languageSet(); - const primaryLanguage = [...languages].find(l => l.id === codegenId); - if (!primaryLanguage) - throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); - languages.delete(primaryLanguage); - this._orderedLanguages = [primaryLanguage, ...languages]; - this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null; - this._generator?.restart(); - } - - languageName(id?: string): Language { - for (const lang of this._orderedLanguages) { - if (!id || lang.id === id) - return lang.highlighter; - } - return 'javascript'; - } - - async install() { - this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); - for (const page of this._context.pages()) - this._onPage(page); - this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); - - // Input actions that potentially lead to navigation are intercepted on the page and are - // performed by the Playwright. - await this._context.exposeBinding('__pw_recorderPerformAction', false, - (source: BindingSource, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) => this._performAction(source.frame, action, simpleDomNode)); - - // Other non-essential actions are simply being recorded. - await this._context.exposeBinding('__pw_recorderRecordAction', false, - (source: BindingSource, action: actions.Action, simpleDomNode?: SimpleDomNode) => this._recordAction(source.frame, action, simpleDomNode)); - - await this._context.extendInjectedScript(recorderSource.source); - } - - setEnabled(enabled: boolean) { - this._generator.setEnabled(enabled); - } - - dispose() { - for (const timer of this._timers) - clearTimeout(timer); - this._timers.clear(); - eventsHelper.removeEventListeners(this._listeners); - } - - private async _onPage(page: Page) { - // First page is called page, others are called popup1, popup2, etc. - const frame = page.mainFrame(); - page.on('close', () => { - this._generator.addAction({ - frame: this._describeMainFrame(page), - committed: true, - action: { - name: 'closePage', - signals: [], - } - }); - this._pageAliases.delete(page); - }); - frame.on(Frame.Events.InternalNavigation, event => { - if (event.isPublic) - this._onFrameNavigated(frame, page); - }); - page.on(Page.Events.Download, () => this._onDownload(page)); - const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; - const pageAlias = 'page' + suffix; - this._pageAliases.set(page, pageAlias); - - if (page.opener()) { - this._onPopup(page.opener()!, page); - } else { - this._generator.addAction({ - frame: this._describeMainFrame(page), - committed: true, - action: { - name: 'openPage', - url: page.mainFrame().url(), - signals: [], - } - }); - } - } - - clearScript(): void { - this._generator.restart(); - if (this._params.mode === 'recording') { - for (const page of this._context.pages()) - this._onFrameNavigated(page.mainFrame(), page); - } - } - - private _describeMainFrame(page: Page): FrameDescription { - return { - pageAlias: this._pageAliases.get(page)!, - framePath: [], - }; - } - - private async _describeFrame(frame: Frame): Promise { - return { - pageAlias: this._pageAliases.get(frame._page)!, - framePath: await generateFrameSelector(frame), - }; - } - - testIdAttributeName(): string { - return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; - } - - private async _performAction(frame: Frame, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { - // Commit last action so that no further signals are added to it. - this._generator.commitLastAction(); - - const frameDescription = await this._describeFrame(frame); - const actionInContext: ActionInContext = { - frame: frameDescription, - action, - description: undefined, // TODO: generate description based on simple dom node. - }; - - this._generator.willPerformAction(actionInContext); - const success = await performAction(frame, action); - if (success) { - this._generator.didPerformAction(actionInContext); - this._setCommittedAfterTimeout(actionInContext); - } else { - this._generator.performedActionFailed(actionInContext); - } - } - - private async _recordAction(frame: Frame, action: actions.Action, simpleDomNode?: SimpleDomNode) { - // Commit last action so that no further signals are added to it. - this._generator.commitLastAction(); - - const frameDescription = await this._describeFrame(frame); - const actionInContext: ActionInContext = { - frame: frameDescription, - action, - description: undefined, // TODO: generate description based on simple dom node. - }; - this._setCommittedAfterTimeout(actionInContext); - this._generator.addAction(actionInContext); - } - - private _setCommittedAfterTimeout(actionInContext: ActionInContext) { - const timer = setTimeout(() => { - // Commit the action after 5 seconds so that no further signals are added to it. - actionInContext.committed = true; - this._timers.delete(timer); - }, isUnderTest() ? 500 : 5000); - this._timers.add(timer); - } - - private _onFrameNavigated(frame: Frame, page: Page) { - const pageAlias = this._pageAliases.get(page); - this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() }); - } - - private _onPopup(page: Page, popup: Page) { - const pageAlias = this._pageAliases.get(page)!; - const popupAlias = this._pageAliases.get(popup)!; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); - } - - private _onDownload(page: Page) { - const pageAlias = this._pageAliases.get(page)!; - ++this._lastDownloadOrdinal; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' }); - } - - private _onDialog(page: Page) { - const pageAlias = this._pageAliases.get(page)!; - ++this._lastDialogOrdinal; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' }); - } +function isScreenshotCommand(metadata: CallMetadata) { + return metadata.method.toLowerCase().includes('screenshot'); } function languageForFile(file: string) { @@ -611,70 +359,3 @@ function languageForFile(file: string) { return 'csharp'; return 'javascript'; } - -class ThrottledFile { - private _file: string; - private _timer: NodeJS.Timeout | undefined; - private _text: string | undefined; - - constructor(file: string) { - this._file = file; - } - - setContent(text: string) { - this._text = text; - if (!this._timer) - this._timer = setTimeout(() => this.flush(), 250); - } - - flush(): void { - if (this._timer) { - clearTimeout(this._timer); - this._timer = undefined; - } - if (this._text) - fs.writeFileSync(this._file, this._text); - this._text = undefined; - } -} - -function isScreenshotCommand(metadata: CallMetadata) { - return metadata.method.toLowerCase().includes('screenshot'); -} - -async function generateFrameSelector(frame: Frame): Promise { - const selectorPromises: Promise[] = []; - while (frame) { - const parent = frame.parentFrame(); - if (!parent) - break; - selectorPromises.push(generateFrameSelectorInParent(parent, frame)); - frame = parent; - } - const result = await Promise.all(selectorPromises); - return result.reverse(); -} - -async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promise { - const result = await raceAgainstDeadline(async () => { - try { - const frameElement = await frame.frameElement(); - if (!frameElement || !parent) - return; - const utility = await parent._utilityContext(); - const injected = await utility.injectedScript(); - const selector = await injected.evaluate((injected, element) => { - return injected.generateSelectorSimple(element as Element); - }, frameElement); - return selector; - } catch (e) { - return e.toString(); - } - }, monotonicTime() + 2000); - if (!result.timedOut && result.result) - return result.result; - - if (frame.name()) - return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; - return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; -} diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index 69c4226c68..22ec3dfc2f 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -1,8 +1,11 @@ [*] ../ +../codegen/language.ts +../codegen/languages.ts ../isomorphic/** ../registry/** ../../common/ +../../generated/recorderSource.ts ../../protocol/ ../../utils/** ../../utilsBundle.ts diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts new file mode 100644 index 0000000000..72f972e6b5 --- /dev/null +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -0,0 +1,324 @@ +/** + * 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 * as channels from '@protocol/channels'; +import type { Source } from '@recorder/recorderTypes'; +import { EventEmitter } from 'events'; +import * as recorderSource from '../../generated/recorderSource'; +import { eventsHelper, isUnderTest, monotonicTime, quoteCSSAttributeValue, type RegisteredListener } from '../../utils'; +import { raceAgainstDeadline } from '../../utils/timeoutRunner'; +import { BrowserContext } from '../browserContext'; +import type { ActionInContext, FrameDescription, LanguageGeneratorOptions, Language, LanguageGenerator } from '../codegen/types'; +import { languageSet } from '../codegen/languages'; +import type { Dialog } from '../dialog'; +import { Frame } from '../frames'; +import type { SimpleDomNode } from '../injected/simpleDom'; +import { Page } from '../page'; +import type * as actions from './recorderActions'; +import { performAction } from './recorderRunner'; +import { ThrottledFile } from './throttledFile'; +import { RecorderCollection } from './recorderCollection'; +import { generateCode } from '../codegen/language'; + +type BindingSource = { frame: Frame, page: Page }; + +export class ContextRecorder extends EventEmitter { + static Events = { + Change: 'change' + }; + + private _collection: RecorderCollection; + private _pageAliases = new Map(); + private _lastPopupOrdinal = 0; + private _lastDialogOrdinal = -1; + private _lastDownloadOrdinal = -1; + private _timers = new Set(); + private _context: BrowserContext; + private _params: channels.BrowserContextRecorderSupplementEnableParams; + private _recorderSources: Source[]; + private _throttledOutputFile: ThrottledFile | null = null; + private _orderedLanguages: LanguageGenerator[] = []; + private _listeners: RegisteredListener[] = []; + + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { + super(); + this._context = context; + this._params = params; + this._recorderSources = []; + const language = params.language || context.attribution.playwright.options.sdkLanguage; + this.setOutput(language, params.outputFile); + + // Make a copy of options to modify them later. + const languageGeneratorOptions: LanguageGeneratorOptions = { + browserName: context._browser.options.name, + launchOptions: { headless: false, ...params.launchOptions }, + contextOptions: { ...params.contextOptions }, + deviceName: params.device, + saveStorage: params.saveStorage, + }; + + const collection = new RecorderCollection(params.mode === 'recording'); + collection.on('change', () => { + this._recorderSources = []; + for (const languageGenerator of this._orderedLanguages) { + const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions); + const source: Source = { + isRecorded: true, + label: languageGenerator.name, + group: languageGenerator.groupName, + id: languageGenerator.id, + text, + header, + footer, + actions: actionTexts, + language: languageGenerator.highlighter, + highlight: [] + }; + source.revealLine = text.split('\n').length - 1; + this._recorderSources.push(source); + if (languageGenerator === this._orderedLanguages[0]) + this._throttledOutputFile?.setContent(source.text); + } + this.emit(ContextRecorder.Events.Change, { + sources: this._recorderSources, + primaryFileName: this._orderedLanguages[0].id + }); + }); + context.on(BrowserContext.Events.BeforeClose, () => { + this._throttledOutputFile?.flush(); + }); + this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { + this._throttledOutputFile?.flush(); + })); + this._collection = collection; + } + + setOutput(codegenId: string, outputFile?: string) { + const languages = languageSet(); + const primaryLanguage = [...languages].find(l => l.id === codegenId); + if (!primaryLanguage) + throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); + languages.delete(primaryLanguage); + this._orderedLanguages = [primaryLanguage, ...languages]; + this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null; + this._collection?.restart(); + } + + languageName(id?: string): Language { + for (const lang of this._orderedLanguages) { + if (!id || lang.id === id) + return lang.highlighter; + } + return 'javascript'; + } + + async install() { + this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); + for (const page of this._context.pages()) + this._onPage(page); + this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); + + // Input actions that potentially lead to navigation are intercepted on the page and are + // performed by the Playwright. + await this._context.exposeBinding('__pw_recorderPerformAction', false, + (source: BindingSource, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) => this._performAction(source.frame, action, simpleDomNode)); + + // Other non-essential actions are simply being recorded. + await this._context.exposeBinding('__pw_recorderRecordAction', false, + (source: BindingSource, action: actions.Action, simpleDomNode?: SimpleDomNode) => this._recordAction(source.frame, action, simpleDomNode)); + + await this._context.extendInjectedScript(recorderSource.source); + } + + setEnabled(enabled: boolean) { + this._collection.setEnabled(enabled); + } + + dispose() { + for (const timer of this._timers) + clearTimeout(timer); + this._timers.clear(); + eventsHelper.removeEventListeners(this._listeners); + } + + private async _onPage(page: Page) { + // First page is called page, others are called popup1, popup2, etc. + const frame = page.mainFrame(); + page.on('close', () => { + this._collection.addAction({ + frame: this._describeMainFrame(page), + committed: true, + action: { + name: 'closePage', + signals: [], + } + }); + this._pageAliases.delete(page); + }); + frame.on(Frame.Events.InternalNavigation, event => { + if (event.isPublic) + this._onFrameNavigated(frame, page); + }); + page.on(Page.Events.Download, () => this._onDownload(page)); + const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; + const pageAlias = 'page' + suffix; + this._pageAliases.set(page, pageAlias); + + if (page.opener()) { + this._onPopup(page.opener()!, page); + } else { + this._collection.addAction({ + frame: this._describeMainFrame(page), + committed: true, + action: { + name: 'openPage', + url: page.mainFrame().url(), + signals: [], + } + }); + } + } + + clearScript(): void { + this._collection.restart(); + if (this._params.mode === 'recording') { + for (const page of this._context.pages()) + this._onFrameNavigated(page.mainFrame(), page); + } + } + + private _describeMainFrame(page: Page): FrameDescription { + return { + pageAlias: this._pageAliases.get(page)!, + framePath: [], + }; + } + + private async _describeFrame(frame: Frame): Promise { + return { + pageAlias: this._pageAliases.get(frame._page)!, + framePath: await generateFrameSelector(frame), + }; + } + + testIdAttributeName(): string { + return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; + } + + private async _performAction(frame: Frame, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { + // Commit last action so that no further signals are added to it. + this._collection.commitLastAction(); + + const frameDescription = await this._describeFrame(frame); + const actionInContext: ActionInContext = { + frame: frameDescription, + action, + description: undefined, // TODO: generate description based on simple dom node. + }; + + this._collection.willPerformAction(actionInContext); + const success = await performAction(frame, action); + if (success) { + this._collection.didPerformAction(actionInContext); + this._setCommittedAfterTimeout(actionInContext); + } else { + this._collection.performedActionFailed(actionInContext); + } + } + + private async _recordAction(frame: Frame, action: actions.Action, simpleDomNode?: SimpleDomNode) { + // Commit last action so that no further signals are added to it. + this._collection.commitLastAction(); + + const frameDescription = await this._describeFrame(frame); + const actionInContext: ActionInContext = { + frame: frameDescription, + action, + description: undefined, // TODO: generate description based on simple dom node. + }; + this._setCommittedAfterTimeout(actionInContext); + this._collection.addAction(actionInContext); + } + + private _setCommittedAfterTimeout(actionInContext: ActionInContext) { + const timer = setTimeout(() => { + // Commit the action after 5 seconds so that no further signals are added to it. + actionInContext.committed = true; + this._timers.delete(timer); + }, isUnderTest() ? 500 : 5000); + this._timers.add(timer); + } + + private _onFrameNavigated(frame: Frame, page: Page) { + const pageAlias = this._pageAliases.get(page); + this._collection.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() }); + } + + private _onPopup(page: Page, popup: Page) { + const pageAlias = this._pageAliases.get(page)!; + const popupAlias = this._pageAliases.get(popup)!; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); + } + + private _onDownload(page: Page) { + const pageAlias = this._pageAliases.get(page)!; + ++this._lastDownloadOrdinal; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' }); + } + + private _onDialog(page: Page) { + const pageAlias = this._pageAliases.get(page)!; + ++this._lastDialogOrdinal; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' }); + } +} + +export async function generateFrameSelector(frame: Frame): Promise { + const selectorPromises: Promise[] = []; + while (frame) { + const parent = frame.parentFrame(); + if (!parent) + break; + selectorPromises.push(generateFrameSelectorInParent(parent, frame)); + frame = parent; + } + const result = await Promise.all(selectorPromises); + return result.reverse(); +} + +async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promise { + const result = await raceAgainstDeadline(async () => { + try { + const frameElement = await frame.frameElement(); + if (!frameElement || !parent) + return; + const utility = await parent._utilityContext(); + const injected = await utility.injectedScript(); + const selector = await injected.evaluate((injected, element) => { + return injected.generateSelectorSimple(element as Element); + }, frameElement); + return selector; + } catch (e) { + return e.toString(); + } + }, monotonicTime() + 2000); + if (!result.timedOut && result.result) + return result.result; + + if (frame.name()) + return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; + return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; +} diff --git a/packages/playwright-core/src/server/codegen/codeGenerator.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts similarity index 75% rename from packages/playwright-core/src/server/codegen/codeGenerator.ts rename to packages/playwright-core/src/server/recorder/recorderCollection.ts index 0818b247e1..29da778ffb 100644 --- a/packages/playwright-core/src/server/codegen/codeGenerator.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -15,38 +15,19 @@ */ import { EventEmitter } from 'events'; -import type { BrowserContextOptions, LaunchOptions } from '../../../types/types'; import type { Frame } from '../frames'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; -import type { Action, Signal } from '../recorder/recorderActions'; +import type { Signal } from './recorderActions'; +import type { ActionInContext } from '../codegen/types'; -export type FrameDescription = { - pageAlias: string; - framePath: string[]; -}; - -export type ActionInContext = { - frame: FrameDescription; - description?: string; - action: Action; - committed?: boolean; -}; - -export class CodeGenerator extends EventEmitter { +export class RecorderCollection extends EventEmitter { private _currentAction: ActionInContext | null = null; private _lastAction: ActionInContext | null = null; private _actions: ActionInContext[] = []; private _enabled: boolean; - private _options: LanguageGeneratorOptions; - constructor(browserName: string, enabled: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) { + constructor(enabled: boolean) { super(); - - // Make a copy of options to modify them later. - launchOptions = { headless: false, ...launchOptions }; - contextOptions = { ...contextOptions }; this._enabled = enabled; - this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage }; this.restart(); } @@ -57,6 +38,10 @@ export class CodeGenerator extends EventEmitter { this.emit('change'); } + actions() { + return this._actions; + } + setEnabled(enabled: boolean) { this._enabled = enabled; } @@ -163,12 +148,4 @@ export class CodeGenerator extends EventEmitter { }); } } - - generateStructure(languageGenerator: LanguageGenerator) { - const header = languageGenerator.generateHeader(this._options); - const footer = languageGenerator.generateFooter(this._options.saveStorage); - const actions = this._actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); - const text = [header, ...actions, footer].join('\n'); - return { header, footer, actions, text }; - } } diff --git a/packages/playwright-core/src/server/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts similarity index 95% rename from packages/playwright-core/src/server/recorderRunner.ts rename to packages/playwright-core/src/server/recorder/recorderRunner.ts index 4058ae4053..beb74c5a6d 100644 --- a/packages/playwright-core/src/server/recorderRunner.ts +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { createGuid, monotonicTime, serializeExpectedTextValues } from '../utils'; -import { toClickOptions, toKeyboardModifiers } from './codegen/language'; -import type { Frame } from './frames'; -import type { CallMetadata } from './instrumentation'; -import type * as actions from './recorder/recorderActions'; +import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils'; +import { toClickOptions, toKeyboardModifiers } from '../codegen/language'; +import type { Frame } from '../frames'; +import type { CallMetadata } from '../instrumentation'; +import type * as actions from './recorderActions'; async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { const callMetadata: CallMetadata = { diff --git a/packages/playwright-core/src/server/recorder/throttledFile.ts b/packages/playwright-core/src/server/recorder/throttledFile.ts new file mode 100644 index 0000000000..4a34f41a0c --- /dev/null +++ b/packages/playwright-core/src/server/recorder/throttledFile.ts @@ -0,0 +1,43 @@ +/** + * 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 fs from 'fs'; + +export class ThrottledFile { + private _file: string; + private _timer: NodeJS.Timeout | undefined; + private _text: string | undefined; + + constructor(file: string) { + this._file = file; + } + + setContent(text: string) { + this._text = text; + if (!this._timer) + this._timer = setTimeout(() => this.flush(), 250); + } + + flush(): void { + if (this._timer) { + clearTimeout(this._timer); + this._timer = undefined; + } + if (this._text) + fs.writeFileSync(this._file, this._text); + this._text = undefined; + } +} From 0fd97cb9ed6d90e224f8ad8124704b69045c67a8 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 27 Aug 2024 14:53:27 -0700 Subject: [PATCH 23/47] tests: delete flaky COOP test (#32346) The scenario that the test covers is inherently racy and has been flaky in all browsers. Fixes https://github.com/microsoft/playwright/issues/32107 --- tests/page/page-goto.spec.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 944b7de87b..ccab5abeba 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -179,27 +179,6 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, expect(firstRequest.url()).toBe(server.PREFIX + '/redirect'); }); -it('should properly cancel Cross-Origin-Opener-Policy navigation', { - annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32107' }, -}, async ({ page, server, browserName, isLinux, headless }) => { - it.fixme(browserName === 'webkit' && isLinux, 'Started failing after https://commits.webkit.org/281488@main'); - it.fixme(browserName === 'chromium' && headless, 'COOP navigation cancels the one that starts later'); - server.setRoute('/empty.html', (req, res) => { - res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); - res.end(); - }); - const requestPromise = page.waitForRequest(server.EMPTY_PAGE); - page.goto(server.EMPTY_PAGE).catch(() => {}); - await new Promise(f => setTimeout(f, 50)); - // Non COOP response. - await page.goto(server.CROSS_PROCESS_PREFIX + '/error.html'); - const req = await requestPromise; - const response = await Promise.race([req.response(), new Promise(f => setTimeout(() => f('timeout'), 5_000))]); - // First navigation request should either receive response or be canceled by the second - // navigation, but never hang unresolved. - expect(response).not.toBe('timeout'); -}); - it('should capture iframe navigation request', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); expect(page.url()).toBe(server.EMPTY_PAGE); From acd2a4ddade8b38eec5e967e31493104ca26d25e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 27 Aug 2024 17:04:53 -0700 Subject: [PATCH 24/47] docs: global beforeEach/beforeAll hooks (#32348) Fixes https://github.com/microsoft/playwright/issues/9468 --- docs/src/test-fixtures-js.md | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index a2e65e8849..f8c5cabc58 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -722,3 +722,63 @@ export const test = base.extend({ }, { title: 'my fixture' }], }); ``` + +## Adding global beforeEach/afterEach hooks + +[`method: Test.beforeEach`] and [`method: Test.afterEach`] hooks run before/after each test declared in the same file and same [`method: Test.describe`] block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this: + +```ts title="fixtures.ts" +import { test as base } from '@playwright/test'; + +export const test = base.extend({ + forEachTest: [async ({ page, baseURL }, use) => { + // This code runs before every test. + await page.goto('http://localhost:8000'); + await use(); + // This code runs after every test. + console.log('Last URL:', page.url()); + }, { auto: true }], // automatically starts for every test. +}); +``` + +And then import the fixtures in all your tests: + +```ts title="mytest.spec.ts" +import { test } from './fixtures'; +import { expect } from '@playwright/test'; + +test('basic', async ({ page, baseURL }) => { + expect(page).toHaveURL(baseURL!); +}); +``` + +## Adding global beforeAll/afterAll hooks + +[`method: Test.beforeAll`] and [`method: Test.afterAll`] hooks run before/after all tests declared in the same file and same [`method: Test.describe`] block (if any), once per worker process. If you want to declare hooks +that run before/after all tests in every file, you can declare them as auto fixtures with `scope: 'worker'` as follows: + +```ts title="fixtures.ts" +import { test as base } from '@playwright/test'; + +export const test = base.extend({ + forEachWorker: [async ({}, use) => { + // This code runs before all the tests in the worker process. + console.log(`Starting test worker ${test.info().workerIndex}`); + await use(); + // This code runs after all the tests in the worker process. + console.log(`Stopping test worker ${test.info().workerIndex}`); + }, { scope: 'worker', auto: true }], // automatically starts for every worker. +}); +``` + +And then import the fixtures in all your tests: + +```ts title="mytest.spec.ts" +import { test } from './fixtures'; +import { expect } from '@playwright/test'; + +test('basic', async ({ }) => { + // ... +}); +``` +Note that the fixtures will still run once per [worker process](./test-parallel.md#worker-processes), but you don't need to redeclare them in every file. From 0b5456d00b61435e5141445cf691846d731ce5f1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Aug 2024 17:17:57 -0700 Subject: [PATCH 25/47] chore: perform action based on frame path (#32347) --- .../playwright-core/src/server/recorder.ts | 5 +- .../src/server/recorder/contextRecorder.ts | 2 +- .../src/server/recorder/recorderActions.ts | 34 ++++----- .../src/server/recorder/recorderRunner.ts | 72 +++++++++++-------- .../src/server/recorder/recorderUtils.ts | 4 ++ 5 files changed, 63 insertions(+), 54 deletions(-) diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 17c38187e8..857df392b3 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -28,7 +28,7 @@ import type { CallMetadata, InstrumentationListener, SdkObject } from './instrum import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; import type { IRecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp'; -import { metadataToCallLog } from './recorder/recorderUtils'; +import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils'; const recorderSymbol = Symbol('recorderSymbol'); @@ -175,8 +175,7 @@ export class Recorder implements InstrumentationListener { await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => { const selectorChain = await generateFrameSelector(frame); - selectorChain.push(selector); - await this._recorderApp?.setSelector(selectorChain.join(' >> internal:control=enter-frame >> '), true); + await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true); }); await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => { diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 72f972e6b5..0d55a2bf32 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -230,7 +230,7 @@ export class ContextRecorder extends EventEmitter { }; this._collection.willPerformAction(actionInContext); - const success = await performAction(frame, action); + const success = await performAction(this._pageAliases, actionInContext); if (success) { this._collection.didPerformAction(actionInContext); this._setCommittedAfterTimeout(actionInContext); diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index c048d21bd3..9447f32457 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -37,28 +37,28 @@ export type ActionBase = { signals: Signal[], }; -export type ClickAction = ActionBase & { - name: 'click', +export type ActionWithSelector = ActionBase & { selector: string, +}; + +export type ClickAction = ActionWithSelector & { + name: 'click', button: 'left' | 'middle' | 'right', modifiers: number, clickCount: number, position?: Point, }; -export type CheckAction = ActionBase & { +export type CheckAction = ActionWithSelector & { name: 'check', - selector: string, }; -export type UncheckAction = ActionBase & { +export type UncheckAction = ActionWithSelector & { name: 'uncheck', - selector: string, }; -export type FillAction = ActionBase & { +export type FillAction = ActionWithSelector & { name: 'fill', - selector: string, text: string, }; @@ -83,40 +83,34 @@ export type PressAction = ActionBase & { modifiers: number, }; -export type SelectAction = ActionBase & { +export type SelectAction = ActionWithSelector & { name: 'select', - selector: string, options: string[], }; -export type SetInputFilesAction = ActionBase & { +export type SetInputFilesAction = ActionWithSelector & { name: 'setInputFiles', - selector: string, files: string[], }; -export type AssertTextAction = ActionBase & { +export type AssertTextAction = ActionWithSelector & { name: 'assertText', - selector: string, text: string, substring: boolean, }; -export type AssertValueAction = ActionBase & { +export type AssertValueAction = ActionWithSelector & { name: 'assertValue', - selector: string, value: string, }; -export type AssertCheckedAction = ActionBase & { +export type AssertCheckedAction = ActionWithSelector & { name: 'assertChecked', - selector: string, checked: boolean, }; -export type AssertVisibleAction = ActionBase & { +export type AssertVisibleAction = ActionWithSelector & { name: 'assertVisible', - selector: string, }; export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; diff --git a/packages/playwright-core/src/server/recorder/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts index beb74c5a6d..b6bdfd1a72 100644 --- a/packages/playwright-core/src/server/recorder/recorderRunner.ts +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -16,17 +16,19 @@ import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils'; import { toClickOptions, toKeyboardModifiers } from '../codegen/language'; +import type { ActionInContext } from '../codegen/types'; import type { Frame } from '../frames'; import type { CallMetadata } from '../instrumentation'; -import type * as actions from './recorderActions'; +import type { Page } from '../page'; +import { buildFullSelector } from './recorderUtils'; -async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { +async function innerPerformAction(mainFrame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { const callMetadata: CallMetadata = { id: `call@${createGuid()}`, apiName: 'frame.' + action, - objectId: frame.guid, - pageId: frame._page.guid, - frameId: frame.guid, + objectId: mainFrame.guid, + pageId: mainFrame._page.guid, + frameId: mainFrame.guid, startTime: monotonicTime(), endTime: 0, type: 'Frame', @@ -36,59 +38,69 @@ async function innerPerformAction(frame: Frame, action: string, params: any, cb: }; try { - await frame.instrumentation.onBeforeCall(frame, callMetadata); + await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); await cb(callMetadata); } catch (e) { callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); + await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata); return false; } callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); + await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata); return true; } -export async function performAction(frame: Frame, action: actions.Action): Promise { +export async function performAction(pageAliases: Map, actionInContext: ActionInContext): Promise { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + const mainFrame = page.mainFrame(); + const { action } = actionInContext; const kActionTimeout = 5000; + + if (action.name === 'navigate') + return await innerPerformAction(mainFrame, 'goto', { url: action.url }, callMetadata => mainFrame.goto(callMetadata, action.url, { timeout: kActionTimeout })); + if (action.name === 'openPage') + throw Error('Not reached'); + if (action.name === 'closePage') + return await innerPerformAction(mainFrame, 'close', {}, callMetadata => mainFrame._page.close(callMetadata)); + + const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); + if (action.name === 'click') { const options = toClickOptions(action); - return await innerPerformAction(frame, 'click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'click', { selector }, callMetadata => mainFrame.click(callMetadata, selector, { ...options, timeout: kActionTimeout, strict: true })); } if (action.name === 'press') { const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - return await innerPerformAction(frame, 'press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'press', { selector, key: shortcut }, callMetadata => mainFrame.press(callMetadata, selector, shortcut, { timeout: kActionTimeout, strict: true })); } if (action.name === 'fill') - return await innerPerformAction(frame, 'fill', { selector: action.selector, text: action.text }, callMetadata => frame.fill(callMetadata, action.selector, action.text, { timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'fill', { selector, text: action.text }, callMetadata => mainFrame.fill(callMetadata, selector, action.text, { timeout: kActionTimeout, strict: true })); if (action.name === 'setInputFiles') - return await innerPerformAction(frame, 'setInputFiles', { selector: action.selector, files: action.files }, callMetadata => frame.setInputFiles(callMetadata, action.selector, { selector: action.selector, payloads: [], timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'setInputFiles', { selector, files: action.files }, callMetadata => mainFrame.setInputFiles(callMetadata, selector, { selector, payloads: [], timeout: kActionTimeout, strict: true })); if (action.name === 'check') - return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'check', { selector }, callMetadata => mainFrame.check(callMetadata, selector, { timeout: kActionTimeout, strict: true })); if (action.name === 'uncheck') - return await innerPerformAction(frame, 'uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'uncheck', { selector }, callMetadata => mainFrame.uncheck(callMetadata, selector, { timeout: kActionTimeout, strict: true })); if (action.name === 'select') { const values = action.options.map(value => ({ value })); - return await innerPerformAction(frame, 'selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); + return await innerPerformAction(mainFrame, 'selectOption', { selector, values }, callMetadata => mainFrame.selectOption(callMetadata, selector, [], values, { timeout: kActionTimeout, strict: true })); } - if (action.name === 'navigate') - return await innerPerformAction(frame, 'goto', { url: action.url }, callMetadata => frame.goto(callMetadata, action.url, { timeout: kActionTimeout })); - if (action.name === 'closePage') - return await innerPerformAction(frame, 'close', {}, callMetadata => frame._page.close(callMetadata)); - if (action.name === 'openPage') - throw Error('Not reached'); if (action.name === 'assertChecked') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, expression: 'to.be.checked', isNot: !action.checked, timeout: kActionTimeout, })); } if (action.name === 'assertText') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, expression: 'to.have.text', expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), isNot: false, @@ -96,8 +108,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi })); } if (action.name === 'assertValue') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, expression: 'to.have.value', expectedValue: action.value, isNot: false, @@ -105,8 +117,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi })); } if (action.name === 'assertVisible') { - return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { - selector: action.selector, + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, expression: 'to.be.visible', isNot: false, timeout: kActionTimeout, diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index d6237b4899..b044da87ac 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -44,3 +44,7 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus) }; return callLog; } + +export function buildFullSelector(framePath: string[], selector: string) { + return [...framePath, selector].join(' >> internal:control=enter-frame >> '); +} From ec681ca78c7ce8a3a841f2583ec2a72c205cba4a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Aug 2024 20:24:19 -0700 Subject: [PATCH 26/47] chore: pass explicit recorder app factory (#32349) --- packages/playwright-core/src/cli/program.ts | 1 - .../src/client/browserContext.ts | 1 - .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/browserContext.ts | 9 ++-- .../src/server/debugController.ts | 4 +- .../dispatchers/browserContextDispatcher.ts | 3 +- .../playwright-core/src/server/recorder.ts | 42 ++++++++----------- .../src/server/recorder/recorderApp.ts | 18 +++++--- packages/protocol/src/channels.ts | 2 - packages/protocol/src/protocol.yml | 1 - 10 files changed, 39 insertions(+), 43 deletions(-) diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 28cf15fddb..9c68271e80 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -571,7 +571,6 @@ async function codegen(options: Options & { target: string, output?: string, tes mode: 'recording', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, - handleSIGINT: false, }); await openPage(context, url); } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index c4f7827840..ef222136dd 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner mode?: 'recording' | 'inspecting', testIdAttributeName?: string, outputFile?: string, - handleSIGINT?: boolean, }) { await this._channel.recorderSupplementEnable(params); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index e0b4a4d3df..1768380d30 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -961,7 +961,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({ device: tOptional(tString), saveStorage: tOptional(tString), outputFile: tOptional(tString), - handleSIGINT: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean), }); scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 09b84b267f..8ddbe68f89 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -43,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch'; import type { Artifact } from './artifact'; import { Clock } from './clock'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; +import { RecorderApp } from './recorder/recorderApp'; export abstract class BrowserContext extends SdkObject { static Events = { @@ -130,13 +131,15 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') - await Recorder.show(this, { pauseOnNextStatement: true }); + await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true }); // When paused, show inspector. if (this._debugger.isPaused()) - Recorder.showInspector(this); + Recorder.showInspector(this, RecorderApp.factory(this)); + this._debugger.on(Debugger.Events.PausedStateChanged, () => { - Recorder.showInspector(this); + if (this._debugger.isPaused()) + Recorder.showInspector(this, RecorderApp.factory(this)); }); if (debugMode() === 'console') diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 1d87485571..2a950d7c6a 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -52,7 +52,6 @@ export class DebugController extends SdkObject { initialize(codegenId: string, sdkLanguage: Language) { this._codegenId = codegenId; this._sdkLanguage = sdkLanguage; - Recorder.setAppFactory(async () => new InspectingRecorderApp(this)); } setAutoCloseAllowed(allowed: boolean) { @@ -62,7 +61,6 @@ export class DebugController extends SdkObject { dispose() { this.setReportStateChanged(false); this.setAutoCloseAllowed(false); - Recorder.setAppFactory(undefined); } setReportStateChanged(enabled: boolean) { @@ -199,7 +197,7 @@ export class DebugController extends SdkObject { const contexts = new Set(); for (const page of this._playwright.allPages()) contexts.add(page.context()); - const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }))); + const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true }))); return result.filter(Boolean) as Recorder[]; } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 5654950360..c70d8e825a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -39,6 +39,7 @@ import type { Dialog } from '../dialog'; import type { ConsoleMessage } from '../console'; import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { RecorderApp } from '../recorder/recorderApp'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -291,7 +292,7 @@ export class BrowserContextDispatcher extends Dispatcher { - await Recorder.show(this._context, params); + await Recorder.show(this._context, RecorderApp.factory(this._context), params); } async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 857df392b3..79b1bde22e 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -26,12 +26,13 @@ import { type Language } from './codegen/types'; import { Debugger } from './debugger'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; -import type { IRecorderApp } from './recorder/recorderApp'; -import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp'; +import { type IRecorderApp } from './recorder/recorderApp'; import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils'; const recorderSymbol = Symbol('recorderSymbol'); +export type RecorderAppFactory = (recorder: Recorder) => Promise; + export class Recorder implements InstrumentationListener { private _context: BrowserContext; private _mode: Mode; @@ -43,40 +44,38 @@ export class Recorder implements InstrumentationListener { private _userSources = new Map(); private _debugger: Debugger; private _contextRecorder: ContextRecorder; - private _handleSIGINT: boolean | undefined; private _omitCallTracking = false; private _currentLanguage: Language; - private static recorderAppFactory: ((recorder: Recorder) => Promise) | undefined; - - static setAppFactory(recorderAppFactory: ((recorder: Recorder) => Promise) | undefined) { - Recorder.recorderAppFactory = recorderAppFactory; - } - - static showInspector(context: BrowserContext) { + static showInspector(context: BrowserContext, recorderAppFactory: RecorderAppFactory) { const params: channels.BrowserContextRecorderSupplementEnableParams = {}; if (isUnderTest()) params.language = process.env.TEST_INSPECTOR_LANGUAGE; - Recorder.show(context, params).catch(() => {}); + Recorder.show(context, recorderAppFactory, params).catch(() => {}); } - static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { + static show(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - const recorder = new Recorder(context, params); - recorderPromise = recorder.install().then(() => recorder); + recorderPromise = Recorder._create(context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } + private static async _create(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { + const recorder = new Recorder(context, params); + const recorderApp = await recorderAppFactory(recorder); + await recorder._install(recorderApp); + return recorder; + } + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { this._mode = params.mode || 'none'; this._contextRecorder = new ContextRecorder(context, params); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); - this._handleSIGINT = params.handleSIGINT; context.instrumentation.addListener(this, context); this._currentLanguage = this._contextRecorder.languageName(); @@ -86,14 +85,7 @@ export class Recorder implements InstrumentationListener { } } - private static async defaultRecorderAppFactory(recorder: Recorder) { - if (process.env.PW_CODEGEN_NO_INSPECTOR) - return new EmptyRecorderApp(); - return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT); - } - - async install() { - const recorderApp = await (Recorder.recorderAppFactory || Recorder.defaultRecorderAppFactory)(this); + private async _install(recorderApp: IRecorderApp) { this._recorderApp = recorderApp; recorderApp.once('close', () => { this._debugger.resume(false); @@ -140,7 +132,7 @@ export class Recorder implements InstrumentationListener { this._context.once(BrowserContext.Events.Close, () => { this._contextRecorder.dispose(); this._context.instrumentation.removeListener(this); - recorderApp.close().catch(() => {}); + this._recorderApp?.close().catch(() => {}); }); this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => { this._recorderSources = data.sources; @@ -201,7 +193,7 @@ export class Recorder implements InstrumentationListener { this._pausedStateChanged(); this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged()); - (this._context as any).recorderAppForTest = recorderApp; + (this._context as any).recorderAppForTest = this._recorderApp; } _pausedStateChanged() { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 7f9166ae73..0faf191ea5 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -24,7 +24,7 @@ import type { CallLog, EventData, Mode, Source } from '@recorder/recorderTypes'; import { isUnderTest } from '../../utils'; import { mime } from '../../utilsBundle'; import { syncLocalStorageWithSettings } from '../launchApp'; -import type { Recorder } from '../recorder'; +import type { Recorder, RecorderAppFactory } from '../recorder'; import type { BrowserContext } from '../browserContext'; import { launchApp } from '../launchApp'; @@ -113,7 +113,15 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); } - static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise { + static factory(context: BrowserContext): RecorderAppFactory { + return async recorder => { + if (process.env.PW_CODEGEN_NO_INSPECTOR) + return new EmptyRecorderApp(); + return await RecorderApp._open(recorder, context); + }; + } + + private static async _open(recorder: Recorder, inspectedContext: BrowserContext): Promise { const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage; const headed = !!inspectedContext._browser.options.headful; const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true }); @@ -125,7 +133,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { noDefaultViewport: true, headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), useWebSocket: !!process.env.PWTEST_RECORDER_PORT, - handleSIGINT, + handleSIGINT: false, args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [], executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, } @@ -170,11 +178,11 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { async setSelector(selector: string, userGesture?: boolean): Promise { if (userGesture) { - if (this._recorder.mode() === 'inspecting') { + if (this._recorder?.mode() === 'inspecting') { this._recorder.setMode('standby'); this._page.bringToFront(); } else { - this._recorder.setMode('recording'); + this._recorder?.setMode('recording'); } } await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => { diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index cc3d07ba57..d0f5d43f79 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1753,7 +1753,6 @@ export type BrowserContextRecorderSupplementEnableParams = { device?: string, saveStorage?: string, outputFile?: string, - handleSIGINT?: boolean, omitCallTracking?: boolean, }; export type BrowserContextRecorderSupplementEnableOptions = { @@ -1766,7 +1765,6 @@ export type BrowserContextRecorderSupplementEnableOptions = { device?: string, saveStorage?: string, outputFile?: string, - handleSIGINT?: boolean, omitCallTracking?: boolean, }; export type BrowserContextRecorderSupplementEnableResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index c0a8d09795..d7c33b05d8 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1189,7 +1189,6 @@ BrowserContext: device: string? saveStorage: string? outputFile: string? - handleSIGINT: boolean? omitCallTracking: boolean? newCDPSession: From d61b207ce3cfcc73b6a54e01e648ec5f09c55346 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:23:39 -0700 Subject: [PATCH 27/47] feat(webkit): roll to r2066 (#32343) Fixes https://github.com/microsoft/playwright/issues/30305 --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Yury Semikhatsky --- packages/playwright-core/browsers.json | 2 +- tests/library/client-certificates.spec.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 43963417fc..1c86a1303c 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2065", + "revision": "2066", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 807af5154c..75ca2468f7 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -601,8 +601,7 @@ test.describe('browser', () => { test('support http2', async ({ browser, startCCServer, asset, browserName }) => { test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost'); - const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process.platform === 'linux'; - const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2 }); + const serverURL = await startCCServer({ http2: true }); const page = await browser.newPage({ ignoreHTTPSErrors: true, clientCertificates: [{ @@ -611,19 +610,16 @@ test.describe('browser', () => { keyPath: asset('client-certificates/client/trusted/key.pem'), }], }); - // TODO: We should investigate why http2 is not supported in WebKit on Linux. - // https://bugs.webkit.org/show_bug.cgi?id=276990 - const expectedProtocol = enableHTTP1FallbackWhenUsingHttp2 ? 'http/1.1' : 'h2'; { await page.goto(serverURL.replace('localhost', 'local.playwright')); await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.'); - await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol); + await expect(page.getByTestId('alpn-protocol')).toHaveText('h2'); await expect(page.getByTestId('servername')).toHaveText('local.playwright'); } { await page.goto(serverURL); await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); - await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol); + await expect(page.getByTestId('alpn-protocol')).toHaveText('h2'); } await page.close(); }); From 5271fe1f263057c5c811670a37cf6260903245a1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 28 Aug 2024 08:24:44 -0700 Subject: [PATCH 28/47] chore: remove unused request param from route.continue (#32307) --- .../playwright-core/src/server/chromium/crNetworkManager.ts | 2 +- .../playwright-core/src/server/firefox/ffNetworkManager.ts | 2 +- packages/playwright-core/src/server/frames.ts | 3 +-- packages/playwright-core/src/server/network.ts | 4 ++-- .../src/server/webkit/wkInterceptableRequest.ts | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 850f466afa..a8ff5a08dc 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate { this._interceptionId = interceptionId; } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise { + async continue(overrides: types.NormalizedContinueOverrides): Promise { this._alreadyContinuedParams = { requestId: this._interceptionId!, url: overrides.url, diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 266f5bcb83..978eb30bd4 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -226,7 +226,7 @@ class FFRouteImpl implements network.RouteDelegate { this._request = request; } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { + async continue(overrides: types.NormalizedContinueOverrides) { await this._session.sendMayFail('Network.resumeInterceptedRequest', { requestId: this._request._id, url: overrides.url, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 9ae6560a5f..3b952ea02a 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -291,8 +291,7 @@ export class FrameManager { if (request._documentId) frame.setPendingDocument({ documentId: request._documentId, request }); if (request._isFavicon) { - if (route) - route.continue(request, { isFallback: true }).catch(() => {}); + route?.continue({ isFallback: true }).catch(() => {}); return; } this._page.emitOnContext(BrowserContext.Events.Request, request); diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index fd62e1751b..e18b43708d 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -324,7 +324,7 @@ export class Route extends SdkObject { this._request._setOverrides(overrides); if (!overrides.isFallback) this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); - await this._delegate.continue(this._request, overrides); + await this._delegate.continue(overrides); this._endHandling(); } @@ -612,7 +612,7 @@ export class WebSocket extends SdkObject { export interface RouteDelegate { abort(errorCode: string): Promise; fulfill(response: types.NormalizedFulfillResponse): Promise; - continue(request: Request, overrides: types.NormalizedContinueOverrides): Promise; + continue(overrides: types.NormalizedContinueOverrides): Promise; } // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. diff --git a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts index a450127789..93367726ed 100644 --- a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts +++ b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts @@ -141,7 +141,7 @@ export class WKRouteImpl implements network.RouteDelegate { }); } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { + async continue(overrides: types.NormalizedContinueOverrides) { // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors. await this._session.sendMayFail('Network.interceptWithRequest', { From 22fe985c5480e3279c4937abcba2e835ed6b5881 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 28 Aug 2024 11:01:34 -0700 Subject: [PATCH 29/47] docs: add SUPPORT.md (#32362) --- SUPPORT.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 SUPPORT.md diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..78cb929fb1 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,17 @@ +# Support + +## How to file issues and get help + +This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template. + +For help and questions about using this project, please see the [docs site for Playwright][docs]. + +Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum. + +## Microsoft Support Policy + +Support for Playwright is limited to the resources listed above. + +[gh-issues]: https://github.com/microsoft/playwright/issues/ +[docs]: https://playwright.dev/ +[discord-server]: https://aka.ms/playwright/discord From d8137f228f4299d244d94bd853ade9a9ae91d35c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 28 Aug 2024 14:24:01 -0700 Subject: [PATCH 30/47] docs: update snippets to fix typescript errors (#32363) Reference: https://github.com/microsoft/playwright/issues/9468 --- docs/src/test-fixtures-js.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index f8c5cabc58..69c080297f 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -730,8 +730,8 @@ export const test = base.extend({ ```ts title="fixtures.ts" import { test as base } from '@playwright/test'; -export const test = base.extend({ - forEachTest: [async ({ page, baseURL }, use) => { +export const test = base.extend<{ forEachTest: void }>({ + forEachTest: [async ({ page }, use) => { // This code runs before every test. await page.goto('http://localhost:8000'); await use(); @@ -747,8 +747,9 @@ And then import the fixtures in all your tests: import { test } from './fixtures'; import { expect } from '@playwright/test'; -test('basic', async ({ page, baseURL }) => { - expect(page).toHaveURL(baseURL!); +test('basic', async ({ page }) => { + expect(page).toHaveURL('http://localhost:8000'); + await page.goto('https://playwright.dev'); }); ``` @@ -760,7 +761,7 @@ that run before/after all tests in every file, you can declare them as auto fixt ```ts title="fixtures.ts" import { test as base } from '@playwright/test'; -export const test = base.extend({ +export const test = base.extend<{}, { forEachWorker: void }>({ forEachWorker: [async ({}, use) => { // This code runs before all the tests in the worker process. console.log(`Starting test worker ${test.info().workerIndex}`); From 896190edbb4e4298129164706c14782c0ad71f79 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 28 Aug 2024 15:39:48 -0700 Subject: [PATCH 31/47] Revert feat(addInitScript): support cjs modules (#32364) Reverting https://github.com/microsoft/playwright/pull/32282 and https://github.com/microsoft/playwright/pull/32240. --- docs/src/api/class-browsercontext.md | 37 +--------- docs/src/api/class-page.md | 37 +--------- .../src/client/clientHelper.ts | 27 ++----- packages/playwright-core/types/types.d.ts | 70 +------------------ tests/assets/injectedmodule.js | 33 --------- tests/page/page-add-init-script.spec.ts | 6 -- 6 files changed, 13 insertions(+), 197 deletions(-) delete mode 100644 tests/assets/injectedmodule.js diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 36f980e0ce..43396f4957 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -415,42 +415,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: -**Bundling** - -If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. - -```js browser title="mocks/mockRandom.ts" -// This script can import other files. -import { defaultValue } from './defaultValue'; - -export default function(value?: number) { - window.Math.random = () => value ?? defaultValue; -} -``` - -```sh -# bundle with esbuild -esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js -``` - -```js title="tests/example.spec.ts" -const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - -// Passing 42 as an argument to the default export function. -await context.addInitScript({ path: mockPath }, 42); - -// Make sure to pass something even if you do not need to pass an argument. -// This instructs Playwright to treat the file as a commonjs module. -await context.addInitScript({ path: mockPath }, ''); -``` - ### param: BrowserContext.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. - - `content` ?<[string]> Raw script content. + current working directory. Optional. + - `content` ?<[string]> Raw script content. Optional. Script to be evaluated in all pages in the browser context. @@ -466,9 +437,7 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional JSON-serializable argument to pass to [`param: script`]. -* When `script` is a function, the argument is passed to it directly. -* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. +Optional argument to pass to [`param: script`] (only supported when passing a function). ### param: BrowserContext.addInitScript.path * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index da5d48f906..ea5fe74dfa 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -619,42 +619,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: -**Bundling** - -If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. - -```js browser title="mocks/mockRandom.ts" -// This script can import other files. -import { defaultValue } from './defaultValue'; - -export default function(value?: number) { - window.Math.random = () => value ?? defaultValue; -} -``` - -```sh -# bundle with esbuild -esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js -``` - -```js title="tests/example.spec.ts" -const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - -// Passing 42 as an argument to the default export function. -await page.addInitScript({ path: mockPath }, 42); - -// Make sure to pass something even if you do not need to pass an argument. -// This instructs Playwright to treat the file as a commonjs module. -await page.addInitScript({ path: mockPath }, ''); -``` - ### param: Page.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. - - `content` ?<[string]> Raw script content. + current working directory. Optional. + - `content` ?<[string]> Raw script content. Optional. Script to be evaluated in the page. @@ -670,9 +641,7 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional JSON-serializable argument to pass to [`param: script`]. -* When `script` is a function, the argument is passed to it directly. -* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. +Optional argument to pass to [`param: script`] (only supported when passing a function). ### param: Page.addInitScript.path * since: v1.8 diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index 793219f10b..540230a4fc 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -28,37 +28,20 @@ export function envObjectToArray(env: types.Env): { name: string, value: string return result; } -export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, addSourceUrl: boolean = true): Promise { +export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { if (typeof fun === 'function') { const source = fun.toString(); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); return `(${source})(${argString})`; } - if (isString(fun)) { - if (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); + if (isString(fun)) return fun; - } - if (fun.content !== undefined) { - if (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); + if (fun.content !== undefined) return fun.content; - } if (fun.path !== undefined) { let source = await fs.promises.readFile(fun.path, 'utf8'); - if (arg !== undefined) { - // Assume a CJS module that has a function default export. - source = `(() => { - var exports = {}; var module = { exports }; - ${source} - let __pw_result__ = module.exports; - if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__)) - __pw_result__ = __pw_result__['default']; - if (typeof __pw_result__ !== 'function') - return __pw_result__; - return __pw_result__(${JSON.stringify(arg)}); - })()`; - } if (addSourceUrl) source = addSourceUrlToScript(source, fun.path); return source; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d0424c3024..f7d662dea7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -288,41 +288,8 @@ export interface Page { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. - * - * **Bundling** - * - * If you have a complex script split into several files, it needs to be bundled into a single file first. We - * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a - * commonjs module and pass `path` and `arg`. - * - * ```js - * // mocks/mockRandom.ts - * // This script can import other files. - * import { defaultValue } from './defaultValue'; - * - * export default function(value?: number) { - * window.Math.random = () => value ?? defaultValue; - * } - * ``` - * - * ```js - * // tests/example.spec.ts - * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - * - * // Passing 42 as an argument to the default export function. - * await page.addInitScript({ path: mockPath }, 42); - * - * // Make sure to pass something even if you do not need to pass an argument. - * // This instructs Playwright to treat the file as a commonjs module. - * await page.addInitScript({ path: mockPath }, ''); - * ``` - * * @param script Script to be evaluated in the page. - * @param arg Optional JSON-serializable argument to pass to `script`. - * - When `script` is a function, the argument is passed to it directly. - * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either - * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this - * argument. + * @param arg Optional argument to pass to `script` (only supported when passing a function). */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -7733,41 +7700,8 @@ export interface BrowserContext { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. - * - * **Bundling** - * - * If you have a complex script split into several files, it needs to be bundled into a single file first. We - * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a - * commonjs module and pass `path` and `arg`. - * - * ```js - * // mocks/mockRandom.ts - * // This script can import other files. - * import { defaultValue } from './defaultValue'; - * - * export default function(value?: number) { - * window.Math.random = () => value ?? defaultValue; - * } - * ``` - * - * ```js - * // tests/example.spec.ts - * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - * - * // Passing 42 as an argument to the default export function. - * await context.addInitScript({ path: mockPath }, 42); - * - * // Make sure to pass something even if you do not need to pass an argument. - * // This instructs Playwright to treat the file as a commonjs module. - * await context.addInitScript({ path: mockPath }, ''); - * ``` - * * @param script Script to be evaluated in all pages in the browser context. - * @param arg Optional JSON-serializable argument to pass to `script`. - * - When `script` is a function, the argument is passed to it directly. - * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either - * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this - * argument. + * @param arg Optional argument to pass to `script` (only supported when passing a function). */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; diff --git a/tests/assets/injectedmodule.js b/tests/assets/injectedmodule.js deleted file mode 100644 index bc099f243f..0000000000 --- a/tests/assets/injectedmodule.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// one.ts -var one_exports = {}; -__export(one_exports, { - default: () => one_default -}); -module.exports = __toCommonJS(one_exports); - -// two.ts -var value = 42; - -// one.ts -function one_default(arg) { - window.injected = arg ?? value; -} diff --git a/tests/page/page-add-init-script.spec.ts b/tests/page/page-add-init-script.spec.ts index 2c8234a550..b2b7782eba 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -31,12 +31,6 @@ it('should work with a path', async ({ page, server, asset }) => { expect(await page.evaluate(() => window['result'])).toBe(123); }); -it('should assume CJS module with a path and arg', async ({ page, server, asset }) => { - await page.addInitScript({ path: asset('injectedmodule.js') }, 17); - await page.goto(server.EMPTY_PAGE); - expect(await page.evaluate(() => window['injected'])).toBe(17); -}); - it('should work with content @smoke', async ({ page, server }) => { await page.addInitScript({ content: 'window["injected"] = 123' }); await page.goto(server.PREFIX + '/tamperable.html'); From 6763d5ab6bd20f1f0fc879537855a26c7644a496 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:59:31 -0700 Subject: [PATCH 32/47] feat(chromium-tip-of-tree): roll to r1254 (#32337) --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 1c86a1303c..21f786a078 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1253", + "revision": "1254", "installByDefault": false, - "browserVersion": "130.0.6670.0" + "browserVersion": "130.0.6681.0" }, { "name": "firefox", From 74a8e59096b6fb76f4d0ea95706aabfbf54473c8 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 29 Aug 2024 14:16:01 -0700 Subject: [PATCH 33/47] chore: allow recorder rewrite annotations (#32381) --- .../src/server/injected/injectedScript.ts | 17 +++++++++--- .../src/server/injected/recorder/recorder.ts | 23 +++------------- .../src/server/injected/simpleDom.ts | 9 ++++--- .../playwright-core/src/server/recorder.ts | 3 +-- .../src/server/recorder/contextRecorder.ts | 26 +++++++++++++------ .../src/server/recorder/recorderUtils.ts | 24 +++++++++++++++++ packages/recorder/src/recorderTypes.ts | 1 - packages/trace-viewer/src/ui/snapshotTab.tsx | 1 - 8 files changed, 65 insertions(+), 39 deletions(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index c78d8d4065..69fe959f81 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -34,7 +34,8 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; import { cacheNormalizedWhitespaces, escapeHTML, escapeHTMLAttribute, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; -import { generateSimpleDom, generateSimpleDomNode, selectorForSimpleDomNodeId } from './simpleDom'; +import { selectorForSimpleDomNodeId, generateSimpleDomNode } from './simpleDom'; +import type { SimpleDomNode } from './simpleDom'; export type FrameExpectParams = Omit & { expectedValue?: any }; @@ -79,15 +80,12 @@ export class InjectedScript { endAriaCaches, escapeHTML, escapeHTMLAttribute, - generateSimpleDom: generateSimpleDom.bind(undefined, this), - generateSimpleDomNode: generateSimpleDomNode.bind(undefined, this), getAriaRole, getElementAccessibleDescription, getElementAccessibleName, isElementVisible, isInsideScope, normalizeWhiteSpace, - selectorForSimpleDomNodeId: selectorForSimpleDomNodeId.bind(undefined, this), }; // eslint-disable-next-line no-restricted-globals @@ -1314,6 +1312,17 @@ export class InjectedScript { } throw this.createStacklessError('Unknown expect matcher: ' + expression); } + + generateSimpleDomNode(selector: string): SimpleDomNode | undefined { + const element = this.querySelector(this.parseSelector(selector), this.document.documentElement, true); + if (!element) + return; + return generateSimpleDomNode(this, element); + } + + selectorForSimpleDomNodeId(nodeId: string) { + return selectorForSimpleDomNodeId(this, nodeId); + } } const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 95885e22d3..8cbf11964f 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -24,8 +24,8 @@ import clipPaths from './clipPaths'; import type { SimpleDomNode } from '../simpleDom'; interface RecorderDelegate { - performAction?(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise; - recordAction?(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise; + performAction?(action: actions.PerformOnRecordAction): Promise; + recordAction?(action: actions.Action): Promise; setSelector?(selector: string): Promise; setMode?(mode: Mode): Promise; setOverlayState?(state: OverlayState): Promise; @@ -931,7 +931,6 @@ export class Recorder { testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 }, - generateSimpleDom: false, }; readonly document: Document; private _delegate: RecorderDelegate = {}; @@ -1186,13 +1185,11 @@ export class Recorder { } async performAction(action: actions.PerformOnRecordAction) { - const simpleDomNode = this._generateSimpleDomNode(action); - await this._delegate.performAction?.(action, simpleDomNode).catch(() => {}); + await this._delegate.performAction?.(action).catch(() => {}); } recordAction(action: actions.Action) { - const simpleDomNode = this._generateSimpleDomNode(action); - void this._delegate.recordAction?.(action, simpleDomNode); + void this._delegate.recordAction?.(action); } setOverlayState(state: { offsetX: number; }) { @@ -1202,18 +1199,6 @@ export class Recorder { setSelector(selector: string) { void this._delegate.setSelector?.(selector); } - - private _generateSimpleDomNode(action: actions.Action): SimpleDomNode | undefined { - if (!this.state.generateSimpleDom) - return; - if (!('selector' in action)) - return; - - const element = this.injectedScript.querySelector(this.injectedScript.parseSelector(action.selector), this.document.documentElement, true); - if (!element) - return; - return this.injectedScript.utils.generateSimpleDomNode(element); - } } class Dialog { diff --git a/packages/playwright-core/src/server/injected/simpleDom.ts b/packages/playwright-core/src/server/injected/simpleDom.ts index 878b8021dd..c31862cd6c 100644 --- a/packages/playwright-core/src/server/injected/simpleDom.ts +++ b/packages/playwright-core/src/server/injected/simpleDom.ts @@ -77,10 +77,11 @@ function generate(injectedScript: InjectedScript, target?: Element): { dom: Simp const name = injectedScript.utils.getElementAccessibleName(element, false); const structuralId = String(++lastId); elements.set(structuralId, element); - const tag = renderTag(injectedScript, role, name, structuralId, { value }); - if (element === target) - resultTarget = { tag, id: structuralId }; - tokens.push(tag); + tokens.push(renderTag(injectedScript, role, name, structuralId, { value })); + if (element === target) { + const tagNoValue = renderTag(injectedScript, role, name, structuralId); + resultTarget = { tag: tagNoValue, id: structuralId }; + } return; } } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 79b1bde22e..97316c2f9e 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -72,7 +72,7 @@ export class Recorder implements InstrumentationListener { constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { this._mode = params.mode || 'none'; - this._contextRecorder = new ContextRecorder(context, params); + this._contextRecorder = new ContextRecorder(context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); @@ -160,7 +160,6 @@ export class Recorder implements InstrumentationListener { language: this._currentLanguage, testIdAttributeName: this._contextRecorder.testIdAttributeName(), overlay: this._overlayState, - generateSimpleDom: false, }; return uiState; }); diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 0d55a2bf32..17d2c2c130 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -25,7 +25,6 @@ import type { ActionInContext, FrameDescription, LanguageGeneratorOptions, Langu import { languageSet } from '../codegen/languages'; import type { Dialog } from '../dialog'; import { Frame } from '../frames'; -import type { SimpleDomNode } from '../injected/simpleDom'; import { Page } from '../page'; import type * as actions from './recorderActions'; import { performAction } from './recorderRunner'; @@ -35,6 +34,10 @@ import { generateCode } from '../codegen/language'; type BindingSource = { frame: Frame, page: Page }; +export interface ContextRecorderDelegate { + rewriteActionInContext?(pageAliases: Map, actionInContext: ActionInContext): Promise; +} + export class ContextRecorder extends EventEmitter { static Events = { Change: 'change' @@ -48,15 +51,17 @@ export class ContextRecorder extends EventEmitter { private _timers = new Set(); private _context: BrowserContext; private _params: channels.BrowserContextRecorderSupplementEnableParams; + private _delegate: ContextRecorderDelegate; private _recorderSources: Source[]; private _throttledOutputFile: ThrottledFile | null = null; private _orderedLanguages: LanguageGenerator[] = []; private _listeners: RegisteredListener[] = []; - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) { super(); this._context = context; this._params = params; + this._delegate = delegate; this._recorderSources = []; const language = params.language || context.attribution.playwright.options.sdkLanguage; this.setOutput(language, params.outputFile); @@ -134,11 +139,11 @@ export class ContextRecorder extends EventEmitter { // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. await this._context.exposeBinding('__pw_recorderPerformAction', false, - (source: BindingSource, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) => this._performAction(source.frame, action, simpleDomNode)); + (source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(source.frame, action)); // Other non-essential actions are simply being recorded. await this._context.exposeBinding('__pw_recorderRecordAction', false, - (source: BindingSource, action: actions.Action, simpleDomNode?: SimpleDomNode) => this._recordAction(source.frame, action, simpleDomNode)); + (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); await this._context.extendInjectedScript(recorderSource.source); } @@ -218,7 +223,7 @@ export class ContextRecorder extends EventEmitter { return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; } - private async _performAction(frame: Frame, action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { + private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) { // Commit last action so that no further signals are added to it. this._collection.commitLastAction(); @@ -226,9 +231,11 @@ export class ContextRecorder extends EventEmitter { const actionInContext: ActionInContext = { frame: frameDescription, action, - description: undefined, // TODO: generate description based on simple dom node. + description: undefined, }; + await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext); + this._collection.willPerformAction(actionInContext); const success = await performAction(this._pageAliases, actionInContext); if (success) { @@ -239,7 +246,7 @@ export class ContextRecorder extends EventEmitter { } } - private async _recordAction(frame: Frame, action: actions.Action, simpleDomNode?: SimpleDomNode) { + private async _recordAction(frame: Frame, action: actions.Action) { // Commit last action so that no further signals are added to it. this._collection.commitLastAction(); @@ -247,8 +254,11 @@ export class ContextRecorder extends EventEmitter { const actionInContext: ActionInContext = { frame: frameDescription, action, - description: undefined, // TODO: generate description based on simple dom node. + description: undefined, }; + + await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext); + this._setCommittedAfterTimeout(actionInContext); this._collection.addAction(actionInContext); } diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index b044da87ac..b4949115d2 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -16,6 +16,10 @@ import type { CallMetadata } from '../instrumentation'; import type { CallLog, CallLogStatus } from '@recorder/recorderTypes'; +import type { Page } from '../page'; +import type { ActionInContext } from '../codegen/types'; +import type { Frame } from '../frames'; +import type * as actions from './recorderActions'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -48,3 +52,23 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus) export function buildFullSelector(framePath: string[], selector: string) { return [...framePath, selector].join(' >> internal:control=enter-frame >> '); } + +export function mainFrameForAction(pageAliases: Map, actionInContext: ActionInContext): Frame { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + return page.mainFrame(); +} + +export async function frameForAction(pageAliases: Map, actionInContext: ActionInContext, action: actions.ActionWithSelector): Promise { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + const fullSelector = buildFullSelector(actionInContext.frame.framePath, action.selector); + const result = await page.mainFrame().selectors.resolveFrameForSelector(fullSelector); + if (!result) + throw new Error('Internal error: frame not found'); + return result.frame; +} diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index 09cb02e3e2..c56984ad6d 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -51,7 +51,6 @@ export type UIState = { language: Language; testIdAttributeName: string; overlay: OverlayState; - generateSimpleDom: boolean; }; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 578f787f3e..4faa668677 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -254,7 +254,6 @@ export const InspectModeController: React.FunctionComponent<{ language: sdkLanguage, testIdAttributeName, overlay: { offsetX: 0 }, - generateSimpleDom: false, }, { async setSelector(selector: string) { setHighlightedLocator(asLocator(sdkLanguage, frameSelector + selector)); From 0a40862bc8b324433e1a1afc2c68cdf605c32b23 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 29 Aug 2024 23:16:29 +0200 Subject: [PATCH 34/47] chore(docs): fix typo (#32372) --- docs/src/test-typescript-js.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/test-typescript-js.md b/docs/src/test-typescript-js.md index 5eaa3670a5..6e18b3c615 100644 --- a/docs/src/test-typescript-js.md +++ b/docs/src/test-typescript-js.md @@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by ```sh # Playwright will choose tsconfig automatically -npx playwrigh test +npx playwright test ``` Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files. ```sh # Pass a specific tsconfig -npx playwrigh test --tsconfig=tsconfig.test.json +npx playwright test --tsconfig=tsconfig.test.json ``` ## Manually compile tests with TypeScript From 90e7b9ebacbd597b7380522001eb6d17ee9c3d86 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 29 Aug 2024 14:16:49 -0700 Subject: [PATCH 35/47] chore: enforce tags format via typescript types (#32384) Leverage [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). Fixes https://github.com/microsoft/playwright/issues/32382 --- packages/playwright/types/test.d.ts | 4 +++- tests/playwright-test/test-tag.spec.ts | 12 ++++++++++++ utils/generate_types/overrides-test.d.ts | 4 +++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 0131cb16c9..5efdca4658 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1825,8 +1825,10 @@ type TestDetailsAnnotation = { description?: string; }; +type TestDetailsTag = `@${string}`; + export type TestDetails = { - tag?: string | string[]; + tag?: TestDetailsTag | TestDetailsTag[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } diff --git a/tests/playwright-test/test-tag.spec.ts b/tests/playwright-test/test-tag.spec.ts index 9487e31ea3..0587cfe7a8 100644 --- a/tests/playwright-test/test-tag.spec.ts +++ b/tests/playwright-test/test-tag.spec.ts @@ -147,6 +147,18 @@ test('should enforce @ symbol', async ({ runInlineTest }) => { expect(result.output).toContain(`Error: Tag must start with "@" symbol, got "foo" instead.`); }); +test('types should enforce @ symbol', async ({ runTSC }) => { + const result = await runTSC({ + 'stdio.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', { tag: 'foo' }, () => { + }); + ` + }); + expect(result.exitCode).toBe(2); + expect(result.output).toContain('error TS2322: Type \'"foo"\' is not assignable to type \'`@${string}` | `@${string}`[] | undefined'); +}); + test('should be included in testInfo', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ 'a.test.ts': ` diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 5c108d7b25..76fecc524a 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -70,8 +70,10 @@ type TestDetailsAnnotation = { description?: string; }; +type TestDetailsTag = `@${string}`; + export type TestDetails = { - tag?: string | string[]; + tag?: TestDetailsTag | TestDetailsTag[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } From ed5c21b827d19ca03e1bf38b2b15cb392bece625 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 30 Aug 2024 08:29:49 +0200 Subject: [PATCH 36/47] fix(ui): respect --output param (#32351) Closes https://github.com/microsoft/playwright/issues/32331 We're already passing the `outputDir` param to the UI, but the UI isn't passing it back to the TestServer. This PR fixes that. I've added it to `listTests`, which is requires to that `TestServerDispatcher#_ignoredProjectOutputs` is populated with the correct output dir. And i've added it to `runGlobalSetup`, which is what the bug report was about. --- .../src/isomorphic/testServerInterface.ts | 3 ++- packages/playwright/src/runner/testServer.ts | 6 ++++- packages/trace-viewer/src/ui/uiModeView.tsx | 8 +++--- .../ui-mode-test-setup.spec.ts | 26 +++++++++++++------ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 0460ae56d9..28f82688dc 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -44,7 +44,7 @@ export interface TestServerInterface { installBrowsers(params: {}): Promise; - runGlobalSetup(params: {}): Promise<{ + runGlobalSetup(params: { outputDir?: string }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] }>; @@ -81,6 +81,7 @@ export interface TestServerInterface { locations?: string[]; grep?: string; grepInvert?: string; + outputDir?: string; }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 7ed1d18191..474ed6ae5d 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -148,7 +148,10 @@ class TestServerDispatcher implements TestServerInterface { async runGlobalSetup(params: Parameters[0]): ReturnType { await this.runGlobalTeardown(); - const { config, error } = await this._loadConfig(); + const overrides: ConfigCLIOverrides = { + outputDir: params.outputDir, + }; + const { config, error } = await this._loadConfig(overrides); if (!config) { const { reporter, report } = await this._collectingInternalReporter(); // Produce dummy config when it has an error. @@ -256,6 +259,7 @@ class TestServerDispatcher implements TestServerInterface { const overrides: ConfigCLIOverrides = { repeatEach: 1, retries: 0, + outputDir: params.outputDir, }; const { config, error } = await this._loadConfig(overrides); if (!config) { diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 0f799b2035..b88aebe6fc 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -205,12 +205,14 @@ export const UIModeView: React.FC<{}> = ({ interceptStdio: true, watchTestDirs: true }); - const { status, report } = await testServerConnection.runGlobalSetup({}); + const { status, report } = await testServerConnection.runGlobalSetup({ + outputDir: queryParams.outputDir, + }); teleSuiteUpdater.processGlobalReport(report); if (status !== 'passed') return; - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); teleSuiteUpdater.processListReport(result.report); testServerConnection.onReport(params => { @@ -333,7 +335,7 @@ export const UIModeView: React.FC<{}> = ({ commandQueue.current = commandQueue.current.then(async () => { setIsLoading(true); try { - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); teleSuiteUpdater.processListReport(result.report); } catch (e) { // eslint-disable-next-line no-console diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index e8809ddad9..cd5503427d 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -19,7 +19,7 @@ import path from 'path'; test.describe.configure({ mode: 'parallel', retries }); -test('should run global setup and teardown', async ({ runUITest }) => { +test('should run global setup and teardown', async ({ runUITest }, testInfo) => { const { page, testProcess } = await runUITest({ 'playwright.config.ts': ` import { defineConfig } from '@playwright/test'; @@ -29,26 +29,36 @@ test('should run global setup and teardown', async ({ runUITest }) => { }); `, 'globalSetup.ts': ` - export default () => console.log('\\n%%from-global-setup'); + import { basename } from "node:path"; + export default (config) => { + console.log('\\n%%from-global-setup'); + console.log("setupOutputDir: " + basename(config.projects[0].outputDir)); + }; `, 'globalTeardown.ts': ` - export default () => console.log('\\n%%from-global-teardown'); + export default (config) => { + console.log('\\n%%from-global-teardown'); + console.log('%%' + JSON.stringify(config)); + }; `, 'a.test.js': ` import { test, expect } from '@playwright/test'; test('should work', async ({}) => {}); ` - }); + }, undefined, { additionalArgs: ['--output=foo'] }); await page.getByTitle('Run all').click(); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('Toggle output').click(); - await expect(page.getByTestId('output')).toContainText('from-global-setup'); + const output = page.getByTestId('output'); + await expect(output).toContainText('from-global-setup'); + await expect(output).toContainText('setupOutputDir: foo'); await page.close(); - await expect.poll(() => testProcess.outputLines()).toEqual([ - 'from-global-teardown', - ]); + await expect.poll(() => testProcess.outputLines()).toContain('from-global-teardown'); + + const teardownConfig = JSON.parse(testProcess.outputLines()[1]); + expect(teardownConfig.projects[0].outputDir).toEqual(testInfo.outputPath('foo')); }); test('should teardown on sigint', async ({ runUITest, nodeVersion }) => { From a6b320e36224f70ad04fd520503c230d5956ba66 Mon Sep 17 00:00:00 2001 From: Kuba Janik Date: Fri, 30 Aug 2024 16:21:51 +0200 Subject: [PATCH 37/47] fix(ui-mode): format request body when headers are lower case (#32395) Resolves https://github.com/microsoft/playwright/issues/32396 Currently, the request body is not formatted when content type header is lower case (`content-type`). Even though the value is `application/json`. It happens because we are looking only for `Content-Type` header ignoring headers that are lower case. 363197933-5178ec23-b9cf-46b5-8284-e8d4d730b036 --- .../src/ui/networkResourceDetails.tsx | 2 +- tests/assets/network-tab/network.html | 19 +++++++++ .../ui-mode-test-network-tab.spec.ts | 42 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 002b7b7fb9..03a2e936df 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -59,7 +59,7 @@ const RequestTab: React.FunctionComponent<{ React.useEffect(() => { const readResources = async () => { if (resource.request.postData) { - const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type'); + const requestContentTypeHeader = resource.request.headers.find(q => q.name.toLowerCase() === 'content-type'); const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; if (resource.request.postData._sha1) { const response = await fetch(`sha1/${resource.request.postData._sha1}`); diff --git a/tests/assets/network-tab/network.html b/tests/assets/network-tab/network.html index d46ff846dc..32f7d2cf6c 100644 --- a/tests/assets/network-tab/network.html +++ b/tests/assets/network-tab/network.html @@ -13,6 +13,25 @@ +

Network Tab Test

diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 45d77aa528..8fc7a4828e 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -93,3 +93,45 @@ test('should filter network requests by url', async ({ runUITest, server }) => { await expect(networkItems).toHaveCount(1); await expect(networkItems.getByText('font.woff2')).toBeVisible(); }); + +test('should format JSON request body', async ({ runUITest, server }) => { + const { page } = await runUITest({ + 'network-tab.test.ts': ` + import { test, expect } from '@playwright/test'; + test('network tab test', async ({ page }) => { + await page.goto('${server.PREFIX}/network-tab/network.html'); + }); + `, + }); + + await page.getByText('network tab test').dblclick(); + await page.getByText('Network', { exact: true }).click(); + + await page.getByText('post-data-1').click(); + + await expect(page.locator('.CodeMirror-code .CodeMirror-line').allInnerTexts()).resolves.toEqual([ + '{', + ' "data": {', + ' "key": "value",', + ' "array": [', + ' "value-1",', + ' "value-2"', + ' ]', + ' }', + '}', + ]); + + await page.getByText('post-data-2').click(); + + await expect(page.locator('.CodeMirror-code .CodeMirror-line').allInnerTexts()).resolves.toEqual([ + '{', + ' "data": {', + ' "key": "value",', + ' "array": [', + ' "value-1",', + ' "value-2"', + ' ]', + ' }', + '}', + ]); +}); From cf8c14f884b6f24966350a5f49b1580c3e183d21 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 2 Sep 2024 08:35:53 +0200 Subject: [PATCH 38/47] feat(html reporter): open html attachments in new tab (#32389) Closes https://github.com/microsoft/playwright/issues/32281. HTML attachments get a linkified name that opens the attachment in a new tab. --- packages/html-reporter/src/links.tsx | 9 +++++-- packages/html-reporter/src/testResultView.tsx | 12 ++++++--- tests/playwright-test/reporter-html.spec.ts | 26 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 419a8725ac..8c1dcc85dc 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{ attachment: TestAttachment, href?: string, linkName?: string, -}> = ({ attachment, href, linkName }) => { + openInNewTab?: boolean, +}> = ({ attachment, href, linkName, openInNewTab }) => { return {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.path && {linkName || attachment.name}} - {!attachment.path && {linkifyText(attachment.name)}} + {!attachment.path && ( + openInNewTab + ? e.stopPropagation()}>{attachment.name} + : {linkifyText(attachment.name)} + )} } loadChildren={attachment.body ? () => { return [
{linkifyText(attachment.body!)}
]; } : undefined} depth={0} style={{ lineHeight: '32px' }}>
; diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 1ec8c65a1e..8ee36d0cda 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -67,15 +67,16 @@ export const TestResultView: React.FC<{ anchor: 'video' | 'diff' | '', }> = ({ result, anchor }) => { - const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => { + const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => { const attachments = result?.attachments || []; const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const videos = attachments.filter(a => a.name === 'video'); const traces = attachments.filter(a => a.name === 'trace'); + const htmls = attachments.filter(a => a.contentType.startsWith('text/html')); const otherAttachments = new Set(attachments); - [...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); + [...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a)); const diffs = groupImageDiffs(screenshots); - return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs }; + return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls }; }, [result]); const videoRef = React.useRef(null); @@ -135,7 +136,10 @@ export const TestResultView: React.FC<{ )} } - {!!otherAttachments.size && + {!!(otherAttachments.size + htmls.length) && + {[...htmls].map((a, i) => ( + ) + )} {[...otherAttachments].map((a, i) => )} } ; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 768cd28fc2..1f5da44a84 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -802,6 +802,32 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(page.locator('.attachment-body')).toHaveText(['foo', '{"foo":1}', 'utf16 encoded']); }); + test('should have link for opening HTML attachments in new tab', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('passing', async ({ page }, testInfo) => { + testInfo.attach('axe-report.html', { + contentType: 'text/html', + body: '

Axe Report

', + }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + + await showReport(); + await page.getByText('passing', { exact: true }).click(); + + const [newTab] = await Promise.all([ + page.waitForEvent('popup'), + page.getByText('axe-report.html', { exact: true }).click(), + ]); + + await expect(newTab).toHaveURL(/^blob:/); + await expect(newTab.getByText('Axe Report')).toBeVisible(); + }); + test('should use file-browser friendly extensions for buffer attachments based on contentType', async ({ runInlineTest, showReport, page }, testInfo) => { const result = await runInlineTest({ 'a.test.js': ` From 3f09d10601acffca3075d1376f696decb2f78ed6 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 2 Sep 2024 09:11:04 +0200 Subject: [PATCH 39/47] fix(test runner): perform shallow clone check in config directory (#32299) Our CI operates on shallow clones. In vcs.ts, we perform a check for shallow clones in `process.cwd()` instead of the test directory. This makes the test in https://github.com/Skn0tt/playwright/blob/3c208aeeff255fd9ccf0528863e6b4b790d0f1b8/tests/playwright-test/only-changed.spec.ts#L201 failing in CI, but only for PRs. The fix is to perform the check on. the test directory. --- packages/playwright/src/runner/vcs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright/src/runner/vcs.ts b/packages/playwright/src/runner/vcs.ts index 707d820ed5..6f7ed55c9a 100644 --- a/packages/playwright/src/runner/vcs.ts +++ b/packages/playwright/src/runner/vcs.ts @@ -30,7 +30,7 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri const unknownRevision = error.output.some(line => line?.includes('unknown revision')); if (unknownRevision) { - const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe' }).trim() === 'true'; + const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe', cwd: configDir }).trim() === 'true'; if (isShallowClone) { throw new Error([ `The repository is a shallow clone and does not have '${baseCommit}' available locally.`, From d9016e506e764355ac4acf74f2faea73f58c059c Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 2 Sep 2024 00:49:38 -0700 Subject: [PATCH 40/47] feat(chromium): roll to r1133 (#32391) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/chromium/protocol.d.ts | 244 ++++++++++++++++-- .../src/server/deviceDescriptorsSource.json | 96 +++---- packages/playwright-core/types/protocol.d.ts | 244 ++++++++++++++++-- 5 files changed, 496 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index fde98ea6fe..d5a3110613 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-128.0.6613.36-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-129.0.6668.22-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 128.0.6613.36 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 129.0.6668.22 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 129.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 21f786a078..2f622c4875 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1131", + "revision": "1133", "installByDefault": true, - "browserVersion": "128.0.6613.36" + "browserVersion": "129.0.6668.22" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 99ce1d3a6a..caadb2a577 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -1131,17 +1131,21 @@ using Audits.issueAdded event. } /** - * Defines commands and events for browser extensions. Available if the client -is connected using the --remote-debugging-pipe flag and -the --enable-unsafe-extension-debugging flag is set. + * Defines commands and events for browser extensions. */ export module Extensions { + /** + * Storage areas. + */ + export type StorageArea = "session"|"local"|"sync"|"managed"; /** * Installs an unpacked extension from the filesystem similar to --load-extension CLI flags. Returns extension ID once the extension -has been installed. +has been installed. Available if the client is connected using the +--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging +flag is set. */ export type loadUnpackedParameters = { /** @@ -1155,6 +1159,81 @@ has been installed. */ id: string; } + /** + * Gets data from extension storage in the given `storageArea`. If `keys` is +specified, these are used to filter the result. + */ + export type getStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to retrieve data from. + */ + storageArea: StorageArea; + /** + * Keys to retrieve. + */ + keys?: string[]; + } + export type getStorageItemsReturnValue = { + data: { [key: string]: string }; + } + /** + * Removes `keys` from extension storage in the given `storageArea`. + */ + export type removeStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + /** + * Keys to remove. + */ + keys: string[]; + } + export type removeStorageItemsReturnValue = { + } + /** + * Clears extension storage in the given `storageArea`. + */ + export type clearStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + } + export type clearStorageItemsReturnValue = { + } + /** + * Sets `values` in extension storage in the given `storageArea`. The provided `values` +will be merged with existing values in the storage area. + */ + export type setStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to set data in. + */ + storageArea: StorageArea; + /** + * Values to set. + */ + values: { [key: string]: string }; + } + export type setStorageItemsReturnValue = { + } } /** @@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from. */ style: CSSStyle; } - /** - * CSS position-fallback rule representation. - */ - export interface CSSPositionFallbackRule { - name: Value; - /** - * List of keyframes. - */ - tryRules: CSSTryRule[]; - } /** * CSS @position-try rule representation. */ @@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`. * A list of CSS keyframed animations matching this node. */ cssKeyframesRules?: CSSKeyframesRule[]; - /** - * A list of CSS position fallbacks matching this node. - */ - cssPositionFallbackRules?: CSSPositionFallbackRule[]; /** * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. */ @@ -3496,7 +3561,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -3646,6 +3711,13 @@ The property is always undefined now. compatibilityMode?: CompatibilityMode; assignedSlot?: BackendNode; } + /** + * A structure to hold the top-level node of a detached tree and an array of its retained descendants. + */ + export interface DetachedElementInfo { + treeNode: Node; + retainedNodeIds: NodeId[]; + } /** * A structure holding an RGBA color. */ @@ -4693,6 +4765,17 @@ File wrapper. export type getFileInfoReturnValue = { path: string; } + /** + * Returns list of detached nodes + */ + export type getDetachedDomNodesParameters = { + } + export type getDetachedDomNodesReturnValue = { + /** + * The list of detached nodes + */ + detachedNodes: DetachedElementInfo[]; + } /** * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). @@ -11369,7 +11452,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com" */ fixed?: number; } - export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; + export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export interface InstallabilityErrorArgument { /** @@ -12298,6 +12381,10 @@ when bfcache navigation fails. * Frame's new url. */ url: string; + /** + * Navigation type + */ + navigationType: "fragment"|"historyApi"|"other"; } /** * Compressed image data requested by the `startScreencast`. @@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17270,6 +17357,101 @@ supported yet. } } + /** + * This domain allows configuring virtual Bluetooth devices to test +the web-bluetooth API. + */ + export module BluetoothEmulation { + /** + * Indicates the various states of Central. + */ + export type CentralState = "absent"|"powered-off"|"powered-on"; + /** + * Stores the manufacturer data + */ + export interface ManufacturerData { + /** + * Company identifier +https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +https://usb.org/developers + */ + key: number; + /** + * Manufacturer-specific data + */ + data: binary; + } + /** + * Stores the byte data of the advertisement packet sent by a Bluetooth device. + */ + export interface ScanRecord { + name?: string; + uuids?: string[]; + /** + * Stores the external appearance description of the device. + */ + appearance?: number; + /** + * Stores the transmission power of a broadcasting device. + */ + txPower?: number; + /** + * Key is the company identifier and the value is an array of bytes of +manufacturer specific data. + */ + manufacturerData?: ManufacturerData[]; + } + /** + * Stores the advertisement packet information that is sent by a Bluetooth device. + */ + export interface ScanEntry { + deviceAddress: string; + rssi: number; + scanRecord: ScanRecord; + } + + + /** + * Enable the BluetoothEmulation domain. + */ + export type enableParameters = { + /** + * State of the simulated central. + */ + state: CentralState; + } + export type enableReturnValue = { + } + /** + * Disable the BluetoothEmulation domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Simulates a peripheral with |address|, |name| and |knownServiceUuids| +that has already been connected to the system. + */ + export type simulatePreconnectedPeripheralParameters = { + address: string; + name: string; + manufacturerData: ManufacturerData[]; + knownServiceUuids: string[]; + } + export type simulatePreconnectedPeripheralReturnValue = { + } + /** + * Simulates an advertisement packet described in |entry| being received by +the central. + */ + export type simulateAdvertisementParameters = { + entry: ScanEntry; + } + export type simulateAdvertisementReturnValue = { + } + } + /** * This domain is deprecated - use Runtime or Log instead. */ @@ -20122,6 +20304,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; + "Extensions.getStorageItems": Extensions.getStorageItemsParameters; + "Extensions.removeStorageItems": Extensions.removeStorageItemsParameters; + "Extensions.clearStorageItems": Extensions.clearStorageItemsParameters; + "Extensions.setStorageItems": Extensions.setStorageItemsParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -20232,6 +20418,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getFileInfo": DOM.getFileInfoParameters; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeValue": DOM.setNodeValueParameters; @@ -20616,6 +20803,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; + "BluetoothEmulation.enable": BluetoothEmulation.enableParameters; + "BluetoothEmulation.disable": BluetoothEmulation.disableParameters; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20722,6 +20913,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; + "Extensions.getStorageItems": Extensions.getStorageItemsReturnValue; + "Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue; + "Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue; + "Extensions.setStorageItems": Extensions.setStorageItemsReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20832,6 +21027,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue; @@ -21216,6 +21412,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; + "BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue; + "BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index c25989baf5..c85866792a 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 99ce1d3a6a..caadb2a577 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -1131,17 +1131,21 @@ using Audits.issueAdded event. } /** - * Defines commands and events for browser extensions. Available if the client -is connected using the --remote-debugging-pipe flag and -the --enable-unsafe-extension-debugging flag is set. + * Defines commands and events for browser extensions. */ export module Extensions { + /** + * Storage areas. + */ + export type StorageArea = "session"|"local"|"sync"|"managed"; /** * Installs an unpacked extension from the filesystem similar to --load-extension CLI flags. Returns extension ID once the extension -has been installed. +has been installed. Available if the client is connected using the +--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging +flag is set. */ export type loadUnpackedParameters = { /** @@ -1155,6 +1159,81 @@ has been installed. */ id: string; } + /** + * Gets data from extension storage in the given `storageArea`. If `keys` is +specified, these are used to filter the result. + */ + export type getStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to retrieve data from. + */ + storageArea: StorageArea; + /** + * Keys to retrieve. + */ + keys?: string[]; + } + export type getStorageItemsReturnValue = { + data: { [key: string]: string }; + } + /** + * Removes `keys` from extension storage in the given `storageArea`. + */ + export type removeStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + /** + * Keys to remove. + */ + keys: string[]; + } + export type removeStorageItemsReturnValue = { + } + /** + * Clears extension storage in the given `storageArea`. + */ + export type clearStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + } + export type clearStorageItemsReturnValue = { + } + /** + * Sets `values` in extension storage in the given `storageArea`. The provided `values` +will be merged with existing values in the storage area. + */ + export type setStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to set data in. + */ + storageArea: StorageArea; + /** + * Values to set. + */ + values: { [key: string]: string }; + } + export type setStorageItemsReturnValue = { + } } /** @@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from. */ style: CSSStyle; } - /** - * CSS position-fallback rule representation. - */ - export interface CSSPositionFallbackRule { - name: Value; - /** - * List of keyframes. - */ - tryRules: CSSTryRule[]; - } /** * CSS @position-try rule representation. */ @@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`. * A list of CSS keyframed animations matching this node. */ cssKeyframesRules?: CSSKeyframesRule[]; - /** - * A list of CSS position fallbacks matching this node. - */ - cssPositionFallbackRules?: CSSPositionFallbackRule[]; /** * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. */ @@ -3496,7 +3561,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -3646,6 +3711,13 @@ The property is always undefined now. compatibilityMode?: CompatibilityMode; assignedSlot?: BackendNode; } + /** + * A structure to hold the top-level node of a detached tree and an array of its retained descendants. + */ + export interface DetachedElementInfo { + treeNode: Node; + retainedNodeIds: NodeId[]; + } /** * A structure holding an RGBA color. */ @@ -4693,6 +4765,17 @@ File wrapper. export type getFileInfoReturnValue = { path: string; } + /** + * Returns list of detached nodes + */ + export type getDetachedDomNodesParameters = { + } + export type getDetachedDomNodesReturnValue = { + /** + * The list of detached nodes + */ + detachedNodes: DetachedElementInfo[]; + } /** * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). @@ -11369,7 +11452,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com" */ fixed?: number; } - export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; + export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export interface InstallabilityErrorArgument { /** @@ -12298,6 +12381,10 @@ when bfcache navigation fails. * Frame's new url. */ url: string; + /** + * Navigation type + */ + navigationType: "fragment"|"historyApi"|"other"; } /** * Compressed image data requested by the `startScreencast`. @@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17270,6 +17357,101 @@ supported yet. } } + /** + * This domain allows configuring virtual Bluetooth devices to test +the web-bluetooth API. + */ + export module BluetoothEmulation { + /** + * Indicates the various states of Central. + */ + export type CentralState = "absent"|"powered-off"|"powered-on"; + /** + * Stores the manufacturer data + */ + export interface ManufacturerData { + /** + * Company identifier +https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +https://usb.org/developers + */ + key: number; + /** + * Manufacturer-specific data + */ + data: binary; + } + /** + * Stores the byte data of the advertisement packet sent by a Bluetooth device. + */ + export interface ScanRecord { + name?: string; + uuids?: string[]; + /** + * Stores the external appearance description of the device. + */ + appearance?: number; + /** + * Stores the transmission power of a broadcasting device. + */ + txPower?: number; + /** + * Key is the company identifier and the value is an array of bytes of +manufacturer specific data. + */ + manufacturerData?: ManufacturerData[]; + } + /** + * Stores the advertisement packet information that is sent by a Bluetooth device. + */ + export interface ScanEntry { + deviceAddress: string; + rssi: number; + scanRecord: ScanRecord; + } + + + /** + * Enable the BluetoothEmulation domain. + */ + export type enableParameters = { + /** + * State of the simulated central. + */ + state: CentralState; + } + export type enableReturnValue = { + } + /** + * Disable the BluetoothEmulation domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Simulates a peripheral with |address|, |name| and |knownServiceUuids| +that has already been connected to the system. + */ + export type simulatePreconnectedPeripheralParameters = { + address: string; + name: string; + manufacturerData: ManufacturerData[]; + knownServiceUuids: string[]; + } + export type simulatePreconnectedPeripheralReturnValue = { + } + /** + * Simulates an advertisement packet described in |entry| being received by +the central. + */ + export type simulateAdvertisementParameters = { + entry: ScanEntry; + } + export type simulateAdvertisementReturnValue = { + } + } + /** * This domain is deprecated - use Runtime or Log instead. */ @@ -20122,6 +20304,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; + "Extensions.getStorageItems": Extensions.getStorageItemsParameters; + "Extensions.removeStorageItems": Extensions.removeStorageItemsParameters; + "Extensions.clearStorageItems": Extensions.clearStorageItemsParameters; + "Extensions.setStorageItems": Extensions.setStorageItemsParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -20232,6 +20418,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getFileInfo": DOM.getFileInfoParameters; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeValue": DOM.setNodeValueParameters; @@ -20616,6 +20803,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; + "BluetoothEmulation.enable": BluetoothEmulation.enableParameters; + "BluetoothEmulation.disable": BluetoothEmulation.disableParameters; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20722,6 +20913,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; + "Extensions.getStorageItems": Extensions.getStorageItemsReturnValue; + "Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue; + "Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue; + "Extensions.setStorageItems": Extensions.setStorageItemsReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20832,6 +21027,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue; @@ -21216,6 +21412,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; + "BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue; + "BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; From 5c2e9962b440e72bff0196bf32b917c4dac632d4 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 2 Sep 2024 00:54:58 -0700 Subject: [PATCH 41/47] feat(chromium-tip-of-tree): roll to r1255 (#32376) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2f622c4875..2a096d49ca 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1254", + "revision": "1255", "installByDefault": false, - "browserVersion": "130.0.6681.0" + "browserVersion": "130.0.6684.0" }, { "name": "firefox", From f62f85ba51842d08e9ca1fa7bab9929dce74ac54 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 2 Sep 2024 13:42:15 +0200 Subject: [PATCH 42/47] fix(test runner): fix types to allow calling custom matchers on `expect.poll` (#32407) The `'should support custom matchers'` test asserts that the functionality works, but it was a type error. This PR updates the types so that it's allowed. Closes https://github.com/microsoft/playwright/issues/32408 --------- Signed-off-by: Simon Knott Co-authored-by: Dmitry Gozman --- packages/playwright/types/test.d.ts | 14 ++++++++------ tests/playwright-test/expect-poll.spec.ts | 13 +++++++++---- utils/generate_types/overrides-test.d.ts | 14 ++++++++------ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 5efdca4658..84fd21453d 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6567,15 +6567,17 @@ type MakeMatchers = { rejects: MakeMatchers, any, ExtendedMatchers>; } & IfAny, SpecificMatchers & ToUserMatcherObject>; +type PollMatchers = { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: PollMatchers; +} & BaseMatchers & ToUserMatcherObject; + export type Expect = { (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers, T> & { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: BaseMatchers, T>; - }; + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers,T, ExtendedMatchers> extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; configure: (configuration: { message?: string, diff --git a/tests/playwright-test/expect-poll.spec.ts b/tests/playwright-test/expect-poll.spec.ts index aa8bbde79d..344fdccdee 100644 --- a/tests/playwright-test/expect-poll.spec.ts +++ b/tests/playwright-test/expect-poll.spec.ts @@ -44,7 +44,10 @@ test('should poll predicate', async ({ runInlineTest }) => { test('should compile', async ({ runTSC }) => { const result = await runTSC({ 'a.spec.ts': ` - import { test, expect } from '@playwright/test'; + import { test, expect as baseExpect } from '@playwright/test'; + const expect = baseExpect.extend({ + toBeWithinRange() { return { message: () => "is within range", pass: true }; }, + }) test('should poll sync predicate', async ({ page }) => { let i = 0; test.expect.poll(() => ++i).toBe(3); @@ -57,6 +60,7 @@ test('should compile', async ({ runTSC }) => { return ++i; }).toBe(3); test.expect.poll(() => Promise.resolve(++i)).toBe(3); + expect.poll(() => Promise.resolve(++i)).toBeWithinRange(); // @ts-expect-error await test.expect.poll(() => page.locator('foo')).toBeEnabled(); @@ -172,7 +176,9 @@ test('should support .not predicate', async ({ runInlineTest }) => { test('should support custom matchers', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.spec.ts': ` - expect.extend({ + import { test, expect as baseExpect } from '@playwright/test'; + + const expect = baseExpect.extend({ toBeWithinRange(received, floor, ceiling) { const pass = received >= floor && received <= ceiling; if (pass) { @@ -191,10 +197,9 @@ test('should support custom matchers', async ({ runInlineTest }) => { }, }); - import { test, expect } from '@playwright/test'; test('should poll', async () => { let i = 0; - await test.expect.poll(() => ++i).toBeWithinRange(3, Number.MAX_VALUE); + await expect.poll(() => ++i).toBeWithinRange(3, Number.MAX_VALUE); }); ` }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 76fecc524a..a5d6eeb0b9 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -405,15 +405,17 @@ type MakeMatchers = { rejects: MakeMatchers, any, ExtendedMatchers>; } & IfAny, SpecificMatchers & ToUserMatcherObject>; +type PollMatchers = { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: PollMatchers; +} & BaseMatchers & ToUserMatcherObject; + export type Expect = { (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers, T> & { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: BaseMatchers, T>; - }; + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; configure: (configuration: { message?: string, From 731829335572a7bdd33182769fb37d7377a56e9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:16:46 +0200 Subject: [PATCH 43/47] chore(deps-dev): bump svelte from 4.2.9 to 4.2.19 (#32398) --- package-lock.json | 8 ++++---- packages/playwright-ct-svelte/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index eeae134a6a..8edb318925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6820,9 +6820,9 @@ } }, "node_modules/svelte": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz", - "integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -8060,7 +8060,7 @@ "playwright": "cli.js" }, "devDependencies": { - "svelte": "^4.2.8" + "svelte": "^4.2.19" }, "engines": { "node": ">=18" diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index 3f287315c2..88cf44d6ee 100644 --- a/packages/playwright-ct-svelte/package.json +++ b/packages/playwright-ct-svelte/package.json @@ -34,7 +34,7 @@ "@sveltejs/vite-plugin-svelte": "^3.0.1" }, "devDependencies": { - "svelte": "^4.2.8" + "svelte": "^4.2.19" }, "bin": { "playwright": "cli.js" From d145c4c91c192cb19f85102f83a2370a06e04a13 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:59:54 -0700 Subject: [PATCH 44/47] feat(webkit): roll to r2067 (#32415) --- packages/playwright-core/browsers.json | 2 +- packages/playwright/types/test.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2a096d49ca..0b29e895ad 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2066", + "revision": "2067", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 84fd21453d..2cf21f7350 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6577,7 +6577,7 @@ type PollMatchers = { export type Expect = { (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers,T, ExtendedMatchers> + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; configure: (configuration: { message?: string, From 787da9b5a53887b40574fb5d39942a3803ff2551 Mon Sep 17 00:00:00 2001 From: Kevin Jagodic <55063773+jkvn@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:57:57 +0200 Subject: [PATCH 45/47] docs(mock): fix routeFromHAR() arguments for Java (#32409) --- docs/src/mock.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/mock.md b/docs/src/mock.md index bd0c4e5c54..87ddf2ec96 100644 --- a/docs/src/mock.md +++ b/docs/src/mock.md @@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync(); ```java // Get the response from the HAR file -page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() +page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions() .setUrl("*/**/api/v1/fruits") .setUpdate(true) ); @@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs // Replay API requests from HAR. // Either use a matching response from the HAR, // or abort the request if nothing matches. -page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() +page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions() .setUrl("*/**/api/v1/fruits") .setUpdate(false) ); From b8c4a477ffe818e64ed8ea1aa33af110d840b2ab Mon Sep 17 00:00:00 2001 From: Przemyslaw Malolepszy Date: Tue, 3 Sep 2024 09:01:01 +0200 Subject: [PATCH 46/47] chore(docs): fix APIResponse.headersArray() desc (#32375) --- docs/src/api/class-apiresponse.md | 2 +- packages/playwright-core/types/types.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-apiresponse.md b/docs/src/api/class-apiresponse.md index 1297d2d4fa..5a901b76ba 100644 --- a/docs/src/api/class-apiresponse.md +++ b/docs/src/api/class-apiresponse.md @@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response. - `name` <[string]> Name of the header. - `value` <[string]> Value of the header. -An array with all the request HTTP headers associated with this response. Header names are not lower-cased. +An array with all the response HTTP headers associated with this response. Header names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. ## async method: APIResponse.json diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f7d662dea7..37281a1eb4 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17320,8 +17320,8 @@ export interface APIResponse { headers(): { [key: string]: string; }; /** - * An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers - * with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + * An array with all the response HTTP headers associated with this response. Header names are not lower-cased. + * Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. */ headersArray(): Array<{ /** From 221b77309c249bc7923ae58fcf285143abd87c7b Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:54:14 -0700 Subject: [PATCH 47/47] feat(webkit): roll to r2068 (#32417) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 0b29e895ad..6d395287a2 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2067", + "revision": "2068", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446",