mirror of
https://github.com/elementor/hello-theme.git
synced 2025-08-17 19:41:10 +08:00
Playwright and phpunit tests [TMZ-733] (#502)
This commit is contained in:
parent
c8994cc6b0
commit
596c78d75b
35 changed files with 6424 additions and 273 deletions
|
@ -24,3 +24,8 @@ webpack.config.js
|
|||
.eslintignore
|
||||
.eslintrc.js
|
||||
.npmrc
|
||||
|
||||
tests/
|
||||
test-results/
|
||||
tsconfig.json
|
||||
.wp-env.json
|
||||
|
|
114
.github/workflows/phpunit.yml
vendored
Normal file
114
.github/workflows/phpunit.yml
vendored
Normal file
|
@ -0,0 +1,114 @@
|
|||
name: PHPUnit
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.txt'
|
||||
- '.github/config.json'
|
||||
- 'bin/**'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
branches:
|
||||
- 'main'
|
||||
- '3.*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.txt'
|
||||
- '.github/config.json'
|
||||
- 'bin/**'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
merge_group:
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
file-diff:
|
||||
runs-on: ubuntu-latest
|
||||
name: File Diff
|
||||
if: startsWith( github.repository, 'elementor/' )
|
||||
outputs:
|
||||
php_diff: ${{ steps.php_diff_files.outputs.diff }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Check PHP files diff
|
||||
id: php_diff_files
|
||||
uses: technote-space/get-diff-action@v6
|
||||
with:
|
||||
PATTERNS: |
|
||||
**/*.php
|
||||
**/*.twig
|
||||
composer.+(json|lock)
|
||||
.github/**/*.yml
|
||||
install-wp-tests.sh
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [ 'file-diff' ]
|
||||
if: ${{ github.event.pull_request.title == null || needs.file-diff.outputs.php_diff }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
wordpress_versions: ['nightly', 'latest', '6.6', '6.5']
|
||||
php_versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
name: PHPUnit - WordPress ${{ matrix.wordpress_versions }} - PHP version ${{ matrix.php_versions }}
|
||||
env:
|
||||
WP_TESTS_DIR: /tmp/wordpress-tests-lib
|
||||
WP_TESTS_ELEMENTOR_DIR: /tmp/elementor/elementor.php
|
||||
WP_TESTS_HELLOPLUS_DIR: /tmp/hello-plus/hello-plus.php
|
||||
THEME_FILE: functions.php
|
||||
COVERAGE: ${{ matrix.php_versions >= 8.3 && matrix.wordpress_versions == 'latest' && github.event_name == 'push' }}
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php_versions }}
|
||||
coverage: none
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:${{ job.services.mysql.ports['3306'] }} ${{ matrix.wordpress_versions }} true
|
||||
composer update --no-interaction
|
||||
- name: Copy theme folder to /tmp/wordpress/wp-content/themes/
|
||||
run: |
|
||||
cp -a $GITHUB_WORKSPACE /tmp/wordpress/wp-content/themes/
|
||||
echo "Copied theme folder to /tmp/wordpress/wp-content/themes/$(basename $GITHUB_WORKSPACE)"
|
||||
- name: Run Tests with Coverage (latest PHP & WP)
|
||||
if: ${{ env.COVERAGE != 'false' }}
|
||||
run: |
|
||||
composer run coverage
|
||||
- name: Run Tests without Coverage
|
||||
if: ${{ env.COVERAGE == 'false' }}
|
||||
run: |
|
||||
composer run test
|
||||
|
||||
test-result:
|
||||
needs: test
|
||||
if: ${{ always() }} # Will be run even if 'test' matrix will be skipped
|
||||
runs-on: ubuntu-22.04
|
||||
name: PHPUnit - Test Results
|
||||
steps:
|
||||
- name: Test status
|
||||
run: echo "Test status is - ${{ needs.test.result }}"
|
||||
- name: Check test matrix status
|
||||
if: ${{ needs.test.result != 'success' && needs.test.result != 'skipped' }}
|
||||
run: exit 1
|
66
.github/workflows/playwright.yml
vendored
Normal file
66
.github/workflows/playwright.yml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
playwright-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
scope: '@elementor'
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: composer
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: composer install --no-dev --optimize-autoloader
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.CLOUD_DEVOPS_TOKEN }}
|
||||
|
||||
- name: Start wp-env
|
||||
run: npm run wp-env:start
|
||||
|
||||
- name: Setup Playwright
|
||||
run: npm run test:setup:playwright
|
||||
|
||||
- name: Setup Chromium
|
||||
run: npm run test:setup:chromium
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: playwright-tests
|
||||
run: |
|
||||
npm run test:playwright
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
continue-on-error: false
|
||||
|
||||
- name: Check test results
|
||||
if: steps.playwright-tests.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Playwright tests failed!"
|
||||
exit 1
|
||||
|
||||
- name: Stop wp-env
|
||||
if: always()
|
||||
run: npm run wp-env:stop
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -17,3 +17,6 @@ assets/js/
|
|||
*.zip
|
||||
.npmrc
|
||||
.env
|
||||
tmp/
|
||||
.phpunit.result.cache
|
||||
!tests/phpunit/hello-elementor
|
||||
|
|
18
.wp-env.json
Normal file
18
.wp-env.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"core": null,
|
||||
"phpVersion": "8.0",
|
||||
"plugins": [
|
||||
"https://downloads.wordpress.org/plugin/elementor.latest-stable.zip"
|
||||
],
|
||||
"themes": [
|
||||
"./"
|
||||
],
|
||||
"mappings": {
|
||||
"hello-elementor-config": "./tests/wp-env/config"
|
||||
},
|
||||
"config": {
|
||||
"ELEMENTOR_SHOW_HIDDEN_EXPERIMENTS": true,
|
||||
"SCRIPT_DEBUG": false,
|
||||
"WP_DEBUG": false
|
||||
}
|
||||
}
|
157
bin/install-wp-tests-local.sh
Normal file
157
bin/install-wp-tests-local.sh
Normal file
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
WORKING_DIR=$(pwd)
|
||||
|
||||
# Remove old tmp folder
|
||||
rm -rf "$WORKING_DIR/tmp"
|
||||
|
||||
# Ask for some parameters to install the test env.
|
||||
echo "Choose a database Name for tests [elementor-tests]:"
|
||||
read -r DB_NAME
|
||||
echo "What is your database username [admin]?"
|
||||
read -r DB_USER
|
||||
echo "What is your database password [admin]?"
|
||||
read -r DB_PASS
|
||||
echo "What is your database host (specify port if needed) [127.0.0.1:PORT]?"
|
||||
read -r DB_HOST
|
||||
echo "Choose WordPress version for testing [latest]:"
|
||||
read -r WP_VERSION
|
||||
|
||||
DB_NAME=${DB_NAME:-"elementor-tests"}
|
||||
DB_USER=${DB_USER:-"admin"}
|
||||
DB_PASS=${DB_PASS:-"admin"}
|
||||
DB_HOST=${DB_HOST:-"127.0.0.1"}
|
||||
WP_VERSION=${WP_VERSION:-"latest"}
|
||||
|
||||
WP_TESTS_UTILS_DIR=${WP_TESTS_UTILS_DIR-$WORKING_DIR/tmp/wordpress-tests-lib}
|
||||
WP_CORE_DIR=${WP_CORE_DIR-$WORKING_DIR/tmp/wordpress/}
|
||||
ELEMENTOR_PLUGIN_DIR=${ELEMENTOR_PLUGIN_DIR-$WORKING_DIR/tmp}
|
||||
HELLOPLUS_PLUGIN_DIR=${HELLOPLUS_PLUGIN_DIR-$WORKING_DIR/tmp}
|
||||
HELLOTHEME_THEME_DIR=${HELLOTHEME_THEME_DIR-$WP_CORE_DIR/wp-content/themes/hello-theme}
|
||||
|
||||
# Download util
|
||||
download() {
|
||||
if [ `which curl` ]; then
|
||||
curl --location -s "$1" > "$2";
|
||||
elif [ `which wget` ]; then
|
||||
wget -nv -O "$2" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
## Determine the WP_TEST_TAG.
|
||||
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
|
||||
WP_TESTS_TAG="tags/$WP_VERSION"
|
||||
else
|
||||
# http serves a single offer, whereas https serves multiple. we only want one
|
||||
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
|
||||
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
|
||||
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
echo "Latest WordPress version could not be found"
|
||||
exit 1
|
||||
fi
|
||||
WP_TESTS_TAG="tags/$LATEST_VERSION"
|
||||
fi
|
||||
|
||||
set -ex
|
||||
|
||||
check_for_svn() {
|
||||
if [ ! `which svn` ]; then
|
||||
echo 'Please install "svn" and re run this script.'
|
||||
echo 'Mac users: `brew install svn`'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_wp() {
|
||||
if [ -d "$WP_CORE_DIR" ]; then
|
||||
return;
|
||||
fi
|
||||
|
||||
mkdir -p "$WP_CORE_DIR"
|
||||
|
||||
if [ $WP_VERSION == 'latest' ]; then
|
||||
local ARCHIVE_NAME='latest'
|
||||
else
|
||||
local ARCHIVE_NAME="wordpress-$WP_VERSION"
|
||||
fi
|
||||
|
||||
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz
|
||||
tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C "$WP_CORE_DIR"
|
||||
|
||||
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php "$WP_CORE_DIR/wp-content/db.php"
|
||||
}
|
||||
|
||||
install_test_suite() {
|
||||
# portable in-place argument for both GNU sed and Mac OSX sed
|
||||
if [[ $(uname -s) == 'Darwin' ]]; then
|
||||
local ioption='-i .bak'
|
||||
else
|
||||
local ioption='-i'
|
||||
fi
|
||||
|
||||
# set up testing suite if it doesn't yet exist
|
||||
if [ ! -d "$WP_TESTS_UTILS_DIR" ]; then
|
||||
# set up testing suite
|
||||
mkdir -p "$WP_TESTS_UTILS_DIR"/includes
|
||||
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ "$WP_TESTS_UTILS_DIR/includes/"
|
||||
fi
|
||||
|
||||
cd "$WP_TESTS_UTILS_DIR"
|
||||
|
||||
if [ ! -f wp-tests-config.php ]; then
|
||||
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_UTILS_DIR/wp-tests-config.php"
|
||||
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_UTILS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_UTILS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_UTILS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_UTILS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_UTILS_DIR"/wp-tests-config.php
|
||||
fi
|
||||
}
|
||||
|
||||
install_db() {
|
||||
# parse DB_HOST for port or socket references
|
||||
local PARTS=(${DB_HOST//\:/ })
|
||||
local DB_HOSTNAME=${PARTS[0]};
|
||||
local DB_SOCK_OR_PORT=${PARTS[1]};
|
||||
local EXTRA=""
|
||||
|
||||
if ! [ -z $DB_HOSTNAME ] ; then
|
||||
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
|
||||
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
|
||||
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
|
||||
EXTRA=" --socket=$DB_SOCK_OR_PORT"
|
||||
elif ! [ -z $DB_HOSTNAME ] ; then
|
||||
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
|
||||
fi
|
||||
fi
|
||||
|
||||
# create database
|
||||
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
|
||||
}
|
||||
|
||||
install_elementor_plugin() {
|
||||
download https://downloads.wordpress.org/plugin/elementor.latest-stable.zip /tmp/elementor.zip
|
||||
# Using double-quotes to wrap the unzip path so directory names with spaces will not cause problems
|
||||
unzip -q /tmp/elementor.zip -d "$ELEMENTOR_PLUGIN_DIR"
|
||||
}
|
||||
|
||||
install_helloplus_plugin() {
|
||||
download https://downloads.wordpress.org/plugin/hello-plus.latest-stable.zip /tmp/hello-plus.zip
|
||||
# Using double-quotes to wrap the unzip path so directory names with spaces will not cause problems
|
||||
unzip -q /tmp/hello-plus.zip -d "$HELLOPLUS_PLUGIN_DIR"
|
||||
}
|
||||
|
||||
symlink_theme() {
|
||||
rm -rf "$HELLOTHEME_THEME_DIR"
|
||||
|
||||
ln -s "$WORKING_DIR" "$HELLOTHEME_THEME_DIR"
|
||||
}
|
||||
|
||||
check_for_svn
|
||||
install_wp
|
||||
install_test_suite
|
||||
install_elementor_plugin
|
||||
install_helloplus_plugin
|
||||
symlink_theme
|
||||
install_db
|
206
bin/install-wp-tests.sh
Normal file
206
bin/install-wp-tests.sh
Normal file
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_NAME=$1
|
||||
DB_USER=$2
|
||||
DB_PASS=$3
|
||||
DB_HOST=${4-localhost}
|
||||
WP_VERSION=${5-latest}
|
||||
|
||||
WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
|
||||
WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
|
||||
ELEMENTOR_PLUGIN_DIR=${ELEMENTOR_PLUGIN_DIR-/tmp}
|
||||
HELLO_PLUS_PLUGIN_DIR=${HELLO_PLUS_PLUGIN_DIR-/tmp}
|
||||
|
||||
download() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
|
||||
if [ `which curl` ]; then
|
||||
curl --location --fail --show-error --silent --output "$output" "$url"
|
||||
local exit_code=$?
|
||||
elif [ `which wget` ]; then
|
||||
wget -nv -O "$output" "$url"
|
||||
local exit_code=$?
|
||||
else
|
||||
echo "Error: Neither curl nor wget found. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo "Error: Failed to download $url"
|
||||
rm -f "$output"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
|
||||
WP_TESTS_TAG="tags/$WP_VERSION"
|
||||
else
|
||||
# http serves a single offer, whereas https serves multiple. we only want one
|
||||
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
|
||||
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
|
||||
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
echo "Latest WordPress version could not be found"
|
||||
exit 1
|
||||
fi
|
||||
WP_TESTS_TAG="tags/$LATEST_VERSION"
|
||||
fi
|
||||
|
||||
set -ex
|
||||
|
||||
install_wp() {
|
||||
if [ -d $WP_CORE_DIR ]; then
|
||||
return;
|
||||
fi
|
||||
|
||||
mkdir -p $WP_CORE_DIR
|
||||
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
|
||||
mkdir -p /tmp/wordpress-nightly
|
||||
download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip
|
||||
unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/
|
||||
mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR
|
||||
else
|
||||
if [ $WP_VERSION == 'latest' ]; then
|
||||
local ARCHIVE_NAME='latest'
|
||||
else
|
||||
local ARCHIVE_NAME="wordpress-$WP_VERSION"
|
||||
fi
|
||||
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz
|
||||
tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A $WP_CORE_DIR/wp-content/themes/twentytwentyone)" ]; then
|
||||
mkdir -p /tmp/twentytwentyone
|
||||
download https://downloads.wordpress.org/theme/twentytwentyone.2.0.zip /tmp/twentytwentyone/twentytwentyone.zip
|
||||
unzip -q /tmp/twentytwentyone/twentytwentyone.zip -d /tmp/twentytwentyone/
|
||||
mv /tmp/twentytwentyone/twentytwentyone $WP_CORE_DIR/wp-content/themes
|
||||
fi
|
||||
|
||||
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
|
||||
}
|
||||
|
||||
install_test_suite() {
|
||||
# portable in-place argument for both GNU sed and Mac OSX sed
|
||||
if [[ $(uname -s) == 'Darwin' ]]; then
|
||||
local ioption='-i .bak'
|
||||
else
|
||||
local ioption='-i'
|
||||
fi
|
||||
|
||||
# set up testing suite if it doesn't yet exist
|
||||
if [ ! -d $WP_TESTS_DIR ]; then
|
||||
# set up testing suite
|
||||
mkdir -p $WP_TESTS_DIR
|
||||
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
|
||||
fi
|
||||
|
||||
cd $WP_TESTS_DIR
|
||||
|
||||
if [ ! -f wp-tests-config.php ]; then
|
||||
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
fi
|
||||
}
|
||||
|
||||
install_db() {
|
||||
# parse DB_HOST for port or socket references
|
||||
local PARTS=(${DB_HOST//\:/ })
|
||||
local DB_HOSTNAME=${PARTS[0]};
|
||||
local DB_SOCK_OR_PORT=${PARTS[1]};
|
||||
local EXTRA=""
|
||||
|
||||
if ! [ -z $DB_HOSTNAME ] ; then
|
||||
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
|
||||
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
|
||||
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
|
||||
EXTRA=" --socket=$DB_SOCK_OR_PORT"
|
||||
elif ! [ -z $DB_HOSTNAME ] ; then
|
||||
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
|
||||
fi
|
||||
fi
|
||||
|
||||
# create database
|
||||
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
|
||||
}
|
||||
|
||||
install_elementor_plugin() {
|
||||
echo "Installing Elementor plugin..."
|
||||
rm -rf ${ELEMENTOR_PLUGIN_DIR}/elementor
|
||||
|
||||
# Download the plugin
|
||||
local zip_file="/tmp/elementor.zip"
|
||||
rm -f "$zip_file"
|
||||
|
||||
echo "Downloading Elementor from WordPress.org..."
|
||||
download https://downloads.wordpress.org/plugin/elementor.latest-stable.zip "$zip_file"
|
||||
|
||||
# Validate the downloaded file is a valid zip
|
||||
if ! unzip -t "$zip_file" >/dev/null 2>&1; then
|
||||
echo "Error: Downloaded file is not a valid zip archive"
|
||||
echo "File size: $(ls -lh "$zip_file" 2>/dev/null | awk '{print $5}' || echo 'File not found')"
|
||||
echo "File type: $(file "$zip_file" 2>/dev/null || echo 'Cannot determine file type')"
|
||||
rm -f "$zip_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the plugin
|
||||
echo "Extracting Elementor plugin..."
|
||||
if ! unzip -q "$zip_file" -d ${ELEMENTOR_PLUGIN_DIR}; then
|
||||
echo "Error: Failed to extract Elementor plugin"
|
||||
rm -f "$zip_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f "$zip_file"
|
||||
echo "Elementor plugin installed successfully"
|
||||
}
|
||||
|
||||
install_hello_plus_plugin() {
|
||||
echo "Installing Hello Plus plugin..."
|
||||
rm -rf ${HELLO_PLUS_PLUGIN_DIR}/hello-plus
|
||||
|
||||
# Download the plugin
|
||||
local zip_file="/tmp/hello-plus.zip"
|
||||
rm -f "$zip_file"
|
||||
|
||||
echo "Downloading Hello Plus from WordPress.org..."
|
||||
download https://downloads.wordpress.org/plugin/hello-plus.latest-stable.zip "$zip_file"
|
||||
|
||||
# Validate the downloaded file is a valid zip
|
||||
if ! unzip -t "$zip_file" >/dev/null 2>&1; then
|
||||
echo "Error: Downloaded file is not a valid zip archive"
|
||||
echo "File size: $(ls -lh "$zip_file" 2>/dev/null | awk '{print $5}' || echo 'File not found')"
|
||||
echo "File type: $(file "$zip_file" 2>/dev/null || echo 'Cannot determine file type')"
|
||||
rm -f "$zip_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the plugin
|
||||
echo "Extracting Hello Plus plugin..."
|
||||
if ! unzip -q "$zip_file" -d ${HELLO_PLUS_PLUGIN_DIR}; then
|
||||
echo "Error: Failed to extract Hello Plus plugin"
|
||||
rm -f "$zip_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f "$zip_file"
|
||||
echo "Hello Plus plugin installed successfully"
|
||||
}
|
||||
|
||||
install_wp
|
||||
install_test_suite
|
||||
install_db
|
||||
install_elementor_plugin
|
||||
install_hello_plus_plugin
|
|
@ -3,10 +3,13 @@
|
|||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.6",
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
|
||||
"wp-coding-standards/wpcs": "^2.3"
|
||||
"wp-coding-standards/wpcs": "^2.3",
|
||||
"phpunit/phpunit": "9.5.14",
|
||||
"elementor/elementor-editor-testing": "0.0.3",
|
||||
"yoast/phpunit-polyfills": "^1.0.1"
|
||||
},
|
||||
"require": {
|
||||
"elementor/wp-notifications-package": "1.2.0"
|
||||
"elementor/wp-notifications-package": "1.2.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
@ -14,6 +17,9 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "phpcs --extensions=php -p"
|
||||
"lint": "phpcs --extensions=php -p",
|
||||
"lint:fix": "vendor/bin/phpcbf --ignore=node_modules,vendor,build .",
|
||||
"test": "phpunit --testsuite hello-elementor",
|
||||
"test:install": "bash ./bin/install-wp-tests-local.sh"
|
||||
}
|
||||
}
|
||||
|
|
2330
composer.lock
generated
2330
composer.lock
generated
File diff suppressed because it is too large
Load diff
651
package-lock.json
generated
651
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -14,6 +14,12 @@
|
|||
"build:dir": "npm run clean:build && npm run build:prod && rsync -av --exclude-from=.buildignore . $npm_package_name",
|
||||
"package": "npm run clean:build && npm run build:prod && rsync -av --exclude-from=.buildignore . $npm_package_name",
|
||||
"package:zip": "npm run package && zip -r $npm_package_name.$npm_package_version.zip ./$npm_package_name/*",
|
||||
"test:php": "docker-compose -f bin/docker-compose.yml run --rm wordpress_phpunit phpunit",
|
||||
"test:playwright": "playwright test -c tests/playwright/playwright.config.ts",
|
||||
"test:playwright:headless": "CI=1 playwright test -c tests/playwright/playwright.config.ts",
|
||||
"test:playwright:debug": "npm run test:playwright -- --debug",
|
||||
"test:setup:playwright": "wp-env run cli bash hello-elementor-config/setup.sh && wp-env run tests-cli bash hello-elementor-config/setup.sh",
|
||||
"test:setup:chromium": "npx playwright install chromium",
|
||||
"zip": "npm run clean:build && npm run build:prod && rsync -av --exclude-from=.buildignore . $npm_package_name && zip -r $npm_package_name.$npm_package_version.zip $npm_package_name/*",
|
||||
"update-version": "node .github/scripts/update-version-in-files.js"
|
||||
},
|
||||
|
@ -23,6 +29,12 @@
|
|||
"@wordpress/i18n": "^5.23.0",
|
||||
"@wordpress/notices": "^5.23.0",
|
||||
"@wordpress/scripts": "^30.16.0",
|
||||
"@elementor/wp-lite-env": "^0.0.20",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-no-jquery": "^3.1.1",
|
||||
|
@ -37,6 +49,7 @@
|
|||
"html-entities": "^2.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.89.0"
|
||||
"sass": "^1.89.0",
|
||||
"composer": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
37
phpunit.xml
Normal file
37
phpunit.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<phpunit
|
||||
bootstrap="tests/bootstrap.php"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
>
|
||||
<php>
|
||||
<env name="THEME_FILE" value="functions.php"/>
|
||||
<env name="WP_TESTS_DIR" value="./tmp/wordpress-tests-lib"/>
|
||||
<env name="WP_TESTS_ELEMENTOR_DIR" value="./tmp/elementor/elementor.php"/>
|
||||
<env name="WP_TESTS_HELLOPLUS_DIR" value="./tmp/hello-plus/hello-plus.php"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="hello-elementor">
|
||||
<directory prefix="test-" suffix=".php">./tests/phpunit/hello-elementor/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./</directory>
|
||||
<exclude>
|
||||
<directory>.github</directory>
|
||||
<directory>assets</directory>
|
||||
<directory>bin</directory>
|
||||
<directory>build</directory>
|
||||
<directory>node_modules</directory>
|
||||
<directory>vendor</directory>
|
||||
<directory>tests</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
82
tests/bootstrap.php
Normal file
82
tests/bootstrap.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
$composer_autoloader_file = __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
if (!file_exists($composer_autoloader_file)) {
|
||||
die('Installing composer are required for running the tests.');
|
||||
}
|
||||
|
||||
require $composer_autoloader_file;
|
||||
|
||||
$_tests_dir = getenv('WP_TESTS_DIR');
|
||||
|
||||
define('ELEMENTOR_TESTS', true);
|
||||
define('HELLO_ELEMENTOR_TESTS', true);
|
||||
|
||||
/**
|
||||
* change PLUGIN_FILE env in phpunit.xml
|
||||
*/
|
||||
define('THEME_FILE', getenv('THEME_FILE'));
|
||||
define('THEME_FOLDER', basename(dirname(__DIR__)));
|
||||
define('PLUGIN_PATH', THEME_FOLDER . '/' . THEME_FILE);
|
||||
|
||||
$elementor_plugin_path = 'elementor/elementor.php';
|
||||
|
||||
$active_plugins = [$elementor_plugin_path];
|
||||
|
||||
// Activates this plugin in WordPress so it can be tested.
|
||||
$GLOBALS['wp_tests_options'] = [
|
||||
'active_plugins' => $active_plugins,
|
||||
'template' => 'hello-theme',
|
||||
'stylesheet' => 'hello-theme',
|
||||
];
|
||||
|
||||
require_once $_tests_dir . '/includes/functions.php';
|
||||
|
||||
tests_add_filter('muplugins_loaded', function () {
|
||||
// Manually load plugin
|
||||
$elementor_plugin_path = getenv('WP_TESTS_ELEMENTOR_DIR');
|
||||
|
||||
require $elementor_plugin_path;
|
||||
});
|
||||
|
||||
// Removes all sql tables on shutdown
|
||||
// Do this action last
|
||||
tests_add_filter('shutdown', 'drop_tables', 999999);
|
||||
|
||||
require $_tests_dir . '/includes/bootstrap.php';
|
||||
|
||||
remove_action('admin_init', '_maybe_update_themes');
|
||||
remove_action('admin_init', '_maybe_update_core');
|
||||
remove_action('admin_init', '_maybe_update_plugins');
|
||||
/**
|
||||
* WordPress added deprecation error messages to print_emoji_styles in 6.4, which causes our PHPUnit assertions
|
||||
* to fail. This is something that might still change during the beta period, but for now we need to remove this action
|
||||
* as to not block all our PRs, but still run tests on WP Nightly.
|
||||
*
|
||||
* @see https://core.trac.wordpress.org/changeset/56682/
|
||||
*/
|
||||
remove_action('wp_print_styles', 'print_emoji_styles');
|
||||
|
||||
// Set behavior like on WP Admin for things like WP_Query->is_admin (default post status will be based on `show_in_admin_all_list`).
|
||||
if (!defined('WP_ADMIN')) {
|
||||
define('WP_ADMIN', true);
|
||||
}
|
||||
|
||||
do_action('plugins_loaded');
|
||||
|
||||
function initialize_elementor_plugin($plugin_class)
|
||||
{
|
||||
if (!class_exists($plugin_class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $plugin_class::instance();
|
||||
}
|
||||
|
||||
$plugin_instance = initialize_elementor_plugin('Elementor\Plugin');
|
||||
|
||||
$plugin_instance->initialize_container();
|
||||
|
||||
do_action('init');
|
||||
do_action('wp_loaded');
|
14
tests/phpunit/hello-elementor/test-first.php
Normal file
14
tests/phpunit/hello-elementor/test-first.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace HelloElementor\Testing;
|
||||
|
||||
use ElementorEditorTesting\Elementor_Test_Base;
|
||||
|
||||
class Elementor_Test_First extends Elementor_Test_Base
|
||||
{
|
||||
|
||||
public function test_truthness()
|
||||
{
|
||||
$this->assertTrue(defined('HELLO_ELEMENTOR_VERSION'));
|
||||
}
|
||||
}
|
295
tests/playwright/assets/api-requests.ts
Normal file
295
tests/playwright/assets/api-requests.ts
Normal file
|
@ -0,0 +1,295 @@
|
|||
import fs from 'fs';
|
||||
import { type APIRequestContext } from '@playwright/test';
|
||||
import crypto from 'crypto';
|
||||
import { Image, Post, WpPage, User } from '../types/types.ts';
|
||||
|
||||
export default class ApiRequests {
|
||||
private readonly nonce: string;
|
||||
private readonly baseUrl: string;
|
||||
constructor( baseUrl: string, nonce: string ) {
|
||||
this.nonce = nonce;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public async create( request: APIRequestContext, entity: string, data: Post ) {
|
||||
const response = await request.post( `${ this.baseUrl }/index.php`, {
|
||||
params: { rest_route: `/wp/v2/${ entity }` },
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
multipart: data,
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to create a ${ entity }: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
${ response.url() }
|
||||
TEST_PARALLEL_INDEX: ${ process.env.TEST_PARALLEL_INDEX }
|
||||
NONCE: ${ this.nonce }
|
||||
` );
|
||||
}
|
||||
const { id } = await response.json();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public async createMedia( request: APIRequestContext, image: Image ) {
|
||||
const imagePath = image.filePath;
|
||||
const response = await request.post( `${ this.baseUrl }/index.php`, {
|
||||
|
||||
params: { rest_route: '/wp/v2/media' },
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
multipart: {
|
||||
file: fs.createReadStream( imagePath ),
|
||||
title: image.title,
|
||||
status: 'publish',
|
||||
description: image.description,
|
||||
altText: image.alt_text,
|
||||
caption: image.caption,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to create default media: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
|
||||
const { id } = await response.json();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public async deleteMedia( request: APIRequestContext, ids: string[] ) {
|
||||
const requests = [];
|
||||
|
||||
for ( const id in ids ) {
|
||||
requests.push( request.delete( `${ this.baseUrl }/index.php`, {
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
params: {
|
||||
rest_route: `/wp/v2/media/${ ids[ id ] }`,
|
||||
force: 1,
|
||||
},
|
||||
} ) );
|
||||
}
|
||||
|
||||
await Promise.all( requests );
|
||||
}
|
||||
|
||||
public async cleanUpTestPages( request: APIRequestContext, shouldDeleteAllPages = false ) {
|
||||
const pagesPublished = await this.getPages( request ),
|
||||
pagesDraft = await this.getPages( request, 'draft' ),
|
||||
pages = [ ...pagesPublished, ...pagesDraft ];
|
||||
|
||||
const pageIds = pages
|
||||
.filter( ( page: WpPage ) => shouldDeleteAllPages || page.title.rendered.includes( 'Playwright Test Page' ) )
|
||||
.map( ( page: WpPage ) => page.id );
|
||||
|
||||
for ( const id of pageIds ) {
|
||||
await this.deletePage( request, id );
|
||||
}
|
||||
}
|
||||
|
||||
public async installPlugin( request: APIRequestContext, slug: string, active: boolean ) {
|
||||
const response = await request.post( `${ this.baseUrl }/index.php`, {
|
||||
params: {
|
||||
rest_route: `/wp/v2/plugins`,
|
||||
slug,
|
||||
status: active ? 'active' : 'inactive',
|
||||
},
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to install a plugin: ${ response ? response.status() : '<no status>' }.
|
||||
${ response ? await response.text() : '<no response>' }
|
||||
slug: ${ slug }
|
||||
` );
|
||||
}
|
||||
const { plugin } = await response.json();
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public async deactivatePlugin( request: APIRequestContext, slug: string ) {
|
||||
const response = await request.post( `${ this.baseUrl }/index.php`, {
|
||||
params: {
|
||||
rest_route: `/wp/v2/plugins/${ slug }`,
|
||||
status: 'inactive',
|
||||
},
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
} );
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to deactivate a plugin: ${ response ? response.status() : '<no status>' }.
|
||||
${ response ? await response.text() : '<no response>' }
|
||||
slug: ${ slug }
|
||||
` );
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePlugin( request: APIRequestContext, slug: string ) {
|
||||
const response = await this._delete( request, 'plugins', slug );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to delete a plugin: ${ response ? response.status() : '<no status>' }.
|
||||
${ response ? await response.text() : '<no response>' }
|
||||
slug: ${ slug }
|
||||
` );
|
||||
}
|
||||
}
|
||||
|
||||
public async getTheme( request: APIRequestContext, status?: 'active' | 'inactive' ) {
|
||||
return await this.get( request, 'themes', status );
|
||||
}
|
||||
|
||||
public async customGet( request: APIRequestContext, restRoute: string, multipart? ) {
|
||||
const response = await request.get( `${ this.baseUrl }/${ restRoute }`, {
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
multipart,
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to get from ${ restRoute }: ${ response.status() }.
|
||||
${ this.baseUrl }
|
||||
` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async customPut( request: APIRequestContext, restRoute: string, data ) {
|
||||
const response = await request.put( `${ this.baseUrl }/${ restRoute }`, {
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
data,
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to put to ${ restRoute }: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
}
|
||||
|
||||
private async get( request: APIRequestContext, entity: string, status: string = 'publish' ) {
|
||||
const response = await request.get( `${ this.baseUrl }/index.php`, {
|
||||
params: {
|
||||
rest_route: `/wp/v2/${ entity }`,
|
||||
status,
|
||||
},
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to get a ${ entity }: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getPages( request: APIRequestContext, status: string = 'publish' ) {
|
||||
return await this.get( request, 'pages', status );
|
||||
}
|
||||
|
||||
private async deletePage( request: APIRequestContext, pageId: string ) {
|
||||
await this._delete( request, 'pages', pageId );
|
||||
}
|
||||
|
||||
public async deleteUser( request: APIRequestContext, userId: string ) {
|
||||
const response = await request.delete( `${ this.baseUrl }/index.php`, {
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
params: {
|
||||
rest_route: `/wp/v2/users/${ userId }`,
|
||||
force: true,
|
||||
reassign: '-1',
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to delete a user with id: ${ userId }: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async _delete( request: APIRequestContext, entity: string, id: string ) {
|
||||
const response = await request.delete( `${ this.baseUrl }/index.php`, {
|
||||
params: {
|
||||
rest_route: `/wp/v2/${ entity }/${ id }`,
|
||||
},
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to delete a ${ entity } with id '${ id }': ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async createNewUser( request: APIRequestContext, user: User ) {
|
||||
const randomNumber = crypto.randomInt( 0, 1000 );
|
||||
const username = `${ user.username }${ randomNumber }`,
|
||||
email = user.email || username + '@example.com',
|
||||
password = user.password || 'password',
|
||||
roles = user.roles;
|
||||
|
||||
const response = await request.post( `${ this.baseUrl }/index.php`, {
|
||||
|
||||
params: { rest_route: '/wp/v2/users' },
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
},
|
||||
multipart: {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
roles: [ ...roles ],
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
Failed to create new user: ${ response.status() }.
|
||||
${ await response.text() }
|
||||
` );
|
||||
}
|
||||
|
||||
const { id } = await response.json();
|
||||
|
||||
return { id, username, password };
|
||||
}
|
||||
}
|
89
tests/playwright/assets/breakpoints.ts
Normal file
89
tests/playwright/assets/breakpoints.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { Locator, type Page } from '@playwright/test';
|
||||
import EditorPage from '../pages/editor-page.ts';
|
||||
import { BreakpointEditableDevice, Device } from '../types/types.ts';
|
||||
import EditorSelectors from '../selectors/editor-selectors.ts';
|
||||
|
||||
export default class {
|
||||
readonly page: Page;
|
||||
constructor( page: Page ) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
static getDeviceLocator( page: Page, device: Device ): Locator {
|
||||
const baseLocator = page.locator( '[aria-label="Switch Device"]' );
|
||||
return baseLocator.locator( `[data-testid="switch-device-to-${ device }"]` );
|
||||
}
|
||||
|
||||
static getAll(): Device[] {
|
||||
return [ 'mobile', 'mobile_extra', 'tablet', 'tablet_extra', 'laptop', 'desktop', 'widescreen' ];
|
||||
}
|
||||
|
||||
static getBasic(): Device[] {
|
||||
return [ 'mobile', 'tablet', 'desktop' ];
|
||||
}
|
||||
|
||||
async saveOrUpdate( editor: EditorPage, toReload = false ) {
|
||||
const hasTopBar: boolean = await editor.hasTopBar();
|
||||
if ( hasTopBar ) {
|
||||
await editor.saveSiteSettingsWithTopBar( toReload );
|
||||
} else {
|
||||
await editor.saveSiteSettingsNoTopBar();
|
||||
}
|
||||
}
|
||||
|
||||
async addAllBreakpoints( editor: EditorPage, experimentPostId?: string ) {
|
||||
await editor.openSiteSettings( 'settings-layout' );
|
||||
await editor.openSection( 'section_breakpoints' );
|
||||
await this.page.waitForSelector( 'text=Active Breakpoints' );
|
||||
|
||||
const devices = [ 'Mobile Landscape', 'Tablet Landscape', 'Laptop', 'Widescreen' ];
|
||||
|
||||
for ( const device of devices ) {
|
||||
if ( await this.page.$( '.select2-selection__e-plus-button' ) ) {
|
||||
await this.page.click( '.select2-selection__e-plus-button' );
|
||||
await this.page.click( `li:has-text("${ device }")` );
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveOrUpdate( editor, true );
|
||||
|
||||
if ( experimentPostId ) {
|
||||
await this.page.goto( `/wp-admin/post.php?post=${ experimentPostId }&action=elementor` );
|
||||
} else {
|
||||
await this.page.reload();
|
||||
|
||||
if ( await this.page.$( '#elementor-panel-header-kit-close' ) ) {
|
||||
await this.page.locator( '#elementor-panel-header-kit-close' ).click( { timeout: 30000 } );
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForSelector( '#elementor-editor-wrapper' );
|
||||
}
|
||||
|
||||
async resetBreakpoints( editor: EditorPage ) {
|
||||
await editor.openSiteSettings( 'settings-layout' );
|
||||
await editor.openSection( 'section_breakpoints' );
|
||||
await this.page.waitForSelector( 'text=Active Breakpoints' );
|
||||
|
||||
const removeBreakpointButton = EditorSelectors.panels.siteSettings.layout.breakpoints.removeBreakpointButton;
|
||||
while ( await this.page.locator( removeBreakpointButton ).count() > 0 ) {
|
||||
await this.page.click( removeBreakpointButton );
|
||||
}
|
||||
await this.saveOrUpdate( editor, true );
|
||||
}
|
||||
|
||||
getBreakpointInputLocator( page: Page, device: BreakpointEditableDevice ): Locator {
|
||||
return page.locator( `input[data-setting="viewport_${ device }"]` );
|
||||
}
|
||||
|
||||
async setBreakpoint( editor: EditorPage, device: BreakpointEditableDevice, value: number ) {
|
||||
await editor.openSiteSettings( 'settings-layout' );
|
||||
await editor.openSection( 'section_breakpoints' );
|
||||
await this.page.waitForSelector( 'text=Active Breakpoints' );
|
||||
|
||||
const locator = this.getBreakpointInputLocator( this.page, device );
|
||||
await locator.fill( String( value ) );
|
||||
await this.saveOrUpdate( editor );
|
||||
await this.page.locator( EditorSelectors.toast ).waitFor();
|
||||
}
|
||||
}
|
59
tests/playwright/assets/elements-utils.ts
Normal file
59
tests/playwright/assets/elements-utils.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { $eType, ElementorType } from '../types/types.ts';
|
||||
|
||||
/**
|
||||
* Add element to the page using model and parent container.
|
||||
* @param {Object} props
|
||||
* @param {Object} props.model
|
||||
* @param {string | null} props.container
|
||||
* @param {boolean} props.isContainerASection
|
||||
* @return {string | undefined}
|
||||
*/
|
||||
|
||||
let parent: unknown;
|
||||
let elementor: ElementorType;
|
||||
let $e: $eType;
|
||||
export const addElement = ( props: { model: unknown, container: null | string, isContainerASection: boolean } ): string | undefined => {
|
||||
if ( props.container ) {
|
||||
parent = elementor.getContainer( props.container );
|
||||
} else {
|
||||
// If a `container` isn't supplied - create a new Section.
|
||||
parent = $e.run(
|
||||
'document/elements/create',
|
||||
{
|
||||
model: { elType: 'section' },
|
||||
columns: 1,
|
||||
container: elementor.getContainer( 'document' ),
|
||||
},
|
||||
);
|
||||
|
||||
props.isContainerASection = true;
|
||||
}
|
||||
|
||||
if ( props.isContainerASection && 'object' === typeof parent && 'children' in parent ) {
|
||||
parent = parent.children[ 0 ];
|
||||
}
|
||||
|
||||
const element = $e.run(
|
||||
'document/elements/create',
|
||||
{
|
||||
model: props.model,
|
||||
container: parent,
|
||||
},
|
||||
);
|
||||
|
||||
if ( 'object' === typeof element && 'id' in element && 'string' === typeof element.id ) {
|
||||
return element.id;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an Elementor element CSS selector using Container ID.
|
||||
*
|
||||
* @param {string} id - Container ID.
|
||||
*
|
||||
* @return {string} css selector
|
||||
*/
|
||||
export const getElementSelector = ( id: string ) => {
|
||||
return `[data-id = "${ id }"]`;
|
||||
};
|
6
tests/playwright/assets/wp-cli.ts
Normal file
6
tests/playwright/assets/wp-cli.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const wpCli = async ( command: string ) => {
|
||||
const port = ( 1 === Number( process.env.TEST_PARALLEL_INDEX ) ) ? 8889 : 8888;
|
||||
|
||||
const { cli } = await import( '@elementor/wp-lite-env' );
|
||||
await cli( port, command );
|
||||
};
|
8
tests/playwright/config/timeouts.ts
Normal file
8
tests/playwright/config/timeouts.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const timeouts = {
|
||||
singleTest: 90_000,
|
||||
global: 15 * 60_000,
|
||||
expect: 5_000,
|
||||
action: 5_000,
|
||||
longAction: 10_000,
|
||||
navigation: 10_000,
|
||||
};
|
49
tests/playwright/pages/base-page.ts
Normal file
49
tests/playwright/pages/base-page.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { type Page, type TestInfo } from '@playwright/test';
|
||||
|
||||
export default class BasePage {
|
||||
readonly page: Page;
|
||||
readonly testInfo: TestInfo;
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('@playwright/test').TestInfo} testInfo
|
||||
*/
|
||||
constructor( page: Page, testInfo: TestInfo ) {
|
||||
if ( ! page || ! testInfo ) {
|
||||
throw new Error( 'Page and TestInfo must be provided' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
this.page = page;
|
||||
|
||||
/**
|
||||
* @type {import('@playwright/test').TestInfo}
|
||||
*/
|
||||
this.testInfo = testInfo;
|
||||
|
||||
const { baseURL, proxy } = this.testInfo.config.projects[ 0 ].use;
|
||||
|
||||
// If wordpress is not located on the domain's top-level (e.g: http://local.host/test-wordpress ), playwright's `baseURL` cannot handle it.
|
||||
if ( proxy ) {
|
||||
this.page = new Proxy( this.page, {
|
||||
get: ( target, key ) => {
|
||||
switch ( key ) {
|
||||
case 'goto':
|
||||
return ( path: string ) => page.goto( baseURL + path );
|
||||
|
||||
case 'waitForNavigation': {
|
||||
return ( args: { url?: string } ) => {
|
||||
args = ( args.url ) ? { url: baseURL + args.url } : args;
|
||||
|
||||
return page.waitForNavigation( args );
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return target[ key ];
|
||||
},
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
1386
tests/playwright/pages/editor-page.ts
Normal file
1386
tests/playwright/pages/editor-page.ts
Normal file
File diff suppressed because it is too large
Load diff
412
tests/playwright/pages/wp-admin-page.ts
Normal file
412
tests/playwright/pages/wp-admin-page.ts
Normal file
|
@ -0,0 +1,412 @@
|
|||
import { type APIRequestContext, type Page, Response, type TestInfo } from '@playwright/test';
|
||||
import BasePage from './base-page.ts';
|
||||
import EditorPage from './editor-page.ts';
|
||||
import { ElementorType, WindowType } from '../types/types.ts';
|
||||
import { wpCli } from '../assets/wp-cli.ts';
|
||||
import ApiRequests from '../assets/api-requests.ts';
|
||||
let elementor: ElementorType;
|
||||
|
||||
export default class WpAdminPage extends BasePage {
|
||||
protected readonly apiRequests: ApiRequests;
|
||||
|
||||
constructor( page: Page, testInfo: TestInfo, apiRequests: ApiRequests ) {
|
||||
super( page, testInfo );
|
||||
this.apiRequests = apiRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the WordPress dashboard.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async gotoDashboard(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* If not logged in, log in to WordPress. Otherwise, go to the WordPress dashboard.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async login(): Promise<void> {
|
||||
await this.gotoDashboard();
|
||||
|
||||
const loggedIn = await this.page.$( 'text=Dashboard' );
|
||||
|
||||
if ( loggedIn ) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.page.waitForSelector( 'text=Log In' );
|
||||
await this.page.fill( 'input[name="log"]', process.env.USERNAME );
|
||||
await this.page.fill( 'input[name="pwd"]', process.env.PASSWORD );
|
||||
await this.page.click( 'text=Log In' );
|
||||
await this.page.waitForSelector( 'text=Dashboard' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in to WordPress with custom credentials.
|
||||
*
|
||||
* @param {string} username - The username to log in with.
|
||||
* @param {string} password - The password to log in with.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async customLogin( username: string, password: string ): Promise<void> {
|
||||
await this.gotoDashboard();
|
||||
const loggedIn = await this.page.$( 'text=Dashboard' );
|
||||
|
||||
if ( loggedIn ) {
|
||||
await this.page.hover( '#wp-admin-bar-top-secondary' );
|
||||
await this.page.click( '#wp-admin-bar-logout > a' );
|
||||
}
|
||||
|
||||
await this.page.fill( 'input[name="log"]', username );
|
||||
await this.page.fill( 'input[name="pwd"]', password );
|
||||
await this.page.locator( 'text=Log In' ).last().click();
|
||||
await this.page.waitForSelector( 'text=Dashboard' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new Elementor page.
|
||||
*
|
||||
* @param {boolean} setWithApi - Optional. Whether to create the page with the API. Default is true.
|
||||
* @param {boolean} setPageName - Optional. Whether to set the page name. Default is true.
|
||||
*
|
||||
* @return {Promise<EditorPage>} A promise that resolves to the new editor page instance.
|
||||
*/
|
||||
async openNewPage( setWithApi: boolean = true, setPageName: boolean = true ): Promise<EditorPage> {
|
||||
if ( setWithApi ) {
|
||||
await this.createNewPostWithAPI();
|
||||
} else {
|
||||
await this.createNewPostFromDashboard( setPageName );
|
||||
}
|
||||
|
||||
await this.page.waitForLoadState( 'load', { timeout: 20000 } );
|
||||
await this.waitForPanel();
|
||||
await this.closeAnnouncementsIfVisible();
|
||||
|
||||
return new EditorPage( this.page, this.testInfo );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new page with the API and open it in Elementor.
|
||||
*
|
||||
* @return {Promise<string>} A promise that resolves to the created page ID.
|
||||
*/
|
||||
async createNewPostWithAPI(): Promise<string> {
|
||||
const request: APIRequestContext = this.page.context().request,
|
||||
postDataInitial = {
|
||||
title: 'Playwright Test Page - Uninitialized',
|
||||
content: '',
|
||||
},
|
||||
postId = await this.apiRequests.create( request, 'pages', postDataInitial ),
|
||||
postDataUpdated = {
|
||||
title: `Playwright Test Page #${ postId }`,
|
||||
};
|
||||
|
||||
await this.apiRequests.create( request, `pages/${ postId }`, postDataUpdated );
|
||||
await this.page.goto( `/wp-admin/post.php?post=${ postId }&action=elementor` );
|
||||
|
||||
return postId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new page from the WordPress dashboard.
|
||||
*
|
||||
* @param {boolean} setPageName - Whether to set the page name.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async createNewPostFromDashboard( setPageName: boolean ): Promise<void> {
|
||||
if ( ! await this.page.$( '.e-overview__create > a' ) ) {
|
||||
await this.gotoDashboard();
|
||||
}
|
||||
|
||||
await this.page.click( '.e-overview__create > a' );
|
||||
|
||||
if ( ! setPageName ) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setPageName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page name.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setPageName(): Promise<void> {
|
||||
await this.page.locator( '#elementor-panel-footer-settings' ).click();
|
||||
|
||||
const pageId = await this.page.evaluate( () => elementor.config.initialDocument.id );
|
||||
await this.page.locator( '.elementor-control-post_title input' ).fill( `Playwright Test Page #${ pageId }` );
|
||||
|
||||
await this.page.locator( '#elementor-panel-footer-saver-options' ).click();
|
||||
await this.page.locator( '#elementor-panel-footer-sub-menu-item-save-draft' ).click();
|
||||
await this.page.locator( '#elementor-panel-header-add-button' ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the current page from Gutenberg to Elementor.
|
||||
*
|
||||
* @return {Promise<EditorPage>} A promise that resolves to the editor page instance.
|
||||
*/
|
||||
async convertFromGutenberg(): Promise<EditorPage> {
|
||||
await Promise.all( [
|
||||
this.page.waitForResponse( async ( response ) => await this.blockUrlResponse( response ) ),
|
||||
this.page.click( '#elementor-switch-mode' ),
|
||||
] );
|
||||
|
||||
await this.page.waitForURL( '**/post.php?post=*&action=elementor' );
|
||||
await this.page.waitForLoadState( 'load', { timeout: 20000 } );
|
||||
await this.waitForPanel();
|
||||
|
||||
await this.closeAnnouncementsIfVisible();
|
||||
|
||||
return new EditorPage( this.page, this.testInfo );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response status for the API request.
|
||||
*
|
||||
* @param {Response} response - The response object.
|
||||
*
|
||||
* @return {Promise<boolean>} A promise that resolves to true if the response is a valid REST/JSON request with a 200 status.
|
||||
*/
|
||||
async blockUrlResponse( response: Response ): Promise<boolean> {
|
||||
const isRestRequest = response.url().includes( 'rest_route=%2Fwp%2Fv2%2Fpages%2' ); // For local testing
|
||||
const isJsonRequest = response.url().includes( 'wp-json/wp/v2/pages' ); // For CI testing
|
||||
return ( isJsonRequest || isRestRequest ) && 200 === response.status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the Elementor editor panel to finish loading.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async waitForPanel(): Promise<void> {
|
||||
await this.page.waitForSelector( '.elementor-panel-loading', { state: 'detached' } );
|
||||
await this.page.waitForSelector( '#elementor-loading', { state: 'hidden' } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate and deactivate Elementor experiments.
|
||||
*
|
||||
* TODO: The testing environment isn't clean between tests - Use with caution!
|
||||
*
|
||||
* @param {Object} experiments - Experiments settings ( `{ experiment_id: true / false }` );
|
||||
* @param {(boolean|string)=} oldUrl - Optional. Whether to use the old URL structure. Default is false.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setExperiments( experiments: { [ n: string ]: boolean | string }, oldUrl: boolean = false ): Promise<void> {
|
||||
if ( oldUrl ) {
|
||||
await this.page.goto( '/wp-admin/admin.php?page=elementor#tab-experiments' );
|
||||
await this.page.click( '#elementor-settings-tab-experiments' );
|
||||
} else {
|
||||
await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' );
|
||||
}
|
||||
|
||||
const prefix = 'e-experiment';
|
||||
|
||||
for ( const [ id, state ] of Object.entries( experiments ) ) {
|
||||
const selector = `#${ prefix }-${ id }`;
|
||||
|
||||
// Try to make the element visible - Since some experiments may be hidden for the user,
|
||||
// but actually exist and need to be tested.
|
||||
await this.page.evaluate( ( el ) => {
|
||||
const element: HTMLElement = document.querySelector( el );
|
||||
|
||||
if ( element ) {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
}, `.elementor_experiment-${ id }` );
|
||||
|
||||
await this.page.selectOption( selector, state ? 'active' : 'inactive' );
|
||||
|
||||
// Click to confirm any experiment that has dependencies.
|
||||
await this.confirmExperimentModalIfOpen();
|
||||
}
|
||||
|
||||
await this.page.click( '#submit' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all Elementor experiments to their default settings.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async resetExperiments(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' );
|
||||
await this.page.getByRole( 'button', { name: 'default' } ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set site language.
|
||||
*
|
||||
* @param {string} language - The site language to set.
|
||||
* @param {string|null} userLanguage - Optional. The user language to set. Default is null.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setSiteLanguage( language: string, userLanguage: string = null ): Promise<void> {
|
||||
let languageCheck = language;
|
||||
|
||||
if ( 'he_IL' === language ) {
|
||||
languageCheck = 'he-IL';
|
||||
} else if ( '' === language ) {
|
||||
languageCheck = 'en_US';
|
||||
}
|
||||
|
||||
await this.page.goto( '/wp-admin/options-general.php' );
|
||||
|
||||
const isLanguageActive = await this.page.locator( 'html[lang=' + languageCheck + ']' ).isVisible();
|
||||
|
||||
if ( ! isLanguageActive ) {
|
||||
await this.page.selectOption( '#WPLANG', language );
|
||||
await this.page.locator( '#submit' ).click();
|
||||
}
|
||||
|
||||
const userProfileLanguage = null !== userLanguage ? userLanguage : language;
|
||||
await this.setUserLanguage( userProfileLanguage );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user language.
|
||||
*
|
||||
* @param {string} language - The language to set.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setUserLanguage( language: string ): Promise<void> {
|
||||
await this.page.goto( 'wp-admin/profile.php' );
|
||||
await this.page.selectOption( '[name="locale"]', language );
|
||||
await this.page.locator( '#submit' ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the Elementor experiment modal if it's open.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async confirmExperimentModalIfOpen(): Promise<void> {
|
||||
const dialogButton = this.page.locator( '.dialog-type-confirm .dialog-confirm-ok' );
|
||||
|
||||
if ( await dialogButton.isVisible() ) {
|
||||
await dialogButton.click();
|
||||
|
||||
// Clicking the confirm button - "Activate" or "Deactivate" - will immediately save the existing experiments,
|
||||
// so we need to wait for the page to save and reload before we continue on to set any more experiments.
|
||||
await this.page.waitForLoadState( 'load' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active WordPress theme.
|
||||
*
|
||||
* @return {Promise<string>} The name of the active WordPress theme.
|
||||
*/
|
||||
async getActiveTheme(): Promise<string> {
|
||||
const request: APIRequestContext = this.page.context().request;
|
||||
const themeData = await this.apiRequests.getTheme( request, 'active' );
|
||||
return themeData[ 0 ].stylesheet;
|
||||
}
|
||||
|
||||
async activateTheme( theme: string ) {
|
||||
await wpCli( `wp theme activate ${ theme }` );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable uploading SVG files.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async enableAdvancedUploads(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' );
|
||||
await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '1' );
|
||||
await this.page.getByRole( 'button', { name: 'Save Changes' } ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable uploading SVG files.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async disableAdvancedUploads(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' );
|
||||
await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '' );
|
||||
await this.page.getByRole( 'button', { name: 'Save Changes' } ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Elementor announcements if they are visible.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async closeAnnouncementsIfVisible(): Promise<void> {
|
||||
if ( await this.page.locator( '#e-announcements-root' ).count() > 0 ) {
|
||||
await this.page.evaluate( ( selector ) => document.getElementById( selector ).remove(), 'e-announcements-root' );
|
||||
}
|
||||
let window: WindowType;
|
||||
await this.page.evaluate( () => {
|
||||
// @ts-ignore editor session is on the window object
|
||||
const editorSessionId = window.EDITOR_SESSION_ID;
|
||||
window.sessionStorage.setItem( 'ai_promotion_introduction_editor_session_key', editorSessionId );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the page with Elementor.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async editWithElementor(): Promise<void> {
|
||||
await this.page.getByRole( 'link', { name: ' Edit with Elementor' } ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the block editor popup if it's visible.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async closeBlockEditorPopupIfVisible(): Promise<void> {
|
||||
await this.page.locator( '#elementor-switch-mode-button' ).waitFor();
|
||||
if ( await this.page.getByRole( 'button', { name: 'Close' } ).isVisible() ) {
|
||||
await this.page.getByRole( 'button', { name: 'Close' } ).click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new WordPress page.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async openNewWordpressPage(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/post-new.php?post_type=page' );
|
||||
await this.closeBlockEditorPopupIfVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the WordPress admin bar.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async hideAdminBar(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/profile.php' );
|
||||
await this.page.locator( '#admin_bar_front' ).uncheck();
|
||||
await this.page.locator( '#submit' ).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the WordPress admin bar.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async showAdminBar(): Promise<void> {
|
||||
await this.page.goto( '/wp-admin/profile.php' );
|
||||
await this.page.locator( '#admin_bar_front' ).check();
|
||||
await this.page.locator( '#submit' ).click();
|
||||
}
|
||||
}
|
52
tests/playwright/parallelTest.ts
Normal file
52
tests/playwright/parallelTest.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { request, test as baseTest } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fetchNonce, login } from './wp-authentication.ts';
|
||||
import ApiRequests from './assets/api-requests.ts';
|
||||
|
||||
export const parallelTest = baseTest.extend< NonNullable<unknown>, { workerStorageState: string, workerBaseURL: string, apiRequests: ApiRequests }>( {
|
||||
// Use the same storage state for all tests in this worker.
|
||||
baseURL: ( { workerBaseURL }, use ) => use( workerBaseURL ),
|
||||
workerBaseURL: [ async ( {}, use ) => {
|
||||
await use( process.env.BASE_URL || ( ( 1 === Number( process.env.TEST_PARALLEL_INDEX ) )
|
||||
? process.env.TEST_SERVER
|
||||
: process.env.DEV_SERVER ),
|
||||
);
|
||||
}, { scope: 'worker' } ],
|
||||
|
||||
// Use the same storage state for all tests in this worker.
|
||||
storageState: ( { workerStorageState }, use ) => use( workerStorageState ),
|
||||
|
||||
// Authenticate once per worker with a worker-scoped fixture.
|
||||
workerStorageState: [ async ( { workerBaseURL }, use, testInfo ) => {
|
||||
// Use parallelIndex as a unique identifier for each worker.
|
||||
const id = testInfo.workerIndex;
|
||||
const fileName = path.resolve( testInfo.project.outputDir, `.storageState-${ id }.json` );
|
||||
|
||||
if ( fs.existsSync( fileName ) ) {
|
||||
// Reuse existing authentication state if any.
|
||||
await use( fileName );
|
||||
return;
|
||||
}
|
||||
|
||||
// Send authentication request.
|
||||
const context = await login( request, process.env.USERNAME || 'admin', process.env.PASSWORD || 'password', workerBaseURL );
|
||||
await context.storageState( { path: fileName } );
|
||||
await context.dispose();
|
||||
|
||||
await use( fileName );
|
||||
}, { scope: 'worker' } ],
|
||||
|
||||
// Use the same storage state for all tests in this worker.
|
||||
apiRequests: [ async ( { workerStorageState, workerBaseURL }, use ) => {
|
||||
const context = await request.newContext( { storageState: workerStorageState } );
|
||||
try {
|
||||
const nonce = await fetchNonce( context, workerBaseURL );
|
||||
const apiRequests = new ApiRequests( workerBaseURL, nonce );
|
||||
await use( apiRequests );
|
||||
} catch ( e ) {
|
||||
throw new Error( `Failed to fetch Nonce. Base URL: ${ workerBaseURL }, Storage State: ${ workerStorageState } `, { cause: e } );
|
||||
}
|
||||
}, { scope: 'worker' } ],
|
||||
|
||||
} );
|
47
tests/playwright/playwright.config.ts
Normal file
47
tests/playwright/playwright.config.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { resolve } from 'path';
|
||||
import { defineConfig } from '@playwright/test';
|
||||
import { config as _config } from 'dotenv';
|
||||
import { timeouts } from './config/timeouts.ts';
|
||||
|
||||
process.env.DEV_SERVER = 'http://localhost:8888';
|
||||
process.env.TEST_SERVER = 'http://localhost:8889';
|
||||
process.env.DEBUG_PORT = ( 1 === Number( process.env.TEST_PARALLEL_INDEX ) ) ? '9223' : '9222';
|
||||
|
||||
_config( {
|
||||
path: resolve( __dirname, '../../.env' ),
|
||||
} );
|
||||
|
||||
export default defineConfig( {
|
||||
testDir: './tests',
|
||||
timeout: timeouts.singleTest,
|
||||
globalTimeout: timeouts.global,
|
||||
expect: {
|
||||
timeout: timeouts.expect,
|
||||
toMatchSnapshot: { maxDiffPixelRatio: 0.03 },
|
||||
toHaveScreenshot: { maxDiffPixelRatio: 0.03 },
|
||||
},
|
||||
forbidOnly: !! process.env.CI,
|
||||
retries: process.env.CI ? 9 : 0,
|
||||
workers: process.env.CI ? 2 : 1,
|
||||
fullyParallel: false,
|
||||
reporter: process.env.CI
|
||||
? [ [ 'github' ], [ 'list' ] ]
|
||||
: [ [ 'list' ] ],
|
||||
use: {
|
||||
launchOptions: {
|
||||
args: [ `--remote-debugging-port=${ process.env.DEBUG_PORT }` ],
|
||||
},
|
||||
headless: !! process.env.CI,
|
||||
ignoreHTTPSErrors: true,
|
||||
actionTimeout: 30000, // Increase from the default
|
||||
navigationTimeout: 45000, // Increase from the default
|
||||
trace: 'retain-on-failure',
|
||||
video: process.env.CI ? 'retain-on-failure' : 'off',
|
||||
baseURL: process.env.BASE_URL ||
|
||||
( ( 1 === Number( process.env.TEST_PARALLEL_INDEX ) )
|
||||
? process.env.TEST_SERVER
|
||||
: process.env.DEV_SERVER ),
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
storageState: `./storageState-${ process.env.TEST_PARALLEL_INDEX }.json`,
|
||||
},
|
||||
} );
|
241
tests/playwright/selectors/editor-selectors.ts
Normal file
241
tests/playwright/selectors/editor-selectors.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
const EditorSelectors = {
|
||||
getWidgetByName: ( title: string ) => `[data-widget_type="${ title }.default"]`,
|
||||
widget: '[data-element_type="widget"]',
|
||||
container: '[data-element_type="container"]',
|
||||
item: '.elementor-repeater-row-item-title',
|
||||
plusIcon: '.eicon-plus-circle',
|
||||
siteTitle: '.site-title >> nth=0',
|
||||
pageTitle: '.entry-title >> nth=0',
|
||||
pageHeader: '.page-header',
|
||||
toast: '#elementor-toast',
|
||||
addNewSection: '#elementor-add-new-section',
|
||||
panels: {
|
||||
topBar: {
|
||||
wrapper: '#elementor-editor-wrapper-v2',
|
||||
},
|
||||
menu: {
|
||||
wrapper: '#elementor-panel-page-menu',
|
||||
footerButton: '#elementor-panel-header-menu-button i',
|
||||
},
|
||||
elements: {
|
||||
wrapper: '#elementor-panel-page-elements',
|
||||
footerButton: '#elementor-panel-header-add-button i',
|
||||
},
|
||||
pageSettings: {
|
||||
wrapper: '#elementor-panel-page-settings',
|
||||
footerButton: '#elementor-panel-footer-settings i',
|
||||
},
|
||||
siteSettings: {
|
||||
wrapper: '#elementor-panel-page-menu',
|
||||
saveButton: '//button[text()="Save Changes"]',
|
||||
layout: {
|
||||
breakpoints: {
|
||||
removeBreakpointButton: '#elementor-kit-panel-content .select2-selection__choice__remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
userPreferences: {
|
||||
wrapper: '#elementor-panel-editorPreferences-settings-controls',
|
||||
},
|
||||
footerTools: {
|
||||
wrapper: '#elementor-panel-footer',
|
||||
updateButton: '#elementor-panel-saver-button-publish-label',
|
||||
},
|
||||
navigator: {
|
||||
wrapper: '#elementor-navigator',
|
||||
footer: '#elementor-navigator__footer',
|
||||
closeButton: '#elementor-navigator__close',
|
||||
footerButton: '#elementor-panel-footer-navigator i',
|
||||
},
|
||||
promotionCard: '[data-testid="e-promotion-card"]',
|
||||
popoverCard: '[data-testid="e-popover-card"]',
|
||||
},
|
||||
refreshPopup: {
|
||||
reloadButton: '#elementor-save-kit-refresh-page .dialog-button.dialog-ok.dialog-alert-ok',
|
||||
},
|
||||
|
||||
media: {
|
||||
preview: '.elementor-control-media__preview',
|
||||
imageByTitle: ( imageTitle: string ) => `[aria-label="${ imageTitle }"]`,
|
||||
selectBtn: '.button.media-button',
|
||||
imageInp: 'input[type="file"]',
|
||||
addGalleryButton: 'button.media-button-gallery',
|
||||
images: '.attachments-wrapper li',
|
||||
imgCaption: '#attachment-details-caption',
|
||||
imgDescription: '#attachment-details-description',
|
||||
},
|
||||
button: {
|
||||
getByName: ( name: string ) => `.elementor-button:has-text("${ name }")`,
|
||||
id: '[data-setting="button_css_id"]',
|
||||
url: 'input[data-setting="url"]',
|
||||
linkOptions: 'button[data-tooltip="Link Options"]',
|
||||
targetBlankChbox: 'input[data-setting="is_external"]',
|
||||
noFollowChbox: 'input[data-setting="nofollow"]',
|
||||
customAttributesInp: 'input[data-setting="custom_attributes"]',
|
||||
},
|
||||
heading: {
|
||||
h2: 'h2.elementor-heading-title',
|
||||
get link() {
|
||||
return `${ this.h2 } a`;
|
||||
},
|
||||
},
|
||||
image: {
|
||||
widget: '[data-widget_type="image.default"]',
|
||||
linkSelect: 'link_to',
|
||||
imageSizeSelect: 'image_size',
|
||||
widthInp: 'input[data-setting="width"]',
|
||||
heightInp: 'input[data-setting="height"]',
|
||||
get image() {
|
||||
return `${ this.widget } img`;
|
||||
},
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
lightBox: '.swiper-zoom-container',
|
||||
},
|
||||
icon: {
|
||||
widget: '[data-widget_type="icon.default"]',
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
},
|
||||
imageBox: {
|
||||
widget: '[data-widget_type="image-box.default"]',
|
||||
imageSizeSelect: 'thumbnail_size',
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
get image() {
|
||||
return `${ this.widget } img`;
|
||||
},
|
||||
},
|
||||
galleryControl: {
|
||||
addGalleryBtn: 'button.elementor-control-gallery-add',
|
||||
},
|
||||
imageCarousel: {
|
||||
widget: '[data-widget_type="image-carousel.default"]',
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
navigationSelect: '.elementor-control-navigation select',
|
||||
autoplaySelect: 'input[data-setting="autoplay"]',
|
||||
autoplaySpeedLabel: 'Autoplay Speed',
|
||||
autoplaySpeedInp: '[data-setting="autoplay_speed"]',
|
||||
autoplayToggle: '.elementor-switch-handle',
|
||||
captionSelect: 'select[data-setting="caption_type"]',
|
||||
imgCaption: 'figcaption.elementor-image-carousel-caption',
|
||||
prevSliderBtn: '.elementor-swiper-button-prev',
|
||||
nextSliderBtn: '.elementor-swiper-button-next',
|
||||
activeSlide: ( id: string ) => `.swiper-pagination-bullet-active[aria-label="Go to slide ${ id }"]`,
|
||||
activeSlideImg: ( name: string ) => `.swiper-slide-active img[alt="${ name }"]`,
|
||||
},
|
||||
textPath: {
|
||||
widget: '[data-widget_type="text-path.default"]',
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
get svgIcon() {
|
||||
return `${ this.widget } svg path.st0`;
|
||||
},
|
||||
},
|
||||
video: {
|
||||
widget: '[data-widget_type="video.default"]',
|
||||
get image() {
|
||||
return `${ this.widget } .elementor-custom-embed-image-overlay`;
|
||||
},
|
||||
lightBoxControlInp: '[data-setting="lightbox"]',
|
||||
lightBoxSetting: 'div[data-elementor-open-lightbox="yes"]',
|
||||
lightBoxDialog: '.elementor-lightbox',
|
||||
iframe: 'iframe[class*="elementor-video"]',
|
||||
playIcon: '[aria-label="Play"]',
|
||||
videoWrapper: '.elementor-video-wrapper',
|
||||
},
|
||||
socialIcons: {
|
||||
widget: '[data-widget_type="social-icons.default"]',
|
||||
get link() {
|
||||
return `${ this.widget } a`;
|
||||
},
|
||||
get svgIcon() {
|
||||
return `${ this.widget } svg path.st0`;
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
textEditorIframe: 'iframe[id*="elementorwpeditorview"]',
|
||||
body: '#tinymce',
|
||||
},
|
||||
googleMaps: {
|
||||
iframe: 'iframe[src*="https://maps.google.com/maps"]',
|
||||
showSatelliteViewBtn: 'button[title="Show satellite imagery"]',
|
||||
},
|
||||
soundCloud: {
|
||||
iframe: 'iframe[src*="https://w.soundcloud.com/"]',
|
||||
waveForm: 'div.waveform.loaded',
|
||||
},
|
||||
ai: {
|
||||
aiButton: '.e-ai-button',
|
||||
aiDialogCloseButton: '.MuiDialog-container button[aria-label="close"]',
|
||||
promptInput: 'input[name="prompt"]',
|
||||
resultTextarea: 'textarea.MuiInputBase-inputMultiline',
|
||||
image: {
|
||||
promptTextarea: '[data-testid="e-image-prompt"] textarea',
|
||||
typeInput: '#image-type + input',
|
||||
styleInput: '#style + input',
|
||||
aspectRationInput: '#aspect-ratio + input',
|
||||
generatedImage: '[data-testid="e-gallery-image"] img',
|
||||
},
|
||||
promptHistory: {
|
||||
button: 'button[aria-label="Show prompt history"]',
|
||||
modal: '#prompt-history-modal',
|
||||
closeButton: 'button[aria-label="Hide prompt history"]',
|
||||
upgradeMessageFullTestId: 'e-ph-upgrade-full',
|
||||
upgradeMessageSmallTestId: 'e-ph-upgrade-small',
|
||||
noDataMessageTestId: 'e-ph-empty',
|
||||
periodTestId: 'e-ph-p',
|
||||
itemTestId: 'e-ph-i',
|
||||
fallbackIconTestId: 'e-ph-fi',
|
||||
removeButton: 'button[aria-label="Remove item"]',
|
||||
reuseButton: 'button[aria-label="Reuse prompt"]',
|
||||
restoreButton: 'button[aria-label="Restore"]',
|
||||
editButton: 'button[aria-label="Edit result"]',
|
||||
},
|
||||
},
|
||||
floatingElements: {
|
||||
floatingButtons: {
|
||||
controls: {
|
||||
advanced: {
|
||||
sections: [
|
||||
'.elementor-control-advanced_layout_section',
|
||||
'.elementor-control-advanced_responsive_section',
|
||||
'.elementor-control-advanced_custom_controls_section',
|
||||
'.elementor-control-section_custom_css_pro',
|
||||
'.elementor-control-section_custom_attributes_pro',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
contextMenu: {
|
||||
menu: '.elementor-context-menu',
|
||||
saveAsGlobal: '.elementor-context-menu-list__item.elementor-context-menu-list__item-save.elementor-context-menu-list__item--disabled',
|
||||
notes: '.elementor-context-menu-list__item.elementor-context-menu-list__item-open_notes.elementor-context-menu-list__item--disabled',
|
||||
},
|
||||
dialog: {
|
||||
lightBox: '.elementor-lightbox',
|
||||
},
|
||||
onboarding: {
|
||||
upgradeButton: '.e-onboarding__button-action',
|
||||
skipButton: '.e-onboarding__button-skip',
|
||||
screenTitle: '.e-onboarding__page-content-section-title',
|
||||
removeLogoButton: '.e-onboarding__logo-remove',
|
||||
progressBar: {
|
||||
skippedItem: '.e-onboarding__progress-bar-item--skipped',
|
||||
completedItem: '.e-onboarding__progress-bar-item--completed',
|
||||
},
|
||||
features: {
|
||||
essential: '#essential',
|
||||
advanced: '#advanced',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default EditorSelectors;
|
35
tests/playwright/selectors/top-bar-selectors.ts
Normal file
35
tests/playwright/selectors/top-bar-selectors.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export type TopBarSelector = {
|
||||
attribute: string;
|
||||
attributeValue: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
elementorLogo: {
|
||||
attribute: 'value',
|
||||
attributeValue: 'selected',
|
||||
},
|
||||
elementsPanel: {
|
||||
attribute: 'value',
|
||||
attributeValue: 'Add Element',
|
||||
},
|
||||
documentSettings: {
|
||||
attribute: 'value',
|
||||
attributeValue: 'document-settings',
|
||||
},
|
||||
siteSettings: {
|
||||
attribute: 'value',
|
||||
attributeValue: 'Site Settings',
|
||||
},
|
||||
saveOptions: {
|
||||
attribute: 'aria-label',
|
||||
attributeValue: 'Save Options',
|
||||
},
|
||||
publish: {
|
||||
attribute: 'text',
|
||||
attributeValue: 'Publish',
|
||||
},
|
||||
checklistToggle: {
|
||||
attribute: 'aria-label',
|
||||
attributeValue: 'Checklist',
|
||||
},
|
||||
};
|
1
tests/playwright/temp-form-page-url.txt
Normal file
1
tests/playwright/temp-form-page-url.txt
Normal file
|
@ -0,0 +1 @@
|
|||
/?p=564
|
23
tests/playwright/tests/theme-settings.test.ts
Normal file
23
tests/playwright/tests/theme-settings.test.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { parallelTest as test } from '../parallelTest.ts';
|
||||
import { expect } from '@playwright/test';
|
||||
import WpAdminPage from '../pages/wp-admin-page.ts';
|
||||
|
||||
test.describe( 'Admin Menu', () => {
|
||||
test( 'Hello Elementor menu exists in sidebar with correct name', async ( { page, apiRequests }, testInfo ) => {
|
||||
// Arrange
|
||||
const wpAdmin = new WpAdminPage( page, testInfo, apiRequests );
|
||||
|
||||
// Navigate to dashboard
|
||||
await wpAdmin.gotoDashboard();
|
||||
|
||||
// Get the Hello Elementor menu element
|
||||
const helloElementorMenu = page.locator( '#toplevel_page_hello-elementor' );
|
||||
|
||||
// Verify the menu exists
|
||||
await expect( helloElementorMenu ).toBeVisible();
|
||||
|
||||
// Verify the menu has the correct title "Hello Elementor"
|
||||
const menuTitle = helloElementorMenu.locator( '.wp-menu-name' );
|
||||
await expect( menuTitle ).toHaveText( 'Hello' );
|
||||
} );
|
||||
} );
|
137
tests/playwright/types/types.ts
Normal file
137
tests/playwright/types/types.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
export type Image = {
|
||||
title: string,
|
||||
description?: string,
|
||||
altText?: string,
|
||||
caption?: string,
|
||||
extension: string,
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id?: string,
|
||||
username: string,
|
||||
password: string,
|
||||
email: string,
|
||||
roles?: string[],
|
||||
}
|
||||
|
||||
export type LinkOptions = {
|
||||
targetBlank?: boolean,
|
||||
noFollow?: boolean,
|
||||
customAttributes?: {key:string, value: string },
|
||||
linkTo?: boolean,
|
||||
linkInpSelector?: string
|
||||
}
|
||||
|
||||
export type WpPage = {
|
||||
title: {
|
||||
rendered?: string,
|
||||
}
|
||||
date?: string,
|
||||
dateGmt?: string,
|
||||
guid?: string,
|
||||
id?: string,
|
||||
link?: string,
|
||||
modified?: string,
|
||||
modifiedGmt?: string,
|
||||
slug: string,
|
||||
status?: 'publish' | 'future' | 'draft' | 'pending' | 'private',
|
||||
type?: string,
|
||||
password?: string,
|
||||
permalinkTemplate?: string,
|
||||
generatedSlug?: string,
|
||||
parent?: string,
|
||||
content: string,
|
||||
author?: string,
|
||||
excerpt?: string,
|
||||
featuredMedia?: string,
|
||||
commentStatus?: string,
|
||||
pingStatus?: string,
|
||||
menuOrder?: string,
|
||||
meta?: string,
|
||||
template?: string,
|
||||
}
|
||||
|
||||
export type Post = {
|
||||
id?: string,
|
||||
date?: string,
|
||||
dateGmt?: string,
|
||||
slug?: string,
|
||||
status?: 'publish' | 'future' | 'draft' | 'pending' | 'private',
|
||||
password?: string,
|
||||
title?: string,
|
||||
content?: string,
|
||||
author?: number,
|
||||
excerpt?: string,
|
||||
featuredMedia?: number,
|
||||
commentStatus?: 'open' | 'closed',
|
||||
pingStatus?: 'open' | 'closed',
|
||||
format?: 'standard' | 'aside' | 'chat' | 'gallery' | 'link' | 'image' | 'quote' | 'status' | 'video' | 'audio',
|
||||
meta?: string,
|
||||
sticky?: boolean,
|
||||
template?: string,
|
||||
tags?: number
|
||||
}
|
||||
|
||||
export type PageData = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type WindowType = Window & {
|
||||
$e?: {
|
||||
run: ( s: string, o: object )=> unknown
|
||||
}
|
||||
wpApiSettings?: { nonce: string }
|
||||
elementorNotifications?: {
|
||||
destroy: () => void
|
||||
},
|
||||
formWasSubmitted?: boolean
|
||||
formErrorDetected?: boolean
|
||||
};
|
||||
export type BackboneType = {
|
||||
Model: new ( o: {title: string} )=> unknown
|
||||
};
|
||||
|
||||
export type $eType = {
|
||||
run: ( s: string, o: object )=> unknown
|
||||
}
|
||||
|
||||
export type ElementorType = {
|
||||
navigator?: {
|
||||
isOpen: ()=> unknown
|
||||
},
|
||||
getContainer?: ( id: string )=> unknown,
|
||||
config?: {
|
||||
initialDocument:{
|
||||
id: string
|
||||
}
|
||||
},
|
||||
isDeviceModeActive?: () => unknown
|
||||
}
|
||||
|
||||
export type Device = 'mobile' | 'mobile_extra' | 'tablet' | 'tablet_extra' | 'laptop' | 'desktop' | 'widescreen';
|
||||
|
||||
export type BreakpointEditableDevice = Exclude<Device, 'desktop'>;
|
||||
|
||||
export type GapControl = string | {
|
||||
column: string,
|
||||
row: string,
|
||||
unit?: string
|
||||
}
|
||||
|
||||
export type ContainerType = 'flex' | 'grid';
|
||||
|
||||
export type ContainerPreset =
|
||||
| 'c100'
|
||||
| 'r100'
|
||||
| '50-50'
|
||||
| '33-66'
|
||||
| '25-25-25-25'
|
||||
| '25-50-25'
|
||||
| '50-50-50-50'
|
||||
| '50-50-100'
|
||||
| 'c100-c50-50'
|
||||
| '33-33-33-33-33-33'
|
||||
| '33-33-33-33-66'
|
||||
| '66-33-33-66'
|
27
tests/playwright/utils/test-page-url.ts
Normal file
27
tests/playwright/utils/test-page-url.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// File to store page URL
|
||||
const urlStorePath = path.join( __dirname, '..', 'temp-form-page-url.txt' );
|
||||
|
||||
/**
|
||||
* Gets the page URL from the file storage
|
||||
*
|
||||
* @return {string} The stored page URL or empty string if not found
|
||||
*/
|
||||
export const getPageUrl = (): string => {
|
||||
try {
|
||||
return fs.readFileSync( urlStorePath, 'utf8' );
|
||||
} catch ( e ) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a page URL to the file storage
|
||||
*
|
||||
* @param {string} url - The URL to save
|
||||
*/
|
||||
export const savePageUrl = ( url: string ): void => {
|
||||
fs.writeFileSync( urlStorePath, url );
|
||||
};
|
60
tests/playwright/wp-authentication.ts
Normal file
60
tests/playwright/wp-authentication.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { APIRequest, APIRequestContext, Page, chromium, APIResponse } from '@playwright/test';
|
||||
|
||||
export async function login( apiRequest: APIRequest, user: string, password: string, baseUrl: string ) {
|
||||
// Important: make sure we authenticate in a clean environment by unsetting storage state.
|
||||
const context = await apiRequest.newContext( { storageState: undefined } );
|
||||
|
||||
await context.post( `${ baseUrl }/wp-login.php`, {
|
||||
form: {
|
||||
log: user,
|
||||
pwd: password,
|
||||
'wp-submit': 'Log In',
|
||||
redirect_to: `${ baseUrl }/wp-admin/`,
|
||||
testcookie: '1',
|
||||
},
|
||||
} );
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function fetchNonce( context: APIRequestContext, baseUrl: string ) {
|
||||
const response = await context.get( `${ baseUrl }/wp-admin/post-new.php` );
|
||||
|
||||
await validateResponse( response, 'Failed to fetch page' );
|
||||
|
||||
let pageText = await response.text();
|
||||
if ( pageText.includes( 'WordPress has been updated!' ) ) {
|
||||
pageText = await updateDatabase( context, baseUrl );
|
||||
}
|
||||
|
||||
const nonceMatch = pageText.match( /var wpApiSettings = .*;/ );
|
||||
if ( ! nonceMatch ) {
|
||||
throw new Error( `Nonce not found on the page:\n"${ pageText }"` );
|
||||
}
|
||||
|
||||
return nonceMatch[ 0 ].replace( /^.*"nonce":"([^"]*)".*$/, '$1' );
|
||||
}
|
||||
|
||||
async function updateDatabase( context: APIRequestContext, baseUrl: string ): Promise<string> {
|
||||
const browser = await chromium.launch();
|
||||
const browserContext = await browser.newContext();
|
||||
const page: Page = await browserContext.newPage();
|
||||
await page.goto( `${ baseUrl }/wp-admin/post-new.php` );
|
||||
await page.getByText( 'Update WordPress Database' ).click();
|
||||
await page.getByText( 'Continue' ).click();
|
||||
|
||||
const retryResponse = await context.get( `${ baseUrl }/wp-admin/post-new.php` );
|
||||
|
||||
const pageText = await retryResponse.text();
|
||||
await browser.close();
|
||||
return pageText;
|
||||
}
|
||||
|
||||
async function validateResponse( response: APIResponse, errorMessage: string ) {
|
||||
if ( ! response.ok() ) {
|
||||
throw new Error( `
|
||||
${ errorMessage }: ${ response.status }.
|
||||
${ await response.text() }
|
||||
${ response.url() }
|
||||
` );
|
||||
}
|
||||
}
|
3
tests/wp-env/config/activate_plugin.sh
Normal file
3
tests/wp-env/config/activate_plugin.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
set -eox pipefail
|
||||
WP_CLI_CACHE_DIR=/tmp wp plugin install "$1" --activate
|
25
tests/wp-env/config/setup.sh
Normal file
25
tests/wp-env/config/setup.sh
Normal file
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
set -eox pipefail
|
||||
|
||||
wp plugin activate elementor
|
||||
wp theme activate hello-elementor
|
||||
|
||||
WP_CLI_CONFIG_PATH=hello-elementor-config/wp-cli.yml wp rewrite structure '/%postname%/' --hard
|
||||
|
||||
# Remove the Guttenberg welcome guide popup
|
||||
wp user meta add admin wp_persisted_preferences 'a:2:{s:14:\"core/edit-post\";a:2:{b:1;s:12:\"welcomeGuide\";b:0;}}'
|
||||
|
||||
# Reset editor counter to avoid auto trigger of the checklist popup when entering the editor for the 2nd time
|
||||
wp option update e_editor_counter 10
|
||||
wp option update elementor_checklist '{"last_opened_timestamp":null,"first_closed_checklist_in_editor":true,"is_popup_minimized":false,"steps":[],"should_open_in_editor":false,"editor_visit_count":10}'
|
||||
|
||||
wp option set elementor_onboarded true
|
||||
|
||||
# Add user meta so the announcement popup will not be displayed - ED-9723
|
||||
for id in $(wp user list --field=ID)
|
||||
do wp user meta add "$id" "announcements_user_counter" 999
|
||||
done
|
||||
|
||||
wp cache flush
|
||||
wp rewrite flush --hard
|
||||
wp elementor flush-css
|
2
tests/wp-env/config/wp-cli.yml
Normal file
2
tests/wp-env/config/wp-cli.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
apache_modules:
|
||||
- mod_rewrite
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Node 18 + ESM + Strictest",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2022"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"target": "ES6",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"checkJs": false,
|
||||
"allowJs": false,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue