Move API system tests to cypress (#40135)

* tests

* api

* artiucles crud

* banners

* contact

* media

* media

* Remove codeception

* cs

* more traces of codeception

* readd certs

* dependencies

* more deps

* secret

* cs

* better doc

* write

* config workaround

* cs

* revert

* prepare db for api tests

* variable

* revert pool

* end connections

* no limit

* cs

* prefix

* Update tests/cypress/drone-system-run.sh

* run specific specs in drone

* Update tests/README.md

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update tests/cypress/integration/api/com_media/Files.cy.js

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update tests/cypress/integration/api/com_media/Files.cy.js

Co-authored-by: Brian Teeman <brian@teeman.net>

* postgres

* media on postgres

* sign

* cs

* remove more traces

* basic api tests

* cleanup

* path

* run recursive specs

* chmod images folder

* path to cms

* correct dependencies and better cleanup

* cleaner deps

* Update tests/cypress/drone-system-run.sh

Co-authored-by: Brian Teeman <brian@teeman.net>

* missing fields

* stabilize cleanup

* cs

* secret in install test

* order of tests

* optimize cleanup

* cs

* cache the connection

* revert connection cache

* docs

* Rename cypress to system

* lint

* simplification and docs

* docs

* better cleanup

* move

* merge conflict

* remove redundant connect

---------

Co-authored-by: Brian Teeman <brian@teeman.net>
This commit is contained in:
Allon Moritz 2023-03-20 09:51:42 +01:00 committed by GitHub
parent d3ba9d5408
commit 7611824c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 1316 additions and 9146 deletions

View File

@ -52,7 +52,7 @@ services:
before_test:
# Run openldap docker image
- ps: docker pull bitnami/openldap:2.6.3
- ps: docker run --rm --name openldap --publish 1389:1389 --publish 1636:1636 -v ${pwd}\tests\Codeception\_data\certs:/certificates --env LDAP_ADMIN_USERNAME=admin --env LDAP_ADMIN_PASSWORD=adminpassword --env LDAP_USERS=customuser --env LDAP_PASSWORDS=custompassword --env LDAP_ENABLE_TLS=yes --env LDAP_TLS_CERT_FILE=/certificates/openldap.crt --env LDAP_TLS_KEY_FILE=/certificates/openldap.key --env LDAP_TLS_CA_FILE=/certificates/CA.crt --env BITNAMI_DEBUG=true --env LDAP_CONFIG_ADMIN_ENABLED=yes --env LDAP_CONFIG_ADMIN_USERNAME=admin --env LDAP_CONFIG_ADMIN_PASSWORD=configpassword -d bitnami/openldap:2.6.3
- ps: docker run --rm --name openldap --publish 1389:1389 --publish 1636:1636 -v ${pwd}\tests\certs:/certificates --env LDAP_ADMIN_USERNAME=admin --env LDAP_ADMIN_PASSWORD=adminpassword --env LDAP_USERS=customuser --env LDAP_PASSWORDS=custompassword --env LDAP_ENABLE_TLS=yes --env LDAP_TLS_CERT_FILE=/certificates/openldap.crt --env LDAP_TLS_KEY_FILE=/certificates/openldap.key --env LDAP_TLS_CA_FILE=/certificates/CA.crt --env BITNAMI_DEBUG=true --env LDAP_CONFIG_ADMIN_ENABLED=yes --env LDAP_CONFIG_ADMIN_USERNAME=admin --env LDAP_CONFIG_ADMIN_PASSWORD=configpassword -d bitnami/openldap:2.6.3
# Database setup for MySQL via PowerShell tools
- >
"C:\Program Files\MySQL\MySQL Server 5.7\bin\mysql" -u root -p"Password12!" -e "CREATE DATABASE IF NOT EXISTS test_joomla;"

View File

@ -11,7 +11,7 @@ steps:
- name: certificates
path: /certificates
commands:
- cp -v tests/Codeception/_data/certs/* /certificates/
- cp -v tests/certs/* /certificates/
- name: composer
image: joomlaprojects/docker-images:php8.2
@ -179,6 +179,7 @@ steps:
image: node:current-alpine
commands:
- npm run lint:js
- npm run lint:testjs
- name: prepare_system_tests
depends_on:
@ -188,54 +189,13 @@ steps:
- name: cypress-cache
path: /root/.cache/Cypress
commands:
- sed -i 's/tests\\/Codeception\\/_output/\\/drone\\/src\\/tests\\/cypress\\/output/' codeception.yml
- mv cypress.config.dist.js cypress.config.js
- php libraries/vendor/bin/codecept build
- npx cypress install
- npx cypress verify
- name: phpmin-api-mysql
depends_on:
- prepare_system_tests
image: joomlaprojects/docker-images:systemtests
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/Codeception/drone-api-run.sh "$(pwd)" mysql mysqli mysql jos_
- name: phpmin-api-postgres
depends_on:
- prepare_system_tests
image: joomlaprojects/docker-images:systemtests
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/Codeception/drone-api-run.sh "$(pwd)" postgres pgsql postgres jos_
- name: phpmax-api-mysql
depends_on:
- phpmin-api-mysql
- phpmin-api-postgres
image: joomlaprojects/docker-images:systemtests8.1
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpmax mysqli mysql phpmax_
- name: phpmax-api-postgres
depends_on:
- phpmin-api-mysql
- phpmin-api-postgres
image: joomlaprojects/docker-images:systemtests8.1
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpmax pgsql postgres phpmax_
- name: phpmin-system-mysql
depends_on:
- phpmax-api-mysql
- phpmax-api-postgres
- prepare_system_tests
image: joomlaprojects/docker-images:cypress
volumes:
- name: cypress-cache
@ -243,12 +203,11 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cmysql mysqli mysql
- bash tests/System/drone-system-run.sh "$(pwd)" cmysql mysqli mysql
- name: phpmin-system-postgres
depends_on:
- phpmax-api-mysql
- phpmax-api-postgres
- prepare_system_tests
image: joomlaprojects/docker-images:cypress
volumes:
- name: cypress-cache
@ -256,7 +215,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cpostgres pgsql postgres
- bash tests/System/drone-system-run.sh "$(pwd)" cpostgres pgsql postgres
- name: phpmax-system-mysql
depends_on:
@ -269,7 +228,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cmysqlmax mysqli mysql
- bash tests/System/drone-system-run.sh "$(pwd)" cmysqlmax mysqli mysql
- name: phpmax-system-postgres
depends_on:
@ -282,7 +241,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cpostgresmax pgsql postgres
- bash tests/System/drone-system-run.sh "$(pwd)" cpostgresmax pgsql postgres
- name: phpmin-system-mysql8
depends_on:
@ -295,11 +254,12 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cmysql8 mysqli mysql8
- bash tests/System/drone-system-run.sh "$(pwd)" cmysql8 mysqli mysql8
- name: phpmax-system-mysql8
depends_on:
- phpmin-system-mysql8
- phpmax-system-mysql
- phpmax-system-postgres
image: joomlaprojects/docker-images:cypress8.1
volumes:
- name: cypress-cache
@ -307,7 +267,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- bash tests/cypress/drone-system-run.sh "$(pwd)" cmysql8max mysqli mysql8
- bash tests/System/drone-system-run.sh "$(pwd)" cmysql8max mysqli mysql8
- name: artifacts-system-tests
image: joomlaprojects/docker-images:packager
@ -318,10 +278,6 @@ steps:
- phpmin-system-mysql
- phpmin-system-mysql8
- phpmin-system-postgres
- phpmax-api-mysql
- phpmax-api-postgres
- phpmin-api-mysql
- phpmin-api-postgres
environment:
FTP_USERNAME:
from_secret: ftpusername
@ -334,7 +290,7 @@ steps:
- echo https://ci.joomla.org$PLUGIN_DEST_DIR
- rclone config create artifacts ftp host ci.joomla.org user $FTP_USERNAME port 21 pass $FTP_PASSWORD
- rclone mkdir artifacts:$PLUGIN_DEST_DIR
- rclone copy tests/cypress/output/ artifacts:$PLUGIN_DEST_DIR
- rclone copy tests/System/output/ artifacts:$PLUGIN_DEST_DIR
- 'curl -X POST "https://api.github.com/repos/$DRONE_REPO/statuses/$DRONE_COMMIT" -H "Content-Type: application/json" -H "Authorization: token $GITHUB_TOKEN" -d "{\"state\":\"failure\", \"context\": \"Artifacts from Failure\", \"description\": \"You can find artifacts from the failure of the build here:\", \"target_url\": \"https://ci.joomla.org$PLUGIN_DEST_DIR\"}" > /dev/null'
when:
status:
@ -497,6 +453,6 @@ trigger:
---
kind: signature
hmac: bc89776fce23d7ca29c126a409bdd18297409fecdffe3cd0b196273e4511b6c8
hmac: 3457679ef0cd23bde8dd9900016aba663a279aa0c32049c8e37db56676cadd38
...

3
.github/CODEOWNERS vendored
View File

@ -26,12 +26,9 @@ libraries/src/Updater/* @rdeutz @zero-24
# Automated Testing
tests/* @hackwar
tests/Codeception/* @rdeutz
tests/javascript/* @rdeutz
tests/Unit/* @rdeutz
.appveyor.yml @rdeutz @hackwar
.drone.yml @rdeutz @hackwar
codeception.yml @rdeutz @hackwar
phpunit.xml.dist @rdeutz @hackwar
phpunit-pgsql.xml.dist @rdeutz @hackwar

12
.gitignore vendored
View File

@ -26,13 +26,9 @@
# Test Related Files
/phpunit.xml
selenium.log
composer.phar
/test-install
/.phpunit.result.cache
/tests/Codeception/acceptance.suite.yml
/tests/Codeception/_output/
/tests/Codeception/api.suite.yml
# Vendor directory handling
@ -103,10 +99,10 @@ RoboFile.ini
/media/com_media/css/mediamanager.min.css.map
#cypress
/tests/cypress/output/screenshots
!/tests/cypress/output/screenshots/.gitkeep
/tests/cypress/output/videos
!/tests/cypress/output/videos/.gitkeep
/tests/System/output/screenshots
!/tests/System/output/screenshots/.gitkeep
/tests/System/output/videos
!/tests/System/output/videos/.gitkeep
cypress.config.js
# WebAuthn FIDO metadata cache

View File

@ -189,6 +189,10 @@ class JsonapiView extends BaseApiView
*/
protected function prepareItem($item)
{
if (!$item) {
return $item;
}
$item->text = $item->introtext . ' ' . $item->fulltext;
// Process the content plugins.

View File

@ -53,9 +53,22 @@
},
"overrides": [
{
"files": ["tests/**/*.cy.js"],
"files": ["tests/**/*.js"],
"rules": {
"no-undef": ["off"]
"no-undef": ["off"],
"import/no-extraneous-dependencies": ["off"]
}
},
{
"files": ["tests/System/support/index.js"],
"rules": {
"no-console": ["off"]
}
},
{
"files": ["tests/System/support/commands/db.js","tests/System/plugins/index.js"],
"rules": {
"no-useless-escape": ["off"]
}
}
]

View File

@ -395,7 +395,6 @@ $doNotPackage = [
'appveyor-phpunit.xml',
'build',
'build.xml',
'codeception.yml',
'CODE_OF_CONDUCT.md',
'composer.json',
'composer.lock',
@ -412,7 +411,6 @@ $doNotPackage = [
'README.md',
'renovate.json',
'ruleset.xml',
'selenium.log',
'tests',
];

View File

@ -1,20 +0,0 @@
actor: Tester
paths:
tests: tests/Codeception
log: tests/cypress/output
data: tests/Codeception/_data
support: tests/Codeception/_support
envs: tests/Codeception/_envs
settings:
colors: true
memory_limit: 1024M
extensions:
enabled:
- Codeception\Extension\RunFailed
modules:
config:
Db:
dsn: ''
user: ''
password: ''
dump: tests/Codeception/_data/dump.sql

View File

@ -60,7 +60,7 @@
"joomla/ldap": "~2.0.0",
"joomla/oauth1": "^2.0.1",
"joomla/oauth2": "^2.0.1",
"joomla/registry": "^2.0.2",
"joomla/registry": "^2.0.4",
"joomla/router": "^2.0.1",
"joomla/session": "^2.0.2",
"joomla/string": "^2.0.1",
@ -73,16 +73,16 @@
"google/recaptcha": "^1.2.4",
"laminas/laminas-diactoros": "^2.4.1",
"paragonie/sodium_compat": "^1.19",
"phpmailer/phpmailer": "^6.7.1",
"phpmailer/phpmailer": "^6.8.0",
"psr/link": "~1.0.0",
"symfony/console": "^5.4.19",
"symfony/error-handler": "^5.4.19",
"symfony/ldap": "^5.4.19",
"symfony/options-resolver": "^5.4.19",
"symfony/console": "^5.4.21",
"symfony/error-handler": "^5.4.21",
"symfony/ldap": "^5.4.21",
"symfony/options-resolver": "^5.4.21",
"symfony/polyfill-mbstring": "^1.27.0",
"symfony/polyfill-php73": "^1.27",
"symfony/web-link": "^5.4.19",
"symfony/yaml": "^5.4.19",
"symfony/web-link": "^5.4.21",
"symfony/yaml": "^5.4.21",
"typo3/phar-stream-wrapper": "^3.1.7",
"wamania/php-stemmer": "^2.2",
"maximebf/debugbar": "dev-master",
@ -99,25 +99,17 @@
"lcobucci/jwt": "^3.4.6",
"web-token/signature-pack": "^2.2.11",
"phpseclib/bcmath_compat": "^2.0.1",
"jfcherng/php-diff": "^6.10.10",
"jfcherng/php-diff": "^6.10.11",
"voku/portable-utf8": "6.0.12 as 5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.32",
"phpunit/phpunit": "^8.5.33",
"friendsofphp/php-cs-fixer": "^3.4.0",
"squizlabs/php_codesniffer": "^3.7.1",
"joomla-projects/joomla-browser": "~4.0.0",
"codeception/codeception": "^4.2.2",
"squizlabs/php_codesniffer": "^3.7.2",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
"joomla/mediawiki": "^1.0.0",
"joomla/test": "^2.0.2",
"codeception/module-asserts": "^1.3.1",
"codeception/module-db": "^1.2",
"codeception/module-rest": "^1.4.2",
"codeception/module-webdriver": "^1.4.1",
"codeception/module-phpbrowser": "^1.0.3",
"hoa/console": "^3.17.05.02",
"phan/phan": "^5.4.1"
"phan/phan": "^5.4.2"
},
"replace": {
"paragonie/random_compat": "9.99.99"

2132
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
const { defineConfig } = require('cypress');
const setupPlugins =require('./tests/cypress/plugins/index');
const setupPlugins = require('./tests/System/plugins/index');
module.exports = defineConfig({
fixturesFolder: 'tests/cypress/fixtures',
videosFolder: 'tests/cypress/output/videos',
screenshotsFolder: 'tests/cypress/output/screenshots',
fixturesFolder: 'tests/System/fixtures',
videosFolder: 'tests/System/output/videos',
screenshotsFolder: 'tests/System/output/screenshots',
viewportHeight: 1000,
viewportWidth: 1200,
e2e: {
@ -13,15 +13,16 @@ module.exports = defineConfig({
},
baseUrl: 'http://localhost/',
specPattern: [
'tests/cypress/integration/install/*.cy.{js,jsx,ts,tsx}',
'tests/cypress/integration/administrator/**/*.cy.{js,jsx,ts,tsx}',
'tests/cypress/integration/site/**/*.cy.{js,jsx,ts,tsx}'
'tests/System/integration/install/**/*.cy.{js,jsx,ts,tsx}',
'tests/System/integration/administrator/**/*.cy.{js,jsx,ts,tsx}',
'tests/System/integration/site/**/*.cy.{js,jsx,ts,tsx}',
'tests/System/integration/api/**/*.cy.{js,jsx,ts,tsx}',
],
supportFile: 'tests/cypress/support/index.js',
supportFile: 'tests/System/support/index.js',
scrollBehavior: 'center',
browser: 'firefox',
screenshotOnRunFailure: true,
video: false
video: false,
},
env: {
sitename: 'Joomla CMS Test',
@ -34,6 +35,7 @@ module.exports = defineConfig({
db_name: 'test_joomla',
db_user: 'root',
db_password: '',
db_prefix: 'jos_'
db_prefix: 'jos_',
cmsPath: '.',
},
})
});

View File

@ -146,7 +146,7 @@ class MediaHelper
);
// Get the mime type configuration
$allowedMime = array_map('trim', explode(',', $allowedMime));
$allowedMime = array_map('trim', explode(',', str_replace('\\', '', $allowedMime)));
// Mime should be available and in the allowed list
return !empty($mime) && \in_array($mime, $allowedMime);

1564
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"watch": "node build/build.js --watch",
"watch:com_media": "node build/build.js --watch-com-media",
"lint:js": "eslint --config build/.eslintrc --ignore-pattern '/media/' --ext .es6.js,.es6,.vue,.cy.js .",
"lint:testjs": "eslint --config build/.eslintrc --ext .js tests/System",
"lint:css": "stylelint --config build/.stylelintrc.json \"administrator/components/com_media/resources/**/*.scss\" \"administrator/templates/**/*.scss\" \"build/media_source/**/*.scss\" \"templates/**/*.scss\" \"installation/template/**/*.scss\"",
"install": "node build/build.js --prepare",
"update": "node build/build.js --copy-assets && node build/build.js --build-pages && node build/build.js --compile-js && node build/build.js --compile-css && node build/build.js --compile-bs && set NODE_ENV=PRODUCTION && node build/build.js --com-media",
@ -54,7 +55,7 @@
"focus-visible": "^5.2.0",
"hotkeys-js": "^3.10.1",
"joomla-ui-custom-elements": "^0.2.0",
"jquery": "^3.6.3",
"jquery": "^3.6.4",
"jquery-migrate": "^3.4.1",
"mark.js": "^8.11.1",
"mediaelement": "^5.1.1",
@ -72,7 +73,7 @@
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@dgrammatiko/compress": "^1.0.4",
"@rollup/plugin-babel": "^5.3.1",
@ -80,13 +81,13 @@
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^3.1.0",
"@vue/compiler-sfc": "^3.2.47",
"autoprefixer": "^10.4.13",
"autoprefixer": "^10.4.14",
"chokidar": "^3.5.3",
"commander": "^8.3.0",
"core-js": "^3.29.0",
"core-js": "^3.29.1",
"cssnano": "^5.1.15",
"cypress": "^12.7.0",
"eslint": "^8.35.0",
"cypress": "^12.8.1",
"eslint": "^8.36.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-vue": "^8.7.1",
@ -97,18 +98,17 @@
"mysql": "^2.18.1",
"postcss": "^8.4.21",
"postcss-scss": "^4.0.6",
"postgres": "^3.3.3",
"postgres": "^3.3.4",
"recursive-readdir": "^2.2.3",
"rimraf": "^3.0.2",
"rollup": "^2.79.1",
"rollup-plugin-vue": "^6.0.0",
"rtlcss": "^3.5.0",
"sass-embedded": "^1.58.3",
"selenium-standalone": "^8.3.0",
"sass-embedded": "^1.59.3",
"stylelint": "^14.16.1",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"stylelint-scss": "^4.4.0",
"terser": "^5.16.5"
"stylelint-scss": "^4.5.0",
"terser": "^5.16.6"
}
}

View File

@ -20,7 +20,7 @@
<const name="JTEST_LDAP_PORT" value="1389" />
<const name="JTEST_LDAP_PORT_SSL" value="1636" />
<!--CACERTFILE path is relative to JPATH_ROOT-->
<const name="JTEST_LDAP_CACERTFILE" value="./tests/Codeception/_data/certs/CA.crt" />
<const name="JTEST_LDAP_CACERTFILE" value="./tests/certs/CA.crt" />
<const name="JTEST_LDAP_USEV3" value="1" />
<const name="JTEST_LDAP_NOREFERRALS" value="1" />
<const name="JTEST_LDAP_BASE" value="dc=example,dc=org" />

View File

@ -20,7 +20,7 @@
<const name="JTEST_LDAP_PORT" value="1389" />
<const name="JTEST_LDAP_PORT_SSL" value="1636" />
<!--CACERTFILE path is relative to JPATH_ROOT-->
<const name="JTEST_LDAP_CACERTFILE" value="./tests/Codeception/_data/certs/CA.crt" />
<const name="JTEST_LDAP_CACERTFILE" value="./tests/certs/CA.crt" />
<const name="JTEST_LDAP_USEV3" value="1" />
<const name="JTEST_LDAP_NOREFERRALS" value="1" />
<const name="JTEST_LDAP_BASE" value="dc=example,dc=org" />

