<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56926] trunk/tests/visual-regression/specs/visual-snapshots.test.js: Build/Test Tools: Migrate Puppeteer tests to Playwright.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56926">56926</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56926","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>swissspidy</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-10-13 08:11:41 +0000 (Fri, 13 Oct 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Build/Test Tools: Migrate Puppeteer tests to Playwright.

As per the migration plan shared last year, this migrates all browser-based tests in WordPress core to use Playwright.
This includes end-to-end, performance, and visual regression tests.

Props swissspidy, mamaduka, kevin940726, bartkalisz, desrosj, adamsilverstein.
Fixes <a href="https://core.trac.wordpress.org/ticket/59517">#59517</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkgithubworkflowscodingstandardsyml">trunk/.github/workflows/coding-standards.yml</a></li>
<li><a href="#trunkgithubworkflowsendtoendtestsyml">trunk/.github/workflows/end-to-end-tests.yml</a></li>
<li><a href="#trunkgithubworkflowsperformanceyml">trunk/.github/workflows/performance.yml</a></li>
<li><a href="#trunkgithubworkflowsphpunittestsrunyml">trunk/.github/workflows/phpunit-tests-run.yml</a></li>
<li><a href="#trunkgithubworkflowstestcoverageyml">trunk/.github/workflows/test-coverage.yml</a></li>
<li><a href="#trunkgithubworkflowstestnpmyml">trunk/.github/workflows/test-npm.yml</a></li>
<li><a href="#trunkgitignore">trunk/.gitignore</a></li>
<li><a href="#trunkGruntfilejs">trunk/Gruntfile.js</a></li>
<li><a href="#trunkpackagelockjson">trunk/package-lock.json</a></li>
<li><a href="#trunkpackagejson">trunk/package.json</a></li>
<li><a href="#trunktestse2eREADMEmd">trunk/tests/e2e/README.md</a></li>
<li><a href="#trunktestse2especscachecontrolheadersdirectivestestjs">trunk/tests/e2e/specs/cache-control-headers-directives.test.js</a></li>
<li><a href="#trunktestse2especsdashboardtestjs">trunk/tests/e2e/specs/dashboard.test.js</a></li>
<li><a href="#trunktestse2especseditpoststestjs">trunk/tests/e2e/specs/edit-posts.test.js</a></li>
<li><a href="#trunktestse2especsemptytrashrestoretrashedpoststestjs">trunk/tests/e2e/specs/empty-trash-restore-trashed-posts.test.js</a></li>
<li><a href="#trunktestse2especsgutenbergplugintestjs">trunk/tests/e2e/specs/gutenberg-plugin.test.js</a></li>
<li><a href="#trunktestse2especshellotestjs">trunk/tests/e2e/specs/hello.test.js</a></li>
<li><a href="#trunktestse2especsprofileapplicationspasswordstestjs">trunk/tests/e2e/specs/profile/applications-passwords.test.js</a></li>
<li><a href="#trunktestsperformancecompareresultsjs">trunk/tests/performance/compare-results.js</a></li>
<li><a href="#trunktestsperformanceresultsjs">trunk/tests/performance/results.js</a></li>
<li><a href="#trunktestsperformancespecshomeblockthemetestjs">trunk/tests/performance/specs/home-block-theme.test.js</a></li>
<li><a href="#trunktestsperformancespecshomeclassicthemetestjs">trunk/tests/performance/specs/home-classic-theme.test.js</a></li>
<li><a href="#trunktestsperformanceutilsjs">trunk/tests/performance/utils.js</a></li>
<li><a href="#trunktestsvisualregressionREADMEmd">trunk/tests/visual-regression/README.md</a></li>
<li><a href="#trunktestsvisualregressionspecsvisualsnapshotstestjs">trunk/tests/visual-regression/specs/visual-snapshots.test.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestse2econfigglobalsetupjs">trunk/tests/e2e/config/global-setup.js</a></li>
<li><a href="#trunktestse2eplaywrightconfigjs">trunk/tests/e2e/playwright.config.js</a></li>
<li><a href="#trunktestsperformanceconfigglobalsetupjs">trunk/tests/performance/config/global-setup.js</a></li>
<li><a href="#trunktestsperformanceconfigperformancereporterjs">trunk/tests/performance/config/performance-reporter.js</a></li>
<li><a href="#trunktestsperformanceplaywrightconfigjs">trunk/tests/performance/playwright.config.js</a></li>
<li><a href="#trunktestsvisualregressionplaywrightconfigjs">trunk/tests/visual-regression/playwright.config.js</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#trunktestse2econfigbootstrapjs">trunk/tests/e2e/config/bootstrap.js</a></li>
<li><a href="#trunktestse2ejestconfigjs">trunk/tests/e2e/jest.config.js</a></li>
<li><a href="#trunktestse2eruntestsjs">trunk/tests/e2e/run-tests.js</a></li>
<li><a href="#trunktestsperformanceconfigbootstrapjs">trunk/tests/performance/config/bootstrap.js</a></li>
<li><a href="#trunktestsperformancejestconfigjs">trunk/tests/performance/jest.config.js</a></li>
<li><a href="#trunktestsperformanceruntestsjs">trunk/tests/performance/run-tests.js</a></li>
<li>trunk/tests/visual-regression/config/</li>
<li><a href="#trunktestsvisualregressionjestconfigjs">trunk/tests/visual-regression/jest.config.js</a></li>
<li><a href="#trunktestsvisualregressionruntestsjs">trunk/tests/visual-regression/run-tests.js</a></li>
</ul>

