├── .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://github.com/10up/cypress-wp-utils/releases/latest)    [](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 |
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 |
--------------------------------------------------------------------------------