View File

@ -1,55 +0,0 @@
# System Testing for Joomla
### Abstract
Acceptance or UI tests for Joomla are present in the `acceptance` folder, there are two major categories of tests
at the moment
1) Installation Tests, inside the `acceptance/01-install` folder
2) Administrator Tests, inside the `acceptance/administrator` folder
### Installation
Here are the steps that are needed to setup UI tests execution on `localhost`
#### Linux OS
1) Checkout the Project in your document root folder, and follow the [setup guide](https://docs.joomla.org/Special:MyLanguage/J4.x:Setting_Up_Your_Local_Environment).
2) Navigate to `tests/Codeception` folder and edit configuration file `acceptance.suite.yml` file.
1) Within the JoomlaBrowser config section change `url` point it to your localhost url
2) update `database host` `database user` & `database password` as per your localhost installed DB,
these values will be used by installation tests
3) change `database name` make sure you have a database with this name created on your localhost DB,
this will help you avoid errors with `JoomlaDb` helper as well.
4) Within the `Helper/JoomlaDb` section, update the values for
`host` `dbname` `user` `password` as per the previous section in the config
5) Within the `Helper/Acceptance` section, update the values for
`url` `cmsPath`, point them as per your localhost setup.
3) Run `./node_modules/.bin/selenium-standalone install` in project to install selenium-standalone server in localhost
4) Run `./node_modules/.bin/selenium-standalone start` and wait for the message `Selenium Started`
5) Run the following in project root: `libraries/vendor/bin/codecept run acceptance tests/Codeception/acceptance/install`
this will start Chrome in headless mode, to view the test execution in Chrome UI, remove `headless` from capabilities in configuration file
# API testing for Joomla
### Abstract
These are the Joomla 4 API (webservices) tests. To run these tests on OSX you will need to install the GNU Sed package with `brew install gnu-sed`
### Installation
1) Checkout the Project in your document root folder, and follow the [setup guide}(https://docs.joomla.org/Special:MyLanguage/J4.x:Setting_Up_Your_Local_Environment).
2) Copy the file `tests/Codeception/api.suite.dist.yml` to `tests/Codeception/api.suite.yml`. Then edit the REST url in the new file to point it to your localhost url.
3) Edit the file configuration.php. Set `$secret` = `'tEstValue'` - see [drone-api-run.sh](https://github.com/joomla/joomla-cms/blob/d8930208814fb52c0871853cfd9298f70998fd1f/tests/Codeception/drone-api-run.sh#L59).
> Tests with authentication always use the super user credentials for now.
### Running
`libraries/vendor/bin/codecept run api`
You can also run the command with
- `--debug` to get some extended information.
- `--steps` to print step-by-step execution.
- `--fail-fast` to stop after first failure.
See [Codeception Console Commands](https://codeception.com/docs/reference/Commands)

View File

@ -1 +0,0 @@
/* Replace this file with actual dump of your database */

View File

@ -1,86 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:ignoreFile
use Codeception\Actor;
use Codeception\Lib\Friend;
/**
* Acceptance Tester global class for entry point.
*
* Inherited Methods.
*
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method Friend haveFriend($name, $actorClass = null)
* @method getConfig(string $string)
*
* @SuppressWarnings(PHPMD)
*
* @since 3.7.3
*/
class AcceptanceTester extends Actor
{
use _generated\AcceptanceTesterActions;
/**
* Function to check for PHP Notices or Warnings.
*
* @param string $page Optional, if not given checks will be done in the current page
*
* @note doAdminLogin() before
*
* @since 3.7.3
*
* @return void
*/
public function checkForPhpNoticesOrWarnings($page = null)
{
$I = $this;
if ($page) {
$I->amOnPage($page);
}
$I->dontSeeInPageSource('Notice:');
$I->dontSeeInPageSource('<b>Notice</b>:');
$I->dontSeeInPageSource('Warning:');
$I->dontSeeInPageSource('<b>Warning</b>:');
$I->dontSeeInPageSource('Strict standards:');
$I->dontSeeInPageSource('<b>Strict standards</b>:');
$I->dontSeeInPageSource('The requested page can\'t be found');
}
/**
* Function to wait for JS to be properly loaded on page change.
*
* @param integer|float $timeout Time to wait for JS to be ready
*
* @since 4.0.0
*
* @return void
*/
public function waitForJsOnPageLoad($timeout = 1)
{
$I = $this;
$I->waitForJS('return document.readyState == "complete"', $timeout);
// Wait an additional 500ms to make sure that really all JS is loaded
$I->wait(0.5);
}
}

View File

@ -1,40 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:ignoreFile
use Codeception\Actor;
use Codeception\Lib\Friend;
/**
* Inherited Methods.
*
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*
* @since 4.0.0
*/
class ApiTester extends Actor
{
use _generated\ApiTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -1,42 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage FunctionalTester
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:ignoreFile
use Codeception\Actor;
use Codeception\Lib\Friend;
/**
* Functional Tester global class for entry point.
*
* Inherited Methods.
*
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*
* @since 3.7.3
*/
class FunctionalTester extends Actor
{
use _generated\FunctionalTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -1,55 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Helper
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Helper;
use Codeception\Configuration;
use Codeception\Exception\ConfigurationException;
use Codeception\Module;
use Exception;
/**
* Helper class for Acceptance.
* Here you can define custom actions.
* All public methods declared in helper class will be available in $I.
*
* @package Codeception\Module
*
* @since 3.7.3
*/
class Acceptance extends Module
{
/**
* Array of the configuration settings.
*
* @var array
* @since 3.7.3
*/
protected static $acceptanceSuiteConfiguration = [];
/**
* Function to get Configuration from the acceptance.suite.yml to be used by a test
*
* @return array
*
* @since 3.7.3
*
* @throws ConfigurationException
* @throws Exception
*/
public function getSuiteConfiguration()
{
if (empty(self::$acceptanceSuiteConfiguration)) {
self::$acceptanceSuiteConfiguration = Configuration::suiteSettings('acceptance', Configuration::config());
}
return self::$acceptanceSuiteConfiguration;
}
}

View File

@ -1,79 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Helper
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Helper;
use Codeception\Module;
/**
* Helper class for Acceptance.
* Here you can define custom actions.
* All public methods declared in helper class will be available in $I.
*
* @package Codeception\Module
*
* @since 3.7.3
*/
class Api extends Module
{
/**
* Creates a user for API authentication and returns a bearer token.
*
* @return string The token
*
* @since 4.1.0
*/
public function getBearerToken(): string
{
/** @var JoomlaDb $db */
$db = $this->getModule('Helper\\JoomlaDb');
$desiredUserId = 3;
if (!$db->grabFromDatabase('users', 'id', ['id' => $desiredUserId])) {
$db->haveInDatabase(
'users',
[
'id' => $desiredUserId,
'name' => 'API',
'email' => 'api@example.com',
'username' => 'api',
'password' => '123',
'block' => 0,
'registerDate' => '2000-01-01',
'params' => '{}',
],
[]
);
$db->haveInDatabase('user_usergroup_map', ['user_id' => $desiredUserId, 'group_id' => 8]);
$enabledData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.enabled', 'profile_value' => 1];
$tokenData = ['user_id' => $desiredUserId, 'profile_key' => 'joomlatoken.token', 'profile_value' => 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4='];
$db->haveInDatabase('user_profiles', $enabledData);
$db->haveInDatabase('user_profiles', $tokenData);
}
return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ==';
}
/**
* Creates a user for API authentication and returns a bearer token.
*
* @param string $name The name of the config key
* @param string $module The module
*
* @return string The config key
*
* @since 4.1.0
*/
public function getConfig($name, $module = 'Helper\Api'): string
{
return $this->getModule($module)->_getConfig()[$name];
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Helper
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Helper;
use Codeception\Module;
/**
* Helper class for Functional.
* Here you can define custom actions.
* All public methods declared in helper class will be available in $I.
*
* @package Codeception\Module
*
* @since 3.7.3
*/
class Functional extends Module
{
}

View File

@ -1,214 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Helper
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Helper;
use Codeception\Module\Db;
/**
* JoomlaDb Helper class for Acceptance.
*
* @package Codeception\Module
*
* @since 3.7.3
*/
class JoomlaDb extends Db
{
/**
* The table prefix.
*
* @var string
* @since 3.7.3
*/
protected $prefix;
/**
* Codeception Hook: called after configuration is loaded.
*
* @return void
*
* @since 3.7.3
*/
// phpcs:ignore
public function _initialize()
{
$this->prefix = (isset($this->config['prefix'])) ? $this->config['prefix'] : '';
parent::_initialize();
}
/**
* Inserts an SQL record into a database. This record will be erased after each test.
*
* @param string $table Table
* @param array $data Data
*
* @return integer The last insert id
*
* @since 3.7.3
*/
public function haveInDatabase($table, array $data)
{
$table = $this->addPrefix($table);
return parent::haveInDatabase($table, $data);
}
/**
* See an entry in the database.
*
* @param string $table Table
* @param array $criteria Criteria
*
* @return void
*
* @since 3.7.3
*/
public function seeInDatabase($table, $criteria = [])
{
$table = $this->addPrefix($table);
parent::seeInDatabase($table, $criteria);
}
/**
* Don't see in database.
*
* @param string $table Table
* @param array $criteria Criteria
*
* @return void
*
* @since 3.7.3
*/
public function dontSeeInDatabase($table, $criteria = [])
{
$table = $this->addPrefix($table);
parent::dontSeeInDatabase($table, $criteria);
}
/**
* Grab an entry from the database.
*
* @param string $table Table
* @param string $column Column
* @param array $criteria Criteria
*
* @return mixed
*
* @since 3.7.3
*/
public function grabFromDatabase($table, $column, $criteria = [])
{
$table = $this->addPrefix($table);
return parent::grabFromDatabase($table, $column, $criteria);
}
/**
* Asserts that the given number of records were found in the database.
*
* @param integer $expectedNumber Expected number
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*
* @return void
*
* @since 3.7.3
*/
public function seeNumRecords($expectedNumber, $table, array $criteria = [])
{
$table = $this->addPrefix($table);
parent::seeNumRecords($expectedNumber, $table, $criteria);
}
/**
* Returns the number of rows in a database.
*
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*
* @return integer
*
* @since 3.7.3
*/
public function grabNumRecords($table, array $criteria = [])
{
$table = $this->addPrefix($table);
return parent::grabNumRecords($table, $criteria);
}
/**
* Update an SQL record into a database.
*
* @param string $table Table name
* @param array $data Data to update in the table. Key=> value is column name => data
* @param array $criteria Search criteria [Optional]
*
* @return void
*
* @since 4.0.0
*/
public function updateInDatabase($table, array $data, array $criteria = [])
{
$table = $this->addPrefix($table);
parent::updateInDatabase($table, $data, $criteria);
}
/**
* Deletes records in a database.
*
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*
* @return void
*
* @since 4.1.0
*/
public function deleteFromDatabase($table, $criteria = []): void
{
$table = $this->addPrefix($table);
$this->driver->deleteQueryByCriteria($table, $criteria);
}
/**
* Add the table prefix.
*
* @param string $table Table without prefix
*
* @return string
*
* @since 3.7.3
*/
protected function addPrefix($table)
{
return $this->prefix . $table;
}
/**
* getConfig
*
* @param string $value Get the setting from the option
*
* @return mixed
*
* @since version
* @throws \Codeception\Exception\ModuleException
*/
public function getConfig($value)
{
return $this->getModule('Joomla\Browser\JoomlaBrowser')->_getConfig($value);
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Helper
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Helper;
use Codeception\Module;
/**
* Helper class for Unit.
* Here you can define custom actions.
* All public methods declared in helper class will be available in $I.
*
* @package Codeception\Module
*
* @since 3.7.3
*/
class Unit extends Module
{
}

View File

@ -1,23 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class to define administrator form page objects.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class AdminFormPage extends AdminPage
{
// @todo.
}

View File

@ -1,23 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class to define administrator list page objects.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class AdminListPage extends AdminPage
{
// @todo.
}

View File

@ -1,343 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
use AcceptanceTester;
use Exception;
/**
* Acceptance Page object class to define administrator page objects.
*
* @package Page\Acceptance\Administrator
*
* @since 3.7.3
*/
class AdminPage extends AcceptanceTester
{
/**
* The element id which contains system messages.
*
* @var array
* @since 3.7.3
*/
public static $systemMessageContainer = ['id' => 'system-message-container'];
/**
* The form element with id "adminForm"
*
* @var array
* @since 4.0.0
*/
public static $adminForm = ['id' => 'adminForm'];
/**
* The element id which contains page title in administrator header.
*
* @var array
* @since 3.7.3
*/
public static $pageTitle = ['class' => 'page-title'];
/**
* Locator for page title.
*
* @var array
* @since 3.7.3
*/
public static $title = ['id' => 'jform_title'];
/**
* Locator for search input field.
*
* @var array
* @since 3.7.3
*/
public static $filterSearch = ['id' => 'filter_search'];
/**
* Locator for status filter under search tool.
*
* @var array
* @since 3.7.3
*/
public static $filterPublished = 'filter_published';
/**
* Locator for search button icon.
*
* @var array
* @since 3.7.3
*/
public static $iconSearch = ['xpath' => "//button[@title='Search']"];
/**
* Locator for the Tabs in Edit View.
*
* @var array
* @since 3.7.3
*/
public static $tabsLocator = ['xpath' => "//div[@role='tablist']/button"];
/**
* Locator for the Check All checkbox.
*
* @var array
* @since 3.7.3
*/
public static $checkAll = ['xpath' => "//thead//input[@name='checkall-toggle' or @name='toggle']"];
/**
* Method to search using given keyword.
*
* @param string $keyword The keyword to search
*
* @return void
*
* @since 3.7.3
*/
public function search($keyword)
{
$I = $this;
$I->amGoingTo('search for "' . $keyword . '"');
$I->fillField(static::$filterSearch, $keyword);
$I->click(static::$iconSearch);
}
/**
* Method to search user with username.
*
* @param string $keyword The username of user
*
* @return void Checkbox for given username will be checked.
*
* @since 3.7.3
*/
public function haveItemUsingSearch($keyword)
{
$I = $this;
$I->amOnPage(static::$url);
$I->search($keyword);
$I->checkAllResults();
$I->wait(1);
}
/**
* Method is used to see system message after waiting for page title.
*
* @param string $title The webpage title
* @param string $message The unpublish successful message
*
* @return void
*
* @since 3.7.3
*/
public function seeSystemMessage($title, $message)
{
$I = $this;
$I->waitForPageTitle($title);
$I->waitForElementVisible(self::$systemMessageContainer, TIMEOUT);
$I->see($message, self::$systemMessageContainer);
}
/**
* Method is to Wait for page title until default timeout.
*
* @param string $title Page Title text
*
* @return void
*
* @since 3.7.3
*
* @throws Exception
*/
public function waitForPageTitle($title)
{
$I = $this;
$I->waitForText($title, $I->getConfig('timeout'), self::$pageTitle);
}
/**
* Function to select all the item in the Search results in Administrator List.
*
* Note: We recommend use of checkAllResults function only after searchForItem to be sure you are selecting only
* the desired result set.
*
* @return void
*
* @since 3.7.3
*/
public function checkAllResults()
{
$I = $this;
$I->comment("Selecting Checkall button");
$I->click(self::$checkAll);
}
/**
* Selects an option in a Chosen Selector based on its id.
*
* @param string $selectId The id of the <select> element
* @param string $option The text in the <option> to be selected in the chosen selector
*
* @return void
*
* @since 3.7.3
*/
public function selectOptionInChosenById($selectId, $option)
{
$chosenSelectID = $selectId . '_chzn';
$I = $this;
$I->comment("I open the $chosenSelectID chosen selector");
$I->click(['xpath' => "//div[@id='$chosenSelectID']/a/div/b"]);
$I->comment("I select $option");
$I->click(['xpath' => "//div[@id='$chosenSelectID']//li[text()='$option']"]);
// Gives time to chosen to close
$I->wait(1);
}
/**
* Function to Logout from Administrator Panel in Joomla!.
*
* @return void
*
* @since 3.7.3
*
* @throws Exception
*/
public function doAdministratorLogout()
{
$I = $this;
$I->click(
['xpath' => "//ul[@class='nav nav-user pull-right']//li//a[@class='dropdown-toggle']"]
);
$I->comment("I click on Top Right corner toggle to Logout from Admin");
$I->waitForElement(
['xpath' => "//li[@class='dropdown open']/ul[@class='dropdown-menu']//a[text() = 'Logout']"],
TIMEOUT
);
$I->click(
['xpath' => "//li[@class='dropdown open']/ul[@class='dropdown-menu']//a[text() = 'Logout']"]
);
$I->waitForElement(['id' => 'mod-login-username'], $I->getConfig('timeout'));
$I->waitForText(
'Log in',
TIMEOUT,
['xpath' => "//fieldset[@class='loginform']//button"]
);
}
/**
* Function to Verify the Tabs on a Joomla! screen.
*
* @param array $expectedTabs Expected Tabs on the Page
* @param array $tabsLocator Locator for the Tabs in Edit View
*
* @return void
*
* @since 3.7.3
*/
public function verifyAvailableTabs($expectedTabs, $tabsLocator = null)
{
$I = $this;
$actualArrayOfTabs = $I->grabMultiple(self::$tabsLocator);
$I->comment(
"Fetch the current list of Tabs in the edit view which is: " . implode(", ", $actualArrayOfTabs)
);
$I->assertEquals(
$expectedTabs,
$actualArrayOfTabs,
"Tab Labels do not match on edit view of" . $I->grabFromCurrentUrl()
);
$I->comment('Verify the Tabs');
}
/**
* Method to see that item is saved.
*
* @param string $item The item Name
*
* @return void
*
* @since 3.7.3
*/
public function seeItemIsCreated($item)
{
$I = $this;
$I->amOnPage(static::$url);
$I->search($item);
$I->see($item, static::$seeName);
}
/**
* Assure the item is trashed.
*
* @param string $item The item name
* @param string $pageTitle The page title
*
* @return void
*
* @since 3.7.3
*/
public function seeItemInTrash($item, $pageTitle)
{
$I = $this;
$I->click('Search Tools');
$I->wait(2);
$I->selectOptionInChosenById(static::$filterPublished, 'Trashed');
$I->waitForPageTitle($pageTitle);
$I->see($item, static::$seeName);
}
/**
* Assure the search tools are displayed.
*
* @return void
*
* @since 3.7.3
*/
public function displaySearchTools()
{
$I = $this;
try {
$I->seeElement(['class' => 'js-stools-btn-filter']);
} catch (Exception $e) {
$I->comment("Search tools button does not exist on this page, skipping");
return;
}
try {
$I->dontSeeElement(['class' => 'js-stools-container-filters']);
} catch (Exception $e) {
$I->comment("Search tools already visible on the page, skipping");
return;
}
$I->click('Search Tools');
}
}

View File

@ -1,75 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for banner list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class BannerListPage extends AdminListPage
{
/**
* Locator for Url location.
*
* @var string
*
* @since 4.0.0
*/
public static $url = "/administrator/index.php?option=com_banners";
/**
* Locator for Title field.
*
* @var array
*
* @since 4.0.0
*/
public static $titleField = ['id' => 'jform_name'];
/**
* Locator for Alias field.
*
* @var array
*
* @since 4.0.0
*/
public static $aliasField = ['id' => 'jform_alias'];
/**
* Locator for Search field.
*
* @var array
*
* @since 4.0.0
*/
public static $searchField = ['id' => 'filter_search'];
/**
* Locator for Search button class.
*
* @var array
*
* @since 4.0.0
*/
public static $searchButton = ['class' => 'icon-search'];
/**
* Locator for Search button tile.
*
* @var array
*
* @since 4.0.0
*/
public static $searchToolButton = ['css' => 'button[data-original-title="Filter the list items."]'];
}

View File

@ -1,77 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object for content category list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class ContentCategoryListPage extends AdminListPage
{
/**
* Link to the article category listing url.
*
* @var string
* @since 4.0.0
*/
public static $url = '/administrator/index.php?option=com_categories&view=categories&extension=com_content';
/**
* Locator for dropdown.
*
* @var array
* @since 4.0.0
*/
public static $dropDownToggle = ['xpath' => "//div[@id='toolbar-dropdown-save-group']/button[contains(@class, 'dropdown-toggle')]"];
/**
* Locator for category name field.
*
* @var array
* @since 4.0.0
*/
public static $seeName = ['xpath' => "//table[@id='categoryList']//tr[1]//td[4]"];
/**
* Locator for category unpublished icon.
*
* @var array
* @since 4.0.0
*/
public static $seeUnpublished = ['xpath' => "//table[@id='categoryList']//*//span[@class='icon-unpublish']"];
/**
* Locator for category access level field.
*
* @var array
* @since 4.0.0
*/
public static $seeAccessLevel = ['xpath' => "//table[@id='categoryList']//tr[1]//td[9]"];
/**
* Locator for category language field.
*
* @var array
* @since 4.0.0
*/
public static $seeLanguage = ['xpath' => "//table[@id='categoryList']//tr[1]//td[10]"];
/**
* Locator for invalid category alert.
*
* @var array
* @since 4.0.0
*/
public static $invalidTitle = ['class' => 'alert-error'];
}

View File

@ -1,29 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for content form page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class ContentFormPage extends AdminFormPage
{
/**
* Link to the article form page.
*
* @var string
* @since 4.0.0
*/
public static $url = 'administrator/index.php?option=com_content&view=article&layout=edit';
}

View File

@ -1,95 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for article list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class ContentListPage extends AdminListPage
{
/**
* Link to the article listing page.
*
* @var string
* @since 4.0.0
*/
public static $url = "/administrator/index.php?option=com_content&view=articles";
/**
* Drop Down Toggle Element.
*
* @var array
* @since 4.0.0
*/
public static $dropDownToggle = ['xpath' => "//button[contains(@class, 'dropdown-toggle')]"];
/**
* Page object for content body editor field.
*
* @var array
* @since 4.0.0
*/
public static $content = ['id' => 'jform_articletext'];
/**
* Page object for the toggle button.
*
* @var string
* @since 4.0.0
*/
public static $toggleEditor = "Toggle editor";
/**
* Locator for article's name field.
*
* @var array
* @since 4.0.0
*/
public static $seeName = ['xpath' => "//table[@id='articleList']//tr[1]//td[4]"];
/**
* Locator for article's featured icon.
*
* @var array
* @since 4.0.0
*/
public static $seeFeatured = ['xpath' => "//table[@id='articleList']//*//span[@class='icon-star']"];
/**
* Locator for article's name field.
*
* @var array
* @since 4.0.0
*/
public static $seeAccessLevel = ['xpath' => "//table[@id='articleList']//tr[1]//td[5]"];
/**
* Locator for article's unpublish icon.
*
* @var array
* @since 4.0.0
*/
public static $seeUnpublished = ['xpath' => "//table[@id='articleList']//*//span[@class='icon-unpublish']"];
public static $articleTitleField = [ 'id' => "jform_title" ];
public static $articleAliasField = [ 'id' => "jform_alias" ];
public static $articleSearchField = [ 'id' => "filter_search" ];
public static $searchButton = [ 'xpath' => "//button[@aria-label='Search']" ];
public static $systemMessageAlertClose = ['class' => "joomla-alert--close"];
}

View File

@ -1,61 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for Field list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class FieldListPage extends AdminListPage
{
/**
* Link to the article listing page.
*
* @var string
* @since 4.0.0
*/
public static $url = "/administrator/index.php?option=com_fields&view=fields&context=com_content.article";
/**
* Locator for Fields publish icon.
*
* @var array
* @since 4.0.0
*/
public static $seePublished = ['xpath' => "//table[@id='fieldList']//*//span[@class='icon-check']"];
public static $titleField = "#jform_title";
public static $fieldType = '#jform_type';
public static $successMessage = 'Field saved';
/**
* Locator for Search field.
*
* @var array
*
* @since 4.0.0
*/
public static $searchField = ['id' => 'filter_search'];
/**
* Locator for Search button class.
*
* @var array
*
* @since 4.0.0
*/
public static $searchButton = ['class' => 'icon-search'];
}

View File

@ -1,29 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for media manager file page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class MediaFilePage extends AdminFormPage
{
/**
* Url to media manager file page.
*
* @var string
* @since 4.0.0
*/
public static $url = "administrator/index.php?option=com_media&view=file";
}

View File

@ -1,406 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class for media manager list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class MediaListPage extends AdminListPage
{
/**
* Url to media manager listing page.
*
* @var string
* @since 4.0.0
*/
public static $url = "administrator/index.php?option=com_media&path=local-images:/";
/**
* Page title of the media manager listing page.
*
* @var string
* @since 4.0.0
*/
public static $pageTitleText = 'Media';
/**
* Page title of the media manager listing page.
*
* @var array
* @since 4.0.0
*/
public static $container = ['class' => 'media-container'];
/**
* Page title of the media manager listing page.
*
* @var array
* @since 4.0.0
*/
public static $infoBar = ['class' => 'media-infobar'];
/**
* The media browser items.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainer = ['class' => 'media-browser-items'];
/**
* The media browser items extra small.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainerExtraSmall = ['class' => 'media-browser-items-xs'];
/**
* The media browser items small.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainerSmall = ['class' => 'media-browser-items-sm'];
/**
* The media browser items medium.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainerMedium = ['class' => 'media-browser-items-md'];
/**
* The media browser items large.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainerLarge = ['class' => 'media-browser-items-lg'];
/**
* The media browser items extra large.
*
* @var array
* @since 4.0.0
*/
public static $itemsContainerExtraLarge = ['class' => 'media-browser-items-xl'];
/**
* The media browser items.
*
* @var array
* @since 4.0.0
*/
public static $items = ['class' => 'media-browser-item'];
/**
* The media browser items selected locator.
*
* @var array
* @since 4.0.0
*/
public static $itemSelected = ['css' => '.media-browser-item.selected'];
/**
* The media tree.
*
* @var array
* @since 4.0.0
*/
public static $mediaTree = ['class' => 'media-tree'];
/**
* The media tree.
*
* @var array
* @since 4.0.0
*/
public static $mediaBreadcrumb = ['class' => 'media-breadcrumb'];
/**
* Button that toggles the info bar.
*
* @var array
* @since 4.0.0
*/
public static $toggleInfoBarButton = ['class' => 'media-toolbar-info'];
/**
* The hidden file upload field.
*
* @var string
* @since 4.0.0
*/
public static $fileInputField = 'input[name=\'file\']';
/**
* The create folder button in the toolbar.
*
* @var string
* @since 4.0.0
*/
public static $toolbarCreateFolderButton = '//button[contains(@onclick, \'onClickCreateFolder\')]';
/**
* The delete button in the toolbar.
*
* @var string
* @since 4.0.0
*/
public static $toolbarDeleteButton = '//button[contains(@onclick, \'onClickDelete\')]';
/**
* The delete button in the toolbar.
*
* @var string
* @since 4.0.0
*/
public static $toolbarModalDeleteButton = ['id' => 'media-delete-item'];
/**
* The select all button.
*
* @var array
* @since 4.0.0
*/
public static $selectAllButton = ['class' => 'media-toolbar-select-all'];
/**
* The increase thumbnail size button.
*
* @var array
* @since 4.0.0
*/
public static $increaseThumbnailSizeButton = ['class' => 'media-toolbar-increase-grid-size'];
/**
* The disabled increase thumbnail size button disabled.
*
* @var array
* @since 4.0.0
*/
public static $increaseThumbnailSizeButtonDisabled = ['css' => '.media-toolbar-increase-grid-size.disabled'];
/**
* The decrease thumbnail size button.
*
* @var array
* @since 4.0.0
*/
public static $decreaseThumbnailSizeButton = ['class' => 'media-toolbar-decrease-grid-size'];
/**
* The decrease thumbnail size button disabled.
*
* @var array
* @since 4.0.0
*/
public static $decreaseThumbnailSizeButtonDisabled = ['css' => '.media-toolbar-decrease-grid-size.disabled'];
/**
* The disabled increase thumbnail size button disabled.
*
* @var array
* @since 4.0.0
*/
public static $toggleListViewButton = ['class' => 'media-toolbar-list-view'];
/**
* The item actions.
*
* @var string
* @since 4.0.0
*/
public static $itemActions = ['class' => 'media-browser-actions'];
/**
* The rename action.
*
* @var string
* @since 4.0.0
*/
public static $renameAction = 'action-rename';
/**
* The rename action.
*
* @var string
* @since 4.0.0
*/
public static $previewAction = 'action-preview';
/**
* The rename action.
*
* @var string
* @since 4.0.0
*/
public static $editAction = 'action-edit';
/**
* The name field of modal forms.
*
* @var array
* @since 4.0.0
*/
public static $renameInputField = ['id' => 'name'];
/**
* The name field of modal forms.
*
* @var array
* @since 4.0.0
*/
public static $newFolderInputField = ['id' => 'folder'];
/**
* The confirm button of modals.
*
* @var array
* @since 4.0.0
*/
public static $modalConfirmButton = ['css' => '.modal button.btn-success'];
/**
* The confirm button of modals.
*
* @var array
* @since 4.0.0
*/
public static $modalConfirmButtonDisabled = ['css' => '.modal button:disabled.btn-success'];
/**
* The preview modal.
*
* @var array
* @since 4.0.0
*/
public static $previewModal = ['class' => 'media-preview-modal'];
/**
* The preview modal image locator.
*
* @var array
* @since 4.0.0
*/
public static $previewModalImg = ['css' => '.media-preview-modal img'];
/**
* The preview modal image locator.
*
* @var array
* @since 4.0.0
*/
public static $previewModalCloseButton = ['class' => 'media-preview-close'];
/**
* The media browser grid.
*
* @var array
* @since 4.0.0
*/
public static $mediaBrowserGrid = ['class' => 'media-browser-grid'];
/**
* The media browser table.
*
* @var array
* @since 4.0.0
*/
public static $mediaBrowserTable = ['class' => 'media-browser-table'];
/**
* The search input field.
*
* @var array
* @since 4.0.5
*/
public static $searchInputField = ['id' => 'media_search'];
/**
* The key for the app storage.
*
* @var string
* @since 4.0.0
*/
public static $loader = ['class' => 'media-loader'];
/**
* The key for the app storage.
*
* @var string
* @since 4.0.0
*/
public static $storageKey = 'joomla.mediamanager';
/**
* Dynamic locator for media item files.
*
* @param string $name Name
*
* @return string
*
* @since 4.0.0
*/
public static function item($name)
{
return self::itemXpath($name);
}
/**
* Dynamic locator for media item action.
*
* @param string $itemName Item name
*
* @return string
*
* @since 4.0.0
*/
public static function itemActionMenuToggler($itemName)
{
return self::itemXpath($itemName) . '//button[@class= \'action-toggle\']';
}
/**
* Dynamic locator for media item action.
*
* @param string $itemName Item name
* @param string $actionName Action name
*
* @return string
*
* @since 4.0.0
*/
public static function itemAction($itemName, $actionName)
{
return self::itemXpath($itemName) . '//button[@class= \'' . $actionName . '\']';
}
/**
* Get the xpath of a media item.
*
* @param string $name name
*
* @return string
*
* @since 4.0.0
*/
protected static function itemXpath($name)
{
return '//div[contains(@class, \'media-browser-item-info\') and normalize-space(text()) = \'' . $name . '\']/parent::div';
}
}

View File

@ -1,61 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object for menu form pages.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class MenuFormPage extends AdminFormPage
{
/**
* Url to menu create page.
*
* @var string
* @since 4.0.0
*/
public static $url = "administrator/index.php?option=com_menus&view=menu&layout=edit";
/**
* Page title for adding a menu.
*
* @var string
* @since 4.0.0
*/
public static $pageTitleText = 'Menus: Add';
/**
* Title field.
*
* @var array
* @since 4.0.0
*/
public static $fieldTitle = ['id' => 'jform_title'];
/**
* Type field.
*
* @var array
* @since 4.0.0
*/
public static $fieldMenuType = ['id' => 'jform_menutype'];
/**
* Optional Description field.
*
* @var array
* @since 4.0.0
*/
public static $fieldMenuDescription = ['id' => 'jform_menudescription'];
}

View File

@ -1,37 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class to menu list page.
*
* @package Page\Acceptance\Administrator
*
* @since 4.0.0
*/
class MenuListPage extends AdminListPage
{
/**
* Url to menu page.
*
* @var string
* @since 4.0.0
*/
public static $url = "administrator/index.php?option=com_menus&view=menus";
/**
* Page title of the menu page.
*
* @var string
* @since 4.0.0
*/
public static $pageTitleText = 'Menus';
}

View File

@ -1,197 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Administrator;
/**
* Acceptance Page object class to user list page.
*
* @package Page\Acceptance\Administrator
*
* @since 3.7.3
*/
class UserListPage extends AdminListPage
{
/**
* Url to user manager listing page.
*
* @var string
* @since 3.7.3
*/
public static $url = "administrator/index.php?option=com_users&view=users";
/**
* Page title of the user manager listing page.
*
* @var string
* @since 3.7.3
*/
public static $pageTitleText = "Users";
/**
* Edit Button.
*
* @var string
* @since 3.7.3
*/
public static $editButton = ['class' => 'button-edit'];
/**
* Locator for the id.
*
* @var array
* @since 3.7.3
*/
public static $userCheckbox = ['id' => 'cb1'];
/**
* Locator for the row. (Checkbox is no longer clickable in j4..)
*
* @var array
* @since 4.0.0
*/
public static $userRow = ['class' => 'row1'];
/**
* Save Button.
*
* @var string
* @since 3.7.3
*/
public static $saveButton = ['class' => 'button-save'];
/**
* Locator for user's name input field.
*
* @var array
* @since 3.7.3
*/
public static $nameField = ['id' => 'jform_name'];
/**
* Locator for the success message.
*
* @var string
* @since 3.7.3
*/
public static $successMessage = 'User saved.';
/**
* Account details.
*
* @var string
* @since 3.7.3
*/
public static $accountDetailsTab = ['xpath' => "//button[@aria-controls='details']"];
/**
* Locator for user's username input field.
*
* @var array
* @since 3.7.3
*/
public static $usernameField = ['id' => 'jform_username'];
/**
* Locator for user's password input field.
*
* @var array
* @since 3.7.3
*/
public static $passwordField = ['id' => 'jform_password'];
/**
* Locator for user's password input field for frontend.
*
* @var array
* @since 3.7.3
*/
public static $password1Field = ['id' => 'jform_password1'];
/**
* Locator for user's repeat password input field.
*
* @var array
* @since 3.7.3
*/
public static $password2Field = ['id' => 'jform_password2'];
/**
* Locator for user's email input field.
*
* @var array
* @since 3.7.3
*/
public static $emailField = ['id' => 'jform_email'];
/**
* Locator for user's email input field for frontend.
*
* @var array
* @since 3.7.3
*/
public static $email1Field = ['id' => 'jform_email1'];
/**
* Locator for user's repeat email input field.
*
* @var array
* @since 3.7.3
*/
public static $email2Field = ['id' => 'jform_email2'];
/**
* Locator for user's username field.
*
* @var array
* @since 3.7.3
*/
public static $seeUserName = ['xpath' => "//table[@id='userList']//tr[1]/td[3]"];
/**
* Locator for user's name field.
*
* @var array
* @since 3.7.3
*/
public static $seeName = ['xpath' => "//table[@id='userList']//tr[1]/td[2]"];
/**
* Locator for user's last login date field in backend listing.
*
* @var array
* @since 3.7.3
*/
public static $lastLoginDate = ['xpath' => "//table[@id='userList']//tr[1]/td[8]"];
/**
* Locator for user is blocked.
*
* @var array
* @since 3.7.3
*/
public static $seeBlocked = ['xpath' => "//table[@id='userList']//*//td[4]//span[@class='icon-unpublish']"];
/**
* Locator for user is unblocked.
*
* @var array
* @since 3.7.3
*/
public static $seeUnblocked = ['xpath' => "//table[@id='userList']//*//td[4]//span[@class='icon-check']"];
/**
* Locator for user is deleted and not found.
*
* @var array
* @since 3.7.3
*/
public static $noItems = ['class' => 'alert-no-items'];
}

View File

@ -1,45 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Page
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Page\Acceptance\Site;
/**
* Acceptance Page object class to define Frontend page objects.
*
* @package Page\Acceptance\Site
*
* @since 3.7.3
*/
class FrontPage extends \AcceptanceTester
{
/**
* Link to the frontend.
*
* @var string
* @since 3.7.3
*/
public static $url = '/';
/**
* Locator for alert message in frontend.
*
* @var array
* @since 3.7.3
*/
public static $alertMessage = ['class' => 'alert-message'];
/**
* Locator for login greeting for the user.
*
* @var array
* @since 3.7.3
*/
public static $loginGreeting = ['class' => 'login-greeting'];
}

View File

@ -1,75 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Step
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Step\Acceptance\Administrator;
use AcceptanceTester;
use Exception;
use Page\Acceptance\Administrator\AdminPage;
/**
* Acceptance Step object class for admin steps.
*
* @package Step\Acceptance\Administrator
*
* @since 4.0.0
*/
class Admin extends AcceptanceTester
{
/**
* Method to confirm system message appear.
*
* @param string $text The text of message
* @param int $timeout Number of seconds to wait
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function seeSystemMessage($text, $timeout = null)
{
$I = $this;
if (is_null($timeout)) {
$timeout = $I->getConfig('timeout');
}
$I->waitForText($text, $timeout, AdminPage::$systemMessageContainer);
$I->see($text, AdminPage::$systemMessageContainer);
}
/**
* Method to confirm and close an appearing system message.
*
* @param string $text The text of message
* @param int $timeout Number of seconds to wait
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function seeAndCloseSystemMessage($text, $timeout = null)
{
$I = $this;
if (is_null($timeout)) {
$timeout = $I->getConfig('timeout');
}
$I->waitForText($text, $timeout, AdminPage::$systemMessageContainer);
$I->see($text, AdminPage::$systemMessageContainer);
$I->executeJS('Joomla.removeMessages()');
$I->wait(2);
}
}

View File

@ -1,221 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Step
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Step\Acceptance\Administrator;
use Exception;
use Page\Acceptance\Administrator\BannerListPage;
/**
* Acceptance Step object class contains suits for Content Manager.
*
* @package Step\Acceptance\Administrator
*
* @since 4.0.0
*/
class Banner extends Admin
{
/**
* Method to create a banner.
*
* @param string $title Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function createBanner($title, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->clickToolbarButton('New');
$I->fillField(BannerListPage::$titleField, $title);
$I->clickToolbarButton('Save & Close');
$I->assertSuccessMessage($message);
}
/**
* Method to see success message.
*
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function assertSuccessMessage($message)
{
$I = $this;
$I->waitForText($message, $I->getConfig('timeout'), BannerListPage::$systemMessageContainer);
$I->see($message, BannerListPage::$systemMessageContainer);
}
/**
* Method to modify a banner.
*
* @param string $bannerTitle Banner Title
* @param string $updatedTitle Update Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function modifyBanner($bannerTitle, $updatedTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->click($bannerTitle);
$I->waitForElement(BannerListPage::$titleField, $I->getConfig('timeout'));
$I->fillField(BannerListPage::$titleField, $updatedTitle);
$I->fillField(BannerListPage::$aliasField, $updatedTitle);
$I->clickToolbarButton('Save & Close');
$I->assertSuccessMessage($message);
}
/**
* Method to publish a banner.
*
* @param string $bannerTitle Banner Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function publishBanner($bannerTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->waitForElement(BannerListPage::$searchField, $I->getConfig('timeout'));
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->Click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->clickToolbarButton('Publish');
$I->assertSuccessMessage($message);
}
/**
* Method to unpublish a banner.
*
* @param string $bannerTitle Banner Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function unpublishBanner($bannerTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->waitForElement(BannerListPage::$searchField, $I->getConfig('timeout'));
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->Click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->clickToolbarButton('Unpublish');
$I->assertSuccessMessage($message);
}
/**
* Method to checkin a banner.
*
* @param string $bannerTitle Banner Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function checkInBanner($bannerTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->waitForElement(BannerListPage::$searchField, $I->getConfig('timeout'));
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->Click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->clickToolbarButton('check-in');
$I->assertSuccessMessage($message);
}
/**
* Method to trash a banner.
*
* @param string $bannerTitle Banner Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function trashBanner($bannerTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->waitForElement(BannerListPage::$searchField, $I->getConfig('timeout'));
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->Click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->clickToolbarButton('Trash');
$I->assertSuccessMessage($message);
}
/**
* Method to delete a banner.
*
* @param string $bannerTitle Banner Title
* @param string $message Message
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function deleteBanner($bannerTitle, $message)
{
$I = $this;
$I->amOnPage(BannerListPage::$url);
$I->waitForElement(BannerListPage::$searchField, $I->getConfig('timeout'));
// Make sure that the element js-stools-container-filters is visible.
// Filter is a toggle button and I never know what happened before.
$I->executeJS("[].forEach.call(document.querySelectorAll('.js-stools-container-filters'), function (el) {
el.classList.add('js-stools-container-filters-visible');
});");
$I->selectOption('//*[@id="filter_published"]', "-2");
$I->wait(2);
$I->fillField(BannerListPage::$searchField, $bannerTitle);
$I->Click(BannerListPage::$filterSearch);
$I->checkAllResults();
$I->clickToolbarButton('Empty trash');
$I->acceptPopup();
}
}

View File

@ -1,248 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Step
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Step\Acceptance\Administrator;
use Exception;
use Page\Acceptance\Administrator\ContentListPage;
/**
* Acceptance Step object class contains suits for Content Manager.
*
* @package Step\Acceptance\Administrator
*
* @since 4.0.0
*/
class Content extends Admin
{
/**
* Flag if workflows are enabled by default
*
* @var bool
*/
private $workflowsEnabled = false;
/**
* Method to create an article.
*
* @param array articleDetails Array with Article Details like Title, Alias, Content etc
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function createArticle($articleDetails)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$pageTitle);
$I->clickToolbarButton('New');
$I->waitForElement(ContentListPage::$articleTitleField, 30);
$I->fillField(ContentListPage::$articleTitleField, $articleDetails['title']);
$I->fillField(ContentListPage::$articleAliasField, $articleDetails['alias']);
$I->clickToolbarButton('Save & Close');
$I->waitForElement(ContentListPage::$articleSearchField, $I->getConfig('timeout'));
$I->click(ContentListPage::$systemMessageAlertClose);
$I->fillField(ContentListPage::$articleSearchField, $articleDetails['title']);
$I->click(ContentListPage::$searchButton);
$I->see($articleDetails['title']);
}
/**
* Method to feature an article.
*
* @param string $title Title
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function featureArticle($title)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->searchForArticle($title);
$I->checkAllResults();
$I->clickToolbarButton('Action');
$I->wait(2);
$I->clickToolbarButton('feature');
$I->wait(2);
$I->see($title);
}
/**
* Method to set an article accesslevel.
*
* @param string $title Title
* @param string $accessLevel AccessLevel
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function setArticleAccessLevel($title, $accessLevel)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->searchForItem($title);
$I->checkAllResults();
$I->click($title);
$I->waitForElement(['id' => "jform_access"], $I->getConfig('timeout'));
$I->selectOption(['id' => "jform_access"], $accessLevel);
$I->click(ContentListPage::$dropDownToggle);
$I->clickToolbarButton('Save & Close');
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->see($accessLevel, ContentListPage::$seeAccessLevel);
}
/**
* Method to unpublish an article.
*
* @param string $title Title
*
* @return void
*
* @since 4.0.0
* @throws Exception
*/
public function unPublishArticle($title)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->searchForArticle($title);
$I->checkAllResults();
$I->clickToolbarButton('Action');
$I->wait(2);
if ($this->workflowsEnabled) {
$I->clickToolbarButton('transition', '1');
} else {
$I->clickToolbarButton('unpublish');
}
$I->filterByCondition($title, "0");
}
/**
* Method to Publish an article.
*
* @param string $title Title
*
* @return void
*
* @since 4.0.0
* @throws Exception
*/
public function publishArticle($title)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->searchForArticle($title);
$I->checkAllResults();
$I->clickToolbarButton('Action');
$I->wait(2);
if ($this->workflowsEnabled) {
$I->clickToolbarButton('transition', '2');
} else {
$I->clickToolbarButton('publish');
}
$I->filterByCondition($title, "1");
}
/**
* Method to trash an article.
*
* @param string $title Title
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function trashArticle($title)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->searchForArticle($title);
$I->checkAllResults();
$I->clickToolbarButton('Action');
$I->wait(2);
if ($this->workflowsEnabled) {
$I->clickToolbarButton('transition', '3');
} else {
$I->clickToolbarButton('trash');
}
$I->filterByCondition($title, "-2");
}
/**
* Method to Delete an article.
*
* @param string $title Title
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function deleteArticle($title)
{
$I = $this;
$I->amOnPage(ContentListPage::$url);
$I->waitForElement(ContentListPage::$filterSearch, $I->getConfig('timeout'));
$I->filterByCondition($title, "-2");
$I->searchForArticle($title);
$I->checkAllResults();
$I->clickToolbarButton('empty trash');
$I->wait(2);
$I->acceptPopup();
}
public function searchForArticle($title)
{
$I = $this;
$I->waitForElement(ContentListPage::$articleSearchField, $I->getConfig('timeout'));
$I->fillField(ContentListPage::$articleSearchField, $title);
$I->click(ContentListPage::$searchButton);
$I->see($title);
}
public function filterByCondition($title, $condition)
{
$I = $this;
// Make sure that the class js-stools-container-filters is visible.
// Filter is a toggle button and I never know what happened before.
$I->executeJS("[].forEach.call(document.querySelectorAll('.js-stools-container-filters'), function (el) {
el.classList.add('js-stools-container-filters-visible');
});");
$I->selectOption('//*[@id="filter_published"]', $condition);
$I->wait(2);
$I->see($title);
}
}

View File

@ -1,354 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage AcceptanceTester.Step
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Step\Acceptance\Administrator;
use Codeception\Util\FileSystem as Util;
use Exception;
use Facebook\WebDriver\Exception\TimeoutException;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Page\Acceptance\Administrator\MediaListPage;
use PHPUnit\Framework\Assert;
/**
* Acceptance Step object class contains suits for Media Manager.
*
* @package Step\Acceptance\Administrator
*
* @since 4.0.0
*/
class Media extends Admin
{
/**
* Method to to wait for the media manager to load the data.
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function waitForMediaLoaded()
{
$I = $this;
try {
$I->waitForElement(MediaListPage::$loader, 3);
$I->waitForElementNotVisible(MediaListPage::$loader);
// Add a small timeout to wait for rendering (otherwise it will fail when executed in headless browser)
$I->wait(0.5);
} catch (TimeoutException $e) {
/*
* Continue if we cant find the loader within 3 seconds.
* In most cases this means that the loader disappeared so quickly, that selenium was not able to see it.
* Unfortunately we currently dont have any better technique to detect when vue components are loaded/updated
*/
}
}
/**
* Method to that tests that you see contents of a directory.
*
* @param array $contents Contents
*
* @return void
*
* @since 4.0.0
*/
public function seeContents(array $contents = [])
{
$I = $this;
$I->seeElement(MediaListPage::$items);
foreach ($contents as $content) {
$I->seeElement(MediaListPage::item($content));
}
}
/**
* Method to upload a file in the current directory.
*
* @param string $fileName Filename
*
* @return void
*
* @since 4.0.0
*/
public function uploadFile($fileName)
{
$I = $this;
$I->seeElementInDOM(MediaListPage::$fileInputField);
$I->attachFile(MediaListPage::$fileInputField, $fileName);
}
/**
* Method to delete a file from filesystem.
*
* @param string $path Path
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*
* @todo extract to JoomlaFilesystem
*/
public function deleteFile($path)
{
$I = $this;
$absolutePath = $this->absolutizePath($path);
if (!file_exists($absolutePath)) {
Assert::fail('file not found.');
}
unlink($absolutePath);
$I->comment('Deleted ' . $absolutePath);
}
/**
* Method to create a new directory on filesystem.
*
* @param string $dirname Dirname
* @param integer $mode Mode
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*
* @todo extract to JoomlaFilesystem
*/
public function createDirectory($dirname, $mode = 0755)
{
$I = $this;
$absolutePath = $this->absolutizePath($dirname);
$oldUmask = @umask(0);
@mkdir($absolutePath, $mode, true);
// This was adjusted to make drone work: codeception is executed as root, joomla runs as www-data
// so we have to run chown after creating new user.
if (!empty($user = $this->getLocalUser())) {
@chown($absolutePath, $user);
}
@umask($oldUmask);
$I->comment('Created ' . $absolutePath);
}
/**
* Method to deletes directory with all subdirectories.
*
* @param string $dirname Dirname
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*
* @todo extract to JoomlaFilesystem
*/
public function deleteDirectory($dirname)
{
$I = $this;
$absolutePath = $this->absolutizePath($dirname);
Util::deleteDir($absolutePath);
$I->comment('Deleted ' . $absolutePath);
}
/**
* Method to click on a link in the media tree.
*
* @param string $link Link
*
* @return void
*
* @since 4.0.0
*/
public function clickOnLinkInTree($link)
{
$I = $this;
$I->click($link, MediaListPage::$mediaTree);
}
/**
* Method to click on a link in the media breadcrumb.
*
* @param string $link Link
*
* @return void
* @since 4.0.0
*/
public function clickOnLinkInBreadcrumb($link)
{
$I = $this;
$I->click($link, MediaListPage::$mediaBreadcrumb);
}
/**
* Method to open the item actions menu of an item.
*
* @param string $itemName Item name
*
* @return void
*
* @since 4.0.0
*/
public function openActionsMenuOf($itemName)
{
$I = $this;
$toggler = MediaListPage::itemActionMenuToggler($itemName);
$I->moveMouseOver(MediaListPage::item($itemName));
$I->seeElement($toggler);
$I->click($toggler);
}
/**
* Method to open the item actions menu and click on one action.
*
* @param string $itemName Item name
* @param string $actionName Action name
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function clickOnActionInMenuOf($itemName, $actionName)
{
$I = $this;
$action = MediaListPage::itemAction($itemName, $actionName);
$I->openActionsMenuOf($itemName);
$I->waitForElementVisible($action);
$I->click($action);
}
/**
* Method to open the media manager info bar.
*
* @return void
*
* @since 4.0.0
*
* @throws Exception
*/
public function openInfobar()
{
$I = $this;
try {
$I->seeElement(MediaListPage::$infoBar);
} catch (Exception $e) {
$I->click(MediaListPage::$toggleInfoBarButton);
$I->waitForElementVisible(MediaListPage::$infoBar);
}
}
/**
* Method to close the media manager info bar.
*
* @return void
*
* @since 4.0.0
*/
public function closeInfobar()
{
$I = $this;
try {
$I->seeElement(MediaListPage::$infoBar);
$I->click(MediaListPage::$toggleInfoBarButton);
$I->waitForElementNotVisible(MediaListPage::$infoBar);
} catch (Exception $e) {
// Do nothing
}
}
/**
* Method to click on an element holding shift key.
*
* @param string $xpath Xpath selector
*
* @return void
*
* @since 4.0.0
*/
public function clickHoldingShiftkey($xpath)
{
$I = $this;
$I->executeInSelenium(
function (RemoteWebDriver $webdriver) use ($xpath) {
$element = $webdriver->findElement(WebDriverBy::xpath($xpath));
$action = new WebDriverActions($webdriver);
$shiftKey = WebDriverKeys::SHIFT;
$action->keyDown(null, $shiftKey)
->click($element)
->keyUp(null, $shiftKey)
->perform();
}
);
}
/**
* Method to get the absolute path.
*
* @param string $path Path
*
* @return string
*
* @since 4.0.0
*
* @throws Exception
*
* @todo extract to JoomlaFilesystem
*/
protected function absolutizePath($path)
{
return rtrim($this->getCmsPath(), '/') . '/' . ltrim($path, '/');
}
/**
* Method to get the local user from the configuration from suite configuration.
*
* @return string
*
* @since 4.0.0
*/
protected function getLocalUser()
{
$I = $this;
return $I->getConfig('localUser');
}
/**
* Method to get the cms path from suite configuration.
*
* @return string
*
* @since 4.0.0
*
* @throws Exception
*/
protected function getCmsPath()
{
$I = $this;
return $I->getConfig('cmsPath');
}
}

View File

@ -1,42 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage UnitTester
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:ignoreFile
use Codeception\Actor;
use Codeception\Lib\Friend;
/**
* Unit Tester global class for entry point.
*
* Inherited Methods.
*
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*
* @since 3.7.3
*/
class UnitTester extends Actor
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -1,157 +0,0 @@
# Codeception Test Suite Configuration
# Suite for acceptance tests.
# Perform tests in browser using the WebDriver or PhpBrowser.
# If you need both WebDriver and PHPBrowser tests - create a separate suite.
class_name: AcceptanceTester
modules:
enabled:
- Asserts
- Joomla\Browser\JoomlaBrowser
- Helper\Acceptance
- Helper\JoomlaDb
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/test-install' # the url that points to the joomla installation at /tests/system/joomla-cms
browser: 'chrome'
window_size: 1920x1080
capabilities:
'goog:chromeOptions':
args: [ "headless", "whitelisted-ips", "disable-gpu", "no-sandbox", "window-size=1920x1080", "--disable-dev-shm-usage" ]
name: 'jane doe' # Name for the Administrator
username: 'ci-admin' # UserName for the Administrator
password: 'joomla-17082005' # Password for the Administrator
database host: 'mysql' # place where the Application is Hosted #server Address
database user: 'root' # MySQL Server user ID, usually root
database password: 'joomla_ut' # MySQL Server password, usually empty or root
database name: 'test_joomla' # DB Name, at the Server
database type: 'mysqli' # type in lowercase one of the options: MySQL\MySQLi\PDO
database prefix: 'jos_' # DB Prefix for tables
install sample data: 'no' # Do you want to Download the Sample Data Along with Joomla Installation, then keep it Yes
sample data: 'Default English (GB) Sample Data' # Default Sample Data
admin email: 'admin@example.org' # email Id of the Admin
language: 'English (United Kingdom)' # Language in which you want the Application to be Installed
timeout: 90 # or 90000 the same result
log_js_errors: true
cmsPath: '/tests/www/test-install-test' # ; If you want to setup your test website (document root) in a different folder, you can do that here.
localUser: 'www-data' # (Linux / Mac only) If you want to set a different owner for the CMS test folder
Helper\JoomlaDb:
dsn: 'mysql:host=mysql;dbname=test_joomla'
user: 'root'
password: 'joomla_ut'
prefix: 'jos_'
error_level: "E_ALL & ~E_STRICT & ~E_DEPRECATED"
env:
postgres:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/postgres'
database host: 'postgres'
database type: 'pgsql'
database prefix: 'jos_'
cmsPath: '/tests/www/postgres'
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'jos_'
mysql8:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysql8'
database host: 'mysql8'
database prefix: 'jos_'
cmsPath: '/tests/www/mysql8'
Helper\JoomlaDb:
prefix: 'jos_'
mysql:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysql'
database prefix: 'jos_'
cmsPath: '/tests/www/mysql'
Helper\JoomlaDb:
prefix: 'jos_'
postgresphpmax:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/postgresphpmax'
database host: 'postgres'
database type: 'pgsql'
database prefix: 'phpmax_'
cmsPath: '/tests/www/postgresphpmax'
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'phpmax_'
mysql8phpmax:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysql8phpmax'
database host: 'mysql8'
database prefix: 'phpmax_'
cmsPath: '/tests/www/mysql8phpmax'
Helper\JoomlaDb:
prefix: 'phpmax_'
mysqlphpmax:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysqlphpmax'
database prefix: 'phpmax_'
cmsPath: '/tests/www/mysqlphpmax'
Helper\JoomlaDb:
prefix: 'phpmax_'
postgresphpnext:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/postgresphpnext'
database host: 'postgres'
database type: 'pgsql'
database prefix: 'phpnext_'
cmsPath: '/tests/www/postgresphpnext'
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'phpnext_'
mysql8phpnext:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysql8phpnext'
database host: 'mysql8'
database prefix: 'phpnext_'
cmsPath: '/tests/www/mysql8phpnext'
Helper\JoomlaDb:
prefix: 'phpnext_'
mysqlphpnext:
paths:
log: /drone/src/tests/Codeception/_output
modules:
config:
Joomla\Browser\JoomlaBrowser:
url: 'http://localhost/mysqlphpnext'
database prefix: 'phpnext_'
cmsPath: '/tests/www/mysqlphpnext'
Helper\JoomlaDb:
prefix: 'phpnext_'

View File

@ -1,51 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Acceptance.tests
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
/**
* Install Joomla
*
* @since 3.7.3
*/
class InstallCest
{
/**
* Install Joomla, disable statistics and enable Error Reporting.
*
* @param AcceptanceTester $I The AcceptanceTester Object
*
* @since 3.7.3
*
* @return void
*/
public function installJoomla(AcceptanceTester $I)
{
$I->am('Administrator');
$I->installJoomla();
}
/**
* Disables the statistics and sets error reporting to development.
*
* @param AcceptanceTester $I The AcceptanceTester Object
*
* @return void
*
* @since 4.0.0
*/
public function configureJoomla(AcceptanceTester $I)
{
$I->am('Administrator');
$I->doAdministratorLogin(null, null, false);
$I->disableStatistics();
$I->setErrorReportingToDevelopment();
}
}

View File

@ -1,958 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Acceptance.tests
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Page\Acceptance\Administrator\MediaFilePage;
use Page\Acceptance\Administrator\MediaListPage;
use Step\Acceptance\Administrator\Media;
/**
* TODO test d&d upload of files
* TODO test download of files
* TODO enable skipped tests
*/
/**
* Media manager list tests
*
* @since 4.0.0
*/
class MediaListCest
{
/**
* The default contents.
*
* @var array
*
* @since 4.0.0
*/
private $contents = [
'root' => [
'banners',
'headers',
'sampledata',
'joomla_black.png',
'powered_by.png',
],
'/banners' => [
'banner.jpg',
'osmbanner1.png',
'osmbanner2.png',
'shop-ad.jpg',
'shop-ad-books.jpg',
'white.png',
],
];
/**
* The name of the test directory, which gets deleted after each test.
*
* @var string
*
* @since 4.0.0
*/
private $testDirectory = 'test-dir';
/**
* Runs before every test.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function _before(Media $I)
{
$I->doAdministratorLogin();
// Create the test directory
$I->createDirectory('images/' . $this->testDirectory);
}
/**
* Runs after every test.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function _after(Media $I)
{
// Delete the test directory
$I->deleteDirectory('images/' . $this->testDirectory);
// Clear localstorage before every test
$I->executeJS('window.sessionStorage.removeItem("' . MediaListPage::$storageKey . '");');
}
/**
* Test that it loads without php notices and warnings.
*
* @param AcceptanceTester $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function loadsWithoutPhpNoticesAndWarnings(AcceptanceTester $I)
{
$I->wantToTest('that it loads without php notices and warnings.');
$I->amOnPage(MediaListPage::$url);
$I->waitForText(MediaListPage::$pageTitleText);
$I->checkForPhpNoticesOrWarnings();
}
/**
* Test that it shows then joomla default media files and folders.
*
* @param Media $I
*
* @since 4.0.0
*
* @throws Exception
*/
public function showsDefaultFilesAndFolders(Media $I)
{
$I->wantToTest('that it shows the joomla default media files and folders.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->seeElement(MediaListPage::$items);
$I->seeContents($this->contents['root']);
}
/**
* Test that it shows then joomla default media files and folders.
*
* @param Media $I
*
* @since 4.0.0
*
* @throws Exception
*/
public function showsFilesAndFoldersOfASubdirectoryWhenOpenedUsingDeepLink(Media $I)
{
$I->wantToTest('that it shows the media files and folders of a subdirectory when opened using deep link.');
$I->amOnPage(MediaListPage::$url . 'banners');
$I->waitForMediaLoaded();
$I->seeElement(MediaListPage::$items);
$I->seeContents($this->contents['/banners']);
}
/**
* Test that it is possible to select a single file.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function selectSingleFile(Media $I)
{
$I->wantToTest('that it is possible to select a single file');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::item('powered_by.png'));
$I->seeNumberOfElements(MediaListPage::$itemSelected, 1);
}
/**
* Test that it is possible to select a single file.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function selectSingleFolder(Media $I)
{
$I->wantToTest('that it is possible to select a single folder');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::item('banners'));
$I->seeNumberOfElements(MediaListPage::$itemSelected, 1);
}
/**
* Test that it is possible to select an image and see the information in the infobar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function selectMultipleItems(Media $I)
{
$I->wantToTest('that it is possible to select multiple');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::item('banners'));
$I->clickHoldingShiftkey(MediaListPage::item('powered_by.png'));
$I->seeNumberOfElements(MediaListPage::$itemSelected, 2);
}
/**
* Test that it's possible to navigate to a subfolder using double click.
*
* @param Media $I
*
* @since 4.0.0
*
* @throws Exception
*/
public function navigateUsingDoubleClickOnFolder(Media $I)
{
$I->wantToTest('that it is possible to navigate to a subfolder using double click.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->doubleClick(MediaListPage::item('banners'));
$I->waitForMediaLoaded();
$I->seeInCurrentUrl(MediaListPage::$url . 'banners');
$I->seeContents($this->contents['/banners']);
}
/**
* Test that it's possible to navigate to a subfolder using tree.
*
* @param Media $I
*
* @since 4.0.0
*
* @throws Exception
*/
public function navigateUsingTree(Media $I)
{
$I->wantToTest('that it is possible to navigate to a subfolder using tree.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->clickOnLinkInTree('banners');
$I->waitForMediaLoaded();
$I->seeInCurrentUrl(MediaListPage::$url . 'banners');
$I->seeContents($this->contents['/banners']);
}
/**
* Test that it's possible to navigate to a subfolder using breadcrumb.
*
* @param Media $I
*
* @since 4.0.0
*
* @throws Exception
*/
public function navigateUsingBreadcrumb(Media $I)
{
$I->wantToTest('that it is possible to navigate to a subfolder using breadcrumb.');
$I->amOnPage(MediaListPage::$url . 'banners');
$I->waitForMediaLoaded();
$I->clickOnLinkInBreadcrumb('images');
$I->waitForMediaLoaded();
$I->seeInCurrentUrl(MediaListPage::$url);
$I->seeContents($this->contents['root']);
}
/**
* Test that search is applied to the current list.
*
* @param Media $I
*
* @throws Exception
*
* @since 4.0.6
*/
public function searchInFilesAndFolders(Media $I)
{
$I->wantToTest('that search is applied to the current list.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->fillField(MediaListPage::$searchInputField, 'joomla');
$I->seeElement(MediaListPage::$items);
$I->seeElement(MediaListPage::item('joomla_black.png'));
$I->dontSeeElement(MediaListPage::item('banners'));
}
/**
* Test that search is cleared when navigating in the tree.
*
* @param Media $I
*
* @throws Exception
*
* @since 4.0.6
*/
public function searchIsClearedOnNavigate(Media $I)
{
$I->wantToTest('that search is cleared when navigating in the tree.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->fillField(MediaListPage::$searchInputField, 'banner');
$I->doubleClick(MediaListPage::item('banners'));
$I->waitForMediaLoaded();
$I->seeInField(MediaListPage::$searchInputField, '');
}
/**
* Test the upload of a single file using toolbar button.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*/
public function uploadSingleFileUsingToolbarButton(Media $I)
{
$testFileName = 'test-image-1.png';
$testFileItem = MediaListPage::item($testFileName);
$I->wantToTest('the upload of a single file using toolbar button.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForJsOnPageLoad();
$I->uploadFile('com_media/' . $testFileName);
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->seeContents([$testFileName]);
$I->click($testFileItem);
$I->click(MediaListPage::$toolbarDeleteButton);
$I->waitForElement(MediaListPage::$toolbarModalDeleteButton);
$I->waitForJsOnPageLoad();
// Sometimes the modal is still fading in
$I->wait(1);
$I->click(MediaListPage::$toolbarModalDeleteButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeAndCloseSystemMessage('Item deleted.');
$I->waitForElementNotVisible($testFileItem);
}
/**
* Test the upload of an existing file using toolbar button.
*
* @skip We need to skip this test, because of a bug in acceptPopup in chrome.
* It throws a Facebook\WebDriver\Exception\UnexpectedAlertOpenException and does not accept the popup.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function uploadExistingFileUsingToolbarButton(Media $I)
{
$testFileName = 'test-image-1.jpg';
$I->wantToTest('that it shows a confirmation dialog when uploading existing file.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForJsOnPageLoad();
$I->uploadFile('com_media/' . $testFileName);
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->uploadFile('com_media/' . $testFileName);
$I->seeContents([$testFileName]);
$I->waitForMediaLoaded();
$I->seeInPopup($testFileName . ' already exists. Do you want to replace it?');
$I->acceptPopup();
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->seeContents([$testFileName]);
}
/**
* Test the create folder using toolbar button.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function createFolderUsingToolbarButton(Media $I)
{
$testFolderName = 'test-folder';
$I->wantToTest('that it is possible to create a new folder using the toolbar button.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForJsOnPageLoad();
$I->click(MediaListPage::$toolbarCreateFolderButton);
$I->waitForJsOnPageLoad();
// Sometimes the modal is still fading in
$I->wait(1);
$I->seeElement(MediaListPage::$newFolderInputField);
$I->seeElement(MediaListPage::$modalConfirmButtonDisabled);
$I->fillField(MediaListPage::$newFolderInputField, $testFolderName);
$I->waitForElementChange(MediaListPage::$modalConfirmButton, function (Facebook\WebDriver\Remote\RemoteWebElement $el) {
return $el->isEnabled();
});
$I->click(MediaListPage::$modalConfirmButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeSystemMessage('Folder created.');
$I->waitForElement(MediaListPage::item($testFolderName));
$I->seeElement(MediaListPage::item($testFolderName));
}
/**
* Test create an existing folder.
*
* @skip Skipping until bug is resolved in media manager
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*/
public function createExistingFolderUsingToolbar(Media $I)
{
$I->wantToTest('that it is not possible to create an existing folder.');
$I->amOnPage(MediaListPage::$url);
$I->click(MediaListPage::$toolbarCreateFolderButton);
$I->seeElement(MediaListPage::$newFolderInputField);
$I->seeElement(MediaListPage::$modalConfirmButtonDisabled);
$I->fillField(MediaListPage::$newFolderInputField, $this->testDirectory);
$I->waitForElementChange(MediaListPage::$modalConfirmButton, function (Facebook\WebDriver\Remote\RemoteWebElement $el) {
return $el->isEnabled();
});
$I->click(MediaListPage::$modalConfirmButton);
$I->seeSystemMessage('Error creating folder.');
}
/**
* Test delete single file using toolbar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function deleteSingleFileUsingToolbar(Media $I)
{
$testFileName = 'test-image-1.png';
$testFileItem = MediaListPage::item($testFileName);
$I->wantToTest('that it is possible to delete a single file.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->uploadFile('com_media/' . $testFileName);
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->waitForElement($testFileItem);
$I->waitForJsOnPageLoad();
$I->click($testFileItem);
$I->click(MediaListPage::$toolbarDeleteButton);
$I->waitForElement(MediaListPage::$toolbarModalDeleteButton);
// Sometimes the modal is still fading in
$I->wait(1);
$I->waitForJsOnPageLoad();
$I->click(MediaListPage::$toolbarModalDeleteButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeSystemMessage('Item deleted.');
$I->waitForElementNotVisible($testFileItem);
$I->dontSeeElement($testFileName);
}
/**
* Test toggle info bar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function deleteSingleFolder(Media $I)
{
$testfolderName = 'test-folder';
$testFolderItem = MediaListPage::item($testfolderName);
$I->wantToTest('that it is possible to delete a single folder.');
$I->createDirectory('images/' . $this->testDirectory . '/' . $testfolderName);
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForElement($testFolderItem);
$I->click($testFolderItem);
$I->click(MediaListPage::$toolbarDeleteButton);
$I->waitForElement(MediaListPage::$toolbarModalDeleteButton);
// Sometimes the modal is still fading in
$I->wait(1);
$I->waitForJsOnPageLoad();
$I->click(MediaListPage::$toolbarModalDeleteButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeSystemMessage('Item deleted.');
$I->waitForElementNotVisible($testFolderItem);
$I->dontSeeElement($testFolderItem);
}
/**
* Test check all items.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function deleteMultipleFiles(Media $I)
{
$testFileName1 = 'test-image-1.png';
$testFileName2 = 'test-image-1.jpg';
$testFileItem1 = MediaListPage::item($testFileName1);
$testFileItem2 = MediaListPage::item($testFileName2);
$I->wantToTest('that it is possible to delete multiple files.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->uploadFile('com_media/' . $testFileName1);
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->wait(10);
$I->waitForElement($testFileItem1);
// We have to clear the file input, otherwise our method of uploading the file via Codeception will upload it twice
$I->executeJS('document.getElementsByName(\'file\')[0].value = \'\'');
$I->waitForMediaLoaded();
$I->uploadFile('com_media/' . $testFileName2);
$I->seeAndCloseSystemMessage('Item uploaded.');
$I->wait(10);
$I->waitForMediaLoaded();
$I->waitForElement($testFileItem2);
$I->click($testFileItem1);
$I->clickHoldingShiftkey($testFileItem2);
$I->click(MediaListPage::$toolbarDeleteButton);
$I->waitForElement(MediaListPage::$toolbarModalDeleteButton);
$I->click(MediaListPage::$toolbarModalDeleteButton);
$I->seeSystemMessage('Item deleted.');
$I->waitForElementNotVisible($testFileItem1);
$I->waitForElementNotVisible($testFileItem2);
$I->dontSeeElement($testFileItem1);
$I->dontSeeElement($testFileItem2);
}
/**
* Test rename a file.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function renameFile(Media $I)
{
$testFileName = 'test-image-1.png';
$testFileItem = MediaListPage::item($testFileName);
$I->wantToTest('that it is possible to rename a file.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->uploadFile('com_media/' . $testFileName);
$I->waitForElement($testFileItem);
$I->wait(1);
$I->clickOnActionInMenuOf($testFileName, MediaListPage::$renameAction);
$I->waitForElement(MediaListPage::$renameInputField);
// Sometimes the modal is still fading in
$I->wait(1);
$I->seeElement(MediaListPage::$renameInputField);
$I->seeElement(MediaListPage::$modalConfirmButton);
$I->fillField(MediaListPage::$renameInputField, 'test-image-1-renamed');
$I->click(MediaListPage::$modalConfirmButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeSystemMessage('Item renamed.');
$I->dontSeeElement($testFileItem);
$I->seeElement(MediaListPage::item('test-image-1-renamed.png'));
}
/**
* Test rename a file to the same name as an existing file.
*
* @skip Skipping until bug is resolved in media manager
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function renameFileToExistingFile(Media $I)
{
$testFileName1 = 'test-image-1.png';
$testFileName2 = 'test-image-2.png';
$testFileItem1 = MediaListPage::item($testFileName1);
$testFileItem2 = MediaListPage::item($testFileName2);
$I->wantToTest('that it is not possible to rename a file to a filename of an existing file.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->uploadFile('com_media/' . $testFileName1);
$I->waitForElement($testFileItem1);
$I->uploadFile('com_media/' . $testFileName2);
$I->waitForElement($testFileItem2);
$I->clickOnActionInMenuOf($testFileName2, MediaListPage::$renameAction);
$I->seeElement(MediaListPage::$renameInputField);
$I->seeElement(MediaListPage::$modalConfirmButton);
$I->fillField(MediaListPage::$renameInputField, 'test-image-1');
$I->click(MediaListPage::$modalConfirmButton);
$I->seeSystemMessage('Error renaming file.');
$I->seeElement($testFileItem1);
$I->seeElement($testFileItem2);
}
/**
* Test rename a file.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function renameFolder(Media $I)
{
$testFolderName = 'test-folder';
$testFolderItem = MediaListPage::item($testFolderName);
$I->wantToTest('that it is possible to rename a folder.');
$I->createDirectory('images/' . $this->testDirectory . '/' . $testFolderName);
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForElement($testFolderItem);
$I->wait(1);
$I->clickOnActionInMenuOf($testFolderName, MediaListPage::$renameAction);
$I->waitForElement(MediaListPage::$renameInputField);
// Sometimes the modal is still fading in
$I->wait(1);
$I->seeElement(MediaListPage::$renameInputField);
$I->seeElement(MediaListPage::$modalConfirmButton);
$I->fillField(MediaListPage::$renameInputField, $testFolderName . '-renamed');
$I->click(MediaListPage::$modalConfirmButton);
// Ensure the modal has closed
$I->wait(1);
$I->seeSystemMessage('Item renamed.');
$I->dontSeeElement($testFolderItem);
$I->seeElement(MediaListPage::item($testFolderName . '-renamed'));
}
/**
* Test rename a folder to the same name as an existing folder.
*
* @skip Skipping until bug is resolved in media manager
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function renameFolderToExistingFolder(Media $I)
{
$testFolderName1 = 'test-folder-1';
$testFolderName2 = 'test-folder-2';
$testFolderItem1 = MediaListPage::item($testFolderName1);
$testFolderItem2 = MediaListPage::item($testFolderName2);
$I->wantToTest('that it is not possible to rename a folder to a foldername of an existing folder.');
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->createDirectory('images/' . $this->testDirectory . '/' . $testFolderName1);
$I->createDirectory('images/' . $this->testDirectory . '/' . $testFolderName2);
$I->amOnPage(MediaListPage::$url . $this->testDirectory);
$I->waitForElement($testFolderItem1);
$I->waitForElement($testFolderItem2);
$I->clickOnActionInMenuOf($testFolderName2, MediaListPage::$renameAction);
$I->seeElement(MediaListPage::$renameInputField);
$I->seeElement(MediaListPage::$modalConfirmButton);
$I->fillField(MediaListPage::$renameInputField, $testFolderName1);
$I->click(MediaListPage::$modalConfirmButton);
$I->seeSystemMessage('Error renaming folder.');
$I->seeElement($testFolderItem1);
$I->seeElement($testFolderItem2);
}
/**
* Test preview using double click on image.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function showPreviewUsingDoubleClickOnImage(Media $I)
{
$I->wantToTest('that it shows a preview for image when user doubleclicks it.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->doubleClick(MediaListPage::item('powered_by.png'));
$I->waitForElement(MediaListPage::$previewModal);
$I->seeElement(MediaListPage::$previewModal);
$I->see('powered_by.png', MediaListPage::$previewModal);
$I->seeElement(MediaListPage::$previewModalImg);
$I->seeElement(MediaListPage::$previewModalCloseButton);
}
/**
* Test preview using action menu.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function showPreviewUsingClickOnActionMenu(Media $I)
{
$I->wantToTest('that it is possible to show a preview of an image using button in action menu.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->clickOnActionInMenuOf('powered_by.png', MediaListPage::$previewAction);
$I->waitForElement(MediaListPage::$previewModal);
$I->seeElement(MediaListPage::$previewModal);
$I->see('powered_by.png', MediaListPage::$previewModal);
$I->seeElement(MediaListPage::$previewModalImg);
$I->seeElement(MediaListPage::$previewModalCloseButton);
}
/**
* Test close the preview modal.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function closePreviewModalUsingCloseButton(Media $I)
{
$I->wantToTest('that it is possible to close the preview modal using the close button.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->doubleClick(MediaListPage::item('powered_by.png'));
$I->waitForElement(MediaListPage::$previewModal);
$I->waitForJsOnPageLoad();
$I->seeElement(MediaListPage::$previewModalCloseButton);
$I->click(MediaListPage::$previewModalCloseButton);
$I->dontSeeElement(MediaListPage::$previewModal);
}
/**
* Test close the preview modal.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function closePreviewModalUsingEscapeKey(Media $I)
{
$I->wantToTest('that it is possible to close the preview modal using escape key.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->doubleClick(MediaListPage::item('powered_by.png'));
$I->waitForElement(MediaListPage::$previewModal);
$I->waitForJsOnPageLoad();
$I->pressKey('body', \Facebook\WebDriver\WebDriverKeys::ESCAPE);
$I->dontSeeElement(MediaListPage::$previewModal);
}
/**
* Test rename a file.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function openImageEditorUsingActionMenu(Media $I)
{
$I->wantToTest('that it is possible to open the image editor using action menu.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->clickOnActionInMenuOf('powered_by.png', MediaListPage::$editAction);
$I->seeInCurrentUrl(MediaFilePage::$url . '&mediatypes=0,1,2,3&path=local-images:/powered_by.png');
}
/**
* Test toggle info bar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function toggleInfoBar(Media $I)
{
$I->wantToTest('that it is possible to toggle the infobar.');
$I->amOnPage(MediaListPage::$url);
$I->openInfobar();
$I->seeElement(MediaListPage::$infoBar);
$I->closeInfobar();
$I->waitForElementNotVisible(MediaListPage::$infoBar);
$I->dontSeeElement(MediaListPage::$infoBar);
}
/**
* Test show file information in infobar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function showFileInformationInInfobar(Media $I)
{
$I->wantToTest('that it shows basic file information in the infobar.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::item('powered_by.png'));
$I->openInfobar();
$I->see('powered_by.png', MediaListPage::$infoBar);
$I->see('image/png', MediaListPage::$infoBar);
}
/**
* Test show folder information in infobar.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function showFolderInformationInInfobar(Media $I)
{
$I->wantToTest('that it shows basic folder information in the infobar.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::item('banners'));
$I->openInfobar();
$I->see('banners', MediaListPage::$infoBar);
$I->see('directory', MediaListPage::$infoBar);
}
/**
* Test resize the thumbnails.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function resizeThumbnails(Media $I)
{
$I->wantToTest('that it is possible to resize the thumbnails.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
// Resize to max
$I->seeElement(MediaListPage::$itemsContainerMedium);
$I->click(MediaListPage::$increaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$itemsContainerLarge);
$I->click(MediaListPage::$increaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$itemsContainerExtraLarge);
$I->seeElement(MediaListPage::$increaseThumbnailSizeButtonDisabled);
// Resize to min
$I->click(MediaListPage::$decreaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$itemsContainerLarge);
$I->click(MediaListPage::$decreaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$itemsContainerMedium);
$I->click(MediaListPage::$decreaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$itemsContainerSmall);
$I->seeElement(MediaListPage::$decreaseThumbnailSizeButtonDisabled);
}
/**
* Test table view.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function toggleListViewUsingToolbarButton(Media $I)
{
$I->wantToTest('that it is possible to toggle the list view (grid/table) using the toolbar button.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->seeElement(MediaListPage::$mediaBrowserGrid);
$I->seeElement(MediaListPage::$toggleListViewButton);
$I->click(MediaListPage::$toggleListViewButton);
$I->dontSeeElement(MediaListPage::$increaseThumbnailSizeButton);
$I->dontSeeElement(MediaListPage::$decreaseThumbnailSizeButton);
$I->seeElement(MediaListPage::$mediaBrowserTable);
$I->click(MediaListPage::$toggleListViewButton);
$I->seeElement(MediaListPage::$mediaBrowserGrid);
}
/**
* Test check all items.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function selectAllItemsUsingToolbarButton(Media $I)
{
$I->wantToTest('that it is possible to select all items using toolbar button.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$I->click(MediaListPage::$selectAllButton);
$I->seeNumberOfElements(MediaListPage::$itemSelected, count($this->contents['root']) + 1);
}
/**
* Test that the app state is synced with session storage.
*
* @param Media $I Acceptance Helper Object
*
* @since 4.0.0
*
* @throws Exception
*/
public function synchronizeAppStateWithSessionStorage(Media $I)
{
$I->wantToTest('that the application state is synchronized with session storage.');
$I->amOnPage(MediaListPage::$url);
$I->waitForMediaLoaded();
$json = $I->executeJS('return sessionStorage.getItem("' . MediaListPage::$storageKey . '")');
$I->assertContains('"selectedDirectory":"local-images:/"', $json);
$I->assertContains('"showInfoBar":false', $json);
$I->assertContains('"listView":"grid"', $json);
$I->assertContains('"gridSize":"md"', $json);
$I->assertContains('"search":""', $json);
$I->clickOnLinkInTree('banners');
$I->waitForMediaLoaded();
$I->openInfobar();
$I->click(MediaListPage::$increaseThumbnailSizeButton);
$I->click(MediaListPage::$toggleListViewButton);
$I->fillField(MediaListPage::$searchInputField, 'joomla');
$json = $I->executeJS('return sessionStorage.getItem("' . MediaListPage::$storageKey . '")');
$I->assertContains('"selectedDirectory":"local-images:/banners"', $json);
$I->assertContains('"showInfoBar":true', $json);
$I->assertContains('"listView":"table"', $json);
$I->assertContains('"gridSize":"lg"', $json);
$I->assertContains('"search":"joomla"', $json);
}
}

View File

@ -1,95 +0,0 @@
actor: ApiTester
modules:
enabled:
- Helper\JoomlaDb
- Helper\Api
config:
Helper\JoomlaDb:
dsn: 'mysql:host=mysql;dbname=test_joomla'
user: 'root'
password: 'joomla_ut'
prefix: 'jos_'
Helper\Api:
MicrosoftEdgeInsiders: false # set this to true, if you are on Windows Insiders # ; If you want to setup your test website (document root) in a different folder, you can do that here.
localUser: 'www-data'
env:
mysql:
modules:
enabled:
- REST:
url: http://localhost/mysql/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\Api:
url: 'http://localhost/mysql'
cmsPath: '/tests/www/mysql'
postgres:
modules:
enabled:
- REST:
url: http://localhost/postgres/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'jos_'
Helper\Api:
url: 'http://localhost/postgres'
cmsPath: '/tests/www/postgres'
postgresphpmax:
modules:
enabled:
- REST:
url: http://localhost/postgresphpmax/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'phpmax_'
Helper\Api:
url: 'http://localhost/postgresphpmax'
cmsPath: '/tests/www/postgresphpmax'
mysqlphpmax:
modules:
enabled:
- REST:
url: http://localhost/mysqlphpmax/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\JoomlaDb:
prefix: 'phpmax_'
Helper\Api:
url: 'http://localhost/mysqlphpmax'
cmsPath: '/tests/www/mysqlphpmax'
postgresphpnext:
modules:
enabled:
- REST:
url: http://localhost/postgresphpnext/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\JoomlaDb:
dsn: 'pgsql:host=postgres;dbname=test_joomla'
prefix: 'phpnext_'
Helper\Api:
url: 'http://localhost/postgresphpnext'
cmsPath: '/tests/www/postgresphpnext'
mysqlphpnext:
modules:
enabled:
- REST:
url: http://localhost/mysqlphpnext/api/index.php/v1
depends: PhpBrowser
part: Json
config:
Helper\JoomlaDb:
prefix: 'phpnext_'
Helper\Api:
url: 'http://localhost/mysqlphpnext'
cmsPath: '/tests/www/mysqlphpnext'

View File

@ -1,72 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Api.tests
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
/**
* Class basicCest.
*
* Basic API function tests.
*
* @since 4.0.0
*/
class BasicCest
{
/**
* Test logging in with wrong credentials.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function testWrongCredentials(ApiTester $I)
{
$I->amBearerAuthenticated('BADTOKEN');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/content/articles/1');
$I->seeResponseCodeIs(Codeception\Util\HttpCode::UNAUTHORIZED);
}
/**
* Test content negotiation fails when accepting no json.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function testContentNegotiation(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'text/xml');
$I->sendGET('/content/articles/1');
$I->seeResponseCodeIs(Codeception\Util\HttpCode::NOT_ACCEPTABLE);
}
/**
* Test not found Resources return 404.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function testRouteNotFound(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/not/existing/1');
$I->seeResponseCodeIs(Codeception\Util\HttpCode::NOT_FOUND);
}
}

View File

@ -1,139 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Api.tests
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Codeception\Util\HttpCode;
/**
* Class BannerCest.
*
* Basic com_banners (banner) tests.
*
* @since 4.0.0
*/
class BannerCest
{
/**
* Api test before running.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function _before(ApiTester $I)
{
$I->deleteFromDatabase('banners');
$I->deleteFromDatabase('categories', ['id >' => 7]);
}
/**
* Test the crud endpoints of com_banners from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing banners
*/
public function testCrudOnBanner(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testBanner = [
'name' => 'My Custom Advert',
'catid' => 3,
'description' => '',
'custombannercode' => '',
'metakey' => '',
'params' => [
'imageurl' => '',
'width' => '',
'height' => '',
'alt' => '',
],
];
$I->sendPOST('/banners', $testBanner);
$I->seeResponseCodeIs(HttpCode::OK);
$id = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/banners/' . $id);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
// Category is a required field for this patch request for now @todo: Remove this dependency
$I->sendPATCH('/banners/' . $id, ['name' => 'Different Custom Advert', 'state' => -2, 'catid' => 3]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/banners/' . $id);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the category crud endpoints of com_banners from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing categories
*/
public function testCrudOnCategory(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testarticle = [
'title' => 'A test category',
'parent_id' => 3,
];
$I->sendPOST('/banners/categories', $testarticle);
$I->seeResponseCodeIs(HttpCode::OK);
$categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/banners/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
// Trash in order to allow the delete in the next step
$I->sendPATCH('/banners/categories/' . $categoryId, ['title' => 'Another Title', 'published' => -2]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/banners/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
}

View File

@ -1,133 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Api.tests
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Codeception\Util\HttpCode;
/**
* Class ContactCest.
*
* Basic com_contact (contact) tests.
*
* @since 4.0.0
*/
class ContactCest
{
/**
* Api test before running.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function _before(ApiTester $I)
{
$I->deleteFromDatabase('contact_details');
$I->deleteFromDatabase('categories', ['id >' => 7]);
}
/**
* Test the crud endpoints of com_contact from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing contacts
*/
public function testCrudOnContact(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testarticle = [
'alias' => 'contact-the-ceo',
'catid' => 4,
'language' => '*',
'name' => 'Francine Blogs',
];
$I->sendPOST('/contacts', $testarticle);
$I->seeResponseCodeIs(HttpCode::OK);
$id = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/contacts/' . $id);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
// Category is a required field for this patch request for now @todo: Remove this dependency
$I->sendPATCH('/contacts/' . $id, ['name' => 'Frankie Blogs', 'catid' => 4, 'published' => -2]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/contacts/' . $id);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the category crud endpoints of com_contact from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing categories
*/
public function testCrudOnCategory(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testContact = [
'title' => 'A test category',
'parent_id' => 4,
'params' => [
'workflow_id' => 'inherit',
],
];
$I->sendPOST('/contacts/categories', $testContact);
$I->seeResponseCodeIs(HttpCode::OK);
$categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/contacts/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPATCH('/contacts/categories/' . $categoryId, ['title' => 'Another Title', 'published' => -2]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/contacts/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
}

View File

@ -1,132 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Api.tests
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Codeception\Util\HttpCode;
/**
* Class ContentCest.
*
* Basic com_content (article) tests.
*
* @since 4.0.0
*/
class ContentCest
{
/**
* Api test before running.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*/
public function _before(ApiTester $I)
{
$I->deleteFromDatabase('content');
$I->deleteFromDatabase('categories', ['id >' => 7]);
}
/**
* Test the article crud endpoints of com_content from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing articles
*/
public function testCrudOnArticle(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testarticle = [
'title' => 'Just for you',
'catid' => 2,
'articletext' => 'A dummy article to save to the database',
'language' => '*',
'alias' => 'tobias',
];
$I->sendPOST('/content/articles', $testarticle);
$I->seeResponseCodeIs(HttpCode::OK);
$id = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/content/articles/' . $id);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPATCH('/content/articles/' . $id, ['title' => 'Another Title', 'state' => -2, 'catid' => 2]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/content/articles/' . $id);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the category crud endpoints of com_content from the API.
*
* @param mixed ApiTester $I Api tester
*
* @return void
*
* @since 4.0.0
*
* @TODO: Make these separate tests but requires sample data being installed so there are existing categories
*/
public function testCrudOnCategory(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$testarticle = [
'title' => 'A test category',
'parent_id' => 2,
'params' => [
'workflow_id' => 'inherit',
],
];
$I->sendPOST('/content/categories', $testarticle);
$I->seeResponseCodeIs(HttpCode::OK);
$categoryId = $I->grabDataFromResponseByJsonPath('$.data.id')[0];
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/content/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPATCH('/content/categories/' . $categoryId, ['title' => 'Another Title', 'params' => ['workflow_id' => 'inherit'], 'published' => -2]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDELETE('/content/categories/' . $categoryId);
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
}

View File

@ -1,549 +0,0 @@
<?php
/**
* @package Joomla.Tests
* @subpackage Api.tests
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Codeception\Util\FileSystem;
use Codeception\Util\HttpCode;
/**
* Class MediaCest.
*
* Basic com_media (files) tests.
*
* @since 4.1.0
*/
class MediaCest
{
/**
* The name of the test directory, which gets deleted after each test.
*
* @var string
*
* @since 4.1.0
*/
private $testDirectory = 'test-dir';
/**
* Runs before every test.
*
* @param ApiTester $I Api tester
*
* @since 4.1.0
*
* @throws Exception
*/
public function _before(ApiTester $I)
{
if (file_exists($this->getImagesDirectory($I))) {
FileSystem::deleteDir($this->getImagesDirectory($I));
}
// Copied from \Step\Acceptance\Administrator\Media:createDirectory()
$oldUmask = @umask(0);
@mkdir($this->getImagesDirectory($I), 0755, true);
if (!empty($user = $I->getConfig('localUser'))) {
@chown($this->getImagesDirectory($I), $user);
}
@umask($oldUmask);
}
/**
* Runs after every test.
*
* @param ApiTester $I Api tester
*
* @since 4.1.0
*
* @throws Exception
*/
public function _after(ApiTester $I)
{
// Delete the test directory
FileSystem::deleteDir($this->getImagesDirectory($I));
}
/**
* Test the GET media adapter endpoint of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetAdapters(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/adapters');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']);
}
/**
* Test the GET media adapter endpoint for a single adapter of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/adapters/local-images');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['provider_id' => 'local', 'name' => 'images']);
}
/**
* Test the GET media files endpoint of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFiles(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'banners']]]);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]);
}
/**
* Test the GET media files endpoint of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFilesInSubfolder(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files/sampledata/cassiopeia/');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'nasa1-1200.jpg']]]);
}
/**
* Test the GET media files endpoint of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFilesWithAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files/local-images:/sampledata/cassiopeia/');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'nasa1-1200.jpg']]]);
}
/**
* Test the GET media files endpoint of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testSearchFiles(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files?filter[search]=joomla');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]);
$I->dontSeeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'powered_by.png']]]);
$I->dontSeeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'banners']]]);
}
/**
* Test the GET media files endpoint for a single file of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFile(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files/joomla_black.png');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'joomla_black.png']]]);
$I->dontSeeResponseContainsJson(['data' => ['attributes' => ['url' => $I->getConfig('url') . '/images/joomla_black.png']]]);
}
/**
* Test the GET media files endpoint for a single file of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFileWithUrl(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files/joomla_black.png?url=1');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['url' => $I->getConfig('url') . '/images/joomla_black.png']]]);
}
/**
* Test the GET media files endpoint for a single file of com_media from the API.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testGetFolder(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendGET('/media/files/sampledata/cassiopeia');
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'cassiopeia']]]);
}
/**
* Test the POST media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testCreateFileWithoutAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPost(
'/media/files',
[
'path' => $this->testDirectory . '/test.jpg',
'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')),
]
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'test.jpg']]]);
}
/**
* Test the POST media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testCreateFolderWithoutAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPost(
'/media/files',
['path' => $this->testDirectory . '/test-from-create']
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'test-from-create']]]);
}
/**
* Test the POST media files endpoint of com_media from the API with adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testCreateFileWithAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPost(
'/media/files',
[
'path' => 'local-images:/' . $this->testDirectory . '/test.jpg',
'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')),
]
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'test.jpg']]]);
}
/**
* Test the POST media files endpoint of com_media from the API with adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testCreateFolderWithAdapter(ApiTester $I)
{
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPost(
'/media/files',
['path' => 'local-images:/' . $this->testDirectory . '/test-from-create']
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'test-from-create']]]);
}
/**
* Test the PATCH media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testUpdateFileWithoutAdapter(ApiTester $I)
{
file_put_contents($this->getImagesDirectory($I) . '/override.jpg', '1');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPatch(
'/media/files/' . $this->testDirectory . '/override.jpg',
[
'path' => $this->testDirectory . '/override.jpg',
'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')),
]
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'override.jpg']]]);
$I->dontSeeResponseContainsJson(['data' => ['attributes' => ['content' => '1']]]);
}
/**
* Test the PATCH media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testUpdateFolderWithoutAdapter(ApiTester $I)
{
mkdir($this->getImagesDirectory($I) . '/override');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPatch(
'/media/files/' . $this->testDirectory . '/override',
['path' => $this->testDirectory . '/override-new']
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'override-new']]]);
}
/**
* Test the PATCH media files endpoint of com_media from the API wit adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testUpdateFileWithAdapter(ApiTester $I)
{
file_put_contents($this->getImagesDirectory($I) . '/override.jpg', '1');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPatch(
'/media/files/local-images:/' . $this->testDirectory . '/override.jpg',
[
'path' => 'local-images:/' . $this->testDirectory . '/override.jpg',
'content' => base64_encode(file_get_contents(codecept_data_dir() . '/com_media/test-image-1.jpg')),
]
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'file', 'name' => 'override.jpg']]]);
$I->dontSeeResponseContainsJson(['data' => ['attributes' => ['content' => '1']]]);
}
/**
* Test the PATCH media files endpoint of com_media from the API with adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testUpdateFolderWithAdapter(ApiTester $I)
{
mkdir($this->getImagesDirectory($I) . '/override');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendPatch(
'/media/files/local-images:/' . $this->testDirectory . '/override',
['path' => 'local-images:/' . $this->testDirectory . '/override-new']
);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson(['data' => ['attributes' => ['type' => 'dir', 'name' => 'override-new']]]);
}
/**
* Test the DELETE media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testDeleteFileWithoutAdapter(ApiTester $I)
{
touch($this->getImagesDirectory($I) . '/todelete.jpg');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDelete('/media/files/' . $this->testDirectory . '/todelete.jpg');
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the DELETE media files endpoint of com_media from the API without adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testDeleteFolderWithoutAdapter(ApiTester $I)
{
mkdir($this->getImagesDirectory($I) . '/todelete');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDelete('/media/files/' . $this->testDirectory . '/todelete');
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the DELETE media files endpoint of com_media from the API with adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testDeleteFileWithAdapter(ApiTester $I)
{
touch($this->getImagesDirectory($I) . '/todelete.jpg');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDelete('/media/files/local-images:/' . $this->testDirectory . '/todelete.jpg');
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Test the DELETE media files endpoint of com_media from the API with adapter information.
*
* @param ApiTester $I Api tester
*
* @return void
*
* @since 4.1.0
*/
public function testDeleteFolderWithAdapter(ApiTester $I)
{
mkdir($this->getImagesDirectory($I) . '/todelete');
$I->amBearerAuthenticated($I->getBearerToken());
$I->haveHttpHeader('Accept', 'application/vnd.api+json');
$I->sendDelete('/media/files/local-images:/' . $this->testDirectory . '/todelete');
$I->seeResponseCodeIs(HttpCode::NO_CONTENT);
}
/**
* Returns the absolute tmp image folder path to work on.
*
* @param ApiTester $I Api tester
*
* @return string The absolute folder path
*
* @since 4.1.0
*/
private function getImagesDirectory(ApiTester $I): string
{
return $I->getConfig('cmsPath') . '/images/' . $this->testDirectory;
}
}

View File

@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -e
JOOMLA_BASE=$1
TEST_SUITE=$2
DB_ENGINE=$3
DB_HOST=$4
DB_PREFIX=$5
echo "[RUNNER] Prepare test environment"
# Switch to Joomla base directory
cd $JOOMLA_BASE
echo "[RUNNER] Copy files to test installation"
rsync -a --exclude-from=tests/Codeception/exclude.txt $JOOMLA_BASE/ /tests/www/$TEST_SUITE/
chown -R www-data /tests/www/$TEST_SUITE/
echo "[RUNNER] Start Apache & Chrome"
apache2ctl -D FOREGROUND &
google-chrome --version
echo "[RUNNER] Start Selenium"
selenium-standalone start > selenium.api.$TEST_SUITE.log 2>&1 &
echo -n "Waiting until Selenium is ready"
until $(curl --output /dev/null --silent --head --fail http://localhost:4444/wd/hub/status); do
printf '.'
sleep 2
done
echo .
echo "[RUNNER] Install Joomla"
cd /tests/www/$TEST_SUITE
php installation/joomla.php install --verbose --site-name="Joomla CMS test" --admin-email=admin@example.org --admin-username=ci-admin --admin-user="jane doe" --admin-password=joomla-17082005 --db-type=$DB_ENGINE --db-host=$DB_HOST --db-name=test_joomla --db-pass=joomla_ut --db-user=root --db-encryption=0 --db-prefix=$DB_PREFIX
# If you have found this line failing on OSX you need to brew install gnu-sed like we mentioned in the codeception readme!
# This replaces the site secret in configuration.php so we can guarantee a consistent API token for our super user.
sed -i "/\$secret/c\ public \$secret = 'tEstValue';" /tests/www/$TEST_SUITE/configuration.php
echo "[RUNNER] Run Codeception"
# Executing API tests
php libraries/vendor/bin/codecept run api --fail-fast --steps --debug --env $TEST_SUITE

View File

@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -e
JOOMLA_BASE=$1
DB_ENGINE=$2
echo "[RUNNER] Prepare test environment"
# Switch to Joomla base directory
cd $JOOMLA_BASE
echo "[RUNNER] Copy files to test installation"
rsync -a --exclude-from=tests/Codeception/exclude.txt $JOOMLA_BASE/ /tests/www/$DB_ENGINE/
chown -R www-data /tests/www/$DB_ENGINE/
echo "[RUNNER] Start Apache & Chrome"
apache2ctl -D FOREGROUND &
google-chrome --version
echo "[RUNNER] Prepare Selenium"
mkdir -p tests/Codeception/_output
if [[ -f "/usr/lib/node_modules/selenium-standalone/lib/default-config.js" ]]; then
flock tests/Codeception/_output/selenium.config.js cp /usr/lib/node_modules/selenium-standalone/lib/default-config.js tests/Codeception/_output/selenium.config.js
fi
echo "[RUNNER] Start Selenium"
selenium-standalone start > tests/Codeception/_output/selenium.$DB_ENGINE.log 2>&1 &
echo -n "Waiting until Selenium is ready"
until $(curl --output /dev/null --silent --head --fail http://localhost:4444/wd/hub/status); do
printf '.'
sleep 2
done
echo .
echo "[RUNNER] Run Codeception"
cd /tests/www/$DB_ENGINE
php libraries/vendor/bin/codecept run acceptance --fail-fast --steps --debug --env $DB_ENGINE

View File

@ -34,7 +34,7 @@ to your local environment.
```
4. Run an openldap docker image and/or configure the ldap settings in `./phpunit.xml`.
* If you set JTEST_LDAP_HOST to `localhost` (or change your hosts file so "openldap" points to the ip where the service listens for connections), the following command should give you a working configuration. If needed, replace `$(pwd)` with the path to where the docker service can access the Joomla root.
`docker run --rm --name openldap --env LDAP_ADMIN_USERNAME=admin --env LDAP_ADMIN_PASSWORD=adminpassword --env LDAP_USERS=customuser --env LDAP_PASSWORDS=custompassword --publish 1389:1389 --publish 1636:1636 --env LDAP_ENABLE_TLS=yes --env LDAP_TLS_CERT_FILE=/opt/bitnami/certs/openldap.crt --env LDAP_TLS_KEY_FILE=/opt/bitnami/certs/openldap.key --env LDAP_TLS_CA_FILE=/opt/bitnami/certs/CA.crt --env LDAP_CONFIG_ADMIN_ENABLED=yes --env LDAP_CONFIG_ADMIN_USERNAME=admin --env LDAP_CONFIG_ADMIN_PASSWORD=configpassword --env BITNAMI_DEBUG=true -v $(pwd)/tests/Codeception/_data/certs:/opt/bitnami/certs bitnami/openldap:latest`
`docker run --rm --name openldap --env LDAP_ADMIN_USERNAME=admin --env LDAP_ADMIN_PASSWORD=adminpassword --env LDAP_USERS=customuser --env LDAP_PASSWORDS=custompassword --publish 1389:1389 --publish 1636:1636 --env LDAP_ENABLE_TLS=yes --env LDAP_TLS_CERT_FILE=/opt/bitnami/certs/openldap.crt --env LDAP_TLS_KEY_FILE=/opt/bitnami/certs/openldap.key --env LDAP_TLS_CA_FILE=/opt/bitnami/certs/CA.crt --env LDAP_CONFIG_ADMIN_ENABLED=yes --env LDAP_CONFIG_ADMIN_USERNAME=admin --env LDAP_CONFIG_ADMIN_PASSWORD=configpassword --env BITNAMI_DEBUG=true -v $(pwd)/tests/certs:/opt/bitnami/certs bitnami/openldap:latest`
* If your ldap server supports "None", "STARTTLS" and "SSL/TLS" encryption and works with both "Bind and Search" and "Bind Directly as User" methods, you can use your own if you configure the `JTEST_LDAP_` directives in your `phpunit.xml` file according to your environment.
* If you do not want to run the docker openldap image and don't have an LDAP server running, you can skip the LDAP tests if you set JTEST_LDAP_HOST to an empty value.
5. Run `./libraries/vendor/bin/phpunit --testsuite Integration`.

View File

@ -15,6 +15,6 @@ Javascript tests
==========
The javascript tests test the Joomla-specific Javascript code. For further information on the tests and on how to run them, please check out https://docs.joomla.org/Special:MyLanguage/Running_JavaScript_Tests_for_the_Joomla_CMS
Codeception tests
Cypress tests
==========
The Codeception tests test the user interface in a real browser and the webservices API of Joomla with the help of Codeception. For further information on the tests and on how to run them, please check out [tests/Codeception/README.md](Codeception/README.md).
The Cypress tests test the user interface in a real browser and the webservices API of Joomla with the help of cypress.io. For further information on the tests and on how to run them, please check out [tests/System/README.md](cypress/README.md).

72
tests/System/README.md Normal file
View File

@ -0,0 +1,72 @@
# System tests in Joomla
The CMS system tests are executed in real browsers and are using the [cypress.io](https://www.cypress.io) framework. This article describes how to setup a local development environment to execute the existing tests and add new ones.
## Installation
A couple of steps are needed before the CMS system tests can be executed on the system.
1. Clone Joomla into a folder where it can be served by a web server
2. Install the PHP and Javascript dependencies by running the following commands:
1. `composer install`
2. `npm ci`
3. Copy the cypress.config.dist.js to cypress.config.js in the root of the joomla folder
4. Adapt the env variables in the file cypress.config.js, they need to point to a database server where joomla can be installed
5. Ensure the system has all the required dependencies according to their [docs article](https://docs.cypress.io/guides/getting-started/installing-cypress)
6. Run the command `npm run cypress:install`
## Run the existing tests
Cypress has a nice gui which lists all the existing tests and is able to launch a browser where the tests are executed. To open the cypress gui, run the following command:
`npm run cypress:open`
## Add new tests
To add new tests, create a cy.js file to a new folder which matches the following pattern (replace foo with the extension name to test):
- Component tests do belong to a folder {site or administrator}/components/com_foo
- Module tests do belong to a folder {site or administrator}/modules/mod_foo
- Plugins tests do belong to a folder plugins/{type}/foo
- API tests do belong to a folder api/com_foo
Probably the easiest way is to copy an existing file and adapt it to the extension which should be tested.
## Some developer information
Tests should be
- repeatable
- not depend on other tests
- small
- do one thing
### Tasks
The CMS tests do come with some convenient [cypress tasks](https://docs.cypress.io/api/commands/task) which are executing actions on the server in a node environment. Thats why the `cy.` namespace is not available. The following tasks are available, served by the file tests/System/plugins/index.js:
- **queryDB** Executes a query on the database
- **cleanupDB** does some cleanup, is executed automatically after every test
- **writeFile** writes a file relative to the CMS root folder
- **deleteFolder** deletes a folder relative to the CMS root folder
With the following code in a test a task can be executed `cy.task('writeFile', { path: 'images/dummy.text', content: '1' })`. Each task is asynchronous and must be chained, so to get the result a `.then(() => {})` must follow when executing a task.
### Commands
Commands are reusable code snippets which can be used in every test. Cypress allows to [add custom commands](https://docs.cypress.io/api/cypress-api/custom-commands) so we can use them across our tests. They can be used to create objects in the database or to call an API. As cypress doesn't support namespaces for commands they must be prefixed with the file name and an underscore. So a database command starts always with db_ and an API one with api_.
Commands can be called like a normal function, for example there is a command `db_createArticle`. We can use it like `cy.db_createArticle({ title: 'automated test article' })`. These commands are executed in the browser where the `cy.` namespace is available.
#### Database commands
The database commands create items in the database like articles or users. They are asynchronous and must be chained like `cy.db_createArticle({ title: 'automated test article' }).then((id) => ... the test)`. The following list of commands are available and are served by the file tests/System/support/commands/db.js:
- **db_createArticle** creates an article and returns the id
- **db_createContact** creates a contact and returns the id
- **db_createBanner** creates a banner and returns the id
- **db_createMenuItem** creates a menu item and returns the id
- **db_createModule** creates a module and returns the id
- **db_createUser** creates a user and returns the id
#### API commands
The API commands making API requests to the CMS API endpoint `/api/index.php/v1`. They are asynchronous and must be chained like `cy.api_get('/content/articles').then((response) => ... the test)`. The response is an object from [the cypress request command](https://docs.cypress.io/api/commands/request). The following list of commands are available and are served by the file tests/System/support/commands/api.js:
- **api_get** add the path as argument
- **api_post** add the path and content for the body as arguments
- **api_patch** add the path and content for the body as arguments
- **api_delete** add the path as argument
- **api_getBearerToken** returns the bearer token and no request object

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
JOOMLA_BASE=$1
TEST_GROUP=$2
DB_ENGINE=$3
DB_HOST=$4
echo "[RUNNER] Prepare test environment"
# Switch to Joomla base directory
cd $JOOMLA_BASE
echo "[RUNNER] Copy files to test installation"
rsync -a --exclude-from=tests/System/exclude.txt $JOOMLA_BASE/ /tests/www/$TEST_GROUP/
chown -R www-data /tests/www/$TEST_GROUP/
# Required for media manager tests
chmod -R 777 /tests/www/$TEST_GROUP/images
echo "[RUNNER] Start Apache"
apache2ctl -D FOREGROUND &
echo "[RUNNER] Run cypress tests"
chmod +rwx /root
npx cypress run --browser=firefox --e2e --env cmsPath=/tests/www/$TEST_GROUP,db_type=$DB_ENGINE,db_host=$DB_HOST,db_password=joomla_ut,db_prefix="${TEST_GROUP}_" --config baseUrl=http://localhost/$TEST_GROUP,screenshotsFolder=$JOOMLA_BASE/tests/System/output/screenshots

View File

@ -16,4 +16,3 @@ package.json
package-lock.json
phpunit.xml.dist
phpunit-pgsql.xml.dist
selenium.log

View File

@ -0,0 +1,20 @@
describe('Test that the API', () => {
it('returns unauthorized return code when a malformed token is set', () => {
cy.request({
method: 'GET', url: '/api/index.php/v1/content/articles', headers: { Authorization: 'Bearer 123' }, failOnStatusCode: false,
})
.its('status').should('equal', 401);
});
it('returns unauthorized return code when no token is set', () => {
cy.request({ method: 'GET', url: '/api/index.php/v1/content/articles', failOnStatusCode: false })
.its('status').should('equal', 401);
});
it('returns not found return code when wrong route is set', () => {
cy.api_getBearerToken().then((token) => cy.request({
method: 'GET', url: '/api/index.php/v1/content/articles/1', headers: { Authorization: `Bearer ${token}` }, failOnStatusCode: false,
})
.its('status').should('equal', 404));
});
});

View File

@ -0,0 +1,43 @@
afterEach(() => {
cy.task('queryDB', 'DELETE FROM #__banners');
});
describe('Test that banners API endpoint', () => {
it('can deliver a list of banners', () => {
cy.db_createBanner({ name: 'automated test banner' })
.then(() => cy.api_get('/banners'))
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'automated test banner'));
});
it('can create a banner', () => {
cy.api_post('/banners', {
name: 'automated test banner',
alias: 'test-banner',
catid: 3,
state: 1,
language: '*',
description: '',
custombannercode: '',
params: {
imageurl: '', width: '', height: '', alt: '',
},
}).then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'automated test banner'));
});
it('can update a banner', () => {
cy.db_createBanner({ name: 'automated test banner' })
.then((id) => cy.api_patch(`/banners/${id}`, { name: 'updated automated test banner' }))
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'updated automated test banner'));
});
it('can delete a banner', () => {
cy.db_createBanner({ name: 'automated test banner', state: -2 })
.then((id) => cy.api_delete(`/banners/${id}`));
});
});

View File

@ -0,0 +1,38 @@
afterEach(() => {
cy.task('queryDB', 'DELETE FROM #__contact_details');
});
describe('Test that contacts API endpoint', () => {
it('can deliver a list of contacts', () => {
cy.db_createContact({ name: 'automated test contact' })
.then(() => cy.api_get('/contacts'))
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'automated test contact'));
});
it('can create a contact', () => {
cy.api_post('/contacts', {
name: 'automated test contact',
alias: 'test-contact',
catid: 4,
published: 1,
language: '*',
}).then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'automated test contact'));
});
it('can update a contact', () => {
cy.db_createContact({ name: 'automated test contact', access: 1 })
.then((id) => cy.api_patch(`/contacts/${id}`, { name: 'updated automated test contact' }))
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'updated automated test contact'));
});
it('can delete a contact', () => {
cy.db_createContact({ name: 'automated test contact', published: -2 })
.then((id) => cy.api_delete(`/contacts/${id}`));
});
});

View File

@ -0,0 +1,47 @@
afterEach(() => {
cy.task('queryDB', 'DELETE FROM #__content');
});
describe('Test that content API endpoint', () => {
it('can deliver a list of articles', () => {
cy.db_createArticle({ title: 'automated test article' })
.then(() => cy.api_get('/content/articles'))
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('title')
.should('include', 'automated test article'));
});
it('can create an article', () => {
cy.api_post('/content/articles', {
title: 'automated test article',
alias: 'test-article',
catid: 2,
introtext: '',
fulltext: '',
state: 1,
access: 1,
language: '*',
created: '2023-01-01 20:00:00',
modified: '2023-01-01 20:00:00',
images: '',
urls: '',
attribs: '',
metadesc: '',
metadata: '',
}).then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('title')
.should('include', 'automated test article'));
});
it('can update an article', () => {
cy.db_createArticle({ title: 'automated test article' })
.then((id) => cy.api_patch(`/content/articles/${id}`, { title: 'updated automated test article' }))
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('title')
.should('include', 'updated automated test article'));
});
it('can delete an article', () => {
cy.db_createArticle({ title: 'automated test article', state: -2 }).then((id) => cy.api_delete(`/content/articles/${id}`));
});
});

View File

@ -0,0 +1,15 @@
describe('Test that media adapters API endpoint', () => {
it('can deliver a list of adapters', () => {
cy.api_get('/media/adapters')
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('provider_id')
.should('include', 'local'));
});
it('can deliver a specific of adapters', () => {
cy.api_get('/media/adapters/local-images')
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('provider_id')
.should('include', 'local'));
});
});

View File

@ -0,0 +1,191 @@
// Ensure test dir is available and has correct permissions
beforeEach(() => cy.task('writeFile', { path: 'images/test-dir/dummy.txt', content: '1' }));
afterEach(() => cy.task('deleteFolder', 'images/test-dir'));
describe('Test that media files API endpoint', () => {
it('can deliver a list of files', () => {
cy.api_get('/media/files')
.then((response) => {
cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'banners');
cy.wrap(response).its('body').its('data.4').its('attributes')
.its('name')
.should('include', 'joomla_black.png');
});
});
it('can deliver a list of files in a subfolder', () => {
cy.api_get('/media/files/sampledata/cassiopeia/')
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'nasa1-1200.jpg'));
});
it('can deliver a list of files with an adapter', () => {
cy.api_get('/media/files/local-images:/sampledata/cassiopeia/')
.then((response) => cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'nasa1-1200.jpg'));
});
it('can search in filenames', () => {
cy.api_get('/media/files?filter[search]=joomla')
.then((response) => {
cy.wrap(response).its('body').its('data.0').its('attributes')
.its('name')
.should('include', 'joomla_black.png');
cy.wrap(response).its('body').its('data').should('have.length', 1);
});
});
it('can deliver a single file', () => {
cy.api_get('/media/files/joomla_black.png')
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'joomla_black.png'));
});
it('can deliver a single file with the url', () => {
cy.api_get('/media/files/joomla_black.png?url=1')
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('url')
.should('include', 'joomla_black.png'));
});
it('can deliver a single folder', () => {
cy.api_get('/media/files/sampledata/cassiopeia')
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'cassiopeia'));
});
it('can create a file without adapter', () => {
cy.readFile('tests/System/data/com_media/test-image-1.jpg', 'binary')
.then((data) => cy.api_post('/media/files', { path: 'test-dir/test.jpg', content: Buffer.from(data, 'binary').toString('base64') }))
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'test.jpg');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/test.jpg');
});
});
it('can create a folder without adapter', () => {
cy.api_post('/media/files', { path: 'test-dir/test-from-create' })
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'test-from-create');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/test-from-create');
});
});
it('can create a file with adapter', () => {
cy.readFile('tests/System/data/com_media/test-image-1.jpg', 'binary')
.then((data) => cy.api_post('/media/files', { path: 'local-images:/test-dir/test.jpg', content: Buffer.from(data, 'binary').toString('base64') }))
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'test.jpg');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/test.jpg');
});
});
it('can create a folder with adapter', () => {
cy.api_post('/media/files', { path: 'local-images:/test-dir/test-from-create' })
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'test-from-create');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/test-from-create');
});
});
it('can update a file without adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/override.jpg', content: '1' })
.then(() => cy.readFile('tests/System/data/com_media/test-image-1.jpg', 'binary'))
.then((data) => cy.api_patch(
'/media/files/test-dir/override.jpg',
{ path: 'test-dir/override.jpg', content: Buffer.from(data, 'binary').toString('base64') },
)).then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'override.jpg');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/override.jpg');
});
});
it('can update a folder without adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/override/test.jpg', content: '1' })
.then(() => cy.api_patch('/media/files/test-dir/override', { path: 'test-dir/override-new' }))
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'override-new');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/override-new');
});
});
it('can update a file with adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/override.jpg', content: '1' })
.then(() => cy.readFile('tests/System/data/com_media/test-image-1.jpg', 'binary'))
.then((data) => cy.api_patch(
'/media/files/local-images:/test-dir/override.jpg',
{ path: 'local-images:/test-dir/override.jpg', content: Buffer.from(data, 'binary').toString('base64') },
)).then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'override.jpg');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/override.jpg');
});
});
it('can update a folder with adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/override/test.jpg', content: '1' })
.then(() => cy.api_patch('/media/files/local-images:/test-dir/override', { path: 'local-images:/test-dir/override-new' }))
.then((response) => {
cy.wrap(response).its('body').its('data').its('attributes')
.its('name')
.should('include', 'override-new');
cy.wrap(response).its('body').its('data').its('attributes')
.its('path')
.should('include', 'local-images:/test-dir/override-new');
});
});
it('can delete a file without adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/todelete.jpg', content: '1' })
.then(() => cy.api_delete('/media/files/test-dir/todelete.jpg'));
});
it('can delete a folder without adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/todelete/dummy.txt', content: '1' })
.then(() => cy.api_delete('/media/files/test-dir/todelete'));
});
it('can delete a file with adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/todelete.jpg', content: '1' })
.then(() => cy.api_delete('/media/files/local-images:/test-dir/todelete.jpg'));
});
it('can delete a folder with adapter', () => {
cy.task('writeFile', { path: 'images/test-dir/todelete/dummy.txt', content: '1' })
.then(() => cy.api_delete('/media/files/local-images:/test-dir/todelete'));
});
});

View File

@ -1,6 +1,3 @@
// type definitions for Cypress object "cy"
// <reference types="cypress" />
describe('Install Joomla', () => {
it('Install Joomla', () => {
cy.exec('rm configuration.php', { failOnNonZeroExit: false });
@ -25,5 +22,9 @@ describe('Install Joomla', () => {
cy.disableStatistics();
cy.setErrorReportingToDevelopment();
cy.doAdministratorLogout();
// Update to the correct secret for the API tests because of the bearer token
cy.readFile(`${Cypress.env('cmsPath')}/configuration.php`)
.then((content) => cy.task('writeFile', { path: 'configuration.php', content: content.replace(/^.*\$secret.*$/mg, "public $secret = 'tEstValue';") }));
});
});

View File

@ -1,13 +1,13 @@
describe('Test that the front page', () => {
it('can display featured contact', () => {
cy.db_createContact({ name: 'automated test contact 1' })
.then(() => cy.db_createContact({ name: 'automated test contact 2' }))
.then(() => cy.db_createContact({ name: 'automated test contact 3' }))
.then(() => cy.db_createContact({ name: 'automated test contact 4' }))
.then(() => cy.db_createMenuItem({ title: 'automated test Contact', link: 'index.php?option=com_contact&view=featured', path: '?option=com_contact&view=featured' }))
cy.db_createContact({ name: 'automated test contact 1', featured: 1 })
.then(() => cy.db_createContact({ name: 'automated test contact 2', featured: 1 }))
.then(() => cy.db_createContact({ name: 'automated test contact 3', featured: 1 }))
.then(() => cy.db_createContact({ name: 'automated test contact 4', featured: 1 }))
.then(() => cy.db_createMenuItem({ title: 'automated test', link: 'index.php?option=com_contact&view=featured', path: '?option=com_contact&view=featured' }))
.then(() => {
cy.visit('/');
cy.get('a:contains(automated test Contact)').click();
cy.get('a:contains(automated test)').click();
cy.contains('automated test contact 1');
cy.contains('automated test contact 2');

View File

@ -1,5 +1,39 @@
const mysql = require('mysql');
const postgres = require('postgres');
const fs = require('fs');
const fspath = require('path');
/**
* Deletes a folder with the given path recursive.
*
* @param {string} path The path
* @param {object} config The config
*
* @returns null
*/
function deleteFolder(path, config) {
fs.rmSync(`${config.env.cmsPath}/${path}`, { recursive: true, force: true });
return null;
}
/**
* Writes the given content to a file for the given path.
*
* @param {string} path The path
* @param {mixed} content The content
* @param {object} config The config
*
* @returns null
*/
function writeFile(path, content, config) {
fs.mkdirSync(fspath.dirname(`${config.env.cmsPath}/${path}`), { recursive: true, mode: 0o777 });
fs.chmod(fspath.dirname(`${config.env.cmsPath}/${path}`), 0o777);
fs.writeFileSync(`${config.env.cmsPath}/${path}`, content);
fs.chmod(`${config.env.cmsPath}/${path}`, 0o777);
return null;
}
// Rows cache of items which got inserted
let insertedItems = [];
@ -11,7 +45,10 @@ let insertedItems = [];
* @param {object} config The config
* @returns Promise
*/
function queryTestDB(query, config) {
function queryTestDB(joomlaQuery, config) {
// Substitute the joomla table prefix
let query = joomlaQuery.replaceAll('#__', config.env.db_prefix);
// Parse the table name
const tableNameOfInsert = query.match(/insert\s+into\s+(.*?)\s/i);
@ -33,13 +70,13 @@ function queryTestDB(query, config) {
database: config.env.db_name,
username: config.env.db_user,
password: config.env.db_password,
idle_timeout: 5,
max_lifetime: 60
idle_timeout: 1,
max_lifetime: 1,
});
// Postgres delivers the data direct as result of the insert query
if (insertItem) {
query += ' returning *'
query += ' returning *';
}
// Postgres needs double quotes
@ -67,12 +104,13 @@ function queryTestDB(query, config) {
host: config.env.db_host,
user: config.env.db_user,
password: config.env.db_password,
database: config.env.db_name
database: config.env.db_name,
});
connection.connect();
// Perform the query
connection.query(query, (error, results) => {
connection.end();
// Reject when an error
if (error && error.errno) {
return reject(error);
@ -84,18 +122,22 @@ function queryTestDB(query, config) {
}
// Resolve the result
resolve(results);
return resolve(results);
});
});
};
}
/**
* Deletes the inserted items from the database.
*
* @param {object} config The configuration
*
* @returns null
*/
function deleteInsertedItems(config) {
// Holds the promises for the deleted items
const promises = [];
// Loop over the cached items
insertedItems.forEach((item) => {
// When there is nothing to delete, ignore it
@ -104,15 +146,30 @@ function deleteInsertedItems(config) {
}
// Delete the items from the database
queryTestDB('DELETE FROM ' + item.table + ' WHERE ID IN (' + item.rows.join(',') + ')', config);
promises.push(queryTestDB(`DELETE FROM ${item.table} WHERE id IN (${item.rows.join(',')})`, config).then(() => {
// Cleanup some tables we do not have control over from inserted items
if (item.table === `${config.env.db_prefix}users`) {
promises.push(queryTestDB(`DELETE FROM #__user_usergroup_map WHERE user_id IN (${item.rows.join(',')})`, config));
promises.push(queryTestDB(`DELETE FROM #__user_profiles WHERE user_id IN (${item.rows.join(',')})`, config));
}
if (item.table === `${config.env.db_prefix}content`) {
promises.push(queryTestDB(`DELETE FROM #__content_frontpage WHERE content_id IN (${item.rows.join(',')})`, config));
promises.push(queryTestDB(`DELETE FROM #__workflow_associations WHERE item_id IN (${item.rows.join(',')}) AND extension = 'com_content.article'`, config));
}
if (item.table === `${config.env.db_prefix}modules`) {
promises.push(queryTestDB(`DELETE FROM #__modules_menu WHERE moduleid IN (${item.rows.join(',')})`, config));
}
}));
});
// Clear the cache
insertedItems = [];
// Cypress wants a return value
return null;
};
// Return the promise which waits for all delete queries
return Promise.all(promises);
}
/**
* Does the setup of the plugins.
@ -124,9 +181,11 @@ function deleteInsertedItems(config) {
*/
function setupPlugins(on, config) {
on('task', {
queryDB: (query) => queryTestDB(query.replace('#__', config.env.db_prefix), config),
queryDB: (query) => queryTestDB(query, config),
cleanupDB: () => deleteInsertedItems(config),
writeFile: ({ path, content }) => writeFile(path, content, config),
deleteFolder: (path) => deleteFolder(path, config),
});
};
}
module.exports = setupPlugins;

View File

@ -0,0 +1,136 @@
/**
* Imports commands fom files. The commands start with the folder name and an underscore as cypress doesn't support
* namespaces for commands.
*
* https://github.com/cypress-io/cypress/issues/6575
*/
import './commands/db';
import './commands/api';
Cypress.Commands.add('createContentCategory', (title) => {
cy.visit('administrator/index.php?option=com_categories&view=categories&extension=com_content');
cy.contains('h1', 'Articles: Categories').should('exist');
cy.clickToolbarButton('New');
cy.get('#jform_title').should('exist').type(title);
cy.clickToolbarButton('Save & Close');
// TODO Still need to implement this. Quick fix: we need to refactor the test
// $testCategory = [
// 'title' => $title,
// 'extension' => 'com_content',
// ];
// $this->seeInDatabase('categories', $testCategory);
});
Cypress.Commands.add('createField', (type, title) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article');
cy.clickToolbarButton('New');
cy.get('#jform_title').type(title);
cy.get('#jform_type').select(type);
cy.clickToolbarButton('Save & Close');
cy.get('#system-message-container').contains('Field saved').should('exist');
});
Cypress.Commands.add('trashField', (title, message) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Action');
cy.clickToolbarButton('Trash');
cy.get('#system-message-container').contains(message).should('exist');
});
Cypress.Commands.add('deleteField', (title, message) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article');
cy.searchForItem();
cy.get('.js-stools-btn-filter').click();
cy.intercept('index.php*').as('setTrashed');
cy.get('#filter_state').select('Trashed');
cy.wait('@setTrashed');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Empty trash');
cy.get('#system-message-container').contains(message).should('exist');
});
Cypress.Commands.add('createArticle', (articleDetails) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.intercept('index.php?option=com_content&view=article*').as('article_edit');
cy.clickToolbarButton('New');
cy.wait('@article_edit');
cy.get('#jform_title').clear().type(articleDetails.title);
cy.get('#jform_alias').clear().type(articleDetails.alias);
cy.intercept('index.php?option=com_content&view=articles').as('article_list');
cy.clickToolbarButton('Save & Close');
cy.wait('@article_list');
cy.get('#system-message-container').contains('Article saved.').should('exist');
});
Cypress.Commands.add('featureArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Action');
cy.intercept('index.php?option=com_content&view=articles').as('article_feature');
cy.clickToolbarButton('feature');
cy.wait('@article_feature');
cy.get('#system-message-container').contains('Article featured.').should('exist');
});
Cypress.Commands.add('setArticleAccessLevel', (title, accessLevel) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.searchForItem(title);
cy.checkAllResults();
cy.intercept('index.php?option=com_content&view=article*').as('article_access');
cy.get('a').contains(title).click();
cy.wait('@article_access');
cy.get('#jform_access').select(accessLevel);
cy.intercept('index.php?option=com_content&view=article*').as('article_list');
cy.clickToolbarButton('Save & Close');
cy.wait('@article_list');
cy.get('td').contains(accessLevel).should('exist');
});
Cypress.Commands.add('unPublishArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Action');
cy.intercept('index.php?option=com_content&view=articles').as('article_unpublish');
cy.clickToolbarButton('unpublish');
cy.wait('@article_unpublish');
});
Cypress.Commands.add('publishArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Action');
cy.intercept('index.php?option=com_content&view=articles').as('article_publish');
cy.clickToolbarButton('publish');
cy.wait('@article_publish');
});
Cypress.Commands.add('trashArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.searchForItem(title);
cy.checkAllResults();
cy.clickToolbarButton('Action');
cy.intercept('index.php?option=com_content&view=articles').as('article_trash');
cy.clickToolbarButton('trash');
cy.wait('@article_trash');
});
Cypress.Commands.add('deleteArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles');
cy.setFilter('published', 'Trashed');
cy.searchForItem(title);
cy.checkAllResults();
cy.on('window:confirm', () => true);
cy.intercept('index.php?option=com_content&view=articles').as('article_delete');
cy.clickToolbarButton('empty trash');
cy.wait('@article_delete');
cy.wait('@article_delete');
});

View File

@ -0,0 +1,39 @@
Cypress.Commands.add('api_get', (path) => cy.api_getBearerToken().then((token) => cy.request({ method: 'GET', url: `/api/index.php/v1${path}`, headers: { Authorization: `Bearer ${token}` } })));
Cypress.Commands.add('api_post', (path, body) => cy.api_getBearerToken().then((token) => cy.request({
method: 'POST', url: `/api/index.php/v1${path}`, body, headers: { Authorization: `Bearer ${token}` }, json: true,
})));
Cypress.Commands.add('api_patch', (path, body) => cy.api_getBearerToken().then((token) => cy.request({
method: 'PATCH', url: `/api/index.php/v1${path}`, body, headers: { Authorization: `Bearer ${token}` }, json: true,
})));
Cypress.Commands.add('api_delete', (path) => cy.api_getBearerToken().then((token) => cy.request({ method: 'DELETE', url: `/api/index.php/v1${path}`, headers: { Authorization: `Bearer ${token}` } })));
Cypress.Commands.add('api_getBearerToken', () => cy.task('queryDB', "SELECT id FROM #__users WHERE username = 'api'").then((user) => {
if (user.length > 0) {
return 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ==';
}
return cy.db_createUser({
id: 3,
name: 'API',
email: 'api@example.com',
username: 'api',
password: '123',
block: 0,
registerDate: '2000-01-01',
params: '{}',
group_id: 8,
}).then((id) => {
cy.task(
'queryDB',
'INSERT INTO #__user_profiles (user_id, profile_key, profile_value) VALUES '
+ `('${id}', 'joomlatoken.token', 'dOi2m1NRrnBHlhaWK/WWxh3B5tqq1INbdf4DhUmYTI4=')`,
);
return cy.task(
'queryDB',
`INSERT INTO #__user_profiles (user_id, profile_key, profile_value) VALUES ('${id}', 'joomlatoken.enabled', 1)`,
);
}).then(() => 'c2hhMjU2OjM6ZTJmMjJlYTNlNTU0NmM1MDJhYTIzYzMwN2MxYzAwZTQ5NzJhMWRmOTUyNjY5MTk2YjE5ODJmZWMwZTcxNzgwMQ==');
}));

View File

@ -1,3 +1,21 @@
/**
* Returns an insert query for the given database and fields.
*
* @param {string} table The DB table name
* @param {Object} values The values to insert
*
* @returns string
*/
function createInsertQuery(table, values) {
let query = `INSERT INTO #__${table} (\`${Object.keys(values).join('\`, \`')}\`) VALUES (:${Object.keys(values).join(',:')})`;
Object.keys(values).forEach((variable) => {
query = query.replace(`:${variable}`, `'${values[variable]}'`);
});
return query;
}
Cypress.Commands.add('db_createArticle', (article) => {
const defaultArticleOptions = {
title: 'test article',
@ -14,12 +32,52 @@ Cypress.Commands.add('db_createArticle', (article) => {
urls: '',
attribs: '',
metadesc: '',
metadata: ''
metadata: '',
};
return cy.task('queryDB', createInsertQuery('content', { ...defaultArticleOptions, ...article })).then((info) =>
cy.task('queryDB', "INSERT INTO #__content_frontpage (content_id, ordering) VALUES ('" + info.insertId + "', '1')").then(() => info)
);
return cy.task('queryDB', createInsertQuery('content', { ...defaultArticleOptions, ...article }))
.then(async (info) => {
await cy.task('queryDB', `INSERT INTO #__content_frontpage (content_id, ordering) VALUES ('${info.insertId}', '1')`);
await cy.task('queryDB', `INSERT INTO #__workflow_associations (item_id, stage_id, extension) VALUES (${info.insertId}, 1, 'com_content.article')`);
return info.insertId;
});
});
Cypress.Commands.add('db_createContact', (contact) => {
const defaultContactOptions = {
name: 'test contact',
alias: 'test-contact',
catid: 4,
published: 1,
access: 1,
language: '*',
created: '2023-01-01 20:00:00',
modified: '2023-01-01 20:00:00',
metadata: '',
metadesc: '',
params: '',
};
return cy.task('queryDB', createInsertQuery('contact_details', { ...defaultContactOptions, ...contact }))
.then(async (info) => info.insertId);
});
Cypress.Commands.add('db_createBanner', (banner) => {
const defaultBannerOptions = {
name: 'test banner',
alias: 'test-banner',
catid: 3,
state: 1,
language: '*',
created: '2023-01-01 20:00:00',
modified: '2023-01-01 20:00:00',
description: '',
custombannercode: '',
params: '',
};
return cy.task('queryDB', createInsertQuery('banners', { ...defaultBannerOptions, ...banner })).then(async (info) => info.insertId);
});
Cypress.Commands.add('db_createMenuItem', (menuItem) => {
@ -37,10 +95,10 @@ Cypress.Commands.add('db_createMenuItem', (menuItem) => {
access: 1,
language: '*',
params: '',
img: ''
img: '',
};
return cy.task('queryDB', createInsertQuery('menu', { ...defaultMenuItemOptions, ...menuItem }));
return cy.task('queryDB', createInsertQuery('menu', { ...defaultMenuItemOptions, ...menuItem })).then(async (info) => info.insertId);
});
Cypress.Commands.add('db_createModule', (module) => {
@ -52,15 +110,18 @@ Cypress.Commands.add('db_createModule', (module) => {
access: 1,
published: 1,
language: '*',
params: ''
params: '',
};
return cy.task('queryDB', createInsertQuery('modules', { ...defaultModuleOptions, ...module })).then((info) =>
cy.task('queryDB', "INSERT INTO #__modules_menu (moduleid, menuid) VALUES ('" + info.insertId + "', '0')").then(() => info)
);
return cy.task('queryDB', createInsertQuery('modules', { ...defaultModuleOptions, ...module }))
.then(async (info) => {
await cy.task('queryDB', `INSERT INTO #__modules_menu (moduleid, menuid) VALUES ('${info.insertId}', '0')`);
return info.insertId;
});
});
Cypress.Commands.add('db_createUser', (user) => {
Cypress.Commands.add('db_createUser', (userData) => {
const defaultUserOptions = {
name: 'test user',
username: 'test',
@ -68,67 +129,16 @@ Cypress.Commands.add('db_createUser', (user) => {
password: '098f6bcd4621d373cade4e832627b4f6', // Is the md5 of the word 'test'
block: 0,
registerDate: '2023-03-01 20:00:00',
params: ''
params: '',
};
user = { ...defaultUserOptions, ...user };
const user = { ...defaultUserOptions, ...userData };
const groupId = user.group_id ?? 2; // Default the group id to registered
delete user.group_id;
return cy.task('queryDB', createInsertQuery('users', user)).then((info) => {
cy.task('queryDB', "INSERT INTO #__user_usergroup_map (user_id, group_id) VALUES ('" + info.insertId + "', '" + groupId + "')").then(() => info);
return cy.task('queryDB', createInsertQuery('users', user)).then(async (info) => {
await cy.task('queryDB', `INSERT INTO #__user_usergroup_map (user_id, group_id) VALUES ('${info.insertId}', '${groupId}')`);
return info.insertId;
});
});
Cypress.Commands.add('db_createContact',(contact)=>{
const defaultContactOptions = {
name: 'test contact',
alias: 'test-contact',
catid:2,
address: '',
country: '',
access: 1,
language: '*',
created: '2023-01-01 20:00:00',
modified: '2023-01-01 20:00:00',
metadesc: '',
metadata: '',
featured: 1,
published: 1,
params: ''
};
return cy.task('queryDB', createInsertQuery('contact_details', { ...defaultContactOptions, ...contact })).then((info) => info.insertId);
});
/**
* Returns an insert query for the given database and fields.
*
* @param {string} table The DB table name
* @param {Object} values The values to insert
*
* @returns string
*/
function createInsertQuery(table, values) {
const query = 'INSERT INTO #__' + table + ' (\`' + Object.keys(values).join('\`, \`') + '\`) VALUES (:' + Object.keys(values).join(',:') + ')';
return prepareQuery(query, values);
}
/**
* Prepares the query by setting the values into the query. Similar to prepared statements but without any security consideration
* as we are in a testing environment.
*
* @param {string} query The query to prepare
* @param {Object} values The values to insert
*
* @returns string
*/
function prepareQuery(query, values) {
Object.keys(values).forEach((variable) => {
query = query.replace(':' + variable, "'" + values[variable] + "'");
});
return query;
}

View File

@ -0,0 +1,16 @@
import './commands';
import 'joomla-cypress';
const { registerCommands } = require('../../../node_modules/joomla-cypress/src/index.js');
before(() => {
registerCommands();
Cypress.on('uncaught:exception', (err, runnable) => {
console.log(`err :${err}`);
console.log(`runnable :${runnable}`);
return false;
});
});
afterEach(() => cy.task('cleanupDB'));

View File

@ -1,12 +0,0 @@
module.exports = {
plugins: [
'cypress',
],
env: {
mocha: true,
'cypress/globals': true,
},
rules: {
strict: 'off',
},
};

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -e
JOOMLA_BASE=$1
TEST_GROUP=$2
DB_ENGINE=$3
DB_HOST=$4
echo "[RUNNER] Prepare test environment"
# Switch to Joomla base directory
cd $JOOMLA_BASE
echo "[RUNNER] Copy files to test installation"
rsync -a --exclude-from=tests/cypress/exclude.txt $JOOMLA_BASE/ /tests/www/$TEST_GROUP/
chown -R www-data /tests/www/$TEST_GROUP/
echo "[RUNNER] Start Apache"
apache2ctl -D FOREGROUND &
echo "[RUNNER] Run cypress"
chmod +rwx /root
# cd /tests/www/$TEST_GROUP
#export CYPRESS_CACHE_FOLDER=/tests/www/$DB_ENGINE/.cache
#npx cypress install
#npx cypress verify
npx cypress run --browser=firefox --e2e --env db_type=$DB_ENGINE,db_host=$DB_HOST,db_password=joomla_ut,db_prefix="${TEST_GROUP}_" --config baseUrl=http://localhost/$TEST_GROUP,screenshotsFolder=$JOOMLA_BASE/tests/cypress/output/screenshots

View File

@ -1,19 +0,0 @@
.git
.github
build
dev
node_modules
.appveyor.yml
.drone.yml
.editorconfig
.gitignore
.php-cs-fixer.dist.php
build.xml
composer.json
composer.lock
crowdin.yml
package.json
package-lock.json
phpunit.xml.dist
phpunit-pgsql.xml.dist
selenium.log

View File

@ -1,138 +0,0 @@
/**
* Imports commands fom files. The commands start with the folder name and an underscore as cypress doesn't support
* namespaces for commands.
*
* https://github.com/cypress-io/cypress/issues/6575
*/
import './commands/db';
Cypress.Commands.add('createContentCategory', (title) => {
cy.visit('administrator/index.php?option=com_categories&view=categories&extension=com_content')
cy.contains('h1', 'Articles: Categories').should('exist')
cy.clickToolbarButton('New')
cy.get('#jform_title').should('exist').type(title)
cy.clickToolbarButton('Save & Close')
// TODO Still need to implement this. Quick fix: we need to refactor the test
//$testCategory = [
// 'title' => $title,
// 'extension' => 'com_content',
//];
//$this->seeInDatabase('categories', $testCategory);
})
Cypress.Commands.add('createField', (type, title) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article')
cy.clickToolbarButton('New')
cy.get('#jform_title').type(title)
cy.get('#jform_type').select(type)
cy.clickToolbarButton('Save & Close')
cy.get('#system-message-container').contains('Field saved').should('exist')
})
Cypress.Commands.add('trashField', (title, message) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Action')
cy.clickToolbarButton('Trash')
cy.get('#system-message-container').contains(message).should('exist')
})
Cypress.Commands.add('deleteField', (title, message) => {
cy.visit('administrator/index.php?option=com_fields&view=fields&context=com_content.article')
cy.searchForItem()
cy.get('.js-stools-btn-filter').click()
cy.intercept('index.php*').as('setTrashed')
cy.get('#filter_state').select('Trashed')
cy.wait('@setTrashed')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Empty trash')
cy.get('#system-message-container').contains(message).should('exist')
})
Cypress.Commands.add('createArticle', (articleDetails) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.intercept('index.php?option=com_content&view=article*').as('article_edit')
cy.clickToolbarButton('New')
cy.wait('@article_edit')
cy.get('#jform_title').clear().type(articleDetails.title)
cy.get('#jform_alias').clear().type(articleDetails.alias)
cy.intercept('index.php?option=com_content&view=articles').as('article_list')
cy.clickToolbarButton('Save & Close')
cy.wait('@article_list')
cy.get('#system-message-container').contains('Article saved.').should('exist')
})
Cypress.Commands.add('featureArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Action')
cy.intercept('index.php?option=com_content&view=articles').as('article_feature')
cy.clickToolbarButton('feature')
cy.wait('@article_feature')
cy.get('#system-message-container').contains('Article featured.').should('exist')
})
Cypress.Commands.add('setArticleAccessLevel', (title, accessLevel) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.searchForItem(title)
cy.checkAllResults()
cy.intercept('index.php?option=com_content&view=article*').as('article_access')
cy.get('a').contains(title).click()
cy.wait('@article_access')
cy.get('#jform_access').select(accessLevel)
cy.intercept('index.php?option=com_content&view=article*').as('article_list')
cy.clickToolbarButton('Save & Close')
cy.wait('@article_list')
cy.get('td').contains(accessLevel).should('exist')
})
Cypress.Commands.add('unPublishArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Action')
cy.intercept('index.php?option=com_content&view=articles').as('article_unpublish')
cy.clickToolbarButton('unpublish')
cy.wait('@article_unpublish')
})
Cypress.Commands.add('publishArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Action')
cy.intercept('index.php?option=com_content&view=articles').as('article_publish')
cy.clickToolbarButton('publish')
cy.wait('@article_publish')
})
Cypress.Commands.add('trashArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.searchForItem(title)
cy.checkAllResults()
cy.clickToolbarButton('Action')
cy.intercept('index.php?option=com_content&view=articles').as('article_trash')
cy.clickToolbarButton('trash')
cy.wait('@article_trash')
})
Cypress.Commands.add('deleteArticle', (title) => {
cy.visit('administrator/index.php?option=com_content&view=articles')
cy.setFilter('published', 'Trashed')
cy.searchForItem(title)
cy.checkAllResults()
cy.on("window:confirm", (s) => {
return true;
});
cy.intercept('index.php?option=com_content&view=articles').as('article_delete')
cy.clickToolbarButton('empty trash');
cy.wait('@article_delete')
cy.wait('@article_delete')
})

Some files were not shown because too many files have changed in this diff Show More