1
0
Fork 0
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:
Giorgos Sarigiannidis 2025-08-14 11:01:55 +03:00 committed by GitHub
parent c8994cc6b0
commit 596c78d75b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 6424 additions and 273 deletions

View file

@ -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
View 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
View 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
View file

@ -17,3 +17,6 @@ assets/js/
*.zip
.npmrc
.env
tmp/
.phpunit.result.cache
!tests/phpunit/hello-elementor

18
.wp-env.json Normal file
View 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
}
}

View 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
View 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

View file

@ -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

File diff suppressed because it is too large Load diff

651
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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
View 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
View 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');

View 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'));
}
}

View 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 };
}
}

View 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();
}
}

View 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 }"]`;
};

View 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 );
};

View 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,
};

View 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 ];
},
} );
}
}
}

File diff suppressed because it is too large Load diff

View 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();
}
}

View 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' } ],
} );

View 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`,
},
} );

View 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;

View 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',
},
};

View file

@ -0,0 +1 @@
/?p=564

View 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' );
} );
} );

View 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'

View 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 );
};

View 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() }
` );
}
}

View file

@ -0,0 +1,3 @@
#!/bin/bash
set -eox pipefail
WP_CLI_CACHE_DIR=/tmp wp plugin install "$1" --activate

View 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

View file

@ -0,0 +1,2 @@
apache_modules:
- mod_rewrite

30
tsconfig.json Normal file
View 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
}
}