diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf5c4419..57617779 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,11 @@ on: required: false type: string default: ${{ inputs.ref }} + php: + required: false + type: number + default: 8.3 + outputs: version: value: ${{ jobs.build.outputs.version }} @@ -14,13 +19,16 @@ on: value: ${{ jobs.build.outputs.artifact_name }} artifact_url: value: ${{ jobs.build.outputs.artifact_url }} + artifact_id: + value: ${{ jobs.build.outputs.artifact_id }} jobs: build: runs-on: ubuntu-latest outputs: + version: ${{ steps.build.outputs.version }} artifact_name: ${{ steps.build.outputs.name }} artifact_url: ${{ steps.artifacts.outputs.artifact-url }} - version: ${{ steps.build.outputs.version }} + artifact_id: ${{ steps.artifacts.outputs.artifact-id }} steps: - uses: actions/checkout@v4 with: @@ -29,23 +37,29 @@ jobs: - name: Set up PHP uses: codesnippetspro/setup-php@v2 with: - php-version: "8.3" + php-version: "${{ inputs.php }}" - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: .node-version + cache: 'npm' - name: Install & Build id: build run: | npm install && npm run bundle - echo "name=$(jq -r .name package.json)" >> $GITHUB_OUTPUT - echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT + name=$(jq -r .name package.json) + echo "name=$name" >> $GITHUB_OUTPUT + echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT + + mkdir -p ./upload/$name + mv ./bundle/* ./upload/$name/ 2>/dev/null || true + - name: Upload id: artifacts uses: actions/upload-artifact@v4 with: name: ${{ steps.build.outputs.name }} - path: ./bundle + path: ./upload diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index d93f5f85..5867abd2 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -5,7 +5,9 @@ on: types: [closed] branches: - core - + - core-beta + - pro + - pro-beta jobs: create-tag: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'tag/v') @@ -13,17 +15,17 @@ jobs: steps: - name: Checkout main uses: actions/checkout@v4 - with: - ref: core - name: Get version from package.json id: version run: | - echo "tag=$(jq -r .version package.json)" >> $GITHUB_OUTPUT + tag=$(jq -r .version package.json) + echo "tag=$tag" >> $GITHUB_OUTPUT + echo "::info::Creating git tag [$tag]" - name: Create tag run: | git config user.name "code-snippets-bot" - git config user.email "sre@codesnippets.pro" + git config user.email "139164393+code-snippets-bot@users.noreply.github.com" git tag "v${{ steps.version.outputs.tag }}" git push origin "v${{ steps.version.outputs.tag }}" diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml new file mode 100644 index 00000000..7103c136 --- /dev/null +++ b/.github/workflows/playwright-test.yml @@ -0,0 +1,104 @@ +name: Playwright Test Runner + +on: + workflow_call: + inputs: + test-mode: + required: true + type: string + description: 'Test mode: default or file-based-execution' + project-name: + required: true + type: string + description: 'Playwright project name to run' + +jobs: + playwright-test: + name: Playwright tests (${{ inputs.test-mode == 'default' && 'Default Mode' || 'File-based Execution' }}) + runs-on: ubuntu-22.04 + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up PHP + uses: codesnippetspro/setup-php@v2 + with: + php-version: "8.1" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: 'npm' + + - name: Compute dependency hash + id: deps-hash + run: | + set -euo pipefail + tmpfile=$(mktemp) + for f in src/composer.lock package-lock.json; do + if [ -f "$f" ]; then + cat "$f" >> "$tmpfile" + fi + done + if [ -s "$tmpfile" ]; then + deps_hash=$(shasum -a 1 "$tmpfile" | awk '{print $1}' | cut -c1-8) + else + deps_hash=$(echo "${GITHUB_SHA:-unknown}" | cut -c1-8) + fi + echo "deps_hash=$deps_hash" >> "$GITHUB_OUTPUT" + + - name: Get build cache + id: deps-cache + uses: actions/cache/restore@v4 + with: + path: | + node_modules + src/vendor + key: ${{ runner.os }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + restore-keys: | + ${{ runner.os }}-deps- + + - name: Install workflow dependencies (wp-env, playwright) + if: steps.deps-cache.outputs.cache-hit != 'true' + run: npm run prepare-environment:ci && npm run bundle + + - name: Save vendor and node_modules cache + if: steps.deps-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + src/vendor + node_modules + key: ${{ runner.os }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + + - name: Start WordPress environment + run: | + npx wp-env start + + - name: Activate code-snippets plugin + run: npx wp-env run cli wp plugin activate code-snippets + + - name: WordPress debug information + run: | + npx wp-env run cli wp core version + npx wp-env run cli wp --info + + - name: Install playwright/test + run: | + npx playwright install chromium + + - name: Run Playwright tests + run: npm run test:playwright -- --project=${{ inputs.project-name }} + + - name: Stop WordPress environment + if: always() + run: npx wp-env stop + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-test-results-${{ inputs.test-mode }} + path: test-results/ + if-no-files-found: ignore + retention-days: 2 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..4d3274f8 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,54 @@ +name: "(Test): Playwright" + +on: + pull_request: + types: [labeled, synchronize, opened, reopened] + push: + branches: + - 'core' + - 'pro' + 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: + playwright-default: + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + uses: ./.github/workflows/playwright-test.yml + with: + test-mode: 'default' + project-name: 'chromium-db-snippets' + + playwright-file-based-execution: + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + uses: ./.github/workflows/playwright-test.yml + with: + test-mode: 'file-based-execution' + project-name: 'chromium-file-based-snippets' + + test-result: + needs: [playwright-default, playwright-file-based-execution] + if: always() && (needs.playwright-default.result != 'skipped' || needs.playwright-file-based-execution.result != 'skipped') + runs-on: ubuntu-22.04 + name: Playwright - Test Results Summary + steps: + - name: Test status summary + run: | + echo "Default Mode: ${{ needs.playwright-default.result }}" + echo "File-based Execution: ${{ needs.playwright-file-based-execution.result }}" + + - name: Check overall status + if: ${{ (needs.playwright-default.result != 'success' && needs.playwright-default.result != 'skipped') || (needs.playwright-file-based-execution.result != 'success' && needs.playwright-file-based-execution.result != 'skipped') }} + run: exit 1 diff --git a/.github/workflows/prepare-tag.yml b/.github/workflows/prepare-tag.yml index 680abc0d..20a3ba94 100644 --- a/.github/workflows/prepare-tag.yml +++ b/.github/workflows/prepare-tag.yml @@ -16,223 +16,137 @@ on: jobs: version: - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.version.outputs.version }} - steps: - - name: Checkout branch for release - uses: actions/checkout@v4 - - - name: Get tag version from package.json - id: version - run: | - INPUT_VERSION=${{ github.event.inputs.version }} - if [ -z "$INPUT_VERSION" ]; then - echo "::info:: No version input provided, defaulting to 'next'." - INPUT_VERSION="next" - fi - - if [ $INPUT_VERSION = "next" ]; then - CURR=$(jq -r .version package.json) - MAJOR=$(echo $CURR | cut -d. -f1) - MINOR=$(echo $CURR | cut -d. -f2) - PATCH=$(echo $CURR | cut -d. -f3) - NEW_PATCH=$((PATCH+1)) - NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" - VERSION="$NEW_VERSION" - else - VERSION="${{ github.event.inputs.version }}" - fi - - if [ -z "$VERSION" ]; then - echo "::error::Version is empty. Failing job." - exit 1 - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT + if: github.event.inputs.version == 'next' + uses: codesnippetspro/.github/.github/workflows/next_version.yml@main + with: + file_path: package.json + ref_name: ${{ github.ref_name }} changelog: + if: always() runs-on: ubuntu-latest needs: version - strategy: - matrix: - include: - - target-file: "CHANGELOG.md" - template: "changelog" - name: "github-changelog" - - target-file: "src/readme.txt" - template: "readme" - name: "wordpress-readme" steps: - - name: Dispatch changelog generation - id: dispatch + - name: Validate repository access + id: validate env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} run: | - # Install latest GitHub CLI - if ! command -v gh &> /dev/null; then - sudo mkdir -p -m 755 /etc/apt/keyrings - wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null - sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - sudo apt update && sudo apt install -y gh - else - # Check if gh version supports --json flag - if ! gh workflow list --help | grep -q "\--json"; then - echo "๐Ÿ“ฆ Updating GitHub CLI to latest version..." - sudo mkdir -p -m 755 /etc/apt/keyrings - wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null - sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - sudo apt update && sudo apt install -y gh - fi - fi - target_repo="codesnippetspro/.github-private" + workflow_file="changelog.yml" - # Try different possible workflow names for "Changelog Generate" - workflow_candidates=( - "(Changelog): Generate" - "Changelog Generate" - "Changelog: Generate" - "changelog" - "generate" - ) + echo "::notice::Validating access to $target_repo..." - workflow_id="" - workflow_name="" + # Test repository access + if ! gh repo view "$target_repo" >/dev/null 2>&1; then + echo "::error::Cannot access repository $target_repo" + echo "::error::Please ensure CHANGELOG_PAT secret is configured with access to private repositories" + exit 1 + fi - # Dynamically retrieve the workflow ID by trying different name patterns - echo "๐Ÿ” Searching for changelog workflow in $target_repo..." + echo "::notice::Repository access confirmed" - for candidate in "${workflow_candidates[@]}"; do - echo " Trying: '$candidate'" - if workflow_info=$(gh workflow list --repo "$target_repo" --json name,id | jq -r ".[] | select(.name == \"$candidate\") | .id"); then - if [ -n "$workflow_info" ] && [ "$workflow_info" != "null" ]; then - workflow_id="$workflow_info" - workflow_name="$candidate" - echo "โœ… Found workflow '$candidate' with ID: $workflow_id" - break - fi - fi - done - - # If exact match failed, try partial matching - if [ -z "$workflow_id" ]; then - echo "๐Ÿ” Trying partial name matching..." - if workflow_info=$(gh workflow list --repo "$target_repo" --json name,id | jq -r '.[] | select(.name | test("(?i)(changelog|generate)")) | "\(.id)|\(.name)"' | head -1); then - if [ -n "$workflow_info" ] && [ "$workflow_info" != "null" ]; then - workflow_id=$(echo "$workflow_info" | cut -d'|' -f1) - workflow_name=$(echo "$workflow_info" | cut -d'|' -f2) - echo "โœ… Found workflow '$workflow_name' with ID: $workflow_id" - fi - fi + # Verify workflow file exists + if ! gh workflow view "$workflow_file" --repo "$target_repo" >/dev/null 2>&1; then + echo "::error::Workflow file '$workflow_file' not found in $target_repo" + echo "::error::Expected: https://github.com/$target_repo/blob/main/.github/workflows/$workflow_file" + exit 1 fi - - if [ -z "$workflow_id" ]; then - echo "::error::No changelog workflow found in $target_repo" - echo "๐Ÿ” Available workflows:" - gh workflow list --repo "$target_repo" --json name,id | jq -r '.[] | " \(.name) (ID: \(.id))"' - echo "skip_monitoring=true" >> $GITHUB_OUTPUT - exit 0 - fi + echo "::notice::Workflow file '$workflow_file' found" + echo "target_repo=$target_repo" >> $GITHUB_OUTPUT + echo "workflow_file=$workflow_file" >> $GITHUB_OUTPUT - echo "โœ… Found workflow '$workflow_name' with ID: $workflow_id" + - name: Dispatch changelog workflow + id: dispatch + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + run: | + target_repo="${{ steps.validate.outputs.target_repo }}" + workflow_file="${{ steps.validate.outputs.workflow_file }}" + version="${{ needs.version.outputs.version || github.event.inputs.version }}" + + echo "::notice::Dispatching workflow '$workflow_file' to generate changelog and readme entries..." + echo " Calling repo: $target_repo" + echo " Version: $version" + echo " Changelog path: ${GITHUB_EVENT_INPUTS_CHANGELOG_PATH:-./CHANGELOG.md}" + echo " Readme path: ${GITHUB_EVENT_INPUTS_README_PATH:-./src/readme.txt}" - # Attempt to dispatch the workflow - if ! gh workflow run "$workflow_id" \ - --repo "$target_repo" --ref main \ + # Dispatch the workflow with required parameters + if ! gh workflow run "$workflow_file" \ + --repo "$target_repo" \ + --ref main \ --field repo="${{ github.repository }}" \ --field branch="${{ github.ref_name }}" \ - --field version="${{ needs.version.outputs.tag }}" \ - --field template="${{ matrix.template }}" \ - --field target-file="./${{ matrix.target-file }}"; then - echo "::error::Failed to dispatch workflow '$workflow_name' in $target_repo" + --field version="$version" \ + --field readme_path="./src/readme.txt" ; then + echo "::error::Failed to dispatch workflow '$workflow_file' in $target_repo" exit 1 fi - - echo "โœ… Successfully dispatched changelog generation for ${{ matrix.name }}" - # Wait a moment for the run to be created and get its URL - echo "โณ Getting workflow run URL..." - sleep 5 + echo "::notice::Successfully dispatched changelog generation" + + # Wait a moment for the run to be created + echo "Waiting for workflow run to be created..." + sleep 10 - # Get the most recent run URL for this workflow - if run_url=$(gh run list --repo "$target_repo" --workflow "$workflow_id" --limit 1 --json url -q '.[0].url' 2>/dev/null); then + # Get the workflow run URL for monitoring + if run_url=$(gh run list --repo "$target_repo" --workflow "$workflow_file" --limit 1 --json url -q '.[0].url' 2>/dev/null); then if [ -n "$run_url" ] && [ "$run_url" != "null" ]; then - echo "๐Ÿ”— View dispatched workflow run: $run_url" + echo "::notice::Workflow run: $run_url" + echo "run_url=$run_url" >> $GITHUB_OUTPUT fi fi - - echo "skip_monitoring=false" >> $GITHUB_OUTPUT - echo "workflow_id=$workflow_id" >> $GITHUB_OUTPUT - echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT - - name: Monitor workflow execution - if: steps.dispatch.outputs.skip_monitoring != 'true' + - name: Monitor workflow completion env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} run: | - workflow_name="${{ matrix.name }}" - workflow_id="${{ steps.dispatch.outputs.workflow_id }}" - target_repo="codesnippetspro/.github-private" - max_attempts=20 + target_repo="${{ steps.validate.outputs.target_repo }}" + workflow_file="${{ steps.validate.outputs.workflow_file }}" + max_attempts=30 attempt=0 - echo "Monitoring workflow: $workflow_name in repository: $target_repo" + echo "::notice::Monitoring workflow completion..." - # Check if the repository exists and is accessible - if ! gh repo view "$target_repo" >/dev/null 2>&1; then - echo "::warning::Repository $target_repo is not accessible or doesn't exist" - echo "::warning::Skipping monitoring for workflow: $workflow_name" - exit 0 - fi - - while : ; do + while [ $attempt -lt $max_attempts ]; do attempt=$((attempt + 1)) - # Check if we've exceeded max attempts - if [ $attempt -gt $max_attempts ]; then - echo "::warning::Timeout reached after $max_attempts attempts. Workflow '$workflow_name' monitoring stopped." - echo "::warning::This might be expected if testing locally or if the workflow doesn't exist yet." - exit 0 - fi - - # Try to get workflow status by changelog.yml first, then fallback to workflow ID - status="" - conclusion="" + # Get latest run status + run_data=$(gh run list \ + --repo "$target_repo" \ + --workflow "$workflow_file" \ + --limit 1 \ + --json status,conclusion,url \ + -q '.[0] | [.status, .conclusion, .url] | @tsv' 2>/dev/null) - # First attempt: Try using changelog.yml filename - if status=$(gh run list --workflow changelog.yml --limit 1 --json status -q '.[0].status' --repo "$target_repo" --ref main 2>/dev/null) && [ -n "$status" ]; then - conclusion=$(gh run list --workflow changelog.yml --limit 1 --json conclusion -q '.[0].conclusion' --repo "$target_repo" --ref main 2>/dev/null) - else - # Fallback: Try using workflow ID from previous step - if [ -n "$workflow_id" ]; then - echo "::info::Attempt $attempt/$max_attempts: changelog.yml not found, trying workflow ID $workflow_id" - run_data=$(gh run list --repo "$target_repo" --limit 10 --json status,conclusion,workflowDatabaseId 2>/dev/null | jq -r ".[] | select(.workflowDatabaseId == $workflow_id) | [.status, .conclusion] | @tsv" | head -1) - if [ -n "$run_data" ]; then - status=$(echo "$run_data" | cut -f1) - conclusion=$(echo "$run_data" | cut -f2) + if [ -n "$run_data" ]; then + status=$(echo "$run_data" | cut -f1) + conclusion=$(echo "$run_data" | cut -f2) + url=$(echo "$run_data" | cut -f3) + + if [ "$status" = "completed" ]; then + if [ "$conclusion" = "success" ]; then + echo "::notice::Workflow completed successfully!" + echo "::notice::Run details: $url" + break + else + echo "::error::Workflow failed with conclusion: $conclusion" + echo "::error::Run details: $url" + exit 1 fi + else + echo "Attempt $attempt/$max_attempts: Workflow status: $status" fi + else + echo "Attempt $attempt/$max_attempts: Waiting for workflow run data..." fi - # If we still couldn't get status, continue waiting - if [ -z "$status" ]; then - echo "::warning::Attempt $attempt/$max_attempts: Could not find workflow '$workflow_name' or no runs exist yet. Checking again..." - sleep 10 - continue - fi - - if [ "$status" = "completed" ]; then - if [ "$conclusion" != "success" ]; then - echo "::error::Workflow $workflow_name failed with conclusion: $conclusion" - exit 1 - fi - echo "โœ… Workflow $workflow_name completed successfully" - break + if [ $attempt -eq $max_attempts ]; then + echo "::warning::Timeout reached after $max_attempts attempts" + echo "::warning::Workflow may still be running. Check manually: ${{ steps.dispatch.outputs.run_url }}" + exit 0 fi - echo "โณ Attempt $attempt/$max_attempts: Workflow $workflow_name is still running (status: $status). Waiting..." sleep 10 done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42d516a4..8e7ee90b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,56 +4,89 @@ on: release: types: [created] +permissions: + contents: read + actions: read + jobs: - install: + build: uses: ./.github/workflows/build.yml with: ref: ${{ github.event.release.tag_name }} upload: - needs: install + needs: build runs-on: ubuntu-latest + permissions: + contents: write outputs: - artifact_url: ${{ steps.zip.outputs.path }} - artifact_name: ${{ needs.install.outputs.artifact_name }} + zip_name: ${{ steps.zip.outputs.zip_name }} steps: - - name: Get artifact url - id: artifact - run: | - url="${{ needs.install.outputs.artifact_url }}" - id="${url##*/}" - echo "id=$id" >> $GITHUB_OUTPUT - - name: Download built zip id: download uses: actions/download-artifact@v4 with: - artifact-ids: ${{ steps.artifact.outputs.id }} - path: ./dist + artifact-ids: ${{ needs.build.outputs.artifact_id }} + path: ./bundle - - name: Get artifact url + - name: Create zip archive id: zip run: | - zip_name="${{ needs.install.outputs.artifact_name }}.zip" + # zip filename format: ..zip + zip_name="${{ needs.build.outputs.artifact_name }}.${{ github.event.release.tag_name }}.zip" - cd ./dist/${{ needs.install.outputs.artifact_name }} + cd ./bundle/${{ needs.build.outputs.artifact_name }} zip -r "../$zip_name" . cd .. + echo "zip_name=$zip_name" >> $GITHUB_OUTPUT echo "path=$(pwd)/$zip_name" >> $GITHUB_OUTPUT - name: Upload release asset - uses: softprops/action-gh-release@v2 + id: upload + uses: codesnippetspro/action-gh-release@master with: files: ${{ steps.zip.outputs.path }} - # deploy: - # needs: [install, upload] - # uses: codesnippetspro/.github-private/.github/workflows/publish_svn.yml@v1 - # with: - # repo: ${{ github.repository }} - # branch: ${{ github.ref_name }} - # tag: ${{ needs.install.outputs.version}} - # slug: code-snippets - # artifact_url: ${{ needs.upload.outputs.artifact_url }} - # artifact_name: ${{ needs.upload.outputs.artifact_name }} + deploy: + needs: [build, upload] + runs-on: ubuntu-latest + steps: + - name: Dispatch publish workflow + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT }} + run: | + target_repo="codesnippetspro/.github-private" + workflow_file="publish.yml" + + echo "::notice::Dispatching publish workflow..." + echo " Repository: $target_repo" + echo " Source Repo: ${{ github.repository }}" + echo " Branch: ${{ github.ref_name }}" + echo " Tag: ${{ github.event.release.tag_name }}" + echo " Artifact Name: ${{ needs.build.outputs.artifact_name }}" + + # Dispatch the workflow with required parameters + if ! gh workflow run "$workflow_file" \ + --repo "$target_repo" \ + --ref main \ + --field repo="${{ github.repository }}" \ + --field branch="${{ github.ref_name }}" \ + --field tag="${{ github.event.release.tag_name }}" \ + --field zip_name="${{ needs.upload.outputs.zip_name }}"; then + echo "::error::Failed to dispatch publish workflow in $target_repo" + exit 1 + fi + + echo "::notice::Successfully dispatched publish workflow" + + # Wait a moment for the run to be created + echo "Waiting for workflow run to be created..." + sleep 10 + + # Get the workflow run URL for monitoring + if run_url=$(gh run list --repo "$target_repo" --workflow "$workflow_file" --limit 1 --json url -q '.[0].url' 2>/dev/null); then + echo "::notice::Monitor workflow progress at: $run_url" + echo "workflow_url=$run_url" >> $GITHUB_OUTPUT + fi + diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 00000000..d379731f --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,41 @@ +name: "(Sync): Core to Beta and Pro" + +on: + push: + branches: + - core + - pro + +jobs: + check-sync: + runs-on: ubuntu-latest + outputs: + should_sync: ${{ steps.check.outputs.should_sync }} + steps: + - name: Check if sync is required + id: check + run: | + COMMIT_MSG="${{ github.event.head_commit.message }}" + if [[ "$COMMIT_MSG" == *"(sync):"* ]]; then + echo "should_sync=true" >> $GITHUB_OUTPUT + echo "::notice::Commit message contains (sync): - triggering sync workflow" + else + echo "should_sync=false" >> $GITHUB_OUTPUT + echo "::notice::Commit message does not contain (sync): - skipping sync" + fi + + sync: + needs: check-sync + if: needs.check-sync.outputs.should_sync == 'true' + runs-on: ubuntu-latest + steps: + - name: Trigger downstream workflow via gh + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT }} + run: | + gh workflow run sync-code-snippets.yml \ + --repo codesnippetspro/.github-private --ref main \ + --field caller_repo="${{ github.repository }}" \ + --field caller_ref="${{ github.ref_name }}" \ + --field commit_sha="${{ github.sha }}" + echo "::notice::Dispatched sync workflow" diff --git a/.gitignore b/.gitignore index e42ed840..1c3b03cb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,12 @@ node_modules/ npm-debug.log .sass-cache/ +# Playwright +playwright-report/ +test-results/ +tests/e2e/.auth/ +auth.json + # 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/CHANGELOG.md b/CHANGELOG.md index 8f1dcd7e..78b6fd46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## [3.8.0] (2025-10-24) + +### Added +* @CarolinaOP and @louiswol94 join the team as plugin contributors. +* File-based execution mode for snippets (optional in plugin settings). +* Version switch option for easily rolling back the plugin to an earlier release. + +### Changed +* Prefixed Composer packages to reduce collisions with other plugins. +* Snippets REST API now supports pagination via page and per_page query parameters. +* Improved editor preview behavior. +* Minor UI improvements to the editor and sidebar. + +### Fixed +* Issues with snippet evaluation and front-end initialization in edge cases. +* Improved reliability of snippet evaluation. +* JavaScript and CSS snippets loading twice due to a conditions bug. (PRO) +* Issue where some conditions didnโ€™t work due to loading before the loop. (PRO) + +## [3.7.1-beta.3] (2025-10-22) + +### Added +* Snippets REST API now supports pagination via page and per_page query parameters. + +## [3.7.1-beta.2] (2025-10-22) + +### Added +* Implemented version switching with a new 'Version Switch' section in Settings + +## [3.7.1-beta.1] (2025-10-16) + +### Added +* Added @CarolinaOP and @louiswol94 as plugin contributors +* File-based execution mode for snippets (Optional in Plugin Settings) + +### Changed +* Minor UI/UX tweaks to the editor form and sidebar +* Improved editor preview behavior. + +### Fixed +* Improved reliability of snippet evaluation and front-end integration. +* Prefixed Composer packages to reduce collisions with other plugins, especially those using Guzzle. +* Functions conditions were loading before loop setup, resulting in some conditions not working. (PRO) +* JavaScript and CSS snippets loading twice due to a conditions bug. (PRO) + +### Removed +* Removed CSS linting within the editor until a modern replacement can be implemented. + ## [3.7.0] (2025-08-29) ### Added diff --git a/eslint.config.mjs b/eslint.config.mjs index 6360acc5..6a5cfefc 100755 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -135,5 +135,11 @@ export default eslintTs.config( objectLiteralTypeAssertions: 'never' }], } + }, + { + files: ['test/**', '**/*.test.*', '**/*.spec.*'], + rules: { + 'max-lines-per-function': 'off' + } } ) diff --git a/package-lock.json b/package-lock.json index 98d143df..0e993b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-snippets", - "version": "3.7.0", + "version": "3.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-snippets", - "version": "3.7.0", + "version": "3.8.0", "license": "GPL-2.0-or-later", "dependencies": { "@codemirror/fold": "^0.19.4", @@ -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", @@ -43,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", @@ -2467,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" @@ -2581,6 +2598,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, @@ -2591,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", @@ -2680,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", @@ -2744,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, @@ -2783,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, @@ -2822,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" @@ -2877,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, @@ -3566,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", @@ -3888,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, @@ -4596,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, @@ -4612,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", @@ -4755,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, @@ -4795,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", @@ -4804,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, @@ -4828,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", @@ -4934,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", @@ -4948,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, @@ -5355,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, @@ -5367,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, @@ -5443,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, @@ -5556,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, @@ -6226,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, @@ -6280,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" @@ -6332,33 +6759,66 @@ "reusify": "^1.0.4" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", + "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, - "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "pend": "~1.2.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", + "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, - "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "escape-string-regexp": "^1.0.5" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-root": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/find-up": { + "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, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/find-up": { "version": "5.0.0", "dev": true, "license": "MIT", @@ -6488,6 +6948,26 @@ } } }, + "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", + "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", @@ -6530,6 +7010,15 @@ "node": ">=6.9.0" } }, + "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, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "dev": true, @@ -6565,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, @@ -6821,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, @@ -6959,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", @@ -7052,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, @@ -7064,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, @@ -7286,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, @@ -7763,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", @@ -7780,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, @@ -7880,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, @@ -7918,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", @@ -7945,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, @@ -8007,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, @@ -8123,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, @@ -8139,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, @@ -8155,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, @@ -8254,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, @@ -8305,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" @@ -8383,6 +9207,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, @@ -9231,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, @@ -9263,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, @@ -9540,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, @@ -9563,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, @@ -9597,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, @@ -9606,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, @@ -9623,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, @@ -9651,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, @@ -9988,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", @@ -10856,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, @@ -10956,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, @@ -11096,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, @@ -11166,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, @@ -11389,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, @@ -11685,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", @@ -11702,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", @@ -11716,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, diff --git a/package.json b/package.json index 2c5dbb32..f29669ba 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,21 @@ "name": "code-snippets", "description": "Manage code snippets running on a WordPress-powered site through a graphical interface.", "homepage": "https://codesnippets.pro", - "version": "3.7.0", + "version": "3.8.0", "main": "src/dist/edit.js", "directories": { "test": "tests" }, "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/scripts/version.ts b/scripts/version.ts index 62258deb..c5b70e01 100644 --- a/scripts/version.ts +++ b/scripts/version.ts @@ -11,12 +11,14 @@ const replaceInFile = (filename: string, transform: (contents: string) => string replaceInFile( 'src/code-snippets.php', contents => contents - .replace(/(?Version:\s+|@version\s+)\d+\.\d+[\w-.]+$/mg, `$1${plugin.version}`) + .replace(/(?Version:\s+|@version\s+)\d+\.\d+\.\d+[\w-.]*$/mg, `$1${plugin.version}`) .replace(/(?'CODE_SNIPPETS_VERSION',\s+)'[\w-.]+'/, `$1'${plugin.version}'`) ) -replaceInFile( - 'src/readme.txt', - contents => contents - .replace(/(?Stable tag:\s+|@version\s+)\d+\.\d+[\w-.]+$/mg, `$1${plugin.version}`) -) +if (!/beta/i.test(plugin.version)) { + replaceInFile( + 'src/readme.txt', + contents => contents + .replace(/(?Stable tag:\s+|@version\s+)\d+\.\d+[\w-.]+$/mg, `$1${plugin.version}`) + ) +} diff --git a/src/code-snippets.php b/src/code-snippets.php index d5a7ce77..b1ebb98d 100644 --- a/src/code-snippets.php +++ b/src/code-snippets.php @@ -8,11 +8,11 @@ * License: GPL-2.0-or-later * License URI: license.txt * Text Domain: code-snippets - * Version: 3.7.0 + * Version: 3.8.0 * Requires PHP: 7.4 * Requires at least: 5.0 * - * @version 3.7.0 + * @version 3.8.0 * @package Code_Snippets * @author Shea Bunge * @copyright 2012-2024 Code Snippets Pro @@ -37,7 +37,7 @@ * * @const string */ - define( 'CODE_SNIPPETS_VERSION', '3.7.0' ); + define( 'CODE_SNIPPETS_VERSION', '3.8.0' ); /** * The full path to the main file of this plugin. diff --git a/src/composer.json b/src/composer.json index f08c96b3..882d16a0 100644 --- a/src/composer.json +++ b/src/composer.json @@ -29,7 +29,8 @@ "php": ">=7.4", "ext-dom": "*", "ext-json": "*", - "composer/installers": "^2.3" + "composer/installers": "^2.3", + "typisttech/imposter-plugin": "^0.6.2" }, "require-dev": { "wp-coding-standards/wpcs": "^3.1", @@ -42,7 +43,13 @@ }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "composer/installers": true + "composer/installers": true, + "typisttech/imposter-plugin": true + } + }, + "extra": { + "imposter": { + "namespace": "Code_Snippets\\Vendor" } } } diff --git a/src/composer.lock b/src/composer.lock index ee0fbc95..2914eafd 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af21aa57ee761a9e969a806ede3a5073", + "content-hash": "c322fb32f6db8844392d9f78341fcefb", "packages": [ { "name": "composer/installers", @@ -151,6 +151,180 @@ } ], "time": "2024-06-24T20:46:46+00:00" + }, + { + "name": "typisttech/imposter", + "version": "0.6.1", + "source": { + "type": "git", + "url": "https://github.com/TypistTech/imposter.git", + "reference": "f52b1a2289d2ea9c660cf9595085d0b11469af83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TypistTech/imposter/zipball/f52b1a2289d2ea9c660cf9595085d0b11469af83", + "reference": "f52b1a2289d2ea9c660cf9595085d0b11469af83", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "codeception/codeception": "^4.1", + "codeception/mockery-module": "^0.4.0", + "codeception/module-asserts": "^1.3", + "codeception/module-filesystem": "^1.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "typisttech/imposter-plugin": "Composer plugin to integrate composer and imposter" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "TypistTech\\Imposter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Typist Tech", + "email": "imposter@typist.tech", + "homepage": "https://typist.tech" + }, + { + "name": "Tang Rufus", + "email": "tangrufus@gmail.com", + "homepage": "https://typist.tech", + "role": "Developer" + } + ], + "description": "Wrapping all composer vendor packages inside your own namespace. Intended for WordPress plugins.", + "homepage": "https://github.com/TypistTech/imposter", + "keywords": [ + "composer", + "dependency", + "monkey-patching", + "namespace", + "wordpress" + ], + "support": { + "email": "imposter@typist.tech", + "issues": "https://github.com/TypistTech/imposter/issues", + "source": "https://github.com/TypistTech/imposter" + }, + "funding": [ + { + "url": "https://typist.tech/donation/", + "type": "custom" + }, + { + "url": "https://www.paypal.me/iAmTangRufus/30usd", + "type": "custom" + }, + { + "url": "https://github.com/tangrufus", + "type": "github" + } + ], + "time": "2020-12-06T22:57:09+00:00" + }, + { + "name": "typisttech/imposter-plugin", + "version": "0.6.2", + "source": { + "type": "git", + "url": "https://github.com/TypistTech/imposter-plugin.git", + "reference": "15fa3c90aca3b79497f438b9e02a6176498de53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TypistTech/imposter-plugin/zipball/15fa3c90aca3b79497f438b9e02a6176498de53c", + "reference": "15fa3c90aca3b79497f438b9e02a6176498de53c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "php": "^7.3 || ^8.0", + "typisttech/imposter": "^0.6.1" + }, + "require-dev": { + "codeception/codeception": "^4.1", + "codeception/module-asserts": "^1.3", + "codeception/module-cli": "^1.1", + "codeception/module-filesystem": "^1.0", + "composer/composer": "^1.10.19 || ^2.0", + "squizlabs/php_codesniffer": "^3.5", + "typisttech/codeception-composer-project-module": "^0.1.1" + }, + "type": "composer-plugin", + "extra": { + "class": "TypistTech\\Imposter\\Plugin\\ImposterPlugin", + "branch-alias": { + "dev-master": "0.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "TypistTech\\Imposter\\Plugin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Typist Tech", + "email": "imposter-plugin@typist.tech", + "homepage": "https://www.typist.tech" + }, + { + "name": "Tang Rufus", + "email": "tangrufus@gmail.com", + "homepage": "https://www.typist.tech", + "role": "Developer" + } + ], + "description": "Composer plugin that wraps all composer vendor packages inside your own namespace. Intended for WordPress plugins.", + "homepage": "https://github.com/TypistTech/imposter-plugin", + "keywords": [ + "composer", + "composer-plugin", + "dependency", + "monkey-patching", + "namespace", + "wordpress" + ], + "support": { + "email": "imposter-plugin@typist.tech", + "issues": "https://github.com/TypistTech/imposter-plugin/issues", + "source": "https://github.com/TypistTech/imposter-plugin" + }, + "funding": [ + { + "url": "https://typist.tech/donation/", + "type": "custom" + }, + { + "url": "https://www.paypal.me/iAmTangRufus/30usd", + "type": "custom" + }, + { + "url": "https://github.com/tangrufus", + "type": "github" + } + ], + "time": "2020-12-06T23:41:30+00:00" } ], "packages-dev": [ diff --git a/src/css/edit/_form.scss b/src/css/edit/_form.scss index 5cd6dbb8..45ead76f 100644 --- a/src/css/edit/_form.scss +++ b/src/css/edit/_form.scss @@ -74,6 +74,9 @@ $sidebar-gap: 30px; .snippet-editor-sidebar { grid-area: span 3 / sidebar; max-inline-size: $sidebar-width; + position: sticky; + inset-block-start: 32px; + align-self: start; } @media (width <= 1024px) { diff --git a/src/css/edit/_sidebar.scss b/src/css/edit/_sidebar.scss index 103832ab..919ce70e 100644 --- a/src/css/edit/_sidebar.scss +++ b/src/css/edit/_sidebar.scss @@ -1,5 +1,11 @@ @use '../common/theme'; +.code-snippets-modal { + p h4 { + margin-block-start: 0; + } +} + .snippet-editor-sidebar { .button-large { block-size: 48px; @@ -147,9 +153,3 @@ p.submit { block-size: 12px; } } - -.code-snippets-modal { - p h4 { - margin-block-start: 0; - } -} diff --git a/src/css/menu.scss b/src/css/menu.scss index d9126e1d..8c371705 100644 --- a/src/css/menu.scss +++ b/src/css/menu.scss @@ -1,5 +1,5 @@ #adminmenu { - .toplevel_page_snippets div.wp-menu-image:before { + .toplevel_page_snippets div.wp-menu-image::before { content: ''; mask-image: url('../assets/menu-icon.svg'); mask-repeat: no-repeat; diff --git a/src/css/settings.scss b/src/css/settings.scss index ffde5fe8..e2a040ae 100644 --- a/src/css/settings.scss +++ b/src/css/settings.scss @@ -1,6 +1,6 @@ @use 'common/codemirror'; -$sections: general, editor, debug; +$sections: general, editor, debug, version-switch; p.submit { display: flex; @@ -127,3 +127,87 @@ body.js { .cloud-settings tbody tr:nth-child(n+5) { display: none; } + +// Version Switch Styles +.code-snippets-version-switch { + .current-version { + font-family: monospace; + font-size: 1.1em; + font-weight: bold; + color: #0073aa; + background: #f0f6fc; + padding: 2px 8px; + border-radius: 3px; + border: 1px solid #c3c4c7; + } + + #target_version { + min-inline-size: 200px; + margin-inline-start: 8px; + } + + #switch-version-btn { + &[disabled] { + opacity: 0.6; + cursor: not-allowed; + background-color: #f0f0f1 !important; + color: #a7aaad !important; + border-color: #dcdcde !important; + } + } + + // Warning box styling + #version-switch-warning { + margin-block-start: 20px !important; + padding: 12px 16px; + border-inline-start: 4px solid #dba617; + background: #fff8e5; + border-radius: 4px; + + p { + margin: 0; + color: #8f6914; + + strong { + color: #8f6914; + } + } + } + + #version-switch-result { + margin-block-start: 12px; + + &.notice { + padding: 8px 12px; + border-radius: 4px; + } + } + + .notice { + &.notice { + &-success { + border-inline-start-color: #00a32a; + } + + &-error { + border-inline-start-color: #d63638; + } + + &-warning { + border-inline-start-color: #dba617; + } + + &-info { + border-inline-start-color: #72aee6; + } + } + } +} + +.version-switch-settings { + .form-table { + th { + inline-size: 180px; + } + } +} diff --git a/src/js/components/EditorSidebar/EditorSidebar.tsx b/src/js/components/EditorSidebar/EditorSidebar.tsx index f22be2c8..f9808cd4 100644 --- a/src/js/components/EditorSidebar/EditorSidebar.tsx +++ b/src/js/components/EditorSidebar/EditorSidebar.tsx @@ -6,7 +6,6 @@ import { isNetworkAdmin } from '../../utils/screen' import { isCondition } from '../../utils/snippets/snippets' import { ConditionModalButton } from '../ConditionModal/ConditionModalButton' import { SnippetLocationInput } from '../SnippetForm/fields/SnippetLocationInput' -import { SnippetTypeInput } from '../SnippetForm/fields/SnippetTypeInput' import { Notices } from '../SnippetForm/page/Notices' import { ShortcodeInfo } from './actions/ShortcodeInfo' import { MultisiteSharingSettings } from './controls/MultisiteSharingSettings' 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 = () => {

> + + + + + +

+ +

+ +

+ + + + __( 'You do not have permission to update plugins.', 'code-snippets' ), + ] ); + } + + $target_version = sanitize_text_field( $_POST['target_version'] ?? '' ); + + if ( empty( $target_version ) ) { + wp_send_json_error( [ + 'message' => __( 'No target version specified.', 'code-snippets' ), + ] ); + } + + $result = self::handle_version_switch( $target_version ); + + if ( $result['success'] ) { + wp_send_json_success( $result ); + } else { + wp_send_json_error( $result ); + } + } + + public static function render_refresh_versions_field( array $args ): void { + ?> + +

+ +

__( 'You do not have permission to manage options.', 'code-snippets' ), + ] ); + } + + delete_transient( VERSION_CACHE_KEY ); + self::get_available_versions(); + + wp_send_json_success( [ + 'message' => __( 'Available versions updated successfully.', 'code-snippets' ), + ] ); + } + + public static function render_version_switch_warning(): void { + ?> + + admin_url( 'admin-ajax.php' ), + 'nonce_switch' => wp_create_nonce( 'code_snippets_version_switch' ), + 'nonce_refresh' => wp_create_nonce( 'code_snippets_refresh_versions' ), + ); + + $strings = array( + 'selectDifferent' => esc_html__( 'Please select a different version to switch to.', 'code-snippets' ), + 'switching' => esc_html__( 'Switching...', 'code-snippets' ), + 'processing' => esc_html__( 'Processing version switch. Please wait...', 'code-snippets' ), + 'error' => esc_html__( 'An error occurred.', 'code-snippets' ), + 'errorSwitch' => esc_html__( 'An error occurred while switching versions. Please try again.', 'code-snippets' ), + 'refreshing' => esc_html__( 'Refreshing...', 'code-snippets' ), + 'refreshed' => esc_html__( 'Refreshed!', 'code-snippets' ), + ); + + wp_add_inline_script( 'code-snippets-settings-menu', 'var code_snippets_version_switch = ' . wp_json_encode( $version_switch ) . '; var __code_snippets_i18n = ' . wp_json_encode( $strings ) . ';', 'before' ); } /** diff --git a/src/php/settings/settings-fields.php b/src/php/settings/settings-fields.php index 9c4208ff..50312ac0 100644 --- a/src/php/settings/settings-fields.php +++ b/src/php/settings/settings-fields.php @@ -37,6 +37,7 @@ function get_default_settings(): array { 'indent_with_tabs' => true, 'tab_size' => 4, 'indent_unit' => 4, + 'font_size' => 14, 'wrap_lines' => true, 'code_folding' => true, 'line_numbers' => true, @@ -46,6 +47,12 @@ function get_default_settings(): array { 'keymap' => 'default', 'theme' => 'default', ], + 'version-switch' => [ + 'selected_version' => '', + ], + 'debug' => [ + 'enable_version_change' => false, + ], ]; $defaults = apply_filters( 'code_snippets_settings_defaults', $defaults ); @@ -79,6 +86,29 @@ function get_settings_fields(): array { 'type' => 'action', 'desc' => __( 'Use this button to manually clear snippets caches.', 'code-snippets' ), ], + 'enable_version_change' => [ + 'name' => __( 'Version Change', 'code-snippets' ), + 'type' => 'checkbox', + 'label' => __( 'Enable the ability to switch or rollback versions of the Code Snippets core plugin.', 'code-snippets' ), + ], + ]; + + $fields['version-switch'] = [ + 'version_switcher' => [ + 'name' => __( 'Switch Version', 'code-snippets' ), + 'type' => 'callback', + 'render_callback' => [ '\\Code_Snippets\\Settings\\Version_Switch', 'render_version_switch_field' ], + ], + 'refresh_versions' => [ + 'name' => __( 'Refresh Versions', 'code-snippets' ), + 'type' => 'callback', + 'render_callback' => [ '\\Code_Snippets\\Settings\\Version_Switch', 'render_refresh_versions_field' ], + ], + 'version_warning' => [ + 'name' => '', + 'type' => 'callback', + 'render_callback' => [ '\\Code_Snippets\\Settings\\Version_Switch', 'render_version_switch_warning' ], + ], ]; $fields['general'] = [ @@ -161,12 +191,21 @@ function get_settings_fields(): array { 'codemirror' => 'indentUnit', 'min' => 0, ], + 'font_size' => [ + 'name' => __( 'Font Size', 'code-snippets' ), + 'type' => 'number', + 'label' => _x( 'px', 'unit', 'code-snippets' ), + 'codemirror' => 'fontSize', + 'min' => 8, + 'max' => 28, + ], 'wrap_lines' => [ 'name' => __( 'Wrap Lines', 'code-snippets' ), 'type' => 'checkbox', 'label' => __( 'Soft-wrap long lines of code instead of horizontally scrolling.', 'code-snippets' ), 'codemirror' => 'lineWrapping', ], + 'code_folding' => [ 'name' => __( 'Code Folding', 'code-snippets' ), 'type' => 'checkbox', diff --git a/src/php/settings/settings.php b/src/php/settings/settings.php index cf94f8b9..249bdc24 100644 --- a/src/php/settings/settings.php +++ b/src/php/settings/settings.php @@ -136,11 +136,17 @@ function update_setting( string $section, string $field, $new_value ): bool { */ function get_settings_sections(): array { $sections = array( - 'general' => __( 'General', 'code-snippets' ), - 'editor' => __( 'Code Editor', 'code-snippets' ), - 'debug' => __( 'Debug', 'code-snippets' ), + 'general' => __( 'General', 'code-snippets' ), + 'editor' => __( 'Code Editor', 'code-snippets' ), + 'debug' => __( 'Debug', 'code-snippets' ), ); + // Only show the Version section when the debug setting to enable version changes is enabled. + $enable_version = get_setting( 'debug', 'enable_version_change' ); + if ( $enable_version ) { + $sections['version-switch'] = __( 'Version', 'code-snippets' ); + } + return apply_filters( 'code_snippets_settings_sections', $sections ); } @@ -168,8 +174,13 @@ function register_plugin_settings() { add_settings_section( $section_id, $section_name, '__return_empty_string', 'code-snippets' ); } - // Register settings fields. + // Register settings fields. Only register fields for sections that exist (some sections may be gated by settings). + $registered_sections = get_settings_sections(); foreach ( get_settings_fields() as $section_id => $fields ) { + if ( ! isset( $registered_sections[ $section_id ] ) ) { + continue; + } + foreach ( $fields as $field_id => $field ) { $field_object = new Setting_Field( $section_id, $field_id, $field ); add_settings_field( $field_id, $field['name'], [ $field_object, 'render' ], 'code-snippets', $section_id ); @@ -330,6 +341,8 @@ function sanitize_settings( array $input ): array { __( 'Settings saved.', 'code-snippets' ), 'updated' ); + + do_action( 'code_snippets/settings_updated', $settings, $input ); } return $settings; diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 10581eb3..9548c923 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -296,10 +296,12 @@ function activate_snippet( int $id, ?bool $network = null ) { // translators: %d: snippet identifier. return sprintf( __( 'Could not locate snippet with ID %d.', 'code-snippets' ), $id ); } - - $validator = new Validator( $snippet->code ); - if ( $validator->validate() ) { - return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' ); + + if('php' == $snippet->type ){ + $validator = new Validator( $snippet->code ); + if ( $validator->validate() ) { + return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' ); + } } $result = $wpdb->update( @@ -315,7 +317,7 @@ function activate_snippet( int $id, ?bool $network = null ) { } update_shared_network_snippets( [ $snippet ] ); - do_action( 'code_snippets/activate_snippet', $snippet ); + do_action( 'code_snippets/activate_snippet', $snippet, $network ); clean_snippets_cache( $table_name ); return $snippet; } @@ -393,7 +395,7 @@ function deactivate_snippet( int $id, ?bool $network = null ): ?Snippet { $network = DB::validate_network_param( $network ); $table = code_snippets()->db->get_table_name( $network ); - // Set the snippet to active. + // Set the snippet to inactive. $result = $wpdb->update( $table, array( 'active' => '0' ), @@ -434,6 +436,8 @@ function delete_snippet( int $id, ?bool $network = null ): bool { $network = DB::validate_network_param( $network ); $table = code_snippets()->db->get_table_name( $network ); + $snippet = get_snippet( $id, $network ); + $result = $wpdb->delete( $table, array( 'id' => $id ), @@ -441,7 +445,7 @@ function delete_snippet( int $id, ?bool $network = null ): bool { ); if ( $result ) { - do_action( 'code_snippets/delete_snippet', $id, $network ); + do_action( 'code_snippets/delete_snippet', $snippet, $network ); clean_snippets_cache( $table ); code_snippets()->cloud_api->delete_snippet_from_transient_data( $id ); } @@ -672,3 +676,17 @@ function update_snippet_fields( int $snippet_id, array $fields, ?bool $network = do_action( 'code_snippets/update_snippet', $snippet->id, $table ); clean_snippets_cache( $table ); } + +function execute_snippet_from_flat_file( $code, $file, int $id = 0, bool $force = false ) { + if ( ! is_file( $file ) ) { + return execute_snippet( $code, $id, $force ); + } + + if ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) { + return false; + } + + require_once $file; + + do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id ); +} diff --git a/src/php/uninstall.php b/src/php/uninstall.php index 4da58cfe..5afaefb6 100644 --- a/src/php/uninstall.php +++ b/src/php/uninstall.php @@ -18,7 +18,7 @@ function complete_uninstall_enabled(): bool { $unified = false; if ( is_multisite() ) { - $menu_perms = get_site_option( 'menu_items', array() ); + $menu_perms = get_site_option( 'menu_items', [] ); $unified = empty( $menu_perms['snippets_settings'] ); } @@ -72,6 +72,25 @@ function uninstall_multisite() { delete_site_option( 'recently_activated_snippets' ); } +function delete_flat_files_directory() { + $flat_files_dir = WP_CONTENT_DIR . '/code-snippets'; + + if ( ! is_dir( $flat_files_dir ) ) { + return; + } + + if ( ! function_exists( 'request_filesystem_credentials' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + global $wp_filesystem; + WP_Filesystem(); + + if ( $wp_filesystem && $wp_filesystem->is_dir( $flat_files_dir ) ) { + $wp_filesystem->delete( $flat_files_dir, true ); + } +} + /** * Uninstall the Code Snippets plugin. * @@ -85,5 +104,7 @@ function uninstall_plugin() { } else { uninstall_current_site(); } + + delete_flat_files_directory(); } } diff --git a/src/readme.txt b/src/readme.txt index 5798c93e..6d75f233 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -1,10 +1,10 @@ === Code Snippets === -Contributors: bungeshea, ver3, lightbulbman, 0aksmith, johnpixle +Contributors: bungeshea, ver3, lightbulbman, 0aksmith, johnpixle, louiswol94, carolinaop Donate link: https://codesnippets.pro Tags: code, snippets, multisite, php, css License: GPL-2.0-or-later License URI: license.txt -Stable tag: 3.7.0 +Stable tag: 3.8.0 Tested up to: 6.8.2 An easy, clean and simple way to enhance your site with code snippets. @@ -104,6 +104,31 @@ You can report security bugs found in the source code of this plugin through the == Changelog == + + + += 3.8.0 (2025-10-24) = + +__Added__ + +* @CarolinaOP and @louiswol94 join the team as plugin contributors. +* File-based execution mode for snippets (Optional in Plugin Settings). +* Version switch option, to help easily rollback the plugin to an earlier release. +* Minor UI improvements to the editor and sidebar. + +__Changed__ + +* Prefixed Composer packages to reduce collisions with other plugins. +* Snippets REST API now supports pagination via page and per_page query parameters. +* Improved editor preview behavior. + +__Fixed__ + +* Fixed issues with snippet evaluation and front-end initialization in edge cases. +* Improved reliability of snippet evaluation. +* JavaScript and CSS snippets loading twice due to a conditions bug. (PRO) +* Fixed issue where some conditions didnโ€™t work due to loading before the loop. (PRO) + = 3.7.0 (2025-08-29) = __Added__ 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/README.md b/tests/e2e/README.md new file mode 100644 index 00000000..2333b60c --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,61 @@ +# Playwright Tests + +End-to-end tests for Code Snippets using `@wordpress/env`. + +## Prerequisites + +- Docker (for wp-env) +- Node.js 18+ + +## Quick Start + +```bash +# Install dependencies +npm install + +# Build plugin and install PHP dependencies +npm run build && cd src && composer install && cd .. + +# Start WordPress environment +npm run wp-env:start + +# Run tests +npm run test:playwright +``` + +## Commands + +```bash +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 +``` + +## CI/CD + +Tests run automatically on: +- Pull requests with `run-tests` label +- Push to `core` branch +- Manual workflow dispatch + +## Troubleshooting + +**Docker not running:** +```bash +docker --version && docker ps +``` + +**WordPress won't start:** +```bash +lsof -i :8888 # Check port availability +npm run wp-env:stop && npm run wp-env:start +``` + +**Tests failing:** +```bash +npm run test:playwright:debug +curl http://localhost:8888/wp-admin/ # Check WordPress +``` \ No newline at end of file diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts new file mode 100644 index 00000000..59a5c39c --- /dev/null +++ b/tests/e2e/auth.setup.ts @@ -0,0 +1,21 @@ +import { join } from 'path' +import { expect, test as setup } from '@playwright/test' + +const authFile = 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-edit.spec.ts b/tests/e2e/code-snippets-edit.spec.ts new file mode 100644 index 00000000..de1204b2 --- /dev/null +++ b/tests/e2e/code-snippets-edit.spec.ts @@ -0,0 +1,41 @@ +import { test } from '@playwright/test' +import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' +import { MESSAGES } from './helpers/constants' + +const TEST_SNIPPET_NAME = 'E2E Test Snippet' + +test.describe('Code Snippets Admin', () => { + let helper: SnippetsTestHelper + + test.beforeEach(async ({ page }) => { + helper = new SnippetsTestHelper(page) + await helper.navigateToSnippetsAdmin() + }) + + test('Can access snippets admin page', async () => { + await helper.expectToBeOnSnippetsAdminPage() + }) + + test('Can add a new snippet', async () => { + await helper.createSnippet({ + name: TEST_SNIPPET_NAME, + code: "add_filter('show_admin_bar', '__return_false');" + }) + }) + + test('Can activate and deactivate a snippet', async () => { + await helper.openSnippet(TEST_SNIPPET_NAME) + + await helper.saveSnippet('save_and_activate') + await helper.expectSuccessMessageInParagraph(MESSAGES.SNIPPET_UPDATED_AND_ACTIVATED) + + await helper.saveSnippet('save_and_deactivate') + await helper.expectSuccessMessageInParagraph(MESSAGES.SNIPPET_UPDATED_AND_DEACTIVATED) + }) + + test('Can delete a snippet', async () => { + await helper.openSnippet(TEST_SNIPPET_NAME) + await helper.deleteSnippet() + await helper.expectTextNotVisible(TEST_SNIPPET_NAME) + }) +}) diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts new file mode 100644 index 00000000..48f11e93 --- /dev/null +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -0,0 +1,176 @@ +import util from 'util' +import { exec } from 'child_process' +import { expect, test } from '@playwright/test' +import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' +import { SELECTORS } from './helpers/constants' +import type { Page } from '@playwright/test' + +const TEST_SNIPPET_NAME = 'E2E Snippet Test' + +const BODY_CLASS_TEST_CODE = ` + add_filter('admin_body_class', function($classes) { + return $classes . ' custom-admin-class'; + }); + + add_filter('body_class', function($classes) { + $classes[] = 'custom-frontend-class'; + return $classes; + }); +` + +const verifyShortcodeRendersCorrectly = async ( + helper: SnippetsTestHelper, + page: Page, + pageUrl: string +): Promise => { + await page.goto(pageUrl) + + await expect(page.locator('.custom-snippet-content')).toBeVisible() + await expect(page.locator('.custom-snippet-content h3')).toContainText('Custom HTML Content') + await expect(page.locator('.custom-snippet-content p')).toContainText('This content was inserted via shortcode!') + + await helper.expectTextVisible('Page content before shortcode.') + await helper.expectTextVisible('Page content after shortcode.') +} + +const createPageWithShortcode = async (snippetId: string): Promise => { + const execAsync = util.promisify(exec) + + const shortcode = `[code_snippet id=${snippetId} format name="${TEST_SNIPPET_NAME}"]` + const pageContent = `

Page content before shortcode.

\n\n${shortcode}\n\n

Page content after shortcode.

` + + try { + const createPageCmd = [ + 'npx wp-env run cli wp post create', + '--post_type=page', + '--post_title="Test Page for Snippet Shortcode"', + `--post_content='${pageContent}'`, + '--post_status=publish', + '--porcelain' + ].join(' ') + + const { stdout } = await execAsync(createPageCmd) + const pageId = stdout.trim() + + const getUrlCmd = `npx wp-env run cli wp post url ${pageId}` + const { stdout: pageUrl } = await execAsync(getUrlCmd) + return pageUrl.trim() + } catch (error) { + console.error('Failed to create page via WP-CLI:', error) + throw error + } +} + +const createHtmlSnippetForEditor = async (helper: SnippetsTestHelper, page: Page): Promise => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + code: '
' + + '

Custom HTML Content

This content was inserted via shortcode!

', + type: 'HTML', + location: 'IN_EDITOR' + }) + + const currentUrl = page.url() + const urlMatch = /[?&]id=(?\d+)/.exec(currentUrl) + expect(urlMatch).toBeTruthy() + return urlMatch?.groups?.id ?? '0' +} + +test.describe('Code Snippets Evaluation', () => { + let helper: SnippetsTestHelper + + test.beforeEach(async ({ page }) => { + helper = new SnippetsTestHelper(page) + await helper.navigateToSnippetsAdmin() + }) + + test('PHP snippet is evaluating correctly', async () => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + code: "add_filter('show_admin_bar', '__return_false');" + }) + + await helper.navigateToFrontend() + await helper.expectElementNotVisible(SELECTORS.ADMIN_BAR) + await helper.expectElementCount(SELECTORS.ADMIN_BAR, 0) + }) + + test('PHP Snippet runs everywhere', async ({ page }) => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + location: 'EVERYWHERE', + code: BODY_CLASS_TEST_CODE + }) + + await page.goto('/wp-admin/') + await expect(page.locator('body')).toHaveClass(/custom-admin-class/) + + await helper.navigateToFrontend() + await expect(page.locator('body')).toHaveClass(/custom-frontend-class/) + }) + + test('PHP Snippet runs only in Admin', async ({ page }) => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + location: 'ADMIN_ONLY', + code: BODY_CLASS_TEST_CODE + }) + + await page.goto('/wp-admin/') + await expect(page.locator('body')).toHaveClass(/custom-admin-class/) + + await helper.navigateToFrontend() + await expect(page.locator('body')).not.toHaveClass(/custom-frontend-class/) + }) + + test('PHP Snippet runs only in Frontend', async ({ page }) => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + location: 'FRONTEND_ONLY', + code: BODY_CLASS_TEST_CODE + }) + + await page.goto('/wp-admin/') + await expect(page.locator('body')).not.toHaveClass(/custom-admin-class/) + + await helper.navigateToFrontend() + await expect(page.locator('body')).toHaveClass(/custom-frontend-class/) + }) + + test('HTML snippet is evaluating correctly in footer', async () => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + code: '

Hello World HTML snippet in footer!

', + type: 'HTML', + location: 'SITE_FOOTER' + }) + + await helper.navigateToFrontend() + await helper.expectTextVisible('Hello World HTML snippet in footer!') + await helper.expectElementCount('text=Hello World HTML snippet in footer!', 1) + }) + + test('HTML snippet is evaluating correctly in header', async () => { + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + code: '

Hello World HTML snippet in header!

', + type: 'HTML', + location: 'SITE_HEADER' + }) + + await helper.navigateToFrontend() + await helper.expectTextVisible('Hello World HTML snippet in header!') + await helper.expectElementCount('text=Hello World HTML snippet in header!', 1) + }) + + test('HTML snippet works with shortcode in editor', async ({ page }) => { + const snippetId = await createHtmlSnippetForEditor(helper, page) + const pageUrl = await createPageWithShortcode(snippetId) + + await verifyShortcodeRendersCorrectly(helper, page, pageUrl) + }) + + test.afterEach(async () => { + await helper.cleanupSnippet(TEST_SNIPPET_NAME) + }) +}) diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts new file mode 100644 index 00000000..07ecedf5 --- /dev/null +++ b/tests/e2e/code-snippets-list.spec.ts @@ -0,0 +1,101 @@ +import { expect, test } from '@playwright/test' +import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' +import { SELECTORS } from './helpers/constants' + +const TEST_SNIPPET_NAME = 'E2E List Test Snippet' + +test.describe('Code Snippets List Page Actions', () => { + let helper: SnippetsTestHelper + + test.beforeEach(async ({ page }) => { + helper = new SnippetsTestHelper(page) + await helper.navigateToSnippetsAdmin() + + await helper.createAndActivateSnippet({ + name: TEST_SNIPPET_NAME, + code: "add_filter('show_admin_bar', '__return_false');" + }) + await helper.navigateToSnippetsAdmin() + }) + + test.afterEach(async () => { + await helper.cleanupSnippet(TEST_SNIPPET_NAME) + }) + + test('Can toggle snippet activation from list page', async ({ page }) => { + const snippetRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + const toggleSwitch = snippetRow.locator('a.snippet-activation-switch') + + await expect(toggleSwitch).toHaveAttribute('title', 'Deactivate') + + await toggleSwitch.click() + await page.waitForLoadState('networkidle') + + const updatedRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + const updatedToggle = updatedRow.locator('a.snippet-activation-switch') + await expect(updatedToggle).toHaveAttribute('title', 'Activate') + + await updatedToggle.click() + await page.waitForLoadState('networkidle') + + const reactivatedRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + const reactivatedToggle = reactivatedRow.locator('a.snippet-activation-switch') + await expect(reactivatedToggle).toHaveAttribute('title', 'Deactivate') + }) + + test('Can access edit from list page', async ({ page }) => { + const snippetRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + + await snippetRow.locator(SELECTORS.EDIT_ACTION).click() + + await expect(page).toHaveURL(/page=edit-snippet/) + await expect(page.locator('#title')).toHaveValue(TEST_SNIPPET_NAME) + }) + + test('Can clone snippet from list page', async ({ page }) => { + const snippetRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + + await snippetRow.locator(SELECTORS.CLONE_ACTION).click() + await page.waitForLoadState('networkidle') + + await expect(page).toHaveURL(/page=snippets/) + + await helper.expectTextVisible(`${TEST_SNIPPET_NAME} [CLONE]`) + + const clonedRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME} [CLONE]")`) + + page.on('dialog', async dialog => { + expect(dialog.type()).toBe('confirm') + await dialog.accept() + }) + + await clonedRow.locator(SELECTORS.DELETE_ACTION).click() + await page.waitForLoadState('networkidle') + }) + + test('Can delete snippet from list page', async ({ page }) => { + const snippetRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + + page.on('dialog', async dialog => { + expect(dialog.type()).toBe('confirm') + await dialog.accept() + }) + + await snippetRow.locator(SELECTORS.DELETE_ACTION).click() + await page.waitForLoadState('networkidle') + + await expect(page).toHaveURL(/page=snippets/) + await helper.expectElementCount(`tr:has-text("${TEST_SNIPPET_NAME}")`, 0) + }) + + test('Can export snippet from list page', async ({ page }) => { + const snippetRow = page.locator(`tr:has-text("${TEST_SNIPPET_NAME}")`) + + const downloadPromise = page.waitForEvent('download') + + await snippetRow.locator(SELECTORS.EXPORT_ACTION).click() + + const download = await downloadPromise + expect(download.suggestedFilename()).toMatch(/\.json$/) + }) +}) diff --git a/tests/e2e/flat-files.setup.ts b/tests/e2e/flat-files.setup.ts new file mode 100644 index 00000000..3a9307c4 --- /dev/null +++ b/tests/e2e/flat-files.setup.ts @@ -0,0 +1,25 @@ +import { expect, test as setup } from '@playwright/test' + +setup('enable flat files', async ({ page }) => { + await page.goto('/wp-admin/admin.php?page=snippets-settings') + await page.waitForSelector('#wpbody-content') + + await page.waitForSelector('form') + + const flatFilesCheckbox = page.locator('input[name="code_snippets_settings[general][enable_flat_files]"]') + await expect(flatFilesCheckbox).toBeVisible() + + const isChecked = await flatFilesCheckbox.isChecked() + if (!isChecked) { + await flatFilesCheckbox.check() + } + + await page.click('input[type="submit"][name="submit"]') + + await page.waitForSelector('.notice-success', { timeout: 10000 }) + await expect(page.locator('.notice-success')).toContainText('Settings saved') + + await page.reload() + await page.waitForSelector('input[name="code_snippets_settings[general][enable_flat_files]"]') + await expect(page.locator('input[name="code_snippets_settings[general][enable_flat_files]"]')).toBeChecked() +}) diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts new file mode 100644 index 00000000..d67b5e18 --- /dev/null +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -0,0 +1,201 @@ +import { expect } from '@playwright/test' +import { + BUTTONS, + MESSAGES, + SELECTORS, + SNIPPET_LOCATIONS, + SNIPPET_TYPES, + TIMEOUTS, + URLS +} from './constants' +import type { Page} from '@playwright/test' + +export interface SnippetFormOptions { + name: string; + code: string; + type?: keyof typeof SNIPPET_TYPES; + location?: keyof typeof SNIPPET_LOCATIONS; +} + +export class SnippetsTestHelper { + constructor(private page: Page) {} + + /** + * Navigate to the Code Snippets admin page + */ + async navigateToSnippetsAdmin(): Promise { + await this.page.goto(URLS.SNIPPETS_ADMIN) + await this.page.waitForLoadState('networkidle') + await this.page.waitForSelector(SELECTORS.WPBODY_CONTENT, { timeout: TIMEOUTS.DEFAULT }) + } + + /** + * Navigate to frontend + */ + async navigateToFrontend(): Promise { + await this.page.goto(URLS.FRONTEND) + await this.page.waitForLoadState('networkidle') + } + + /** + * Click the "Add New" button to start creating a snippet + */ + async clickAddNewSnippet(): Promise { + await this.page.waitForSelector(SELECTORS.PAGE_TITLE, { timeout: TIMEOUTS.DEFAULT }) + await this.page.click(SELECTORS.ADD_NEW_BUTTON) + await this.page.waitForLoadState('networkidle') + } + + /** + * Fill the snippet form with the provided options + */ + async fillSnippetForm(options: SnippetFormOptions): Promise { + await this.page.waitForSelector(SELECTORS.TITLE_INPUT) + await this.page.fill(SELECTORS.TITLE_INPUT, options.name) + + if (options.type && 'PHP' !== options.type) { + await this.page.click(SELECTORS.SNIPPET_TYPE_SELECT) + await this.page.click(`text=${SNIPPET_TYPES[options.type]}`) + } + + await this.page.waitForSelector(SELECTORS.CODE_MIRROR_TEXTAREA) + await this.page.fill(SELECTORS.CODE_MIRROR_TEXTAREA, options.code) + + if (options.location) { + await this.page.waitForSelector(SELECTORS.LOCATION_SELECT, { timeout: TIMEOUTS.SHORT }) + await this.page.click(SELECTORS.LOCATION_SELECT) + + await this.page.waitForSelector(`text=${SNIPPET_LOCATIONS[options.location]}`, { timeout: TIMEOUTS.SHORT }) + await this.page.click(`text=${SNIPPET_LOCATIONS[options.location]}`, { force: true }) + } + } + + /** + * Save the snippet with the specified action + */ + async saveSnippet(action: 'save' | 'save_and_activate' | 'save_and_deactivate' = 'save'): Promise { + const buttonMap = { + save: BUTTONS.SAVE, + save_and_activate: BUTTONS.SAVE_AND_ACTIVATE, + save_and_deactivate: BUTTONS.SAVE_AND_DEACTIVATE, + } + + await this.page.click(buttonMap[action]) + } + + /** + * Expect a success message with the specified text + */ + async expectSuccessMessage(expectedMessage: string): Promise { + await expect(this.page.locator(SELECTORS.SUCCESS_MESSAGE)).toContainText(expectedMessage) + } + + /** + * Expect a success message in paragraph element + */ + async expectSuccessMessageInParagraph(expectedMessage: string): Promise { + await expect(this.page.locator(SELECTORS.SUCCESS_MESSAGE_P)).toContainText(expectedMessage) + } + + /** + * Open an existing snippet by name + */ + async openSnippet(snippetName: string): Promise { + await this.page.waitForSelector(`text=${snippetName}`) + await this.page.click(`text=${snippetName}`) + await this.page.waitForLoadState('networkidle') + } + + /** + * Delete a snippet (assumes you're already on the snippet edit page) + */ + async deleteSnippet(): Promise { + await this.page.click(BUTTONS.DELETE) + await this.page.click(SELECTORS.DELETE_CONFIRM_BUTTON) + } + + /** + * Check if a snippet exists on the snippets list page + */ + async snippetExists(snippetName: string): Promise { + const count = await this.page.locator(`text=${snippetName}`).count() + return 0 < count + } + + /** + * Clean up a snippet by name (navigate to admin, find snippet, delete it) + */ + async cleanupSnippet(snippetName: string): Promise { + await this.navigateToSnippetsAdmin() + + if (await this.snippetExists(snippetName)) { + await this.openSnippet(snippetName) + await this.deleteSnippet() + } + } + + /** + * Verify the current URL contains the snippets admin page + */ + async expectToBeOnSnippetsAdminPage(): Promise { + const currentUrl = this.page.url() + expect(currentUrl).toContain('page=snippets') + await expect(this.page.locator(SELECTORS.PAGE_TITLE)).toBeVisible() + } + + /** + * Expect an element to be visible + */ + async expectElementVisible(selector: string): Promise { + await expect(this.page.locator(selector)).toBeVisible() + } + + /** + * Expect an element to not be visible + */ + async expectElementNotVisible(selector: string): Promise { + await expect(this.page.locator(selector)).not.toBeVisible() + } + + /** + * Expect an element to have a specific count + */ + async expectElementCount(selector: string, expectedCount: number): Promise { + const count = await this.page.locator(selector).count() + expect(count).toBe(expectedCount) + } + + /** + * Expect text to be visible on the page + */ + async expectTextVisible(text: string): Promise { + await expect(this.page.locator(`text=${text}`)).toBeVisible() + } + + /** + * Expect text to not be visible on the page + */ + async expectTextNotVisible(text: string): Promise { + await expect(this.page.locator('body')).not.toContainText(text) + } + + /** + * Create a complete snippet with save and activate + */ + async createAndActivateSnippet(options: SnippetFormOptions): Promise { + await this.clickAddNewSnippet() + await this.fillSnippetForm(options) + await this.saveSnippet('save_and_activate') + await this.expectSuccessMessage(MESSAGES.SNIPPET_CREATED_AND_ACTIVATED) + } + + /** + * Create a snippet without activating + */ + async createSnippet(options: SnippetFormOptions): Promise { + await this.clickAddNewSnippet() + await this.fillSnippetForm(options) + await this.saveSnippet('save') + await this.expectSuccessMessage(MESSAGES.SNIPPET_CREATED) + } +} diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts new file mode 100644 index 00000000..1686accf --- /dev/null +++ b/tests/e2e/helpers/constants.ts @@ -0,0 +1,65 @@ +export const SELECTORS = { + WPBODY_CONTENT: '#wpbody-content, .wrap, #wpcontent', + PAGE_TITLE: 'h1, .page-title', + ADD_NEW_BUTTON: '.page-title-action', + + TITLE_INPUT: '#title', + CODE_MIRROR_TEXTAREA: '.CodeMirror textarea', + SNIPPET_TYPE_SELECT: '#snippet-type-select-input', + LOCATION_SELECT: '.code-snippets-select-location', + + SUCCESS_MESSAGE: '#message.notice', + SUCCESS_MESSAGE_P: '#message.notice p', + + DELETE_CONFIRM_BUTTON: 'button.components-button.is-destructive.is-primary', + + SNIPPETS_TABLE: '.wp-list-table', + SNIPPET_ROW: '.wp-list-table tbody tr', + SNIPPET_TOGGLE: '.snippet-activation-switch input[type="checkbox"]', + SNIPPET_NAME_LINK: '.row-title', + + EDIT_ACTION: '.row-actions .edit a', + CLONE_ACTION: '.row-actions .clone a', + DELETE_ACTION: '.row-actions .delete a', + EXPORT_ACTION: '.row-actions .export a', + + ADMIN_BAR: '#wpadminbar' +} + +export const TIMEOUTS = { + DEFAULT: 10000, + SHORT: 5000 +} + +export const URLS = { + SNIPPETS_ADMIN: '/wp-admin/admin.php?page=snippets', + FRONTEND: '/' +} + +export const MESSAGES = { + SNIPPET_CREATED: 'Snippet created', + SNIPPET_CREATED_AND_ACTIVATED: 'Snippet created and activated', + SNIPPET_UPDATED_AND_ACTIVATED: 'Snippet updated and activated', + SNIPPET_UPDATED_AND_DEACTIVATED: 'Snippet updated and deactivated' +} + +export const SNIPPET_TYPES = { + PHP: 'PHP', + HTML: 'HTML' +} + +export const SNIPPET_LOCATIONS = { + SITE_FOOTER: 'In site footer', + SITE_HEADER: 'In site section', + IN_EDITOR: 'Where inserted in editor', + ADMIN_ONLY: 'Only run in administration area', + FRONTEND_ONLY: 'Only run on site front-end', + EVERYWHERE: 'Run everywhere' +} + +export const BUTTONS = { + SAVE: 'text=Save Snippet', + SAVE_AND_ACTIVATE: 'text=Save and Activate', + SAVE_AND_DEACTIVATE: 'text=Save and Deactivate', + DELETE: 'text=Delete' +} diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts new file mode 100644 index 00000000..cde090e2 --- /dev/null +++ b/tests/playwright/playwright.config.ts @@ -0,0 +1,71 @@ +/// +import { join } from 'path' +import { defineConfig, devices } from '@playwright/test' + +const RETRIES = 2 +const WORKERS = 1 + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: '../e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? RETRIES : 0, + workers: process.env.CI ? WORKERS : 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: 'setup', + testMatch: /auth\.setup\.ts/ + }, + + { + name: 'flat-files-setup', + testMatch: /flat-files\.setup\.ts/, + use: { + ...devices['Desktop Chrome'], + storageState: join(__dirname, '../e2e/.auth/user.json') + }, + dependencies: ['setup'] + }, + + { + name: 'chromium-db-snippets', + use: { + ...devices['Desktop Chrome'], + storageState: join(__dirname, '../e2e/.auth/user.json') + }, + dependencies: ['setup'], + testIgnore: /.*\.setup\.ts/ + }, + + { + name: 'chromium-file-based-snippets', + use: { + ...devices['Desktop Chrome'], + storageState: join(__dirname, '../e2e/.auth/user.json') + }, + dependencies: ['setup', 'flat-files-setup'], + testIgnore: /.*\.setup\.ts/ + } + ], + + timeout: 30000, + + expect: { + timeout: 10000 + } +}) diff --git a/tsconfig.json b/tsconfig.json index 76987a2b..b3b84b90 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "src/js", "config", "scripts", + "tests", "*.config.mjs", "*.config.js" ],