From b4a9b247b4c2825d82864c1f381aaabe3129ad4e Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 20 Aug 2024 09:02:23 -0700 Subject: [PATCH 001/104] fix(role): make sure to ignore style/script/noscript/template (#32231) Even when these are a part of a hidden `aria-labelledby` traversal, all browsers ignore them anyway. --- .../src/server/injected/roleUtils.ts | 25 ++++++++++++------- tests/library/role-utils.spec.ts | 13 ++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 57681e0542..99f3cafa29 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) { return attr === null ? undefined : attr.toLowerCase() === 'true'; } +function isElementIgnoredForAria(element: Element) { + return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)); +} + // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles // Not implemented: // `Any descendants of elements that have the characteristic "Children Presentational: True"` // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden export function isElementHiddenForAria(element: Element): boolean { - if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element))) + if (isElementIgnoredForAria(element)) return true; const style = getElementComputedStyle(element); const isSlot = element.nodeName === 'SLOT'; @@ -496,14 +500,17 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt // step 2a. Hidden Not Referenced: If the current node is hidden and is: // Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden. // Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden. - if (!options.includeHidden && - !options.embeddedInLabelledBy?.hidden && - !options.embeddedInDescribedBy?.hidden && - !options?.embeddedInNativeTextAlternative?.hidden && - !options?.embeddedInLabel?.hidden && - isElementHiddenForAria(element)) { - options.visitedElements.add(element); - return ''; + if (!options.includeHidden) { + const isEmbeddedInHiddenReferenceTraversal = + !!options.embeddedInLabelledBy?.hidden || + !!options.embeddedInDescribedBy?.hidden || + !!options.embeddedInNativeTextAlternative?.hidden || + !!options.embeddedInLabel?.hidden; + if (isElementIgnoredForAria(element) || + (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) { + options.visitedElements.add(element); + return ''; + } } const labelledBy = getAriaLabelledByElements(element); diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 4acc7f8a85..067053ba11 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -462,6 +462,19 @@ test('should work with form and tricky input names', async ({ page }) => { expect.soft(await getNameAndRole(page, 'form')).toEqual({ role: 'form', name: 'my form' }); }); +test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ page }) => { + await page.setContent(` + + + `); + expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' }); +}); + function toArray(x: any): any[] { return Array.isArray(x) ? x : [x]; } From fc4d8f2bb61a17363967c128725f7f73469e81cb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 20 Aug 2024 10:56:46 -0700 Subject: [PATCH 002/104] chore: roll codicon (#32234) --- .../web/src/third_party/vscode/codicon.css | 98 +++++++++++++----- .../web/src/third_party/vscode/codicon.ttf | Bin 73464 -> 80340 bytes 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/packages/web/src/third_party/vscode/codicon.css b/packages/web/src/third_party/vscode/codicon.css index d1bc9ece13..41360ce21d 100644 --- a/packages/web/src/third_party/vscode/codicon.css +++ b/packages/web/src/third_party/vscode/codicon.css @@ -9,21 +9,20 @@ } .codicon { - font: normal normal normal 16px/1 codicon; - flex: none; - display: inline-block; - text-decoration: none; - text-rendering: auto; - text-align: center; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + font: normal normal normal 16px/1 codicon; + flex: none; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } -.codicon-blank:before { content: '\2003'; } .codicon-add:before { content: '\ea60'; } .codicon-plus:before { content: '\ea60'; } .codicon-gist-new:before { content: '\ea60'; } @@ -39,6 +38,7 @@ .codicon-record-keys:before { content: '\ea65'; } .codicon-keyboard:before { content: '\ea65'; } .codicon-tag:before { content: '\ea66'; } +.codicon-git-pull-request-label:before { content: '\ea66'; } .codicon-tag-add:before { content: '\ea66'; } .codicon-tag-remove:before { content: '\ea66'; } .codicon-person:before { content: '\ea67'; } @@ -74,6 +74,7 @@ .codicon-debug-breakpoint:before { content: '\ea71'; } .codicon-debug-breakpoint-disabled:before { content: '\ea71'; } .codicon-debug-hint:before { content: '\ea71'; } +.codicon-terminal-decoration-success:before { content: '\ea71'; } .codicon-primitive-square:before { content: '\ea72'; } .codicon-edit:before { content: '\ea73'; } .codicon-pencil:before { content: '\ea73'; } @@ -185,7 +186,6 @@ .codicon-check:before { content: '\eab2'; } .codicon-checklist:before { content: '\eab3'; } .codicon-chevron-down:before { content: '\eab4'; } -.codicon-drop-down-button:before { content: '\eab4'; } .codicon-chevron-left:before { content: '\eab5'; } .codicon-chevron-right:before { content: '\eab6'; } .codicon-chevron-up:before { content: '\eab7'; } @@ -193,9 +193,10 @@ .codicon-chrome-maximize:before { content: '\eab9'; } .codicon-chrome-minimize:before { content: '\eaba'; } .codicon-chrome-restore:before { content: '\eabb'; } -.codicon-circle:before { content: '\eabc'; } .codicon-circle-outline:before { content: '\eabc'; } +.codicon-circle:before { content: '\eabc'; } .codicon-debug-breakpoint-unverified:before { content: '\eabc'; } +.codicon-terminal-decoration-incomplete:before { content: '\eabc'; } .codicon-circle-slash:before { content: '\eabd'; } .codicon-circuit-board:before { content: '\eabe'; } .codicon-clear-all:before { content: '\eabf'; } @@ -207,7 +208,6 @@ .codicon-collapse-all:before { content: '\eac5'; } .codicon-color-mode:before { content: '\eac6'; } .codicon-comment-discussion:before { content: '\eac7'; } -.codicon-compare-changes:before { content: '\eafd'; } .codicon-credit-card:before { content: '\eac9'; } .codicon-dash:before { content: '\eacc'; } .codicon-dashboard:before { content: '\eacd'; } @@ -231,6 +231,7 @@ .codicon-diff-removed:before { content: '\eadf'; } .codicon-diff-renamed:before { content: '\eae0'; } .codicon-diff:before { content: '\eae1'; } +.codicon-diff-sidebyside:before { content: '\eae1'; } .codicon-discard:before { content: '\eae2'; } .codicon-editor-layout:before { content: '\eae3'; } .codicon-empty-window:before { content: '\eae4'; } @@ -259,6 +260,7 @@ .codicon-gist:before { content: '\eafb'; } .codicon-git-commit:before { content: '\eafc'; } .codicon-git-compare:before { content: '\eafd'; } +.codicon-compare-changes:before { content: '\eafd'; } .codicon-git-merge:before { content: '\eafe'; } .codicon-github-action:before { content: '\eaff'; } .codicon-github-alt:before { content: '\eb00'; } @@ -271,13 +273,11 @@ .codicon-horizontal-rule:before { content: '\eb07'; } .codicon-hubot:before { content: '\eb08'; } .codicon-inbox:before { content: '\eb09'; } -.codicon-issue-closed:before { content: '\eba4'; } .codicon-issue-reopened:before { content: '\eb0b'; } .codicon-issues:before { content: '\eb0c'; } .codicon-italic:before { content: '\eb0d'; } .codicon-jersey:before { content: '\eb0e'; } .codicon-json:before { content: '\eb0f'; } -.codicon-bracket:before { content: '\eb0f'; } .codicon-kebab-vertical:before { content: '\eb10'; } .codicon-key:before { content: '\eb11'; } .codicon-law:before { content: '\eb12'; } @@ -295,6 +295,7 @@ .codicon-megaphone:before { content: '\eb1e'; } .codicon-mention:before { content: '\eb1f'; } .codicon-milestone:before { content: '\eb20'; } +.codicon-git-pull-request-milestone:before { content: '\eb20'; } .codicon-mortar-board:before { content: '\eb21'; } .codicon-move:before { content: '\eb22'; } .codicon-multiple-windows:before { content: '\eb23'; } @@ -355,7 +356,6 @@ .codicon-star-half:before { content: '\eb5a'; } .codicon-symbol-class:before { content: '\eb5b'; } .codicon-symbol-color:before { content: '\eb5c'; } -.codicon-symbol-customcolor:before { content: '\eb5c'; } .codicon-symbol-constant:before { content: '\eb5d'; } .codicon-symbol-enum-member:before { content: '\eb5e'; } .codicon-symbol-field:before { content: '\eb5f'; } @@ -407,6 +407,7 @@ .codicon-debug-stackframe-active:before { content: '\eb89'; } .codicon-circle-small-filled:before { content: '\eb8a'; } .codicon-debug-stackframe-dot:before { content: '\eb8a'; } +.codicon-terminal-decoration-mark:before { content: '\eb8a'; } .codicon-debug-stackframe:before { content: '\eb8b'; } .codicon-debug-stackframe-focused:before { content: '\eb8b'; } .codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; } @@ -414,14 +415,17 @@ .codicon-debug-reverse-continue:before { content: '\eb8e'; } .codicon-debug-step-back:before { content: '\eb8f'; } .codicon-debug-restart-frame:before { content: '\eb90'; } +.codicon-debug-alt:before { content: '\eb91'; } .codicon-call-incoming:before { content: '\eb92'; } .codicon-call-outgoing:before { content: '\eb93'; } .codicon-menu:before { content: '\eb94'; } .codicon-expand-all:before { content: '\eb95'; } .codicon-feedback:before { content: '\eb96'; } +.codicon-git-pull-request-reviewer:before { content: '\eb96'; } .codicon-group-by-ref-type:before { content: '\eb97'; } .codicon-ungroup-by-ref-type:before { content: '\eb98'; } .codicon-account:before { content: '\eb99'; } +.codicon-git-pull-request-assignee:before { content: '\eb99'; } .codicon-bell-dot:before { content: '\eb9a'; } .codicon-debug-console:before { content: '\eb9b'; } .codicon-library:before { content: '\eb9c'; } @@ -430,10 +434,10 @@ .codicon-sync-ignored:before { content: '\eb9f'; } .codicon-pinned:before { content: '\eba0'; } .codicon-github-inverted:before { content: '\eba1'; } -.codicon-debug-alt:before { content: '\eb91'; } .codicon-server-process:before { content: '\eba2'; } .codicon-server-environment:before { content: '\eba3'; } .codicon-pass:before { content: '\eba4'; } +.codicon-issue-closed:before { content: '\eba4'; } .codicon-stop-circle:before { content: '\eba5'; } .codicon-play-circle:before { content: '\eba6'; } .codicon-record:before { content: '\eba7'; } @@ -466,7 +470,7 @@ .codicon-debug-rerun:before { content: '\ebc0'; } .codicon-workspace-trusted:before { content: '\ebc1'; } .codicon-workspace-untrusted:before { content: '\ebc2'; } -.codicon-workspace-unspecified:before { content: '\ebc3'; } +.codicon-workspace-unknown:before { content: '\ebc3'; } .codicon-terminal-cmd:before { content: '\ebc4'; } .codicon-terminal-debian:before { content: '\ebc5'; } .codicon-terminal-linux:before { content: '\ebc6'; } @@ -500,6 +504,7 @@ .codicon-graph-line:before { content: '\ebe2'; } .codicon-graph-scatter:before { content: '\ebe3'; } .codicon-pie-chart:before { content: '\ebe4'; } +.codicon-bracket:before { content: '\eb0f'; } .codicon-bracket-dot:before { content: '\ebe5'; } .codicon-bracket-error:before { content: '\ebe6'; } .codicon-lock-small:before { content: '\ebe7'; } @@ -519,20 +524,26 @@ .codicon-layout-statusbar:before { content: '\ebf5'; } .codicon-layout-menubar:before { content: '\ebf6'; } .codicon-layout-centered:before { content: '\ebf7'; } -.codicon-layout-sidebar-right-off:before { content: '\ec00'; } -.codicon-layout-panel-off:before { content: '\ec01'; } -.codicon-layout-sidebar-left-off:before { content: '\ec02'; } .codicon-target:before { content: '\ebf8'; } .codicon-indent:before { content: '\ebf9'; } .codicon-record-small:before { content: '\ebfa'; } .codicon-error-small:before { content: '\ebfb'; } +.codicon-terminal-decoration-error:before { content: '\ebfb'; } .codicon-arrow-circle-down:before { content: '\ebfc'; } .codicon-arrow-circle-left:before { content: '\ebfd'; } .codicon-arrow-circle-right:before { content: '\ebfe'; } .codicon-arrow-circle-up:before { content: '\ebff'; } +.codicon-layout-sidebar-right-off:before { content: '\ec00'; } +.codicon-layout-panel-off:before { content: '\ec01'; } +.codicon-layout-sidebar-left-off:before { content: '\ec02'; } +.codicon-blank:before { content: '\ec03'; } .codicon-heart-filled:before { content: '\ec04'; } .codicon-map:before { content: '\ec05'; } +.codicon-map-horizontal:before { content: '\ec05'; } +.codicon-fold-horizontal:before { content: '\ec05'; } .codicon-map-filled:before { content: '\ec06'; } +.codicon-map-horizontal-filled:before { content: '\ec06'; } +.codicon-fold-horizontal-filled:before { content: '\ec06'; } .codicon-circle-small:before { content: '\ec07'; } .codicon-bell-slash:before { content: '\ec08'; } .codicon-bell-slash-dot:before { content: '\ec09'; } @@ -544,3 +555,42 @@ .codicon-send:before { content: '\ec0f'; } .codicon-sparkle:before { content: '\ec10'; } .codicon-insert:before { content: '\ec11'; } +.codicon-mic:before { content: '\ec12'; } +.codicon-thumbsdown-filled:before { content: '\ec13'; } +.codicon-thumbsup-filled:before { content: '\ec14'; } +.codicon-coffee:before { content: '\ec15'; } +.codicon-snake:before { content: '\ec16'; } +.codicon-game:before { content: '\ec17'; } +.codicon-vr:before { content: '\ec18'; } +.codicon-chip:before { content: '\ec19'; } +.codicon-piano:before { content: '\ec1a'; } +.codicon-music:before { content: '\ec1b'; } +.codicon-mic-filled:before { content: '\ec1c'; } +.codicon-repo-fetch:before { content: '\ec1d'; } +.codicon-copilot:before { content: '\ec1e'; } +.codicon-lightbulb-sparkle:before { content: '\ec1f'; } +.codicon-robot:before { content: '\ec20'; } +.codicon-sparkle-filled:before { content: '\ec21'; } +.codicon-diff-single:before { content: '\ec22'; } +.codicon-diff-multiple:before { content: '\ec23'; } +.codicon-surround-with:before { content: '\ec24'; } +.codicon-share:before { content: '\ec25'; } +.codicon-git-stash:before { content: '\ec26'; } +.codicon-git-stash-apply:before { content: '\ec27'; } +.codicon-git-stash-pop:before { content: '\ec28'; } +.codicon-vscode:before { content: '\ec29'; } +.codicon-vscode-insiders:before { content: '\ec2a'; } +.codicon-code-oss:before { content: '\ec2b'; } +.codicon-run-coverage:before { content: '\ec2c'; } +.codicon-run-all-coverage:before { content: '\ec2d'; } +.codicon-coverage:before { content: '\ec2e'; } +.codicon-github-project:before { content: '\ec2f'; } +.codicon-map-vertical:before { content: '\ec30'; } +.codicon-fold-vertical:before { content: '\ec30'; } +.codicon-map-vertical-filled:before { content: '\ec31'; } +.codicon-fold-vertical-filled:before { content: '\ec31'; } +.codicon-go-to-search:before { content: '\ec32'; } +.codicon-percentage:before { content: '\ec33'; } +.codicon-sort-percentage:before { content: '\ec33'; } +.codicon-attach:before { content: '\ec34'; } +.codicon-git-fetch:before { content: '\f101'; } diff --git a/packages/web/src/third_party/vscode/codicon.ttf b/packages/web/src/third_party/vscode/codicon.ttf index c4a33a4d5669d1c4f6ac274fae5792096bdb082b..27ee4c68caef1cd22342f481420d6dbda1648012 100644 GIT binary patch delta 14648 zcma)@31C#!_5aU#GxOf;GnqF__HB}xup}Yt3|oN69z;|`M2sYm1q>uW*pxbmOD&3` zQ9x=HtsB-o3W#yv5z)5RT7UkmP;2db`MI>VT59$Gy>kODKimH=$>*K>*1Ml`?s+d? zJZ^pCuhwnFsitW}d?gVrTD-ia=cJ!bxPZt}LX`b(N7sh-zbr4?Koq}?=<{G_Tg#I9 zJ9eLrYu^L)oj9PJt^5P&0Z`Jpymx)>q_2i|vXR=i!u<$a2&2mi0Z# z$9xgaXX5(86)nr#`f@&hw23%>KMsD;)4jTPapu%#hzlPe$||{LAnG|xjBCN%fBs|T zHG@@rxG$eKI9{P&;qY*JrEfP^D4%lw>)g(LS6PF7U-96O>>848A^V@O7Fuv}PT#xQ z-fNX$-*xst$`(sMovejqNvB3aI*D-HYWWNq1jvsgigCGdrSYQivgt5q57vIlUw=(x zEHbtjR~s)IhfJ3_M;^l+-~M6T-%9QDCcVjX_)W`AIBB6zc_vSzhdIHi>$#Lyu$Oyj zHb2XYco{FHYp9+_(Pi`+eMOVFk2X+*pP--8<$Mz7agalN7rjh(&;ak|O>_lc!yEW= zzKqZ3pYS%mf-j*bIK^8@AuHKX85g<9LtgTsDgnx&Jj$m6T1e+mbBfMI`4&?#MX7{J zDMrI+1dXI}s-Q|5Mb%V8wNyvrXf%zd2AV(PPaU+B&Z8By zih5}ct)+Fep3bKWXe0fIE~FG)M4Ranx|DuQKcOq>YT8D9bRAt!sqM6bcG3;Bi*BTw z=w{kYx6rNhbGn`GqCIpsF8T%CL-)~sdXyfgr|3C)o?fDZ^a>rKSLxUE2K@$&`&)X8 zen)T9$MhHaEB&4RLC5Hy^d%EBtE{n|J?vvYXRyu@&g2}<jBzJ9!V^&A;G# z_+EaH`}tvhg!l8Ke1He|aej`U=NI@zeu)q9tMnQDjXtL@Xc4tg4~?Ofw1lswZdy&t z>1>Kq89QhzT}v+)WqLCSK1Sw2vO4z5Fykl}%IlOrA?~ z=`pslg`VUn>2xpDjoP=;eXMVj+mUjM2_1rZ5*1mGa#CJBciFOonjlCfC= z{Yb{e5@<>?E|CZ`Wn3!Z-H@VkfW`BEETJnPML_^x1Lb9G!46CD7qy z+$MoGC*yVr^g0=$z5p7Yj5{UJ^<;?l0n`k+M*{Uv#@!Ma0y2Iffk`0a9tn&C8JH3& zv4Nc+<30&21{wEDU^~coKmsd5#)A^r6EYr>z_O6hFM*9A<6#M`4H=I}V0XyaD}eFa4g&P`?cvb={NXBy#*h4a&m%uWT@qz?4l8hH6u$E-JEP>r5^9e-!=yN7VjG?0g`BBSFTWBycIn z_)r4pf{Z^);6adKNZ@dgVM=H_uE z3mJcrz=0v-uM*k~`I!Vx4HT+(g$+C+ zGX5z^*X&EA0lXwK{w0CGM8;PVcur(|ErAaO<0^qSMJ7w&SCOem=qX5x1iluTRtdZ= zN|`q7061V|suH+jWNH#PWn|hVaL>qeNZ_cE>5{-@BhxK`^G2pe0ymCKuLKSqnLY_z zJ2L%g(Ee{Bks*PxEkppxCZGVxEARlxDM$dxE|>tcr4Nh36FzJN_afvXbCq!j*;*L%zsnd z1b8B(xC!tiNO2S3Mo4iJ;K`5^Bs>*T+yi(zq__w03`lVg;3mkEBs>dJ+z)s*E zQBfdb6K1>Uf7F(*!cK>TcR-3-1KtU_RKmAFE|c)BkfJpJ-v-$w;X5Ej%K*L;a)pHV zKz2*`Zpa=9{{sDQisl1+52R>5!1qFm<^%j7WUqw#Aw^RHei%|TCE!OO*GYIk0xtc%IqF2M?!Ofhx@GH;QP=wmTB0gD;%a|yRXih&Ba81i-rr^KMSL&75< z@04IQPv%_`S^z0VFu)3*OfiB1O@{o1gq@IL00S)l$-GyBKmeKdN$6|H`z4$W`GDwu zY!D|PQ;c$e;2W8u$p8<76eAuWia@3q@c{7zGR24oh%At~S3)x(_eofE*M13$^I|3d zMt-JfCBXNI`7de&5SAcQObbBbA~7ofpAPwigy%s%DM9#x%%>#O1u1F@_+`jvB>W2G zvl2cC`J9AzK|U{-h#S!VrWi1Q#Ta-|!eR`F=>=Gf0r6nKVho6f0luLxKR=QZMSN9) z$O)N;B_y8kngr1lGGCVUd5dI?bpAy>A zx2$k%3UwP~3BqACs7MePqd|)VAu<}YO6WyMn}mf_B?zF=pe8{mjRx%!1l4HJAwhVJ z2AvWF+Gx;)(IYm9xzV6oLj8~)3F2@x=#|hcNS}mTApH_V>1Z%RLa##xBrKk%OZexI z=u`osc{GR-Dxhf?J%i{~0YZE<7?IGOkeL#M{b*2>98eXcs1e{8WR8SIrlOSqqe6pu z5(ElqFkga@Aq^Ht2m@iTP=L|HqA4&G1&AopV6gZfOvrKxBAGN;Awf)&1}i1>8007k zi?JbE1|Z@|gVhoeEmtE!6qE*QC5VUOg@r_+d;3-u4NqCJElJB3%llTpwcUEimTOyv z-<>u?Rn!RJEtp)H}$!gIrW!-Eljq%4w*OpnZsbVk-jc4cN}R%g!2+?D0bs?VC9 zwK?lh)|c7g?1t#m=jX1?y)kzn_r1LAy!yQPd6(xM z$onAQohl`Iz$3%BWpNW1_ zqLk#9+*6sZEbB&?e5x->h7p}tG=QBj{0MXafxM#&52!!{fYOI)?_R>IoX=r zl-!xzn>?Hx9Bmz)J9^&eYe&C7=E5;gj5#`X^4NuA*N^j#i;bH)ZvD7-#vL0!YyA50 zcZ?rw2sBhT%xqZFu&rUB;pl|GgjEx+oN#nv=ENlvubOypk}|0=HEHdn7bbnt7;9YA zcwyr+jmAkmC*3`{Wb%qBr%l;1WngOY)TvXioBGDI`e|29J2>4xeaZB9W{jG#dB)Be zFU|DMoI104=4CVQXi}TTG(9(~c-AGe_MWVsTz&G+lV3XdquEnux6bZ4W%w!EPI-7v z#hkn6d~)idQ+J;F`e|jS%{%R~)5E9toPKC-DmHia+*Nb8&;95O?-|u+Tz1CMdAaiz z&f7e1@4QdW9CPN*GY_8m;rx>M51f^K)^%qcTd-ik>u1}~Zan+Oh5m)}7Cv*%%w~Uc zMf0q4$DG@B?&fpvI`=P&l8fdq+Op_yOLNPnmIE!vTJu|Hwsy7Nx;VMGYw^y-?=IQC zY~_NLyH>uvs$|u&Rd27hu0Cz`TfM!#xAp#g zP1%|yYYwdWa_#iBoonA-m%nb&x&!OpUZ1&s^7@Py})_F>S9 zGGI$#PGSb!$BXb#j#qXYh%So2gB79@EWT^QGIyD7_P2go-wL8W3xRo z!`^GL*)6@^+%oTGugjHJR_0r-uxh`$(_dKO-)Q$o?A?mprgVE^Ii8K)SS;7+^mbb8 zHe?Cgfqb+3w+;WBKP7iG_=pK%Nc5UeP2CWYP)#U$0xcSf)}0_;7p1_6htsAtrMCfjs3-7zFefMIR?D)Jq2kcMo}$R;NRjSv`~7ata;SFAZgaYwPSuY0 zsH)TL_`eoUDRiad31pnanIInv>*=NJjq+CoQt2RPpv4Z6jVxXW|7`2Udj_ zV5v}zRtaJRkLTL@2!^nVU5whgcsP=%kDwt!K`l~~FJ6LcUcAs%HT)LG(O8vuhbtPg zwmwmlh}G35>J#-o4vBlJ6ZI3gF0Mw!g}6RioJz#J=?CK~HIk2ZtF7QQs@?9k+vkam z+g`1(B_kH~du$f=`Yej3xm3;V8Ftz*k6Tk+nx(8Tq=~uVr~N zZK}m#v)iniEfBD2RwNx3)t2ea()?~cKSQ_LHY;;jwfTHDmFFm`E}p5k+HHE7Hy8}t z?cre1Tc(StIy5T9^wiMSu;dfCCer7fF&c9OBNZP5l!+IA2_MH3kwm0{>(CU@Vl5V} zuc^*QXK3{r##n8GlCOAKt8QSNFGlO-bL7rSZ>}d)HpAfzP(c9FA9rO=Me=bG@NaBOIBYKr}yKv+0p|MVZSx;`qfSxNqoU+`;|hXWZqN z^}q}2Y`g>Z!a>ca(!OJl6-5&T9MURzZ&KCw==#c)dy*@gpbKI#y|lUciV?N|C;O@f zg4*E`p2zgV$-Y0Fbz$G1AM5DvT+pnh(gRRDGX$+Q8Pie=B_dEB!Zhj+KrF9eHRT+??l7!+w`L z;Pgavr&Eu3oVv^95BtPz@tpqKTds4Y(%p~{b(_a#8`&<5Jzh;6JjGU24WF04QuQ3Srdr{t`3Kqwg;m@{0)gnQBh=j zq-b$b2`4|J10W=TrR9k`V;59WSMM5P~C#>STe+lRfl2GFl5nXiHH^#Dji2K4*boY z+4{-eWc>mrGGREr3W zA^JV)<>FXe7>PP)o9cS;d|}8g)~h{@oc2Sb+%xdcUqpOYD{HrZe_09ouQ>wQF*DWzRZ)E6b8=W z5VkLw=_y+=#WSTit01>BIVwlVfDQ`A!@-OeTpAgUT!U)9cmAirp_1jHWMlD; z7VA)9Y~XVx>Y+TX#a^oh<(Qs+`9f7RC>T(&cu1IRVcKdMxF%5@!R3kino(84dP&11 z^ouZ9_{FJekqDkJaJ6?~c3BlggY|Uz^-yL=_qkMN4{WnVbJ+524k#JLp;;{XinY|@ z(iCfr>UDWEn`XDW>`trAX?JN34S(1xaHxu=s18Lro>=d++f*2PkI$WcwCeKdA$K6p z?TmQD!!05gJFC80#cEZCsdk=|>2ul?hAMMrSF;=Vi`4Vr)gfABo zQ3<$<7*LVu1dd0=K*msjh1YPjYh835{+5iiZ?#vBOmDYg|6fTx**Mkfwlz7XPV-{x zO;Ug7j=PlphzXVJwa!!wK zDCY{c{G0qca$~vps~obhFcLZct0-HW^xRy%2}65#MnOhX`qDrXl3)Gl`$g+_? zOo*7bZZvQl?kdVx8V$wLbE*LzrX~!WumeJI#GoFfy%3sH3u$#jr)%q>Z#_O_9M7q6 zWx(BIf7N!!(2+~v!B>i-e*d0I2c{I~=w~ExQ69M7;n?bAVb_8y?SZ+@Qb%m7xDp;? z`Hj1eTR9Ei99hzZ%ttZd%S5!qar3JQ+a;`7MA|F3F2|2d-tOJ5q|1}xP8Tj`hZ{wU zWMsI|QpmGmz&)@@W+&WMlD#b{I+p38VR^Ty+m5#6%-`##| zl>_^M1w|1o3ePc8AHEpF_9erFCSJh2m>bmUCwa~5!txSKiD+M zp%OKHH!oz>yg@dJXaH#uW&N|Lc+t12|NpP_5BJHJ?d|ExF8^N{BftK44lL(yjW;`7 zuWYpYD15Qhj?ATtSRK}cG7xhw@=YS`J>pb-0**6$sYE_Lrp1&DVeOj7@B~hn!+4$0 zgk!l5%2p`gv|B2CQlWQhW2xx&a6uSe3RlvYGvU0PW9%nkN$nMN^vf@$Ucjwsyg@Ft` zn&ENh^-X{B2CnNn`ed~#GwdsPszf`b)QeSHLEkA)4Oc^@I+FdbK3Ql_No^xkZ5BFv zocOd23v+l|FaetMjNxO!6bMHKiwfE4(I2=*EXLF8j-g9dxvxKrjmOQJ< zR!e~;qsk7Oops#gMuj6x)yQA3r~I9G~%v ziNjeiyly!Aq)gZ1s4rb0v|_(QO1f-I%;BU!{4Z2 zHKbLKidHo@!dG?bk3oeGFY<-7DO0qNFXXf;tj`J-g_z-Q`63PnTU2H8WJR@%W2LX(&Nss znEH0S)R>Pd9G`sgVl@HM!!3z=Qtg9msqXY z_lS+|vgbM+F}L4|TD-&;DspZ3Ar*777Pmjta9qmnbCB#QN$eY_te#S8k(9|3k1aekt3tUk@3zNy>Hz| zTU^pB$%a>w)93v-Z&bV8YqJJwHMho=sNLeR-|6!CTv;pZQCEgz7^^O?Iy0gBB zr>{fnyX9lO&;RlD^S|{Vlqz2lV?STP3PeN^M4T}_O6p(u!RKc$w_{oB6gg-vkI2C} zl!4lR%f|;iDG{N_!pFKIOgwEUMv?Y@F$fU_72#^RK7!{WA`@x1TST?d;orn8&@iPS zE+e3F^nR1BX+lJzD57Qfe9o}b=fjc@yXmo;(%cTex7h1o*Heq*83Q zpGbX{cH|NARGrSE%qB!b!@eeeSamrZF2rM+d|^a1lcjQ^$nA0OM8lT`Hkio*5m(Sf zSJsB&qHf+425vnjs0dvqglVgf#WnZ@s;GSJzEjFZUa6q7ENs2S=CxXDtyZ>KKMWV- z$fJSj1w~`kUCR`U+v2cXXH$pmbQ&4v4=_4Y3cyX9eMQBuH zl0q)fU+I?vXi^kGOePV8*2G!|s{N3vS}co(r}MLvlvq$0yE44hYPVKft>b3Q7;m%2 zEf!Yh7s^5u6{93>i&A(N4ra04x^Pl5Q8B}6#{`zaZ!w2mh_eWnp}*r_6%MZ2v0de@ z5G5M^h2-$QXEdH`nXcTU+^gKD+^;;KJg7XR^eYc5k0^VUeae32QRRU0m@=R|t~{YU zsXV1TtvsVVt30PXue_kVsJx`StQ=HcQ4T4uDu5x33YLIdwbgssQ67`ZE<(c(ys1aoaotr%htB7YUya( zcc+&>SG=AjOWWJam#^vSUE0&tw&SJ@&UU-f$5t<0(b3hmZ+`~<`NV>MWZMzZx!Ts= z*1NcK$7G#vD)V(L?JZy3+p@Z|yrrk7YlG*T{hsa~$BDfiM|B=n>&1TOn%453Ro%);|+p~D|DF3Q8xUs9N{CM+fLkAo}wzhS3uXBsk@zXnwWbtm*wz{)rRojk>vbg~1 z6)oqr?bwa9&pTAjp;GU7GMg8AJ*(HOTGhQ~#gg)MXvuwutd3O5Gtj2XTURgXUbkZC tV&^wUd@{MFXXwF`%`UC{c>IRBU1nmvX60RK)P?ex;xYnRMzJoXlleF8Z1QO)87)iWEL zrt;h~R9wRsJhMH2;r>y|u352q-M8_-T;%t)0p8}?m5Uav+Fteo(0w%!QMF>hx_Zx_ zjke3`f4# z{ym6z2NCUieHo(xz8)E|k6)F*{}F)MY_^(L&FeN`E89B!@(W*?Z?>4b%S`0Wig(V`BK-A5SVv-Mhdf+@{>aAw6krfaP=>)6f}t3W5g3Kh zxD;bB36pUdreFaUVKHh@i#n`CJ+8tkti~Fw#Wh%mYjGW}$9mj=Fm4RvX55C&xE)(@ z2kyjO*oN)cft}ceM(oBtxEJ@~0X&E%Jj8SM;$iH=V|X0D!|(Al4&zz;fp^byIEI(; z3XbDdyoT5D2L8l*>>U1%&+$(fShV45e1r4&9vAR0{2LeXAN(v~k3@)9d=f1&(m|3X zMN%bA(xsDhmM+p&x=FV5kQ~XCp3+NtOCRYcdD36bmM#xAR zE#ss@#>)hmB$H*TOq1y{LuSexnJe?;3Rx(Nq?$KdjVzZHVOc5bcaT4g)$*i>ABPb45^at(&%AR8ns8|6B5K&8atZrQ`z z`6axCH*o?JF#+R|gg@dDJc_AUC_`{RZ}?=i;3-L%hi=Hg{~#p0L~t5!;~o43f9BKh zSG{HbAnFc6M)&s>sUpFBC}o=VmJ%r4+Q;Vk1qB?A~2DL#uZ)e7#jiW_U zajBAc#u~+^7v{>a+wc$k#JEgJ72|S+3dUL`4>GP$aw%h-l1j#vihTe~y<%?wbCp6N z<0{1-0_JLk@CNR%4|UjWz^qa1KwwbTHM#5d}Z($x*?0I4KDfYoIk0|!WF#8qzWtc}5duW(u#l9Nm zfMTx=b5ODWhIve}Cx>}lu}_Ejt!oHw*uBHJN+fplFuzmm@?l*0iJd>p?-hpum_v#K z0!)kI&;aw4;vfO@wBm39b69cUfN516Lf8<`@PIg)z&xusuE4m~CXO&Le^4B2V2&z| zIxx>Ejz2KR6h|VM=M~2!m=_dBCzux%$0?YX9Czyt=guD$2QQeH6^AjHR}=>{nB$5= z8_cVUgB;B3io+eu3B`dA=1s*R5aun#!4T$c#bJ@R{yW?dM@X1=6~{`LlZvAz%%2s< zPnc7RBPk3gN)E?VnD-P%SD3#ljeVe3pQ49&IKD(oPEK@DbB-S;}vIQunFy0|9pIG z2gPX`Y@*_X4K}23hA~NT5(k^CIGuw{QJmPprYcVHVAB*Qd$1jqt16qWI01z1#J_Jl z@#=XG*v^WxLf9^f^F!FKiZeynZi;h8*bK$lBW!oYc_eJ6;*1hDOL1-qo2|fRv^^B( zo7~A!;@T)ziEE0UN?cR)QWDMBTZyawK1y5-_Eq9)r=Jp6J9$c6?OdYVg30!G?az(4 zD#=&E+OPwZxEf)X>xiq7LM5(7ij=q-DOTdHutbTw!cryf0tYH_7dS{sPd@*)j0Ys{ z3I{83S2#q8yTYML+*J)z;;w4A5?+KIp~PLtNF}cDqm;P9k5=Lef2qUv7gzdnC9d>i zl(^E5RpLrNPKhf_g%Veo@k(4_CMa=*nW)4Src#M3z+@%v`@)xTL*l-0iW2vQQW37@~7~L0>+{##|WHY1t8zjGBtXHyy?QgGAay#QH<60#<8Lv^&$mlMDWH+O`2$FjkuTyd#PaHz;|Caf6b*jIOXG z4>N95vXAjbCC!YkmWaziSXWELg(0k~CE^kh*3}YmvB>A&RTFXD2z#623KG^;8F4KM z>#B^ns)XI5xW0sSHAY-%!nztGxO?Uf#nmV5or>#F*t-;0q_D15x3T@XIECG=xJ-q0 zRZLv4!n!IZE?r?=6%!Y+uy-piXJL0KE^J|4)e~A7UDXp8yRfe6$t^AHy^0H9*!x_S zvMsn4hIP$A_=NERB>~1gO3D}?RO0T9CdJh=tZOR5Zy8-vk&I<@O+_+>(KQuGkns`4 z^)`3*D@kH}RB;Us+pI8+@qpsG9QL5%3LW+_#kD%@2x~eBx!}yF6*IdskareLx zC3m>z{|`JMW)onKD!3YXPRSs~V@d`yKChT{fPFz>3FC`OT-Cm$#MR~>mALPGS;_T` zuQ|+K!JTvF_i-QSJ#N#FslOlzG8j__5;OC z3+#uAITzT!DP~__KT^!Yz<#W-knyx)ZU)vp4#X@C>=}jmjAs=yH*CanJRl}>P9&n?W6N0sh*&*0AhmFHT5v={p6uB-pPN6H2h(D5jQRzg0{!!G5QhZh}3pn0SKyUNHp)dqKIZu^spWHw4$F z?m;AGs9^u4@I2#Bidieze=BTisOVl6W`nfxx*TS)(B@IhXQ3@ZF|&m>-tZ1{Txg3@ z%yyy8tC;sfn@=$VhBm)qE(~n}#jF_Gq80OFXo~?V-G-?$w8bhMU<@jz&(OwO+QC@H zcqQeGY*dHIHMDh5c(K8g-8<~L7*QF~9C0eLcjUInGf{c`KPT#ms5iXJd|iF>d|&vz z{yP7e!0zY?(L17#M4yOBiphy7jj4#46SFpEbIi_|LouggV`J-M_s1TIJr|4)t_vOv z{unnPZbIDNxQp?l<7?t~#ve&YOPG?dKjEVe=up?;L}E(fpv3CL`owdgaB-+92}ui+ zb|)Q8I+gTk()na0CnaYlmnXlRd^-8Zl=76rDd$qor{<-uOKnblDb=KT(>kYBrq!qI zOnWWuOvm{hw{<+69+{q*J~q83eQSDi`WxvNJN4{T(P>quJ)Mqs`nhve=l8orcggND zr^}Hp7rWMVeWRPF+q7;+Ga@r8GPZ;>zV4pbeR%gN-Rru4o0*W=H*;*}uFO+e{;cAx z=Io5@+U#9DQhMB<(?93&+``-qxi9ss>bbL5ajz$PSM)y7C%RA9KGl6T_j#(%`M&9W z*Y$1g7u~PC-|Bux^3wAv@@n!9=b205E?Ixc$^LQuhxXr^KRSPH{zn5w57<56!vcRn zMnP#oxT;`7!QO(G3(ggyuxnv);k3fq!Yze+3y&0@FX~^kspzGm&x^+v&o5qIl33Ea zq@rYf$(fR$ODC7EFFjcL^}v*Y#RDq`UN^95;5&o*52_f{H0X^oZ`shY4P}SQJ{_Dm zc*5XigP$1u-jF#%b`4D!I&0X_VQYsq504u@VED%2=SB<~u`xX2)W``Vn?`;+%0H@f z)P_-qN4+)b!swLIYeyfvwD8g$m!2=rDPLQ@cZ`2b>6m$A4v#rG=FHf8$6;LdxIN=O zuc)dxI^I8i{`fn`ADfUiVfBQ!CR~`9K5^*8c@sBHJYAVqIkR#@<)O;9NyU?PPC7p6 z)5#f=AHOW+vTc{WKc#ZYty5l@nm#o=f9i>8JElE3?XBrYr+--$S=GC0b=A(Q<1?aX z+&kmwj0-c1XU>{=>&)hvr!QZ9dGqC`W~I+MIqTx=^|POulR2ki&dqavnp-^gy17T^ z{y1;wye;$2UE#fA@)cjqAHHz-q6^iT)#cTPs^44eS)9JOcJZ+#bC&E{vVY0pCC8V% zx3qZa+NFnUP*_u5bGYWrm3db_wQTaTjmu6hk6b={`L5;f*Jjs_uH936toGuHo+~!3 zXsWBOJGFA{$}j5&)YrA_&3!(q<%50~6IvDzd&k#sV9eN-z_>Y44R21WY+iT|@ zdm|zPDbZ=sDFI)k$LG)W&-cgsB4WMq2`+lQvCnnSPw1YWy3ZdKxiYFlOloJpKb{}v zcf|Yso%5n2qk};o4+BwAf#k%HH#Wu-;p1B!V!dAO_&UUUWB1*ZpP!JPzNo8+!g;4?%(Zed1c=LPs^(2Zl0F>$LBm%`*?-!DtU`N_4E+|SZ$#t^ITce{#l$MqrC>`SU`@KVk1d~LP`UHFA2eU&t!Ga#a!Z_&|I2%aJ z3GLby3PeRm1rjAGcqlG3D?XUriKpGUjpwEJ2o^Qm^!m9lyJeP%zaue4+K<`IB6iTD zT$VqSoYh_^Eum|cBo;22E#u0UnVXZFGep{3u}pkLsqN)YF7WXxN__5!7cQ65iJ&>Qx)zFZp8e~rQHwd-<%MNg(il}1TI=g_W^ zybOtmitt49LlKhbKH)vY4|_e2-yRtXC5A#(?blrt+}yde6dZg0uY!AUzF6SWuOU`z zYO*)Q-A9?O%*AEWeq*^Jhq);!^7Zc`C0b^!>#STB`#-Jil^YzQRn0mtEgcd4_P|gm z_SZi;I24kOp}0dlFR{I_E9?3#sIKB#li4GNJL?0vVX- zTeXJo*VYDHT)4P) Date: Tue, 20 Aug 2024 10:56:55 -0700 Subject: [PATCH 003/104] chore: extract recorder dialog into a class (#32233) --- .../src/server/injected/recorder/recorder.ts | 156 +++++++++++------- 1 file changed, 96 insertions(+), 60 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index c9413e0a72..3432f159dd 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -552,28 +552,14 @@ class TextAssertionTool implements RecorderTool { private _recorder: Recorder; private _hoverHighlight: HighlightModel | null = null; private _action: actions.AssertAction | null = null; - private _dialogElement: HTMLElement | null = null; - private _acceptButton: HTMLElement; - private _cancelButton: HTMLElement; - private _keyboardListener: ((event: KeyboardEvent) => void) | undefined; + private _dialog: Dialog; private _textCache = new Map(); private _kind: 'text' | 'value'; constructor(recorder: Recorder, kind: 'text' | 'value') { this._recorder = recorder; this._kind = kind; - - this._acceptButton = this._recorder.document.createElement('x-pw-tool-item'); - this._acceptButton.title = 'Accept'; - this._acceptButton.classList.add('accept'); - this._acceptButton.appendChild(this._recorder.document.createElement('x-div')); - this._acceptButton.addEventListener('click', () => this._commit()); - - this._cancelButton = this._recorder.document.createElement('x-pw-tool-item'); - this._cancelButton.title = 'Close'; - this._cancelButton.classList.add('cancel'); - this._cancelButton.appendChild(this._recorder.document.createElement('x-div')); - this._cancelButton.addEventListener('click', () => this._closeDialog()); + this._dialog = new Dialog(recorder); } cursor() { @@ -581,7 +567,7 @@ class TextAssertionTool implements RecorderTool { } cleanup() { - this._closeDialog(); + this._dialog.close(); this._hoverHighlight = null; } @@ -590,7 +576,7 @@ class TextAssertionTool implements RecorderTool { if (this._kind === 'value') { this._commitAssertValue(); } else { - if (!this._dialogElement) + if (!this._dialog.isShowing()) this._showDialog(); } } @@ -611,7 +597,7 @@ class TextAssertionTool implements RecorderTool { } onMouseMove(event: MouseEvent) { - if (this._dialogElement) + if (this._dialog.isShowing()) return; const target = this._recorder.deepEventTarget(event); if (this._hoverHighlight?.elements[0] === target) @@ -691,9 +677,9 @@ class TextAssertionTool implements RecorderTool { } private _commit() { - if (!this._action || !this._dialogElement) + if (!this._action || !this._dialog.isShowing()) return; - this._closeDialog(); + this._dialog.close(); this._recorder.delegate.recordAction?.(this._action); this._recorder.delegate.setMode?.('recording'); } @@ -705,31 +691,6 @@ class TextAssertionTool implements RecorderTool { if (!this._action || this._action.name !== 'assertText') return; - this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); - this._keyboardListener = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - this._closeDialog(); - return; - } - if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { - if (this._dialogElement) - this._commit(); - return; - } - }; - - this._recorder.document.addEventListener('keydown', this._keyboardListener, true); - const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); - const labelElement = this._recorder.document.createElement('label'); - labelElement.textContent = 'Assert that element contains text'; - toolbarElement.appendChild(labelElement); - toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); - toolbarElement.appendChild(this._acceptButton); - toolbarElement.appendChild(this._cancelButton); - - this._dialogElement.appendChild(toolbarElement); - const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); - const action = this._action; const textElement = this._recorder.document.createElement('textarea'); textElement.setAttribute('spellcheck', 'false'); @@ -747,24 +708,18 @@ class TextAssertionTool implements RecorderTool { textElement.classList.toggle('does-not-match', !matches); }; textElement.addEventListener('input', updateAndValidate); - bodyElement.appendChild(textElement); - this._dialogElement.appendChild(bodyElement); - this._recorder.highlight.appendChild(this._dialogElement); - const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement); - this._dialogElement.style.top = position.anchorTop + 'px'; - this._dialogElement.style.left = position.anchorLeft + 'px'; + const label = 'Assert that element contains text'; + const dialogElement = this._dialog.show({ + label, + body: textElement, + onCommit: () => this._commit(), + }); + const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, dialogElement); + this._dialog.moveTo(position.anchorTop, position.anchorLeft); textElement.focus(); } - private _closeDialog() { - if (!this._dialogElement) - return; - this._dialogElement.remove(); - this._recorder.document.removeEventListener('keydown', this._keyboardListener!); - this._dialogElement = null; - } - private _commitAssertValue() { if (this._kind !== 'value') return; @@ -1219,6 +1174,87 @@ export class Recorder { } } +class Dialog { + private _recorder: Recorder; + private _dialogElement: HTMLElement | null = null; + private _keyboardListener: ((event: KeyboardEvent) => void) | undefined; + + constructor(recorder: Recorder) { + this._recorder = recorder; + } + + isShowing(): boolean { + return !!this._dialogElement; + } + + show(options: { + label: string; + body: Element; + onCommit: () => void; + onCancel?: () => void; + }) { + const acceptButton = this._recorder.document.createElement('x-pw-tool-item'); + acceptButton.title = 'Accept'; + acceptButton.classList.add('accept'); + acceptButton.appendChild(this._recorder.document.createElement('x-div')); + acceptButton.addEventListener('click', () => options.onCommit()); + + const cancelButton = this._recorder.document.createElement('x-pw-tool-item'); + cancelButton.title = 'Close'; + cancelButton.classList.add('cancel'); + cancelButton.appendChild(this._recorder.document.createElement('x-div')); + cancelButton.addEventListener('click', () => { + this.close(); + options.onCancel?.(); + }); + + this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); + this._keyboardListener = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.close(); + options.onCancel?.(); + return; + } + if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { + if (this._dialogElement) + options.onCommit(); + return; + } + }; + + this._recorder.document.addEventListener('keydown', this._keyboardListener, true); + const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); + const labelElement = this._recorder.document.createElement('label'); + labelElement.textContent = options.label; + toolbarElement.appendChild(labelElement); + toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); + toolbarElement.appendChild(acceptButton); + toolbarElement.appendChild(cancelButton); + + this._dialogElement.appendChild(toolbarElement); + const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); + bodyElement.appendChild(options.body); + this._dialogElement.appendChild(bodyElement); + this._recorder.highlight.appendChild(this._dialogElement); + return this._dialogElement; + } + + moveTo(top: number, left: number) { + if (!this._dialogElement) + return; + this._dialogElement.style.top = top + 'px'; + this._dialogElement.style.left = left + 'px'; + } + + close() { + if (!this._dialogElement) + return; + this._dialogElement.remove(); + this._recorder.document.removeEventListener('keydown', this._keyboardListener!); + this._dialogElement = null; + } +} + function deepActiveElement(document: Document): Element | null { let activeElement = document.activeElement; while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) From b66cb6caaaba17e03af2b95f4abd54234beb4a50 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 21 Aug 2024 01:31:41 -0700 Subject: [PATCH 004/104] docs(evaluate): improve the guide (#32222) --- docs/src/evaluating.md | 222 ++++++++++++++++++++++++++--------------- 1 file changed, 142 insertions(+), 80 deletions(-) diff --git a/docs/src/evaluating.md b/docs/src/evaluating.md index 50851110d0..903aeb8a90 100644 --- a/docs/src/evaluating.md +++ b/docs/src/evaluating.md @@ -68,9 +68,95 @@ int status = await page.EvaluateAsync(@"async () => { }"); ``` +## Different environments + +Evaluated scripts run in the browser environment, while your test runs in a testing environments. This means you cannot use variables from your test in the page and vice versa. Instead, you should pass them explicitly as an argument. + +The following snippet is **WRONG** because it uses the variable directly: + +```js +const data = 'some data'; +const result = await page.evaluate(() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data); +}); +``` + +```java +String data = "some data"; +Object result = page.evaluate("() => {\n" + + " // WRONG: there is no 'data' in the web page.\n" + + " window.myApp.use(data);\n" + + "}"); +``` + +```python async +data = "some data" +result = await page.evaluate("""() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data) +}""") +``` + +```python sync +data = "some data" +result = page.evaluate("""() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data) +}""") +``` + +```csharp +var data = "some data"; +var result = await page.EvaluateAsync(@"() => { + // WRONG: there is no 'data' in the web page. + window.myApp.use(data); +}"); +``` + +The following snippet is **CORRECT** because it passes the value explicitly as an argument: + +```js +const data = 'some data'; +// Pass |data| as a parameter. +const result = await page.evaluate(data => { + window.myApp.use(data); +}, data); +``` + +```java +String data = "some data"; +// Pass |data| as a parameter. +Object result = page.evaluate("data => {\n" + + " window.myApp.use(data);\n" + + "}", data); +``` + +```python async +data = "some data" +# Pass |data| as a parameter. +result = await page.evaluate("""data => { + window.myApp.use(data) +}""", data) +``` + +```python sync +data = "some data" +# Pass |data| as a parameter. +result = page.evaluate("""data => { + window.myApp.use(data) +}""", data) +``` + +```csharp +var data = "some data"; +// Pass |data| as a parameter. +var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data); +``` + ## Evaluation Argument -Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] or [ElementHandle] instances. Handles are automatically converted to the value they represent. +Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] instances. Handles are automatically converted to the value they represent. ```js // A primitive value. @@ -86,7 +172,7 @@ await page.evaluate(object => object.foo, { foo: 'bar' }); const button = await page.evaluateHandle('window.button'); await page.evaluate(button => button.textContent, button); -// Alternative notation using elementHandle.evaluate. +// Alternative notation using JSHandle.evaluate. await button.evaluate((button, from) => button.textContent.substring(from), 5); // Object with multiple handles. @@ -109,7 +195,7 @@ await page.evaluate( ([b1, b2]) => b1.textContent + b2.textContent, [button1, button2]); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. await page.evaluate( x => x.button1.textContent + x.list[0].textContent + String(x.foo), { button1, list: [button2], foo: null }); @@ -131,7 +217,7 @@ page.evaluate("object => object.foo", obj); ElementHandle button = page.evaluateHandle("window.button"); page.evaluate("button => button.textContent", button); -// Alternative notation using elementHandle.evaluate. +// Alternative notation using JSHandle.evaluate. button.evaluate("(button, from) => button.textContent.substring(from)", 5); // Object with multiple handles. @@ -156,7 +242,7 @@ page.evaluate( "([b1, b2]) => b1.textContent + b2.textContent", Arrays.asList(button1, button2)); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. Map arg = new HashMap<>(); arg.put("button1", button1); arg.put("list", Arrays.asList(button2)); @@ -180,7 +266,7 @@ await page.evaluate('object => object.foo', { 'foo': 'bar' }) button = await page.evaluate_handle('button') await page.evaluate('button => button.textContent', button) -# Alternative notation using elementHandle.evaluate. +# Alternative notation using JSHandle.evaluate. await button.evaluate('(button, from) => button.textContent.substring(from)', 5) # Object with multiple handles. @@ -203,7 +289,7 @@ await page.evaluate(""" ([b1, b2]) => b1.textContent + b2.textContent""", [button1, button2]) -# Any non-cyclic mix of serializables and handles works. +# Any mix of serializables and handles works. await page.evaluate(""" x => x.button1.textContent + x.list[0].textContent + String(x.foo)""", { 'button1': button1, 'list': [button2], 'foo': None }) @@ -223,7 +309,7 @@ page.evaluate('object => object.foo', { 'foo': 'bar' }) button = page.evaluate_handle('window.button') page.evaluate('button => button.textContent', button) -# Alternative notation using elementHandle.evaluate. +# Alternative notation using JSHandle.evaluate. button.evaluate('(button, from) => button.textContent.substring(from)', 5) # Object with multiple handles. @@ -245,7 +331,7 @@ page.evaluate(""" ([b1, b2]) => b1.textContent + b2.textContent""", [button1, button2]) -# Any non-cyclic mix of serializables and handles works. +# Any mix of serializables and handles works. page.evaluate(""" x => x.button1.textContent + x.list[0].textContent + String(x.foo)""", { 'button1': button1, 'list': [button2], 'foo': None }) @@ -265,7 +351,7 @@ await page.EvaluateAsync("object => object.foo", new { foo = "bar" }); var button = await page.EvaluateHandleAsync("window.button"); await page.EvaluateAsync("button => button.textContent", button); -// Alternative notation using elementHandle.EvaluateAsync. +// Alternative notation using JSHandle.EvaluateAsync. await button.EvaluateAsync("(button, from) => button.textContent.substring(from)", 5); // Object with multiple handles. @@ -282,93 +368,69 @@ await page.EvaluateAsync("({ button1, button2 }) => button1.textContent + button // Note the required parenthesis. await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 }); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. await page.EvaluateAsync("x => x.button1.textContent + x.list[0].textContent + String(x.foo)", new { button1, list = new[] { button2 }, foo = null as object }); ``` -Right: +## Init scripts + +Sometimes it is convenient to evaluate something in the page before it starts loading. For example, you might want to setup some mocks or test data. + +In this case, use [`method: Page.addInitScript`] or [`method: BrowserContext.addInitScript`]. In the example below, we will replace `Math.random()` with a constant value. + +First, create a `preload.js` file that contains the mock. + +```js browser +// preload.js +Math.random = () => 42; +``` + +Next, add init script to the page. ```js -const data = { text: 'some data', value: 1 }; -// Pass |data| as a parameter. -const result = await page.evaluate(data => { - window.myApp.use(data); -}, data); -``` +import { test, expect } from '@playwright/test'; +import path from 'path'; -```java -Map data = new HashMap<>(); -data.put("text", "some data"); -data.put("value", 1); -// Pass |data| as a parameter. -Object result = page.evaluate("data => {\n" + - " window.myApp.use(data);\n" + - "}", data); -``` - -```python async -data = { 'text': 'some data', 'value': 1 } -# Pass |data| as a parameter. -result = await page.evaluate("""data => { - window.myApp.use(data) -}""", data) -``` - -```python sync -data = { 'text': 'some data', 'value': 1 } -# Pass |data| as a parameter. -result = page.evaluate("""data => { - window.myApp.use(data) -}""", data) -``` - -```csharp -var data = new { text = "some data", value = 1}; -// Pass data as a parameter -var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data); -``` - -Wrong: - -```js -const data = { text: 'some data', value: 1 }; -const result = await page.evaluate(() => { - // There is no |data| in the web page. - window.myApp.use(data); +test.beforeEach(async ({ page }) => { + // Add script for every test in the beforeEach hook. + // Make sure to correctly resolve the script path. + await page.addInitScript({ path: path.resolve(__dirname, '../mocks/preload.js') }); }); ``` ```java -Map data = new HashMap<>(); -data.put("text", "some data"); -data.put("value", 1); -Object result = page.evaluate("() => {\n" + - " // There is no |data| in the web page.\n" + - " window.myApp.use(data);\n" + - "}"); +// In your test, assuming the "preload.js" file is in the "mocks" directory. +page.addInitScript(Paths.get("mocks/preload.js")); ``` ```python async -data = { 'text': 'some data', 'value': 1 } -result = await page.evaluate("""() => { - // There is no |data| in the web page. - window.myApp.use(data) -}""") +# In your test, assuming the "preload.js" file is in the "mocks" directory. +await page.add_init_script(path="mocks/preload.js") ``` ```python sync -data = { 'text': 'some data', 'value': 1 } -result = page.evaluate("""() => { - // There is no |data| in the web page. - window.myApp.use(data) -}""") +# In your test, assuming the "preload.js" file is in the "mocks" directory. +page.add_init_script(path="mocks/preload.js") ``` ```csharp -var data = new { text = "some data", value = 1}; -// Pass data as a parameter -var result = await page.EvaluateAsync(@"data => { - // There is no |data| in the web page. - window.myApp.use(data); -}"); +// In your test, assuming the "preload.js" file is in the "mocks" directory. +await Page.AddInitScriptAsync(scriptPath: "mocks/preload.js"); +``` + +###### +* langs: js + +Alternatively, you can pass a function instead of creating a preload script file. This is more convenient for short or one-off scripts. You can also pass an argument this way. + +```js +import { test, expect } from '@playwright/test'; + +// Add script for every test in the beforeEach hook. +test.beforeEach(async ({ page }) => { + const value = 42; + await page.addInitScript(value => { + Math.random = () => value; + }, value); +}); ``` From 6512bccffdb451228361fd8ddfdb1ae1d499cbf3 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 21 Aug 2024 11:37:53 +0200 Subject: [PATCH 005/104] docs(best-practises): add note about tsc (#32245) --- docs/src/best-practices-js.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md index 7d4830b6cb..3b70817923 100644 --- a/docs/src/best-practices-js.md +++ b/docs/src/best-practices-js.md @@ -265,7 +265,7 @@ Use Linux when running your tests on CI as it is cheaper. Developers can use wha ### Lint your tests -Linting the tests helps catching errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. +We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature. ### Use parallelism and sharding From 918dbe5e3aeb40a3a4c5443b6fb5d523bc0f81d8 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 21 Aug 2024 08:34:55 -0700 Subject: [PATCH 006/104] chore: start listening for navigation events before navigation starts (#32237) There is a chance in case of cross-process navigation that the navigation event comes before `navigateFrame` finishes. --- packages/playwright-core/src/server/frames.ts | 26 ++++++++++++++----- tests/page/page-goto.spec.ts | 3 ++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 87569f00d6..931ba8ef73 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -659,18 +659,24 @@ export class Frame extends SdkObject { } url = helper.completeUserURL(url); - const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument); - const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); + const navigationEvents: NavigationEvent[] = []; + const collectNavigations = (arg: NavigationEvent) => navigationEvents.push(arg); + this.on(Frame.Events.InternalNavigation, collectNavigations); + const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).finally( + () => this.off(Frame.Events.InternalNavigation, collectNavigations)); let event: NavigationEvent; if (navigateResult.newDocumentId) { - sameDocument.dispose(); - event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => { + const predicate = (event: NavigationEvent) => { // We are interested either in this specific document, or any other document that // did commit and replaced the expected document. return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error); - }).promise; - + }; + const events = navigationEvents.filter(predicate); + if (events.length) + event = events[0]; + else + event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise; if (event.newDocument!.documentId !== navigateResult.newDocumentId) { // This is just a sanity check. In practice, new navigation should // cancel the previous one and report "request cancelled"-like error. @@ -679,7 +685,13 @@ export class Frame extends SdkObject { if (event.error) throw event.error; } else { - event = await sameDocument.promise; + // Wait for same document navigation. + const predicate = (e: NavigationEvent) => !e.newDocument; + const events = navigationEvents.filter(predicate); + if (events.length) + event = events[0]; + else + event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise; } if (!this._firedLifecycleEvents.has(waitUntil)) diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 79107c0d21..015fa0e4ef 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -181,8 +181,9 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, 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 }) => { +}, 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(); From 837e2a883b737f9e5cf6871ef3cea0b66950fa39 Mon Sep 17 00:00:00 2001 From: Guillaume M Date: Wed, 21 Aug 2024 17:35:47 +0200 Subject: [PATCH 007/104] docs(browsers): fix typo (#32250) --- docs/src/browsers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/browsers.md b/docs/src/browsers.md index c1ad722d60..3b9e76d006 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz ### WebKit -Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that avialability of certain features, which depend heavily on the underlying platform, may vary between operating systems. +Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems. ## Install behind a firewall or a proxy From d5a74950410feb1a4fb4745a1e58573d3ff40819 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 21 Aug 2024 09:46:38 -0700 Subject: [PATCH 008/104] feat(addInitScript): support cjs modules when passing both `path` and `arg` (#32240) This works with scripts bundled by: - `esbuild entrypoint.ts --bundle --format=cjs --outfile=injected.js` - webpack with a typical config ```js module.exports = { entry: { 'injected': './entrypoint.js', }, output: { path: require('path').resolve(__dirname), filename: '[name].js', libraryTarget: 'commonjs2', }, }; ``` --- docs/src/api/class-browsercontext.md | 37 +++++++++- docs/src/api/class-page.md | 37 +++++++++- .../src/client/browserContext.ts | 2 +- .../src/client/clientHelper.ts | 27 +++++-- packages/playwright-core/src/client/page.ts | 2 +- .../playwright-core/src/client/selectors.ts | 2 +- packages/playwright-core/types/types.d.ts | 70 ++++++++++++++++++- tests/assets/injectedmodule.js | 33 +++++++++ tests/page/page-add-init-script.spec.ts | 12 ++++ 9 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 tests/assets/injectedmodule.js diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 6d5558ae55..7b6c6aab1d 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -415,13 +415,42 @@ 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 undefined 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 }, undefined); +``` + ### 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. Optional. - - `content` ?<[string]> Raw script content. Optional. + current working directory. + - `content` ?<[string]> Raw script content. Script to be evaluated in all pages in the browser context. @@ -437,7 +466,9 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional argument to pass to [`param: script`] (only supported when passing a function). +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. ### param: BrowserContext.addInitScript.path * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index ee9623c867..11d7cbdd74 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -619,13 +619,42 @@ 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 undefined 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 }, undefined); +``` + ### 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. Optional. - - `content` ?<[string]> Raw script content. Optional. + current working directory. + - `content` ?<[string]> Raw script content. Script to be evaluated in the page. @@ -641,7 +670,9 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional argument to pass to [`param: script`] (only supported when passing a function). +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. ### param: Page.addInitScript.path * since: v1.8 diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index c4f7827840..b0b72917cf 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - const source = await evaluationScript(script, arg); + const source = await evaluationScript(script, arg, arguments.length > 1); await this._channel.addInitScript({ source }); } diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index 540230a4fc..fcc785b71b 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -28,20 +28,37 @@ 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, hasArg: boolean, 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 (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); - if (isString(fun)) + if (isString(fun)) { + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); return fun; - if (fun.content !== undefined) + } + if (fun.content !== undefined) { + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); return fun.content; + } if (fun.path !== undefined) { let source = await fs.promises.readFile(fun.path, 'utf8'); + if (hasArg) { + // 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/src/client/page.ts b/packages/playwright-core/src/client/page.ts index a10286fa9a..5ff6d6178d 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -492,7 +492,7 @@ export class Page extends ChannelOwner implements api.Page } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await evaluationScript(script, arg); + const source = await evaluationScript(script, arg, arguments.length > 1); await this._channel.addInitScript({ source }); } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 2739be0e8d..c7a7967559 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -26,7 +26,7 @@ export class Selectors implements api.Selectors { private _registrations: channels.SelectorsRegisterParams[] = []; async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { - const source = await evaluationScript(script, undefined, false); + const source = await evaluationScript(script, undefined, false, false); const params = { ...options, name, source }; for (const channel of this._channels) await channel._channel.register(params); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a4bf9fa812..0d018dcfd3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -288,8 +288,41 @@ 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 undefined 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 }, undefined); + * ``` + * * @param script Script to be evaluated in the page. - * @param arg Optional argument to pass to `script` (only supported when passing a function). + * @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. */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -7666,8 +7699,41 @@ 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 undefined 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 }, undefined); + * ``` + * * @param script Script to be evaluated in all pages in the browser context. - * @param arg Optional argument to pass to `script` (only supported when passing a function). + * @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. */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; diff --git a/tests/assets/injectedmodule.js b/tests/assets/injectedmodule.js new file mode 100644 index 0000000000..bc099f243f --- /dev/null +++ b/tests/assets/injectedmodule.js @@ -0,0 +1,33 @@ +"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 b2b7782eba..2b12f00251 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -31,6 +31,18 @@ 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 assume CJS module with a path and undefined arg', async ({ page, server, asset }) => { + await page.addInitScript({ path: asset('injectedmodule.js') }, undefined); + await page.goto(server.EMPTY_PAGE); + expect(await page.evaluate(() => window['injected'])).toBe(42); +}); + it('should work with content @smoke', async ({ page, server }) => { await page.addInitScript({ content: 'window["injected"] = 123' }); await page.goto(server.PREFIX + '/tamperable.html'); From 571f25a7d36f09a5d3c3ddf061d950d18f74c802 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 21 Aug 2024 11:14:41 -0700 Subject: [PATCH 009/104] fix(role): hidden pseudos should not contribute to accessible name (#32251) --- .../src/server/injected/roleUtils.ts | 3 ++- tests/library/role-utils.spec.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 99f3cafa29..498fc189aa 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -375,7 +375,8 @@ function getPseudoContent(element: Element, pseudo: '::before' | '::after') { } function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) { - if (!pseudoStyle) + // Note: all browsers ignore display:none and visibility:hidden pseudos. + if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden') return ''; const content = pseudoStyle.content; if ((content[0] === '\'' && content[content.length - 1] === '\'') || diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 067053ba11..6c45686f68 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -475,6 +475,26 @@ test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ pa expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' }); }); +test('should not include hidden pseudo into accessible name', async ({ page }) => { + await page.setContent(` + + + hello +
hello
+
+ `); + expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' }); +}); + function toArray(x: any): any[] { return Array.isArray(x) ? x : [x]; } From e3480d1886a9cc219027499e65c04e52c2530dad Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 22 Aug 2024 08:42:09 +0200 Subject: [PATCH 010/104] test: add test for TLS renegotiation and client-certificates (#32252) --- tests/library/client-certificates.spec.ts | 134 ++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 49e0dc0dcc..a6f6628569 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -16,6 +16,8 @@ import fs from 'fs'; import tls from 'tls'; +import type https from 'https'; +import zlib from 'zlib'; import type http2 from 'http2'; import type http from 'http'; import { expect, playwrightTest as base } from '../config/browserTest'; @@ -375,6 +377,138 @@ test.describe('browser', () => { await page.close(); }); + test('should handle TLS renegotiation with client certificates', async ({ browser, asset, browserName, platform }) => { + const server: https.Server = createHttpsServer({ + key: fs.readFileSync(asset('client-certificates/server/server_key.pem')), + cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')), + ca: [fs.readFileSync(asset('client-certificates/server/server_cert.pem'))], + requestCert: false, // Initially don't request client cert + rejectUnauthorized: false, + // TLSv1.3 does not support renegotiation + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2', + }); + + server.on('request', async (req, res) => { + if (!req.socket) + return; + const renegotiate = () => new Promise((resolve, reject) => { + (req.socket as tls.TLSSocket).renegotiate({ + requestCert: true, + rejectUnauthorized: false + }, err => { + if (err) + reject(err); + else + resolve(); + }); + }); + if (req.url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html', 'connection': 'close' }); + res.end(); + } else if (req.url === '/from-fetch-api') { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Transfer-Encoding': 'chunked' + }); + res.flushHeaders(); + + await new Promise(resolve => req.once('data', data => { + res.write(`server received: ${data.toString()}\n`); + resolve(); + })); + + await renegotiate(); + for (let i = 0; i < 4; i++) { + res.write(`${i}-from-server\n`); + // Best-effort to trigger a new chunk + await new Promise(resolve => setTimeout(resolve, 100)); + } + res.end('server closed the connection'); + } else if (req.url === '/style.css') { + res.writeHead(200, { + 'Content-Type': 'text/css', + 'Content-Encoding': 'gzip', + 'Transfer-Encoding': 'chunked' + }); + + await renegotiate(); + + const stylesheet = ` + button { + background-color: red; + } + `; + + const stylesheetBuffer = await new Promise((resolve, reject) => { + zlib.gzip(stylesheet, (err, buffer) => { + if (err) + reject(err); + else + resolve(buffer); + }); + }); + for (let i = 0; i < stylesheetBuffer.length; i += 100) { + res.write(stylesheetBuffer.slice(i, i + 100)); + // Best-effort to trigger a new chunk + await new Promise(resolve => setTimeout(resolve, 20)); + } + res.end(); + } else { + res.writeHead(404); + res.end(); + } + }); + + await new Promise(resolve => server.listen(0, 'localhost', resolve)); + const port = (server.address() as import('net').AddressInfo).port; + const origin = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost'); + const serverUrl = `${origin}:${port}`; + + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }); + + const page = await context.newPage(); + + await test.step('fetch API', async () => { + await page.goto(serverUrl); + const response = await page.evaluate(async () => { + const response = await fetch('/from-fetch-api', { + method: 'POST', + body: 'client-request-payload' + }); + return await response.text(); + }); + expect(response).toBe([ + 'server received: client-request-payload', + '0-from-server', + '1-from-server', + '2-from-server', + '3-from-server', + 'server closed the connection' + ].join('\n')); + }); + + await test.step('Gzip encoded CSS Stylesheet', async () => { + await page.goto(serverUrl); + // The would throw with net::ERR_INVALID_CHUNKED_ENCODING + await page.setContent(` + + + `); + await expect(page.locator('button')).toHaveCSS('background-color', /* red */'rgb(255, 0, 0)'); + }); + + await context.close(); + await new Promise(resolve => server.close(() => resolve())); + }); + test('should pass with matching certificates in pfx format when passing as content', async ({ browser, startCCServer, asset, browserName }) => { const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); const page = await browser.newPage({ From 666a8f22cf175686651e3cfe7bb77184e2497616 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 22 Aug 2024 10:15:47 +0200 Subject: [PATCH 011/104] chore: fix api.json serializer for language ports (#32260) Fixes https://github.com/microsoft/playwright/issues/32241 --- utils/doclint/generateApiJson.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/doclint/generateApiJson.js b/utils/doclint/generateApiJson.js index c0b533d97b..bb639abbaf 100644 --- a/utils/doclint/generateApiJson.js +++ b/utils/doclint/generateApiJson.js @@ -77,7 +77,7 @@ function serializeMember(member) { } function serializeProperty(arg) { - const result = { ...arg }; + const result = { ...arg, parent: undefined }; sanitize(result); if (arg.type) result.type = serializeType(arg.type, arg.name === 'options'); From f74c6d77dbe66e0d82634c3869f189d52c667df6 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 22 Aug 2024 03:07:44 -0700 Subject: [PATCH 012/104] chore(driver): roll driver to recent Node.js LTS version (#32264) --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index 0ee23c15d9..09c3fc75ae 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="20.16.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="20.17.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From dc4a8e48ebcb9f57bf8860b4c3beaad38e2967ca Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 22 Aug 2024 05:47:19 -0700 Subject: [PATCH 013/104] docs(fixtures): explain an option array value edge case (#32261) Closes #32033. --- docs/src/test-fixtures-js.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index 60c9219a95..a2e65e8849 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -454,10 +454,6 @@ test('example test', async ({ slowFixture }) => { ## Fixtures-options -:::note -Overriding custom fixtures in the config file has changed in version 1.18. [Learn more](./release-notes#breaking-change-custom-config-options). -::: - Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about [parametrizing tests](./test-parameterize.md). Below we'll create a `defaultItem` option in addition to the `todoPage` fixture from other examples. This option will be set in configuration file. Note the tuple syntax and `{ option: true }` argument. @@ -555,6 +551,30 @@ export default defineConfig({ }); ``` +**Array as an option value** + +If the value of your option is an array, for example `[{ name: 'Alice' }, { name: 'Bob' }]`, you'll need to wrap it into an extra array when providing the value. This is best illustrated with an example. + +```js +type Person = { name: string }; +const test = base.extend<{ persons: Person[] }>({ + // Declare the option, default value is an empty array. + persons: [[], { option: true }], +}); + +// Option value is an array of persons. +const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }]; +test.use({ + // CORRECT: Wrap the value into an array and pass the scope. + persons: [actualPersons, { scope: 'test' }], +}); + +test.use({ + // WRONG: passing an array value directly will not work. + persons: actualPersons, +}); +``` + ## Execution order Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore. From 7758b330b1cc3ec08f39d4e89e0f58c3cbdbfa4f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 22 Aug 2024 05:48:31 -0700 Subject: [PATCH 014/104] fix(ui mode): make sure that reload does correctly restart the webserver (#32263) Fixes #32103. --- .../playwright/src/plugins/webServerPlugin.ts | 2 + packages/playwright/src/runner/testServer.ts | 20 ++++----- .../ui-mode-test-setup.spec.ts | 42 ++++++++++++++++++- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/packages/playwright/src/plugins/webServerPlugin.ts b/packages/playwright/src/plugins/webServerPlugin.ts index 86f84ad652..96a369bddb 100644 --- a/packages/playwright/src/plugins/webServerPlugin.ts +++ b/packages/playwright/src/plugins/webServerPlugin.ts @@ -73,7 +73,9 @@ export class WebServerPlugin implements TestRunnerPlugin { } public async teardown() { + debugWebServer(`Terminating the WebServer`); await this._killProcess?.(); + debugWebServer(`Terminated the WebServer`); } private async _startProcess(): Promise { diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 5a498337af..7ed1d18191 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -114,14 +114,11 @@ class TestServerDispatcher implements TestServerInterface { } async initialize(params: Parameters[0]): ReturnType { - if (params.serializer) - this._serializer = params.serializer; - if (params.closeOnDisconnect) - this._closeOnDisconnect = true; - if (params.interceptStdio) - await this._setInterceptStdio(true); - if (params.watchTestDirs) - this._watchTestDirs = true; + // Note: this method can be called multiple times, for example from a new connection after UI mode reload. + this._serializer = params.serializer || require.resolve('./uiModeReporter'); + this._closeOnDisconnect = !!params.closeOnDisconnect; + await this._setInterceptStdio(!!params.interceptStdio); + this._watchTestDirs = !!params.watchTestDirs; } async ping() {} @@ -161,7 +158,6 @@ class TestServerDispatcher implements TestServerInterface { return { status: 'failed', report }; } - webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); const { collectingReporter, report } = await this._collectingReporter(); const listReporter = new ListReporter(); const taskRunner = createTaskRunnerForWatchSetup(config, [collectingReporter, listReporter]); @@ -418,10 +414,12 @@ class TestServerDispatcher implements TestServerInterface { try { const config = await loadConfig(this._configLocation, overrides); // Preserve plugin instances between setup and build. - if (!this._plugins) + if (!this._plugins) { + webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); this._plugins = config.plugins || []; - else + } else { config.plugins.splice(0, config.plugins.length, ...this._plugins); + } return { config }; } catch (e) { return { config: null, error: serializeError(e) }; diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index 08857d5508..e8809ddad9 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -15,6 +15,7 @@ */ import { test, expect, retries, dumpTestTree } from './ui-mode-fixtures'; +import path from 'path'; test.describe.configure({ mode: 'parallel', retries }); @@ -245,4 +246,43 @@ for (const useWeb of [true, false]) { ]); }); }); -} \ No newline at end of file +} + +test('should restart webserver on reload', async ({ runUITest }) => { + const SIMPLE_SERVER_PATH = path.join(__dirname, 'assets', 'simple-server.js'); + const port = test.info().workerIndex * 2 + 10500; + + const { page } = await runUITest({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + webServer: { + command: 'node ${JSON.stringify(SIMPLE_SERVER_PATH)} ${port}', + port: ${port}, + reuseExistingServer: false, + }, + }); + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('should work', async ({ page }) => { + await page.goto('http://localhost:${port}'); + }); + ` + }, { DEBUG: 'pw:webserver' }); + 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('[WebServer] listening'); + + await page.getByTitle('Clear output').click(); + await expect(page.getByTestId('output')).not.toContainText('[WebServer] listening'); + + await page.getByTitle('Reload').click(); + await expect(page.getByTestId('output')).toContainText('[WebServer] listening'); + await expect(page.getByTestId('output')).not.toContainText('set reuseExistingServer:true'); + + await page.getByTitle('Run all').click(); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); +}); From 5368fd7ca7fd3b1de93a44bcc5f0ce976c3d7f6f Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 22 Aug 2024 14:53:00 +0200 Subject: [PATCH 015/104] fix(only-changed): exit successfully if there were no changes (#32197) Closes https://github.com/microsoft/playwright/issues/32180 I was briefly wondering if we should output a log line a la "no tests found", but my understanding is that that's the reporters job - so I didn't change anything in that regard. --- packages/playwright/src/runner/tasks.ts | 2 +- tests/playwright-test/only-changed.spec.ts | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 73eb31f1e7..0a1e001a91 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -239,7 +239,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly, cliOnlyChangedMatcher); testRun.failureTracker.onRootSuite(testRun.rootSuite); // Fail when no tests. - if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) { + if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) { if (testRun.config.cliArgs.length) { throw new Error([ `No tests found.`, diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 901ec7511c..6fe915f702 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -256,10 +256,9 @@ test('should suppport component tests', async ({ runInlineTest, git, writeFiles const result = await runInlineTest({}, { 'workers': 1, 'only-changed': true }); - expect(result.exitCode).toBe(1); + expect(result.exitCode).toBe(0); expect(result.passed).toBe(0); expect(result.failed).toBe(0); - expect(result.output).toContain('No tests found'); const result2 = await runInlineTest({ 'src/button2.test.tsx': ` @@ -437,3 +436,20 @@ test('should work with list mode', async ({ runInlineTest, git, writeFiles }) => expect(result.output).toContain('b.spec.ts'); expect(result.output).not.toContain('a.spec.ts'); }); + +test('exits successfully if there are no changes', async ({ runInlineTest, git, writeFiles }) => { + await writeFiles({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('fails', () => { expect(1).toBe(2); }); + `, + }); + + git(`add .`); + git(`commit -m init`); + + const result = await runInlineTest({}, { 'only-changed': true }); + + expect(result.exitCode).toBe(0); +}); + From 16e76cb71a746c15b84131e66257df1095c6ac0d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 22 Aug 2024 15:13:54 +0200 Subject: [PATCH 016/104] fix(client-certificates): errors during http2 TLS handshake (#32258) --- .../socksClientCertificatesInterceptor.ts | 4 +++ tests/library/client-certificates.spec.ts | 35 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 32a7b1cebd..2dd900bf89 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -169,6 +169,10 @@ class SocksProxyConnection { this.target.removeListener('close', this._targetCloseEventListener); // @ts-expect-error const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS); + session.on('error', () => { + this.target.destroy(); + this._targetCloseEventListener(); + }); session.once('stream', (stream: http2.ServerHttp2Stream) => { stream.respond({ 'content-type': 'text/html', diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index a6f6628569..807af5154c 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -461,7 +461,7 @@ test.describe('browser', () => { }); await new Promise(resolve => server.listen(0, 'localhost', resolve)); - const port = (server.address() as import('net').AddressInfo).port; + const port = (server.address() as net.AddressInfo).port; const origin = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost'); const serverUrl = `${origin}:${port}`; @@ -671,6 +671,39 @@ test.describe('browser', () => { await page.close(); }); + test('should handle rejected certificate in handshake with HTTP/2', async ({ browser, asset, browserName, platform }) => { + const server: http2.Http2SecureServer = createHttp2Server({ + key: fs.readFileSync(asset('client-certificates/server/server_key.pem')), + cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')), + ca: [fs.readFileSync(asset('client-certificates/server/server_cert.pem'))], + requestCert: true, + }, async (req: http2.Http2ServerRequest, res: http2.Http2ServerResponse) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('Hello world'); + }); + + await new Promise(resolve => server.listen(0, 'localhost', resolve)); + const port = (server.address() as net.AddressInfo).port; + const serverUrl = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost') + ':' + port; + + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com', + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }); + + const page = await context.newPage(); + + // This was triggering an unhandled error before. + await page.goto(serverUrl).catch(() => {}); + + await context.close(); + await new Promise(resolve => server.close(() => resolve())); + }); + test.describe('persistentContext', () => { test('validate input', async ({ launchPersistent }) => { test.slow(); From 850436c656fc749a6bb27a50a04507b004b1a34a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 22 Aug 2024 17:29:10 +0200 Subject: [PATCH 017/104] chore(ui): move TeleSuiteUpdater into testIsomorphic (#32273) Preparation for https://github.com/microsoft/playwright/issues/32076. --- .../src/isomorphic}/teleSuiteUpdater.ts | 45 ++++++++++++------- .../trace-viewer/src/ui/uiModeFiltersView.tsx | 4 +- packages/trace-viewer/src/ui/uiModeModel.ts | 33 -------------- .../src/ui/uiModeTestListView.tsx | 6 +-- packages/trace-viewer/src/ui/uiModeView.tsx | 11 +++-- 5 files changed, 39 insertions(+), 60 deletions(-) rename packages/{trace-viewer/src/ui => playwright/src/isomorphic}/teleSuiteUpdater.ts (84%) delete mode 100644 packages/trace-viewer/src/ui/uiModeModel.ts diff --git a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts b/packages/playwright/src/isomorphic/teleSuiteUpdater.ts similarity index 84% rename from packages/trace-viewer/src/ui/teleSuiteUpdater.ts rename to packages/playwright/src/isomorphic/teleSuiteUpdater.ts index 0d448ea172..ef1c2dbb3e 100644 --- a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts +++ b/packages/playwright/src/isomorphic/teleSuiteUpdater.ts @@ -14,11 +14,24 @@ * limitations under the License. */ -import { TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver'; -import { statusEx } from '@testIsomorphic/testTree'; -import type { ReporterV2 } from 'playwright/src/reporters/reporterV2'; -import type * as reporterTypes from 'playwright/types/testReporter'; -import type { Progress, TestModel } from './uiModeModel'; +import { TeleReporterReceiver, TeleSuite } from './teleReceiver'; +import { statusEx } from './testTree'; +import type { ReporterV2 } from '../reporters/reporterV2'; +import type * as reporterTypes from '../../types/testReporter'; + +export type TeleSuiteUpdaterProgress = { + total: number; + passed: number; + failed: number; + skipped: number; +}; + +export type TeleSuiteUpdaterTestModel = { + config: reporterTypes.FullConfig; + rootSuite: reporterTypes.Suite; + loadErrors: reporterTypes.TestError[]; + progress: TeleSuiteUpdaterProgress; +}; export type TeleSuiteUpdaterOptions = { onUpdate: (force?: boolean) => void, @@ -30,7 +43,7 @@ export class TeleSuiteUpdater { rootSuite: TeleSuite | undefined; config: reporterTypes.FullConfig | undefined; readonly loadErrors: reporterTypes.TestError[] = []; - readonly progress: Progress = { + readonly progress: TeleSuiteUpdaterProgress = { total: 0, passed: 0, failed: 0, @@ -118,11 +131,11 @@ export class TeleSuiteUpdater { return false; }, - onStdOut: () => {}, - onStdErr: () => {}, - onExit: () => {}, - onStepBegin: () => {}, - onStepEnd: () => {}, + onStdOut: () => { }, + onStdErr: () => { }, + onExit: () => { }, + onStepBegin: () => { }, + onStepEnd: () => { }, }; } @@ -134,7 +147,7 @@ export class TeleSuiteUpdater { onError: (error: reporterTypes.TestError) => this._handleOnError(error) }); for (const message of report) - receiver.dispatch(message); + void receiver.dispatch(message); } processListReport(report: any[]) { @@ -144,14 +157,14 @@ export class TeleSuiteUpdater { this._testResultsSnapshot = new Map(tests.map(test => [test.id, test.results])); this._receiver.reset(); for (const message of report) - this._receiver.dispatch(message); + void this._receiver.dispatch(message); } processTestReportEvent(message: any) { // The order of receiver dispatches matters here, we want to assign `lastRunTestCount` // before we use it. - this._lastRunReceiver?.dispatch(message)?.catch(() => {}); - this._receiver.dispatch(message)?.catch(() => {}); + this._lastRunReceiver?.dispatch(message)?.catch(() => { }); + this._receiver.dispatch(message)?.catch(() => { }); } private _handleOnError(error: reporterTypes.TestError) { @@ -160,7 +173,7 @@ export class TeleSuiteUpdater { this._options.onUpdate(); } - asModel(): TestModel { + asModel(): TeleSuiteUpdaterTestModel { return { rootSuite: this.rootSuite || new TeleSuite('', 'root'), config: this.config!, diff --git a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx index 1c1be674f7..7671c2e9ba 100644 --- a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx +++ b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx @@ -20,7 +20,7 @@ import '@web/third_party/vscode/codicon.css'; import { settings } from '@web/uiUtils'; import React from 'react'; import './uiModeFiltersView.css'; -import type { TestModel } from './uiModeModel'; +import type { TeleSuiteUpdaterTestModel } from '@testIsomorphic/teleSuiteUpdater'; export const FiltersView: React.FC<{ filterText: string; @@ -29,7 +29,7 @@ export const FiltersView: React.FC<{ setStatusFilters: (filters: Map) => void; projectFilters: Map; setProjectFilters: (filters: Map) => void; - testModel: TestModel | undefined, + testModel: TeleSuiteUpdaterTestModel | undefined, runTests: () => void; }> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, testModel, runTests }) => { const [expanded, setExpanded] = React.useState(false); diff --git a/packages/trace-viewer/src/ui/uiModeModel.ts b/packages/trace-viewer/src/ui/uiModeModel.ts deleted file mode 100644 index 97952cfbfd..0000000000 --- a/packages/trace-viewer/src/ui/uiModeModel.ts +++ /dev/null @@ -1,33 +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 * as reporterTypes from 'playwright/types/testReporter'; - -export type Progress = { - total: number; - passed: number; - failed: number; - skipped: number; -}; - -export type TestModel = { - config: reporterTypes.FullConfig; - rootSuite: reporterTypes.Suite; - loadErrors: reporterTypes.TestError[]; - progress: Progress; -}; - -export const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/'; diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index b7a9d8e1d1..99a5f22dfa 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -27,10 +27,10 @@ import type * as reporterTypes from 'playwright/types/testReporter'; import React from 'react'; import type { SourceLocation } from './modelUtil'; import { testStatusIcon } from './testUtils'; -import type { TestModel } from './uiModeModel'; import './uiModeTestListView.css'; import type { TestServerConnection } from '@testIsomorphic/testServerConnection'; import { TagView } from './tag'; +import type { TeleSuiteUpdaterTestModel } from '@testIsomorphic/teleSuiteUpdater'; const TestTreeView = TreeView; @@ -38,7 +38,7 @@ export const TestListView: React.FC<{ filterText: string, testTree: TestTree, testServerConnection: TestServerConnection | undefined, - testModel?: TestModel, + testModel?: TeleSuiteUpdaterTestModel, runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set) => void, runningState?: { testIds: Set, itemSelectedByUser?: boolean, completed?: boolean }, watchAll: boolean, @@ -179,7 +179,7 @@ export const TestListView: React.FC<{ noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />; }; -function itemLocation(item: TreeItem | undefined, model: TestModel | undefined): SourceLocation | undefined { +function itemLocation(item: TreeItem | undefined, model: TeleSuiteUpdaterTestModel | undefined): SourceLocation | undefined { if (!item || !model) return; return { diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index b12617c300..4a73575a11 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -18,8 +18,7 @@ import '@web/third_party/vscode/codicon.css'; import '@web/common.css'; import React from 'react'; import { TeleSuite } from '@testIsomorphic/teleReceiver'; -import { TeleSuiteUpdater } from './teleSuiteUpdater'; -import type { Progress } from './uiModeModel'; +import { TeleSuiteUpdater, type TeleSuiteUpdaterProgress, type TeleSuiteUpdaterTestModel } from '@testIsomorphic/teleSuiteUpdater'; import type { TeleTestCase } from '@testIsomorphic/teleReceiver'; import type * as reporterTypes from 'playwright/types/testReporter'; import { SplitView } from '@web/components/splitView'; @@ -34,13 +33,13 @@ import { clsx, settings, useSetting } from '@web/uiUtils'; import { statusEx, TestTree } from '@testIsomorphic/testTree'; import type { TreeItem } from '@testIsomorphic/testTree'; import { TestServerConnection } from '@testIsomorphic/testServerConnection'; -import { pathSeparator } from './uiModeModel'; -import type { TestModel } from './uiModeModel'; import { FiltersView } from './uiModeFiltersView'; import { TestListView } from './uiModeTestListView'; import { TraceView } from './uiModeTraceView'; import { SettingsView } from './settingsView'; +const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/'; + let xtermSize = { cols: 80, rows: 24 }; const xtermDataSource: XtermDataSource = { pending: [], @@ -80,8 +79,8 @@ export const UIModeView: React.FC<{}> = ({ ['skipped', false], ])); const [projectFilters, setProjectFilters] = React.useState>(new Map()); - const [testModel, setTestModel] = React.useState(); - const [progress, setProgress] = React.useState(); + const [testModel, setTestModel] = React.useState(); + const [progress, setProgress] = React.useState(); const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }>({}); const [visibleTestIds, setVisibleTestIds] = React.useState>(new Set()); const [isLoading, setIsLoading] = React.useState(false); From 947fbc859000423f7e6b80eba79725ad073f47f2 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:43:39 -0700 Subject: [PATCH 018/104] feat(chromium-tip-of-tree): roll to r1253 (#32266) 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 dd9fd60588..576b81035d 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1250", + "revision": "1253", "installByDefault": false, - "browserVersion": "129.0.6658.0" + "browserVersion": "130.0.6670.0" }, { "name": "firefox", From 0b9c036505b7c2d00f6bec4d58d49cd63bb6a9db Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 22 Aug 2024 17:56:07 +0200 Subject: [PATCH 019/104] chore(ui): add test for font preview (#32225) Adds a test for the font preview feature. --- tests/library/trace-viewer.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 3af586d67f..4ebffc1f42 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -289,6 +289,20 @@ test('should filter network requests by resource type', async ({ page, runAndTra await expect(traceViewer.networkRequests.getByText('font.woff2')).toBeVisible(); }); +test('should show font preview', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(`${server.PREFIX}/network-tab/network.html`); + }); + await traceViewer.selectAction('http://localhost'); + await traceViewer.showNetworkTab(); + + await traceViewer.page.getByText('Font', { exact: true }).click(); + await expect(traceViewer.networkRequests).toHaveCount(1); + await traceViewer.networkRequests.getByText('font.woff2').click(); + await traceViewer.page.getByTestId('network-request-details').getByTitle('Body').click(); + await expect(traceViewer.page.locator('.network-request-details-tab')).toContainText('ABCDEF'); +}); + test('should filter network requests by url', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(`${server.PREFIX}/network-tab/network.html`); From 3a75f23ea1d116a57f6a9d6bf758cf8d693eb7c3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 23 Aug 2024 02:48:56 -0700 Subject: [PATCH 020/104] fix(addInitScript): require non-undefined arg to trigger commonjs module (#32282) --- docs/src/api/class-browsercontext.md | 4 ++-- docs/src/api/class-page.md | 4 ++-- packages/playwright-core/src/client/browserContext.ts | 2 +- packages/playwright-core/src/client/clientHelper.ts | 4 ++-- packages/playwright-core/src/client/page.ts | 2 +- packages/playwright-core/src/client/selectors.ts | 2 +- packages/playwright-core/types/types.d.ts | 8 ++++---- tests/page/page-add-init-script.spec.ts | 6 ------ 8 files changed, 13 insertions(+), 19 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 7b6c6aab1d..eed27fc1c1 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -439,9 +439,9 @@ 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 undefined even if you do not need to pass an argument. +// 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 }, undefined); +await context.addInitScript({ path: mockPath }, ''); ``` ### param: BrowserContext.addInitScript.script diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 11d7cbdd74..78d38a6f7a 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -643,9 +643,9 @@ 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 undefined even if you do not need to pass an argument. +// 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 }, undefined); +await page.addInitScript({ path: mockPath }, ''); ``` ### param: Page.addInitScript.script diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index b0b72917cf..c4f7827840 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - const source = await evaluationScript(script, arg, arguments.length > 1); + const source = await evaluationScript(script, arg); await this._channel.addInitScript({ source }); } diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index fcc785b71b..793219f10b 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -28,7 +28,7 @@ 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, hasArg: boolean, 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); @@ -46,7 +46,7 @@ export async function evaluationScript(fun: Function | string | { path?: string, } if (fun.path !== undefined) { let source = await fs.promises.readFile(fun.path, 'utf8'); - if (hasArg) { + if (arg !== undefined) { // Assume a CJS module that has a function default export. source = `(() => { var exports = {}; var module = { exports }; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 5ff6d6178d..a10286fa9a 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -492,7 +492,7 @@ export class Page extends ChannelOwner implements api.Page } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await evaluationScript(script, arg, arguments.length > 1); + const source = await evaluationScript(script, arg); await this._channel.addInitScript({ source }); } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index c7a7967559..2739be0e8d 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -26,7 +26,7 @@ export class Selectors implements api.Selectors { private _registrations: channels.SelectorsRegisterParams[] = []; async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { - const source = await evaluationScript(script, undefined, false, false); + const source = await evaluationScript(script, undefined, false); const params = { ...options, name, source }; for (const channel of this._channels) await channel._channel.register(params); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 0d018dcfd3..f4baa9a352 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -312,9 +312,9 @@ export interface Page { * // Passing 42 as an argument to the default export function. * await page.addInitScript({ path: mockPath }, 42); * - * // Make sure to pass undefined even if you do not need to pass an argument. + * // 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 }, undefined); + * await page.addInitScript({ path: mockPath }, ''); * ``` * * @param script Script to be evaluated in the page. @@ -7723,9 +7723,9 @@ export interface BrowserContext { * // Passing 42 as an argument to the default export function. * await context.addInitScript({ path: mockPath }, 42); * - * // Make sure to pass undefined even if you do not need to pass an argument. + * // 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 }, undefined); + * await context.addInitScript({ path: mockPath }, ''); * ``` * * @param script Script to be evaluated in all pages in the browser context. diff --git a/tests/page/page-add-init-script.spec.ts b/tests/page/page-add-init-script.spec.ts index 2b12f00251..2c8234a550 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -37,12 +37,6 @@ it('should assume CJS module with a path and arg', async ({ page, server, asset expect(await page.evaluate(() => window['injected'])).toBe(17); }); -it('should assume CJS module with a path and undefined arg', async ({ page, server, asset }) => { - await page.addInitScript({ path: asset('injectedmodule.js') }, undefined); - await page.goto(server.EMPTY_PAGE); - expect(await page.evaluate(() => window['injected'])).toBe(42); -}); - it('should work with content @smoke', async ({ page, server }) => { await page.addInitScript({ content: 'window["injected"] = 123' }); await page.goto(server.PREFIX + '/tamperable.html'); From 8c0e173d6c7d157d95f03a1aa6e64ba9f9704503 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 23 Aug 2024 12:51:49 +0200 Subject: [PATCH 021/104] test: rebase modernizer Linux tests (#32268) --- tests/assets/modernizr/mobile-safari-14-1.json | 4 ++-- tests/assets/modernizr/safari-14-1.json | 4 ++-- tests/library/modernizr.spec.ts | 16 +++++----------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/assets/modernizr/mobile-safari-14-1.json b/tests/assets/modernizr/mobile-safari-14-1.json index a67ccfb232..4e959333fa 100644 --- a/tests/assets/modernizr/mobile-safari-14-1.json +++ b/tests/assets/modernizr/mobile-safari-14-1.json @@ -29,7 +29,7 @@ "htmlimports": false, "history": true, "ie8compat": false, - "applicationcache": true, + "applicationcache": false, "blobconstructor": true, "blob-constructor": true, "cookies": true, @@ -166,7 +166,7 @@ "srcdoc": true, "imgcrossorigin": true, "hashchange": true, - "inputsearchevent": true, + "inputsearchevent": false, "ambientlight": false, "datalistelem": true, "videoloop": true, diff --git a/tests/assets/modernizr/safari-14-1.json b/tests/assets/modernizr/safari-14-1.json index 5be7c70bcc..5d39648297 100644 --- a/tests/assets/modernizr/safari-14-1.json +++ b/tests/assets/modernizr/safari-14-1.json @@ -29,7 +29,7 @@ "htmlimports": false, "history": true, "ie8compat": false, - "applicationcache": true, + "applicationcache": false, "blobconstructor": true, "blob-constructor": true, "cookies": true, @@ -166,7 +166,7 @@ "srcdoc": true, "imgcrossorigin": true, "hashchange": true, - "inputsearchevent": true, + "inputsearchevent": false, "ambientlight": false, "datalistelem": true, "videoloop": true, diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index 327a3c4169..9db5b24aa5 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -32,7 +32,6 @@ async function checkFeatures(name: string, context: any, server: any) { it('safari-14-1', async ({ browser, browserName, platform, server, headless, isMac }) => { it.skip(browserName !== 'webkit'); - it.skip(browserName === 'webkit' && platform === 'darwin', '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 context = await browser.newContext({ deviceScaleFactor: 2 @@ -41,6 +40,7 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM if (platform === 'linux') { expected.subpixelfont = false; + expected.speechrecognition = false; if (headless) expected.todataurljpeg = false; @@ -56,7 +56,6 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM if (platform === 'win32') { expected.datalistelem = false; - expected.fileinputdirectory = false; expected.getusermedia = false; expected.peerconnection = false; expected.speechrecognition = false; @@ -64,6 +63,7 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM expected.todataurljpeg = false; expected.unicode = false; expected.webaudio = false; + expected.gamepads = false; expected.input.list = false; expected.inputtypes.color = false; @@ -72,10 +72,8 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM expected.inputtypes.time = false; } - if (isMac && parseInt(os.release(), 10) > 20) { + if (isMac && parseInt(os.release(), 10) > 20) expected.applicationcache = false; - expected.inputsearchevent = false; - } expect(actual).toEqual(expected); }); @@ -99,6 +97,7 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is if (platform === 'linux') { expected.subpixelfont = false; + expected.speechrecognition = false; if (headless) expected.todataurljpeg = false; @@ -114,7 +113,6 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is if (platform === 'win32') { expected.datalistelem = false; - expected.fileinputdirectory = false; expected.getusermedia = false; expected.peerconnection = false; expected.speechrecognition = false; @@ -122,6 +120,7 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.todataurljpeg = false; expected.unicode = false; expected.webaudio = false; + expected.gamepads = false; expected.input.list = false; expected.inputtypes.color = false; @@ -133,11 +132,6 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.inputtypes.time = false; } - if (isMac && parseInt(os.release(), 10) > 20) { - expected.applicationcache = false; - expected.inputsearchevent = false; - } - expect(actual).toEqual(expected); }); From 785ca19e5188c9dab16ce5f526b9d074e5627233 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 23 Aug 2024 03:52:27 -0700 Subject: [PATCH 022/104] fix(webserver): prefix each line of webserver output (#32286) This unflakes various `web-server.spec.ts` tests and makes the output more consistent. --- .../playwright/src/plugins/webServerPlugin.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/playwright/src/plugins/webServerPlugin.ts b/packages/playwright/src/plugins/webServerPlugin.ts index 96a369bddb..e2474f35f2 100644 --- a/packages/playwright/src/plugins/webServerPlugin.ts +++ b/packages/playwright/src/plugins/webServerPlugin.ts @@ -113,13 +113,13 @@ export class WebServerPlugin implements TestRunnerPlugin { debugWebServer(`Process started`); - launchedProcess.stderr!.on('data', line => { + launchedProcess.stderr!.on('data', data => { if (debugWebServer.enabled || (this._options.stderr === 'pipe' || !this._options.stderr)) - this._reporter!.onStdErr?.(colors.dim('[WebServer] ') + line.toString()); + this._reporter!.onStdErr?.(prefixOutputLines(data.toString())); }); - launchedProcess.stdout!.on('data', line => { + launchedProcess.stdout!.on('data', data => { if (debugWebServer.enabled || this._options.stdout === 'pipe') - this._reporter!.onStdOut?.(colors.dim('[WebServer] ') + line.toString()); + this._reporter!.onStdOut?.(prefixOutputLines(data.toString())); }); } @@ -201,3 +201,14 @@ export const webServerPluginsForConfig = (config: FullConfigInternal): TestRunne return webServerPlugins; }; + +function prefixOutputLines(output: string) { + const lastIsNewLine = output[output.length - 1] === '\n'; + let lines = output.split('\n'); + if (lastIsNewLine) + lines.pop(); + lines = lines.map(line => colors.dim('[WebServer] ') + line); + if (lastIsNewLine) + lines.push(''); + return lines.join('\n'); +} From 3fb33e7144de86180c539ad59ea9085554419415 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 23 Aug 2024 14:58:34 +0200 Subject: [PATCH 023/104] chore(ui): decouple TestServerConnection from websocket transport (#32274) Preparation for https://github.com/microsoft/playwright/issues/32076. --- .../src/isomorphic/testServerConnection.ts | 68 +++++++++++++++---- packages/trace-viewer/src/ui/uiModeView.tsx | 4 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 4 +- .../test-server-connection.spec.ts | 4 +- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 34ce0e31ef..00dafdf20b 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -19,6 +19,48 @@ import * as events from './events'; // -- Reuse boundary -- Everything below this line is reused in the vscode extension. +export interface TestServerTransport { + onmessage(listener: (message: string) => void): void; + onopen(listener: () => void): void; + onerror(listener: () => void): void; + onclose(listener: () => void): void; + + send(data: string): void; + close(): void; +} + +export class WebSocketTestServerTransport implements TestServerTransport { + private _ws: WebSocket; + + constructor(url: string | URL) { + this._ws = new WebSocket(url); + } + + onmessage(listener: (message: string) => void) { + this._ws.addEventListener('message', event => listener(event.data)); + } + + onopen(listener: () => void) { + this._ws.addEventListener('open', listener); + } + + onerror(listener: () => void) { + this._ws.addEventListener('error', listener); + } + + onclose(listener: () => void) { + this._ws.addEventListener('close', listener); + } + + send(data: string) { + this._ws.send(data); + } + + close() { + this._ws.close(); + } +} + export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents { readonly onClose: events.Event; readonly onReport: events.Event; @@ -33,21 +75,21 @@ export class TestServerConnection implements TestServerInterface, TestServerInte private _onLoadTraceRequestedEmitter = new events.EventEmitter<{ traceUrl: string }>(); private _lastId = 0; - private _ws: WebSocket; + private _transport: TestServerTransport; private _callbacks = new Map void, reject: (arg: Error) => void }>(); private _connectedPromise: Promise; private _isClosed = false; - constructor(wsURL: string) { + constructor(transport: TestServerTransport) { this.onClose = this._onCloseEmitter.event; this.onReport = this._onReportEmitter.event; this.onStdio = this._onStdioEmitter.event; this.onTestFilesChanged = this._onTestFilesChangedEmitter.event; this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event; - this._ws = new WebSocket(wsURL); - this._ws.addEventListener('message', event => { - const message = JSON.parse(String(event.data)); + this._transport = transport; + this._transport.onmessage(data => { + const message = JSON.parse(data); const { id, result, error, method, params } = message; if (id) { const callback = this._callbacks.get(id); @@ -62,12 +104,12 @@ export class TestServerConnection implements TestServerInterface, TestServerInte this._dispatchEvent(method, params); } }); - const pingInterval = setInterval(() => this._sendMessage('ping').catch(() => {}), 30000); + const pingInterval = setInterval(() => this._sendMessage('ping').catch(() => { }), 30000); this._connectedPromise = new Promise((f, r) => { - this._ws.addEventListener('open', () => f()); - this._ws.addEventListener('error', r); + this._transport.onopen(f); + this._transport.onerror(r); }); - this._ws.addEventListener('close', () => { + this._transport.onclose(() => { this._isClosed = true; this._onCloseEmitter.fire(); clearInterval(pingInterval); @@ -85,14 +127,14 @@ export class TestServerConnection implements TestServerInterface, TestServerInte await this._connectedPromise; const id = ++this._lastId; const message = { id, method, params }; - this._ws.send(JSON.stringify(message)); + this._transport.send(JSON.stringify(message)); return new Promise((resolve, reject) => { this._callbacks.set(id, { resolve, reject }); }); } private _sendMessageNoReply(method: string, params?: any) { - this._sendMessage(method, params).catch(() => {}); + this._sendMessage(method, params).catch(() => { }); } private _dispatchEvent(method: string, params?: any) { @@ -200,8 +242,8 @@ export class TestServerConnection implements TestServerInterface, TestServerInte close() { try { - this._ws.close(); + this._transport.close(); } catch { } } -} +} \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 4a73575a11..0f799b2035 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -32,7 +32,7 @@ import { useDarkModeSetting } from '@web/theme'; import { clsx, settings, useSetting } from '@web/uiUtils'; import { statusEx, TestTree } from '@testIsomorphic/testTree'; import type { TreeItem } from '@testIsomorphic/testTree'; -import { TestServerConnection } from '@testIsomorphic/testServerConnection'; +import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection'; import { FiltersView } from './uiModeFiltersView'; import { TestListView } from './uiModeTestListView'; import { TraceView } from './uiModeTraceView'; @@ -134,7 +134,7 @@ export const UIModeView: React.FC<{}> = ({ const inputRef = React.useRef(null); const reloadTests = React.useCallback(() => { - setTestServerConnection(new TestServerConnection(wsURL.toString())); + setTestServerConnection(new TestServerConnection(new WebSocketTestServerTransport(wsURL))); }, []); // Load tests on startup. diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index e7f94e969a..4764b3d6a8 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -21,7 +21,7 @@ import { MultiTraceModel } from './modelUtil'; import './workbenchLoader.css'; import { toggleTheme } from '@web/theme'; import { Workbench } from './workbench'; -import { TestServerConnection } from '@testIsomorphic/testServerConnection'; +import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { @@ -102,7 +102,7 @@ export const WorkbenchLoader: React.FunctionComponent<{ const guid = new URLSearchParams(window.location.search).get('ws'); const wsURL = new URL(`../${guid}`, window.location.toString()); wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); - const testServerConnection = new TestServerConnection(wsURL.toString()); + const testServerConnection = new TestServerConnection(new WebSocketTestServerTransport(wsURL)); testServerConnection.onLoadTraceRequested(async params => { setTraceURLs(params.traceUrl ? [params.traceUrl] : []); setDragOver(false); diff --git a/tests/playwright-test/test-server-connection.spec.ts b/tests/playwright-test/test-server-connection.spec.ts index af5f0223e5..aef2b63483 100644 --- a/tests/playwright-test/test-server-connection.spec.ts +++ b/tests/playwright-test/test-server-connection.spec.ts @@ -15,13 +15,13 @@ */ import { test as baseTest, expect } from './ui-mode-fixtures'; -import { TestServerConnection } from '../../packages/playwright/lib/isomorphic/testServerConnection'; +import { TestServerConnection, WebSocketTestServerTransport } from '../../packages/playwright/lib/isomorphic/testServerConnection'; class TestServerConnectionUnderTest extends TestServerConnection { events: [string, any][] = []; constructor(wsUrl: string) { - super(wsUrl); + super(new WebSocketTestServerTransport(wsUrl)); this.onTestFilesChanged(params => this.events.push(['testFilesChanged', params])); this.onStdio(params => this.events.push(['stdio', params])); this.onLoadTraceRequested(params => this.events.push(['loadTraceRequested', params])); From 9a5b72d02abeea28a24b55e1c7a667f93992fe52 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 23 Aug 2024 06:16:18 -0700 Subject: [PATCH 024/104] chore: remove `TestInfoImpl._stages` (#32285) This is a preparation to a bigger stages cleanup. --- packages/playwright/src/worker/testInfo.ts | 28 ++++++++----------- .../playwright/src/worker/timeoutManager.ts | 4 +++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index b94da22bd9..e6b07be7c0 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -69,7 +69,6 @@ export class TestInfoImpl implements TestInfo { readonly _configInternal: FullConfigInternal; private readonly _steps: TestStepInternal[] = []; _onDidFinishTestFunction: (() => Promise) | undefined; - private readonly _stages: TestStage[] = []; _hasNonRetriableError = false; _hasUnhandledError = false; _allowSkips = false; @@ -227,10 +226,14 @@ export class TestInfoImpl implements TestInfo { } } - private _findLastStageStep() { - for (let i = this._stages.length - 1; i >= 0; i--) { - if (this._stages[i].step) - return this._stages[i].step; + private _findLastStageStep(steps: TestStepInternal[]): TestStepInternal | undefined { + // Find the deepest step that is marked as isStage and has not finished yet. + for (let i = steps.length - 1; i >= 0; i--) { + const child = this._findLastStageStep(steps[i].steps); + if (child) + return child; + if (steps[i].isStage && !steps[i].endWallTime) + return steps[i]; } } @@ -240,12 +243,12 @@ export class TestInfoImpl implements TestInfo { let parentStep: TestStepInternal | undefined; if (data.isStage) { // Predefined stages form a fixed hierarchy - use the current one as parent. - parentStep = this._findLastStageStep(); + parentStep = this._findLastStageStep(this._steps); } else { parentStep = zones.zoneData('stepZone'); if (!parentStep) { // If no parent step on stack, assume the current stage as parent. - parentStep = this._findLastStageStep(); + parentStep = this._findLastStageStep(this._steps); } } @@ -341,7 +344,6 @@ export class TestInfoImpl implements TestInfo { debugTest(`started stage "${stage.title}"${location}`); } stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, isStage: true }) : undefined; - this._stages.push(stage); try { await this._timeoutManager.withRunnable(stage.runnable, async () => { @@ -376,9 +378,6 @@ export class TestInfoImpl implements TestInfo { stage.step?.complete({ error }); throw error; } finally { - if (this._stages[this._stages.length - 1] !== stage) - throw new Error(`Internal error: inconsistent stages!`); - this._stages.pop(); debugTest(`finished stage "${stage.title}"`); } } @@ -388,11 +387,8 @@ export class TestInfoImpl implements TestInfo { } _currentHookType() { - for (let i = this._stages.length - 1; i >= 0; i--) { - const type = this._stages[i].runnable?.type; - if (type && ['beforeAll', 'afterAll', 'beforeEach', 'afterEach'].includes(type)) - return type; - } + const type = this._timeoutManager.currentSlotType(); + return ['beforeAll', 'afterAll', 'beforeEach', 'afterEach'].includes(type) ? type : undefined; } _setDebugMode() { diff --git a/packages/playwright/src/worker/timeoutManager.ts b/packages/playwright/src/worker/timeoutManager.ts index e88af26933..71b853c463 100644 --- a/packages/playwright/src/worker/timeoutManager.ts +++ b/packages/playwright/src/worker/timeoutManager.ts @@ -142,6 +142,10 @@ export class TimeoutManager { return this._running ? this._running.deadline : kMaxDeadline; } + currentSlotType() { + return this._running ? this._running.runnable.type : 'test'; + } + private _createTimeoutError(running: Running): Error { let message = ''; const timeout = running.slot.timeout; From 1b220c528952dab0dbe5272df844f755173cc4d1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 23 Aug 2024 15:17:00 +0200 Subject: [PATCH 025/104] chore: remove Chromium Windows proxy hacks (#31724) Fixes https://github.com/microsoft/playwright/issues/17252 --- docs/src/api/params.md | 6 --- docs/src/network.md | 48 +++++++------------ .../src/server/browserContext.ts | 6 +-- .../src/server/chromium/chromium.ts | 6 --- packages/playwright-core/types/types.d.ts | 8 ---- tests/library/browsercontext-proxy.spec.ts | 25 ---------- tests/library/fetch-proxy.spec.ts | 9 ---- 7 files changed, 19 insertions(+), 89 deletions(-) diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 5aa9c6a5ab..ccd6bae8fe 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -769,12 +769,6 @@ Actual picture of each page will be scaled down if necessary to fit the specifie Network proxy settings to use with this context. Defaults to none. -:::note -For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all -contexts override the proxy, global proxy will be never used and can be any string, for example -`launch({ proxy: { server: 'http://per-context' } })`. -::: - ## context-option-strict - `strictSelectors` <[boolean]> diff --git a/docs/src/network.md b/docs/src/network.md index f1fbc620b3..c7e6bdca1c 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -147,7 +147,7 @@ const browser = await chromium.launch({ Browser browser = chromium.launch(new BrowserType.LaunchOptions() .setProxy(new Proxy("http://myproxy.com:3128") .setUsername('usr') - .setPassword('pwd')); + .setPassword('pwd'))); ``` ```python async @@ -179,60 +179,48 @@ await using var browser = await BrowserType.LaunchAsync(new() }); ``` -When specifying proxy for each context individually, **Chromium on Windows** needs a hint that proxy will be set. This is done via passing a non-empty proxy server to the browser itself. Here is an example of a context-specific proxy: +Its also possible to specify it per context: -```js tab=js-test title="playwright.config.ts" -import { defineConfig } from '@playwright/test'; -export default defineConfig({ - use: { - launchOptions: { - // Browser proxy option is required for Chromium on Windows. - proxy: { server: 'per-context' } - }, +```js tab=js-test title="example.spec.ts" +import { test, expect } from '@playwright/test'; + +test('should use custom proxy on a new context', async ({ browser }) => { + const context = await browser.newContext({ proxy: { server: 'http://myproxy.com:3128', } - } + }); + const page = await context.newPage(); + + await context.close(); }); ``` ```js tab=js-library -const browser = await chromium.launch({ - // Browser proxy option is required for Chromium on Windows. - proxy: { server: 'per-context' } -}); +const browser = await chromium.launch(); const context = await browser.newContext({ proxy: { server: 'http://myproxy.com:3128' } }); ``` ```java -Browser browser = chromium.launch(new BrowserType.LaunchOptions() - // Browser proxy option is required for Chromium on Windows. - .setProxy(new Proxy("per-context")); -BrowserContext context = chromium.launch(new Browser.NewContextOptions() - .setProxy(new Proxy("http://myproxy.com:3128")); +Browser browser = chromium.launch(); +BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .setProxy(new Proxy("http://myproxy.com:3128"))); ``` ```python async -# Browser proxy option is required for Chromium on Windows. -browser = await chromium.launch(proxy={"server": "per-context"}) +browser = await chromium.launch() context = await browser.new_context(proxy={"server": "http://myproxy.com:3128"}) ``` ```python sync -# Browser proxy option is required for Chromium on Windows. -browser = chromium.launch(proxy={"server": "per-context"}) +browser = chromium.launch() context = browser.new_context(proxy={"server": "http://myproxy.com:3128"}) ``` ```csharp -var proxy = new Proxy { Server = "per-context" }; -await using var browser = await BrowserType.LaunchAsync(new() -{ - // Browser proxy option is required for Chromium on Windows. - Proxy = proxy -}); +await using var browser = await BrowserType.LaunchAsync(); await using var context = await browser.NewContextAsync(new() { Proxy = new Proxy { Server = "http://myproxy.com:3128" }, diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 80681130ef..db20728904 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import * as os from 'os'; import { TimeoutSettings } from '../common/timeoutSettings'; import { createGuid, debugMode } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; @@ -700,11 +699,8 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex options.recordVideo.size!.width &= ~1; options.recordVideo.size!.height &= ~1; } - if (options.proxy) { - if (!browserOptions.proxy && browserOptions.isChromium && os.platform() === 'win32') - throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'http://per-context' } })"`); + if (options.proxy) options.proxy = normalizeProxySettings(options.proxy); - } verifyGeolocation(options.geolocation); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index a30be361e7..04177b4690 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -110,12 +110,6 @@ export class Chromium extends BrowserType { artifactsDir, downloadsPath: options.downloadsPath || artifactsDir, tracesDir: options.tracesDir || artifactsDir, - // On Windows context level proxies only work, if there isn't a global proxy - // set. This is currently a bug in the CR/Windows networking stack. By - // passing an arbitrary value we disable the check in PW land which warns - // users in normal (launch/launchServer) mode since otherwise connectOverCDP - // does not work at all with proxies on Windows. - proxy: { server: 'per-context' }, originalLaunchOptions: {}, }; validateBrowserContextOptions(persistent, browserOptions); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f4baa9a352..9c125fef1e 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9372,10 +9372,6 @@ export interface Browser { /** * Network proxy settings to use with this context. Defaults to none. - * - * **NOTE** For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If - * all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ - * proxy: { server: 'http://per-context' } })`. */ proxy?: { /** @@ -20847,10 +20843,6 @@ export interface BrowserContextOptions { /** * Network proxy settings to use with this context. Defaults to none. - * - * **NOTE** For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If - * all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ - * proxy: { server: 'http://per-context' } })`. */ proxy?: { /** diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index d56bce0972..c2d9d5b31c 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -18,37 +18,12 @@ import { browserTest as it, expect } from '../config/browserTest'; it.skip(({ mode }) => mode.startsWith('service')); -it.use({ - launchOptions: async ({ launchOptions }, use) => { - await use({ - ...launchOptions, - proxy: { server: 'per-context' } - }); - } -}); - - it.beforeEach(({ server }) => { server.setRoute('/target.html', async (req, res) => { res.end('Served by the proxy'); }); }); -it('should throw for missing global proxy on Chromium Windows', async ({ browserName, platform, browserType, server }) => { - it.skip(browserName !== 'chromium' || platform !== 'win32'); - - let browser; - try { - browser = await browserType.launch({ - proxy: undefined, - }); - const error = await browser.newContext({ proxy: { server: `localhost:${server.PORT}` } }).catch(e => e); - expect(error.toString()).toContain('Browser needs to be launched with the global proxy'); - } finally { - await browser.close(); - } -}); - it('should work when passing the proxy only on the context level', async ({ browserName, platform, browserType, server, proxyServer }) => { // Currently an upstream bug in the network stack of Chromium which leads that // the wrong proxy gets used in the BrowserContext. diff --git a/tests/library/fetch-proxy.spec.ts b/tests/library/fetch-proxy.spec.ts index b0c921c8e3..f6961e6de5 100644 --- a/tests/library/fetch-proxy.spec.ts +++ b/tests/library/fetch-proxy.spec.ts @@ -16,15 +16,6 @@ import { contextTest as it, expect } from '../config/browserTest'; -it.use({ - launchOptions: async ({ launchOptions }, use) => { - await use({ - ...launchOptions, - proxy: { server: 'per-context' } - }); - } -}); - it.skip(({ mode }) => mode !== 'default'); it('context request should pick up proxy credentials', async ({ browserType, server, proxyServer }) => { From 787f20c920dade722b046e3199ea8fe38e3eae7f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 23 Aug 2024 16:26:39 +0200 Subject: [PATCH 026/104] 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 027/104] 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 028/104] 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 029/104] 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 030/104] 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 031/104] 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 032/104] 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 033/104] 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 034/104] 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 035/104] 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 036/104] 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 037/104] 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 038/104] 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 039/104] 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 040/104] 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 041/104] 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 042/104] 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 043/104] 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 044/104] 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 045/104] 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 046/104] 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 047/104] 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 048/104] 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 049/104] 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 050/104] 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 051/104] 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 052/104] 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 053/104] 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 054/104] 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 055/104] 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 056/104] 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 057/104] 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 058/104] 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 059/104] 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 060/104] 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 061/104] 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 062/104] 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 063/104] 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 064/104] 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 065/104] 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 066/104] 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 067/104] 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 068/104] 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 069/104] 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 070/104] 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 071/104] 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 072/104] 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", From 847d29dd865af08d8bf05281347a9d4b0032d16e Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 3 Sep 2024 04:28:15 -0700 Subject: [PATCH 073/104] feat(webkit): roll to r2069 (#32422) 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 6d395287a2..6a71bdc01e 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2068", + "revision": "2069", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 53bf9534ec0fada103d0978f02c7a35c2310ef29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:52:52 +0200 Subject: [PATCH 074/104] chore(deps): bump micromatch from 4.0.5 to 4.0.8 in /packages/playwright/bundles/expect (#32399) --- packages/playwright/ThirdPartyNotices.txt | 62 ++----------------- .../bundles/expect/package-lock.json | 44 ++++++------- 2 files changed, 26 insertions(+), 80 deletions(-) diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index 21bb223549..020b2bc3f6 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -89,7 +89,6 @@ This project incorporates components from the projects listed below. The origina - ansi-styles@5.2.0 (https://github.com/chalk/ansi-styles) - anymatch@3.1.3 (https://github.com/micromatch/anymatch) - binary-extensions@2.2.0 (https://github.com/sindresorhus/binary-extensions) -- braces@3.0.2 (https://github.com/micromatch/braces) - braces@3.0.3 (https://github.com/micromatch/braces) - browserslist@4.22.2 (https://github.com/browserslist/browserslist) - buffer-from@1.1.2 (https://github.com/LinusU/buffer-from) @@ -112,7 +111,6 @@ This project incorporates components from the projects listed below. The origina - escape-string-regexp@1.0.5 (https://github.com/sindresorhus/escape-string-regexp) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - expect@29.5.0 (https://github.com/facebook/jest) -- fill-range@7.0.1 (https://github.com/jonschlinkert/fill-range) - fill-range@7.1.1 (https://github.com/jonschlinkert/fill-range) - gensync@1.0.0-beta.2 (https://github.com/loganfsmyth/gensync) - glob-parent@5.1.2 (https://github.com/gulpjs/glob-parent) @@ -133,7 +131,7 @@ This project incorporates components from the projects listed below. The origina - jsesc@2.5.2 (https://github.com/mathiasbynens/jsesc) - json5@2.2.3 (https://github.com/json5/json5) - lru-cache@5.1.1 (https://github.com/isaacs/node-lru-cache) -- micromatch@4.0.5 (https://github.com/micromatch/micromatch) +- micromatch@4.0.8 (https://github.com/micromatch/micromatch) - ms@2.1.2 (https://github.com/zeit/ms) - node-releases@2.0.14 (https://github.com/chicoxyzzy/node-releases) - normalize-path@3.0.0 (https://github.com/jonschlinkert/normalize-path) @@ -2545,32 +2543,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF binary-extensions@2.2.0 AND INFORMATION -%% braces@3.0.2 NOTICES AND INFORMATION BEGIN HERE -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF braces@3.0.2 AND INFORMATION - %% braces@3.0.3 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -3424,32 +3396,6 @@ SOFTWARE. ========================================= END OF expect@29.5.0 AND INFORMATION -%% fill-range@7.0.1 NOTICES AND INFORMATION BEGIN HERE -========================================= -The MIT License (MIT) - -Copyright (c) 2014-present, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF fill-range@7.0.1 AND INFORMATION - %% fill-range@7.1.1 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -3891,7 +3837,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF lru-cache@5.1.1 AND INFORMATION -%% micromatch@4.0.5 NOTICES AND INFORMATION BEGIN HERE +%% micromatch@4.0.8 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -3915,7 +3861,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF micromatch@4.0.5 AND INFORMATION +END OF micromatch@4.0.8 AND INFORMATION %% ms@2.1.2 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -4405,6 +4351,6 @@ END OF yallist@3.1.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 151 +Total Packages: 149 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright/bundles/expect/package-lock.json b/packages/playwright/bundles/expect/package-lock.json index a6bf58d4d2..9a1707ff38 100644 --- a/packages/playwright/bundles/expect/package-lock.json +++ b/packages/playwright/bundles/expect/package-lock.json @@ -210,11 +210,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -297,9 +297,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -405,11 +405,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -660,11 +660,11 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "chalk": { @@ -717,9 +717,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } @@ -801,11 +801,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, From 201bad75d3c44334059da387a947bae68fa500e9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 3 Sep 2024 15:15:44 +0200 Subject: [PATCH 075/104] chore(test runner): rebase watch mode onto TestServerConnection (#32156) Closes https://github.com/microsoft/playwright/issues/32076. This PR rewrites `watchMode.ts` to use `TestServer` under the hood. It's essentially a complete rewrite, so don't pay too much attention on the old implementation. Note that there's no changes to tests, so all behaviour we have specced out there still works. To make this work without a superfluous WebSocket connection, I had to refactor `TestServerConnection` a little. Originally, I pulled this into a [separate PR](https://github.com/microsoft/playwright/pull/32132), but then realised how small the refactoring is. So it's in this PR now. Let me know if you'd like to land it separately. --- .../src/isomorphic/testServerConnection.ts | 2 +- packages/playwright/src/program.ts | 31 +- packages/playwright/src/runner/DEPS.list | 3 +- packages/playwright/src/runner/loadUtils.ts | 4 +- packages/playwright/src/runner/runner.ts | 7 - packages/playwright/src/runner/tasks.ts | 11 +- packages/playwright/src/runner/testServer.ts | 2 +- packages/playwright/src/runner/watchMode.ts | 358 ++++++++---------- tests/playwright-test/only-changed.spec.ts | 36 +- tests/playwright-test/watch.spec.ts | 16 +- 10 files changed, 199 insertions(+), 271 deletions(-) diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 00dafdf20b..22bdce30ff 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -246,4 +246,4 @@ export class TestServerConnection implements TestServerInterface, TestServerInte } catch { } } -} \ No newline at end of file +} diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 803b9d2291..4b5bea19d3 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -24,9 +24,9 @@ import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'p import { serializeError } from './util'; import { showHTMLReport } from './reporters/html'; import { createMergedReport } from './reporters/merge'; -import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports } from './common/configLoader'; +import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader'; import type { ConfigCLIOverrides } from './common/ipc'; -import type { FullResult, TestError } from '../types/testReporter'; +import type { TestError } from '../types/testReporter'; import type { TraceMode } from '../types/test'; import { builtInReporters, defaultReporter, defaultTimeout } from './common/config'; import { program } from 'playwright-core/lib/cli/program'; @@ -35,6 +35,7 @@ import type { ReporterDescription } from '../types/test'; import { prepareErrorStack } from './reporters/base'; import * as testServer from './runner/testServer'; import { clearCacheAndLogToConsole } from './runner/testServer'; +import { runWatchModeLoop } from './runner/watchMode'; function addTestCommand(program: Command) { const command = program.command('test [test-filter...]'); @@ -183,6 +184,26 @@ async function runTests(args: string[], opts: { [key: string]: any }) { return; } + if (process.env.PWTEST_WATCH) { + if (opts.onlyChanged) + throw new Error(`--only-changed is not supported in watch mode. If you'd like that to change, file an issue and let us know about your usecase for it.`); + + const status = await runWatchModeLoop( + resolveConfigLocation(opts.config), + { + projects: opts.project, + files: args, + grep: opts.grep + } + ); + await stopProfiling('runner'); + if (status === 'restarted') + return; + const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); + gracefullyProcessExitDoNotHang(exitCode); + return; + } + const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false); if (!config) return; @@ -202,11 +223,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliFailOnFlakyTests = !!opts.failOnFlakyTests; const runner = new Runner(config); - let status: FullResult['status']; - if (process.env.PWTEST_WATCH) - status = await runner.watchAllTests(); - else - status = await runner.runAllTests(); + const status = await runner.runAllTests(); await stopProfiling('runner'); const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); gracefullyProcessExitDoNotHang(exitCode); diff --git a/packages/playwright/src/runner/DEPS.list b/packages/playwright/src/runner/DEPS.list index cdf6044844..bc1cc6d763 100644 --- a/packages/playwright/src/runner/DEPS.list +++ b/packages/playwright/src/runner/DEPS.list @@ -7,6 +7,5 @@ ../plugins/ ../util.ts ../utilsBundle.ts -../isomorphic/folders.ts -../isomorphic/teleReceiver.ts +../isomorphic/ ../fsWatcher.ts diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts index cd735ceca3..63a2307507 100644 --- a/packages/playwright/src/runner/loadUtils.ts +++ b/packages/playwright/src/runner/loadUtils.ts @@ -33,7 +33,7 @@ import { sourceMapSupport } from '../utilsBundle'; import type { RawSourceMap } from 'source-map'; -export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) { +export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean) { const config = testRun.config; const fsCache = new Map(); const sourceMapCache = new Map(); @@ -52,8 +52,6 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest for (const [project, files] of allFilesForProject) { const matchedFiles = files.filter(file => { const hasMatchingSources = sourceMapSources(file, sourceMapCache).some(source => { - if (additionalFileMatcher && !additionalFileMatcher(source)) - return false; if (cliFileMatcher && !cliFileMatcher(source)) return false; return true; diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index a7fd28ec87..05cf8a6aac 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -24,7 +24,6 @@ import { collectFilesForProject, filterProjects } from './projectUtils'; import { createReporters } from './reporters'; import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; import type { FullConfigInternal } from '../common/config'; -import { runWatchModeLoop } from './watchMode'; import type { Suite } from '../common/test'; import { wrapReporterAsV2 } from '../reporters/reporterV2'; import { affectedTestFiles } from '../transform/compilationCache'; @@ -131,12 +130,6 @@ export class Runner { return { status, suite: testRun.rootSuite, errors }; } - async watchAllTests(): Promise { - const config = this._config; - webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); - return await runWatchModeLoop(config); - } - async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise { const result = await this.loadAllTests(mode); if (result.status !== 'passed' || !result.suite) diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 0a1e001a91..09a8a1fdaf 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -74,13 +74,6 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report return taskRunner; } -export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner { - const taskRunner = TaskRunner.create(reporters); - taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher })); - addRunTasks(taskRunner, config); - return taskRunner; -} - export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { const taskRunner = TaskRunner.create(reporters); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); @@ -222,10 +215,10 @@ function createListFilesTask(): Task { }; } -function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task { +function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean }): Task { return { setup: async (reporter, testRun, errors, softErrors) => { - await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher); + await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter); await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); let cliOnlyChangedMatcher: Matcher | undefined = undefined; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 474ed6ae5d..1df2d72e8c 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -62,7 +62,7 @@ class TestServer { } } -class TestServerDispatcher implements TestServerInterface { +export class TestServerDispatcher implements TestServerInterface { private _configLocation: ConfigLocation; private _watcher: Watcher; diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 709e39100b..924449adce 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -15,130 +15,130 @@ */ import readline from 'readline'; +import path from 'path'; import { createGuid, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils'; -import type { FullConfigInternal, FullProjectInternal } from '../common/config'; -import { createFileMatcher, createFileMatcherFromArguments } from '../util'; -import type { Matcher } from '../util'; -import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; -import { buildProjectsClosure, filterProjects } from './projectUtils'; -import { collectAffectedTestFiles } from '../transform/compilationCache'; +import type { ConfigLocation } from '../common/config'; import type { FullResult } from '../../types/testReporter'; -import { chokidar } from '../utilsBundle'; -import type { FSWatcher as CFSWatcher } from 'chokidar'; import { colors } from 'playwright-core/lib/utilsBundle'; import { enquirer } from '../utilsBundle'; import { separator } from '../reporters/base'; import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer'; -import ListReporter from '../reporters/list'; +import { TestServerDispatcher } from './testServer'; +import { EventEmitter } from 'stream'; +import { type TestServerTransport, TestServerConnection } from '../isomorphic/testServerConnection'; +import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater'; +import { restartWithExperimentalTsEsm } from '../common/configLoader'; -class FSWatcher { - private _dirtyTestFiles = new Map>(); - private _notifyDirtyFiles: (() => void) | undefined; - private _watcher: CFSWatcher | undefined; - private _timer: NodeJS.Timeout | undefined; - - async update(config: FullConfigInternal) { - const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true; - const projects = filterProjects(config.projects, config.cliProjectFilter); - const projectClosure = buildProjectsClosure(projects); - const projectFilters = new Map(); - for (const [project, type] of projectClosure) { - const testMatch = createFileMatcher(project.project.testMatch); - const testIgnore = createFileMatcher(project.project.testIgnore); - projectFilters.set(project, file => { - if (!file.startsWith(project.project.testDir) || !testMatch(file) || testIgnore(file)) - return false; - return type === 'dependency' || commandLineFileMatcher(file); - }); - } - - if (this._timer) - clearTimeout(this._timer); - if (this._watcher) - await this._watcher.close(); - - this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.project.testDir), { ignoreInitial: true }).on('all', async (event, file) => { - if (event !== 'add' && event !== 'change') - return; - - const testFiles = new Set(); - collectAffectedTestFiles(file, testFiles); - const testFileArray = [...testFiles]; - - let hasMatches = false; - for (const [project, filter] of projectFilters) { - const filteredFiles = testFileArray.filter(filter); - if (!filteredFiles.length) - continue; - let set = this._dirtyTestFiles.get(project); - if (!set) { - set = new Set(); - this._dirtyTestFiles.set(project, set); - } - filteredFiles.map(f => set!.add(f)); - hasMatches = true; - } - - if (!hasMatches) - return; - - if (this._timer) - clearTimeout(this._timer); - this._timer = setTimeout(() => { - this._notifyDirtyFiles?.(); - }, 250); - }); +class InMemoryTransport extends EventEmitter implements TestServerTransport { + public readonly _send: (data: string) => void; + constructor(send: (data: any) => void) { + super(); + this._send = send; } - async onDirtyTestFiles(): Promise { - if (this._dirtyTestFiles.size) - return; - await new Promise(f => this._notifyDirtyFiles = f); + close() { + this.emit('close'); } - takeDirtyTestFiles(): Map> { - const result = this._dirtyTestFiles; - this._dirtyTestFiles = new Map(); - return result; + onclose(listener: () => void): void { + this.on('close', listener); + } + + onerror(listener: () => void): void { + // no-op to fulfil the interface, the user of InMemoryTransport doesn't emit any errors. + } + + onmessage(listener: (message: string) => void): void { + this.on('message', listener); + } + + onopen(listener: () => void): void { + this.on('open', listener); + } + + send(data: string): void { + this._send(data); } } -export async function runWatchModeLoop(config: FullConfigInternal): Promise { - // Reset the settings that don't apply to watch. - config.cliPassWithNoTests = true; - for (const p of config.projects) - p.project.retries = 0; +interface WatchModeOptions { + files?: string[]; + projects?: string[]; + grep?: string; +} - // Perform global setup. - const testRun = new TestRun(config); - const taskRunner = createTaskRunnerForWatchSetup(config, [new ListReporter()]); - taskRunner.reporter.onConfigure(config.config); - const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); - if (status !== 'passed') - await globalCleanup(); - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); - if (status !== 'passed') - return status; +export async function runWatchModeLoop(configLocation: ConfigLocation, initialOptions: WatchModeOptions): Promise { + if (restartWithExperimentalTsEsm(undefined, true)) + return 'restarted'; - // Prepare projects that will be watched, set up watcher. - const failedTestIdCollector = new Set(); - const originalWorkers = config.config.workers; - const fsWatcher = new FSWatcher(); - await fsWatcher.update(config); + const options: WatchModeOptions = { ...initialOptions }; - let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set, dirtyTestFiles?: Map> } = { type: 'regular' }; + const testServerDispatcher = new TestServerDispatcher(configLocation); + const transport = new InMemoryTransport( + async data => { + const { id, method, params } = JSON.parse(data); + try { + const result = await testServerDispatcher.transport.dispatch(method, params); + transport.emit('message', JSON.stringify({ id, result })); + } catch (e) { + transport.emit('message', JSON.stringify({ id, error: String(e) })); + } + } + ); + testServerDispatcher.transport.sendEvent = (method, params) => { + transport.emit('message', JSON.stringify({ method, params })); + }; + const testServerConnection = new TestServerConnection(transport); + transport.emit('open'); + + const teleSuiteUpdater = new TeleSuiteUpdater({ pathSeparator: path.sep, onUpdate() { } }); + + const dirtyTestIds = new Set(); + let onDirtyTests = new ManualPromise(); + + let queue = Promise.resolve(); + const changedFiles = new Set(); + testServerConnection.onTestFilesChanged(({ testFiles }) => { + testFiles.forEach(file => changedFiles.add(file)); + + queue = queue.then(async () => { + if (changedFiles.size === 0) + return; + + const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep }); + teleSuiteUpdater.processListReport(report); + + for (const test of teleSuiteUpdater.rootSuite!.allTests()) { + if (changedFiles.has(test.location.file)) + dirtyTestIds.add(test.id); + } + + changedFiles.clear(); + + if (dirtyTestIds.size > 0) + onDirtyTests.resolve?.(); + }); + }); + testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report)); + + await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true }); + await testServerConnection.runGlobalSetup({}); + + const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep }); + teleSuiteUpdater.processListReport(report); + + let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestIds?: string[] } = { type: 'regular' }; let result: FullResult['status'] = 'passed'; // Enter the watch loop. - await runTests(config, failedTestIdCollector); + await runTests(options, testServerConnection); while (true) { printPrompt(); const readCommandPromise = readCommand(); await Promise.race([ - fsWatcher.onDirtyTestFiles(), + onDirtyTests, readCommandPromise, ]); if (!readCommandPromise.isDone()) @@ -147,32 +147,32 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise({ + const { selectedProjects } = await enquirer.prompt<{ selectedProjects: string[] }>({ type: 'multiselect', - name: 'projectNames', + name: 'selectedProjects', message: 'Select projects', - choices: config.projects.map(p => ({ name: p.project.name })), - }).catch(() => ({ projectNames: null })); - if (!projectNames) + choices: teleSuiteUpdater.rootSuite!.suites.map(s => s.title), + }).catch(() => ({ selectedProjects: null })); + if (!selectedProjects) continue; - config.cliProjectFilter = projectNames.length ? projectNames : undefined; - await fsWatcher.update(config); - await runTests(config, failedTestIdCollector); + options.projects = selectedProjects.length ? selectedProjects : undefined; + await runTests(options, testServerConnection); lastRun = { type: 'regular' }; continue; } @@ -186,11 +186,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise failedTestIdCollector.has(id); - const failedTestIds = new Set(failedTestIdCollector); - await runTests(config, failedTestIdCollector, { title: 'running failed tests' }); - config.testIdMatcher = undefined; + const failedTestIds = teleSuiteUpdater.rootSuite!.allTests().filter(t => !t.ok()).map(t => t.id); + await runTests({}, testServerConnection, { title: 'running failed tests', testIds: failedTestIds }); lastRun = { type: 'failed', failedTestIds }; continue; } if (command === 'repeat') { if (lastRun.type === 'regular') { - await runTests(config, failedTestIdCollector, { title: 're-running tests' }); + await runTests(options, testServerConnection, { title: 're-running tests' }); continue; } else if (lastRun.type === 'changed') { - await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles!, 're-running tests'); + await runTests(options, testServerConnection, { title: 're-running tests', testIds: lastRun.dirtyTestIds }); } else if (lastRun.type === 'failed') { - config.testIdMatcher = id => lastRun.failedTestIds!.has(id); - await runTests(config, failedTestIdCollector, { title: 're-running tests' }); - config.testIdMatcher = undefined; + await runTests({}, testServerConnection, { title: 're-running tests', testIds: lastRun.failedTestIds }); } continue; } if (command === 'toggle-show-browser') { - await toggleShowBrowser(config, originalWorkers); + await toggleShowBrowser(); continue; } @@ -250,71 +244,27 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise, filesByProject: Map>, title?: string) { - const testFiles = new Set(); - for (const files of filesByProject.values()) - files.forEach(f => testFiles.add(f)); - - // Collect all the affected projects, follow project dependencies. - // Prepare to exclude all the projects that do not depend on this file, as if they did not exist. - const projects = filterProjects(config.projects, config.cliProjectFilter); - const projectClosure = buildProjectsClosure(projects); - const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]); - const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency'); - - // If there are affected dependency projects, do the full run, respect the original CLI. - // if there are no affected dependency projects, intersect CLI with dirty files - const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file); - await runTests(config, failedTestIdCollector, { additionalFileMatcher, title: title || 'files changed' }); -} - -async function runTests(config: FullConfigInternal, failedTestIdCollector: Set, options?: { - projectsToIgnore?: Set, - additionalFileMatcher?: Matcher, +async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: { title?: string, + testIds?: string[], }) { - printConfiguration(config, options?.title); - const taskRunner = createTaskRunnerForWatch(config, [new ListReporter()], options?.additionalFileMatcher); - const testRun = new TestRun(config); - taskRunner.reporter.onConfigure(config.config); - const taskStatus = await taskRunner.run(testRun, 0); - let status: FullResult['status'] = 'passed'; + printConfiguration(watchOptions, options?.title); - let hasFailedTests = false; - for (const test of testRun.rootSuite?.allTests() || []) { - if (test.outcome() === 'unexpected') { - failedTestIdCollector.add(test.id); - hasFailedTests = true; - } else { - failedTestIdCollector.delete(test.id); - } - } - - if (testRun.failureTracker.hasWorkerErrors() || hasFailedTests) - status = 'failed'; - if (status === 'passed' && taskStatus !== 'passed') - status = taskStatus; - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); -} - -function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected: FullProjectInternal[]): Set { - const result = new Set(affected); - for (let i = 0; i < projectClosure.length; ++i) { - for (const p of projectClosure) { - for (const dep of p.deps) { - if (result.has(dep)) - result.add(p); - } - if (p.teardown && result.has(p.teardown)) - result.add(p); - } - } - return result; + await testServerConnection.runTests({ + grep: watchOptions.grep, + testIds: options?.testIds, + locations: watchOptions?.files, + projects: watchOptions.projects, + connectWsEndpoint, + reuseContext: connectWsEndpoint ? true : undefined, + workers: connectWsEndpoint ? 1 : undefined, + headed: connectWsEndpoint ? true : undefined, + }); } function readCommand(): ManualPromise { @@ -377,17 +327,19 @@ Change settings } let showBrowserServer: PlaywrightServer | undefined; +let connectWsEndpoint: string | undefined = undefined; let seq = 0; -function printConfiguration(config: FullConfigInternal, title?: string) { +function printConfiguration(options: WatchModeOptions, title?: string) { const packageManagerCommand = getPackageManagerExecCommand(); const tokens: string[] = []; tokens.push(`${packageManagerCommand} playwright test`); - tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); - if (config.cliGrep) - tokens.push(colors.red(`--grep ${config.cliGrep}`)); - if (config.cliArgs) - tokens.push(...config.cliArgs.map(a => colors.bold(a))); + if (options.projects) + tokens.push(...options.projects.map(p => colors.blue(`--project ${p}`))); + if (options.grep) + tokens.push(colors.red(`--grep ${options.grep}`)); + if (options.files) + tokens.push(...options.files.map(a => colors.bold(a))); if (title) tokens.push(colors.dim(`(${title})`)); if (seq) @@ -409,25 +361,15 @@ ${colors.dim('Waiting for file changes. Press')} ${colors.bold('enter')} ${color `); } -async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) { +async function toggleShowBrowser() { if (!showBrowserServer) { - config.config.workers = 1; showBrowserServer = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: 1 }); - const wsEndpoint = await showBrowserServer.listen(); - config.configCLIOverrides.use = { - ...config.configCLIOverrides.use, - _optionContextReuseMode: 'when-possible', - _optionConnectOptions: { wsEndpoint }, - }; + connectWsEndpoint = await showBrowserServer.listen(); process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`); } else { - config.config.workers = originalWorkers; - if (config.configCLIOverrides.use) { - delete config.configCLIOverrides.use._optionContextReuseMode; - delete config.configCLIOverrides.use._optionConnectOptions; - } await showBrowserServer?.close(); showBrowserServer = undefined; + connectWsEndpoint = undefined; process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('off')}\n`); } } diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 6fe915f702..674234a240 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -166,39 +166,13 @@ test('should understand dependency structure', async ({ runInlineTest, git, writ expect(result.output).not.toContain('c.spec.ts'); }); -test('should support watch mode', async ({ git, writeFiles, runWatchTest }) => { - await writeFiles({ - 'a.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('fails', () => { expect(1).toBe(2); }); - `, - 'b.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('fails', () => { expect(1).toBe(2); }); - `, - }); - - git(`add .`); - git(`commit -m init`); - - await writeFiles({ - 'b.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('fails', () => { expect(1).toBe(3); }); - `, - }); - git(`commit -a -m update`); - - const testProcess = await runWatchTest({}, { 'only-changed': `HEAD~1` }); - await testProcess.waitForOutput('Waiting for file changes.'); - testProcess.clearOutput(); - testProcess.write('r'); - - await testProcess.waitForOutput('b.spec.ts:3:13 › fails'); - expect(testProcess.output).not.toContain('a.spec'); +test('watch mode is not supported', async ({ runWatchTest }) => { + const testProcess = await runWatchTest({}, { 'only-changed': true }); + await testProcess.exited; + expect(testProcess.output).toContain('--only-changed is not supported in watch mode'); }); -test('should throw nice error message if git doesnt work', async ({ git, runInlineTest }) => { +test('should throw nice error message if git doesnt work', async ({ runInlineTest, git }) => { const result = await runInlineTest({}, { 'only-changed': `this-commit-does-not-exist` }); expect(result.exitCode).toBe(1); diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index 946377a357..0dba9dd861 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -15,6 +15,7 @@ */ import path from 'path'; +import timers from 'timers/promises'; import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; test.describe.configure({ mode: 'parallel' }); @@ -418,6 +419,17 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => { expect(testProcess.output).not.toContain('a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('b.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); + + testProcess.clearOutput(); + await writeFiles({ + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + }); + + await testProcess.waitForOutput('b.test.ts:3:11 › passes'); + expect(testProcess.output).not.toContain('c.test.ts:3:11 › passes'); }); test('should run on changed deps', async ({ runWatchTest, writeFiles }) => { @@ -545,7 +557,7 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF `, }); - await new Promise(f => setTimeout(f, 1000)); + await timers.setTimeout(1000); expect(testProcess.output).not.toContain('Waiting for file changes.'); }); @@ -603,7 +615,7 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => { `, }); - await new Promise(f => setTimeout(f, 1000)); + await timers.setTimeout(1000); expect(testProcess.output).not.toContain('Waiting for file changes.'); }); From 9f466a1ead30cbdabe531346e17c66072cec24f6 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:12:54 -0700 Subject: [PATCH 076/104] feat(chromium-tip-of-tree): roll to r1256 (#32423) 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 6a71bdc01e..46fa8eb648 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1255", + "revision": "1256", "installByDefault": false, - "browserVersion": "130.0.6684.0" + "browserVersion": "130.0.6695.0" }, { "name": "firefox", From 565aed6c393dac3ccba6ca98e7627e34842f4d25 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Sep 2024 10:07:08 -0700 Subject: [PATCH 077/104] Revert "chore: enforce tags format via typescript types (#32384)" (#32431) After API review we decided to revert it: * VSCode extension and UI mode users already get the (runtime) error if the tag is not prefixed * The typescript error message is not very nice * The type change would break those clients that generate tests with tags passed as string This reverts commit 90e7b9ebacbd597b7380522001eb6d17ee9c3d86. --- 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, 2 insertions(+), 18 deletions(-) diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 2cf21f7350..21bd59af5d 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1825,10 +1825,8 @@ type TestDetailsAnnotation = { description?: string; }; -type TestDetailsTag = `@${string}`; - export type TestDetails = { - tag?: TestDetailsTag | TestDetailsTag[]; + tag?: string | string[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } diff --git a/tests/playwright-test/test-tag.spec.ts b/tests/playwright-test/test-tag.spec.ts index 0587cfe7a8..9487e31ea3 100644 --- a/tests/playwright-test/test-tag.spec.ts +++ b/tests/playwright-test/test-tag.spec.ts @@ -147,18 +147,6 @@ 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 a5d6eeb0b9..8494671c76 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -70,10 +70,8 @@ type TestDetailsAnnotation = { description?: string; }; -type TestDetailsTag = `@${string}`; - export type TestDetails = { - tag?: TestDetailsTag | TestDetailsTag[]; + tag?: string | string[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } From b75483bbb49fd27ccb7efa93444fae162f10a413 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Sep 2024 10:18:20 -0700 Subject: [PATCH 078/104] Revert "docs: deprecate: Request.serviceWorker() (#32136)" (#32432) This reverts commit b7ed4d7b9e433f1be414c6e580a0ddc39de71618. --- docs/src/api/class-request.md | 9 +++++++-- packages/playwright-core/types/types.d.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/src/api/class-request.md b/docs/src/api/class-request.md index db878f9995..e13d9de69f 100644 --- a/docs/src/api/class-request.md +++ b/docs/src/api/class-request.md @@ -288,10 +288,15 @@ Returns the matching [Response] object, or `null` if the response was not receiv ## method: Request.serviceWorker * since: v1.24 * langs: js -* deprecated: Requests made by a Service Worker are not reported in Playwright. - returns: <[null]|[Worker]> -This method will always return `null`. +The Service [Worker] that is performing the request. + +**Details** + +This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`. + +Requests originated in a Service Worker do not have a [`method: Request.frame`] available. ## async method: Request.sizes * since: v1.15 diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 37281a1eb4..4735669267 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19237,8 +19237,14 @@ export interface Request { response(): Promise; /** - * This method will always return `null`. - * @deprecated Requests made by a Service Worker are not reported in Playwright. + * The Service {@link Worker} that is performing the request. + * + * **Details** + * + * This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`. + * + * Requests originated in a Service Worker do not have a + * [request.frame()](https://playwright.dev/docs/api/class-request#request-frame) available. */ serviceWorker(): null|Worker; From 446ed72878981d6c88861b3dc20820cd6ffe36a1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 3 Sep 2024 10:18:40 -0700 Subject: [PATCH 079/104] docs: revert typo (#32433) --- docs/src/api/class-page.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index ea5fe74dfa..e1aa908041 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2164,7 +2164,6 @@ 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 From cfae7f755c38ee489979cae22da6b85854f3ec58 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 3 Sep 2024 22:38:02 -0700 Subject: [PATCH 080/104] chore(test runner): always go through internal reporter (#32426) This way we guarantee the API contract and do not miss errors because we forgot to call `onBegin()`. --- .../src/reporters/internalReporter.ts | 5 +- packages/playwright/src/runner/runner.ts | 24 ++-- packages/playwright/src/runner/taskRunner.ts | 23 ++-- packages/playwright/src/runner/tasks.ts | 22 ++-- packages/playwright/src/runner/testServer.ts | 108 +++++++++--------- 5 files changed, 87 insertions(+), 95 deletions(-) diff --git a/packages/playwright/src/reporters/internalReporter.ts b/packages/playwright/src/reporters/internalReporter.ts index 02959ab11b..392a8d9374 100644 --- a/packages/playwright/src/reporters/internalReporter.ts +++ b/packages/playwright/src/reporters/internalReporter.ts @@ -21,6 +21,7 @@ import { Suite } from '../common/test'; import { colors, prepareErrorStack, relativeFilePath } from './base'; import type { ReporterV2 } from './reporterV2'; import { monotonicTime } from 'playwright-core/lib/utils'; +import { Multiplexer } from './multiplexer'; export class InternalReporter { private _reporter: ReporterV2; @@ -29,8 +30,8 @@ export class InternalReporter { private _startTime: Date | undefined; private _monotonicStartTime: number | undefined; - constructor(reporter: ReporterV2) { - this._reporter = reporter; + constructor(reporters: ReporterV2[]) { + this._reporter = new Multiplexer(reporters); } version(): 'v2' { diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index 05cf8a6aac..c37964ce2d 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -27,6 +27,7 @@ import type { FullConfigInternal } from '../common/config'; import type { Suite } from '../common/test'; import { wrapReporterAsV2 } from '../reporters/reporterV2'; import { affectedTestFiles } from '../transform/compilationCache'; +import { InternalReporter } from '../reporters/internalReporter'; type ProjectConfigWithFiles = { name: string; @@ -77,27 +78,28 @@ export class Runner { webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); const reporters = await createReporters(config, listOnly ? 'list' : 'test', false); + const reporter = new InternalReporter(reporters); const taskRunner = listOnly ? createTaskRunnerForList( config, - reporters, + reporter, 'in-process', - { failOnLoadErrors: true }) : createTaskRunner(config, reporters); + { failOnLoadErrors: true }) : createTaskRunner(config, reporter); const testRun = new TestRun(config); - taskRunner.reporter.onConfigure(config.config); + reporter.onConfigure(config.config); const taskStatus = await taskRunner.run(testRun, deadline); let status: FullResult['status'] = testRun.failureTracker.result(); if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; - const modifiedResult = await taskRunner.reporter.onEnd({ status }); + const modifiedResult = await reporter.onEnd({ status }); if (modifiedResult && modifiedResult.status) status = modifiedResult.status; if (!listOnly) await writeLastRunInfo(testRun, status); - await taskRunner.reporter.onExit(); + await reporter.onExit(); // Calling process.exit() might truncate large stdout/stderr output. // See https://github.com/nodejs/node/issues/6456. @@ -110,23 +112,23 @@ export class Runner { async loadAllTests(mode: 'in-process' | 'out-of-process' = 'in-process'): Promise<{ status: FullResult['status'], suite?: Suite, errors: TestError[] }> { const config = this._config; const errors: TestError[] = []; - const reporters = [wrapReporterAsV2({ + const reporter = new InternalReporter([wrapReporterAsV2({ onError(error: TestError) { errors.push(error); } - })]; - const taskRunner = createTaskRunnerForList(config, reporters, mode, { failOnLoadErrors: true }); + })]); + const taskRunner = createTaskRunnerForList(config, reporter, mode, { failOnLoadErrors: true }); const testRun = new TestRun(config); - taskRunner.reporter.onConfigure(config.config); + reporter.onConfigure(config.config); const taskStatus = await taskRunner.run(testRun, 0); let status: FullResult['status'] = testRun.failureTracker.result(); if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; - const modifiedResult = await taskRunner.reporter.onEnd({ status }); + const modifiedResult = await reporter.onEnd({ status }); if (modifiedResult && modifiedResult.status) status = modifiedResult.status; - await taskRunner.reporter.onExit(); + await reporter.onExit(); return { status, suite: testRun.rootSuite, errors }; } diff --git a/packages/playwright/src/runner/taskRunner.ts b/packages/playwright/src/runner/taskRunner.ts index 96505bef2a..52a06aa98c 100644 --- a/packages/playwright/src/runner/taskRunner.ts +++ b/packages/playwright/src/runner/taskRunner.ts @@ -20,26 +20,25 @@ import type { FullResult, TestError } from '../../types/testReporter'; import { SigIntWatcher } from './sigIntWatcher'; import { serializeError } from '../util'; import type { ReporterV2 } from '../reporters/reporterV2'; -import { InternalReporter } from '../reporters/internalReporter'; -import { Multiplexer } from '../reporters/multiplexer'; +import type { InternalReporter } from '../reporters/internalReporter'; type TaskPhase = (reporter: ReporterV2, context: Context, errors: TestError[], softErrors: TestError[]) => Promise | void; export type Task = { setup?: TaskPhase, teardown?: TaskPhase }; export class TaskRunner { private _tasks: { name: string, task: Task }[] = []; - readonly reporter: InternalReporter; + private _reporter: InternalReporter; private _hasErrors = false; private _interrupted = false; private _isTearDown = false; private _globalTimeoutForError: number; - static create(reporters: ReporterV2[], globalTimeoutForError: number = 0) { - return new TaskRunner(createInternalReporter(reporters), globalTimeoutForError); + static create(reporter: InternalReporter, globalTimeoutForError: number = 0) { + return new TaskRunner(reporter, globalTimeoutForError); } private constructor(reporter: InternalReporter, globalTimeoutForError: number) { - this.reporter = reporter; + this._reporter = reporter; this._globalTimeoutForError = globalTimeoutForError; } @@ -56,7 +55,7 @@ export class TaskRunner { async runDeferCleanup(context: Context, deadline: number, cancelPromise = new ManualPromise()): Promise<{ status: FullResult['status'], cleanup: () => Promise }> { const sigintWatcher = new SigIntWatcher(); const timeoutWatcher = new TimeoutWatcher(deadline); - const teardownRunner = new TaskRunner(this.reporter, this._globalTimeoutForError); + const teardownRunner = new TaskRunner(this._reporter, this._globalTimeoutForError); teardownRunner._isTearDown = true; let currentTaskName: string | undefined; @@ -71,13 +70,13 @@ export class TaskRunner { const softErrors: TestError[] = []; try { teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: { setup: task.teardown } }); - await task.setup?.(this.reporter, context, errors, softErrors); + await task.setup?.(this._reporter, context, errors, softErrors); } catch (e) { debug('pw:test:task')(`error in "${name}": `, e); errors.push(serializeError(e)); } finally { for (const error of [...softErrors, ...errors]) - this.reporter.onError?.(error); + this._reporter.onError?.(error); if (errors.length) { if (!this._isTearDown) this._interrupted = true; @@ -105,7 +104,7 @@ export class TaskRunner { if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) { status = 'interrupted'; } else if (timeoutWatcher.timedOut()) { - this.reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) }); + this._reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) }); status = 'timedout'; } else if (this._hasErrors) { status = 'failed'; @@ -146,7 +145,3 @@ class TimeoutWatcher { clearTimeout(this._timer); } } - -function createInternalReporter(reporters: ReporterV2[]): InternalReporter { - return new InternalReporter(new Multiplexer(reporters)); -} diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 09a8a1fdaf..8a74c0337a 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -21,7 +21,6 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { removeFolders } from 'playwright-core/lib/utils'; import { Dispatcher, type EnvByProjectId } from './dispatcher'; import type { TestRunnerPluginRegistration } from '../plugins'; -import type { ReporterV2 } from '../reporters/reporterV2'; import { createTestGroups, type TestGroup } from '../runner/testGroups'; import type { Task } from './taskRunner'; import { TaskRunner } from './taskRunner'; @@ -32,6 +31,7 @@ import { Suite } from '../common/test'; import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils'; import { FailureTracker } from './failureTracker'; import { detectChangedTestFiles } from './vcs'; +import type { InternalReporter } from '../reporters/internalReporter'; const readDirAsync = promisify(fs.readdir); @@ -60,22 +60,22 @@ export class TestRun { } } -export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { - const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); +export function createTaskRunner(config: FullConfigInternal, reporter: InternalReporter): TaskRunner { + const taskRunner = TaskRunner.create(reporter, config.config.globalTimeout); addGlobalSetupTasks(taskRunner, config); taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true })); addRunTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { - const taskRunner = TaskRunner.create(reporters); +export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: InternalReporter): TaskRunner { + const taskRunner = TaskRunner.create(reporter); addGlobalSetupTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { - const taskRunner = TaskRunner.create(reporters); +export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: InternalReporter): TaskRunner { + const taskRunner = TaskRunner.create(reporter); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); addRunTasks(taskRunner, config); return taskRunner; @@ -99,15 +99,15 @@ function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal return taskRunner; } -export function createTaskRunnerForList(config: FullConfigInternal, reporters: ReporterV2[], mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner { - const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); +export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner { + const taskRunner = TaskRunner.create(reporter, config.config.globalTimeout); taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false })); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; } -export function createTaskRunnerForListFiles(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { - const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); +export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: InternalReporter): TaskRunner { + const taskRunner = TaskRunner.create(reporter, config.config.globalTimeout); taskRunner.addTask('load tests', createListFilesTask()); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 1df2d72e8c..4765bf5089 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -39,6 +39,7 @@ import { serializeError } from '../util'; import { cacheDir } from '../transform/compilationCache'; import { baseFullConfig } from '../isomorphic/teleReceiver'; import { InternalReporter } from '../reporters/internalReporter'; +import type { ReporterV2 } from '../reporters/reporterV2'; const originalStdoutWrite = process.stdout.write; const originalStderrWrite = process.stderr.write; @@ -102,15 +103,10 @@ export class TestServerDispatcher implements TestServerInterface { return await createReporterForTestServer(this._serializer, messageSink); } - private async _collectingReporter() { + private async _collectingInternalReporter(...extraReporters: ReporterV2[]) { const report: ReportEntry[] = []; const collectingReporter = await createReporterForTestServer(this._serializer, e => report.push(e)); - return { collectingReporter, report }; - } - - private async _collectingInternalReporter() { - const { collectingReporter, report } = await this._collectingReporter(); - return { reporter: new InternalReporter(collectingReporter), report }; + return { reporter: new InternalReporter([collectingReporter, ...extraReporters]), report }; } async initialize(params: Parameters[0]): ReturnType { @@ -151,24 +147,17 @@ export class TestServerDispatcher implements TestServerInterface { 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. - reporter.onConfigure(baseFullConfig); - reporter.onError(error!); - await reporter.onExit(); + const { reporter, report } = await this._collectingInternalReporter(new ListReporter()); + const config = await this._loadConfigOrReportError(reporter, overrides); + if (!config) return { status: 'failed', report }; - } - const { collectingReporter, report } = await this._collectingReporter(); - const listReporter = new ListReporter(); - const taskRunner = createTaskRunnerForWatchSetup(config, [collectingReporter, listReporter]); - taskRunner.reporter.onConfigure(config.config); + const taskRunner = createTaskRunnerForWatchSetup(config, reporter); + reporter.onConfigure(config.config); const testRun = new TestRun(config); const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); + await reporter.onEnd({ status }); + await reporter.onExit(); if (status !== 'passed') { await globalCleanup(); return { report, status }; @@ -188,11 +177,9 @@ export class TestServerDispatcher implements TestServerInterface { if (this._devServerHandle) return { status: 'failed', report: [] }; const { reporter, report } = await this._collectingInternalReporter(); - const { config, error } = await this._loadConfig(); - if (!config) { - reporter.onError(error!); + const config = await this._loadConfigOrReportError(reporter); + if (!config) return { status: 'failed', report }; - } const devServerCommand = (config.config as any)['@playwright/test']?.['cli']?.['dev-server']; if (!devServerCommand) { reporter.onError({ message: 'No dev-server command found in the configuration' }); @@ -216,7 +203,11 @@ export class TestServerDispatcher implements TestServerInterface { return { status: 'passed', report: [] }; } catch (e) { const { reporter, report } = await this._collectingInternalReporter(); + // Produce dummy config when it has an error. + reporter.onConfigure(baseFullConfig); reporter.onError(serializeError(e)); + await reporter.onEnd({ status: 'failed' }); + await reporter.onExit(); return { status: 'failed', report }; } } @@ -228,21 +219,18 @@ export class TestServerDispatcher implements TestServerInterface { } async listFiles(params: Parameters[0]): ReturnType { - const { config, error } = await this._loadConfig(); - if (!config) { - const { reporter, report } = await this._collectingInternalReporter(); - reporter.onError(error!); + const { reporter, report } = await this._collectingInternalReporter(); + const config = await this._loadConfigOrReportError(reporter); + if (!config) return { status: 'failed', report }; - } - const { collectingReporter, report } = await this._collectingReporter(); config.cliProjectFilter = params.projects?.length ? params.projects : undefined; - const taskRunner = createTaskRunnerForListFiles(config, [collectingReporter]); - taskRunner.reporter.onConfigure(config.config); + const taskRunner = createTaskRunnerForListFiles(config, reporter); + reporter.onConfigure(config.config); const testRun = new TestRun(config); const status = await taskRunner.run(testRun, 0); - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); + await reporter.onEnd({ status }); + await reporter.onExit(); return { report, status }; } @@ -261,12 +249,10 @@ export class TestServerDispatcher implements TestServerInterface { retries: 0, outputDir: params.outputDir, }; - const { config, error } = await this._loadConfig(overrides); - if (!config) { - const { reporter, report } = await this._collectingInternalReporter(); - reporter.onError(error!); + const { reporter, report } = await this._collectingInternalReporter(); + const config = await this._loadConfigOrReportError(reporter, overrides); + if (!config) return { report, status: 'failed' }; - } config.cliArgs = params.locations || []; config.cliGrep = params.grep; @@ -274,13 +260,12 @@ export class TestServerDispatcher implements TestServerInterface { config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliListOnly = true; - const { collectingReporter, report } = await this._collectingReporter(); - const taskRunner = createTaskRunnerForList(config, [collectingReporter], 'out-of-process', { failOnLoadErrors: false }); + const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false }); const testRun = new TestRun(config); - taskRunner.reporter.onConfigure(config.config); + reporter.onConfigure(config.config); const status = await taskRunner.run(testRun, 0); - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); + await reporter.onEnd({ status }); + await reporter.onExit(); this._watchedProjectDirs = new Set(); this._ignoredProjectOutputs = new Set(); @@ -337,12 +322,10 @@ export class TestServerDispatcher implements TestServerInterface { else process.env.PW_LIVE_TRACE_STACKS = undefined; - const { config, error } = await this._loadConfig(overrides); - if (!config) { - const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); - wireReporter.onError(error!); + const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); + const config = await this._loadConfigOrReportError(new InternalReporter([wireReporter]), overrides); + if (!config) return { status: 'failed' }; - } const testIdSet = params.testIds ? new Set(params.testIds) : null; config.cliListOnly = false; @@ -353,16 +336,15 @@ export class TestServerDispatcher implements TestServerInterface { config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.testIdMatcher = testIdSet ? id => testIdSet.has(id) : undefined; - const reporters = await createReporters(config, 'test', true); - const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); - reporters.push(wireReporter); - const taskRunner = createTaskRunnerForTestServer(config, reporters); + const configReporters = await createReporters(config, 'test', true); + const reporter = new InternalReporter([...configReporters, wireReporter]); + const taskRunner = createTaskRunnerForTestServer(config, reporter); const testRun = new TestRun(config); - taskRunner.reporter.onConfigure(config.config); + reporter.onConfigure(config.config); const stop = new ManualPromise(); const run = taskRunner.run(testRun, 0, stop).then(async status => { - await taskRunner.reporter.onEnd({ status }); - await taskRunner.reporter.onExit(); + await reporter.onEnd({ status }); + await reporter.onExit(); this._testRun = undefined; return status; }); @@ -429,6 +411,18 @@ export class TestServerDispatcher implements TestServerInterface { return { config: null, error: serializeError(e) }; } } + + private async _loadConfigOrReportError(reporter: InternalReporter, overrides?: ConfigCLIOverrides): Promise { + const { config, error } = await this._loadConfig(overrides); + if (config) + return config; + // Produce dummy config when it has an error. + reporter.onConfigure(baseFullConfig); + reporter.onError(error!); + await reporter.onEnd({ status: 'failed' }); + await reporter.onExit(); + return null; + } } export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { From d7b9cf21db62b1f1bcfaca2563b232e143f4ab52 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Sep 2024 23:00:59 -0700 Subject: [PATCH 081/104] chore: ignore third-party execution contexts (#32437) * Only track main and utility world contexts * Properly update click metadata --- packages/playwright-core/src/server/chromium/crPage.ts | 7 ++++--- packages/playwright-core/src/server/dom.ts | 4 ++-- packages/playwright-core/src/server/firefox/ffPage.ts | 7 ++++--- packages/playwright-core/src/server/input.ts | 2 +- packages/playwright-core/src/server/webkit/wkPage.ts | 7 ++++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 904ed5a479..05beba882c 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -690,15 +690,16 @@ class FrameSession { if (!frame || this._eventBelongsToStaleFrame(frame._id)) return; const delegate = new CRExecutionContext(this._client, contextPayload); - let worldName: types.World|null = null; + let worldName: types.World; if (contextPayload.auxData && !!contextPayload.auxData.isDefault) worldName = 'main'; else if (contextPayload.name === UTILITY_WORLD_NAME) worldName = 'utility'; + else + return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - if (worldName) - frame._contextCreated(worldName, context); + frame._contextCreated(worldName, context); this._contextIdToContext.set(contextPayload.id, context); } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 175d2a0f4b..75bf2d7a98 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -50,9 +50,9 @@ export function isNonRecoverableDOMError(error: Error) { export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; private _injectedScriptPromise?: Promise; - readonly world: types.World | null; + readonly world: types.World; - constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) { + constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World) { super(frame, delegate, world || 'content-script'); this.frame = frame; this.world = world; diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 6778a777f2..d1066876eb 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -163,15 +163,16 @@ export class FFPage implements PageDelegate { if (!frame) return; const delegate = new FFExecutionContext(this._session, executionContextId); - let worldName: types.World|null = null; + let worldName: types.World; if (auxData.name === UTILITY_WORLD_NAME) worldName = 'utility'; else if (!auxData.name) worldName = 'main'; + else + return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - if (worldName) - frame._contextCreated(worldName, context); + frame._contextCreated(worldName, context); this._contextIdToContext.set(executionContextId, context); } diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index f09f91b86f..4e4c95a8f3 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -215,7 +215,7 @@ export class Mouse { async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}, metadata?: CallMetadata) { if (metadata) - metadata.point = { x: this._x, y: this._y }; + metadata.point = { x, y }; const { delay = null, clickCount = 1 } = options; if (delay) { this.move(x, y, { forClick: true }); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 26ffd2dbab..c3954b4882 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -502,15 +502,16 @@ export class WKPage implements PageDelegate { if (!frame) return; const delegate = new WKExecutionContext(this._session, contextPayload.id); - let worldName: types.World|null = null; + let worldName: types.World; if (contextPayload.type === 'normal') worldName = 'main'; else if (contextPayload.type === 'user' && contextPayload.name === UTILITY_WORLD_NAME) worldName = 'utility'; + else + return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - if (worldName) - frame._contextCreated(worldName, context); + frame._contextCreated(worldName, context); this._contextIdToContext.set(contextPayload.id, context); } From 2fabd15fef077cbe96d34616e33ab07e55d36090 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:49:10 -0700 Subject: [PATCH 082/104] feat(firefox): roll to r1463 (#32439) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 ++-- packages/playwright-core/browsers.json | 4 ++-- .../playwright-core/src/server/deviceDescriptorsSource.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d5a3110613..5d21174879 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-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) +[![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-130.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) @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | :--- | :---: | :---: | :---: | | 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: | +| Firefox 130.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 46fa8eb648..359d4071c6 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,9 +15,9 @@ }, { "name": "firefox", - "revision": "1462", + "revision": "1463", "installByDefault": true, - "browserVersion": "129.0" + "browserVersion": "130.0" }, { "name": "firefox-beta", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index c85866792a..c2bca627fb 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -1592,7 +1592,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", "screen": { "width": 1792, "height": 1120 @@ -1652,7 +1652,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", "screen": { "width": 1920, "height": 1080 From b3d767fa14629793ce9b0ad79e83d52d26cba42b Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 4 Sep 2024 09:57:15 +0200 Subject: [PATCH 083/104] fix(trace viewer): fix memory leak (#32379) In the `visit` method, we currently cache the rendered HTML for every walked node. This re-use works well for traces that consist mostly of references to earlier snapshots. But for traces that don't share much, this is a large memory overhead and leads to the memory crash documented in https://github.com/microsoft/playwright/issues/32336. For the algocracks amongst you, the current memory usage for an html tree $h$ is $\mathcal{O}(|h| * \text{height}(h))$. This PR removes that cache from the nodes and replaces it with a snapshot-level cache, fixing the memory crash. Traces *without* reference should not see a performance impact from this. Traces *with* references will have slower initial rendering, but re-rendering maintains speed because of the snapshot-level cache. Closes https://github.com/microsoft/playwright/issues/32336 --------- Signed-off-by: Simon Knott Co-authored-by: Max Schmitt --- packages/trace-viewer/src/snapshotRenderer.ts | 168 ++++++++++-------- 1 file changed, 98 insertions(+), 70 deletions(-) diff --git a/packages/trace-viewer/src/snapshotRenderer.ts b/packages/trace-viewer/src/snapshotRenderer.ts index 0b458c0cb5..11e63de04f 100644 --- a/packages/trace-viewer/src/snapshotRenderer.ts +++ b/packages/trace-viewer/src/snapshotRenderer.ts @@ -25,6 +25,34 @@ function isSubtreeReferenceSnapshot(n: NodeSnapshot): n is SubtreeReferenceSnaps return Array.isArray(n) && Array.isArray(n[0]); } +let cacheSize = 0; +const cache = new Map(); +const CACHE_SIZE = 300_000_000; // 300mb + +function lruCache(key: SnapshotRenderer, compute: () => string): string { + if (cache.has(key)) { + const value = cache.get(key)!; + // reinserting makes this the least recently used entry + cache.delete(key); + cache.set(key, value); + return value; + } + + + const result = compute(); + + while (cache.size && cacheSize + result.length > CACHE_SIZE) { + const [firstKey, firstValue] = cache.entries().next().value; + cacheSize -= firstValue.length; + cache.delete(firstKey); + } + + cache.set(key, result); + cacheSize += result.length; + + return result; +} + export class SnapshotRenderer { private _snapshots: FrameSnapshot[]; private _index: number; @@ -51,89 +79,89 @@ export class SnapshotRenderer { } render(): RenderedFrameSnapshot { - const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined): string => { + const result: string[] = []; + const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => { // Text node. if (typeof n === 'string') { // Best-effort Electron support: rewrite custom protocol in url() links in stylesheets. // Old snapshotter was sending lower-case. if (parentTag === 'STYLE' || parentTag === 'style') - return rewriteURLsInStyleSheetForCustomProtocol(n); - return escapeHTML(n); + result.push(rewriteURLsInStyleSheetForCustomProtocol(n)); + else + result.push(escapeHTML(n)); + return; } - if (!(n as any)._string) { - if (isSubtreeReferenceSnapshot(n)) { - // Node reference. - const referenceIndex = snapshotIndex - n[0][0]; - if (referenceIndex >= 0 && referenceIndex <= snapshotIndex) { - const nodes = snapshotNodes(this._snapshots[referenceIndex]); - const nodeIndex = n[0][1]; - if (nodeIndex >= 0 && nodeIndex < nodes.length) - (n as any)._string = visit(nodes[nodeIndex], referenceIndex, parentTag, parentAttrs); - } - } else if (isNodeNameAttributesChildNodesSnapshot(n)) { - const [name, nodeAttrs, ...children] = n; - // Element node. - // Note that