<h3>Property Changed</h3>
<ul>
<li><a href="#trunktestsvisualregressionspecs">trunk/tests/visual-regression/specs/</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkgithubworkflowscodingstandardsyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/coding-standards.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/coding-standards.yml      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/coding-standards.yml        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -142,8 +142,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">       contents: read
</span><span class="cx" style="display: block; padding: 0 10px">     timeout-minutes: 20
</span><span class="cx" style="display: block; padding: 0 10px">     if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }}
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    env:
-      PUPPETEER_SKIP_DOWNLOAD: ${{ true }}
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">     steps:
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Checkout repository
</span></span></pre></div>
<a id="trunkgithubworkflowsendtoendtestsyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/end-to-end-tests.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/end-to-end-tests.yml      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/end-to-end-tests.yml        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,11 +42,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">   # - Sets up Node.js.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Logs debug information about the GitHub Action runner.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Installs npm dependencies.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  # - Install Playwright browsers.
</ins><span class="cx" style="display: block; padding: 0 10px">   # - Builds WordPress to run from the `build` directory.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Starts the WordPress Docker container.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Logs the running Docker containers.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Logs Docker debug information (about both the Docker installation within the runner and the WordPress container).
</span><span class="cx" style="display: block; padding: 0 10px">   # - Install WordPress within the Docker container.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  # - Install Gutenberg.
</ins><span class="cx" style="display: block; padding: 0 10px">   # - Run the E2E tests.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Ensures version-controlled files are not modified or deleted.
</span><span class="cx" style="display: block; padding: 0 10px">   e2e-tests:
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -90,6 +92,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Install npm Dependencies
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm ci
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+      - name: Install Playwright browsers
+        run: npx playwright install --with-deps
+
</ins><span class="cx" style="display: block; padding: 0 10px">       - name: Build WordPress
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm run build
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,6 +120,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">           LOCAL_SCRIPT_DEBUG: ${{ matrix.LOCAL_SCRIPT_DEBUG }}
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm run env:install
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+      - name: Install Gutenberg
+        run: npm run env:cli -- plugin install gutenberg --path=/var/www/${{ env.LOCAL_DIR }}
+
</ins><span class="cx" style="display: block; padding: 0 10px">       - name: Run E2E tests
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm run test:e2e
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -129,6 +137,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Ensure version-controlled files are not modified or deleted
</span><span class="cx" style="display: block; padding: 0 10px">         run: git diff --exit-code
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  slack-notifications:
+    name: Slack Notifications
+    uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk
+    permissions:
+      actions: read
+      contents: read
+    needs: [ e2e-tests ]
+    if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }}
+    with:
+      calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }}
+    secrets:
+      SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }}
+      SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }}
+      SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }}
+      SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }}
+
</ins><span class="cx" style="display: block; padding: 0 10px">   failed-workflow:
</span><span class="cx" style="display: block; padding: 0 10px">     name: Failed workflow tasks
</span><span class="cx" style="display: block; padding: 0 10px">     runs-on: ubuntu-latest
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -141,7 +165,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">       github.event_name != 'pull_request' &&
</span><span class="cx" style="display: block; padding: 0 10px">       github.run_attempt < 2 &&
</span><span class="cx" style="display: block; padding: 0 10px">       (
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        needs.e2e-tests.result == 'cancelled' || needs.e2e-tests.result == 'failure'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        contains( needs.*.result, 'cancelled' ) ||
+        contains( needs.*.result, 'failure' )
</ins><span class="cx" style="display: block; padding: 0 10px">       )
</span><span class="cx" style="display: block; padding: 0 10px">     steps:
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Dispatch workflow run
</span></span></pre></div>
<a id="trunkgithubworkflowsperformanceyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/performance.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/performance.yml   2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/performance.yml     2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -31,10 +31,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> env:
</span><span class="cx" style="display: block; padding: 0 10px">   # Performance testing should be performed in an environment reflecting a standard production environment.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  WP_DEBUG: false
-  SCRIPT_DEBUG: false
-  SAVEQUERIES : false
-  WP_DEVELOPMENT_MODE: ''
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  LOCAL_WP_DEBUG: false
+  LOCAL_SCRIPT_DEBUG: false
+  LOCAL_SAVEQUERIES: false
+  LOCAL_WP_DEVELOPMENT_MODE: "''"
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">   # This workflow takes two sets of measurements — one for the current commit,
</span><span class="cx" style="display: block; padding: 0 10px">   # and another against a consistent version that is used as a baseline measurement.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -56,6 +56,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">   # - Set up Node.js.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Log debug information.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Install npm dependencies.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  # - Install Playwright browsers.
</ins><span class="cx" style="display: block; padding: 0 10px">   # - Build WordPress.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Start Docker environment.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Log running Docker containers.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -73,6 +74,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">   # - Run performance tests (previous/target commit).
</span><span class="cx" style="display: block; padding: 0 10px">   # - Print target performance tests results.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Reset to original commit.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  # - Install npm dependencies.
</ins><span class="cx" style="display: block; padding: 0 10px">   # - Set the environment to the baseline version.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Run baseline performance tests.
</span><span class="cx" style="display: block; padding: 0 10px">   # - Print baseline performance tests results.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -119,6 +121,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Install npm dependencies
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm ci
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+      - name: Install Playwright browsers
+        run: npx playwright install --with-deps
+
</ins><span class="cx" style="display: block; padding: 0 10px">       - name: Build WordPress
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm run build
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -182,14 +187,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">         run: npm run build
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Run target performance tests (base/previous commit)
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        run: npm run test:performance -- --prefix=before
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        env:
+          TEST_RESULTS_PREFIX: before
+        run: npm run test:performance
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Print target performance tests results
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        run: node ./tests/performance/results.js --prefix=before
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        env:
+          TEST_RESULTS_PREFIX: before
+        run: node ./tests/performance/results.js
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Reset to original commit
</span><span class="cx" style="display: block; padding: 0 10px">         run: git reset --hard $GITHUB_SHA
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+      - name: Install npm dependencies
+        run: npm ci
+
</ins><span class="cx" style="display: block; padding: 0 10px">       - name: Set the environment to the baseline version
</span><span class="cx" style="display: block; padding: 0 10px">         run: |
</span><span class="cx" style="display: block; padding: 0 10px">           npm run env:cli -- core update --version=${{ env.BASE_TAG }} --force --path=/var/www/${{ env.LOCAL_DIR }}
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -196,10 +208,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">           npm run env:cli -- core version --path=/var/www/${{ env.LOCAL_DIR }}
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Run baseline performance tests
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        run: npm run test:performance -- --prefix=base
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        env:
+          TEST_RESULTS_PREFIX: base
+        run: npm run test:performance
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Print baseline performance tests results
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        run: node ./tests/performance/results.js --prefix=base
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        env:
+          TEST_RESULTS_PREFIX: base
+        run: node ./tests/performance/results.js
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">       - name: Compare results with base
</span><span class="cx" style="display: block; padding: 0 10px">         run: node ./tests/performance/compare-results.js ${{ runner.temp }}/summary.md
</span></span></pre></div>
<a id="trunkgithubworkflowsphpunittestsrunyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/phpunit-tests-run.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/phpunit-tests-run.yml     2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/phpunit-tests-run.yml       2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -51,7 +51,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">   LOCAL_DB_VERSION: ${{ inputs.db-version }}
</span><span class="cx" style="display: block; padding: 0 10px">   LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }}
</span><span class="cx" style="display: block; padding: 0 10px">   PHPUNIT_CONFIG: ${{ inputs.phpunit-config }}
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  PUPPETEER_SKIP_DOWNLOAD: ${{ true }}
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> jobs:
</span><span class="cx" style="display: block; padding: 0 10px">   # Runs the PHPUnit tests for WordPress.
</span></span></pre></div>
<a id="trunkgithubworkflowstestcoverageyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/test-coverage.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/test-coverage.yml 2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/test-coverage.yml   2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -29,7 +29,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> permissions: {}
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> env:
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  PUPPETEER_SKIP_DOWNLOAD: ${{ true }}
</del><span class="cx" style="display: block; padding: 0 10px">   LOCAL_PHP: '7.4-fpm'
</span><span class="cx" style="display: block; padding: 0 10px">   LOCAL_PHP_XDEBUG: true
</span><span class="cx" style="display: block; padding: 0 10px">   LOCAL_PHP_XDEBUG_MODE: 'coverage'
</span></span></pre></div>
<a id="trunkgithubworkflowstestnpmyml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.github/workflows/test-npm.yml</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.github/workflows/test-npm.yml      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.github/workflows/test-npm.yml        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -37,9 +37,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> # Any needed permissions should be configured at the job level.
</span><span class="cx" style="display: block; padding: 0 10px"> permissions: {}
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-env:
-  PUPPETEER_SKIP_DOWNLOAD: ${{ true }}
-
</del><span class="cx" style="display: block; padding: 0 10px"> jobs:
</span><span class="cx" style="display: block; padding: 0 10px">   # Verifies that installing npm dependencies and building WordPress works as expected.
</span><span class="cx" style="display: block; padding: 0 10px">   #
</span></span></pre></div>
<a id="trunkgitignore"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/.gitignore</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/.gitignore  2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/.gitignore    2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -100,4 +100,4 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /docker-compose.override.yml
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> # Visual regression test diffs
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-tests/visual-regression/specs/__image_snapshots__
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+tests/visual-regression/specs/__snapshots__
</ins></span></pre></div>
<a id="trunkGruntfilejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/Gruntfile.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/Gruntfile.js        2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/Gruntfile.js  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -80,7 +80,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // First do `npm install` if package.json has changed.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        installChanged.watchPackage();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+//       installChanged.watchPackage();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // Load tasks.
</span><span class="cx" style="display: block; padding: 0 10px">        require('matchdep').filterDev(['grunt-*', '!grunt-legacy-util']).forEach( grunt.loadNpmTasks );
</span></span></pre></div>
<a id="trunkpackagelockjson"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/package-lock.json</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/package-lock.json   2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/package-lock.json     2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -106,10 +106,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        },
</span><span class="cx" style="display: block; padding: 0 10px">                        "devDependencies": {
</span><span class="cx" style="display: block; padding: 0 10px">                                "@lodder/grunt-postcss": "^3.1.1",
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "@playwright/test": "1.32.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
</span><span class="cx" style="display: block; padding: 0 10px">                                "@wordpress/babel-preset-default": "7.26.6",
</span><span class="cx" style="display: block; padding: 0 10px">                                "@wordpress/dependency-extraction-webpack-plugin": "4.25.6",
</span><span class="cx" style="display: block; padding: 0 10px">                                "@wordpress/e2e-test-utils": "10.13.6",
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "@wordpress/e2e-test-utils-playwright": "0.11.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "@wordpress/scripts": "26.13.6",
</span><span class="cx" style="display: block; padding: 0 10px">                                "autoprefixer": "10.4.16",
</span><span class="cx" style="display: block; padding: 0 10px">                                "chalk": "5.3.0",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3743,6 +3745,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "node": ">=8"
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "node_modules/@playwright/test": {
+                       "version": "1.32.0",
+                       "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz",
+                       "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==",
+                       "dev": true,
+                       "dependencies": {
+                               "@types/node": "*",
+                               "playwright-core": "1.32.0"
+                       },
+                       "bin": {
+                               "playwright": "cli.js"
+                       },
+                       "engines": {
+                               "node": ">=14"
+                       },
+                       "optionalDependencies": {
+                               "fsevents": "2.3.2"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "0.5.5",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7011,14 +7032,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><span class="cx" style="display: block; padding: 0 10px">                "node_modules/@wordpress/e2e-test-utils-playwright": {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        "version": "0.10.6",
-                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.10.6.tgz",
-                       "integrity": "sha512-qUIcQTB4lFG6BUVCPtzs4gDeO/9Pzz1Knq3Uvt1QIYojy9Yr6G6c3f3Mudql+HFfiXoj3B3BxGbA4oLSb7bI6w==",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 "version": "0.11.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.11.0.tgz",
+                       "integrity": "sha512-UxDkVvm24FJdi4nkn5+n9XirYxdJ1QDZgnHotdrgGRel8NOvlEOlhmT/xpuAPQrVwo+yynxEKeb1Y2AT6jX9og==",
</ins><span class="cx" style="display: block; padding: 0 10px">                         "dev": true,
</span><span class="cx" style="display: block; padding: 0 10px">                        "dependencies": {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                "@wordpress/api-fetch": "^6.39.6",
-                               "@wordpress/keycodes": "^3.42.6",
-                               "@wordpress/url": "^3.43.6",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         "@wordpress/api-fetch": "^6.40.0",
+                               "@wordpress/keycodes": "^3.43.0",
+                               "@wordpress/url": "^3.44.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "change-case": "^4.1.2",
</span><span class="cx" style="display: block; padding: 0 10px">                                "form-data": "^4.0.0",
</span><span class="cx" style="display: block; padding: 0 10px">                                "get-port": "^5.1.1",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7032,6 +7053,79 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "@playwright/test": ">=1"
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/api-fetch": {
+                       "version": "6.40.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.40.0.tgz",
+                       "integrity": "sha512-sNk6vZW02ldci1EpNIjmm61323x/0n2Ra/cDHuehZf8avOH/OV0zF0dXxttT8M9Fncz+XZDSIHopm76dU3Phug==",
+                       "dev": true,
+                       "dependencies": {
+                               "@babel/runtime": "^7.16.0",
+                               "@wordpress/i18n": "^4.43.0",
+                               "@wordpress/url": "^3.44.0"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       }
+               },
+               "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/hooks": {
+                       "version": "3.43.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.43.0.tgz",
+                       "integrity": "sha512-SHSiyFUEsggihl0pDvY1l72q+fHMDyFHtIR3GCt0uV2ifctvoa/PIYdVwrxpGQaGdNEV25XCZ4kNldqJmfTddw==",
+                       "dev": true,
+                       "dependencies": {
+                               "@babel/runtime": "^7.16.0"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       }
+               },
+               "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/i18n": {
+                       "version": "4.43.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.43.0.tgz",
+                       "integrity": "sha512-XHU/vGgI+pgjJU9WzWDHke1u948z8i3OPpKUNdxc/gMcTkKaKM4D8DW1+VMSQHyU6pneP8+ph7EF+1RIehP3lQ==",
+                       "dev": true,
+                       "dependencies": {
+                               "@babel/runtime": "^7.16.0",
+                               "@wordpress/hooks": "^3.43.0",
+                               "gettext-parser": "^1.3.1",
+                               "memize": "^2.1.0",
+                               "sprintf-js": "^1.1.1",
+                               "tannin": "^1.2.0"
+                       },
+                       "bin": {
+                               "pot-to-php": "tools/pot-to-php.js"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       }
+               },
+               "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/keycodes": {
+                       "version": "3.43.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.43.0.tgz",
+                       "integrity": "sha512-B6rYPiKFdQTlnJfm93R+usQnjEODUX/K4+hMvY5ZZOinvxe7KyU/xyFGz7gRrS8WmIEYcJowqSmAlGgVs4XwKQ==",
+                       "dev": true,
+                       "dependencies": {
+                               "@babel/runtime": "^7.16.0",
+                               "@wordpress/i18n": "^4.43.0",
+                               "change-case": "^4.1.2"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       }
+               },
+               "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/url": {
+                       "version": "3.44.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.44.0.tgz",
+                       "integrity": "sha512-QNtTPFg/cGHTJLOvOtQCvCgn5quFQgJml8A88I05o4dyUH/tc92rb8LNXi0qcVz/z4JPrx2g3+Ki8heYellP4A==",
+                       "dev": true,
+                       "dependencies": {
+                               "@babel/runtime": "^7.16.0",
+                               "remove-accents": "^0.5.0"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/form-data": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "4.0.0",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7956,6 +8050,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "react-dom": "^18.0.0"
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "node_modules/@wordpress/scripts/node_modules/@wordpress/e2e-test-utils-playwright": {
+                       "version": "0.10.6",
+                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.10.6.tgz",
+                       "integrity": "sha512-qUIcQTB4lFG6BUVCPtzs4gDeO/9Pzz1Knq3Uvt1QIYojy9Yr6G6c3f3Mudql+HFfiXoj3B3BxGbA4oLSb7bI6w==",
+                       "dev": true,
+                       "dependencies": {
+                               "@wordpress/api-fetch": "^6.39.6",
+                               "@wordpress/keycodes": "^3.42.6",
+                               "@wordpress/url": "^3.43.6",
+                               "change-case": "^4.1.2",
+                               "form-data": "^4.0.0",
+                               "get-port": "^5.1.1",
+                               "lighthouse": "^10.4.0",
+                               "mime": "^3.0.0"
+                       },
+                       "engines": {
+                               "node": ">=12"
+                       },
+                       "peerDependencies": {
+                               "@playwright/test": ">=1"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "node_modules/@wordpress/scripts/node_modules/ajv": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "8.12.0",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8099,6 +8215,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "node": ">=8"
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "node_modules/@wordpress/scripts/node_modules/form-data": {
+                       "version": "4.0.0",
+                       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+                       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+                       "dev": true,
+                       "dependencies": {
+                               "asynckit": "^0.4.0",
+                               "combined-stream": "^1.0.8",
+                               "mime-types": "^2.1.12"
+                       },
+                       "engines": {
+                               "node": ">= 6"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "node_modules/@wordpress/scripts/node_modules/glob-parent": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "6.0.2",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8158,6 +8288,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "node": ">=8"
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "node_modules/@wordpress/scripts/node_modules/mime": {
+                       "version": "3.0.0",
+                       "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+                       "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+                       "dev": true,
+                       "bin": {
+                               "mime": "cli.js"
+                       },
+                       "engines": {
+                               "node": ">=10.0.0"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "node_modules/@wordpress/scripts/node_modules/p-locate": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "4.1.0",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -36729,6 +36871,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "@playwright/test": {
+                       "version": "1.32.0",
+                       "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz",
+                       "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==",
+                       "dev": true,
+                       "requires": {
+                               "@types/node": "*",
+                               "fsevents": "2.3.2",
+                               "playwright-core": "1.32.0"
+                       }
+               },
</ins><span class="cx" style="display: block; padding: 0 10px">                 "@pmmmwh/react-refresh-webpack-plugin": {
</span><span class="cx" style="display: block; padding: 0 10px">                        "version": "0.5.5",
</span><span class="cx" style="display: block; padding: 0 10px">                        "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -39263,14 +39416,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><span class="cx" style="display: block; padding: 0 10px">                "@wordpress/e2e-test-utils-playwright": {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        "version": "0.10.6",
-                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.10.6.tgz",
-                       "integrity": "sha512-qUIcQTB4lFG6BUVCPtzs4gDeO/9Pzz1Knq3Uvt1QIYojy9Yr6G6c3f3Mudql+HFfiXoj3B3BxGbA4oLSb7bI6w==",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 "version": "0.11.0",
+                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.11.0.tgz",
+                       "integrity": "sha512-UxDkVvm24FJdi4nkn5+n9XirYxdJ1QDZgnHotdrgGRel8NOvlEOlhmT/xpuAPQrVwo+yynxEKeb1Y2AT6jX9og==",
</ins><span class="cx" style="display: block; padding: 0 10px">                         "dev": true,
</span><span class="cx" style="display: block; padding: 0 10px">                        "requires": {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                "@wordpress/api-fetch": "^6.39.6",
-                               "@wordpress/keycodes": "^3.42.6",
-                               "@wordpress/url": "^3.43.6",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         "@wordpress/api-fetch": "^6.40.0",
+                               "@wordpress/keycodes": "^3.43.0",
+                               "@wordpress/url": "^3.44.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "change-case": "^4.1.2",
</span><span class="cx" style="display: block; padding: 0 10px">                                "form-data": "^4.0.0",
</span><span class="cx" style="display: block; padding: 0 10px">                                "get-port": "^5.1.1",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -39278,6 +39431,61 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "mime": "^3.0.0"
</span><span class="cx" style="display: block; padding: 0 10px">                        },
</span><span class="cx" style="display: block; padding: 0 10px">                        "dependencies": {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "@wordpress/api-fetch": {
+                                       "version": "6.40.0",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.40.0.tgz",
+                                       "integrity": "sha512-sNk6vZW02ldci1EpNIjmm61323x/0n2Ra/cDHuehZf8avOH/OV0zF0dXxttT8M9Fncz+XZDSIHopm76dU3Phug==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@babel/runtime": "^7.16.0",
+                                               "@wordpress/i18n": "^4.43.0",
+                                               "@wordpress/url": "^3.44.0"
+                                       }
+                               },
+                               "@wordpress/hooks": {
+                                       "version": "3.43.0",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.43.0.tgz",
+                                       "integrity": "sha512-SHSiyFUEsggihl0pDvY1l72q+fHMDyFHtIR3GCt0uV2ifctvoa/PIYdVwrxpGQaGdNEV25XCZ4kNldqJmfTddw==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@babel/runtime": "^7.16.0"
+                                       }
+                               },
+                               "@wordpress/i18n": {
+                                       "version": "4.43.0",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.43.0.tgz",
+                                       "integrity": "sha512-XHU/vGgI+pgjJU9WzWDHke1u948z8i3OPpKUNdxc/gMcTkKaKM4D8DW1+VMSQHyU6pneP8+ph7EF+1RIehP3lQ==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@babel/runtime": "^7.16.0",
+                                               "@wordpress/hooks": "^3.43.0",
+                                               "gettext-parser": "^1.3.1",
+                                               "memize": "^2.1.0",
+                                               "sprintf-js": "^1.1.1",
+                                               "tannin": "^1.2.0"
+                                       }
+                               },
+                               "@wordpress/keycodes": {
+                                       "version": "3.43.0",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.43.0.tgz",
+                                       "integrity": "sha512-B6rYPiKFdQTlnJfm93R+usQnjEODUX/K4+hMvY5ZZOinvxe7KyU/xyFGz7gRrS8WmIEYcJowqSmAlGgVs4XwKQ==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@babel/runtime": "^7.16.0",
+                                               "@wordpress/i18n": "^4.43.0",
+                                               "change-case": "^4.1.2"
+                                       }
+                               },
+                               "@wordpress/url": {
+                                       "version": "3.44.0",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.44.0.tgz",
+                                       "integrity": "sha512-QNtTPFg/cGHTJLOvOtQCvCgn5quFQgJml8A88I05o4dyUH/tc92rb8LNXi0qcVz/z4JPrx2g3+Ki8heYellP4A==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@babel/runtime": "^7.16.0",
+                                               "remove-accents": "^0.5.0"
+                                       }
+                               },
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "form-data": {
</span><span class="cx" style="display: block; padding: 0 10px">                                        "version": "4.0.0",
</span><span class="cx" style="display: block; padding: 0 10px">                                        "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -39957,6 +40165,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                "webpack-dev-server": "^4.4.0"
</span><span class="cx" style="display: block; padding: 0 10px">                        },
</span><span class="cx" style="display: block; padding: 0 10px">                        "dependencies": {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "@wordpress/e2e-test-utils-playwright": {
+                                       "version": "0.10.6",
+                                       "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.10.6.tgz",
+                                       "integrity": "sha512-qUIcQTB4lFG6BUVCPtzs4gDeO/9Pzz1Knq3Uvt1QIYojy9Yr6G6c3f3Mudql+HFfiXoj3B3BxGbA4oLSb7bI6w==",
+                                       "dev": true,
+                                       "requires": {
+                                               "@wordpress/api-fetch": "^6.39.6",
+                                               "@wordpress/keycodes": "^3.42.6",
+                                               "@wordpress/url": "^3.43.6",
+                                               "change-case": "^4.1.2",
+                                               "form-data": "^4.0.0",
+                                               "get-port": "^5.1.1",
+                                               "lighthouse": "^10.4.0",
+                                               "mime": "^3.0.0"
+                                       }
+                               },
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "ajv": {
</span><span class="cx" style="display: block; padding: 0 10px">                                        "version": "8.12.0",
</span><span class="cx" style="display: block; padding: 0 10px">                                        "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -40053,6 +40277,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                "path-exists": "^4.0.0"
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "form-data": {
+                                       "version": "4.0.0",
+                                       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+                                       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+                                       "dev": true,
+                                       "requires": {
+                                               "asynckit": "^0.4.0",
+                                               "combined-stream": "^1.0.8",
+                                               "mime-types": "^2.1.12"
+                                       }
+                               },
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "glob-parent": {
</span><span class="cx" style="display: block; padding: 0 10px">                                        "version": "6.0.2",
</span><span class="cx" style="display: block; padding: 0 10px">                                        "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -40097,6 +40332,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                "p-locate": "^4.1.0"
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                "mime": {
+                                       "version": "3.0.0",
+                                       "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+                                       "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+                                       "dev": true
+                               },
</ins><span class="cx" style="display: block; padding: 0 10px">                                 "p-locate": {
</span><span class="cx" style="display: block; padding: 0 10px">                                        "version": "4.1.0",
</span><span class="cx" style="display: block; padding: 0 10px">                                        "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
</span></span></pre></div>
<a id="trunkpackagejson"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/package.json</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/package.json        2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/package.json  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -25,10 +25,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">        ],
</span><span class="cx" style="display: block; padding: 0 10px">        "devDependencies": {
</span><span class="cx" style="display: block; padding: 0 10px">                "@lodder/grunt-postcss": "^3.1.1",
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "@playwright/test": "1.32.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                 "@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
</span><span class="cx" style="display: block; padding: 0 10px">                "@wordpress/babel-preset-default": "7.26.6",
</span><span class="cx" style="display: block; padding: 0 10px">                "@wordpress/dependency-extraction-webpack-plugin": "4.25.6",
</span><span class="cx" style="display: block; padding: 0 10px">                "@wordpress/e2e-test-utils": "10.13.6",
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                "@wordpress/e2e-test-utils-playwright": "0.11.0",
</ins><span class="cx" style="display: block; padding: 0 10px">                 "@wordpress/scripts": "26.13.6",
</span><span class="cx" style="display: block; padding: 0 10px">                "autoprefixer": "10.4.16",
</span><span class="cx" style="display: block; padding: 0 10px">                "chalk": "5.3.0",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -189,10 +191,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                "env:cli": "node ./tools/local-env/scripts/docker.js run cli",
</span><span class="cx" style="display: block; padding: 0 10px">                "env:logs": "node ./tools/local-env/scripts/docker.js logs",
</span><span class="cx" style="display: block; padding: 0 10px">                "env:pull": "node ./tools/local-env/scripts/docker.js pull",
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                "test:performance": "node ./tests/performance/run-tests.js",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         "test:performance": "wp-scripts test-playwright --config tests/performance/playwright.config.js",
</ins><span class="cx" style="display: block; padding: 0 10px">                 "test:php": "node ./tools/local-env/scripts/docker.js run -T php composer update -W && node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit",
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                "test:e2e": "node ./tests/e2e/run-tests.js",
-               "test:visual": "node ./tests/visual-regression/run-tests.js",
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js",
+               "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js",
</ins><span class="cx" style="display: block; padding: 0 10px">                 "sync-gutenberg-packages": "grunt sync-gutenberg-packages",
</span><span class="cx" style="display: block; padding: 0 10px">                "postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build"
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre></div>
<a id="trunktestse2eREADMEmd"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/README.md</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/README.md 2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/README.md   2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5,7 +5,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">   ## Running the tests   -The e2e tests require a production-like environment to run. By default, they will assume an environment is available at `http://localhost:8889`, with username=admin and password=password. +The e2e tests require a production-like environment to run. By default, they will assume an environment is available at `http://localhost:8889`, with username `admin` and password `password`.    If you don't already have an environment ready, you can set one up by following [these instructions](https://github.com/WordPress/wordpress-develop/blob/master/README.md).   @@ -20,16 +20,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> If your environment has a different url, username or password to the default, you can provide the base URL, username and password like this:    ``` -npm run test:e2e -- --wordpress-base-url=http://mycustomurl --wordpress-username=username --wordpress-password=password +WP_BASE_URL=http://mycustomurl WP_USERNAME=username WP_PASSWORD=password npm run test:e2e  ```  **DO NOT run these tests in an actual production environment, as they will delete all your content.**   -For debugging purposes, you might want to follow the test visually. You can do so by running the tests in an interactive mode. +For debugging purposes, you might want to follow the test visually. You can do so by running the tests in an interactive mode:    ``` -npm run test:e2e -- --puppeteer-interactive +npm run test:e2e -- --ui  ```   +[UI Mode](https://playwright.dev/docs/test-ui-mode) let's you explore, run and debug tests with a time travel experience 
 complete with watch mode. +All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. +  You can also run a single test file separately:    ``` @@ -41,6 +44,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">   * Block Editor Handbook end to end testing overview: https://developer.wordpress.org/block-editor/contributors/code/testing-overview/#end-to-end-testing   -* Gutenberg e2e-test-utils package API docs: https://github.com/WordPress/gutenberg/tree/trunk/packages/e2e-test-utils +* Gutenberg e2e-test-utils-playwright package API docs: https://github.com/WordPress/gutenberg/tree/trunk/packages/e2e-test-utils-playwright   -* Puppeteer API docs: https://github.com/puppeteer/puppeteer#readme (the version we are using is indicated in the @wordpress/scripts package: https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/package.json) +* Playwright API docs: https://playwright.dev/docs (the version we are using is indicated in the `@wordpress/scripts` package: https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/package.json)
</span></span></pre></div>
<a id="trunktestse2econfigbootstrapjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/e2e/config/bootstrap.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/config/bootstrap.js       2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/config/bootstrap.js 2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,145 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import { get } from 'lodash';
-import {
-       clearLocalStorage,
-       enablePageDialogAccept,
-       setBrowserViewport,
-} from '@wordpress/e2e-test-utils';
-
-/**
- * Environment variables
- */
-const { PUPPETEER_TIMEOUT } = process.env;
-
-/**
- * Set of console logging types observed to protect against unexpected yet
- * handled (i.e. not catastrophic) errors or warnings. Each key corresponds
- * to the Puppeteer ConsoleMessage type, its value the corresponding function
- * on the console global object.
- *
- * @type {Object<string,string>}
- */
-const OBSERVED_CONSOLE_MESSAGE_TYPES = {
-       warning: 'warn',
-       error: 'error',
-};
-
-/**
- * Array of page event tuples of [ eventName, handler ].
- *
- * @type {Array}
- */
-const pageEvents = [];
-
-// The Jest timeout is increased because these tests are a bit slow
-jest.setTimeout( PUPPETEER_TIMEOUT || 100000 );
-
-
-/**
- * Adds an event listener to the page to handle additions of page event
- * handlers, to assure that they are removed at test teardown.
- */
-function capturePageEventsForTearDown() {
-       page.on( 'newListener', ( eventName, listener ) => {
-               pageEvents.push( [ eventName, listener ] );
-       } );
-}
-
-/**
- * Removes all bound page event handlers.
- */
-function removePageEvents() {
-       pageEvents.forEach( ( [ eventName, handler ] ) => {
-               page.removeListener( eventName, handler );
-       } );
-}
-
-/**
- * Adds a page event handler to emit uncaught exception to process if one of
- * the observed console logging types is encountered.
- */
-function observeConsoleLogging() {
-       page.on( 'console', ( message ) => {
-               const type = message.type();
-               if ( ! OBSERVED_CONSOLE_MESSAGE_TYPES.hasOwnProperty( type ) ) {
-                       return;
-               }
-
-               let text = message.text();
-
-               // An exception is made for _blanket_ deprecation warnings: Those
-               // which log regardless of whether a deprecated feature is in use.
-               if ( text.includes( 'This is a global warning' ) ) {
-                       return;
-               }
-
-               // An exception is made for jQuery migrate console warnings output by
-               // the unminified script loaded in development environments.
-               if ( text.includes( 'JQMIGRATE' ) ) {
-                       return;
-               }
-
-               // Viewing posts on the front end can result in this error, which
-               // has nothing to do with Gutenberg.
-               if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
-                       return;
-               }
-
-               // A bug present in WordPress 5.2 will produce console warnings when
-               // loading the Dashicons font. These can be safely ignored, as they do
-               // not otherwise regress on application behavior. This logic should be
-               // removed once the associated ticket has been closed.
-               //
-               // See: https://core.trac.wordpress.org/ticket/47183
-               if (
-                       text.startsWith( 'Failed to decode downloaded font:' ) ||
-                       text.startsWith( 'OTS parsing error:' )
-               ) {
-                       return;
-               }
-
-               const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ];
-
-               // As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of
-               // type JSHandle for error logging, instead of the expected string.
-               //
-               // See: https://github.com/GoogleChrome/puppeteer/issues/3397
-               //
-               // The recommendation there to asynchronously resolve the error value
-               // upon a console event may be prone to a race condition with the test
-               // completion, leaving a possibility of an error not being surfaced
-               // correctly. Instead, the logic here synchronously inspects the
-               // internal object shape of the JSHandle to find the error text. If it
-               // cannot be found, the default text value is used instead.
-               text = get( message.args(), [ 0, '_remoteObject', 'description' ], text );
-
-               // Disable reason: We intentionally bubble up the console message
-               // which, unless the test explicitly anticipates the logging via
-               // @wordpress/jest-console matchers, will cause the intended test
-               // failure.
-
-               // eslint-disable-next-line no-console
-               console[ logFunction ]( text );
-       } );
-}
-
-// Before every test suite run, delete all content created by the test. This ensures
-// other posts/comments/etc. aren't dirtying tests and tests don't depend on
-// each other's side-effects.
-beforeAll( async () => {
-       capturePageEventsForTearDown();
-       enablePageDialogAccept();
-       observeConsoleLogging();
-       await page.emulateMediaFeatures( [
-               { name: 'prefers-reduced-motion', value: 'reduce' },
-       ] );
-       await setBrowserViewport( 'large' );
-} );
-
-afterEach( async () => {
-       await clearLocalStorage();
-       await setBrowserViewport( 'large' );
-} );
-
-afterAll( () => {
-       removePageEvents();
-} );
</del></span></pre></div>
<a id="trunktestse2econfigglobalsetupjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/e2e/config/global-setup.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/config/global-setup.js                            (rev 0)
+++ trunk/tests/e2e/config/global-setup.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,43 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import { request } from '@playwright/test';
+
+/**
+ * WordPress dependencies
+ */
+import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ *
+ * @param {import('@playwright/test').FullConfig} config
+ * @returns {Promise<void>}
+ */
+async function globalSetup( config ) {
+       const { storageState, baseURL } = config.projects[ 0 ].use;
+       const storageStatePath =
+               typeof storageState === 'string' ? storageState : undefined;
+
+       const requestContext = await request.newContext( {
+               baseURL,
+       } );
+
+       const requestUtils = new RequestUtils( requestContext, {
+               storageStatePath,
+       } );
+
+       // Authenticate and save the storageState to disk.
+       await requestUtils.setupRest();
+
+       // Reset the test environment before running the tests.
+       await Promise.all( [
+               requestUtils.activateTheme( 'twentytwentyone' ),
+               requestUtils.deleteAllPosts(),
+               requestUtils.deleteAllBlocks(),
+               requestUtils.resetPreferences(),
+       ] );
+
+       await requestContext.dispose();
+}
+
+export default globalSetup;
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/e2e/config/global-setup.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestse2ejestconfigjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/e2e/jest.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/jest.config.js    2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/jest.config.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,10 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const config = require( '@wordpress/scripts/config/jest-e2e.config' );
-
-const jestE2EConfig = {
-       ...config,
-       setupFilesAfterEnv: [
-               '<rootDir>/config/bootstrap.js',
-       ],
-};
-
-module.exports = jestE2EConfig;
</del></span></pre></div>
<a id="trunktestse2eplaywrightconfigjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/e2e/playwright.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/playwright.config.js                              (rev 0)
+++ trunk/tests/e2e/playwright.config.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,27 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import path from 'node:path';
+import { defineConfig } from '@playwright/test';
+
+/**
+ * WordPress dependencies
+ */
+const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
+
+process.env.WP_ARTIFACTS_PATH ??= path.join( process.cwd(), 'artifacts' );
+process.env.STORAGE_STATE_PATH ??= path.join(
+       process.env.WP_ARTIFACTS_PATH,
+       'storage-states/admin.json'
+);
+
+const config = defineConfig( {
+       ...baseConfig,
+       globalSetup: require.resolve( './config/global-setup.js' ),
+       webServer: {
+               ...baseConfig.webServer,
+               command: 'npm run env:start',
+       },
+} );
+
+export default config;
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/e2e/playwright.config.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestse2eruntestsjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/e2e/run-tests.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/run-tests.js      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/run-tests.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,13 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const dotenv = require( 'dotenv' );
-const dotenv_expand = require( 'dotenv-expand' );
-const { execSync } = require( 'child_process' );
-
-// WP_BASE_URL interpolates LOCAL_PORT, so needs to be parsed by dotenv_expand().
-dotenv_expand.expand( dotenv.config() );
-
-// Run the tests, passing additional arguments through to the test script.
-execSync(
-       'wp-scripts test-e2e --config tests/e2e/jest.config.js ' +
-               process.argv.slice( 2 ).join( ' ' ),
-       { stdio: 'inherit' }
-);
</del></span></pre></div>
<a id="trunktestse2especscachecontrolheadersdirectivestestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/cache-control-headers-directives.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/cache-control-headers-directives.test.js    2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/cache-control-headers-directives.test.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,38 +1,46 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-       visitAdminPage,
-       createNewPost,
-       publishPost,
-       trashAllPosts,
-       createURL,
-       logout,
-} from "@wordpress/e2e-test-utils";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Cache Control header directives', () => {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Cache Control header directives', () => {
+       test.beforeAll( async ( { requestUtils } ) => {
+               await requestUtils.deleteAllPosts();
+       });
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        beforeEach( async () => {
-               await trashAllPosts();
-       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test(
+               'No private directive present in cache control when user not logged in.',
+               async ( { browser, admin, editor}
+               ) => {
+               await admin.createNewPost( { title: 'Hello World' } );
+               await editor.publishPost();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'No private directive present in cache control when user not logged in.', async () => {
-               await createNewPost( { title: 'Hello World' } );
-               await publishPost();
-               await logout();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.visitAdminPage( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const response = await page.goto( createURL( '/hello-world/' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Create a new incognito browser context to simulate logged-out state.
+               const context = await browser.newContext();
+               const loggedOutPage = await context.newPage();
+
+               const response = await loggedOutPage.goto( '/hello-world/' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 const responseHeaders = response.headers();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Dispose context once it's no longer needed.
+               await context.close();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 expect( responseHeaders ).toEqual( expect.not.objectContaining( { "cache-control": "no-store" } ) );
</span><span class="cx" style="display: block; padding: 0 10px">                expect( responseHeaders ).toEqual( expect.not.objectContaining( { "cache-control": "private" } ) );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Private directive header present in cache control when logged in.', async () => {
-               await visitAdminPage( '/' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test(
+               'Private directive header present in cache control when logged in.',
+               async ( { page, admin }
+               ) => {
+               await admin.visitAdminPage( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const response = await page.goto( createURL( '/wp-admin' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const response = await page.goto( '/wp-admin' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 const responseHeaders = response.headers();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                expect( responseHeaders[ 'cache-control' ] ).toContain( 'no-store' );
</span><span class="cx" style="display: block; padding: 0 10px">                expect( responseHeaders[ 'cache-control' ] ).toContain( 'private' );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestse2especsdashboardtestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/dashboard.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/dashboard.test.js   2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/dashboard.test.js     2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,26 +1,29 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-       pressKeyTimes,
-       trashAllPosts,
-       visitAdminPage,
-} from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Quick Draft', () => {
-       beforeEach( async () => {
-               await trashAllPosts();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Quick Draft', () => {
+       test.beforeEach( async ({ requestUtils }) => {
+               await requestUtils.deleteAllPosts();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Allows draft to be created with Title and Content', async () => {
-               await visitAdminPage( '/' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Allows draft to be created with Title and Content', async ( {
+          admin,
+          page
+       } ) => {
+               await admin.visitAdminPage( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Wait for Quick Draft title field to appear and focus it
-               const draftTitleField = await page.waitForSelector(
-                       '#quick-press #title'
-               );
-               await draftTitleField.focus();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Wait for Quick Draft title field to appear.
+               const draftTitleField = page.locator(
+                       '#quick-press'
+               ).getByRole( 'textbox', { name: 'Title' } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Type in a title.
-               await page.keyboard.type( 'Test Draft Title' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect( draftTitleField ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Focus and fill in a title.
+               await draftTitleField.fill( 'Test Draft Title' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Navigate to content field and type in some content
</span><span class="cx" style="display: block; padding: 0 10px">                await page.keyboard.press( 'Tab' );
</span><span class="cx" style="display: block; padding: 0 10px">                await page.keyboard.type( 'Test Draft Content' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -30,47 +33,42 @@
</span><span class="cx" style="display: block; padding: 0 10px">                await page.keyboard.press( 'Enter' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Check that new draft appears in Your Recent Drafts section
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const newDraft = await page.waitForSelector( '.drafts .draft-title' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.locator( '.drafts .draft-title' ).first().getByRole( 'link' )
+               ).toHaveText( 'Test Draft Title' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                expect(
-                       await newDraft.evaluate( ( element ) => element.innerText )
-               ).toContain( 'Test Draft Title' );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Check that new draft appears in Posts page
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await visitAdminPage( '/edit.php' );
-               const postsListDraft = await page.waitForSelector(
-                       '.type-post.status-draft .title'
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                expect(
-                       await postsListDraft.evaluate( ( element ) => element.innerText )
-               ).toContain( 'Test Draft Title' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.locator( '.type-post.status-draft .title' ).first()
+               ).toContainText( 'Test Draft Title' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Allows draft to be created without Title or Content', async () => {
-               await visitAdminPage( '/' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Allows draft to be created without Title or Content', async ( {
+                admin,
+                page
+       } ) => {
+               await admin.visitAdminPage( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Wait for Save Draft button to appear and click it
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const saveDraftButton = await page.waitForSelector(
-                       '#quick-press #save-post'
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const saveDraftButton = page.locator(
+                       '#quick-press'
+               ).getByRole( 'button', { name: 'Save Draft' } );
+
+               await expect( saveDraftButton ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px">                 await saveDraftButton.click();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Check that new draft appears in Your Recent Drafts section
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const newDraft = await page.waitForSelector( '.drafts .draft-title a' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.locator( '.drafts .draft-title' ).first().getByRole( 'link' )
+               ).toHaveText( 'Untitled' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                expect(
-                       await newDraft.evaluate( ( element ) => element.innerText )
-               ).toContain( '(no title)' );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Check that new draft appears in Posts page
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await visitAdminPage( '/edit.php' );
-               const postsListDraft = await page.waitForSelector(
-                       '.type-post.status-draft .title a'
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                expect(
-                       await postsListDraft.evaluate( ( element ) => element.innerText )
-               ).toContain( '(no title)' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.locator( '.type-post.status-draft .title' ).first()
+               ).toContainText( 'Untitled' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestse2especseditpoststestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/edit-posts.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/edit-posts.test.js  2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/edit-posts.test.js    2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,90 +1,93 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-       createNewPost,
-       pressKeyTimes,
-       publishPost,
-       trashAllPosts,
-       visitAdminPage,
-} from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Edit Posts', () => {
-       beforeEach( async () => {
-               await trashAllPosts();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Edit Posts', () => {
+       test.beforeEach( async ( { requestUtils }) => {
+               await requestUtils.deleteAllPosts();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'displays a message in the posts table when no posts are present', async () => {
-               await visitAdminPage( '/edit.php' );
-               const noPostsMessage = await page.$x(
-                       '//td[text()="No posts found."]'
-               );
-               expect( noPostsMessage.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'displays a message in the posts table when no posts are present',async ( {
+               admin,
+               page,
+       } ) => {
+               await admin.visitAdminPage( '/edit.php' );
+               await expect(
+                       page.getByRole( 'cell', { name: 'No posts found.' } )
+               ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'shows a single post after one is published with the correct title', async () => {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'shows a single post after one is published with the correct title',async ( {
+               admin,
+               editor,
+               page,
+       } ) => {
</ins><span class="cx" style="display: block; padding: 0 10px">                 const title = 'Test Title';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await createNewPost( { title } );
-               await publishPost();
-               await visitAdminPage( '/edit.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.createNewPost( { title } );
+               await editor.publishPost();
+               await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector( '#the-list .type-post' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Expect there to be one row in the post list.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const posts = await page.$$( '#the-list .type-post' );
-               expect( posts.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const posts = listTable.locator( '.row-title' );
+               await expect( posts ).toHaveCount( 1 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const [ firstPost ] = posts;
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Expect the title of the post to be correct.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const postTitle = await firstPost.$x(
-                       `//a[contains(@class, "row-title")][contains(text(), "${ title }")]`
-               );
-               expect( postTitle.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         expect( posts.first() ).toHaveText( title );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'allows an existing post to be edited using the Edit button', async () => {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'allows an existing post to be edited using the Edit button', async ( {
+               admin,
+               editor,
+               page,
+       } ) => {
</ins><span class="cx" style="display: block; padding: 0 10px">                 const title = 'Test Title';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await createNewPost( { title } );
-               await publishPost();
-               await visitAdminPage( '/edit.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.createNewPost( { title } );
+               await editor.publishPost();
+               await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector( '#the-list .type-post' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Click the post title (edit) link
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const [ editLink ] = await page.$x(
-                       `//a[contains(@class, "row-title")][contains(text(), "${ title }")]`
-               );
-               await editLink.click();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await listTable.getByRole( 'link', { name: `“${ title }” (Edit)` } ).click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Wait for the editor iframe to load, and switch to it as the active content frame.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const editorFrame = await page.waitForSelector( 'iframe[name="editor-canvas"]' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await page
+                               .frameLocator( '[name=editor-canvas]' )
+                               .locator( 'body > *' )
+                               .first()
+                               .waitFor();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const innerFrame = await editorFrame.contentFrame();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const editorPostTitle = editor.canvas.getByRole( 'textbox', { name: 'Add title' } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Wait for title field to render onscreen.
-               await innerFrame.waitForSelector( '.editor-post-title__input' );
-
-               // Expect to now be in the editor with the correct post title shown.
-               const editorPostTitleInput = await innerFrame.$x(
-                       `//h1[contains(@class, "editor-post-title__input")][contains(text(), "${ title }")]`
-               );
-               expect( editorPostTitleInput.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Expect title field to be in the editor with correct title shown.
+               await expect( editorPostTitle ).toBeVisible();
+               await expect( editorPostTitle ).toHaveText( title );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'allows an existing post to be quick edited using the Quick Edit button', async () => {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'allows an existing post to be quick edited using the Quick Edit button', async ( {
+               admin,
+               editor,
+               page,
+               pageUtils
+       } ) => {
</ins><span class="cx" style="display: block; padding: 0 10px">                 const title = 'Test Title';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await createNewPost( { title } );
-               await publishPost();
-               await visitAdminPage( '/edit.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.createNewPost( { title } );
+               await editor.publishPost();
+               await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector( '#the-list .type-post' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Focus on the post title link.
-               const [ editLink ] = await page.$x(
-                       `//a[contains(@class, "row-title")][contains(text(), "${ title }")]`
-               );
-               await editLink.focus();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // // Focus on the post title link.
+               await listTable.getByRole( 'link', { name: `“${ title }” (Edit)` } ).focus();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Tab to the Quick Edit button and press Enter to quick edit.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await pressKeyTimes( 'Tab', 2 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await pageUtils.pressKeys( 'Tab', { times: 2 } )
</ins><span class="cx" style="display: block; padding: 0 10px">                 await page.keyboard.press( 'Enter' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Type in the currently focused (title) field to modify the title, testing that focus is moved to the input.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -91,47 +94,42 @@
</span><span class="cx" style="display: block; padding: 0 10px">                await page.keyboard.type( ' Edited' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Update the post.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.click( '.button.save' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await page.getByRole( 'button', { name: 'Update' } ).click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Wait for the quick edit button to reappear.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector( 'button.editinline', { visible: true } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect( page.getByRole( 'button', { name: 'Quick Edit' } ) ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Expect there to be one row in the post list.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const posts = await page.$$( '#the-list tr.type-post' );
-               expect( posts.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const posts = listTable.locator( '.row-title' );
+               await expect( posts ).toHaveCount( 1 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const [ firstPost ] = posts;
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Expect the title of the post to be correct.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const postTitle = await firstPost.$x(
-                       `//a[contains(@class, "row-title")][contains(text(), "${ title } Edited")]`
-               );
-               expect( postTitle.length ).toBe( 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         expect( posts.first() ).toHaveText( `${ title } Edited` );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'allows an existing post to be deleted using the Trash button', async () => {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       test( 'allows an existing post to be deleted using the Trash button', async ( {
+               admin,
+               editor,
+               page,
+               pageUtils
+       } ) => {
</ins><span class="cx" style="display: block; padding: 0 10px">                 const title = 'Test Title';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await createNewPost( { title } );
-               await publishPost();
-               await visitAdminPage( '/edit.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.createNewPost( { title } );
+               await editor.publishPost();
+               await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector( '#the-list .type-post' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Focus on the post title link.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const [ editLink ] = await page.$x(
-                       `//a[contains(@class, "row-title")][contains(text(), "${ title }")]`
-               );
-               await editLink.focus();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await listTable.getByRole( 'link', { name: `“${ title }” (Edit)` } ).focus();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Tab to the Trash button and press Enter to delete the post.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await pressKeyTimes( 'Tab', 3 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await pageUtils.pressKeys( 'Tab', { times: 3 } )
</ins><span class="cx" style="display: block; padding: 0 10px">                 await page.keyboard.press( 'Enter' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const noPostsMessage = await page.waitForSelector(
-                       '#the-list .no-items td'
-               );
-
-               expect(
-                       await noPostsMessage.evaluate( ( element ) => element.innerText )
-               ).toBe( 'No posts found.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.getByRole( 'cell', { name: 'No posts found.' } )
+               ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestse2especsemptytrashrestoretrashedpoststestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/empty-trash-restore-trashed-posts.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/empty-trash-restore-trashed-posts.test.js   2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/empty-trash-restore-trashed-posts.test.js     2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,72 +1,55 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-  visitAdminPage,
-  createNewPost,
-  trashAllPosts,
-  publishPost,
-} from "@wordpress/e2e-test-utils";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const POST_TITLE = "Test Title";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const POST_TITLE = 'Test Title';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe("Empty Trash", () => {
-  async function createPost(title) {
-    // Create a Post
-    await createNewPost({ title });
-    await publishPost();
-  }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Empty Trash', () => {
+       test.beforeEach( async ( { requestUtils } ) => {
+               await requestUtils.deleteAllPosts();
+       });
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  afterEach(async () => {
-    await trashAllPosts();
-  });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test('Empty Trash', async ({ admin, editor, page }) => {
+               await admin.createNewPost( { title: POST_TITLE } );
+               await editor.publishPost();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  it("Empty Trash", async () => {
-    await createPost(POST_TITLE);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    await visitAdminPage("/edit.php");
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    // Move post to trash
-    await page.hover(`[aria-label^="“${POST_TITLE}”"]`);
-    await page.click(`[aria-label='Move “${POST_TITLE}” to the Trash']`);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Move post to trash
+               await listTable.getByRole( 'link', { name: `“${ POST_TITLE }” (Edit)` } ).hover();
+               await listTable.getByRole( 'link', { name: `Move “${POST_TITLE}” to the Trash` } ).click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    // Empty trash
-    const trashTab = await page.waitForXPath('//h2[text()="Filter posts list"]/following-sibling::ul//a[contains(text(), "Trash")]');
-    await Promise.all([
-      trashTab.click(),
-      page.waitForNavigation(),
-    ]);
-    const deleteAllButton = await page.waitForSelector('input[value="Empty Trash"]');
-    await Promise.all([
-      deleteAllButton.click(),
-      page.waitForNavigation(),
-    ]);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Empty trash
+               await page.getByRole( 'link', { name: 'Trash' } ).click();
+               await page.getByRole( 'button', { name: 'Empty Trash' } ).first().click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    const messageElement = await page.waitForSelector("#message");
-    const message = await messageElement.evaluate((node) => node.innerText);
-    // Until we have `deleteAllPosts`, the number of posts being deleted could be dynamic.
-    expect(message).toMatch(/\d+ posts? permanently deleted\./);
-  });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect( page.locator( '#message' ) ).toContainText( '1 post permanently deleted.' );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-  it("Restore trash post", async () => {
-    await createPost(POST_TITLE);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test('Restore trash post', async ( { admin, editor, page }) => {
+               await admin.createNewPost( { title: POST_TITLE } );
+               await editor.publishPost();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    await visitAdminPage("/edit.php");
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await admin.visitAdminPage( '/edit.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    // Move one post to trash.
-    await page.hover(`[aria-label^="“${POST_TITLE}”"]`);
-    await page.click(`[aria-label='Move “${POST_TITLE}” to the Trash']`);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
+               await expect( listTable ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    // Remove post from trash.
-    const trashTab = await page.waitForXPath('//h2[text()="Filter posts list"]/following-sibling::ul//a[contains(text(), "Trash")]');
-    await Promise.all([
-      trashTab.click(),
-      page.waitForNavigation(),
-    ]);
-    const [postTitle] = await page.$x(`//*[text()="${POST_TITLE}"]`);
-    await postTitle.hover();
-    await page.click(`[aria-label="Restore “${POST_TITLE}” from the Trash"]`);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Move post to trash
+               await listTable.getByRole( 'link', { name: `“${ POST_TITLE }” (Edit)` } ).hover();
+               await listTable.getByRole( 'link', { name: `Move “${POST_TITLE}” to the Trash` } ).click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-    // Expect for success message for trashed post.
-    const messageElement = await page.waitForSelector("#message");
-    const message = await messageElement.evaluate((element) => element.innerText);
-    expect(message).toContain("1 post restored from the Trash.");
-  });
-});
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await page.getByRole( 'link', { name: 'Trash' } ).click();
+
+               // Remove post from trash.
+               await listTable.getByRole( 'cell' ).filter( { hasText: POST_TITLE } ).hover();
+               await listTable.getByRole( 'link', { name: `Restore “${POST_TITLE}” from the Trash` } ).click();
+
+               // Expect for success message for restored post.
+               await expect( page.locator( '#message' ) ).toContainText( '1 post restored from the Trash.' );
+       } );
+} );
</ins></span></pre></div>
<a id="trunktestse2especsgutenbergplugintestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/gutenberg-plugin.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/gutenberg-plugin.test.js    2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/gutenberg-plugin.test.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,26 +1,48 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-       activatePlugin,
-       deactivatePlugin,
-       installPlugin,
-       uninstallPlugin,
-} from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Gutenberg plugin', () => {
-       beforeAll( async () => {
-               await installPlugin( 'gutenberg' );
-       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Gutenberg plugin', () => {
+       // Increasing timeout to 5 minutes because potential plugin install could take longer.
+       test.setTimeout( 300_000 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        afterAll( async () => {
-               await uninstallPlugin( 'gutenberg' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.beforeAll( async ( { requestUtils } ) => {
+               // Install Gutenberg plugin if it's not yet installed.
+               const pluginsMap = await requestUtils.getPluginsMap();
+               if ( ! pluginsMap.gutenberg ) {
+                       await requestUtils.rest( {
+                               method: 'POST',
+                               path: 'wp/v2/plugins?slug=gutenberg',
+                       } );
+               }
+
+               // Refetch installed plugin details. It avoids stale values when the test installs the plugin.
+               await requestUtils.getPluginsMap( /* forceRefetch */ true );
+               await requestUtils.deactivatePlugin( 'gutenberg' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'should activate', async () => {
-               await activatePlugin( 'gutenberg' );
-               /*
-                * If plugin activation fails, it will time out and throw an error,
-                * since the activatePlugin helper is looking for a `.deactivate` link
-                * which is only there if activation succeeds.
-                */
-               await deactivatePlugin( 'gutenberg' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'should activate', async ( { requestUtils }) => {
+               let plugin = await requestUtils.rest( {
+                       path: 'wp/v2/plugins/gutenberg/gutenberg',
+               } );
+
+               expect( plugin.status ).toBe( 'inactive' );
+
+               await requestUtils.activatePlugin( 'gutenberg' );
+
+               plugin = await requestUtils.rest( {
+                       path: 'wp/v2/plugins/gutenberg/gutenberg',
+               } );
+
+               expect( plugin.status ).toBe( 'active' );
+
+               await requestUtils.deactivatePlugin( 'gutenberg' );
+
+               plugin = await requestUtils.rest( {
+                       path: 'wp/v2/plugins/gutenberg/gutenberg',
+               } );
+
+               expect( plugin.status ).toBe( 'inactive' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestse2especshellotestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/hello.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/hello.test.js       2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/hello.test.js 2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,11 +1,13 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import { visitAdminPage } from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Hello World', () => {
-       it( 'Should load properly', async () => {
-               await visitAdminPage( '/' );
-               const nodes = await page.$x(
-                       '//h2[contains(text(), "Welcome to WordPress!")]'
-               );
-               expect( nodes.length ).not.toEqual( 0 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Hello World', () => {
+       test( 'Should load properly', async ( { admin, page }) => {
+               await admin.visitAdminPage( '/' );
+               await expect(
+                       page.getByRole('heading', { name: 'Welcome to WordPress', level: 2 })
+               ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestse2especsprofileapplicationspasswordstestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/e2e/specs/profile/applications-passwords.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/e2e/specs/profile/applications-passwords.test.js      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/e2e/specs/profile/applications-passwords.test.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,138 +1,133 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import {
-       visitAdminPage,
-       __experimentalRest as rest,
-} from "@wordpress/e2e-test-utils";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * WordPress dependencies
+ */
+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-async function getResponseForApplicationPassword() {
-       return await rest({
-               method: "GET",
-               path: "/wp/v2/users/me/application-passwords",
-       });
-}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const TEST_APPLICATION_NAME = 'Test Application';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-async function createApplicationPassword(applicationName) {
-       await visitAdminPage("profile.php");
-       await page.waitForSelector("#new_application_password_name");
-       await page.type("#new_application_password_name", applicationName);
-       await page.click("#do_new_application_password");
-
-       await page.waitForSelector("#application-passwords-section .notice");
-}
-
-async function createApplicationPasswordWithApi(applicationName) {
-       await rest({
-               method: "POST",
-               path: "/wp/v2/users/me/application-passwords",
-               data: {
-                       name: applicationName,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Manage applications passwords', () => {
+       test.use( {
+               applicationPasswords: async ( { requestUtils, admin, page }, use ) => {
+                       await use( new ApplicationPasswords( { requestUtils, admin, page } ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 },
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        });
-}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-async function revokeAllApplicationPasswordsWithApi() {
-       await rest({
-               method: "DELETE",
-               path: `/wp/v2/users/me/application-passwords`,
-       });
-}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.beforeEach(async ( { applicationPasswords } ) => {
+               await applicationPasswords.delete();
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe("Manage applications passwords", () => {
-       const TEST_APPLICATION_NAME = "Test Application";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test('should correctly create a new application password', async ( {
+               page,
+               applicationPasswords
+       } ) => {
+               await applicationPasswords.create();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        beforeEach(async () => {
-               await revokeAllApplicationPasswordsWithApi();
-       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const [ app ] = await applicationPasswords.get();
+               expect( app['name']).toBe( TEST_APPLICATION_NAME );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it("should correctly create a new application password", async () => {
-               await createApplicationPassword(TEST_APPLICATION_NAME);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const successMessage = page.getByRole( 'alert' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const response = await getResponseForApplicationPassword();
-               expect(response[0]["name"]).toBe(TEST_APPLICATION_NAME);
-
-               const successMessage = await page.waitForSelector(
-                       "#application-passwords-section .notice-success"
-               );
-               expect(
-                       await successMessage.evaluate((element) => element.innerText)
-               ).toContain(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect( successMessage ).toHaveClass( /notice-success/ );
+               await expect(
+                       successMessage
+               ).toContainText(
</ins><span class="cx" style="display: block; padding: 0 10px">                         `Your new password for ${TEST_APPLICATION_NAME} is: \n\nBe sure to save this in a safe location. You will not be able to retrieve it.`
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it("should not allow to create two applications passwords with the same name", async () => {
-               await createApplicationPassword(TEST_APPLICATION_NAME);
-               await createApplicationPassword(TEST_APPLICATION_NAME);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test('should not allow to create two applications passwords with the same name', async ( {
+               page,
+               applicationPasswords
+       } ) => {
+               await applicationPasswords.create();
+               await applicationPasswords.create();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const errorMessage = await page.waitForSelector(
-                       "#application-passwords-section .notice-error"
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const errorMessage = page.getByRole( 'alert' );
+
+               await expect( errorMessage ).toHaveClass( /notice-error/ );
+               await expect(
+                       errorMessage
+               ).toContainText(
+                       'Each application name should be unique.'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               expect(
-                       await errorMessage.evaluate((element) => element.textContent)
-               ).toContain("Each application name should be unique.");
</del><span class="cx" style="display: block; padding: 0 10px">         });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it("should correctly revoke a single application password", async () => {
-               await createApplicationPassword(TEST_APPLICATION_NAME);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'should correctly revoke a single application password', async ( {
+               page,
+               applicationPasswords
+       } ) => {
+               await applicationPasswords.create();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const revokeApplicationButton = await page.waitForSelector(
-                       ".application-passwords-user tr button.delete"
-               );
-               
-               const revocationDialogPromise = new Promise((resolve) => {
-                       page.once("dialog", resolve);
-               });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const revokeButton = page.getByRole( 'button', { name: `Revoke "${ TEST_APPLICATION_NAME }"` } );
+               await expect( revokeButton ).toBeVisible();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await Promise.all([
-                       revocationDialogPromise,
-                       revokeApplicationButton.click(),
-               ]);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Revoke password.
+               page.on( 'dialog', ( dialog ) => dialog.accept() );
+               await revokeButton.click();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const successMessage = await page.waitForSelector(
-                       "#application-passwords-section .notice-success"
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await expect(
+                       page.getByRole( 'alert' )
+               ).toContainText(
+                       'Application password revoked.'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                expect(
-                       await successMessage.evaluate((element) => element.textContent)
-               ).toContain("Application password revoked.");
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const response = await getResponseForApplicationPassword();
-               expect(response).toEqual([]);
-       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const response = await applicationPasswords.get();
+               expect( response ).toEqual([]);
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it("should correctly revoke all the application passwords", async () => {
-               await createApplicationPassword(TEST_APPLICATION_NAME);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'should correctly revoke all the application passwords', async ( {
+               page,
+               applicationPasswords
+       } ) => {
+               await applicationPasswords.create();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const revokeAllApplicationPasswordsButton = await page.waitForSelector(
-                       "#revoke-all-application-passwords"
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const revokeAllButton = page.getByRole( 'button', { name: 'Revoke all application passwords' } );
+               await expect( revokeAllButton ).toBeVisible();
+
+               // Confirms revoking action.
+               page.on( 'dialog', ( dialog ) => dialog.accept() );
+               await revokeAllButton.click();
+
+               await expect(
+                       page.getByRole( 'alert' )
+               ).toContainText(
+                       'All application passwords revoked.'
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const revocationDialogPromise = new Promise((resolve) => {
-                       page.once("dialog", resolve);
-               });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const response = await applicationPasswords.get();
+               expect( response ).toEqual([]);
+       } );
+} );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await Promise.all([
-                       revocationDialogPromise,
-                       revokeAllApplicationPasswordsButton.click(),
-               ]);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+class ApplicationPasswords {
+       constructor( { requestUtils, page, admin }) {
+               this.requestUtils = requestUtils;
+               this.page = page;
+               this.admin = admin;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /**
-                * This is commented out because we're using enablePageDialogAccept
-                * which is overly aggressive and no way to temporary disable it either.
-                */
-               // await dialog.accept();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ async create(applicationName = TEST_APPLICATION_NAME) {
+               await this.admin.visitAdminPage( '/profile.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                await page.waitForSelector(
-                       "#application-passwords-section .notice-success"
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         const newPasswordField = this.page.getByRole( 'textbox', { name: 'New Application Password Name' } );
+               await expect( newPasswordField ).toBeVisible();
+               await newPasswordField.fill( applicationName );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const successMessage = await page.waitForSelector(
-                       "#application-passwords-section .notice-success"
-               );
-               expect(
-                       await successMessage.evaluate((element) => element.textContent)
-               ).toContain("All application passwords revoked.");
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         await this.page.getByRole( 'button', { name: 'Add New Application Password' } ).click();
+               await expect( this.page.getByRole( 'alert' ) ).toBeVisible();
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                const response = await getResponseForApplicationPassword();
-               expect(response).toEqual([]);
-       });
-});
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ async get() {
+               return this.requestUtils.rest( {
+                       method: 'GET',
+                       path: '/wp/v2/users/me/application-passwords',
+               } );
+       }
+
+       async delete() {
+               await this.requestUtils.rest( {
+                       method: 'DELETE',
+                       path: '/wp/v2/users/me/application-passwords',
+               } );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsperformancecompareresultsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/performance/compare-results.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/compare-results.js        2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/compare-results.js  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3,8 +3,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * External dependencies.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const fs = require( 'fs' );
-const path = require( 'path' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const fs = require( 'node:fs' );
+const path = require( 'node:path' );
+
+/**
+ * Internal dependencies
+ */
</ins><span class="cx" style="display: block; padding: 0 10px"> const { median } = require( './utils' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -23,18 +27,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // The current commit's results.
</span><span class="cx" style="display: block; padding: 0 10px"> const testResults = Object.fromEntries(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        testSuites.map( ( key ) => [
-               key,
-               parseFile( `${ key }.test.results.json` ),
-       ] )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ testSuites
+               .filter( ( key ) => fs.existsSync( `${ key }.test.results.json` ) )
+               .map( ( key ) => [ key, parseFile( `${ key }.test.results.json` ) ] )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // The previous commit's results.
</span><span class="cx" style="display: block; padding: 0 10px"> const prevResults = Object.fromEntries(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        testSuites.map( ( key ) => [
-               key,
-               parseFile( `before-${ key }.test.results.json` ),
-       ] )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ testSuites
+               .filter( ( key ) => fs.existsSync( `before-${ key }.test.results.json` ) )
+               .map( ( key ) => [ key, parseFile( `before-${ key }.test.results.json` ) ] )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> const args = process.argv.slice( 2 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -127,8 +129,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> console.log( 'Note: Due to the nature of how GitHub Actions work, some variance in the results is expected.\n' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> for ( const key of testSuites ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        const current = testResults[ key ];
-       const prev = prevResults[ key ];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ const current = testResults[ key ] || {};
+       const prev = prevResults[ key ] || {};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        const title = ( key.charAt( 0 ).toUpperCase() + key.slice( 1 ) ).replace(
</span><span class="cx" style="display: block; padding: 0 10px">                /-+/g,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -152,14 +154,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        summaryMarkdown += `## ${ title }\n\n`;
-       summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( rows.length > 0 ) {
+               summaryMarkdown += `## ${ title }\n\n`;
+               summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        console.log( title );
-       console.table( rows );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         console.log( title );
+               console.table( rows );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-fs.writeFileSync(
-       summaryFile,
-       summaryMarkdown
-);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+if ( summaryFile ) {
+       fs.writeFileSync(
+               summaryFile,
+               summaryMarkdown
+       );
+}
</ins></span></pre></div>
<a id="trunktestsperformanceconfigbootstrapjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/performance/config/bootstrap.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/config/bootstrap.js       2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/config/bootstrap.js 2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,41 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/**
- * WordPress dependencies.
- */
-import {
-       clearLocalStorage,
-       enablePageDialogAccept,
-       setBrowserViewport,
-} from '@wordpress/e2e-test-utils';
-
-/**
- * Timeout, in seconds, that the test should be allowed to run.
- *
- * @type {string|undefined}
- */
-const PUPPETEER_TIMEOUT = process.env.PUPPETEER_TIMEOUT;
-
-// The Jest timeout is increased because these tests are a bit slow.
-jest.setTimeout( PUPPETEER_TIMEOUT || 100000 );
-
-async function setupBrowser() {
-       await clearLocalStorage();
-       await setBrowserViewport( 'large' );
-}
-
-/*
- * Before every test suite run, delete all content created by the test. This ensures
- * other posts/comments/etc. aren't dirtying tests and tests don't depend on
- * each other's side-effects.
- */
-beforeAll( async () => {
-       enablePageDialogAccept();
-
-       await setBrowserViewport( 'large' );
-       await page.emulateMediaFeatures( [
-               { name: 'prefers-reduced-motion', value: 'reduce' },
-       ] );
-} );
-
-afterEach( async () => {
-       await setupBrowser();
-} );
</del></span></pre></div>
<a id="trunktestsperformanceconfigglobalsetupjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/performance/config/global-setup.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/config/global-setup.js                            (rev 0)
+++ trunk/tests/performance/config/global-setup.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,40 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import { request } from '@playwright/test';
+
+/**
+ * WordPress dependencies
+ */
+import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ *
+ * @param {import('@playwright/test').FullConfig} config
+ * @returns {Promise<void>}
+ */
+async function globalSetup( config ) {
+       const { storageState, baseURL } = config.projects[ 0 ].use;
+       const storageStatePath =
+               typeof storageState === 'string' ? storageState : undefined;
+
+       const requestContext = await request.newContext( {
+               baseURL,
+       } );
+
+       const requestUtils = new RequestUtils( requestContext, {
+               storageStatePath,
+       } );
+
+       // Authenticate and save the storageState to disk.
+       await requestUtils.setupRest();
+
+       // Reset the test environment before running the tests.
+       await Promise.all( [
+               requestUtils.activateTheme( 'twentytwentyone' ),
+       ] );
+
+       await requestContext.dispose();
+}
+
+export default globalSetup;
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/performance/config/global-setup.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsperformanceconfigperformancereporterjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/performance/config/performance-reporter.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/config/performance-reporter.js                            (rev 0)
+++ trunk/tests/performance/config/performance-reporter.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,38 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import { join, dirname, basename } from 'node:path';
+import { writeFileSync } from 'node:fs';
+
+/**
+ * Internal dependencies
+ */
+import { getResultsFilename } from '../utils';
+
+/**
+ * @implements {import('@playwright/test/reporter').Reporter}
+ */
+class PerformanceReporter {
+       /**
+        *
+        * @param {import('@playwright/test/reporter').TestCase} test
+        * @param {import('@playwright/test/reporter').TestResult} result
+        */
+       onTestEnd( test, result ) {
+               const performanceResults = result.attachments.find(
+                       ( attachment ) => attachment.name === 'results'
+               );
+
+               if ( performanceResults?.body ) {
+                       writeFileSync(
+                               join(
+                                       dirname( test.location.file ),
+                                       getResultsFilename( basename( test.location.file, '.js' ) )
+                               ),
+                               performanceResults.body.toString( 'utf-8' )
+                       );
+               }
+       }
+}
+
+export default PerformanceReporter;
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/performance/config/performance-reporter.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsperformancejestconfigjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/performance/jest.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/jest.config.js    2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/jest.config.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,14 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const config = require( '@wordpress/scripts/config/jest-e2e.config' );
-
-const jestE2EConfig = {
-       ...config,
-       setupFilesAfterEnv: [
-               '<rootDir>/config/bootstrap.js',
-       ],
-       globals: {
-               // Number of requests to run per test.
-               TEST_RUNS: 20,
-       }
-};
-
-module.exports = jestE2EConfig;
</del></span></pre></div>
<a id="trunktestsperformanceplaywrightconfigjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/performance/playwright.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/playwright.config.js                              (rev 0)
+++ trunk/tests/performance/playwright.config.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,42 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import path from 'node:path';
+import { defineConfig } from '@playwright/test';
+
+/**
+ * WordPress dependencies
+ */
+import baseConfig from '@wordpress/scripts/config/playwright.config';
+
+process.env.WP_ARTIFACTS_PATH ??= path.join( process.cwd(), 'artifacts' );
+process.env.STORAGE_STATE_PATH ??= path.join(
+       process.env.WP_ARTIFACTS_PATH,
+       'storage-states/admin.json'
+);
+process.env.TEST_RUNS ??= '20';
+
+const config = defineConfig( {
+       ...baseConfig,
+       globalSetup: require.resolve( './config/global-setup.js' ),
+       reporter: process.env.CI
+               ? './config/performance-reporter.js'
+               : [ [ 'list' ], [ './config/performance-reporter.js' ] ],
+       forbidOnly: !! process.env.CI,
+       workers: 1,
+       retries: 0,
+       timeout: parseInt( process.env.TIMEOUT || '', 10 ) || 600_000, // Defaults to 10 minutes.
+       // Don't report slow test "files", as we will be running our tests in serial.
+       reportSlowTests: null,
+       webServer: {
+               ...baseConfig.webServer,
+               command: 'npm run env:start',
+       },
+       use: {
+               ...baseConfig.use,
+               video: 'off',
+       },
+} );
+
+export default config;
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/performance/playwright.config.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsperformanceresultsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/performance/results.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/results.js        2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/results.js  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3,8 +3,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * External dependencies.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const fs = require( 'fs' );
-const { join } = require( 'path' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const fs = require( 'node:fs' );
+const { join } = require( 'node:path' );
</ins><span class="cx" style="display: block; padding: 0 10px"> const { median, getResultsFilename } = require( './utils' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> const testSuites = [
</span></span></pre></div>
<a id="trunktestsperformanceruntestsjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/performance/run-tests.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/run-tests.js      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/run-tests.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,16 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/**
- * External dependencies.
- */
-const dotenv = require( 'dotenv' );
-const dotenv_expand = require( 'dotenv-expand' );
-const { execSync } = require( 'child_process' );
-
-// WP_BASE_URL interpolates LOCAL_PORT, so needs to be parsed by dotenv_expand().
-dotenv_expand.expand( dotenv.config() );
-
-// Run the tests, passing additional arguments through to the test script.
-execSync(
-       'wp-scripts test-e2e --config tests/performance/jest.config.js ' +
-               process.argv.slice( 2 ).join( ' ' ),
-       { stdio: 'inherit' }
-);
</del></span></pre></div>
<a id="trunktestsperformancespecshomeblockthemetestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/performance/specs/home-block-theme.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/specs/home-block-theme.test.js    2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/specs/home-block-theme.test.js      2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,67 +1,57 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * External dependencies.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * WordPress dependencies
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const { basename, join } = require( 'path' );
-const { writeFileSync } = require( 'fs' );
-const {
-       getResultsFilename,
-       getTimeToFirstByte,
-       getLargestContentfulPaint,
-} = require( './../utils' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+import { test } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * WordPress dependencies.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Internal dependencies
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import { activateTheme, createURL } from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+import { camelCaseDashes } from '../utils';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Server Timing - Twenty Twenty Three', () => {
-       const results = {
-               wpBeforeTemplate: [],
-               wpTemplate: [],
-               wpTotal: [],
-               timeToFirstByte: [],
-               largestContentfulPaint: [],
-               lcpMinusTtfb: [],
-       };
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const results = {
+       timeToFirstByte: [],
+       largestContentfulPaint: [],
+       lcpMinusTtfb: [],
+};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        beforeAll( async () => {
-               await activateTheme( 'twentytwentythree' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Front End - Twenty Twenty Three', () => {
+       test.use( {
+               storageState: {}, // User will be logged out.
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        afterAll( async () => {
-               const resultsFilename = getResultsFilename(
-                       basename( __filename, '.js' )
-               );
-               writeFileSync(
-                       join( __dirname, resultsFilename ),
-                       JSON.stringify( results, null, 2 )
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.beforeAll( async ( { requestUtils } ) => {
+               await requestUtils.activateTheme( 'twentytwentythree' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Server Timing Metrics', async () => {
-               let i = TEST_RUNS;
-               while ( i-- ) {
-                       await page.goto( createURL( '/' ) );
-                       const navigationTimingJson = await page.evaluate( () =>
-                               JSON.stringify( performance.getEntriesByType( 'navigation' ) )
-                       );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.afterAll( async ( { requestUtils }, testInfo ) => {
+               await testInfo.attach( 'results', {
+                       body: JSON.stringify( results, null, 2 ),
+                       contentType: 'application/json',
+               } );
+               await requestUtils.activateTheme( 'twentytwentyone' );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        const [ navigationTiming ] = JSON.parse( navigationTimingJson );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ const iterations = Number( process.env.TEST_RUNS );
+       for ( let i = 1; i <= iterations; i++ ) {
+               test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
+                       page,
+                       metrics,
+               } ) => {
+                       await page.goto( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        results.wpBeforeTemplate.push(
-                               navigationTiming.serverTiming[ 0 ].duration
-                       );
-                       results.wpTemplate.push(
-                               navigationTiming.serverTiming[ 1 ].duration
-                       );
-                       results.wpTotal.push( navigationTiming.serverTiming[ 2 ].duration );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 const serverTiming = await metrics.getServerTiming();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        const ttfb = await getTimeToFirstByte();
-                       const lcp = await getLargestContentfulPaint();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 for ( const [key, value] of Object.entries( serverTiming ) ) {
+                               results[ camelCaseDashes( key ) ] ??= [];
+                               results[ camelCaseDashes( key ) ].push( value );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        const ttfb = await metrics.getTimeToFirstByte();
+                       const lcp = await metrics.getLargestContentfulPaint();
+
+                       results.largestContentfulPaint.push( lcp );
</ins><span class="cx" style="display: block; padding: 0 10px">                         results.timeToFirstByte.push( ttfb );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        results.largestContentfulPaint.push( lcp );
</del><span class="cx" style="display: block; padding: 0 10px">                         results.lcpMinusTtfb.push( lcp - ttfb );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                }
-       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         } );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestsperformancespecshomeclassicthemetestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/performance/specs/home-classic-theme.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/specs/home-classic-theme.test.js  2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/specs/home-classic-theme.test.js    2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,71 +1,56 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * External dependencies.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * WordPress dependencies
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const { basename, join } = require( 'path' );
-const { writeFileSync } = require( 'fs' );
-const { exec } = require( 'child_process' );
-const {
-       getResultsFilename,
-       getTimeToFirstByte,
-       getLargestContentfulPaint,
-} = require( './../utils' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+import { test } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * WordPress dependencies.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Internal dependencies
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import { activateTheme, createURL } from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+import { camelCaseDashes } from '../utils';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-describe( 'Server Timing - Twenty Twenty One', () => {
-       const results = {
-               wpBeforeTemplate: [],
-               wpTemplate: [],
-               wpTotal: [],
-               timeToFirstByte: [],
-               largestContentfulPaint: [],
-               lcpMinusTtfb: [],
-       };
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const results = {
+       timeToFirstByte: [],
+       largestContentfulPaint: [],
+       lcpMinusTtfb: [],
+};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        beforeAll( async () => {
-               await activateTheme( 'twentytwentyone' );
-               await exec(
-                       'npm run env:cli -- menu location assign all-pages primary'
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Front End - Twenty Twenty One', () => {
+       test.use( {
+               storageState: {}, // User will be logged out.
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        afterAll( async () => {
-               const resultsFilename = getResultsFilename(
-                       basename( __filename, '.js' )
-               );
-               writeFileSync(
-                       join( __dirname, resultsFilename ),
-                       JSON.stringify( results, null, 2 )
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.beforeAll( async ( { requestUtils } ) => {
+               await requestUtils.activateTheme( 'twentytwentyone' );
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Server Timing Metrics', async () => {
-               let i = TEST_RUNS;
-               while ( i-- ) {
-                       await page.goto( createURL( '/' ) );
-                       const navigationTimingJson = await page.evaluate( () =>
-                               JSON.stringify( performance.getEntriesByType( 'navigation' ) )
-                       );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test.afterAll( async ( {}, testInfo ) => {
+               await testInfo.attach( 'results', {
+                       body: JSON.stringify( results, null, 2 ),
+                       contentType: 'application/json',
+               } );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        const [ navigationTiming ] = JSON.parse( navigationTimingJson );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ const iterations = Number( process.env.TEST_RUNS );
+       for ( let i = 1; i <= iterations; i++ ) {
+               test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
+                       page,
+                       metrics,
+               } ) => {
+                       await page.goto( '/' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        results.wpBeforeTemplate.push(
-                               navigationTiming.serverTiming[ 0 ].duration
-                       );
-                       results.wpTemplate.push(
-                               navigationTiming.serverTiming[ 1 ].duration
-                       );
-                       results.wpTotal.push( navigationTiming.serverTiming[ 2 ].duration );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 const serverTiming = await metrics.getServerTiming();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        const ttfb = await getTimeToFirstByte();
-                       const lcp = await getLargestContentfulPaint();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 for (const [key, value] of Object.entries( serverTiming ) ) {
+                               results[ camelCaseDashes( key ) ] ??= [];
+                               results[ camelCaseDashes( key ) ].push( value );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        const ttfb = await metrics.getTimeToFirstByte();
+                       const lcp = await metrics.getLargestContentfulPaint();
+
+                       results.largestContentfulPaint.push( lcp );
</ins><span class="cx" style="display: block; padding: 0 10px">                         results.timeToFirstByte.push( ttfb );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        results.largestContentfulPaint.push( lcp );
</del><span class="cx" style="display: block; padding: 0 10px">                         results.lcpMinusTtfb.push( lcp - ttfb );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                }
-       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         } );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre></div>
<a id="trunktestsperformanceutilsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/performance/utils.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/performance/utils.js  2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/performance/utils.js    2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -16,63 +16,24 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Gets the result file name.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param {string} File name.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param {string} fileName File name.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @return {string} Result file name.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function getResultsFilename( fileName ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        const prefixArg = process.argv.find( ( arg ) =>
-               arg.startsWith( '--prefix' )
-       );
-       const fileNamePrefix = prefixArg ? `${ prefixArg.split( '=' )[ 1 ] }-` : '';
-       const resultsFilename = fileNamePrefix + fileName + '.results.json';
-       return resultsFilename;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ const prefix = process.env.TEST_RESULTS_PREFIX;
+       const fileNamePrefix = prefix ? `${ prefix.split( '=' )[ 1 ] }-` : '';
+       return `${fileNamePrefix + fileName}.results.json`;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/**
- * Returns time to first byte (TTFB) using the Navigation Timing API.
- *
- * @see https://web.dev/ttfb/#measure-ttfb-in-javascript
- *
- * @return {Promise<number>}
- */
-async function getTimeToFirstByte() {
-       return page.evaluate( () => {
-               const { responseStart, startTime } =
-                       performance.getEntriesByType( 'navigation' )[ 0 ];
-               return responseStart - startTime;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function camelCaseDashes( str ) {
+       return str.replace( /-([a-z])/g, function( g ) {
+               return g[ 1 ].toUpperCase();
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/**
- * Returns the Largest Contentful Paint (LCP) value using the dedicated API.
- *
- * @see https://w3c.github.io/largest-contentful-paint/
- * @see https://web.dev/lcp/#measure-lcp-in-javascript
- *
- * @return {Promise<number>}
- */
-async function getLargestContentfulPaint() {
-       return page.evaluate(
-               () =>
-                       new Promise( ( resolve ) => {
-                               new PerformanceObserver( ( entryList ) => {
-                                       const entries = entryList.getEntries();
-                                       // The last entry is the largest contentful paint.
-                                       const largestPaintEntry = entries.at( -1 );
-
-                                       resolve( largestPaintEntry?.startTime || 0 );
-                               } ).observe( {
-                                       type: 'largest-contentful-paint',
-                                       buffered: true,
-                               } );
-                       } )
-       );
-}
-
</del><span class="cx" style="display: block; padding: 0 10px"> module.exports = {
</span><span class="cx" style="display: block; padding: 0 10px">        median,
</span><span class="cx" style="display: block; padding: 0 10px">        getResultsFilename,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        getTimeToFirstByte,
-       getLargestContentfulPaint,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ camelCaseDashes,
</ins><span class="cx" style="display: block; padding: 0 10px"> };
</span></span></pre></div>
<a id="trunktestsvisualregressionREADMEmd"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/visual-regression/README.md</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/visual-regression/README.md   2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/visual-regression/README.md     2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,6 +1,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> # Visual Regression Tests in WordPress Core
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-These tests make use of Jest and Puppeteer, with a setup very similar to that of the e2e tests, together with [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot) for generating the visual diffs.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+These tests make use of Playwright, with a setup very similar to that of the e2e tests.
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> ## How to Run the Tests Locally
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7,5 +7,5 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 1. Check out trunk.
</span><span class="cx" style="display: block; padding: 0 10px"> 2. Run `npm run test:visual` to generate some base snapshots.
</span><span class="cx" style="display: block; padding: 0 10px"> 3. Check out the feature branch to be tested.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-4. Run `npm run test:visual` again. If any tests fail, the diff images can be found in `tests/visual-regression/specs/__image_snapshots__/__diff_output__`.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+4. Run `npm run test:visual` again. If any tests fail, the diff images can be found in `artifacts/`
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsvisualregressionjestconfigjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/visual-regression/jest.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/visual-regression/jest.config.js      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/visual-regression/jest.config.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,8 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const config = require( '@wordpress/scripts/config/jest-e2e.config' );
-
-const jestVisualRegressionConfig = {
-       ...config,
-       setupFilesAfterEnv: [ '<rootDir>/config/bootstrap.js' ],
-};
-
-module.exports = jestVisualRegressionConfig;
</del></span></pre></div>
<a id="trunktestsvisualregressionplaywrightconfigjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/visual-regression/playwright.config.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/visual-regression/playwright.config.js                                (rev 0)
+++ trunk/tests/visual-regression/playwright.config.js  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,27 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * External dependencies
+ */
+import path from 'node:path';
+import { defineConfig } from '@playwright/test';
+
+/**
+ * WordPress dependencies
+ */
+const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
+
+process.env.WP_ARTIFACTS_PATH ??= path.join( process.cwd(), 'artifacts' );
+process.env.STORAGE_STATE_PATH ??= path.join(
+       process.env.WP_ARTIFACTS_PATH,
+       'storage-states/admin.json'
+);
+
+const config = defineConfig( {
+       ...baseConfig,
+       globalSetup: undefined,
+       webServer: {
+               ...baseConfig.webServer,
+               command: 'npm run env:start',
+       },
+} );
+
+export default config;
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/visual-regression/playwright.config.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsvisualregressionruntestsjs"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/visual-regression/run-tests.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/visual-regression/run-tests.js        2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/visual-regression/run-tests.js  2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,13 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-const dotenv = require( 'dotenv' );
-const dotenv_expand = require( 'dotenv-expand' );
-const { execSync } = require( 'child_process' );
-
-// WP_BASE_URL interpolates LOCAL_PORT, so needs to be parsed by dotenv_expand().
-dotenv_expand.expand( dotenv.config() );
-
-// Run the tests, passing additional arguments through to the test script.
-execSync(
-       'wp-scripts test-e2e --config tests/visual-regression/jest.config.js ' +
-               process.argv.slice( 2 ).join( ' ' ),
-       { stdio: 'inherit' }
-);
</del><span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/visual-regression/specs
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/visual-regression/specs  2023-10-12 23:39:05 UTC (rev 56925)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/visual-regression/specs   2023-10-13 08:11:41 UTC (rev 56926)
</ins></span></pre></div>
<a id="trunktestsvisualregressionspecs"></a>
<div class="propset"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Property changes: trunk/tests/visual-regression/specs</h4>
<pre class="diff"><span>
</span></pre></div>
<a id="svnignore"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: svn:ignore</h4></div>
<del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-__image_snapshots__
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+__snapshots__
</ins><a id="trunktestsvisualregressionspecsvisualsnapshotstestjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/visual-regression/specs/visual-snapshots.test.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/visual-regression/specs/visual-snapshots.test.js      2023-10-12 23:39:05 UTC (rev 56925)
+++ trunk/tests/visual-regression/specs/visual-snapshots.test.js        2023-10-13 08:11:41 UTC (rev 56926)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,222 +1,166 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-import { visitAdminPage } from '@wordpress/e2e-test-utils';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+import { test, expect } from '@wordpress/e2e-test-utils-playwright';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-// See https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagescreenshotoptions for more available options.
-const screenshotOptions = {
-       fullPage: true,
-};
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const elementsToHide = [
+       '#footer-upgrade',
+       '#wp-admin-bar-root-default',
+       '#toplevel_page_gutenberg'
+];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-async function hideElementVisibility( elements ) {
-       for ( let i = 0; i < elements.length; i++ ) {
-               const elementOnPage = await page.$( elements[ i ] );
-               if ( elementOnPage ) {
-                       await elementOnPage.evaluate( ( el ) => {
-                               el.style.visibility = 'hidden';
-                       } );
-               }
-       }
-       await page.waitFor( 1000 );
-}
-
-async function removeElementFromLayout( elements ) {
-       for ( let i = 0; i < elements.length; i++ ) {
-               const elementOnPage = await page.$( elements[ i ] );
-               if ( elementOnPage ) {
-                       await elementOnPage.evaluate( ( el ) => {
-                               el.style.visibility = 'hidden';
-                       } );
-               }
-       }
-       await page.waitFor( 1000 );
-}
-
-const elementsToHide = [ '#footer-upgrade', '#wp-admin-bar-root-default' ];
-
-const elementsToRemove = [ '#toplevel_page_gutenberg' ];
-
-describe( 'Admin Visual Snapshots', () => {
-       beforeAll( async () => {
-               await page.setViewport( {
-                       width: 1000,
-                       height: 750,
-               } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+test.describe( 'Admin Visual Snapshots', () => {
+       test( 'All Posts', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/edit.php' );
+               await expect( page ).toHaveScreenshot( 'All Posts.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'All Posts', async () => {
-               await visitAdminPage( '/edit.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Categories', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/edit-tags.php', 'taxonomy=category' );
+               await expect( page ).toHaveScreenshot( 'Categories.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Categories', async () => {
-               await visitAdminPage( '/edit-tags.php', 'taxonomy=category' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Tags', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/edit-tags.php', 'taxonomy=post_tag' );
+               await expect( page ).toHaveScreenshot( 'Tags.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Tags', async () => {
-               await visitAdminPage( '/edit-tags.php', 'taxonomy=post_tag' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Media Library', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/upload.php' );
+               await expect( page ).toHaveScreenshot( 'Media Library.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Media Library', async () => {
-               await visitAdminPage( '/upload.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Add New Media', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/media-new.php' );
+               await expect( page ).toHaveScreenshot( 'Add New Media.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Add New Media', async () => {
-               await visitAdminPage( '/media-new.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'All Pages', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/edit.php', 'post_type=page' );
+               await expect( page ).toHaveScreenshot( 'All Pages.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'All Pages', async () => {
-               await visitAdminPage( '/edit.php', 'post_type=page' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Comments', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/edit-comments.php' );
+               await expect( page ).toHaveScreenshot( 'Comments.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Comments', async () => {
-               await visitAdminPage( '/edit-comments.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Widgets', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/widgets.php' );
+               await expect( page ).toHaveScreenshot( 'Widgets.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Widgets', async () => {
-               await visitAdminPage( '/widgets.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Menus', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/nav-menus.php' );
+               await expect( page ).toHaveScreenshot( 'Menus.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Menus', async () => {
-               await visitAdminPage( '/nav-menus.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Plugins', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/plugins.php' );
+               await expect( page ).toHaveScreenshot( 'Plugins.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Plugins', async () => {
-               await visitAdminPage( '/plugins.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'All Users', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/users.php' );
+               await expect( page ).toHaveScreenshot( 'All Users.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'All Users', async () => {
-               await visitAdminPage( '/users.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Add New User', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/user-new.php' );
+               await expect( page ).toHaveScreenshot( 'Add New User.png', {
+                       mask: [
+                                       ...elementsToHide,
+                                       '.password-input-wrapper'
+                       ].map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Add New User', async () => {
-               await visitAdminPage( '/user-new.php' );
-               await hideElementVisibility( [
-                       ...elementsToHide,
-                       '.password-input-wrapper',
-               ] );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Your Profile', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/profile.php' );
+               await expect( page ).toHaveScreenshot( 'Your Profile.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Your Profile', async () => {
-               await visitAdminPage( '/profile.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Available Tools', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/tools.php' );
+               await expect( page ).toHaveScreenshot( 'Available Tools.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Available Tools', async () => {
-               await visitAdminPage( '/tools.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Import', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/import.php' );
+               await expect( page ).toHaveScreenshot( 'Import.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Import', async () => {
-               await visitAdminPage( '/import.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Export', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/export.php' );
+               await expect( page ).toHaveScreenshot( 'Export.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Export', async () => {
-               await visitAdminPage( '/export.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Export Personal Data', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/export-personal-data.php' );
+               await expect( page ).toHaveScreenshot( 'Export Personal Data.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Export Personal Data', async () => {
-               await visitAdminPage( '/export-personal-data.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Erase Personal Data', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/erase-personal-data.php' );
+               await expect( page ).toHaveScreenshot( 'Erase Personal Data.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Erase Personal Data', async () => {
-               await visitAdminPage( '/erase-personal-data.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Reading Settings', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/options-reading.php' );
+               await expect( page ).toHaveScreenshot( 'Reading Settings.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Reading Settings', async () => {
-               await visitAdminPage( '/options-reading.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Discussion Settings', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/options-discussion.php' );
+               await expect( page ).toHaveScreenshot( 'Discussion Settings.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Discussion Settings', async () => {
-               await visitAdminPage( '/options-discussion.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Media Settings', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/options-media.php' );
+               await expect( page ).toHaveScreenshot( 'Media Settings.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        it( 'Media Settings', async () => {
-               await visitAdminPage( '/options-media.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ test( 'Privacy Settings', async ({ admin, page }) => {
+               await admin.visitAdminPage( '/options-privacy.php' );
+               await expect( page ).toHaveScreenshot( 'Privacy Settings.png', {
+                       mask: elementsToHide.map( ( selector ) => page.locator( selector ) ),
+               });
</ins><span class="cx" style="display: block; padding: 0 10px">         } );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-       it( 'Privacy Settings', async () => {
-               await visitAdminPage( '/options-privacy.php' );
-               await hideElementVisibility( elementsToHide );
-               await removeElementFromLayout( elementsToRemove );
-               const image = await page.screenshot( screenshotOptions );
-               expect( image ).toMatchImageSnapshot();
-       } );
</del><span class="cx" style="display: block; padding: 0 10px"> } );
</span></span></pre>
</div>
</div>

</body>
</html>