From f857c3e222fd92076522f2111db21538172303c8 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 15:28:57 +0200 Subject: [PATCH 01/33] Initial setup for PlayWright --- .github/workflows/playwright.yml | 98 ++++++++++++++ .gitignore | 4 + .wp-env.json | 17 +++ package-lock.json | 60 +++++++++ package.json | 12 +- test-playwright.sh | 43 ++++++ tests/e2e/CI-SETUP.md | 184 ++++++++++++++++++++++++++ tests/e2e/README.md | 132 ++++++++++++++++++ tests/e2e/admin-page.spec.ts | 24 ++++ tests/playwright/playwright.config.ts | 41 ++++++ 10 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 .wp-env.json create mode 100755 test-playwright.sh create mode 100644 tests/e2e/CI-SETUP.md create mode 100644 tests/e2e/README.md create mode 100644 tests/e2e/admin-page.spec.ts create mode 100644 tests/playwright/playwright.config.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..9cbe84a4 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,98 @@ +name: Playwright + +on: + pull_request: + types: [labeled, synchronize, opened, reopened] + push: + branches: + - 'core' + paths-ignore: + - '**.md' + - '**.txt' + - '.gitignore' + - 'docs/**' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: read + +concurrency: + group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build-plugin: + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + name: Build plugin + uses: ./.github/workflows/build.yml + + Playwright: + name: Playwright test on PHP 8.1 + runs-on: ubuntu-22.04 + needs: [build-plugin] + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: | + npm run prepare-environment:ci + cd src && composer install + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-plugin.outputs.artifact_name }} + path: ./build + + - name: Start WordPress environment + run: | + npm run wp-env:start + + - name: Setup test data + run: npm run test:setup:playwright + + - name: WordPress debug information + run: | + wp-env run cli wp core version + wp-env run cli wp --info + + - name: Install playwright/test + run: | + npx playwright install chromium + + - name: Run Playwright tests + run: npm run test:playwright + + - name: Stop WordPress environment + if: always() + run: npm run wp-env:stop + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-test-results + path: test-results/ + if-no-files-found: ignore + retention-days: 2 + + test-result: + needs: [Playwright] + if: always() && (needs.Playwright.result != 'skipped') + runs-on: ubuntu-22.04 + name: Playwright - Test Results + steps: + - name: Test status + run: echo "Test status is - ${{ needs.Playwright.result }}" + - name: Check Playwright status + if: ${{ needs.Playwright.result != 'success' && needs.Playwright.result != 'skipped' }} + run: exit 1 diff --git a/.gitignore b/.gitignore index e42ed840..76ceb1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ node_modules/ npm-debug.log .sass-cache/ +# Playwright +playwright-report/ +test-results/ + # Local files (ideally, should be in a global .gitignore) .idea/ Thumbs.db diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 00000000..8d57611a --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,17 @@ +{ + "core": null, + "phpVersion": "8.1", + "mappings": { + "wp-content/plugins/code-snippets": "./src" + }, + "themes": ["WordPress/twentytwentyfour"], + "port": 8888, + "testsPort": 8889, + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": "local" + } +} diff --git a/package-lock.json b/package-lock.json index 2f804753..66bd5e9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.20.0", + "@playwright/test": "^1.48.0", "@stylistic/eslint-plugin": "^3.1.0", "@stylistic/stylelint-plugin": "^3.1.2", "@tsconfig/node18": "^18.2.4", @@ -2581,6 +2582,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, @@ -6488,6 +6504,20 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -8383,6 +8413,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "dev": true, diff --git a/package.json b/package.json index 71f583ad..efcd6902 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,14 @@ }, "scripts": { "test": "npm run stylelint && eslint && npm run phpcs", + "test:playwright": "playwright test -c tests/playwright/playwright.config.ts", + "test:playwright:debug": "npm run test:playwright -- --debug", + "test:playwright:ui": "npm run test:playwright -- --ui", + "prepare-environment:ci": "npm ci", + "wp-env:start": "wp-env start", + "wp-env:stop": "wp-env stop", + "wp-env:clean": "wp-env clean all", + "test:setup:playwright": "wp-env run cli wp plugin activate code-snippets", "build": "webpack", "watch": "webpack --watch", "bundle": "ts-node scripts/bundle.ts", @@ -105,7 +113,9 @@ "webpack": "^5.97.1", "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1", - "webpack-remove-empty-scripts": "^1.0.4" + "webpack-remove-empty-scripts": "^1.0.4", + "@playwright/test": "^1.48.0", + "@wordpress/env": "^9.0.0" }, "overrides": { "eslint": "^9.20.1", diff --git a/test-playwright.sh b/test-playwright.sh new file mode 100755 index 00000000..738fa88f --- /dev/null +++ b/test-playwright.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +echo "๐Ÿงช Testing Playwright Setup for Code Snippets" +echo "==============================================" + +if [ ! -f "package.json" ]; then + echo "โŒ Error: Please run this script from the project root directory" + exit 1 +fi + +if [ ! -d "node_modules" ]; then + echo "๐Ÿ“ฆ Installing dependencies..." + npm install +fi + +echo "๐Ÿ”จ Building plugin and installing PHP dependencies..." +npm run build +cd src && composer install && cd .. + +echo "๐Ÿš€ Starting WordPress environment..." +npm run wp-env:start + +echo "โณ Waiting for WordPress to start..." +sleep 15 + +if curl -s http://localhost:8888/wp-admin/ > /dev/null; then + echo "โœ… WordPress is running on http://localhost:8888" +else + echo "โŒ WordPress failed to start" + npm run wp-env:stop + exit 1 +fi + +echo "๐Ÿ”ง Setting up test data..." +npm run test:setup:playwright + +echo "๐Ÿงช Running Playwright tests..." +npm run test:playwright + +echo "๐Ÿงน Cleaning up..." +npm run wp-env:stop + +echo "โœ… Test complete!" diff --git a/tests/e2e/CI-SETUP.md b/tests/e2e/CI-SETUP.md new file mode 100644 index 00000000..69b1da7e --- /dev/null +++ b/tests/e2e/CI-SETUP.md @@ -0,0 +1,184 @@ +# Playwright Tests with @wordpress/env + +Simple Playwright testing setup for Code Snippets using `@wordpress/env`. + +## ๐ŸŽฏ Approach + +We use `@wordpress/env` to create a complete WordPress environment for testing, both locally and in CI. + +## ๐Ÿš€ Local Development + +### Prerequisites +- **Docker**: wp-env requires Docker to be running +- **Node.js**: Version 18 or higher + +### Setup +```bash +# Install dependencies +npm install + +# Build plugin and install PHP dependencies +npm run build +cd src && composer install + +# Start WordPress environment +npm run wp-env:start + +# Run tests +npm run test:playwright +``` + +### Available Commands +```bash +npm run wp-env:start # Start WordPress environment +npm run wp-env:stop # Stop WordPress environment +npm run wp-env:clean # Clean WordPress environment +npm run test:playwright # Run all tests +npm run test:playwright:ui # Run with UI +npm run test:playwright:debug # Run in debug mode +``` + +## ๐Ÿ”„ CI/CD (GitHub Actions) + +### Automatic Triggers +- **Pull Requests**: Tests run when PR has `run-tests` label +- **Push to main/develop**: Tests run automatically + +### Manual Triggers +- **Workflow Dispatch**: Manual trigger from GitHub Actions tab + +### How It Works +1. **Build Plugin**: Uses existing build workflow +2. **WordPress Environment**: Creates fresh WordPress with MySQL +3. **Plugin Installation**: Installs and activates Code Snippets +4. **Test Execution**: Runs Playwright tests +5. **Results**: Uploads test reports and artifacts + +## ๐Ÿ“ File Structure + +``` +tests/ +โ”œโ”€โ”€ playwright/ +โ”‚ โ”œโ”€โ”€ playwright.config.ts # Playwright configuration +โ”‚ โ””โ”€โ”€ playwright-report/ # Test results +โ”œโ”€โ”€ e2e/ +โ”‚ โ”œโ”€โ”€ admin-page.spec.ts # @admin tests +โ”‚ โ””โ”€โ”€ README.md # Local testing guide +โ”œโ”€โ”€ .wp-env.json # WordPress environment config +โ””โ”€โ”€ test-playwright.sh # Convenience script +``` + +## ๐Ÿท๏ธ Test Categories + +- **@admin**: Admin interface tests + +### Running Specific Categories +```bash +npm run test:playwright -- --grep="@admin" +``` + +## ๐Ÿ› ๏ธ Configuration + +### WordPress Environment +```json +{ + "core": null, + "phpVersion": "8.1", + "plugins": ["./src"], + "themes": ["WordPress/twentytwentyfour"], + "port": 8888, + "testsPort": 8889, + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": "local" + } +} +``` + +### Playwright Configuration +- Base URL: `http://localhost:8888` +- HTML, JSON, and JUnit reporters +- Screenshots and videos on failure + +## ๐Ÿ“Š Test Reports + +### Local +- HTML report opens automatically after tests +- Screenshots saved to `test-results/` + +### CI +- **HTML Report**: Interactive test results +- **JSON Report**: Machine-readable results +- **JUnit Report**: CI integration +- **Screenshots**: Failure screenshots +- **Videos**: Test execution videos + +### Accessing CI Reports +1. Go to GitHub Actions +2. Select a workflow run +3. Download the `playwright-test-results` artifact +4. Open `index.html` in your browser + +## ๐Ÿ”ง Troubleshooting + +### Local Issues + +**WordPress Not Starting** +```bash +# Check if port 8888 is available +lsof -i :8888 + +# Stop any existing wp-env +npm run wp-env:stop + +# Clean restart +npm run wp-env:clean +npm run wp-env:start +``` + +**Tests Failing** +```bash +# Run with debug output +npm run test:playwright:debug + +# Check WordPress is running +curl http://localhost:8888/wp-admin/ +``` + +### CI Issues + +**WordPress Not Ready** +- Check MySQL service health +- Verify wp-lite-env configuration +- Check global setup logs + +**Plugin Not Activated** +- Verify plugin path in wp-lite-env config +- Check setup script execution +- Review WordPress CLI output + +## ๐Ÿ“ Best Practices + +1. **Test Organization**: Use descriptive tags and test names +2. **Selectors**: Use data attributes when possible +3. **Wait Strategies**: Always wait for page load states +4. **Error Handling**: Include proper error messages +5. **Test Data**: Use setup scripts for consistent test data +6. **Parallel Execution**: Design tests to run independently + +## ๐ŸŽฏ What Gets Tested + +- โœ… Admin page loads correctly +- โœ… Functions tab displays and is visible +- โœ… WordPress authentication works +- โœ… Plugin is properly activated + +## ๐Ÿš€ Next Steps + +1. **Add More Tests**: Expand test coverage for all plugin features +2. **Performance Tests**: Add performance monitoring +3. **Visual Regression**: Add screenshot comparison tests +4. **Cross-Browser**: Enable WebKit when issues are resolved \ No newline at end of file diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 00000000..8f53389b --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,132 @@ +# Playwright Tests for Code Snippets + +Simple Playwright tests for the Code Snippets WordPress plugin using `@wordpress/env`. + +## ๐Ÿ“‹ Prerequisites + +- **Docker**: wp-env requires Docker to be running +- **Node.js**: Version 18 or higher +- **npm**: For package management + +## ๐Ÿš€ Quick Start + +### 1. Install Dependencies +```bash +npm install +``` + +### 2. Build Plugin and Install PHP Dependencies +```bash +npm run build +cd src && composer install +``` + +### 3. Start WordPress Environment +```bash +# Make sure Docker is running first +npm run wp-env:start +``` + +### 4. Run Tests +```bash +# Run all tests +npm run test:playwright + +# Run with UI +npm run test:playwright:ui + +# Run in debug mode +npm run test:playwright:debug + +# Run specific test categories +npm run test:playwright -- --grep="@admin" +``` + +### 5. Quick Test Script +```bash +# Run everything with one command +./test-playwright.sh +``` + +## ๐Ÿ“ Test Structure + +- `admin-page.spec.ts` - Tests for the main admin page (@admin) + +## ๐Ÿ”ง Configuration + +- **WordPress Environment**: `.wp-env.json` +- **Playwright Config**: `tests/playwright/playwright.config.ts` + +## ๐ŸŽฏ What Gets Tested + +- โœ… Admin page loads correctly +- โœ… Functions tab displays and is visible +- โœ… WordPress authentication works +- โœ… Plugin is properly activated + +## ๐Ÿ› ๏ธ Troubleshooting + +### Docker Issues +```bash +# Check if Docker is running +docker --version +docker ps + +# Start Docker Desktop (if installed) +# Or start Docker service on Linux +sudo systemctl start docker +``` + +### WordPress Not Starting +```bash +# Check if port 8888 is available +lsof -i :8888 + +# Stop any existing wp-env +npm run wp-env:stop +``` + +### Tests Failing +```bash +# Run with debug output +npm run test:playwright:debug + +# Check WordPress is running +curl http://localhost:8888/wp-admin/ +``` + +### Clean Restart +```bash +# Stop WordPress +npm run wp-env:stop + +# Clean environment +npm run wp-env:clean + +# Start fresh +npm run wp-env:start +``` + +### Alternative: Test Against Existing WordPress Site +If you don't want to use Docker, you can test against an existing WordPress site: + +```bash +# Set environment variables +export WP_URL=http://your-wordpress-site.local +export WP_USERNAME=admin +export WP_PASSWORD=password + +# Run tests against existing site +npm run test:playwright +``` + +## ๐Ÿ“‹ Available Commands + +```bash +npm run wp-env:start # Start WordPress environment +npm run wp-env:stop # Stop WordPress environment +npm run wp-env:clean # Clean WordPress environment +npm run test:playwright # Run all tests +npm run test:playwright:ui # Run with UI +npm run test:playwright:debug # Run in debug mode +``` \ No newline at end of file diff --git a/tests/e2e/admin-page.spec.ts b/tests/e2e/admin-page.spec.ts new file mode 100644 index 00000000..cc696e4b --- /dev/null +++ b/tests/e2e/admin-page.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Code Snippets Admin Page @admin', () => { + test('should display the Functions tab on the snippets admin page', async ({ page }) => { + // First, log in to WordPress + await page.goto('/wp-login.php'); + await page.fill('#user_login', 'admin'); + await page.fill('#user_pass', 'password'); + await page.click('#wp-submit'); + await page.waitForURL(/wp-admin/); + + // Navigate to the Code Snippets admin page + await page.goto('/wp-admin/admin.php?page=snippets'); + + // Wait for the page to load + await page.waitForLoadState('networkidle'); + + // Look for the Functions tab - just check if it exists + const functionsTab = page.locator('text=Functions').first(); + + // Check that the tab exists and is visible + await expect(functionsTab).toBeVisible(); + }); +}); diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts new file mode 100644 index 00000000..373a7317 --- /dev/null +++ b/tests/playwright/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: '../e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html'], + ['json', { outputFile: 'test-results/results.json' }], + ['junit', { outputFile: 'test-results/results.xml' }] + ], + use: { + baseURL: 'http://localhost:8888', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], + + timeout: 30000, + + expect: { + timeout: 10000, + }, +}); From 048291cc04bca347278009204a44516ca5e1f61c Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 15:40:55 +0200 Subject: [PATCH 02/33] remove redundant spaces --- .github/workflows/playwright.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 9cbe84a4..784ff5ec 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,47 +36,47 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 - + - name: Install Node.js 20.x uses: actions/setup-node@v4 with: node-version: 20.x cache: 'npm' - + - name: Install dependencies run: | npm run prepare-environment:ci cd src && composer install - + - name: Download build artifact uses: actions/download-artifact@v4 with: name: ${{ needs.build-plugin.outputs.artifact_name }} path: ./build - + - name: Start WordPress environment run: | npm run wp-env:start - + - name: Setup test data run: npm run test:setup:playwright - + - name: WordPress debug information run: | wp-env run cli wp core version wp-env run cli wp --info - + - name: Install playwright/test run: | npx playwright install chromium - + - name: Run Playwright tests run: npm run test:playwright - + - name: Stop WordPress environment if: always() run: npm run wp-env:stop - + - uses: actions/upload-artifact@v4 if: always() with: From 09a2f14d9893b18f4c3a1633bae72093059a96f9 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 15:51:42 +0200 Subject: [PATCH 03/33] remove redundant comments --- tests/e2e/admin-page.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/e2e/admin-page.spec.ts b/tests/e2e/admin-page.spec.ts index cc696e4b..c66c6dd0 100644 --- a/tests/e2e/admin-page.spec.ts +++ b/tests/e2e/admin-page.spec.ts @@ -2,23 +2,18 @@ import { test, expect } from '@playwright/test'; test.describe('Code Snippets Admin Page @admin', () => { test('should display the Functions tab on the snippets admin page', async ({ page }) => { - // First, log in to WordPress await page.goto('/wp-login.php'); await page.fill('#user_login', 'admin'); await page.fill('#user_pass', 'password'); await page.click('#wp-submit'); await page.waitForURL(/wp-admin/); - // Navigate to the Code Snippets admin page await page.goto('/wp-admin/admin.php?page=snippets'); - // Wait for the page to load await page.waitForLoadState('networkidle'); - // Look for the Functions tab - just check if it exists const functionsTab = page.locator('text=Functions').first(); - // Check that the tab exists and is visible await expect(functionsTab).toBeVisible(); }); }); From 2cc2aaf4f3d537685f009d37fc15033fc4b967dd Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 15:55:20 +0200 Subject: [PATCH 04/33] update lock file --- package-lock.json | 1102 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1099 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66bd5e9b..b0a855cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "@wordpress/babel-preset-default": "^8.17.0", + "@wordpress/env": "^9.0.0", "archiver": "^7.0.1", "autoprefixer": "^10.4.20", "babel-loader": "^9.2.1", @@ -2468,6 +2469,21 @@ "buffer": "^6.0.3" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, "node_modules/@lezer/common": { "version": "0.15.12", "license": "MIT" @@ -2607,6 +2623,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz", @@ -2696,6 +2724,18 @@ "@csstools/css-tokenizer": "^3.0.1" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tannin/compile": { "version": "1.1.0", "license": "MIT", @@ -2760,6 +2800,18 @@ "@types/readdir-glob": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/codemirror": { "version": "5.60.15", "dev": true, @@ -2799,6 +2851,12 @@ "version": "1.2.1", "license": "MIT" }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "dev": true, @@ -2838,6 +2896,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mousetrap": { "version": "1.6.14", "license": "MIT" @@ -2893,6 +2960,15 @@ "@types/node": "*" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/rtlcss": { "version": "3.5.4", "dev": true, @@ -3582,6 +3658,57 @@ "npm": ">=8.19.2" } }, + "node_modules/@wordpress/env": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-9.10.0.tgz", + "integrity": "sha512-GqUg1XdrUXI3l5NhHhEZisrccW+VPqJSU5xO1IXybI6KOvmSecidxWEqlMj26vzu2P5aLCWZcx28QkrrY3jvdg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "copy-dir": "^1.3.0", + "docker-compose": "^0.24.3", + "extract-zip": "^1.6.7", + "got": "^11.8.5", + "inquirer": "^7.1.0", + "js-yaml": "^3.13.1", + "ora": "^4.0.2", + "rimraf": "^3.0.2", + "simple-git": "^3.5.0", + "terminal-link": "^2.0.0", + "yargs": "^17.3.0" + }, + "bin": { + "wp-env": "bin/wp-env" + } + }, + "node_modules/@wordpress/env/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@wordpress/env/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@wordpress/env/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/@wordpress/escape-html": { "version": "3.28.0", "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.28.0.tgz", @@ -3904,6 +4031,21 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "dev": true, @@ -4612,6 +4754,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -4628,6 +4779,33 @@ "keyv": "^5.3.2" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable/node_modules/keyv": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", @@ -4771,6 +4949,12 @@ "tslib": "^2.0.3" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/chokidar": { "version": "4.0.1", "dev": true, @@ -4811,6 +4995,39 @@ "version": "2.5.1", "license": "MIT" }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/clipboard": { "version": "2.0.11", "license": "MIT", @@ -4820,6 +5037,29 @@ "tiny-emitter": "^2.0.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "dev": true, @@ -4844,6 +5084,18 @@ "node": ">=0.10.0" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -4950,6 +5202,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/constant-case": { "version": "3.0.4", "license": "MIT", @@ -4964,6 +5231,12 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz", + "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw==", + "dev": true + }, "node_modules/core-js": { "version": "3.33.2", "dev": true, @@ -5371,6 +5644,33 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -5383,6 +5683,27 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "dev": true, @@ -5459,6 +5780,18 @@ "node": ">=8" } }, + "node_modules/docker-compose": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", + "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "dev": true, + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -5572,6 +5905,15 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "dev": true, @@ -6242,6 +6584,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "dev": true, @@ -6296,6 +6651,62 @@ "node": ">=0.8.x" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -6348,6 +6759,39 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -6504,6 +6948,12 @@ } } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -6560,10 +7010,19 @@ "node": ">=6.9.0" } }, - "node_modules/get-intrinsic": { - "version": "1.2.7", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "dev": true, + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", @@ -6595,6 +7054,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "dev": true, @@ -6851,6 +7325,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "dev": true, @@ -6989,6 +7488,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "license": "MIT", @@ -7082,6 +7600,17 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "dev": true, @@ -7094,6 +7623,30 @@ "dev": true, "license": "ISC" }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "dev": true, @@ -7316,6 +7869,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.3", "dev": true, @@ -7793,6 +8355,89 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/log-symbols/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -7810,6 +8455,15 @@ "tslib": "^2.0.3" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -7910,6 +8564,24 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.2", "dev": true, @@ -7948,6 +8620,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/moment": { "version": "2.30.1", "license": "MIT", @@ -7975,6 +8659,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "node_modules/nanoid": { "version": "3.3.8", "dev": true, @@ -8037,6 +8727,18 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "dev": true, @@ -8153,6 +8855,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -8169,6 +8895,50 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", + "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "dev": true, @@ -8185,6 +8955,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "dev": true, @@ -8284,6 +9063,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "dev": true, @@ -8335,6 +9123,12 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "node_modules/php-parser": { "version": "3.2.2", "license": "BSD-3-Clause" @@ -9291,6 +10085,16 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "dev": true, @@ -9323,6 +10127,18 @@ "dev": true, "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -9600,6 +10416,15 @@ "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "dev": true, @@ -9623,6 +10448,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-cwd": { "version": "3.0.0", "dev": true, @@ -9657,6 +10488,37 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -9666,6 +10528,43 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "dev": true, @@ -9683,6 +10582,15 @@ "node": ">=12.0.0" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -9711,6 +10619,24 @@ "integrity": "sha512-zWl10xu2D7zoR8zSC2U6bg5bYF6T/Wk7rxwp8IPaJH7f0Ge21G03kNHVgHR7tyVkSSfAOG0Rqf/Cl38JftSmtw==", "license": "MIT" }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/safe-array-concat": { "version": "1.1.3", "dev": true, @@ -10048,6 +10974,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-git": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10916,6 +11857,35 @@ "streamx": "^2.15.0" } }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link/node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/terser": { "version": "5.31.6", "dev": true, @@ -11016,10 +11986,28 @@ "dev": true, "license": "MIT" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "node_modules/tiny-emitter": { "version": "2.1.0", "license": "MIT" }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -11156,6 +12144,18 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "dev": true, @@ -11226,6 +12226,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, "node_modules/typescript": { "version": "5.7.3", "dev": true, @@ -11449,6 +12455,15 @@ "node": ">=10.13.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webpack": { "version": "5.97.1", "dev": true, @@ -11745,6 +12760,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -11762,6 +12794,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -11776,11 +12814,69 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "dev": true, From 25e1edc2b447d893e1f938e39ce13945ba04fac5 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 16:00:43 +0200 Subject: [PATCH 05/33] wp-env fixes --- .github/workflows/playwright.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 784ff5ec..9a9bfaa0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -56,15 +56,15 @@ jobs: - name: Start WordPress environment run: | - npm run wp-env:start + npx wp-env start - name: Setup test data - run: npm run test:setup:playwright + run: npx wp-env run cli wp plugin activate code-snippets - name: WordPress debug information run: | - wp-env run cli wp core version - wp-env run cli wp --info + npx wp-env run cli wp core version + npx wp-env run cli wp --info - name: Install playwright/test run: | @@ -75,7 +75,7 @@ jobs: - name: Stop WordPress environment if: always() - run: npm run wp-env:stop + run: npx wp-env stop - uses: actions/upload-artifact@v4 if: always() From bed35bdcf2949ae8eb73c312290ed23f23001f0d Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 16:08:08 +0200 Subject: [PATCH 06/33] remove firefox --- tests/playwright/playwright.config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index 373a7317..f12e2f10 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -26,11 +26,6 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, ], timeout: 30000, From d6afc3fe13be0c59057418ecdbdeed1c359a75ed Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 16:16:50 +0200 Subject: [PATCH 07/33] include nodejs type definitions --- tests/playwright/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index f12e2f10..64f8e7aa 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -1,3 +1,4 @@ +/// import { defineConfig, devices } from '@playwright/test'; /** From 39ea959069bc2b843b6526213f586ce1622338bf Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 21:30:25 +0200 Subject: [PATCH 08/33] improve tests --- .gitignore | 1 + tests/e2e/admin-page.spec.ts | 19 --------------- tests/e2e/auth.setup.ts | 12 ++++++++++ tests/e2e/code-snippets.spec.ts | 33 +++++++++++++++++++++++++++ tests/playwright/playwright.config.ts | 10 +++++++- 5 files changed, 55 insertions(+), 20 deletions(-) delete mode 100644 tests/e2e/admin-page.spec.ts create mode 100644 tests/e2e/auth.setup.ts create mode 100644 tests/e2e/code-snippets.spec.ts diff --git a/.gitignore b/.gitignore index 76ceb1c8..5b2fe500 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ npm-debug.log # Playwright playwright-report/ test-results/ +auth.json # Local files (ideally, should be in a global .gitignore) .idea/ diff --git a/tests/e2e/admin-page.spec.ts b/tests/e2e/admin-page.spec.ts deleted file mode 100644 index c66c6dd0..00000000 --- a/tests/e2e/admin-page.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Code Snippets Admin Page @admin', () => { - test('should display the Functions tab on the snippets admin page', async ({ page }) => { - await page.goto('/wp-login.php'); - await page.fill('#user_login', 'admin'); - await page.fill('#user_pass', 'password'); - await page.click('#wp-submit'); - await page.waitForURL(/wp-admin/); - - await page.goto('/wp-admin/admin.php?page=snippets'); - - await page.waitForLoadState('networkidle'); - - const functionsTab = page.locator('text=Functions').first(); - - await expect(functionsTab).toBeVisible(); - }); -}); diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts new file mode 100644 index 00000000..56dd845d --- /dev/null +++ b/tests/e2e/auth.setup.ts @@ -0,0 +1,12 @@ +import { test as setup } from '@playwright/test'; + +setup('authenticate', async ({ page }) => { + await page.goto('/wp-login.php'); + await page.waitForSelector('#user_login'); + await page.fill('#user_login', 'admin'); + await page.fill('#user_pass', 'password'); + await page.click('#wp-submit'); + await page.waitForURL(/wp-admin/); + + await page.context().storageState({ path: 'auth.json' }); +}); diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts new file mode 100644 index 00000000..d5554a97 --- /dev/null +++ b/tests/e2e/code-snippets.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; + +const TEST_SNIPPET_NAME = 'E2E Test Snippet'; + +test.describe('Code Snippets Admin Page @admin', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + }); + + test('Can add a new snippet', async ({ page }) => { + await page.click('text=Add New') + await page.fill('#title', TEST_SNIPPET_NAME) + await page.fill('.CodeMirror textarea', 'echo "Hello World!";') + await page.click('text=Save Snippet') + await expect(page.locator('#message.notice')).toContainText('Snippet created') + }); + + test('Can activate and deactivate a snippet', async ({ page }) => { + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.click('text=Save and Activate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated.'); + await page.click('text=Save and Deactivate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and deactivated'); + }); + + test('Can delete a snippet', async ({ page }) => { + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.click('text=Delete'); + await page.click('button.components-button.is-destructive.is-primary'); // Confirm dialog + await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); + }); +}); diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index 64f8e7aa..0a0e27f9 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -23,9 +23,17 @@ export default defineConfig({ }, projects: [ + { + name: 'setup', + testMatch: /.*\.setup\.ts/, + }, { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { + ...devices['Desktop Chrome'], + storageState: 'auth.json', + }, + dependencies: ['setup'], }, ], From 6545e209ae98695d238f07583c559b23c78bfa13 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 21:42:10 +0200 Subject: [PATCH 09/33] debug --- tests/e2e/CI-SETUP.md | 184 -------------------------------- tests/e2e/README.md | 135 ++++++----------------- tests/e2e/code-snippets.spec.ts | 26 ++++- 3 files changed, 53 insertions(+), 292 deletions(-) delete mode 100644 tests/e2e/CI-SETUP.md diff --git a/tests/e2e/CI-SETUP.md b/tests/e2e/CI-SETUP.md deleted file mode 100644 index 69b1da7e..00000000 --- a/tests/e2e/CI-SETUP.md +++ /dev/null @@ -1,184 +0,0 @@ -# Playwright Tests with @wordpress/env - -Simple Playwright testing setup for Code Snippets using `@wordpress/env`. - -## ๐ŸŽฏ Approach - -We use `@wordpress/env` to create a complete WordPress environment for testing, both locally and in CI. - -## ๐Ÿš€ Local Development - -### Prerequisites -- **Docker**: wp-env requires Docker to be running -- **Node.js**: Version 18 or higher - -### Setup -```bash -# Install dependencies -npm install - -# Build plugin and install PHP dependencies -npm run build -cd src && composer install - -# Start WordPress environment -npm run wp-env:start - -# Run tests -npm run test:playwright -``` - -### Available Commands -```bash -npm run wp-env:start # Start WordPress environment -npm run wp-env:stop # Stop WordPress environment -npm run wp-env:clean # Clean WordPress environment -npm run test:playwright # Run all tests -npm run test:playwright:ui # Run with UI -npm run test:playwright:debug # Run in debug mode -``` - -## ๐Ÿ”„ CI/CD (GitHub Actions) - -### Automatic Triggers -- **Pull Requests**: Tests run when PR has `run-tests` label -- **Push to main/develop**: Tests run automatically - -### Manual Triggers -- **Workflow Dispatch**: Manual trigger from GitHub Actions tab - -### How It Works -1. **Build Plugin**: Uses existing build workflow -2. **WordPress Environment**: Creates fresh WordPress with MySQL -3. **Plugin Installation**: Installs and activates Code Snippets -4. **Test Execution**: Runs Playwright tests -5. **Results**: Uploads test reports and artifacts - -## ๐Ÿ“ File Structure - -``` -tests/ -โ”œโ”€โ”€ playwright/ -โ”‚ โ”œโ”€โ”€ playwright.config.ts # Playwright configuration -โ”‚ โ””โ”€โ”€ playwright-report/ # Test results -โ”œโ”€โ”€ e2e/ -โ”‚ โ”œโ”€โ”€ admin-page.spec.ts # @admin tests -โ”‚ โ””โ”€โ”€ README.md # Local testing guide -โ”œโ”€โ”€ .wp-env.json # WordPress environment config -โ””โ”€โ”€ test-playwright.sh # Convenience script -``` - -## ๐Ÿท๏ธ Test Categories - -- **@admin**: Admin interface tests - -### Running Specific Categories -```bash -npm run test:playwright -- --grep="@admin" -``` - -## ๐Ÿ› ๏ธ Configuration - -### WordPress Environment -```json -{ - "core": null, - "phpVersion": "8.1", - "plugins": ["./src"], - "themes": ["WordPress/twentytwentyfour"], - "port": 8888, - "testsPort": 8889, - "config": { - "WP_DEBUG": true, - "WP_DEBUG_LOG": true, - "WP_DEBUG_DISPLAY": false, - "SCRIPT_DEBUG": true, - "WP_ENVIRONMENT_TYPE": "local" - } -} -``` - -### Playwright Configuration -- Base URL: `http://localhost:8888` -- HTML, JSON, and JUnit reporters -- Screenshots and videos on failure - -## ๐Ÿ“Š Test Reports - -### Local -- HTML report opens automatically after tests -- Screenshots saved to `test-results/` - -### CI -- **HTML Report**: Interactive test results -- **JSON Report**: Machine-readable results -- **JUnit Report**: CI integration -- **Screenshots**: Failure screenshots -- **Videos**: Test execution videos - -### Accessing CI Reports -1. Go to GitHub Actions -2. Select a workflow run -3. Download the `playwright-test-results` artifact -4. Open `index.html` in your browser - -## ๐Ÿ”ง Troubleshooting - -### Local Issues - -**WordPress Not Starting** -```bash -# Check if port 8888 is available -lsof -i :8888 - -# Stop any existing wp-env -npm run wp-env:stop - -# Clean restart -npm run wp-env:clean -npm run wp-env:start -``` - -**Tests Failing** -```bash -# Run with debug output -npm run test:playwright:debug - -# Check WordPress is running -curl http://localhost:8888/wp-admin/ -``` - -### CI Issues - -**WordPress Not Ready** -- Check MySQL service health -- Verify wp-lite-env configuration -- Check global setup logs - -**Plugin Not Activated** -- Verify plugin path in wp-lite-env config -- Check setup script execution -- Review WordPress CLI output - -## ๐Ÿ“ Best Practices - -1. **Test Organization**: Use descriptive tags and test names -2. **Selectors**: Use data attributes when possible -3. **Wait Strategies**: Always wait for page load states -4. **Error Handling**: Include proper error messages -5. **Test Data**: Use setup scripts for consistent test data -6. **Parallel Execution**: Design tests to run independently - -## ๐ŸŽฏ What Gets Tested - -- โœ… Admin page loads correctly -- โœ… Functions tab displays and is visible -- โœ… WordPress authentication works -- โœ… Plugin is properly activated - -## ๐Ÿš€ Next Steps - -1. **Add More Tests**: Expand test coverage for all plugin features -2. **Performance Tests**: Add performance monitoring -3. **Visual Regression**: Add screenshot comparison tests -4. **Cross-Browser**: Enable WebKit when issues are resolved \ No newline at end of file diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 8f53389b..2333b60c 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,132 +1,61 @@ -# Playwright Tests for Code Snippets +# Playwright Tests -Simple Playwright tests for the Code Snippets WordPress plugin using `@wordpress/env`. +End-to-end tests for Code Snippets using `@wordpress/env`. -## ๐Ÿ“‹ Prerequisites +## Prerequisites -- **Docker**: wp-env requires Docker to be running -- **Node.js**: Version 18 or higher -- **npm**: For package management +- Docker (for wp-env) +- Node.js 18+ -## ๐Ÿš€ Quick Start +## Quick Start -### 1. Install Dependencies ```bash +# Install dependencies npm install -``` -### 2. Build Plugin and Install PHP Dependencies -```bash -npm run build -cd src && composer install -``` +# Build plugin and install PHP dependencies +npm run build && cd src && composer install && cd .. -### 3. Start WordPress Environment -```bash -# Make sure Docker is running first +# Start WordPress environment npm run wp-env:start -``` -### 4. Run Tests -```bash -# Run all tests +# Run tests npm run test:playwright - -# Run with UI -npm run test:playwright:ui - -# Run in debug mode -npm run test:playwright:debug - -# Run specific test categories -npm run test:playwright -- --grep="@admin" -``` - -### 5. Quick Test Script -```bash -# Run everything with one command -./test-playwright.sh ``` -## ๐Ÿ“ Test Structure - -- `admin-page.spec.ts` - Tests for the main admin page (@admin) - -## ๐Ÿ”ง Configuration - -- **WordPress Environment**: `.wp-env.json` -- **Playwright Config**: `tests/playwright/playwright.config.ts` - -## ๐ŸŽฏ What Gets Tested +## Commands -- โœ… Admin page loads correctly -- โœ… Functions tab displays and is visible -- โœ… WordPress authentication works -- โœ… Plugin is properly activated - -## ๐Ÿ› ๏ธ Troubleshooting - -### Docker Issues ```bash -# Check if Docker is running -docker --version -docker ps - -# Start Docker Desktop (if installed) -# Or start Docker service on Linux -sudo systemctl start docker +npm run wp-env:start # Start WordPress +npm run wp-env:stop # Stop WordPress +npm run wp-env:clean # Clean environment +npm run test:playwright # Run tests +npm run test:playwright:ui # Run with UI +npm run test:playwright:debug # Debug mode ``` -### WordPress Not Starting -```bash -# Check if port 8888 is available -lsof -i :8888 +## CI/CD -# Stop any existing wp-env -npm run wp-env:stop -``` +Tests run automatically on: +- Pull requests with `run-tests` label +- Push to `core` branch +- Manual workflow dispatch -### Tests Failing -```bash -# Run with debug output -npm run test:playwright:debug - -# Check WordPress is running -curl http://localhost:8888/wp-admin/ -``` +## Troubleshooting -### Clean Restart +**Docker not running:** ```bash -# Stop WordPress -npm run wp-env:stop - -# Clean environment -npm run wp-env:clean - -# Start fresh -npm run wp-env:start +docker --version && docker ps ``` -### Alternative: Test Against Existing WordPress Site -If you don't want to use Docker, you can test against an existing WordPress site: - +**WordPress won't start:** ```bash -# Set environment variables -export WP_URL=http://your-wordpress-site.local -export WP_USERNAME=admin -export WP_PASSWORD=password - -# Run tests against existing site -npm run test:playwright +lsof -i :8888 # Check port availability +npm run wp-env:stop && npm run wp-env:start ``` -## ๐Ÿ“‹ Available Commands - +**Tests failing:** ```bash -npm run wp-env:start # Start WordPress environment -npm run wp-env:stop # Stop WordPress environment -npm run wp-env:clean # Clean WordPress environment -npm run test:playwright # Run all tests -npm run test:playwright:ui # Run with UI -npm run test:playwright:debug # Run in debug mode +npm run test:playwright:debug +curl http://localhost:8888/wp-admin/ # Check WordPress ``` \ No newline at end of file diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index d5554a97..d59fda0f 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -9,15 +9,27 @@ test.describe('Code Snippets Admin Page @admin', () => { }); test('Can add a new snippet', async ({ page }) => { - await page.click('text=Add New') - await page.fill('#title', TEST_SNIPPET_NAME) - await page.fill('.CodeMirror textarea', 'echo "Hello World!";') - await page.click('text=Save Snippet') - await expect(page.locator('#message.notice')).toContainText('Snippet created') + await page.click('text=Add New'); + await page.waitForLoadState('networkidle'); + + // Wait for the form to be ready + await page.waitForSelector('#title'); + await page.fill('#title', TEST_SNIPPET_NAME); + + // Wait for CodeMirror to be ready + await page.waitForSelector('.CodeMirror textarea'); + await page.fill('.CodeMirror textarea', 'echo "Hello World!";'); + + await page.click('text=Save Snippet'); + await expect(page.locator('#message.notice')).toContainText('Snippet created'); }); test('Can activate and deactivate a snippet', async ({ page }) => { + // Wait for the snippet to exist before clicking + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); + await page.click('text=Save and Activate'); await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated.'); await page.click('text=Save and Deactivate'); @@ -25,7 +37,11 @@ test.describe('Code Snippets Admin Page @admin', () => { }); test('Can delete a snippet', async ({ page }) => { + // Wait for the snippet to exist before clicking + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); + await page.click('text=Delete'); await page.click('button.components-button.is-destructive.is-primary'); // Confirm dialog await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); From 949d02cb567d50593ff1d42bc2009328e60ca37f Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 22:29:02 +0200 Subject: [PATCH 10/33] debug --- tests/e2e/auth.setup.ts | 12 +++++++ tests/e2e/code-snippets.spec.ts | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts index 56dd845d..0ed9255e 100644 --- a/tests/e2e/auth.setup.ts +++ b/tests/e2e/auth.setup.ts @@ -1,12 +1,24 @@ import { test as setup } from '@playwright/test'; setup('authenticate', async ({ page }) => { + console.log('Starting authentication setup...'); + await page.goto('/wp-login.php'); + console.log('Navigated to login page:', page.url()); + await page.waitForSelector('#user_login'); await page.fill('#user_login', 'admin'); await page.fill('#user_pass', 'password'); await page.click('#wp-submit'); + await page.waitForURL(/wp-admin/); + console.log('Successfully logged in, current URL:', page.url()); + + // Verify we can access admin + await page.goto('/wp-admin/'); + const adminTitle = await page.title(); + console.log('Admin page title:', adminTitle); await page.context().storageState({ path: 'auth.json' }); + console.log('Authentication state saved to auth.json'); }); diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index d59fda0f..bfc71909 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -6,9 +6,65 @@ test.describe('Code Snippets Admin Page @admin', () => { test.beforeEach(async ({ page }) => { await page.goto('/wp-admin/admin.php?page=snippets'); await page.waitForLoadState('networkidle'); + + // Debug: Check if we're actually logged in and on the right page + const currentUrl = page.url(); + console.log('Current URL:', currentUrl); + + // If we're redirected to login, something is wrong with auth + if (currentUrl.includes('wp-login.php')) { + throw new Error('Authentication failed - redirected to login page'); + } + }); + + test('Can access admin page', async ({ page }) => { + // Simple test to verify we can access the admin + const title = await page.title(); + console.log('Page title:', title); + + // Check if we're actually on the snippets page + const url = page.url(); + console.log('Current URL:', url); + + // Verify the page has some expected content + const bodyText = await page.textContent('body'); + const hasSnippetsContent = bodyText?.includes('Snippets') || bodyText?.includes('snippet'); + console.log('Has snippets content:', hasSnippetsContent); + + expect(url).toContain('page=snippets'); }); test('Can add a new snippet', async ({ page }) => { + // Debug: Check what's actually on the page + const pageContent = await page.textContent('body'); + console.log('Page content preview:', pageContent?.substring(0, 500)); + + // Check if Add New button exists + const addNewButton = page.locator('text=Add New'); + const buttonCount = await addNewButton.count(); + console.log('Add New button count:', buttonCount); + + // Also check for alternative button text + const addNewVariants = [ + 'text=Add New', + 'text=Add New Snippet', + 'text=New Snippet', + '[href*="action=add"]', + 'button:has-text("Add")', + 'button:has-text("New")' + ]; + + for (const selector of addNewVariants) { + const count = await page.locator(selector).count(); + console.log(`Selector "${selector}" count:`, count); + } + + if (buttonCount === 0) { + // Take a screenshot for debugging + await page.screenshot({ path: 'debug-no-add-new.png' }); + throw new Error('Add New button not found on page'); + } + await page.click('text=Add New'); await page.waitForLoadState('networkidle'); From 1fe1691420b6ca4bab6fd54fe0fec51915378533 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 22:35:37 +0200 Subject: [PATCH 11/33] debug --- tests/e2e/code-snippets.spec.ts | 35 +++++++++++---------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index bfc71909..5cd02607 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -7,6 +7,9 @@ test.describe('Code Snippets Admin Page @admin', () => { await page.goto('/wp-admin/admin.php?page=snippets'); await page.waitForLoadState('networkidle'); + // Wait for WordPress admin content to be fully loaded + await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); + // Debug: Check if we're actually logged in and on the right page const currentUrl = page.url(); console.log('Current URL:', currentUrl); @@ -35,35 +38,19 @@ test.describe('Code Snippets Admin Page @admin', () => { }); test('Can add a new snippet', async ({ page }) => { + // Wait for the actual snippets page content to load + await page.waitForSelector('.wrap h1, .page-title, .wp-heading-inline', { timeout: 10000 }); + // Debug: Check what's actually on the page const pageContent = await page.textContent('body'); console.log('Page content preview:', pageContent?.substring(0, 500)); - // Check if Add New button exists - const addNewButton = page.locator('text=Add New'); - const buttonCount = await addNewButton.count(); - console.log('Add New button count:', buttonCount); - - // Also check for alternative button text - const addNewVariants = [ - 'text=Add New', - 'text=Add New Snippet', - 'text=New Snippet', - '[href*="action=add"]', - 'button:has-text("Add")', - 'button:has-text("New")' - ]; + // Check if Add New button exists and is visible + const addNewButton = page.locator('text=Add New').first(); + await addNewButton.waitFor({ state: 'visible', timeout: 10000 }); - for (const selector of addNewVariants) { - const count = await page.locator(selector).count(); - console.log(`Selector "${selector}" count:`, count); - } - - if (buttonCount === 0) { - // Take a screenshot for debugging - await page.screenshot({ path: 'debug-no-add-new.png' }); - throw new Error('Add New button not found on page'); - } + const buttonCount = await page.locator('text=Add New').count(); + console.log('Add New button count:', buttonCount); await page.click('text=Add New'); await page.waitForLoadState('networkidle'); From 2707cf46ad6ede698cad69e3211506355e6fd301 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 22:47:31 +0200 Subject: [PATCH 12/33] debug --- tests/e2e/code-snippets.spec.ts | 34 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index 5cd02607..4f3f85fe 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -6,14 +6,14 @@ test.describe('Code Snippets Admin Page @admin', () => { test.beforeEach(async ({ page }) => { await page.goto('/wp-admin/admin.php?page=snippets'); await page.waitForLoadState('networkidle'); - + // Wait for WordPress admin content to be fully loaded await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); - + // Debug: Check if we're actually logged in and on the right page const currentUrl = page.url(); console.log('Current URL:', currentUrl); - + // If we're redirected to login, something is wrong with auth if (currentUrl.includes('wp-login.php')) { throw new Error('Authentication failed - redirected to login page'); @@ -24,35 +24,25 @@ test.describe('Code Snippets Admin Page @admin', () => { // Simple test to verify we can access the admin const title = await page.title(); console.log('Page title:', title); - + // Check if we're actually on the snippets page const url = page.url(); console.log('Current URL:', url); - + // Verify the page has some expected content const bodyText = await page.textContent('body'); const hasSnippetsContent = bodyText?.includes('Snippets') || bodyText?.includes('snippet'); console.log('Has snippets content:', hasSnippetsContent); - + expect(url).toContain('page=snippets'); }); test('Can add a new snippet', async ({ page }) => { - // Wait for the actual snippets page content to load - await page.waitForSelector('.wrap h1, .page-title, .wp-heading-inline', { timeout: 10000 }); - - // Debug: Check what's actually on the page - const pageContent = await page.textContent('body'); - console.log('Page content preview:', pageContent?.substring(0, 500)); - - // Check if Add New button exists and is visible - const addNewButton = page.locator('text=Add New').first(); - await addNewButton.waitFor({ state: 'visible', timeout: 10000 }); - - const buttonCount = await page.locator('text=Add New').count(); - console.log('Add New button count:', buttonCount); + // Wait for the page to load + await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - await page.click('text=Add New'); + // Click the correct "Add New" button (should be in the page header, not admin menu) + await page.click('.page-title-action, .wrap .page-title-action'); await page.waitForLoadState('networkidle'); // Wait for the form to be ready @@ -72,7 +62,7 @@ test.describe('Code Snippets Admin Page @admin', () => { await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); await page.click(`text=${TEST_SNIPPET_NAME}`); await page.waitForLoadState('networkidle'); - + await page.click('text=Save and Activate'); await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated.'); await page.click('text=Save and Deactivate'); @@ -84,7 +74,7 @@ test.describe('Code Snippets Admin Page @admin', () => { await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); await page.click(`text=${TEST_SNIPPET_NAME}`); await page.waitForLoadState('networkidle'); - + await page.click('text=Delete'); await page.click('button.components-button.is-destructive.is-primary'); // Confirm dialog await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); From b6e54b43ad1910b431b4351d48fa59e30886a18f Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 22:51:43 +0200 Subject: [PATCH 13/33] wip - refactor into test steps --- tests/e2e/auth.setup.ts | 24 ----- tests/e2e/code-snippets.spec.ts | 123 ++++++++++++-------------- tests/playwright/playwright.config.ts | 6 -- 3 files changed, 56 insertions(+), 97 deletions(-) delete mode 100644 tests/e2e/auth.setup.ts diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts deleted file mode 100644 index 0ed9255e..00000000 --- a/tests/e2e/auth.setup.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { test as setup } from '@playwright/test'; - -setup('authenticate', async ({ page }) => { - console.log('Starting authentication setup...'); - - await page.goto('/wp-login.php'); - console.log('Navigated to login page:', page.url()); - - await page.waitForSelector('#user_login'); - await page.fill('#user_login', 'admin'); - await page.fill('#user_pass', 'password'); - await page.click('#wp-submit'); - - await page.waitForURL(/wp-admin/); - console.log('Successfully logged in, current URL:', page.url()); - - // Verify we can access admin - await page.goto('/wp-admin/'); - const adminTitle = await page.title(); - console.log('Admin page title:', adminTitle); - - await page.context().storageState({ path: 'auth.json' }); - console.log('Authentication state saved to auth.json'); -}); diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index 4f3f85fe..c30f98a3 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -2,81 +2,70 @@ import { test, expect } from '@playwright/test'; const TEST_SNIPPET_NAME = 'E2E Test Snippet'; -test.describe('Code Snippets Admin Page @admin', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/wp-admin/admin.php?page=snippets'); - await page.waitForLoadState('networkidle'); - - // Wait for WordPress admin content to be fully loaded - await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); - - // Debug: Check if we're actually logged in and on the right page - const currentUrl = page.url(); - console.log('Current URL:', currentUrl); - - // If we're redirected to login, something is wrong with auth - if (currentUrl.includes('wp-login.php')) { - throw new Error('Authentication failed - redirected to login page'); - } - }); +test.describe('Code Snippets Plugin', () => { + test('Complete snippet workflow', async ({ page }) => { + await test.step('Login to WordPress', async () => { + await page.goto('/wp-login.php'); + await page.waitForSelector('#user_login'); + await page.fill('#user_login', 'admin'); + await page.fill('#user_pass', 'password'); + await page.click('#wp-submit'); + await page.waitForURL(/wp-admin/); + }); - test('Can access admin page', async ({ page }) => { - // Simple test to verify we can access the admin - const title = await page.title(); - console.log('Page title:', title); + await test.step('Navigate to snippets page', async () => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); - // Check if we're actually on the snippets page - const url = page.url(); - console.log('Current URL:', url); + const currentUrl = page.url(); + expect(currentUrl).toContain('page=snippets'); + }); - // Verify the page has some expected content - const bodyText = await page.textContent('body'); - const hasSnippetsContent = bodyText?.includes('Snippets') || bodyText?.includes('snippet'); - console.log('Has snippets content:', hasSnippetsContent); + await test.step('Add a new snippet', async () => { + await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - expect(url).toContain('page=snippets'); - }); + await page.click('.page-title-action, .wrap .page-title-action'); + await page.waitForLoadState('networkidle'); - test('Can add a new snippet', async ({ page }) => { - // Wait for the page to load - await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - - // Click the correct "Add New" button (should be in the page header, not admin menu) - await page.click('.page-title-action, .wrap .page-title-action'); - await page.waitForLoadState('networkidle'); - - // Wait for the form to be ready - await page.waitForSelector('#title'); - await page.fill('#title', TEST_SNIPPET_NAME); - - // Wait for CodeMirror to be ready - await page.waitForSelector('.CodeMirror textarea'); - await page.fill('.CodeMirror textarea', 'echo "Hello World!";'); - - await page.click('text=Save Snippet'); - await expect(page.locator('#message.notice')).toContainText('Snippet created'); - }); + await page.waitForSelector('#title'); + await page.fill('#title', TEST_SNIPPET_NAME); - test('Can activate and deactivate a snippet', async ({ page }) => { - // Wait for the snippet to exist before clicking - await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); - await page.click(`text=${TEST_SNIPPET_NAME}`); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('.CodeMirror textarea'); + await page.fill('.CodeMirror textarea', 'echo "Hello World!";'); - await page.click('text=Save and Activate'); - await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated.'); - await page.click('text=Save and Deactivate'); - await expect(page.locator('#message.notice p')).toContainText('Snippet updated and deactivated'); - }); + await page.click('text=Save Snippet'); + await expect(page.locator('#message.notice')).toContainText('Snippet created'); + }); + + await test.step('Activate the snippet', async () => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); + + await page.click('text=Save and Activate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated'); + }); + + await test.step('Deactivate the snippet', async () => { + await page.click('text=Save and Deactivate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and deactivated'); + }); + + await test.step('Delete the snippet', async () => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); - test('Can delete a snippet', async ({ page }) => { - // Wait for the snippet to exist before clicking - await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); - await page.click(`text=${TEST_SNIPPET_NAME}`); - await page.waitForLoadState('networkidle'); + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); - await page.click('text=Delete'); - await page.click('button.components-button.is-destructive.is-primary'); // Confirm dialog - await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); + await page.click('text=Delete'); + await page.click('button.components-button.is-destructive.is-primary'); + await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); + }); }); }); diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index 0a0e27f9..4e6860c2 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -23,17 +23,11 @@ export default defineConfig({ }, projects: [ - { - name: 'setup', - testMatch: /.*\.setup\.ts/, - }, { name: 'chromium', use: { ...devices['Desktop Chrome'], - storageState: 'auth.json', }, - dependencies: ['setup'], }, ], From c183ec6198fd832b2cf740048f4e6964285fe0d9 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 22:59:53 +0200 Subject: [PATCH 14/33] wip --- tests/e2e/code-snippets.spec.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index c30f98a3..9f3b2de6 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -24,10 +24,28 @@ test.describe('Code Snippets Plugin', () => { await test.step('Add a new snippet', async () => { await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - - await page.click('.page-title-action, .wrap .page-title-action'); + + // Debug: Check what buttons are available + const buttonCount = await page.locator('.page-title-action').count(); + console.log('Page title action buttons found:', buttonCount); + + if (buttonCount === 0) { + // Try alternative selectors + const altButtons = await page.locator('a[href*="action=add"], button:has-text("Add"), a:has-text("Add New")').count(); + console.log('Alternative add buttons found:', altButtons); + + // Take screenshot for debugging + await page.screenshot({ path: 'debug-no-add-button.png' }); + throw new Error('No Add New button found'); + } + + await page.click('.page-title-action'); await page.waitForLoadState('networkidle'); - + + // Debug: Check where we are after clicking + const currentUrl = page.url(); + console.log('URL after clicking Add New:', currentUrl); + await page.waitForSelector('#title'); await page.fill('#title', TEST_SNIPPET_NAME); From 08d085e6eefea023f6e928c719144d54cd206fbe Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 23:20:05 +0200 Subject: [PATCH 15/33] debug --- .github/workflows/playwright.yml | 5 +++++ tests/e2e/code-snippets.spec.ts | 19 ------------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 9a9bfaa0..d58e98ba 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -54,6 +54,11 @@ jobs: name: ${{ needs.build-plugin.outputs.artifact_name }} path: ./build + - name: Copy built plugin to source directory + run: | + rm -rf ./src + cp -r ./build/code-snippets ./src + - name: Start WordPress environment run: | npx wp-env start diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index 9f3b2de6..6495c220 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -24,28 +24,9 @@ test.describe('Code Snippets Plugin', () => { await test.step('Add a new snippet', async () => { await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - - // Debug: Check what buttons are available - const buttonCount = await page.locator('.page-title-action').count(); - console.log('Page title action buttons found:', buttonCount); - - if (buttonCount === 0) { - // Try alternative selectors - const altButtons = await page.locator('a[href*="action=add"], button:has-text("Add"), a:has-text("Add New")').count(); - console.log('Alternative add buttons found:', altButtons); - - // Take screenshot for debugging - await page.screenshot({ path: 'debug-no-add-button.png' }); - throw new Error('No Add New button found'); - } - await page.click('.page-title-action'); await page.waitForLoadState('networkidle'); - // Debug: Check where we are after clicking - const currentUrl = page.url(); - console.log('URL after clicking Add New:', currentUrl); - await page.waitForSelector('#title'); await page.fill('#title', TEST_SNIPPET_NAME); From 8944c2116c3a2d9a4253e430c506ee753150d59f Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 9 Sep 2025 23:31:19 +0200 Subject: [PATCH 16/33] use storage state --- .gitignore | 1 + tests/e2e/auth.setup.ts | 21 +++++++ tests/e2e/code-snippets.spec.ts | 89 +++++++++++---------------- tests/playwright/playwright.config.ts | 8 +++ 4 files changed, 67 insertions(+), 52 deletions(-) create mode 100644 tests/e2e/auth.setup.ts diff --git a/.gitignore b/.gitignore index 5b2fe500..1c3b03cb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ npm-debug.log # Playwright playwright-report/ test-results/ +tests/e2e/.auth/ auth.json # Local files (ideally, should be in a global .gitignore) diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts new file mode 100644 index 00000000..0a2f37bd --- /dev/null +++ b/tests/e2e/auth.setup.ts @@ -0,0 +1,21 @@ +import { test as setup, expect } from '@playwright/test'; +import * as path from 'path'; + +const authFile = path.join(__dirname, '.auth/user.json'); + +setup('authenticate', async ({ page }) => { + await page.goto('/wp-login.php'); + await page.waitForSelector('#user_login'); + + await page.fill('#user_login', 'admin'); + await page.fill('#user_pass', 'password'); + + await page.click('#wp-submit'); + + await page.waitForURL(/wp-admin/); + await page.waitForSelector('#wpbody-content, #adminmenu'); + + await expect(page.locator('#adminmenu')).toBeVisible(); + + await page.context().storageState({ path: authFile }); +}); diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets.spec.ts index 6495c220..d79fb599 100644 --- a/tests/e2e/code-snippets.spec.ts +++ b/tests/e2e/code-snippets.spec.ts @@ -3,68 +3,53 @@ import { test, expect } from '@playwright/test'; const TEST_SNIPPET_NAME = 'E2E Test Snippet'; test.describe('Code Snippets Plugin', () => { - test('Complete snippet workflow', async ({ page }) => { - await test.step('Login to WordPress', async () => { - await page.goto('/wp-login.php'); - await page.waitForSelector('#user_login'); - await page.fill('#user_login', 'admin'); - await page.fill('#user_pass', 'password'); - await page.click('#wp-submit'); - await page.waitForURL(/wp-admin/); - }); - - await test.step('Navigate to snippets page', async () => { - await page.goto('/wp-admin/admin.php?page=snippets'); - await page.waitForLoadState('networkidle'); - await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); + test.beforeEach(async ({ page }) => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); + }); - const currentUrl = page.url(); - expect(currentUrl).toContain('page=snippets'); - }); + test('Can access snippets admin page', async ({ page }) => { + const currentUrl = page.url(); + expect(currentUrl).toContain('page=snippets'); - await test.step('Add a new snippet', async () => { - await page.waitForSelector('h1, .page-title', { timeout: 10000 }); - await page.click('.page-title-action'); - await page.waitForLoadState('networkidle'); - - await page.waitForSelector('#title'); - await page.fill('#title', TEST_SNIPPET_NAME); + await expect(page.locator('h1, .page-title')).toBeVisible(); + }); - await page.waitForSelector('.CodeMirror textarea'); - await page.fill('.CodeMirror textarea', 'echo "Hello World!";'); + test('Can add a new snippet', async ({ page }) => { + await page.waitForSelector('h1, .page-title', { timeout: 10000 }); + await page.click('.page-title-action'); + await page.waitForLoadState('networkidle'); - await page.click('text=Save Snippet'); - await expect(page.locator('#message.notice')).toContainText('Snippet created'); - }); + await page.waitForSelector('#title'); + await page.fill('#title', TEST_SNIPPET_NAME); - await test.step('Activate the snippet', async () => { - await page.goto('/wp-admin/admin.php?page=snippets'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('.CodeMirror textarea'); + await page.fill('.CodeMirror textarea', 'echo "Hello World!";'); - await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); - await page.click(`text=${TEST_SNIPPET_NAME}`); - await page.waitForLoadState('networkidle'); + await page.click('text=Save Snippet'); + await expect(page.locator('#message.notice')).toContainText('Snippet created'); + }); - await page.click('text=Save and Activate'); - await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated'); - }); + test('Can activate and deactivate a snippet', async ({ page }) => { + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); - await test.step('Deactivate the snippet', async () => { - await page.click('text=Save and Deactivate'); - await expect(page.locator('#message.notice p')).toContainText('Snippet updated and deactivated'); - }); + await page.click('text=Save and Activate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and activated'); - await test.step('Delete the snippet', async () => { - await page.goto('/wp-admin/admin.php?page=snippets'); - await page.waitForLoadState('networkidle'); + await page.click('text=Save and Deactivate'); + await expect(page.locator('#message.notice p')).toContainText('Snippet updated and deactivated'); + }); - await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); - await page.click(`text=${TEST_SNIPPET_NAME}`); - await page.waitForLoadState('networkidle'); + test('Can delete a snippet', async ({ page }) => { + await page.waitForSelector(`text=${TEST_SNIPPET_NAME}`); + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); - await page.click('text=Delete'); - await page.click('button.components-button.is-destructive.is-primary'); - await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); - }); + await page.click('text=Delete'); + await page.click('button.components-button.is-destructive.is-primary'); + await expect(page.locator('body')).not.toContainText(TEST_SNIPPET_NAME); }); }); diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index 4e6860c2..5fe39026 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -1,5 +1,6 @@ /// import { defineConfig, devices } from '@playwright/test'; +import * as path from 'path'; /** * @see https://playwright.dev/docs/test-configuration @@ -23,11 +24,18 @@ export default defineConfig({ }, projects: [ + { + name: 'setup', + testMatch: /.*\.setup\.ts/, + }, + { name: 'chromium', use: { ...devices['Desktop Chrome'], + storageState: path.join(__dirname, '../e2e/.auth/user.json'), }, + dependencies: ['setup'], }, ], From 83934d3d409e758bfa423e8fde03939f290fb84f Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 10 Sep 2025 09:48:30 +0200 Subject: [PATCH 17/33] wip: add tests for snippet evaluation --- ...ts.spec.ts => code-snippets-admin.spec.ts} | 0 tests/e2e/code-snippets-evaluation.spec.ts | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+) rename tests/e2e/{code-snippets.spec.ts => code-snippets-admin.spec.ts} (100%) create mode 100644 tests/e2e/code-snippets-evaluation.spec.ts diff --git a/tests/e2e/code-snippets.spec.ts b/tests/e2e/code-snippets-admin.spec.ts similarity index 100% rename from tests/e2e/code-snippets.spec.ts rename to tests/e2e/code-snippets-admin.spec.ts diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts new file mode 100644 index 00000000..450f7911 --- /dev/null +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; + +const TEST_SNIPPET_NAME = 'E2E Admin Bar Hide Test'; + +test.describe('Code Snippets Evaluation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); + }); + + test('Can hide admin bar with activated snippet', async ({ page }) => { + await page.waitForSelector('h1, .page-title', { timeout: 10000 }); + await page.click('.page-title-action'); + await page.waitForLoadState('networkidle'); + + await page.waitForSelector('#title'); + await page.fill('#title', TEST_SNIPPET_NAME); + + await page.waitForSelector('.CodeMirror textarea'); + await page.fill('.CodeMirror textarea', "add_filter('show_admin_bar', '__return_false');"); + + await page.click('text=Save and Activate'); + await expect(page.locator('#message.notice')).toContainText('Snippet created and activated'); + + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('#wpadminbar')).not.toBeVisible(); + + const adminBarCount = await page.locator('#wpadminbar').count(); + expect(adminBarCount).toBe(0); + }); + + test.afterEach(async ({ page }) => { + // Clean up + await page.goto('/wp-admin/admin.php?page=snippets'); + await page.waitForLoadState('networkidle'); + + const snippetExists = await page.locator(`text=${TEST_SNIPPET_NAME}`).count(); + if (snippetExists > 0) { + await page.click(`text=${TEST_SNIPPET_NAME}`); + await page.waitForLoadState('networkidle'); + + await page.click('text=Delete'); + await page.click('button.components-button.is-destructive.is-primary'); + } + }); +}); From 485f15be47648ce133a94a00478f8e4cf1f7d20c Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 10 Sep 2025 09:49:51 +0200 Subject: [PATCH 18/33] wip --- tests/e2e/code-snippets-evaluation.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 450f7911..a7ae93dd 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -9,7 +9,7 @@ test.describe('Code Snippets Evaluation', () => { await page.waitForSelector('#wpbody-content, .wrap, #wpcontent', { timeout: 10000 }); }); - test('Can hide admin bar with activated snippet', async ({ page }) => { + test('PHP snippet is evaluating correctly', async ({ page }) => { await page.waitForSelector('h1, .page-title', { timeout: 10000 }); await page.click('.page-title-action'); await page.waitForLoadState('networkidle'); @@ -27,7 +27,7 @@ test.describe('Code Snippets Evaluation', () => { await page.waitForLoadState('networkidle'); await expect(page.locator('#wpadminbar')).not.toBeVisible(); - + const adminBarCount = await page.locator('#wpadminbar').count(); expect(adminBarCount).toBe(0); }); @@ -36,12 +36,12 @@ test.describe('Code Snippets Evaluation', () => { // Clean up await page.goto('/wp-admin/admin.php?page=snippets'); await page.waitForLoadState('networkidle'); - + const snippetExists = await page.locator(`text=${TEST_SNIPPET_NAME}`).count(); if (snippetExists > 0) { await page.click(`text=${TEST_SNIPPET_NAME}`); await page.waitForLoadState('networkidle'); - + await page.click('text=Delete'); await page.click('button.components-button.is-destructive.is-primary'); } From 262c15795f0e921d7f9f6f1051914dda24786fa9 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 10 Sep 2025 10:42:24 +0200 Subject: [PATCH 19/33] HTML snippet test --- .../fields/SnippetLocationInput.tsx | 2 +- tests/e2e/code-snippets-admin.spec.ts | 2 +- tests/e2e/code-snippets-evaluation.spec.ts | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx b/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx index ccc9c218..2a39ac32 100644 --- a/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx +++ b/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx @@ -52,7 +52,7 @@ export const SnippetLocationInput: React.FC = () => {