diff --git a/.distignore b/.distignore index 04d2425f6..1cf71d09d 100644 --- a/.distignore +++ b/.distignore @@ -6,6 +6,7 @@ .phpstan.neon.dist .scrutinizer.yml .wp-env.json +/.claude /.git /.github /.idea diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 60af8659d..6e55be070 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,11 @@ updates: interval: weekly open-pull-requests-limit: 10 target-branch: 'master' + ignore: + # @wordpress/e2e-test-utils-playwright peer-requires @types/node ^20.x; + # block major bumps that would re-create the ERESOLVE conflict. + - dependency-name: "@types/node" + update-types: ["version-update:semver-major"] - package-ecosystem: composer directory: '/' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..e6ff788c1 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,51 @@ +name: E2E Tests +permissions: + contents: read +on: + pull_request: + push: + branches: + - master +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true +jobs: + playwright: + name: Playwright (local project) + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout project + uses: actions/checkout@v7 + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: '24' + cache: 'npm' + - name: Node dependencies + run: npm ci + # The plugin requires vendor/autoload.php for PHP-DI, and the Gutenberg + # block is not committed — both have to exist before wp-env loads the + # plugin from the mapped checkout. + - name: Composer dependencies + run: composer install --no-dev --no-interaction --optimize-autoloader + - name: Build assets + run: npm run build + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Start WordPress multisite + run: npx wp-env start + # Seeds the de/it subsites and the linked demo posts via global-setup, + # then runs the admin and frontend specs. Visual specs are excluded — + # their baselines are only stable inside the Playwright Linux image. + - name: Run Playwright + run: npm run playwright:local + - name: Upload report + if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: | + tests/playwright/artifacts/html-report + tests/playwright/artifacts/test-results + retention-days: 7 diff --git a/.gitignore b/.gitignore index dfd75ea48..b17f2a924 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,12 @@ .phpunit.cache .phpunit.result.cache .vscode/ +assets/css/msls.css +assets/js/msls-quick-create.js +assets/js/msls-translation-picker.js assets/js/msls-widget-block/ assets/js/msls.js -composer.lock +build/translations.json composer.phar multisite-language-switcher.zip multisite-language-switcher/ @@ -16,7 +19,5 @@ out/ phpunit.xml.bak tests/coverage.xml tests/coverage/ -tests/playwright-report/ -tests/playwright-results/ -tests/playwright/.env.local +tests/playwright/artifacts/ vendor/ diff --git a/.wp-env.json b/.wp-env.json index 3a407b1fa..83d7bc66a 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,9 +1,7 @@ { "phpVersion": "8.3", "multisite": true, - "plugins": [ - "." - ], + "plugins": [], "env": { "tests": { "config": { @@ -12,7 +10,6 @@ }, "development": { "plugins": [ - ".", "https://downloads.wordpress.org/plugin/query-monitor.zip" ] } diff --git a/CLAUDE.md b/CLAUDE.md index 921b43e56..a60f33263 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,15 +32,30 @@ npm run build-msls-block # Build only the Gutenberg block ### E2E Tests ```bash -npx playwright test # Run Playwright tests (against msls.co) +npx wp-env start # Required first: the local suite runs against wp-env +npm run playwright:local # Admin + frontend specs (skips visual) +npm run playwright:visual # Visual specs inside the Playwright Linux image +npm run playwright:docker # Whole local suite inside that image +npm run playwright:update-snapshots # Regenerate visual baselines +npm run playwright:live # Only specs/live, against msls.co npx playwright test --ui # Run with UI ``` +Visual baselines are committed and only pixel-stable when generated inside the container — +always use the `:visual` / `:update-snapshots` scripts, never a bare `npx playwright test` +for them. ### Local Development Environment ```bash npx wp-env start # Start WordPress multisite via wp-env (PHP 8.3) npx wp-env stop +npx wp-env reset tests # Wipe the tests database (fresh-install check) +npx wp-env run cli wp plugin activate multisite-language-switcher --network ``` +The plugin reaches the container through `mappings` in `.wp-env.json`, not through +`plugins` — mounting it both ways created a duplicate `Multisite-Language-Switcher` +plugin directory and made `wp-env start` fail. Consequence: wp-env does not auto-activate +it, so activate it once per fresh **development** environment with the command above. The +**tests** environment is network-activated by the Playwright global setup. ## Architecture @@ -80,7 +95,15 @@ npx wp-env stop ## CI -GitHub Actions runs PHPStan, PHPCS, PHPUnit, and Playwright on every pull request. +GitHub Actions workflows: + +- `test.yml` — PHPUnit on every push (plus Codecov upload on `master`) +- `e2e.yml` — the Playwright `local` project (admin + frontend specs) on pull requests and + pushes to `master`; visual specs and the `live` project are not run in CI +- `plugin-check.yml` — WordPress.org Plugin Check on pull requests and `master` +- `deploy.yml` — WordPress.org deploy on tags + +PHPStan and PHPCS are not wired into CI yet — run them locally via `composer qa`. ## Conventions diff --git a/assets/css-flags/flags.php b/assets/css-flags/flags.php index b6e0a033e..8be42bf79 100644 --- a/assets/css-flags/flags.php +++ b/assets/css-flags/flags.php @@ -33,30 +33,30 @@ 'cy' => 'flag-icon-gb-wls', 'da_DK' => 'flag-icon-dk', 'de_AT' => 'flag-icon-at', - 'de_DE' => 'flag-icon-de', - 'de_DE_formal' => 'flag-icon-de', 'de_CH' => 'flag-icon-ch', 'de_CH_informal' => 'flag-icon-ch', + 'de_DE' => 'flag-icon-de', + 'de_DE_formal' => 'flag-icon-de', 'dzo' => 'flag-icon-bt', 'el' => 'flag-icon-gr', - 'en_ZA' => 'flag-icon-za', + 'en_AU' => 'flag-icon-au', 'en_GB' => 'flag-icon-gb', + 'en_ZA' => 'flag-icon-za', 'en_NZ' => 'flag-icon-nz', - 'en_AU' => 'flag-icon-au', 'en_CA' => 'flag-icon-ca', 'eo' => 'flag-icon-eu', - 'es_CL' => 'flag-icon-cl', - 'es_CO' => 'flag-icon-co', - 'es_MX' => 'flag-icon-mx', - 'es_ES' => 'flag-icon-es', - 'es_CR' => 'flag-icon-cr', 'es_AR' => 'flag-icon-ar', + 'es_CR' => 'flag-icon-cr', + 'es_ES' => 'flag-icon-es', + 'es_MX' => 'flag-icon-mx', + 'es_CO' => 'flag-icon-co', 'es_PE' => 'flag-icon-pe', 'es_VE' => 'flag-icon-ve', 'es_EC' => 'flag-icon-ec', 'es_DO' => 'flag-icon-do', 'es_UY' => 'flag-icon-uy', 'es_PR' => 'flag-icon-pr', + 'es_CL' => 'flag-icon-cl', 'es_GT' => 'flag-icon-gt', 'et' => 'flag-icon-ee', 'eu' => 'flag-icon-es', @@ -99,17 +99,17 @@ 'nb_NO' => 'flag-icon-no', 'ne_NP' => 'flag-icon-np', 'nl_NL' => 'flag-icon-nl', - 'nl_BE' => 'flag-icon-be', 'nl_NL_formal' => 'flag-icon-nl', + 'nl_BE' => 'flag-icon-be', 'nn_NO' => 'flag-icon-no', 'oci' => 'flag-icon-es-ca', 'pa_IN' => 'flag-icon-in', 'pl_PL' => 'flag-icon-pl', 'ps' => 'flag-icon-af', - 'pt_PT_ao90' => 'flag-icon-90', - 'pt_PT' => 'flag-icon-pt', 'pt_BR' => 'flag-icon-br', 'pt_AO' => 'flag-icon-ao', + 'pt_PT' => 'flag-icon-pt', + 'pt_PT_ao90' => 'flag-icon-90', 'ro_RO' => 'flag-icon-ro', 'ru_RU' => 'flag-icon-ru', 'sah' => 'flag-icon-ru', @@ -134,9 +134,9 @@ 'ur' => 'flag-icon-pk', 'uz_UZ' => 'flag-icon-uz', 'vi' => 'flag-icon-vn', + 'zh_CN' => 'flag-icon-cn', 'zh_TW' => 'flag-icon-tw', 'zh_HK' => 'flag-icon-hk', - 'zh_CN' => 'flag-icon-cn', ); /** diff --git a/assets/css/msls.css b/assets/css/msls.css deleted file mode 100644 index 8946ab9ff..000000000 --- a/assets/css/msls.css +++ /dev/null @@ -1 +0,0 @@ -div#msls.postbox label{margin-right:6px}div#msls.postbox input.msls_title,div#msls.postbox select{width:100%}select.msls-translations{width:226px}#msls.postbox .inside li{display:flex;align-items:center}#msls.postbox .inside li label{display:flex}#msls.postbox .inside li input.msls_title,#msls.postbox .inside li select{flex-grow:1}#msls.postbox .inside li .msls-create-new,#msls.postbox .inside li .msls-edit-link{text-decoration:none;margin-left:4px;color:#2271b1}#msls.postbox .inside li .msls-create-new:hover,#msls.postbox .inside li .msls-edit-link:hover{color:#135e96}.msls-quick-create{background:0 0;border:none;padding:0;margin:0;cursor:pointer;color:inherit;font:inherit;line-height:inherit}.msls-quick-create.msls-loading .dashicons{animation:msls-spin 1s linear infinite}@keyframes msls-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}#msls-content-import .button-primary{margin:1em auto}.flag-icon{width:1.3333em!important;height:1em!important;vertical-align:middle;overflow:hidden;line-height:1!important;color:transparent}.msls-icon-wrapper{display:inline-flex;justify-content:center;align-items:center;text-align:center}.msls-icon-wrapper.flag{min-width:36px}.msls-icon-wrapper.label{min-width:48px}label .msls-icon-wrapper{text-align:left}#wpadminbar * .language-badge,#wpadminbar .language-badge,.language-badge{display:inline-block;min-width:32px;height:auto;padding:4px 6px;white-space:nowrap;font-size:10px;line-height:1;text-align:center;background-color:currentColor;border-radius:9px;user-select:none}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span,.language-badge>span{display:inline-block;vertical-align:top;margin:0 1px;font-size:10px;font-weight:600;line-height:1;text-transform:uppercase;color:#fff;text-align:center}#wpadminbar * .language-badge>span:nth-child(2),#wpadminbar .language-badge>span:nth-child(2),.language-badge>span:nth-child(2){opacity:.5}.column-mslscol .language-badge{margin:0 1px!important}.column-mslscol{width:56px}#wpadminbar * .language-badge,#wpadminbar .language-badge{position:relative;top:-1px;padding-top:3px;padding-bottom:3px;background-color:transparent;border:1px currentColor solid}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span{color:currentColor}#adminmenu .wp-submenu a[href*=msls-translation-picker-]{padding-left:28px}.msls-tp-page .msls-tp-back{margin:4px 0 0;font-size:13px}.msls-tp-page .msls-tp-back a{color:#2271b1;text-decoration:none}.msls-tp-page .msls-tp-back a:focus,.msls-tp-page .msls-tp-back a:hover{color:#135e96;text-decoration:underline}.msls-tp-page .msls-tp-banner{display:flex;align-items:center;gap:8px;margin:14px 0}.msls-tp-page .msls-tp-banner .msls-tp-banner-arrow{color:#2271b1;font-weight:600}.msls-tp-page .msls-tp-sources{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:14px 0 10px}.msls-tp-page .msls-tp-sources .msls-tp-sources-label{font-weight:600;margin-right:4px;color:#1d2327}.msls-tp-page .msls-tp-source-flag{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;background:#fff;border:1px solid #c3c4c7;border-radius:20px;text-decoration:none;color:#1d2327;font-size:13px;line-height:1.2;transition:background 120ms ease,border-color 120ms ease,box-shadow 120ms ease}.msls-tp-page .msls-tp-source-flag .flag-icon{font-size:18px;line-height:1}.msls-tp-page .msls-tp-source-flag:focus,.msls-tp-page .msls-tp-source-flag:hover{background:#f6f7f7;border-color:#8c8f94;color:#1d2327;box-shadow:none}.msls-tp-page .msls-tp-source-flag.is-active{background:#2271b1;border-color:#2271b1;color:#fff;box-shadow:0 0 0 1px #2271b1}.msls-tp-page .msls-tp-source-flag.is-active:focus,.msls-tp-page .msls-tp-source-flag.is-active:hover{background:#135e96;border-color:#135e96;color:#fff}.msls-tp-page .msls-tp-source-flag .msls-tp-source-label{font-weight:500}.msls-tp-page .msls-tp-filters{display:flex;align-items:center;gap:8px;margin:4px 0 16px;flex-wrap:wrap}.msls-tp-page .msls-tp-filters input[type=search]{min-width:240px}.msls-tp-page .msls-tp-progress{margin:12px 0}.msls-tp-page table.wp-list-table .row-actions .msls-tp-create.msls-loading{opacity:.7;pointer-events:none}.msls-tp-page table.wp-list-table .row-actions .msls-tp-create.msls-loading::after{content:"";display:inline-block;width:10px;height:10px;margin-left:6px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:msls-spin .8s linear infinite;vertical-align:middle}.msls-tp-page table.wp-list-table .column-status{width:110px}.msls-tp-page table.wp-list-table .column-date{width:140px}.msls-tp-lang-chip{display:inline-block;padding:2px 6px;background:#f0f0f1;border-radius:10px;font-size:10px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;color:#50575e}.msls-tp-status-badge{display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}.msls-tp-status-badge.msls-tp-status-publish{background:#e0f7e5;color:#1d593f}.msls-tp-status-badge.msls-tp-status-draft{background:#f0f0f1;color:#50575e}.msls-tp-status-badge.msls-tp-status-pending{background:#fff3cd;color:#674d03}.msls-tp-status-badge.msls-tp-status-future{background:#e7e5f7;color:#3a2d6b} \ No newline at end of file diff --git a/assets/flags/flags.php b/assets/flags/flags.php index 6c256213a..e25481ce1 100644 --- a/assets/flags/flags.php +++ b/assets/flags/flags.php @@ -33,30 +33,30 @@ 'cy' => 'wales.png', 'da_DK' => 'dk.png', 'de_AT' => 'at.png', - 'de_DE' => 'de.png', - 'de_DE_formal' => 'de.png', 'de_CH' => 'ch.png', 'de_CH_informal' => 'ch.png', + 'de_DE' => 'de.png', + 'de_DE_formal' => 'de.png', 'dzo' => 'bt.png', 'el' => 'gr.png', - 'en_ZA' => 'za.png', + 'en_AU' => 'au.png', 'en_GB' => 'gb.png', + 'en_ZA' => 'za.png', 'en_NZ' => 'nz.png', - 'en_AU' => 'au.png', 'en_CA' => 'ca.png', 'eo' => 'europeanunion.png', - 'es_CL' => 'cl.png', - 'es_CO' => 'co.png', - 'es_MX' => 'mx.png', - 'es_ES' => 'es.png', - 'es_CR' => 'cr.png', 'es_AR' => 'ar.png', + 'es_CR' => 'cr.png', + 'es_ES' => 'es.png', + 'es_MX' => 'mx.png', + 'es_CO' => 'co.png', 'es_PE' => 'pe.png', 'es_VE' => 've.png', 'es_EC' => 'ec.png', 'es_DO' => 'do.png', 'es_UY' => 'uy.png', 'es_PR' => 'pr.png', + 'es_CL' => 'cl.png', 'es_GT' => 'gt.png', 'et' => 'ee.png', 'eu' => 'es.png', @@ -99,17 +99,17 @@ 'nb_NO' => 'no.png', 'ne_NP' => 'np.png', 'nl_NL' => 'nl.png', - 'nl_BE' => 'be.png', 'nl_NL_formal' => 'nl.png', + 'nl_BE' => 'be.png', 'nn_NO' => 'no.png', 'oci' => 'catalonia.png', 'pa_IN' => 'in.png', 'pl_PL' => 'pl.png', 'ps' => 'af.png', - 'pt_PT_ao90' => '90.png', - 'pt_PT' => 'pt.png', 'pt_BR' => 'br.png', 'pt_AO' => 'ao.png', + 'pt_PT' => 'pt.png', + 'pt_PT_ao90' => '90.png', 'ro_RO' => 'ro.png', 'ru_RU' => 'ru.png', 'sah' => 'ru.png', @@ -134,9 +134,9 @@ 'ur' => 'pk.png', 'uz_UZ' => 'uz.png', 'vi' => 'vn.png', + 'zh_CN' => 'cn.png', 'zh_TW' => 'tw.png', 'zh_HK' => 'hk.png', - 'zh_CN' => 'cn.png', ); /** diff --git a/assets/js/msls-quick-create.js b/assets/js/msls-quick-create.js deleted file mode 100644 index 311ab2826..000000000 --- a/assets/js/msls-quick-create.js +++ /dev/null @@ -1 +0,0 @@ -jQuery(document).ready(function($){$(document).on("click",".msls-quick-create",function(){var $button=$(this);if($button.hasClass("msls-loading")){return}$button.addClass("msls-loading");$button.find(".dashicons").removeClass("dashicons-plus").addClass("dashicons-update");wp.apiFetch({path:"/msls/v1/create-translation",method:"POST",data:{source_post_id:parseInt($button.data("source-post-id"),10),source_blog_id:parseInt($button.data("source-blog-id"),10),target_blog_id:parseInt($button.data("target-blog-id"),10)}}).then(function(response){var isMetabox=$button.closest("#msls").length>0;var editTitle=$button.data("edit-title")||$button.attr("title");var $link=$("").attr("href",response.edit_url).attr("title",editTitle).html($button.html());if(isMetabox){$link.addClass("msls-edit-link").attr("target","_blank")}var successIcon=isMetabox?"dashicons-external":"dashicons-edit";$link.find(".dashicons").removeClass("dashicons-update dashicons-plus").addClass(successIcon);$button.replaceWith($link);var $container=$link.closest("li");if(!$container.length){return}var $hiddenInput=$container.find('input[type="hidden"][name^="msls_input_"]');if($hiddenInput.length){$hiddenInput.val(response.post_id)}var $titleInput=$container.find("input.msls_title");if($titleInput.length){$titleInput.val(response.post_title||"")}var $select=$container.find('select[name^="msls_input_"]');if($select.length){$select.append($("