├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS ├── release-pull-request-template.md └── workflows │ ├── codeql-analysis.yml │ ├── cypress.yml │ ├── docs.yml │ ├── publish.yml │ ├── release-pull-request.yml │ └── update-built-branch.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── .wp-env.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CREDITS.md ├── LICENSE.md ├── README.md ├── _templates ├── cypress-command │ └── new │ │ ├── command.ejs.t │ │ ├── import.ejs.t │ │ ├── register.ejs.t │ │ ├── test.ejs.t │ │ └── type.ejs.t └── generator │ ├── help │ └── index.ejs.t │ ├── new │ └── hello.ejs.t │ └── with-prompt │ ├── hello.ejs.t │ └── prompt.ejs.t ├── cypress-wp-utils.php ├── package-lock.json ├── package.json ├── run-all-cores.sh ├── src ├── commands │ ├── activate-all-plugins.ts │ ├── activate-plugin.ts │ ├── check-block-pattern-exists.ts │ ├── check-post-exists.ts │ ├── check-sitemap-exists.ts │ ├── classic-create-post.ts │ ├── close-welcome-guide.ts │ ├── create-post.ts │ ├── create-term.ts │ ├── deactivate-all-plugins.ts │ ├── deactivate-plugin.ts │ ├── delete-all-terms.ts │ ├── get-block-editor.ts │ ├── insert-block.ts │ ├── login.ts │ ├── logout.ts │ ├── open-document-settings-panel.ts │ ├── open-document-settings-sidebar.ts │ ├── set-permalink-structure.ts │ ├── upload-media.ts │ ├── wp-cli-eval.ts │ └── wp-cli.ts ├── functions │ ├── capitalize.ts │ ├── get-iframe.ts │ └── uc-first.ts ├── index.ts └── interface │ └── post-data.ts ├── tests ├── bin │ ├── initialize.sh │ ├── set-core-version.js │ └── wp-cli.yml └── cypress │ ├── cypress-config.js │ ├── e2e │ ├── check-post-exists.test.js │ ├── check-sitemap-exists.test.js │ ├── classic-create-post.test.js │ ├── close-welcome-guide.test.js │ ├── create-post.test.js │ ├── create-term.test.js │ ├── delete-all-terms.test.js │ ├── insert-block.test.js │ ├── login.test.js │ ├── logout.test.js │ ├── open-document-settings.test.js │ ├── plugins.test.js │ ├── set-permalink-structure.test.js │ ├── upload-media.test.js │ ├── wp-cli.test.js │ └── z.check-block-pattern-exists.test.js │ ├── fixtures │ ├── 10up.png │ └── example.json │ ├── support │ ├── e2e.js │ └── functions.js │ └── tsconfig.json └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | # Local env vars for debugging 2 | TS_NODE_IGNORE="false" 3 | TS_NODE_FILES="true" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/types/global.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'node', 'prettier', 'eslint-plugin-tsdoc'], 5 | parserOptions: { 6 | tsconfigRootDir: __dirname, 7 | project: ['./tsconfig.json'], 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:node/recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 15 | 'plugin:prettier/recommended', 16 | 'plugin:cypress/recommended', 17 | ], 18 | rules: { 19 | 'prettier/prettier': 'warn', 20 | 'node/no-missing-import': 'off', 21 | 'node/no-empty-function': 'off', 22 | 'node/no-unsupported-features/es-syntax': 'off', 23 | 'node/no-missing-require': 'off', 24 | 'node/shebang': 'off', 25 | '@typescript-eslint/no-use-before-define': 'off', 26 | quotes: ['warn', 'single', { avoidEscape: true }], 27 | 'node/no-unpublished-import': 'off', 28 | '@typescript-eslint/no-unsafe-assignment': 'off', 29 | '@typescript-eslint/no-var-requires': 'off', 30 | '@typescript-eslint/ban-ts-comment': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | 'tsdoc/syntax': 'warn', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the repository to show as TypeScript rather than JS in GitHub 2 | *.js linguist-detectable=false -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. Unless a later match takes precedence, @10up/open-source-practice, as primary maintainers will be requested for review when someone opens a Pull Request. 2 | * @10up/open-source-practice 3 | 4 | # GitHub and WordPress.org specifics 5 | /.github/ @jeffpaul 6 | CODE_OF_CONDUCT.md @jeffpaul 7 | LICENSE.md @jeffpaul 8 | -------------------------------------------------------------------------------- /.github/release-pull-request-template.md: -------------------------------------------------------------------------------- 1 | - [x] Branch: Starting from `develop`, cut a release branch named `release/X.Y.Z` for your changes. 2 | - [ ] Version bump: Bump the version number in `package.json` and `package-lock.json` if it does not already reflect the version being released. 3 | - [ ] Changelog: Add/update the changelog in `CHANGELOG.md`. 4 | - [ ] Props: Update `CREDITS.md` file with any new contributors, confirm maintainers are accurate. 5 | - [ ] Readme updates: Make any other readme changes as necessary in `README.md`. 6 | - [ ] Merge: Make a non-fast-forward merge from your release branch to `develop` (or merge the Pull Request), then merge `develop` into `trunk` (`git checkout develop && git pull origin develop && git checkout trunk && git pull origin trunk && git merge --no-ff develop`). `trunk` contains the stable development version. 7 | - [ ] Push: Push your `trunk` branch to GitHub (e.g. `git push origin trunk`). 8 | - [ ] Release: Create a [new release](https://github.com/10up/cypress-wp-utils/releases/new), naming the tag and the release with the new version number, and targeting the `trunk` branch. Paste the changelog from `CHANGELOG.md` into the body of the release and include a link to the closed issues on the [milestone](https://github.com/10up/cypress-wp-utils/milestones/#?closed=1). The release should now appear under [releases](https://github.com/10up/cypress-wp-utils/releases). 9 | - [ ] Close milestone: Edit the [milestone](https://github.com/10up/cypress-wp-utils/milestones/) with release date (in the `Due date (optional)` field) and link to GitHub release (in the `Description field`), then close the milestone. 10 | - [ ] Punt incomplete items: If any open issues or PRs which were milestoned for `X.Y.Z` do not make it into the release, update their milestone to `X.Y.Z+1`, `X.Y+- [ ]0`, `X+- [ ]0.0` or `Future Release`. 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [trunk, develop] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [trunk, develop] 20 | schedule: 21 | - cron: '36 7 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 72 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: E2E test 2 | 3 | on: 4 | push: 5 | branches: [trunk, develop] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [develop] 9 | schedule: 10 | - cron: '36 7 * * 6' 11 | 12 | jobs: 13 | changed-files: 14 | name: Changed Files 15 | outputs: 16 | status: ${{ steps.changed-files.outputs.any_changed }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - id: changed-files 22 | uses: tj-actions/changed-files@2f7c5bfce28377bc069a65ba478de0a74aa0ca32 # v46.0.1 23 | with: 24 | files: | 25 | .github/workflows/cypress.yml 26 | src/** 27 | tests/** 28 | cypress-wp-utils.php 29 | .wp-env.json 30 | package.json 31 | package-lock.json 32 | 33 | build: 34 | name: Build 35 | needs: changed-files 36 | if: ${{ needs.changed-files.outputs.status == 'true' }} 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | - name: Cache Node 42 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 43 | with: 44 | path: | 45 | node_modules 46 | ~/.cache 47 | ~/.npm 48 | key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }} 49 | - name: Cache Build 50 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 51 | with: 52 | path: lib 53 | key: ${{ runner.os }}-build-${{ hashFiles('src/**') }} 54 | - name: Install dependencies 55 | run: npm install 56 | - name: Build 57 | run: npm run build 58 | 59 | cypress: 60 | name: ${{ matrix.core.name }} 61 | needs: [changed-files, build] 62 | if: ${{ needs.changed-files.outputs.status == 'true' }} 63 | runs-on: ubuntu-latest 64 | strategy: 65 | fail-fast: false 66 | matrix: 67 | core: 68 | - { 69 | name: 'WP trunk', 70 | version: 'WordPress/WordPress#master', 71 | number: 'trunk', 72 | } 73 | - { 74 | name: 'WP 6.7', 75 | version: 'WordPress/WordPress#6.7-branch', 76 | number: '6.7' 77 | } 78 | - { 79 | name: 'WP 6.6', 80 | version: 'WordPress/WordPress#6.6-branch', 81 | number: '6.6', 82 | } 83 | - { 84 | name: 'WP 6.5', 85 | version: 'WordPress/WordPress#6.5-branch', 86 | number: '6.5', 87 | } 88 | - { 89 | name: 'WP 6.4', 90 | version: 'WordPress/WordPress#6.4-branch', 91 | number: '6.4', 92 | } 93 | - { 94 | name: 'WP 6.3', 95 | version: 'WordPress/WordPress#6.3-branch', 96 | number: '6.3', 97 | } 98 | - { 99 | name: 'WP 6.2', 100 | version: 'WordPress/WordPress#6.2-branch', 101 | number: '6.2', 102 | } 103 | - { 104 | name: 'WP 6.1', 105 | version: 'WordPress/WordPress#6.1-branch', 106 | number: '6.1', 107 | } 108 | - { 109 | name: 'WP 6.0', 110 | version: 'WordPress/WordPress#6.0-branch', 111 | number: '6.0', 112 | } 113 | - { 114 | name: 'WP 5.9', 115 | version: 'WordPress/WordPress#5.9-branch', 116 | number: '5.9', 117 | } 118 | - { 119 | name: 'WP 5.8', 120 | version: 'WordPress/WordPress#5.8-branch', 121 | number: '5.8', 122 | } 123 | - { 124 | name: 'WP 5.7 (minimum)', 125 | version: 'WordPress/WordPress#5.7-branch', 126 | number: '5.7', 127 | } 128 | steps: 129 | - name: Checkout 130 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 131 | - name: Cache Node 132 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 133 | with: 134 | path: | 135 | node_modules 136 | ~/.cache 137 | ~/.npm 138 | key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }} 139 | - name: Cache Build 140 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 141 | with: 142 | path: lib 143 | key: ${{ runner.os }}-build-${{ hashFiles('src/**') }} 144 | - name: Install Chrome 145 | uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3 146 | - name: Set the core version 147 | run: ./tests/bin/set-core-version.js ${{ matrix.core.version }} 148 | - name: Set up WP environment 149 | run: npm run env:start 150 | - name: Test 151 | run: npm run cypress:run -- --browser chrome 152 | env: 153 | CYPRESS_WORDPRESS_CORE: ${{ matrix.core.number }} 154 | - name: Update summary 155 | run: | 156 | npx mochawesome-merge ./tests/cypress/reports/*.json -o tests/cypress/reports/mochawesome.json 157 | rm -rf ./tests/cypress/reports/mochawesome-*.json 158 | npx mochawesome-json-to-md -p ./tests/cypress/reports/mochawesome.json -o ./tests/cypress/reports/mochawesome.md 159 | npx mochawesome-report-generator tests/cypress/reports/mochawesome.json -o tests/cypress/reports/ 160 | cat ./tests/cypress/reports/mochawesome.md >> $GITHUB_STEP_SUMMARY 161 | - name: Make artifacts available 162 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 163 | if: failure() 164 | with: 165 | name: cypress-artifact-${{ matrix.core.number }} 166 | retention-days: 2 167 | path: | 168 | ${{ github.workspace }}/tests/cypress/screenshots/ 169 | ${{ github.workspace }}/tests/cypress/videos/ 170 | ${{ github.workspace }}/tests/cypress/logs/ 171 | ${{ github.workspace }}/tests/cypress/reports/ 172 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | 14 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 15 | run: | 16 | npm i 17 | npm run docs 18 | 19 | - name: Deploy 🚀 20 | uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 21 | with: 22 | branch: docs # The branch the action should deploy to. 23 | folder: docs # The folder the action should deploy. 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish the NPM package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | name: Publish 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 16 | with: 17 | node-version-file: .nvmrc 18 | always-auth: true 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Publish 25 | run: npm publish --access public 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Release Pull Request Automation 2 | 3 | on: 4 | create: 5 | jobs: 6 | release-pull-request-automation: 7 | if: ${{ github.event.ref_type == 'branch' && contains( github.ref, 'release/' ) }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | - name: Generate title 13 | run: | 14 | BRANCH=${GITHUB_REF##*/} 15 | echo $BRANCH 16 | VERSION=${BRANCH#'release/'} 17 | echo "result=Release: ${VERSION}" >> "${GITHUB_OUTPUT}" 18 | id: title 19 | - name: Create Pull Request 20 | run: gh pr create --title "${{ steps.title.outputs.result }}" --body-file ./.github/release-pull-request-template.md 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/update-built-branch.yml: -------------------------------------------------------------------------------- 1 | name: Update built branch 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Build and Push 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Install dependencies 16 | run: npm install 17 | 18 | - name: Build 19 | run: npm run build 20 | 21 | - name: Create the build folder 22 | run: | 23 | mkdir build 24 | cp package.json build/ 25 | mv lib build/ 26 | 27 | - name: Push 28 | uses: s0/git-publish-subdir-action@ac113f6bfe8896e85a373534242c949a7ea74c98 # develop 29 | env: 30 | REPO: self 31 | BRANCH: build 32 | FOLDER: build 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | MESSAGE: 'Build: ({sha}) {msg}' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/cypress/screenshots 2 | tests/cypress/videos 3 | tests/cypress/reports 4 | 5 | # wp-env files 6 | .wp-env.override.json 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Snowpack dependency directory (https://snowpack.dev/) 52 | web_modules/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env.test 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | 118 | # PhpStorm 119 | .idea/ 120 | 121 | # yarn v2 122 | .yarn/cache 123 | .yarn/unplugged 124 | .yarn/build-state.yml 125 | .yarn/install-state.gz 126 | .pnp.* 127 | 128 | # Compiled code 129 | lib/ 130 | 131 | # Documentation 132 | docs/ 133 | 134 | # OS files 135 | .DS_Store -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec Utilities library for WordPress E2E testing in the Cypress environment. 4 | 5 | [![Support Level](https://img.shields.io/badge/support-beta-blueviolet.svg)](#support-level) ![WordPress latest](https://img.shields.io/badge/WordPress%20up%20to-6.2-blue) [![Release Version](https://img.shields.io/github/release/10up/cypress-wp-utils.svg)](https://github.com/10up/cypress-wp-utils/releases/latest) ![WordPress minimum](https://img.shields.io/badge/WordPress%20since-5.7-blue) ![Test PRs](https://github.com/10up/cypress-wp-utils/actions/workflows/cypress.yml/badge.svg) ![CodeQL](https://github.com/10up/cypress-wp-utils/actions/workflows/codeql-analysis.yml/badge.svg) [![MIT License](https://img.shields.io/github/license/10up/cypress-wp-utils.svg)](https://github.com/10up/cypress-wp-utils/blob/develop/LICENSE.md) 6 | 7 | ## Prerequisites 8 | 9 | This library requires Cypress. Use [@10up/cypress-wp-setup](https://github.com/10up/cypress-wp-setup) to set up Cypress automatically, including this library. If running tests against WordPress 6.3, you'll probably need to set `chromeWebSecurity: false` in your Cypress config file. This allows Cypress to properly interact with the iframed Block Editor. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | npm install @10up/cypress-wp-utils --save-dev 15 | ``` 16 | 17 | ## Usage 18 | 19 | Import the libary in `support/index.js` file: 20 | 21 | ```js 22 | // tests/cypress/support/index.js 23 | import '@10up/cypress-wp-utils'; 24 | ``` 25 | 26 | Documentation for commands can be found at [https://10up.github.io/cypress-wp-utils/](https://10up.github.io/cypress-wp-utils/). 27 | 28 | ### IntelliSense and code completion for Cypress commands 29 | 30 | Add a `tsconfig.json` file into the cypress folder to enable code completion for both Cypress built-in commands and commands from this library: 31 | 32 | ```js 33 | { 34 | "compilerOptions": { 35 | "allowJs": true, 36 | "types": ["cypress"] 37 | }, 38 | "include": ["**/*.*"] 39 | } 40 | ``` 41 | 42 | ### Adding a new command 43 | 44 | This project uses `hygen` to scaffold new commands to reduce the effort of manually importing and registering new commands: 45 | 46 | ```sh 47 | $ npx hygen cypress-command new customCommand 48 | 49 | Loaded templates: _templates 50 | added: src/commands/custom-command.ts 51 | inject: src/index.ts 52 | inject: src/index.ts 53 | inject: src/index.ts 54 | ``` 55 | 56 | ### Install the library locally 57 | 58 | ```sh 59 | npm i -D path/to/the/library 60 | ``` 61 | 62 | ### Test against every WordPress major release 63 | 64 | Every incoming pull request will automatically run tests against: 65 | 66 | - our current minimum supported WordPress version, 5.7 67 | - WordPress [latest release](https://github.com/WordPress/WordPress/tags) 68 | - current WordPress [future release](https://github.com/WordPress/WordPress/tree/master) 69 | 70 | To run tests locally against every WordPress major release since minimum support (5.7) to the latest nightly build (e.g., 6.4-alpha) use this script: 71 | 72 | ```sh 73 | ./run-all-cores.sh 74 | ``` 75 | 76 | It has optional parameter `-s` to specify only one test suite to run: 77 | 78 | ```sh 79 | ./run-all-cores.sh -s tests/cypress/intergation/login.test.js 80 | ``` 81 | 82 | ## Contributing 83 | 84 | Please read [CODE_OF_CONDUCT.md](https://github.com/10up/cypress-wp-utils/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct, [CONTRIBUTING.md](https://github.com/10up/cypress-wp-utils/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests to us, and [CREDITS.md](https://github.com/10up/cypress-wp-utils/blob/trunk/CREDITS.md) for a list of maintainers, contributors, and libraries used in this repository. 85 | 86 | ## Support Level 87 | 88 | **Beta:** This project is quite new and we're not sure what our ongoing support level for this will be. Bug reports, feature requests, questions, and pull requests are welcome. If you like this project please let us know, but be cautious using this in a Production environment! 89 | 90 | ## Like what you see? 91 | 92 | Work with the 10up WordPress Practice at Fueled 93 | -------------------------------------------------------------------------------- /_templates/cypress-command/new/command.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/commands/<%= h.changeCase.param(name) %>.ts 3 | --- 4 | /** 5 | * <%= h.changeCase.title(name) %> 6 | * 7 | * @example 8 | * ``` 9 | * cy.<%= h.changeCase.camel(name) %>() 10 | * ``` 11 | */ 12 | export const <%= h.changeCase.camel(name) %> = (): void => {}; -------------------------------------------------------------------------------- /_templates/cypress-command/new/import.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | to: src/index.ts 4 | after: Import commands. 5 | skip_if: import { <%= h.changeCase.camel(name) %> } 6 | --- 7 | import { <%= h.changeCase.camel(name) %> } from './commands/<%= h.changeCase.param(name) %>'; -------------------------------------------------------------------------------- /_templates/cypress-command/new/register.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | to: src/index.ts 4 | after: Register commands 5 | skip_if: Cypress.Commands.add\('<%= h.changeCase.camel(name) %>' 6 | --- 7 | Cypress.Commands.add('<%= h.changeCase.camel(name) %>', <%= h.changeCase.camel(name) %>); -------------------------------------------------------------------------------- /_templates/cypress-command/new/test.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: tests/cypress/e2e/<%= h.changeCase.param(name) %>.test.js 3 | --- 4 | describe('Command: <%= h.changeCase.camel(name) %>', () => { 5 | it('Should be able to <%= h.changeCase.title(name) %>', () => { 6 | cy.<%= h.changeCase.camel(name) %>(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /_templates/cypress-command/new/type.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | to: src/index.ts 4 | after: interface Chainable { 5 | skip_if: typeof <%= h.changeCase.camel(name) %> 6 | --- 7 | <%= h.changeCase.camel(name) %>: typeof <%= h.changeCase.camel(name) %>; -------------------------------------------------------------------------------- /_templates/generator/help/index.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | message: | 3 | hygen {bold generator new} --name [NAME] --action [ACTION] 4 | hygen {bold generator with-prompt} --name [NAME] --action [ACTION] 5 | --- -------------------------------------------------------------------------------- /_templates/generator/new/hello.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t 3 | --- 4 | --- 5 | to: app/hello.js 6 | --- 7 | const hello = ``` 8 | Hello! 9 | This is your first hygen template. 10 | 11 | Learn what it can do here: 12 | 13 | https://github.com/jondot/hygen 14 | ``` 15 | 16 | console.log(hello) 17 | 18 | 19 | -------------------------------------------------------------------------------- /_templates/generator/with-prompt/hello.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t 3 | --- 4 | --- 5 | to: app/hello.js 6 | --- 7 | const hello = ``` 8 | Hello! 9 | This is your first prompt based hygen template. 10 | 11 | Learn what it can do here: 12 | 13 | https://github.com/jondot/hygen 14 | ``` 15 | 16 | console.log(hello) 17 | 18 | 19 | -------------------------------------------------------------------------------- /_templates/generator/with-prompt/prompt.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js 3 | --- 4 | 5 | // see types of prompts: 6 | // https://github.com/enquirer/enquirer/tree/master/examples 7 | // 8 | module.exports = [ 9 | { 10 | type: 'input', 11 | name: 'message', 12 | message: "What's your message?" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /cypress-wp-utils.php: -------------------------------------------------------------------------------- 1 | (https://github.com/10up)", 34 | "engines": { 35 | "node": ">=12.0" 36 | }, 37 | "keywords": [ 38 | "wordpress", 39 | "cypress", 40 | "testing", 41 | "e2e" 42 | ], 43 | "bugs": { 44 | "url": "https://github.com/10up/cypress-wp-utils/issues" 45 | }, 46 | "homepage": "https://github.com/10up/cypress-wp-utils#readme", 47 | "devDependencies": { 48 | "@types/node": "^12.20.11", 49 | "@typescript-eslint/eslint-plugin": "^4.22.0", 50 | "@typescript-eslint/parser": "^4.22.0", 51 | "@wordpress/env": "^10.2.0", 52 | "codecov": "^3.8.1", 53 | "compare-versions": "^4.1.3", 54 | "cypress": "^13.6.4", 55 | "cypress-mochawesome-reporter": "^3.6.0", 56 | "eslint": "^7.25.0", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-cypress": "^2.12.1", 59 | "eslint-plugin-node": "^11.1.0", 60 | "eslint-plugin-prettier": "^3.4.0", 61 | "eslint-plugin-tsdoc": "^0.2.14", 62 | "husky": "^6.0.0", 63 | "lint-staged": "^13.2.1", 64 | "mochawesome-json-to-md": "^0.7.2", 65 | "prettier": "^2.2.1", 66 | "ts-node": "^10.2.1", 67 | "typedoc": "^0.22.12", 68 | "typescript": "^4.2.4" 69 | }, 70 | "lint-staged": { 71 | "*.ts": "eslint --cache --cache-location .eslintcache --fix", 72 | "*.{ts,js}": "prettier --write" 73 | }, 74 | "types": "./lib/index.d.ts", 75 | "directories": { 76 | "lib": "lib", 77 | "test": "tests" 78 | } 79 | } -------------------------------------------------------------------------------- /run-all-cores.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MAJOR_VERSIONS="5.7 5.8 5.9 6.0 6.1 6.2 6.3 6.4 6.5 6.6 6.7" 4 | TRUNK="master:trunk" 5 | 6 | VERSIONS="" 7 | for MAJOR_VERSION in $MAJOR_VERSIONS; do 8 | # This ensures the latest patch version is used. 9 | VERSIONS="$VERSIONS $MAJOR_VERSION-branch:$MAJOR_VERSION" 10 | done 11 | VERSIONS="$TRUNK $VERSIONS" 12 | 13 | echo "Running tests for the following core versions: $VERSIONS" 14 | 15 | SPEC="-- --quiet" 16 | 17 | while getopts s: flag 18 | do 19 | case "${flag}" in 20 | s) SPEC="-- --quiet --spec $OPTARG";; 21 | esac 22 | done 23 | 24 | for VERSION in $VERSIONS; do 25 | CORE=$(echo $VERSION|cut -d ":" -f 1) 26 | NUMBER=$(echo $VERSION|cut -d ":" -f 2) 27 | [[ -z "$NUMBER" ]] && NUMBER="$CORE" 28 | echo "**********************************************" 29 | echo "Core: $CORE, Version number: $NUMBER" 30 | echo "**********************************************" 31 | ./tests/bin/set-core-version.js $CORE 32 | npm run env:start > /dev/null 33 | npm run env run tests-cli "core update-db" > /dev/null 34 | npm run env clean > /dev/null 35 | CYPRESS_WORDPRESS_CORE="$NUMBER" npm run cypress:run $SPEC 36 | npm run env:stop > /dev/null 37 | done 38 | -------------------------------------------------------------------------------- /src/commands/activate-all-plugins.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Activate All Plugins 3 | * 4 | * @example 5 | * ``` 6 | * cy.activateAllPlugins() 7 | * ``` 8 | */ 9 | export const activateAllPlugins = (): void => { 10 | cy.visit('/wp-admin/plugins.php'); 11 | cy.get('#cb-select-all-1').click(); 12 | cy.get('#bulk-action-selector-top').select('activate-selected'); 13 | cy.get('#doaction').click(); 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/activate-plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Activate Plugin 3 | * 4 | * @param slug - Plugin slug 5 | * 6 | * @example 7 | * Activate Classic Editor plugin 8 | * ``` 9 | * cy.activatePlugin('classic-editor') 10 | * ``` 11 | */ 12 | export const activatePlugin = (slug: string): void => { 13 | cy.visit('/wp-admin/plugins.php'); 14 | cy.get(`#the-list tr[data-slug="${slug}"]`).then($pluginRow => { 15 | if ($pluginRow.find('.activate > a').length > 0) { 16 | cy.get(`#the-list tr[data-slug="${slug}"] .activate > a`) 17 | .should('have.text', 'Activate') 18 | .click(); 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/commands/check-block-pattern-exists.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable tsdoc/syntax */ 2 | /** 3 | * Check Block Pattern Exists. Works only with WordPress \>=5.5 4 | * 5 | * \@param postData { 6 | * `title` - Patttern name/title, 7 | * `categoryValue` - Value of the pattern category, 8 | * } 9 | * 10 | * @example 11 | * For WP v5.5 12 | * ``` 13 | * cy.checkBlockPatternExists({ 14 | * title: 'Two buttons', 15 | * }); 16 | * ``` 17 | * 18 | * @example 19 | * For WP v5.9 20 | * ``` 21 | * cy.checkBlockPatternExists({ 22 | * title: 'Three columns with offset images', 23 | * categoryValue: 'gallery', 24 | * }); 25 | * ``` 26 | */ 27 | declare global { 28 | interface Window { 29 | wp: any; 30 | } 31 | } 32 | 33 | export const checkBlockPatternExists = ({ 34 | title, 35 | categoryValue = 'featured', 36 | }: { 37 | title: string; 38 | categoryValue?: string; 39 | }): void => { 40 | // opening the inserter loads the patterns in WP trunk after 6.4.3. 41 | cy.get('button[class*="__inserter-toggle"][aria-pressed="false"]').click(); 42 | cy.get('button[class*="__inserter-toggle"][aria-pressed="true"]').click(); 43 | 44 | cy.window() 45 | .then(win => { 46 | /* eslint-disable */ 47 | return new Promise(resolve => { 48 | let elapsed = 0; 49 | 50 | const inverval = setInterval(function () { 51 | if (elapsed > 2500) { 52 | clearInterval(inverval); 53 | resolve(false); 54 | } 55 | 56 | const { wp } = win; 57 | 58 | let allRegisteredPatterns; 59 | 60 | if (wp?.data?.select('core')?.getBlockPatterns) { 61 | allRegisteredPatterns = wp.data.select('core').getBlockPatterns(); 62 | } else { 63 | allRegisteredPatterns = wp.data 64 | .select('core/block-editor') 65 | .getSettings().__experimentalBlockPatterns; 66 | } 67 | 68 | if (undefined !== allRegisteredPatterns) { 69 | for (let i = 0; i < allRegisteredPatterns.length; i++) { 70 | if ( 71 | title === allRegisteredPatterns[i].title && 72 | allRegisteredPatterns[i].categories && 73 | allRegisteredPatterns[i].categories.includes(categoryValue) 74 | ) { 75 | resolve(true); 76 | return; 77 | } 78 | } 79 | } 80 | elapsed += 100; 81 | }, 100); 82 | }); 83 | }) 84 | .then(val => { 85 | /* eslint-enable */ 86 | cy.wrap(val); 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /src/commands/check-post-exists.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check Post Exists 3 | * 4 | * @param postData { 5 | * `title` - Post Title, 6 | * `postType` - Post type, 7 | * } 8 | * 9 | * @example 10 | * Check for the `post`, without spefifying second parameer. 11 | * ``` 12 | * cy.checkPostExists({ 13 | * title: 'Hello world!', 14 | * }) 15 | * ``` 16 | * 17 | * @example 18 | * Check for the `page`. 19 | * ``` 20 | * cy.checkPostExists({ 21 | * title: 'Sample Page', 22 | * postType: 'page', 23 | * }) 24 | * ``` 25 | */ 26 | export const checkPostExists = ({ 27 | title, 28 | postType = 'post', 29 | }: { 30 | title: string; 31 | postType?: string; 32 | }): void => { 33 | cy.visit(`/wp-admin/edit.php?post_type=${postType}`); 34 | 35 | cy.get('#posts-filter').then($postsFilter => { 36 | // If there are no posts, bail early. 37 | if ($postsFilter.find('.no-items').length > 0) { 38 | cy.wrap(false); 39 | } else { 40 | const searchInput = '#post-search-input'; 41 | const searchSubmit = '#search-submit'; 42 | const postLabel = '[aria-label="Move “' + title + '” to the Trash"]'; 43 | 44 | // Search for the post title. 45 | cy.get(searchInput).clear().type(title).get(searchSubmit).click(); 46 | 47 | // See if the post is listed in the search result. 48 | cy.get('body').then($body => { 49 | if ($body.find(postLabel).length > 0) { 50 | cy.wrap(true); 51 | } else { 52 | cy.wrap(false); 53 | } 54 | }); 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/commands/check-sitemap-exists.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable tsdoc/syntax */ 2 | /** 3 | * Check Sitemap Exists. 4 | * 5 | * @example 6 | * Use the command without any argument, sitemap.xml will be used: 7 | * ``` 8 | * cy.checkSitemap() 9 | * ``` 10 | * 11 | * @example 12 | * Use the command with custom sitemap path: 13 | * ``` 14 | * cy.checkSitemap( '/alternative-sitemap.xml') 15 | * ``` 16 | */ 17 | 18 | export const checkSitemap = (sitemap_url = '/sitemap.xml'): void => { 19 | cy.request(sitemap_url).then(response => { 20 | if (response.status === 200) { 21 | cy.log('Sitemap exists'); 22 | } else { 23 | cy.log('Sitemap does not exist'); 24 | // Send an alert to the team 25 | // You can use a messaging service like Slack or email to send an alert 26 | cy.task('sendAlert', 'Sitemap has disappeared'); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/commands/classic-create-post.ts: -------------------------------------------------------------------------------- 1 | import PostData from '../interface/post-data'; 2 | 3 | /** 4 | * Create Post in Classic Editor 5 | * 6 | * @param postData - Post data. 7 | * 8 | * @example 9 | * ``` 10 | * cy.classicCreatePost({ 11 | * title: 'Post title', 12 | * content: 'Post content', 13 | * beforeSave: () => { 14 | * // Do something before save. 15 | * }, 16 | * postType: 'page', 17 | * status: 'draft' 18 | * }).then(postID => { 19 | * cy.log(postID); 20 | * }) 21 | * ``` 22 | */ 23 | export const classicCreatePost = ({ 24 | postType = 'post', 25 | title = 'Test Post', 26 | content = 'Test content', 27 | status = 'publish', 28 | beforeSave, 29 | }: PostData): void => { 30 | cy.visit(`/wp-admin/post-new.php?post_type=${postType}`); 31 | 32 | cy.get('#title').click().clear().type(title); 33 | 34 | cy.get('#content_ifr').then($iframe => { 35 | const doc = $iframe.contents().find('body#tinymce'); 36 | cy.wrap(doc).find('p:last-child').type(content); 37 | }); 38 | 39 | if ('undefined' !== typeof beforeSave) { 40 | beforeSave(); 41 | } 42 | 43 | cy.intercept('POST', '/wp-admin/post.php', req => { 44 | req.alias = 'savePost'; 45 | }); 46 | 47 | if ('draft' === status) { 48 | cy.get('#save-post').should('not.have.class', 'disabled').click(); 49 | } else { 50 | cy.get('#publish').should('not.have.class', 'disabled').click(); 51 | } 52 | 53 | cy.wait('@savePost').then(response => { 54 | const body = new URLSearchParams(response.request?.body); 55 | const id = body.get('post_ID'); 56 | cy.wrap(id); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/commands/close-welcome-guide.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Close Welcome Guide 3 | * 4 | * @example 5 | * ``` 6 | * cy.closeWelcomeGuide() 7 | * ``` 8 | */ 9 | export const closeWelcomeGuide = (): void => { 10 | const titleInput = 'h1.editor-post-title__input, #post-title-0'; 11 | const closeButtonSelector = 12 | '.edit-post-welcome-guide .components-modal__header button'; 13 | 14 | // Wait for edit page to load 15 | cy.getBlockEditor().find(titleInput).should('exist'); 16 | 17 | cy.get('body').then($body => { 18 | if ($body.find(closeButtonSelector).length > 0) { 19 | cy.get(closeButtonSelector).click(); 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/commands/create-post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a Post 3 | * 4 | * @param postData - Post data 5 | * 6 | * @returns Wraps post data object. See WP_REST_Posts_Controller::prepare_item_for_response 7 | * for the reference of post object contents: 8 | * https://github.com/WordPress/WordPress/blob/master/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php 9 | * 10 | * @example 11 | * Create a Post and get ID 12 | * ``` 13 | * cy.createPost({ 14 | * title: 'Test Post', 15 | * content: 'Test Content' 16 | * }).then(post => { 17 | * const id = post.id; 18 | * }); 19 | * ``` 20 | * 21 | * @example 22 | * Create a Post with draft status. 23 | * ``` 24 | * cy.createPost({ 25 | * title: 'Test Post', 26 | * content: 'Test Content', 27 | * status: 'draft' 28 | * }) 29 | * ``` 30 | * 31 | * @example 32 | * Create a Page 33 | * ``` 34 | * cy.createPost({ 35 | * postType: 'page' 36 | * title: 'Test page', 37 | * content: 'Page Content' 38 | * }) 39 | * ``` 40 | * 41 | * @example 42 | * Perform custom actions before saving the post 43 | * ``` 44 | * cy.createPost({ 45 | * title: 'Post Title', 46 | * beforeSave: () => { 47 | * // Change additional metaboxes. 48 | * } 49 | * }) 50 | * ``` 51 | */ 52 | export const createPost = ({ 53 | postType = 'post', 54 | title = 'Test Post', 55 | content = 'Test content', 56 | status = 'publish', 57 | beforeSave, 58 | }: { 59 | title: string; 60 | postType?: string; 61 | content?: string; 62 | status?: string; 63 | beforeSave?: CallableFunction; 64 | }): void => { 65 | cy.visit(`/wp-admin/post-new.php?post_type=${postType}`); 66 | 67 | const titleInput = 'h1.editor-post-title__input, #post-title-0'; 68 | const contentInput = '.block-editor-default-block-appender__content'; 69 | 70 | // Close Start Page Options. 71 | if (postType === 'page') { 72 | // eslint-disable-next-line cypress/no-unnecessary-waiting -- Wait for the modal to appear. Didn't find a better way to handle this. 73 | cy.wait(1500); 74 | cy.get('body').then($body => { 75 | if ($body.find('.edit-post-start-page-options__modal').length > 0) { 76 | cy.get( 77 | '.edit-post-start-page-options__modal button[aria-label="Close"]' 78 | ).click(); 79 | } else if ($body.find('.editor-start-page-options__modal').length > 0) { 80 | // WP 6.8+. 81 | cy.get( 82 | '.editor-start-page-options__modal button[aria-label="Close"]' 83 | ).click(); 84 | 85 | cy.openDocumentSettingsSidebar('Post'); 86 | 87 | // Switch out of template mode. 88 | if ( 89 | $body.find( 90 | '.editor-post-summary button[aria-label="Template options"]' 91 | ).length > 0 92 | ) { 93 | cy.get( 94 | '.editor-post-summary button[aria-label="Template options"]' 95 | ).click(); 96 | 97 | cy.get('.editor-post-template__dropdown').then($dropdown => { 98 | if ($dropdown.find('button[aria-checked="true"]').length > 0) { 99 | cy.get('button[aria-checked="true"]').click(); 100 | 101 | cy.reload(); 102 | 103 | cy.get( 104 | '.editor-start-page-options__modal button[aria-label="Close"]' 105 | ).click(); 106 | } else { 107 | cy.get( 108 | '.editor-post-summary button[aria-label="Template options"]' 109 | ).click(); 110 | } 111 | }); 112 | } 113 | } 114 | }); 115 | } 116 | 117 | // Close Welcome Guide. 118 | cy.closeWelcomeGuide(); 119 | 120 | // Fill out data. 121 | if (title.length > 0) { 122 | cy.getBlockEditor().find(titleInput).clear(); 123 | cy.getBlockEditor().find(titleInput).type(title); 124 | } 125 | 126 | if (content.length > 0) { 127 | cy.getBlockEditor().find(contentInput).click(); 128 | cy.getBlockEditor() 129 | .find('.block-editor-rich-text__editable') 130 | .first() 131 | .type(content); 132 | } 133 | 134 | if ('undefined' !== typeof beforeSave) { 135 | beforeSave(); 136 | } 137 | 138 | // Save/Publish Post. 139 | if (status === 'draft') { 140 | cy.get('.editor-post-save-draft').click(); 141 | cy.get('.editor-post-saved-state').should('have.text', 'Saved'); 142 | } else { 143 | cy.get('.editor-post-publish-panel__toggle').should('be.enabled'); 144 | cy.get('.editor-post-publish-panel__toggle').click(); 145 | 146 | cy.intercept({ method: 'POST' }, req => { 147 | const body: { 148 | status: string; 149 | title: string; 150 | } = req.body; 151 | if (body.status === 'publish' && body.title === title) { 152 | req.alias = 'publishPost'; 153 | } 154 | }); 155 | 156 | cy.get('.editor-post-publish-button').click(); 157 | 158 | cy.get('.components-snackbar, .components-notice.is-success').should( 159 | 'be.visible' 160 | ); 161 | 162 | cy.wait('@publishPost').then(response => { 163 | cy.wrap(response.response?.body); 164 | }); 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /src/commands/create-term.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a Term of a given taxonomy 3 | * 4 | * @param name - Term name 5 | * @param taxonomy - Taxonomy 6 | * @param options { 7 | * slug - Taxonomy slug 8 | * parent - Parent taxonomy (ID or name) 9 | * description - Taxonomy description 10 | * beforeSave - Callable function hook 11 | * } 12 | * 13 | * @example 14 | * Create new category with default name "Test category" 15 | * ``` 16 | * cy.createTerm() 17 | * ``` 18 | * 19 | * @example 20 | * Create new category with given name 21 | * ``` 22 | * cy.createTerm('Category') 23 | * ``` 24 | * 25 | * @example 26 | * Create a category and use it's ID 27 | * ``` 28 | * cy.createTerm('Category').then(term => { 29 | * cy.log(term.term_id); 30 | * }); 31 | * ``` 32 | * 33 | * @example 34 | * Create new term in a product taxonomy 35 | * ``` 36 | * cy.createTerm('Product name', 'product') 37 | * ``` 38 | * 39 | * @example 40 | * Create child category for existing Parent with custom description and slug 41 | * ``` 42 | * cy.createTerm('Child', 'category', { 43 | * parent: 'Parent', 44 | * slug: 'child-slug', 45 | * description: 'Custom description' 46 | * }) 47 | * ``` 48 | */ 49 | export const createTerm = ( 50 | name = 'Test category', 51 | taxonomy = 'category', 52 | { 53 | slug = '', 54 | parent = -1, 55 | description = '', 56 | beforeSave, 57 | }: { 58 | slug?: string; 59 | parent?: number | string; 60 | description?: string; 61 | beforeSave?: CallableFunction; 62 | } = {} 63 | ): void => { 64 | cy.visit(`/wp-admin/edit-tags.php?taxonomy=${taxonomy}`); 65 | 66 | cy.intercept('POST', '/wp-admin/admin-ajax.php', req => { 67 | if ('string' === typeof req.body && req.body.includes('action=add-tag')) { 68 | req.alias = 'ajaxAddTag'; 69 | } 70 | }); 71 | 72 | cy.get('#tag-name').click().type(`${name}`); 73 | 74 | if (slug) { 75 | cy.get('#tag-slug').click().type(`${slug}`); 76 | } 77 | 78 | if (description) { 79 | cy.get('#tag-description').click().type(`${description}`); 80 | } 81 | 82 | if (parent !== -1) { 83 | cy.get('body').then($body => { 84 | if ($body.find('#parent').length !== 0) { 85 | cy.get('#parent').select(parent.toString()); 86 | } 87 | }); 88 | } 89 | 90 | if ('undefined' !== typeof beforeSave) { 91 | beforeSave(); 92 | } 93 | 94 | cy.get('#submit').click(); 95 | 96 | cy.wait('@ajaxAddTag').then(response => { 97 | // WordPress AJAX result for add tag is XML document, so we parse it with jQuery. 98 | const body = Cypress.$.parseXML(response.response?.body); 99 | 100 | // Find term data. 101 | const term_data = Cypress.$(body).find('response term supplemental > *'); 102 | 103 | if (term_data.length === 0) { 104 | cy.wrap(false); 105 | return; 106 | } 107 | 108 | interface TermData { 109 | [key: string]: any; 110 | } 111 | 112 | // Extract term data into the object. 113 | const term = term_data.toArray().reduce((map: TermData, el) => { 114 | const $el = Cypress.$(el); 115 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 116 | map[$el.prop('tagName')] = $el.text(); 117 | return map; 118 | }, {}); 119 | 120 | // Sanitize numeric values. 121 | ['term_id', 'count', 'parent', 'term_group', 'term_taxonomy_id'].forEach( 122 | index => { 123 | term[index] = parseInt(term[index]); 124 | } 125 | ); 126 | 127 | cy.wrap(term); 128 | }); 129 | }; 130 | -------------------------------------------------------------------------------- /src/commands/deactivate-all-plugins.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deactivate All Plugins 3 | * 4 | * @example 5 | * ``` 6 | * cy.deactivateAllPlugins() 7 | * ``` 8 | */ 9 | export const deactivateAllPlugins = (): void => { 10 | cy.visit('/wp-admin/plugins.php'); 11 | cy.get('#cb-select-all-1').click(); 12 | cy.get('#bulk-action-selector-top').select('deactivate-selected'); 13 | cy.get('#doaction').click(); 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/deactivate-plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deactivate Plugin 3 | * 4 | * @param slug - Plugin name 5 | * 6 | * @example 7 | * Deactivate Classic Editor 8 | * ``` 9 | * cy.deactivatePlugin('classic-editor') 10 | * ``` 11 | */ 12 | export const deactivatePlugin = (slug: string): void => { 13 | cy.visit('/wp-admin/plugins.php'); 14 | cy.get(`#the-list tr[data-slug="${slug}"]`).then($pluginRow => { 15 | if ($pluginRow.find('.deactivate > a').length > 0) { 16 | cy.get(`#the-list tr[data-slug="${slug}"] .deactivate > a`) 17 | .should('have.text', 'Deactivate') 18 | .click(); 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/commands/delete-all-terms.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Delete All Terms of a given taxonomy 3 | * 4 | * @param taxonomy - Taxonomy to empty 5 | * 6 | * @example 7 | * Delete all categories (note that Uncategorized term is protected) 8 | * ``` 9 | * cy.deleteAllTerms() 10 | * ``` 11 | * 12 | * @example 13 | * Delete all tags 14 | * ``` 15 | * cy.deleteAllTerms('post_tag') 16 | * ``` 17 | */ 18 | export const deleteAllTerms = (taxonomy = 'category'): void => { 19 | cy.visit(`/wp-admin/edit-tags.php?taxonomy=${taxonomy}`); 20 | 21 | cy.get('body').then($body => { 22 | if ($body.find('#cb-select-all-1').length !== 0) { 23 | cy.get('#cb-select-all-1').click(); 24 | } 25 | 26 | if ($body.find('#bulk-action-selector-top').length !== 0) { 27 | cy.get('#bulk-action-selector-top').select('delete'); 28 | cy.get('#doaction').click(); 29 | 30 | /** 31 | * Check if the result page contain any terms 32 | * available to delete by searching for individual 33 | * checkboxes and perform recursive call. 34 | * 35 | * The 'Uncategorized' item could not be deleted 36 | * and does not have the checkbox. 37 | */ 38 | cy.get('body').then($updatedBody => { 39 | if ( 40 | $updatedBody.find( 41 | '#the-list input[type="checkbox"][name="delete_tags[]"]' 42 | ).length !== 0 43 | ) { 44 | deleteAllTerms(taxonomy); 45 | } 46 | }); 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/commands/get-block-editor.ts: -------------------------------------------------------------------------------- 1 | import { getIframe } from '../functions/get-iframe'; 2 | 3 | /** 4 | * Get the Block Editor 5 | * 6 | * @returns Block Editor element. 7 | * 8 | * @example 9 | * Find the title input and type in it 10 | * ``` 11 | * cy.getBlockEditor().find('.editor-post-title__input').type('Test Post'); 12 | * ``` 13 | */ 14 | export const getBlockEditor = (): Cypress.Chainable => { 15 | // Ensure the editor is loaded. 16 | cy.get('.edit-post-visual-editor').should('exist'); 17 | 18 | return cy 19 | .get('body') 20 | .then($body => { 21 | if ($body.find('iframe[name="editor-canvas"]').length) { 22 | return getIframe('iframe[name="editor-canvas"]'); 23 | } 24 | return $body; 25 | }) 26 | .then(cy.wrap); // eslint-disable-line @typescript-eslint/unbound-method 27 | }; 28 | -------------------------------------------------------------------------------- /src/commands/insert-block.ts: -------------------------------------------------------------------------------- 1 | import { getIframe } from '../functions/get-iframe'; 2 | 3 | /** 4 | * Inserts Block 5 | * 6 | * The resulting block id is yielded 7 | * 8 | * @param type - Block type 9 | * @param name - Block name (used to search) 10 | * 11 | * @example 12 | * ``` 13 | * cy.insertBlock('core/heading').then(id => { 14 | * cy.get(`#${id}`).click().type('A quick brown fox'); 15 | * }); 16 | * ``` 17 | */ 18 | export const insertBlock = (type: string, name?: string): void => { 19 | const [namespace = '', ...blockNameRest] = type.split('/'); 20 | let blockNames = [ 21 | blockNameRest.join('/').replace(/\//g, '-'), 22 | blockNameRest.join('/').replace(/\//g, String.raw`\/`), 23 | ]; 24 | 25 | blockNames = blockNames.filter((x, i, a) => a.indexOf(x) == i); 26 | // let blockName = blockNameRest.join('/').replace( '/', '\\/' ); 27 | 28 | let inserterBtn: Cypress.Chainable>; 29 | let search = ''; 30 | 31 | if (typeof name === 'string' && name.length) { 32 | search = name; 33 | } else { 34 | search = type; 35 | } 36 | 37 | // Start of block inserter toggle button click logic. 38 | cy.get('body').then($body => { 39 | const selectors = [ 40 | 'button[aria-label="Add block"]', // 5.7 41 | 'button[aria-label="Toggle block inserter"]', // 6.4 42 | 'button[aria-label="Block Inserter"]', // 6.8 43 | ]; 44 | 45 | selectors.forEach(selector => { 46 | if ($body.find(selector).length) { 47 | cy.get(selector).then($button => { 48 | if ($button.length) { 49 | inserterBtn = cy.wrap($button); 50 | inserterBtn.first().click(); 51 | } 52 | }); 53 | } 54 | }); 55 | }); 56 | // End of block inserter toggle button click logic. 57 | 58 | // Start of Block tab click logic. 59 | cy.get('button[role="tab"]') 60 | .contains('Blocks') 61 | .then($tab => { 62 | if ($tab.length) { 63 | cy.wrap($tab).click(); 64 | } 65 | }); 66 | // End of Block tab click logic. 67 | 68 | // Start of Block search logic. 69 | cy.get('input[placeholder="Search"]').then($input => { 70 | if ($input.length) { 71 | cy.wrap($input).type(search); 72 | } 73 | }); 74 | // End of Block search logic. 75 | 76 | blockNames.forEach(blockName => { 77 | const blockSelector = `.editor-block-list-item-${ 78 | 'core' === namespace ? '' : namespace + '-' 79 | }${blockName}`; 80 | 81 | cy.get('body').then($body => { 82 | if ($body.find(blockSelector).length) { 83 | // Start of Block insertion by click logic. 84 | cy.get(blockSelector).then($block => { 85 | if ($block.length) { 86 | cy.wrap($block).click(); 87 | inserterBtn.click(); 88 | 89 | const [ns, rest] = type.split('/'); // namespace = ns, second namespace or block name = rest 90 | 91 | cy.get('body').then($body => { 92 | if ($body.find('iframe[name="editor-canvas"]').length) { 93 | // Works with WP 6.4 94 | getIframe('iframe[name="editor-canvas"]').then($iframe => { 95 | const blockInIframe = $iframe.find( 96 | `.wp-block[data-type="${ns}/${rest}"]` 97 | ); 98 | if (blockInIframe.length > 0) { 99 | cy.wrap(blockInIframe.last().prop('id')); 100 | } 101 | }); 102 | } else if ( 103 | $body.find(`.wp-block[data-type="${ns}/${rest}"]`).length 104 | ) { 105 | // Works with WP 5.7 106 | cy.get(`.wp-block[data-type="${ns}/${rest}"]`).then( 107 | $blockInEditor => { 108 | expect($blockInEditor.length).to.equal(1); 109 | cy.wrap($blockInEditor.prop('id')); 110 | } 111 | ); 112 | } else { 113 | throw new Error(`${ns}/${rest} not found.`); 114 | } 115 | }); 116 | } 117 | }); 118 | // End of Block insertion by click logic. 119 | } 120 | }); 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /src/commands/login.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Log a user in to the WordPress dashboard. 3 | * 4 | * @param username - Username of the user 5 | * @param password - Password of the user 6 | * 7 | * @example 8 | * Use the command without any argument, the admin will be used: 9 | * ``` 10 | * cy.login() 11 | * ``` 12 | * 13 | * @example 14 | * Use the command with username and password: 15 | * ``` 16 | * cy.login( 'customer', 'strongpassword') 17 | * ``` 18 | */ 19 | export const login = (username = 'admin', password = 'password'): void => { 20 | cy.session( 21 | [username, password], 22 | () => { 23 | cy.visit('/wp-admin/'); 24 | cy.get('body').then($body => { 25 | if ($body.find('#wpwrap').length == 0) { 26 | cy.get('input#user_login').clear(); 27 | cy.get('input#user_login').click().type(username); 28 | cy.get('input#user_pass').type(`${password}{enter}`); 29 | } 30 | }); 31 | }, 32 | { 33 | validate() { 34 | cy.visit('/wp-admin/profile.php'); 35 | cy.get('#user_login').should('have.value', username); 36 | }, 37 | } 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/commands/logout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logout 3 | * 4 | * @example 5 | * ``` 6 | * cy.logout() 7 | * ``` 8 | */ 9 | export const logout = (): void => { 10 | cy.visit('/wp-admin'); 11 | cy.get('body').then($body => { 12 | if ($body.find('#wpadminbar').length !== 0) { 13 | cy.get('#wp-admin-bar-my-account').invoke('addClass', 'hover'); 14 | cy.get('#wp-admin-bar-logout > a').click(); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/commands/open-document-settings-panel.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from '../functions/capitalize'; 2 | import { ucFirst } from '../functions/uc-first'; 3 | 4 | /** 5 | * Open Document Settings Panel 6 | * 7 | * @param name - Panel name 8 | * @param tab - Settings tab 9 | * 10 | * @example 11 | * Open featured image panel of the Post editor 12 | * ``` 13 | * cy.openDocumentSettingsPanel('Featured image') 14 | * ``` 15 | * 16 | * @example 17 | * Open Permalink panel of the Page editor 18 | * ``` 19 | * cy.openDocumentSettingsPanel('Permalink', 'Page') 20 | * ``` 21 | */ 22 | export const openDocumentSettingsPanel = (name: string, tab = 'Post'): void => { 23 | // Open Settings tab 24 | cy.openDocumentSettingsSidebar(tab); 25 | 26 | // WordPress prior to 5.4 is using upper-case-words for panel names 27 | // WordPress 5.3 and below: "Status & Visibility" 28 | // WordPress 5.4 and after: "Status & visibility" 29 | const ucFirstName = ucFirst(name); 30 | const ucWordsName = capitalize(name); 31 | 32 | const panelButtonSelector = `.components-panel__body .components-panel__body-title button:contains("${ucWordsName}"),.components-panel__body .components-panel__body-title button:contains("${ucFirstName}")`; 33 | 34 | cy.get(panelButtonSelector).then($button => { 35 | // Find the panel container 36 | const $panel = $button.parents('.components-panel__body'); 37 | 38 | // Only click the button if the panel is collapsed 39 | if (!$panel.hasClass('is-opened')) { 40 | cy.wrap($button) 41 | .click() 42 | .parents('.components-panel__body') 43 | .should('have.class', 'is-opened'); 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/commands/open-document-settings-sidebar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Open Document Settings Sidebar 3 | * 4 | * @param tab - Name of the tab 5 | * 6 | * @example 7 | * Open 'Post' tab 8 | * ``` 9 | * cy.openDocumentSettingsSidebar() 10 | * ``` 11 | * 12 | * @example 13 | * Open 'Block' tab 14 | * ``` 15 | * cy.openDocumentSettingsSidebar('Block') 16 | * ``` 17 | */ 18 | export const openDocumentSettingsSidebar = (tab = 'Post'): void => { 19 | cy.get('body').then($body => { 20 | const $settingButtonIds = [ 21 | 'button[aria-expanded="false"][aria-label="Settings"]', 22 | ]; 23 | 24 | $settingButtonIds.forEach($settingButtonId => { 25 | if ($body.find($settingButtonId).length) { 26 | cy.get($settingButtonId).first().click(); 27 | cy.wrap($body.find($settingButtonId).first()).as('sidebarButton'); 28 | } 29 | }); 30 | 31 | const $tabSelectors = [ 32 | `div[role="tablist"] button:contains("${tab}")`, 33 | `.edit-post-sidebar__panel-tabs button:contains("${tab}")`, 34 | ]; 35 | 36 | $tabSelectors.forEach($tabSelector => { 37 | if ($body.find($tabSelector).length) { 38 | cy.get($tabSelector).first().click(); 39 | cy.wrap($body.find($tabSelector).first()).as('selectedTab'); 40 | } 41 | }); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/commands/set-permalink-structure.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Set Permalink Structure 3 | * 4 | * @param type - Permalink structure, should contain structure tags or be an empty string (which means "Plain" structure) 5 | * 6 | * @example 7 | * ``` 8 | * cy.setPermalinkStructure('/%year%/%postname%/') 9 | * ``` 10 | */ 11 | export const setPermalinkStructure = (type: string): void => { 12 | cy.visit('/wp-admin/options-permalink.php'); 13 | cy.get('#permalink_structure').click().clear(); 14 | if ('' !== type) { 15 | cy.get('#permalink_structure').click().type(type); 16 | } 17 | cy.get('#submit').click(); 18 | }; 19 | -------------------------------------------------------------------------------- /src/commands/upload-media.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Upload a media file 3 | * 4 | * @param filePath - A path to a file within the project root (Eg: 'tests/cypress/fixtures/10up.png'). 5 | * 6 | * @returns Media ID with upload status. eg: `{ success: true, mediaId: 123}`, 7 | * for failure `{ success: false, errorMessage: '"file" has failed to upload' }` 8 | * 9 | * @example 10 | * Upload a media file. 11 | * ``` 12 | * cy.uploadMedia('tests/cypress/fixtures/image.png') 13 | * ``` 14 | */ 15 | export const uploadMedia = (filePath: string): void => { 16 | cy.visit('/wp-admin/media-new.php'); 17 | 18 | // wait for drag-drop area become active. 19 | cy.get('.drag-drop').should('exist'); 20 | cy.get('#drag-drop-area').should('exist'); 21 | 22 | // intercept media upload request. 23 | cy.intercept('POST', '**/async-upload.php').as('uploadMediaRequest'); 24 | 25 | // Upload file. 26 | cy.get('#drag-drop-area').selectFile(filePath, { action: 'drag-drop' }); 27 | 28 | // Wait for file upload complete. 29 | cy.wait('@uploadMediaRequest').then(response => { 30 | if (response.response?.body && !isNaN(response.response?.body)) { 31 | cy.wrap({ 32 | success: true, 33 | mediaId: response.response?.body, 34 | }); 35 | } else { 36 | let errorMessage = ''; 37 | cy.get('.media-item .error-div.error').then(ele => { 38 | if (ele) { 39 | errorMessage = ele.text().replace('Dismiss', ''); 40 | } 41 | cy.wrap({ 42 | success: false, 43 | errorMessage, 44 | }); 45 | }); 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/commands/wp-cli-eval.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run PHP code as WP CLI eval-file command 3 | * 4 | * @param command - PHP code to execute with WP CLI. The '{ 9 | * const output = response.stdout; 10 | * // Do whatever with the output. 11 | * }) 12 | * ``` 13 | */ 14 | export const wpCliEval = (command: string): void => { 15 | const fileName = (Math.random() + 1).toString(36).substring(7); 16 | 17 | // this will be written "local" plugin directory 18 | const escapedCommand = command.replace(/^<\?php /, ''); 19 | cy.writeFile(fileName, ` { 22 | const pluginName = result.stdout; 23 | 24 | // which is read from it's proper location in the plugins directory 25 | cy.exec( 26 | `npm --silent run env run tests-cli -- wp eval-file wp-content/plugins/${pluginName}/${fileName}` // eslint-disable-line @typescript-eslint/restrict-template-expressions 27 | ).then(result => { 28 | cy.exec(`rm ${fileName}`); 29 | cy.wrap(result); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/commands/wp-cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Perform a WP CLI command 3 | * 4 | * @param command - WP CLI command. The 'wp ' prefix is optional 5 | * @param ignoreFailures - Prevent command to fail if CLI command exits with error 6 | * 7 | * @example 8 | * ``` 9 | * cy.wpCli('plugin list --field=name').then(response=>{ 10 | * const plugins = res.stdout.split('\n'); 11 | * // Do whatever with plugins list 12 | * }); 13 | * ``` 14 | */ 15 | export const wpCli = (command: string, ignoreFailures = false): void => { 16 | const escapedCommand = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 17 | const options = { 18 | failOnNonZeroExit: !ignoreFailures, 19 | }; 20 | cy.exec( 21 | `npm --silent run env run tests-cli -- ${escapedCommand}`, 22 | options 23 | ).then(result => { 24 | cy.wrap(result); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/functions/capitalize.ts: -------------------------------------------------------------------------------- 1 | export const capitalize = (str: string, lower = true) => 2 | (lower ? str.toLowerCase() : str).replace(/(?:^|\s|["'([{])+\S/g, match => 3 | match.toUpperCase() 4 | ); 5 | -------------------------------------------------------------------------------- /src/functions/get-iframe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code taken from the Cypress iframe package. 3 | * 4 | * https://gitlab.com/kgroat/cypress-iframe 5 | */ 6 | 7 | const DEFAULT_OPTS: Cypress.Loggable & Cypress.Timeoutable = { 8 | log: true, 9 | timeout: 30000, 10 | }; 11 | 12 | const DEFAULT_IFRAME_SELECTOR = 'iframe'; 13 | 14 | function sleep(timeout: number) { 15 | return new Promise(resolve => setTimeout(resolve, timeout)); 16 | } 17 | 18 | const frameLoaded: Cypress.Chainable['frameLoaded'] = ( 19 | selector?: string | Partial, 20 | opts?: Partial 21 | ) => { 22 | if (selector === undefined) { 23 | selector = DEFAULT_IFRAME_SELECTOR; 24 | } else if (typeof selector === 'object') { 25 | opts = selector; 26 | selector = DEFAULT_IFRAME_SELECTOR; 27 | } 28 | 29 | const fullOpts: Cypress.IframeOptions = { 30 | ...DEFAULT_OPTS, 31 | ...opts, 32 | }; 33 | 34 | const log = fullOpts.log 35 | ? Cypress.log({ 36 | name: 'frame loaded', 37 | displayName: 'frame loaded', 38 | message: [selector], 39 | }).snapshot() 40 | : null; 41 | 42 | return cy 43 | .get(selector, { log: false }) 44 | .then( 45 | { timeout: fullOpts.timeout }, 46 | async ($frame: JQuery) => { 47 | log?.set('$el', $frame); 48 | 49 | if ($frame.length !== 1) { 50 | throw new Error( 51 | `cypress-iframe commands can only be applied to exactly one iframe at a time. Instead found ${$frame.length}` 52 | ); 53 | } 54 | 55 | const contentWindow: Window = $frame.prop('contentWindow'); 56 | 57 | const hasNavigated = fullOpts.url 58 | ? () => 59 | typeof fullOpts.url === 'string' 60 | ? contentWindow.location.toString().includes(fullOpts.url) 61 | : fullOpts.url?.test(contentWindow.location.toString()) 62 | : () => contentWindow.location.toString() !== 'about:blank'; 63 | 64 | while (!hasNavigated()) { 65 | await sleep(100); 66 | } 67 | 68 | if (contentWindow.document.readyState === 'complete') { 69 | return $frame; 70 | } 71 | 72 | const loadLog = Cypress.log({ 73 | name: 'Frame Load', 74 | message: [contentWindow.location.toString()], 75 | event: true, 76 | } as any).snapshot(); 77 | 78 | await new Promise(resolve => { 79 | Cypress.$(contentWindow).on('load', resolve); 80 | }); 81 | 82 | loadLog.end(); 83 | log?.finish(); 84 | 85 | return $frame; 86 | } 87 | ); 88 | }; 89 | 90 | export const getIframe: Cypress.Chainable['iframe'] = ( 91 | selector?: string | Partial, 92 | opts?: Partial 93 | ) => { 94 | if (selector === undefined) { 95 | selector = DEFAULT_IFRAME_SELECTOR; 96 | } else if (typeof selector === 'object') { 97 | opts = selector; 98 | selector = DEFAULT_IFRAME_SELECTOR; 99 | } 100 | 101 | const fullOpts: Cypress.IframeOptions = { 102 | ...DEFAULT_OPTS, 103 | ...opts, 104 | }; 105 | 106 | const log = fullOpts.log 107 | ? Cypress.log({ 108 | name: 'iframe', 109 | displayName: 'iframe', 110 | message: [selector], 111 | }).snapshot() 112 | : null; 113 | 114 | return frameLoaded(selector, { ...fullOpts, log: false }).then($frame => { 115 | log?.set('$el', $frame).end(); 116 | const contentWindow: Window = $frame.prop('contentWindow'); 117 | return Cypress.$(contentWindow.document.body as HTMLBodyElement); 118 | }); 119 | }; 120 | -------------------------------------------------------------------------------- /src/functions/uc-first.ts: -------------------------------------------------------------------------------- 1 | export const ucFirst = (string: string) => 2 | string.toLowerCase().charAt(0).toUpperCase() + string.toLowerCase().slice(1); 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | /// 3 | 4 | // Import commands. 5 | import { checkPostExists } from './commands/check-post-exists'; 6 | import { classicCreatePost } from './commands/classic-create-post'; 7 | import { insertBlock } from './commands/insert-block'; 8 | import { closeWelcomeGuide } from './commands/close-welcome-guide'; 9 | import { wpCliEval } from './commands/wp-cli-eval'; 10 | import { wpCli } from './commands/wp-cli'; 11 | import { deactivatePlugin } from './commands/deactivate-plugin'; 12 | import { activateAllPlugins } from './commands/activate-all-plugins'; 13 | import { deactivateAllPlugins } from './commands/deactivate-all-plugins'; 14 | import { activatePlugin } from './commands/activate-plugin'; 15 | import { setPermalinkStructure } from './commands/set-permalink-structure'; 16 | import { openDocumentSettingsPanel } from './commands/open-document-settings-panel'; 17 | import { openDocumentSettingsSidebar } from './commands/open-document-settings-sidebar'; 18 | import { checkBlockPatternExists } from './commands/check-block-pattern-exists'; 19 | import { deleteAllTerms } from './commands/delete-all-terms'; 20 | import { createTerm } from './commands/create-term'; 21 | import { logout } from './commands/logout'; 22 | import { login } from './commands/login'; 23 | import { createPost } from './commands/create-post'; 24 | import { uploadMedia } from './commands/upload-media'; 25 | import { checkSitemap } from './commands/check-sitemap-exists'; 26 | import { getBlockEditor } from './commands/get-block-editor'; 27 | 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | checkPostExists: typeof checkPostExists; 32 | classicCreatePost: typeof classicCreatePost; 33 | insertBlock: typeof insertBlock; 34 | closeWelcomeGuide: typeof closeWelcomeGuide; 35 | wpCliEval: typeof wpCliEval; 36 | wpCli: typeof wpCli; 37 | deactivatePlugin: typeof deactivatePlugin; 38 | activateAllPlugins: typeof activateAllPlugins; 39 | deactivateAllPlugins: typeof deactivateAllPlugins; 40 | activatePlugin: typeof activatePlugin; 41 | setPermalinkStructure: typeof setPermalinkStructure; 42 | openDocumentSettingsPanel: typeof openDocumentSettingsPanel; 43 | openDocumentSettingsSidebar: typeof openDocumentSettingsSidebar; 44 | checkBlockPatternExists: typeof checkBlockPatternExists; 45 | deleteAllTerms: typeof deleteAllTerms; 46 | createTerm: typeof createTerm; 47 | createPost: typeof createPost; 48 | uploadMedia: typeof uploadMedia; 49 | logout: typeof logout; 50 | login: typeof login; 51 | checkSitemap: typeof checkSitemap; 52 | getBlockEditor: typeof getBlockEditor; 53 | frameLoaded: IframeHandler>; 54 | iframe: IframeHandler>; 55 | } 56 | 57 | interface IframeHandler { 58 | (options?: Partial): Chainable; 59 | (selector: string, options?: Partial): Chainable; 60 | } 61 | 62 | interface IframeOptions extends Loggable, Timeoutable { 63 | url?: RegExp | string; 64 | } 65 | } 66 | } 67 | 68 | // Register commands 69 | Cypress.Commands.add('checkPostExists', checkPostExists); 70 | Cypress.Commands.add('classicCreatePost', classicCreatePost); 71 | Cypress.Commands.add('insertBlock', insertBlock); 72 | Cypress.Commands.add('closeWelcomeGuide', closeWelcomeGuide); 73 | Cypress.Commands.add('wpCliEval', wpCliEval); 74 | Cypress.Commands.add('wpCli', wpCli); 75 | Cypress.Commands.add('deactivatePlugin', deactivatePlugin); 76 | Cypress.Commands.add('activateAllPlugins', activateAllPlugins); 77 | Cypress.Commands.add('deactivateAllPlugins', deactivateAllPlugins); 78 | Cypress.Commands.add('activatePlugin', activatePlugin); 79 | Cypress.Commands.add('setPermalinkStructure', setPermalinkStructure); 80 | Cypress.Commands.add('checkBlockPatternExists', checkBlockPatternExists); 81 | Cypress.Commands.add('openDocumentSettingsPanel', openDocumentSettingsPanel); 82 | Cypress.Commands.add( 83 | 'openDocumentSettingsSidebar', 84 | openDocumentSettingsSidebar 85 | ); 86 | Cypress.Commands.add('deleteAllTerms', deleteAllTerms); 87 | Cypress.Commands.add('createTerm', createTerm); 88 | Cypress.Commands.add('createPost', createPost); 89 | Cypress.Commands.add('uploadMedia', uploadMedia); 90 | Cypress.Commands.add('logout', logout); 91 | Cypress.Commands.add('login', login); 92 | Cypress.Commands.add('checkSitemap', checkSitemap); 93 | Cypress.Commands.add('getBlockEditor', getBlockEditor); 94 | -------------------------------------------------------------------------------- /src/interface/post-data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Post Data 3 | */ 4 | export default interface PostData { 5 | /** 6 | * Post title 7 | */ 8 | title: string; 9 | 10 | /** 11 | * Post type 12 | */ 13 | postType?: string; 14 | 15 | /** 16 | * Post content 17 | */ 18 | content?: string; 19 | 20 | /** 21 | * Post status 22 | */ 23 | status?: string; 24 | 25 | /** 26 | * Before save callback 27 | */ 28 | beforeSave?: CallableFunction; 29 | } 30 | -------------------------------------------------------------------------------- /tests/bin/initialize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wp-env run tests-wordpress chmod -c ugo+w /var/www/html 3 | wp-env run tests-cli wp rewrite structure '/%postname%/' --hard 4 | 5 | wp-env run tests-cli wp user create user1 user1@example.test --role=author --user_pass=password1 6 | wp-env run tests-cli wp user create user2 user2@example.test --role=author --user_pass=password2 7 | -------------------------------------------------------------------------------- /tests/bin/set-core-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | 5 | const path = `${process.cwd()}/.wp-env.override.json`; 6 | 7 | let config = fs.existsSync(path) ? require(path) : {}; 8 | 9 | const args = process.argv.slice(2); 10 | 11 | if (args.length == 0) return; 12 | 13 | if (args[0] == 'latest') { 14 | if (fs.existsSync(path)) { 15 | fs.unlinkSync(path); 16 | } 17 | return; 18 | } 19 | 20 | config.core = args[0]; 21 | 22 | if (!config.core.match(/^WordPress\/WordPress\#/)) { 23 | config.core = 'WordPress/WordPress#' + config.core; 24 | } 25 | 26 | try { 27 | fs.writeFileSync(path, JSON.stringify(config)); 28 | } catch (err) { 29 | console.error(err); 30 | } 31 | -------------------------------------------------------------------------------- /tests/bin/wp-cli.yml: -------------------------------------------------------------------------------- 1 | apache_modules: 2 | - mod_rewrite 3 | -------------------------------------------------------------------------------- /tests/cypress/cypress-config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress'); 2 | const { loadConfig } = require('@wordpress/env/lib/config'); 3 | const getCacheDirectory = require('@wordpress/env/lib/config/get-cache-directory'); 4 | 5 | module.exports = defineConfig({ 6 | chromeWebSecurity: false, 7 | fixturesFolder: 'tests/cypress/fixtures', 8 | screenshotsFolder: 'tests/cypress/screenshots', 9 | videosFolder: 'tests/cypress/videos', 10 | downloadsFolder: 'tests/cypress/downloads', 11 | video: false, 12 | e2e: { 13 | setupNodeEvents(on, config) { 14 | return setBaseUrl(on, config); 15 | }, 16 | specPattern: 'tests/cypress/e2e/**/*.test.{js,jsx,ts,tsx}', 17 | supportFile: 'tests/cypress/support/e2e.js', 18 | }, 19 | reporter: 'mochawesome', 20 | reporterOptions: { 21 | reportFilename: 'mochawesome-[name]', 22 | reportDir: 'tests/cypress/reports', 23 | overwrite: false, 24 | html: false, 25 | json: true, 26 | }, 27 | }); 28 | 29 | /** 30 | * Set WP URL as baseUrl in Cypress config. 31 | * 32 | * @param {Function} on function that used to register listeners on various events. 33 | * @param {object} config Cypress Config object. 34 | * @returns config Updated Cypress Config object. 35 | */ 36 | const setBaseUrl = async (on, config) => { 37 | const cacheDirectory = await getCacheDirectory(); 38 | const wpEnvConfig = await loadConfig(cacheDirectory); 39 | 40 | if (wpEnvConfig) { 41 | const port = wpEnvConfig.env.tests.port || null; 42 | 43 | if (port) { 44 | config.baseUrl = wpEnvConfig.env.tests.config.WP_SITEURL; 45 | } 46 | } 47 | 48 | return config; 49 | }; 50 | -------------------------------------------------------------------------------- /tests/cypress/e2e/check-post-exists.test.js: -------------------------------------------------------------------------------- 1 | const { randomName } = require('../support/functions'); 2 | 3 | describe('Command: checkPostExists', () => { 4 | const tests = [ 5 | { 6 | postType: 'post', 7 | postTitle: 'Post ' + randomName(), 8 | expected: true, 9 | }, 10 | { 11 | postType: 'post', 12 | postTitle: 'Post ' + randomName(), 13 | expected: false, 14 | }, 15 | { 16 | postType: 'page', 17 | postTitle: 'Page ' + randomName(), 18 | expected: true, 19 | }, 20 | { 21 | postType: 'page', 22 | postTitle: 'Post ' + randomName(), 23 | expected: false, 24 | }, 25 | ]; 26 | 27 | before(() => { 28 | cy.login(); 29 | cy.deactivatePlugin('classic-editor'); 30 | 31 | // Ignore WP 5.2 Synchronous XHR error. 32 | Cypress.on('uncaught:exception', (err, runnable) => { 33 | if ( 34 | err.message.includes( 35 | "Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'http://localhost:8889/wp-admin/admin-ajax.php': Synchronous XHR in page dismissal" 36 | ) 37 | ) { 38 | return false; 39 | } 40 | }); 41 | 42 | // Run the tests before seeding any posts to ensure we get false. 43 | tests.forEach(test => { 44 | it(`${test.postTitle} should not exist`, () => { 45 | const args = { 46 | title: test.postTitle, 47 | }; 48 | 49 | // make 'postType' argument optional to test default value 50 | if ('post' !== test.postType) { 51 | args.postType = test.postType; 52 | } 53 | cy.checkPostExists(args).then(exists => { 54 | assert(exists === false, `Post should not exist`); 55 | }); 56 | }); 57 | }); 58 | 59 | // Create posts which expected to exist during tests 60 | tests.forEach(test => { 61 | if (test.expected) { 62 | cy.createPost({ 63 | postType: test.postType, 64 | title: test.postTitle, 65 | }); 66 | } 67 | }); 68 | }); 69 | 70 | beforeEach(() => { 71 | cy.login(); 72 | }); 73 | 74 | // Run the tests again after seeding posts to ensure we get the correct response. 75 | tests.forEach(test => { 76 | const shouldIt = test.expected ? 'should' : 'should not'; 77 | it(`${test.postTitle} ${shouldIt} exist`, () => { 78 | const args = { 79 | title: test.postTitle, 80 | }; 81 | 82 | // make 'postType' argument optional to test default value 83 | if ('post' !== test.postType) { 84 | args.postType = test.postType; 85 | } 86 | cy.checkPostExists(args).then(exists => { 87 | assert(exists === test.expected, `Post ${shouldIt} exist`); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/cypress/e2e/check-sitemap-exists.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: checkSitemap', () => { 2 | it('Test sitemap existence', () => { 3 | cy.checkSitemap(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/cypress/e2e/classic-create-post.test.js: -------------------------------------------------------------------------------- 1 | const { randomName } = require('../support/functions'); 2 | 3 | describe('Command: classicCreatePost', () => { 4 | before(() => { 5 | cy.login(); 6 | cy.activatePlugin('classic-editor'); 7 | }); 8 | 9 | beforeEach(() => { 10 | cy.login(); 11 | }); 12 | 13 | it('Should be able to Classic Create Post', () => { 14 | const title = 'Title ' + randomName(); 15 | const content = 'Content ' + randomName(); 16 | cy.classicCreatePost({ 17 | title: title, 18 | content: content, 19 | }); 20 | 21 | cy.get('.notice-success').should('contain.text', 'Post published'); 22 | cy.get('#title').should('have.value', title); 23 | cy.get('#content_ifr').then($iframe => { 24 | const doc = $iframe.contents().find('body#tinymce'); 25 | cy.wrap(doc).should('contain.text', content); 26 | }); 27 | }); 28 | 29 | it('Should wrap post ID', () => { 30 | const title = 'Title ' + randomName(); 31 | const content = 'Content ' + randomName(); 32 | cy.classicCreatePost({ 33 | title: title, 34 | content: content, 35 | }).then(id => { 36 | cy.visit(`/wp-admin/post.php?post=${id}&action=edit`); 37 | cy.get('#title').should('have.value', title); 38 | }); 39 | }); 40 | 41 | it('Should perform beforeSave', () => { 42 | const title = 'Title ' + randomName(); 43 | const content = 'Content ' + randomName(); 44 | const additional = 'Content ' + randomName(); 45 | cy.classicCreatePost({ 46 | title: title, 47 | content: content, 48 | beforeSave: () => { 49 | cy.get('#content_ifr').then($iframe => { 50 | const doc = $iframe.contents().find('body#tinymce'); 51 | cy.wrap(doc) 52 | .find('p:last-child') 53 | .type('{enter}' + additional); 54 | }); 55 | }, 56 | }); 57 | 58 | cy.get('.notice-success').should('contain.text', 'Post published'); 59 | cy.get('#title').should('have.value', title); 60 | cy.get('#content_ifr').then($iframe => { 61 | const doc = $iframe.contents().find('body#tinymce'); 62 | cy.wrap(doc) 63 | .should('contain.text', content) 64 | .should('contain.text', additional); 65 | }); 66 | }); 67 | 68 | it('Should save draft', () => { 69 | const title = 'Title ' + randomName(); 70 | const content = 'Content ' + randomName(); 71 | cy.classicCreatePost({ 72 | title: title, 73 | content: content, 74 | status: 'draft', 75 | }); 76 | 77 | cy.get('.notice-success').should('contain.text', 'Post draft updated'); 78 | }); 79 | 80 | it('Should be able to create page', () => { 81 | const title = 'Title ' + randomName(); 82 | const content = 'Content ' + randomName(); 83 | cy.classicCreatePost({ 84 | title: title, 85 | content: content, 86 | postType: 'page', 87 | }); 88 | 89 | cy.get('.notice-success').should('contain.text', 'Page published'); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/cypress/e2e/close-welcome-guide.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: closeWelcomeGuide', () => { 2 | before(() => { 3 | cy.login(); 4 | 5 | cy.deactivatePlugin('classic-editor'); 6 | 7 | // Disable Classic Editor if it's enabled 8 | cy.visit('/wp-admin/options-writing.php'); 9 | cy.get('body').then($body => { 10 | if ( 11 | $body.find('.classic-editor-options').length !== 0 && 12 | $body.find('#classic-editor-classic').is(':checked') 13 | ) { 14 | cy.get('#classic-editor-block').click(); 15 | cy.get('#submit').click(); 16 | } 17 | }); 18 | 19 | // Ignore WP 5.2 Synchronous XHR error. 20 | Cypress.on('uncaught:exception', (err, runnable) => { 21 | if ( 22 | err.message.includes( 23 | "Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'http://localhost:8889/wp-admin/admin-ajax.php': Synchronous XHR in page dismissal" 24 | ) 25 | ) { 26 | return false; 27 | } 28 | }); 29 | }); 30 | 31 | beforeEach(() => { 32 | cy.login(); 33 | }); 34 | 35 | it('Should be able to Close Welcome Guide', () => { 36 | const welcomeGuideWindow = '.edit-post-welcome-guide'; 37 | 38 | cy.visit('/wp-admin/post-new.php'); 39 | cy.closeWelcomeGuide(); 40 | cy.get(welcomeGuideWindow).should('not.exist'); 41 | }); 42 | 43 | it('Should not fail closing Welcome Guide', () => { 44 | const welcomeGuideWindow = '.edit-post-welcome-guide'; 45 | 46 | cy.visit('/wp-admin/post-new.php'); 47 | cy.closeWelcomeGuide(); 48 | cy.get(welcomeGuideWindow).should('not.exist'); 49 | 50 | cy.visit('/wp-admin/post-new.php'); 51 | cy.closeWelcomeGuide(); 52 | cy.get(welcomeGuideWindow).should('not.exist'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/cypress/e2e/create-post.test.js: -------------------------------------------------------------------------------- 1 | const { randomName } = require('../support/functions'); 2 | 3 | describe('Command: createPost', () => { 4 | before(() => { 5 | cy.login(); 6 | 7 | cy.deactivatePlugin('classic-editor'); 8 | 9 | // Ignore WP 5.2 Synchronous XHR error. 10 | Cypress.on('uncaught:exception', (err, runnable) => { 11 | if ( 12 | err.message.includes( 13 | "Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'http://localhost:8889/wp-admin/admin-ajax.php': Synchronous XHR in page dismissal" 14 | ) 15 | ) { 16 | return false; 17 | } 18 | }); 19 | }); 20 | 21 | beforeEach(() => { 22 | cy.login(); 23 | }); 24 | 25 | it('Should be able to create Post', () => { 26 | const title = 'Test Post'; 27 | cy.createPost({ 28 | title, 29 | content: 'Test Content', 30 | }); 31 | 32 | cy.visit('/wp-admin/edit.php?orderby=date&order=desc'); 33 | cy.get('#the-list td.title a.row-title').first().should('have.text', title); 34 | }); 35 | 36 | it('Should be able to create Draft Post', () => { 37 | const title = 'Test Draft Post'; 38 | cy.createPost({ 39 | title, 40 | status: 'draft', 41 | content: 'Test Draft Content', 42 | }); 43 | 44 | cy.visit('/wp-admin/edit.php?orderby=date&order=desc'); 45 | cy.get('#the-list td.title') 46 | .first() 47 | .then($row => { 48 | cy.wrap($row).find('a.row-title').should('have.text', title); 49 | cy.wrap($row).find('span.post-state').should('have.text', 'Draft'); 50 | }); 51 | }); 52 | 53 | it('Should be able to create Page', () => { 54 | const title = 'Test page'; 55 | cy.createPost({ 56 | title, 57 | content: 'page Content', 58 | postType: 'page', 59 | }); 60 | 61 | cy.visit('/wp-admin/edit.php?post_type=page&orderby=date&order=desc'); 62 | cy.get('#the-list td.title a.row-title').first().should('have.text', title); 63 | }); 64 | 65 | it('Should be able to create Draft Page', () => { 66 | const title = 'Test Draft Page'; 67 | cy.createPost({ 68 | title, 69 | status: 'draft', 70 | content: 'Test Draft Content', 71 | postType: 'page', 72 | }); 73 | 74 | cy.visit('/wp-admin/edit.php?post_type=page&orderby=date&order=desc'); 75 | cy.get('#the-list td.title') 76 | .first() 77 | .then($row => { 78 | cy.wrap($row).find('a.row-title').should('have.text', title); 79 | cy.wrap($row).find('span.post-state').should('have.text', 'Draft'); 80 | }); 81 | }); 82 | 83 | it('Should be able to use beforeSave hook', () => { 84 | const postTitle = 'Post ' + randomName(); 85 | const postContent = 'Content ' + randomName(); 86 | cy.createPost({ 87 | title: postTitle, 88 | content: postContent, 89 | beforeSave: () => { 90 | // WP 6.1 renamed the panel name "Status & visibility" to "Summary". 91 | cy.openDocumentSettingsSidebar('Post'); 92 | cy.get('body').then($body => { 93 | let name = 'Status & visibility'; 94 | if ( 95 | $body.find( 96 | '.components-panel__body .components-panel__body-title button:contains("Summary")' 97 | ).length > 0 98 | ) { 99 | name = 'Summary'; 100 | } 101 | 102 | // WP 6.6 handling. 103 | if ($body.find('.editor-post-summary').length === 0) { 104 | cy.openDocumentSettingsPanel(name); 105 | 106 | cy.get('label') 107 | .contains('Stick to the top of the blog') 108 | .click() 109 | .parent() 110 | .find('input[type="checkbox"]') 111 | .should('be.checked'); 112 | } else if ($body.find('.editor-post-sticky__toggle-control').length) { 113 | cy.get( 114 | '.editor-post-sticky__toggle-control input[type="checkbox"]' 115 | ).check(); 116 | cy.get( 117 | '.editor-post-sticky__toggle-control input[type="checkbox"]' 118 | ).should('be.checked'); 119 | } else if ($body.find('.editor-post-status__toggle').length) { 120 | // WP 6.7+ handling. 121 | cy.get('.editor-post-status__toggle').click(); 122 | cy.get( 123 | '.editor-post-sticky__checkbox-control input[type="checkbox"]' 124 | ).check(); 125 | } 126 | }); 127 | }, 128 | }); 129 | 130 | cy.visit('/wp-admin/edit.php?post_type=post'); 131 | cy.get('td.title') 132 | .contains(postTitle) 133 | .parent() 134 | .find('.post-state') 135 | .should('contain.text', 'Sticky'); 136 | }); 137 | 138 | it('Should be able to get published post details', () => { 139 | const postTitle = 'Post ' + randomName(); 140 | const postContent = 'Content ' + randomName(); 141 | cy.createPost({ 142 | title: postTitle, 143 | content: postContent, 144 | }).then(post => { 145 | assert(post.title.raw === postTitle, 'Post title is the same'); 146 | assert( 147 | post.content.rendered.includes(postContent), 148 | 'Post content is the same' 149 | ); 150 | }); 151 | }); 152 | 153 | it('Should be able to create Post without title', () => { 154 | cy.createPost({ 155 | title: '', 156 | content: 'Test Content', 157 | }); 158 | 159 | cy.visit('/wp-admin/edit.php?orderby=date&order=desc'); 160 | cy.get('#the-list td.title a.row-title') 161 | .first() 162 | .should(element => { 163 | // WordPress changed the default title for posts without a title at some point. 164 | expect(element.text()).to.be.oneOf(['(no title)', 'Untitled']); 165 | }); 166 | }); 167 | 168 | it('Should be able to create Post without content', () => { 169 | const title = 'No Content Post'; 170 | cy.createPost({ 171 | title, 172 | content: '', 173 | }).then(post => { 174 | assert(post.title.raw === title, 'Post title is the same'); 175 | assert(post.content.rendered.length === 0, 'Post content is empty'); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /tests/cypress/e2e/create-term.test.js: -------------------------------------------------------------------------------- 1 | const { randomName } = require('../support/functions'); 2 | 3 | describe('Command: createTerm', () => { 4 | beforeEach(() => { 5 | cy.login(); 6 | cy.deleteAllTerms(); 7 | cy.deleteAllTerms('post_tag'); 8 | }); 9 | 10 | it('Should be able to Create a category', () => { 11 | const termName = 'Category ' + randomName(); 12 | cy.createTerm(termName); 13 | cy.get(`.row-title:contains("${termName}")`).should('exist'); 14 | }); 15 | 16 | it('Should be able to Create a tag', () => { 17 | const termName = 'Tag ' + randomName(); 18 | cy.createTerm(termName, 'post_tag'); 19 | cy.get(`.row-title:contains("${termName}")`).should('exist'); 20 | }); 21 | 22 | it('Duplicate category should not be created', () => { 23 | const termName = 'Category ' + randomName(); 24 | cy.createTerm(termName); 25 | cy.get(`.row-title:contains("${termName}")`).should('exist'); 26 | 27 | cy.createTerm(termName); 28 | cy.get('.error, .notice-error').should( 29 | 'contain', 30 | 'A term with the name provided already exists with this parent' 31 | ); 32 | }); 33 | 34 | it('Duplicate tag should not be created', () => { 35 | const termName = 'Tag ' + randomName(); 36 | cy.createTerm(termName, 'post_tag'); 37 | cy.get(`.row-title:contains("${termName}")`).should('exist'); 38 | 39 | cy.createTerm(termName, 'post_tag'); 40 | cy.get('.error, .notice-error').should( 41 | 'contain', 42 | 'A term with the name provided already exists in this taxonomy' 43 | ); 44 | }); 45 | 46 | it('Should create categories with options', () => { 47 | const parentCategory = { 48 | name: 'Parent ' + randomName(), 49 | description: 'Description ' + randomName(), 50 | slug: 'parent-slug', 51 | }; 52 | 53 | cy.createTerm(parentCategory.name, 'category', { 54 | description: parentCategory.description, 55 | slug: parentCategory.slug, 56 | }); 57 | 58 | // Assertions for parent category 59 | cy.get(`.row-title:contains("${parentCategory.name}")`).then( 60 | $parentLink => { 61 | // Assertions of parent category 62 | const $parentRow = $parentLink.parents('tr'); 63 | 64 | cy.wrap($parentRow) 65 | .get('.description') 66 | .should('contain', parentCategory.description); 67 | 68 | cy.wrap($parentRow).get('.slug').should('contain', parentCategory.slug); 69 | 70 | const parentId = $parentRow.find('input[name="delete_tags[]"]').val(); 71 | 72 | const childById = 'Child ' + randomName(); 73 | cy.createTerm(childById, 'category', { parent: parentId }); 74 | cy.get(`.row-title:contains("${childById}")`).then($child => { 75 | cy.wrap($child.parents('tr')) 76 | .get('.name .parent') 77 | .should('contain', parentId.toString()); 78 | }); 79 | 80 | const childByName = 'Child ' + randomName(); 81 | cy.createTerm(childByName, 'category', { 82 | parent: parentCategory.name, 83 | }); 84 | cy.get(`.row-title:contains("${childByName}")`).then($child => { 85 | cy.wrap($child.parents('tr')) 86 | .get('.name .parent') 87 | .should('contain', parentId.toString()); 88 | }); 89 | } 90 | ); 91 | }); 92 | 93 | it('Should retrieve term data from the command', () => { 94 | const randomSuffix = randomName(); 95 | const termName = 'Category ' + randomSuffix; 96 | const expectedSlug = 'category-' + randomSuffix; 97 | cy.createTerm(termName).then(term => { 98 | assert(term.name === termName, 'Term name is the same'); 99 | assert(term.term_id > 0, 'Term ID should be greater than 0'); 100 | assert(term.slug === expectedSlug, 'Should have correct term slug'); 101 | }); 102 | }); 103 | 104 | it('Should be able to use beforeSave hook', () => { 105 | const termName = 'Category ' + randomName(); 106 | const termSlug = 'cat-' + randomName(); 107 | const overrideSlug = 'cat-' + randomName(); 108 | cy.createTerm(termName, 'category', { 109 | slug: termSlug, 110 | beforeSave: () => { 111 | cy.get('#tag-slug').click().type('{selectAll}').type(overrideSlug); 112 | }, 113 | }); 114 | 115 | cy.get('td.name') 116 | .contains(termName) 117 | .parents('tr') 118 | .find('.slug') 119 | .should('contain.text', overrideSlug); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/cypress/e2e/delete-all-terms.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: deleteAllTerms', () => { 2 | beforeEach(() => { 3 | cy.login(); 4 | }); 5 | 6 | after(() => { 7 | cy.login(); 8 | // Restore default 20 items per page 9 | cy.visit(`/wp-admin/edit-tags.php?taxonomy=category`); 10 | cy.get('#show-settings-link').click(); 11 | cy.get('input.screen-per-page').click().clear().type(20); 12 | cy.get('#screen-options-apply').click(); 13 | 14 | cy.visit(`/wp-admin/edit-tags.php?taxonomy=post_tag`); 15 | cy.get('#show-settings-link').click(); 16 | cy.get('input.screen-per-page').click().clear().type(20); 17 | cy.get('#screen-options-apply').click(); 18 | }); 19 | 20 | it('Should be able to Delete All Categories', () => { 21 | cy.createTerm('Term to delete'); 22 | cy.createTerm('Term to delete 2'); 23 | cy.deleteAllTerms(); 24 | cy.get('.notice').should('contain', 'Categories deleted'); 25 | cy.get('#the-list a.row-title').should('have.length', 1); 26 | cy.get('#the-list a.row-title') 27 | .first() 28 | .should('have.text', 'Uncategorized'); 29 | }); 30 | 31 | it('Should be able to delete paginated categories', () => { 32 | for (let index = 0; index < 10; index++) { 33 | cy.createTerm('Multi page category ' + index); 34 | } 35 | // Set 2 items per page 36 | cy.get('#show-settings-link').click(); 37 | cy.get('input.screen-per-page').click().clear().type(2); 38 | cy.get('#screen-options-apply').click(); 39 | 40 | cy.deleteAllTerms(); 41 | cy.get('#the-list a.row-title').should('have.length', 1); 42 | cy.get('#the-list a.row-title') 43 | .first() 44 | .should('have.text', 'Uncategorized'); 45 | }); 46 | 47 | it('Should be able to Delete All Tags', () => { 48 | cy.createTerm('Tag to delete', 'post_tag'); 49 | cy.createTerm('Tag to delete 2', 'post_tag'); 50 | cy.deleteAllTerms('post_tag'); 51 | cy.get('.notice').should('contain', 'Tags deleted'); 52 | cy.get('#the-list a.row-title').should('have.length', 0); 53 | }); 54 | 55 | it('Should be able to delete paginated tags', () => { 56 | for (let index = 0; index < 10; index++) { 57 | cy.createTerm('Multi page tag ' + index, 'post_tag'); 58 | } 59 | // Set 2 items per page 60 | cy.get('#show-settings-link').click(); 61 | cy.get('input.screen-per-page').click().clear().type(2); 62 | cy.get('#screen-options-apply').click(); 63 | 64 | cy.deleteAllTerms('post_tag'); 65 | cy.get('#the-list a.row-title').should('have.length', 0); 66 | }); 67 | 68 | it('Should not fail if trying to delete empty tags list', () => { 69 | cy.deleteAllTerms('post_tag'); 70 | 71 | cy.deleteAllTerms('post_tag'); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/cypress/e2e/insert-block.test.js: -------------------------------------------------------------------------------- 1 | const { randomName } = require('../support/functions'); 2 | import { compare } from 'compare-versions'; 3 | 4 | describe('Command: insertBlock', () => { 5 | before(() => { 6 | cy.login(); 7 | cy.deactivatePlugin('classic-editor'); 8 | // Ignore WP 5.2 Synchronous XHR error. 9 | Cypress.on('uncaught:exception', (err, runnable) => { 10 | if ( 11 | err.message.includes( 12 | "Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'http://localhost:8889/wp-admin/admin-ajax.php': Synchronous XHR in page dismissal" 13 | ) 14 | ) { 15 | return false; 16 | } 17 | }); 18 | }); 19 | 20 | beforeEach(() => { 21 | cy.login(); 22 | }); 23 | 24 | it('Should be able to Insert code block on page', () => { 25 | const code = 'code ' + randomName(); 26 | cy.createPost({ 27 | beforeSave: () => { 28 | cy.insertBlock('core/code').then(id => { 29 | cy.getBlockEditor().find(`#${id}`).click().type(code); 30 | }); 31 | }, 32 | }); 33 | 34 | cy.getBlockEditor() 35 | .find('.wp-block-post-content, .block-editor-writing-flow') 36 | .should('contain.text', code); 37 | }); 38 | 39 | it('Should be able to Insert Heading', () => { 40 | const heading = 'Heading ' + randomName(); 41 | cy.createPost({ 42 | beforeSave: () => { 43 | cy.insertBlock('core/heading').then(id => { 44 | cy.getBlockEditor().find(`#${id}`).click().type(heading); 45 | }); 46 | }, 47 | }); 48 | 49 | cy.getBlockEditor() 50 | .find('.wp-block-post-content h2, .block-editor-writing-flow h2') 51 | .should('contain.text', heading); 52 | }); 53 | 54 | it('Should be able to insert Pullquote', () => { 55 | const quote = 'Quote ' + randomName(); 56 | const cite = 'Cite ' + randomName(); 57 | cy.createPost({ 58 | beforeSave: () => { 59 | cy.insertBlock('core/pullquote').then(id => { 60 | cy.getBlockEditor() 61 | .find( 62 | `#${id} [aria-label="Pullquote text"], #${id} [aria-label="Write quote…"]` 63 | ) 64 | .click() 65 | .type(quote); 66 | cy.getBlockEditor() 67 | .find( 68 | `#${id} [aria-label="Pullquote citation text"], #${id} [aria-label="Write citation…"]` 69 | ) 70 | .click() 71 | .type(cite); 72 | }); 73 | }, 74 | }); 75 | 76 | cy.getBlockEditor() 77 | .find('.wp-block-pullquote') 78 | .should('contain.text', quote) 79 | .should('contain.text', cite); 80 | }); 81 | 82 | it('Should be able to insert an Embed sub-block', () => { 83 | cy.createPost({ 84 | beforeSave: () => { 85 | cy.insertBlock('core/embed/twitter', 'Twitter'); 86 | }, 87 | }); 88 | 89 | cy.getBlockEditor() 90 | .find('.wp-block-embed') 91 | .should('contain.text', 'Twitter'); 92 | }); 93 | 94 | it('Should be able to insert custom block', () => { 95 | if ( 96 | 'trunk' !== Cypress.env('WORDPRESS_CORE').toString() && 97 | compare(Cypress.env('WORDPRESS_CORE').toString(), '6.1', '<') 98 | ) { 99 | // WinAmp block does not support this version of WordPress. 100 | assert(true, 'Skipping test, WinAmp block does not exist'); 101 | return; 102 | } 103 | 104 | cy.activatePlugin('retro-winamp-block'); 105 | 106 | cy.createPost({ 107 | beforeSave: () => { 108 | cy.insertBlock('tenup/winamp-block', 'WinAmp'); 109 | }, 110 | }); 111 | 112 | cy.getBlockEditor() 113 | .find('.wp-block-tenup-winamp-block') 114 | .should('contain.text', 'Add Audio') 115 | .should('have.attr', 'data-type') 116 | .and('eq', 'tenup/winamp-block'); 117 | 118 | cy.deactivatePlugin('retro-winamp-block'); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/cypress/e2e/login.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: login', () => { 2 | before(() => { 3 | cy.logout(); 4 | }); 5 | 6 | it('Login admin by default', () => { 7 | cy.login(); 8 | cy.visit('/wp-admin'); 9 | cy.get('h1').should('contain', 'Dashboard'); 10 | }); 11 | 12 | it('Switch users', () => { 13 | cy.login('user1', 'password1'); 14 | cy.login('user2', 'password2'); 15 | cy.login(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/cypress/e2e/logout.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: logout', () => { 2 | it('Logout logged in', () => { 3 | cy.login(); 4 | cy.logout(); 5 | cy.get('#login .message').should('contain', 'You are now logged out.'); 6 | }); 7 | 8 | it('Logout should not fail if not logged in', () => { 9 | cy.logout(); 10 | cy.visit(`/`); 11 | cy.logout(); 12 | }); 13 | 14 | after(() => { 15 | cy.login(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/cypress/e2e/open-document-settings.test.js: -------------------------------------------------------------------------------- 1 | import { compare } from 'compare-versions'; 2 | const { randomName } = require('../support/functions'); 3 | import { getIframe } from '../../../lib/functions/get-iframe'; 4 | 5 | describe('Commands: openDocumentSettings*', () => { 6 | before(() => { 7 | cy.login(); 8 | 9 | cy.deactivatePlugin('classic-editor'); 10 | 11 | // Disable Classic Editor if it's enabled 12 | cy.visit('/wp-admin/options-writing.php'); 13 | cy.get('body').then($body => { 14 | if ( 15 | $body.find('.classic-editor-options').length !== 0 && 16 | $body.find('#classic-editor-classic').is(':checked') 17 | ) { 18 | cy.get('#classic-editor-block').click(); 19 | cy.get('#submit').click(); 20 | } 21 | }); 22 | 23 | // Ignore WP 5.2 Synchronous XHR error. 24 | Cypress.on('uncaught:exception', (err, runnable) => { 25 | if ( 26 | err.message.includes( 27 | "Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'http://localhost:8889/wp-admin/admin-ajax.php': Synchronous XHR in page dismissal" 28 | ) 29 | ) { 30 | return false; 31 | } 32 | }); 33 | }); 34 | 35 | beforeEach(() => { 36 | cy.login(); 37 | }); 38 | 39 | it("Should be able to open (don't close) Status Panel on a new post", () => { 40 | cy.visit(`/wp-admin/post-new.php`); 41 | cy.closeWelcomeGuide(); 42 | 43 | // WP 6.1 renamed the panel name "Status & visibility" to "Summary". 44 | cy.openDocumentSettingsSidebar('Post'); 45 | cy.get('body').then($body => { 46 | let name = 'Status & visibility'; 47 | if ( 48 | $body.find( 49 | '.components-panel__body .components-panel__body-title button:contains("Summary")' 50 | ).length > 0 51 | ) { 52 | name = 'Summary'; 53 | } 54 | // WP 6.6 handling. 55 | if ($body.find('.editor-post-summary').length === 0) { 56 | cy.openDocumentSettingsPanel(name); 57 | // Assertion: Stick to the top checkbox should be visible 58 | cy.get('.components-panel__body .components-panel__body-title button') 59 | .contains(name, { matchCase: false }) 60 | .then($button => { 61 | const $panel = $button.parents('.components-panel__body'); 62 | cy.wrap($panel).should('contain', 'Stick to the top of the blog'); 63 | }); 64 | } else if ($body.find('.editor-post-sticky__toggle-control').length) { 65 | cy.get('.editor-post-sticky__toggle-control').should('be.visible'); 66 | } else if ($body.find('.editor-post-status__toggle').length) { 67 | // WP 6.7+ handling. 68 | cy.get('.editor-post-status__toggle').click(); 69 | cy.get('.editor-post-sticky__checkbox-control').should('be.visible'); 70 | } 71 | }); 72 | }); 73 | 74 | it('Should be able to open Tags panel on the existing post', () => { 75 | cy.createPost({ 76 | title: randomName(), 77 | }).then(post => { 78 | cy.visit(`/wp-admin/post.php?post=${post.id}&action=edit`); 79 | cy.closeWelcomeGuide(); 80 | 81 | const name = 'Tags'; 82 | cy.openDocumentSettingsPanel(name); 83 | 84 | // Assertion: Add new tag input should be visible 85 | cy.get('.components-panel__body .components-panel__body-title button') 86 | .contains(name) 87 | .then($button => { 88 | const $panel = $button.parents('.components-panel__body'); 89 | cy.wrap($panel).should('contain', 'Add'); 90 | }); 91 | }); 92 | }); 93 | 94 | it('Should be able to open Discussion panel on the existing page', () => { 95 | if ( 96 | 'trunk' === Cypress.env('WORDPRESS_CORE').toString() || 97 | compare(Cypress.env('WORDPRESS_CORE').toString(), '6.6', '>=') 98 | ) { 99 | assert(true, 'Skipping test'); 100 | return; 101 | } 102 | 103 | cy.createPost({ 104 | title: randomName(), 105 | postType: 'page', 106 | }).then(post => { 107 | cy.visit(`/wp-admin/post.php?post=${post.id}&action=edit`); 108 | cy.closeWelcomeGuide(); 109 | 110 | const name = 'Discussion'; 111 | cy.openDocumentSettingsPanel(name, 'Page'); 112 | 113 | // Assertion: Allow comments checkbox should be visible 114 | cy.get('.components-panel__body .components-panel__body-title button') 115 | .contains(name) 116 | .then($button => { 117 | const $panel = $button.parents('.components-panel__body'); 118 | cy.wrap($panel) 119 | .contains('Allow comments', { matchCase: false }) 120 | .should('exist'); 121 | }); 122 | }); 123 | }); 124 | 125 | it('Should be able to Open Post Settings Sidebar on a new Post', () => { 126 | cy.visit(`/wp-admin/post-new.php`); 127 | cy.closeWelcomeGuide(); 128 | 129 | cy.openDocumentSettingsSidebar(); 130 | 131 | cy.get('body').then($body => { 132 | if ($body.find('div[role="tablist"]').length) { 133 | cy.get('@selectedTab').should('have.attr', 'aria-selected', 'true'); 134 | } else if ($body.find('.edit-post-sidebar__panel-tabs').length) { 135 | cy.get('@selectedTab').should('have.class', 'is-active'); 136 | } 137 | }); 138 | }); 139 | 140 | it('Should be able to Open Block tab of the first block on existing post', () => { 141 | cy.createPost({ 142 | title: randomName(), 143 | }).then(post => { 144 | cy.visit(`/wp-admin/post.php?post=${post.id}&action=edit`); 145 | cy.closeWelcomeGuide(); 146 | 147 | cy.get('body').then($body => { 148 | if ($body.find('iframe[name="editor-canvas"]').length) { 149 | if ( 150 | getIframe('iframe[name="editor-canvas"]').find( 151 | '.wp-block-post-content > .wp-block' 152 | ).length > 0 153 | ) { 154 | getIframe('iframe[name="editor-canvas"]') 155 | .find('.wp-block-post-content > .wp-block') 156 | .first() 157 | .click(); 158 | } else { 159 | // Fallback for WordPress 5.7 160 | getIframe('iframe[name="editor-canvas"]') 161 | .find('.block-editor-block-list__layout > .wp-block') 162 | .first() 163 | .click(); 164 | } 165 | } else { 166 | if ($body.find('.wp-block-post-content > .wp-block').length > 0) { 167 | cy.get('.wp-block-post-content > .wp-block').first().click(); 168 | } else { 169 | // Fallback for WordPress 5.7 170 | cy.get('.block-editor-block-list__layout > .wp-block') 171 | .first() 172 | .click(); 173 | } 174 | } 175 | }); 176 | 177 | cy.openDocumentSettingsSidebar('Block'); 178 | 179 | // Assertions: 180 | cy.get('body').then($body => { 181 | if ($body.find('div[role="tablist"]').length) { 182 | cy.get('@selectedTab').should('have.attr', 'aria-selected', 'true'); 183 | } else if ($body.find('.edit-post-sidebar__panel-tabs').length) { 184 | cy.get('@selectedTab').should('have.class', 'is-active'); 185 | } 186 | }); 187 | }); 188 | }); 189 | 190 | it('Should be able to open Page Settings sidebar on an existing page', () => { 191 | cy.createPost({ 192 | title: randomName(), 193 | postType: 'page', 194 | }).then(post => { 195 | cy.visit(`/wp-admin/post.php?post=${post.id}&action=edit`); 196 | cy.closeWelcomeGuide(); 197 | 198 | cy.openDocumentSettingsSidebar('Page'); 199 | 200 | cy.get('body').then($body => { 201 | if ($body.find('div[role="tablist"]').length) { 202 | cy.get('@selectedTab').should('have.attr', 'aria-selected', 'true'); 203 | } else if ($body.find('.edit-post-sidebar__panel-tabs').length) { 204 | cy.get('@selectedTab').should('have.class', 'is-active'); 205 | } 206 | }); 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /tests/cypress/e2e/plugins.test.js: -------------------------------------------------------------------------------- 1 | import { compare } from 'compare-versions'; 2 | 3 | describe('Plugins commands', () => { 4 | beforeEach(() => { 5 | cy.login(); 6 | }); 7 | 8 | it('Test plugins commands', () => { 9 | cy.activateAllPlugins(); 10 | cy.visit('/wp-admin/plugins.php'); 11 | cy.get('body').then($body => { 12 | const winAmpMinimumVersion = '6.1'; 13 | 14 | if ( 15 | 'trunk' !== Cypress.env('WORDPRESS_CORE').toString() && 16 | compare( 17 | Cypress.env('WORDPRESS_CORE').toString(), 18 | winAmpMinimumVersion, 19 | '<' 20 | ) 21 | ) { 22 | // WinAmp block is not expected to activate. 23 | assert.equal( 24 | $body.find('#the-list tr.inactive').length, 25 | 1, 26 | 'Only WinAmp plugin is inactive as it is unsupported.' 27 | ); 28 | return; 29 | } 30 | 31 | assert.equal( 32 | $body.find('#the-list tr.inactive').length, 33 | 0, 34 | 'No inactive plugins' 35 | ); 36 | }); 37 | 38 | cy.deactivateAllPlugins(); 39 | cy.get('#message.updated.notice').should( 40 | 'contain', 41 | 'Selected plugins deactivated.' 42 | ); 43 | cy.get('body').then($body => { 44 | assert.equal( 45 | $body.find('#the-list tr.active').length, 46 | 0, 47 | 'No active plugins' 48 | ); 49 | }); 50 | 51 | const plugins = ['classic-editor', 'cypress-wp-utils']; 52 | 53 | plugins.forEach(plugin => { 54 | cy.activatePlugin(plugin); 55 | cy.get(`[data-slug="${plugin}"]`).should('have.class', 'active'); 56 | cy.get('#message.updated.notice').should('contain', 'Plugin activated.'); 57 | 58 | // Should not fail if activated again 59 | cy.activatePlugin(plugin); 60 | cy.get('body').then($body => { 61 | assert.equal( 62 | $body.find('#message.updated.notice').length, 63 | 0, 64 | 'No notice output' 65 | ); 66 | }); 67 | 68 | cy.deactivatePlugin(plugin); 69 | cy.get(`[data-slug="${plugin}"]`).should('have.class', 'inactive'); 70 | cy.get('#message.updated.notice').should( 71 | 'contain', 72 | 'Plugin deactivated.' 73 | ); 74 | 75 | // Should not fail if deactivated again 76 | cy.deactivatePlugin(plugin); 77 | cy.get('body').then($body => { 78 | assert.equal( 79 | $body.find('#message.updated.notice').length, 80 | 0, 81 | 'No notice output' 82 | ); 83 | }); 84 | 85 | // Bring previously active plugin back 86 | cy.activatePlugin(plugin); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/cypress/e2e/set-permalink-structure.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: setPermalinkStructure', () => { 2 | beforeEach(() => { 3 | cy.login(); 4 | }); 5 | 6 | const structures = [ 7 | { name: 'Plain', value: '' }, 8 | { name: 'Day and name', value: '/%year%/%monthnum%/%day%/%postname%/' }, 9 | { name: 'Month and name', value: '/%year%/%monthnum%/%postname%/' }, 10 | { name: 'Numeric', value: '/archives/%post_id%' }, 11 | { name: 'Post name', value: '/%postname%/' }, 12 | ]; 13 | 14 | structures.forEach(structure => { 15 | it(`Should be able to set predefined ${structure.name} permalinks`, () => { 16 | cy.setPermalinkStructure(structure.value); 17 | cy.get('.notice-success, .notice.updated').should( 18 | 'contain', 19 | 'Permalink structure updated.' 20 | ); 21 | cy.get('.form-table.permalink-structure :checked').should( 22 | 'have.value', 23 | structure.value 24 | ); 25 | }); 26 | }); 27 | 28 | it('Should be able to set custom permalinks', () => { 29 | const structure = '/custom/%second%/'; 30 | cy.setPermalinkStructure(structure); 31 | cy.get('.notice-success, .notice.updated').should( 32 | 'contain', 33 | 'Permalink structure updated.' 34 | ); 35 | cy.get('.form-table.permalink-structure :checked').should( 36 | 'have.value', 37 | 'custom' 38 | ); 39 | cy.get('#permalink_structure').should('have.value', structure); 40 | }); 41 | 42 | it('Should receive error if no tag added', () => { 43 | cy.setPermalinkStructure('no-tag'); 44 | cy.get('.notice-error, .notice.error').should( 45 | 'contain', 46 | 'A structure tag is required when using custom permalinks.' 47 | ); 48 | }); 49 | 50 | after(() => { 51 | // Restore default permalink structure. 52 | cy.setPermalinkStructure('/%postname%/'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/cypress/e2e/upload-media.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: uploadMedia', () => { 2 | beforeEach(() => { 3 | cy.login(); 4 | }); 5 | 6 | it('Should be able to upload media', () => { 7 | cy.uploadMedia('tests/cypress/fixtures/10up.png').then(response => { 8 | expect(response.success).to.equal(true); 9 | expect(Number.isNaN(response.mediaId)).to.equal(false); 10 | cy.visit(`/wp-admin/post.php?post=${response.mediaId}&action=edit`); 11 | cy.get('#title').then(ele => { 12 | expect(ele.val()).contains('10up'); 13 | }); 14 | }); 15 | }); 16 | 17 | it('Should be able to detect upload media failure', () => { 18 | cy.uploadMedia('tests/cypress/fixtures/example.json').then(response => { 19 | expect(response.success).to.equal(false); 20 | expect(response.errorMessage).to.be.a('string'); 21 | expect(response.errorMessage).contains('has failed to upload'); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/cypress/e2e/wp-cli.test.js: -------------------------------------------------------------------------------- 1 | describe('Command: wpCli', () => { 2 | it('Should run cli command and receive the response', () => { 3 | cy.wpCli('wp cli version') 4 | .its('stdout') 5 | .should('match', /^WP-CLI \d+\.\d+/); 6 | }); 7 | 8 | it('Should not fail with ignoreFailures=true', () => { 9 | const randomCommand = 10 | 'command' + (Math.random() + 1).toString(16).substring(3); 11 | cy.wpCli(randomCommand, true).its('code').should('equal', 1); 12 | }); 13 | 14 | it('Should run cli in eval mode', () => { 15 | const evalSting = (Math.random() + 1).toString(16).substring(2); 16 | cy.wpCliEval(` { 9 | if ( 10 | 'trunk' === Cypress.env('WORDPRESS_CORE').toString() || 11 | compare(Cypress.env('WORDPRESS_CORE').toString(), '5.5', '>=') 12 | ) { 13 | before(() => { 14 | cy.login(); 15 | cy.deactivatePlugin('classic-editor'); 16 | }); 17 | 18 | beforeEach(() => { 19 | cy.login(); 20 | cy.visit('/wp-admin/post-new.php'); 21 | cy.get('.edit-post-header').should('exist'); 22 | cy.closeWelcomeGuide(); 23 | }); 24 | 25 | const testPatterns = [ 26 | { title: randomName(), cat: 'text', expected: false }, 27 | { title: 'Quote', cat: randomName(), expected: false }, 28 | ]; 29 | 30 | testPatterns.forEach(testCase => { 31 | const shouldIt = testCase.expected ? 'should' : 'should not'; 32 | it(`Pattern "${testCase.title}" ${shouldIt} exist in category "${testCase.cat}"`, () => { 33 | // Wait for patterns to load on the post edit page. 34 | 35 | const args = { 36 | title: testCase.title, 37 | }; 38 | 39 | if ( 40 | 'trunk' === Cypress.env('WORDPRESS_CORE').toString() || 41 | compare(Cypress.env('WORDPRESS_CORE').toString(), '5.7', '>=') 42 | ) { 43 | args.categoryValue = testCase.cat; 44 | } 45 | 46 | cy.checkBlockPatternExists(args).then(exists => { 47 | assert( 48 | exists === testCase.expected, 49 | `Pattern "${testCase.title}" in category "${testCase.cat}": ${testCase.expected}` 50 | ); 51 | }); 52 | }); 53 | }); 54 | } else { 55 | it('Skip checkBlockPatternExists test, WordPress version too low', () => { 56 | assert(true); 57 | }); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /tests/cypress/fixtures/10up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10up/cypress-wp-utils/99ac4014fb80bc3cc2fc46db45e297234fa2b281/tests/cypress/fixtures/10up.png -------------------------------------------------------------------------------- /tests/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /tests/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import '../../../lib/index'; 18 | -------------------------------------------------------------------------------- /tests/cypress/support/functions.js: -------------------------------------------------------------------------------- 1 | export const randomName = () => Math.random().toString(16).substring(7); 2 | -------------------------------------------------------------------------------- /tests/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "types": ["cypress"] 5 | }, 6 | "include": ["**/*.*"] 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 9 | "outDir": "./lib/", /* Redirect output structure to the directory. */ 10 | 11 | /* Strict Type-Checking Options */ 12 | "strict": true, /* Enable all strict type-checking options. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | 15 | /* Advanced Options */ 16 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 17 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 18 | 19 | "types": ["node", "cypress"], 20 | }, 21 | "include": ["src/**/*.ts"] 22 | } 23 | --------------------------------------------------------------------------------