├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── src └── DockerChrome.php └── tests ├── _bootstrap.php ├── _data └── dump.sql ├── _support ├── Helper │ └── Unit.php ├── UnitTester.php └── _generated │ └── UnitTesterActions.php ├── unit.suite.yml └── unit ├── DockerChromeTest.php └── _bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Composer template 3 | composer.phar 4 | /vendor/ 5 | /src/docker-compose.yml 6 | /tests/_output/* 7 | 8 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 9 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 10 | composer.lock 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.0' 4 | - '7.1' 5 | 6 | install: 7 | - composer install 8 | 9 | script: 10 | - vendor/bin/codecept run unit --coverage --coverage-xml --coverage-html 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 neusta GmbH | Ein team neusta Unternehmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![team neusta][logo] 2 | 3 | # Docker Selenium Chrome for Codeception Extension # 4 | 5 | [![Build Status](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/badges/build.png?b=master)](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/build-status/master) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/?branch=master) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/teamneusta/codeception-docker-chrome/?branch=master) 8 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/ab3e62a0-03dd-4f4b-8b82-39dd3d942f97/mini.png)](https://insight.sensiolabs.com/projects/ab3e62a0-03dd-4f4b-8b82-39dd3d942f97) 9 | [![Latest Stable Version](https://img.shields.io/packagist/v/teamneusta/codeception-docker-chrome.svg?label=stable)](https://packagist.org/packages/teamneusta/codeception-docker-chrome) 10 | [![Latest Stable Version](https://img.shields.io/packagist/l/teamneusta/codeception-docker-chrome.svg?label=stable)](https://packagist.org/packages/teamneusta/codeception-docker-chrome) 11 | 12 | 13 | ### What's Docker Selenium Chrome for Codeception? ### 14 | 15 | **Docker Selenium Chrome for Codeception** is a extension to integrate automatic selenium with chrome in your codeception tests. 16 | 17 | ### Minimum Requirements ### 18 | 19 | - Unix System 20 | - [Codeception](http://codeception.com/) 2.2.0 21 | - PHP 7.0 > 22 | - [docker](https://docs.docker.com/engine/installation/linux/) 1.12.0 23 | - [docker-compose](https://docs.docker.com/compose/install/) 1.11.0 24 | 25 | ### Installing ### 26 | 27 | Simply add the following dependency to your project’s composer.json file: 28 | 29 | ```json 30 | "require": { 31 | "teamneusta/codeception-docker-chrome": "^1.0" 32 | } 33 | ``` 34 | Finally you can use **Docker Selenium Chrome for Codeception** in your codeception.yml 35 | 36 | ```yaml 37 | extensions: 38 | enabled: 39 | - Codeception\Extension\DockerChrome 40 | config: 41 | Codeception\Extension\DockerChrome: 42 | suites: ['acceptance'] 43 | debug: true 44 | extra_hosts: ['foo.loc:192.168.0.123'] 45 | ``` 46 | 47 | #### Available options #### 48 | 49 | ##### Basic ##### 50 | 51 | - `path: {path}` 52 | - Full path to the docker-compose binary. 53 | - Default: `/usr/local/bin/docker-compose` 54 | - `port: {port}` 55 | - Webdriver port to start chrome with. 56 | - Default: `4444` 57 | - `debug: {true|false}` 58 | - Display debug output 59 | - Default: `false` 60 | - `extra_hosts: ['domain:ip', 'domain:ip']` 61 | - set extra hosts for docker container to connect to local environment over network (not 127.0.0.1) 62 | - Default: `null` 63 | - `suites: {array|string}` 64 | - If omitted, Chrome is started for all suites. 65 | - Specify an array of suites or a single suite name. 66 | - If you're using an environment (`--env`), Codeception appends the 67 | environment name to the suite name in brackets. You need to include 68 | each suite/environment combination separately in the array. 69 | - `suites: ['acceptance', 'acceptance (staging)', 'acceptance (prod)']` 70 | 71 | ##### Proxy Support ##### 72 | 73 | - `http_proxy: {address:port}` 74 | - Sets the http proxy server. 75 | - `https_proxy: {address:port}` 76 | - Sets the https proxy server. 77 | - `no_proxy: address1.local,adress2.de` 78 | - Sets the no proxy for specific domains. 79 | 80 | ##### Registry Support ##### 81 | 82 | - `private-registry: {address:port}` 83 | 84 | #### Suite configuration example #### 85 | 86 | **this configuration override the codeception.yml configuration** 87 | 88 | ```yaml 89 | class_name: AcceptanceTester 90 | modules: 91 | enabled: 92 | - WebDriver: 93 | port: 5555 94 | browser: chrome 95 | url: https://www.example.de/ 96 | capabilities: 97 | proxyType: 'manual' 98 | httpProxy: 'http-proxy.example.de:3128' 99 | sslProxy: 'https-proxy.example.de:3128' 100 | noProxy: 'address1.local,adress2.de' 101 | ``` 102 | 103 | ### Usage ### 104 | 105 | Once installed and enabled, running your tests with `php codecept run` will 106 | automatically start the chrome and wait for it to be accessible before 107 | proceeding with the tests. 108 | 109 | **be patient on first start. It could take a while** 110 | 111 | ```bash 112 | Docker server now accessible 113 | ``` 114 | 115 | Once the tests are complete, Docker Server will be shut down. 116 | 117 | ```bash 118 | Stopping Docker Server 119 | ``` 120 | 121 | 122 | [logo]: https://www.team-neusta.de/typo3temp/pics/t_0d7f868b56.png "team neusta logo" 123 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | support: tests/_support 7 | envs: tests/_envs 8 | settings: 9 | bootstrap: _bootstrap.php 10 | colors: true 11 | memory_limit: 1024M 12 | coverage: 13 | enabled: true 14 | include: 15 | - src/* 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teamneusta/codeception-docker-chrome", 3 | "description": "Codeception extension to start automatically an docker chrome instance", 4 | "license": "MIT", 5 | "type": "library", 6 | "minimum-stability": "stable", 7 | "keywords": ["codeception", "chrome", "docker"], 8 | "authors": [ 9 | { 10 | "name": "Benjamin Kluge", 11 | "email": "b.kluge@neusta.de" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.0.0", 16 | "codeception/codeception": "^2.2", 17 | "monolog/monolog": "*", 18 | "symfony/process": "*" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^5.7", 22 | "mikey179/vfsStream": "^1.6" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Codeception\\Extension\\": "src/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DockerChrome.php: -------------------------------------------------------------------------------- 1 | 'moduleInit', 28 | ]; 29 | 30 | /** 31 | * process 32 | * 33 | * @var \Symfony\Component\Process\Process 34 | */ 35 | private $process; 36 | 37 | /** 38 | * dockerStarted 39 | * 40 | * @var bool 41 | */ 42 | private $dockerStarted = false; 43 | 44 | /** 45 | * dockerComposePath 46 | * 47 | * @var string 48 | */ 49 | private $dockerComposePath; 50 | 51 | /** 52 | * DockerChrome constructor. 53 | * 54 | * @param array $config 55 | * @param array $options 56 | * @param \Symfony\Component\Process\Process|null $process 57 | * @param string $defaultDockerComposePath 58 | */ 59 | public function __construct( 60 | array $config, 61 | array $options, 62 | Process $process = null, 63 | string $defaultDockerComposePath = '/usr/local/bin/docker-compose' 64 | ) { 65 | 66 | // Set default https proxy 67 | if (!isset($options['silent'])) { 68 | $options['silent'] = false; 69 | } 70 | 71 | $this->dockerComposePath = $defaultDockerComposePath; 72 | 73 | parent::__construct($config, $options); 74 | 75 | $this->initDefaultConfig(); 76 | $command = $this->getCommand(); 77 | $this->process = $process ?: new Process($command, realpath(__DIR__), null, null, 3600); 78 | } 79 | 80 | /** 81 | * initDefaultConfig 82 | * 83 | * @return void 84 | * @throws \Codeception\Exception\ExtensionException 85 | */ 86 | protected function initDefaultConfig() 87 | { 88 | $this->config['path'] = $this->config['path'] ?? $this->dockerComposePath; 89 | 90 | // Set default WebDriver port 91 | $this->config['port'] = $this->config['port'] ?? 4444; 92 | 93 | // Set default debug mode 94 | $this->config['debug'] = $this->config['debug'] ?? false; 95 | 96 | // Set default extra_hosts mode 97 | $this->config['extra_hosts'] = $this->config['extra_hosts'] ?? false; 98 | 99 | // Set default http proxy 100 | $this->config['http_proxy'] = $this->config['http_proxy'] ?? ''; 101 | 102 | // Set default https proxy 103 | $this->config['https_proxy'] = $this->config['https_proxy'] ?? ''; 104 | 105 | // Set default https proxy 106 | $this->config['no_proxy'] = $this->config['no_proxy'] ?? ''; 107 | 108 | if (!file_exists($this->config['path'])) { 109 | throw new ExtensionException($this, "File not found: {$this->config['path']}."); 110 | } 111 | } 112 | 113 | /** 114 | * getCommand 115 | * 116 | * @return string 117 | */ 118 | private function getCommand(): string 119 | { 120 | return 'exec ' . escapeshellarg(realpath($this->config['path'])) . ' up'; 121 | } 122 | 123 | /** 124 | * getConfig 125 | * 126 | * @return array 127 | */ 128 | public function getConfig(): array 129 | { 130 | return $this->config; 131 | } 132 | 133 | /** 134 | * 135 | */ 136 | public function __destruct() 137 | { 138 | $this->stopServer(); 139 | } 140 | 141 | /** 142 | * stopServer 143 | * 144 | * @return void 145 | */ 146 | private function stopServer() 147 | { 148 | if ($this->process && $this->process->isRunning()) { 149 | $this->write('Stopping Docker Server'); 150 | 151 | $this->process->signal(2); 152 | $this->process->wait(); 153 | } 154 | } 155 | 156 | /** 157 | * moduleInit 158 | * 159 | * @param \Codeception\Event\SuiteEvent $e 160 | * @return void 161 | */ 162 | public function moduleInit(\Codeception\Event\SuiteEvent $e) 163 | { 164 | if (!$this->suiteAllowed($e)) { 165 | return; 166 | } 167 | 168 | $this->overrideWithModuleConfig($e); 169 | $this->generateYaml(); 170 | $this->startServer(); 171 | } 172 | 173 | /** 174 | * suiteAllowed 175 | * 176 | * @param \Codeception\Event\SuiteEvent $e 177 | * @return bool 178 | */ 179 | protected function suiteAllowed(\Codeception\Event\SuiteEvent $e): bool 180 | { 181 | $allowed = true; 182 | if (isset($this->config['suites'])) { 183 | $suites = (array)$this->config['suites']; 184 | 185 | $e->getSuite()->getBaseName(); 186 | 187 | if (!in_array($e->getSuite()->getBaseName(), $suites) 188 | && !in_array($e->getSuite()->getName(), $suites) 189 | ) { 190 | $allowed = false; 191 | } 192 | } 193 | 194 | return $allowed; 195 | } 196 | 197 | /** 198 | * overrideWithModuleConfig 199 | * 200 | * @param \Codeception\Event\SuiteEvent $e 201 | * @return void 202 | */ 203 | protected function overrideWithModuleConfig(\Codeception\Event\SuiteEvent $e) 204 | { 205 | $modules = array_filter($e->getSettings()['modules']['enabled']); 206 | foreach ($modules as $module) { 207 | if (is_array($module)) { 208 | $moduleSettings = current($module); 209 | $this->config['port'] = $moduleSettings['port'] ?? $this->config['port']; 210 | $this->config['http_proxy'] = $moduleSettings['capabilities']['httpProxy'] ?? $this->config['http_proxy']; 211 | $this->config['https_proxy'] = $moduleSettings['capabilities']['sslProxy'] ?? $this->config['https_proxy']; 212 | $this->config['no_proxy'] = $moduleSettings['capabilities']['noProxy'] ?? $this->config['no_proxy']; 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * generateYaml 219 | * 220 | * @return void 221 | */ 222 | protected function generateYaml() 223 | { 224 | $environment = []; 225 | 226 | if (!empty($this->config['http_proxy'])) { 227 | $environment[] = 'http_proxy=' . $this->config['http_proxy']; 228 | } 229 | if (!empty($this->config['https_proxy'])) { 230 | $environment[] = 'https_proxy=' . $this->config['https_proxy']; 231 | } 232 | if (!empty($this->config['no_proxy'])) { 233 | $environment[] = 'no_proxy=' . $this->config['no_proxy']; 234 | } 235 | 236 | $registryPrefix = !empty($this->config['private-registry']) ? $this->config['private-registry'] . '/' : ''; 237 | 238 | $dockerYaml = [ 239 | 'hub' => [ 240 | 'image' => $registryPrefix . 'selenium/hub', 241 | 'ports' => [$this->config['port'] . ':4444'], 242 | 'environment' => $environment 243 | ], 244 | 'chrome' => [ 245 | 'volumes' => ['/dev/shm:/dev/shm'], 246 | 'image' => $registryPrefix . 'selenium/node-chrome', 247 | 'links' => ['hub'], 248 | 'environment' => $environment 249 | ] 250 | ]; 251 | 252 | if ($this->config['extra_hosts']) { 253 | $dockerYaml['hub']['extra_hosts'] = $this->config['extra_hosts']; 254 | $dockerYaml['chrome']['extra_hosts'] = $this->config['extra_hosts']; 255 | } 256 | 257 | file_put_contents(__DIR__ . '/docker-compose.yml', Yaml::dump($dockerYaml)); 258 | } 259 | 260 | /** 261 | * startServer 262 | * 263 | * @return void 264 | * @throws \Codeception\Exception\ExtensionException 265 | */ 266 | private function startServer() 267 | { 268 | if ($this->config['debug']) { 269 | $this->writeln(['Generated Docker Command:', $this->process->getCommandLine()]); 270 | } 271 | $this->writeln('Starting Docker Server'); 272 | $this->process->start(function($type, $buffer) { 273 | if (strpos($buffer, 'Registered a node') !== false) { 274 | $this->dockerStarted = true; 275 | } 276 | }); 277 | 278 | // wait until docker is finished to start 279 | while ($this->process->isRunning() && !$this->dockerStarted) { 280 | } 281 | 282 | if (!$this->process->isRunning()) { 283 | throw new ExtensionException($this, 'Failed to start Docker server.'); 284 | } 285 | $this->writeln(['', 'Docker server now accessible']); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | assertEquals($element->getChildrenCount(), 5); 30 | * ``` 31 | * 32 | * Floating-point example: 33 | * ```php 34 | * assertEquals($calculator->add(0.1, 0.2), 0.3, 'Calculator should add the two numbers correctly.', 0.01); 36 | * ``` 37 | * 38 | * @param $expected 39 | * @param $actual 40 | * @param string $message 41 | * @param float $delta 42 | * @see \Codeception\Module\Asserts::assertEquals() 43 | */ 44 | public function assertEquals($expected, $actual, $message = null, $delta = null) { 45 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEquals', func_get_args())); 46 | } 47 | 48 | 49 | /** 50 | * [!] Method is generated. Documentation taken from corresponding module. 51 | * 52 | * Checks that two variables are not equal. If you're comparing floating-point values, 53 | * you can specify the optional "delta" parameter which dictates how great of a precision 54 | * error are you willing to tolerate in order to consider the two values not equal. 55 | * 56 | * Regular example: 57 | * ```php 58 | * assertNotEquals($element->getChildrenCount(), 0); 60 | * ``` 61 | * 62 | * Floating-point example: 63 | * ```php 64 | * assertNotEquals($calculator->add(0.1, 0.2), 0.4, 'Calculator should add the two numbers correctly.', 0.01); 66 | * ``` 67 | * 68 | * @param $expected 69 | * @param $actual 70 | * @param string $message 71 | * @param float $delta 72 | * @see \Codeception\Module\Asserts::assertNotEquals() 73 | */ 74 | public function assertNotEquals($expected, $actual, $message = null, $delta = null) { 75 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEquals', func_get_args())); 76 | } 77 | 78 | 79 | /** 80 | * [!] Method is generated. Documentation taken from corresponding module. 81 | * 82 | * Checks that two variables are same 83 | * 84 | * @param $expected 85 | * @param $actual 86 | * @param string $message 87 | * @see \Codeception\Module\Asserts::assertSame() 88 | */ 89 | public function assertSame($expected, $actual, $message = null) { 90 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertSame', func_get_args())); 91 | } 92 | 93 | 94 | /** 95 | * [!] Method is generated. Documentation taken from corresponding module. 96 | * 97 | * Checks that two variables are not same 98 | * 99 | * @param $expected 100 | * @param $actual 101 | * @param string $message 102 | * @see \Codeception\Module\Asserts::assertNotSame() 103 | */ 104 | public function assertNotSame($expected, $actual, $message = null) { 105 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotSame', func_get_args())); 106 | } 107 | 108 | 109 | /** 110 | * [!] Method is generated. Documentation taken from corresponding module. 111 | * 112 | * Checks that actual is greater than expected 113 | * 114 | * @param $expected 115 | * @param $actual 116 | * @param string $message 117 | * @see \Codeception\Module\Asserts::assertGreaterThan() 118 | */ 119 | public function assertGreaterThan($expected, $actual, $message = null) { 120 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThan', func_get_args())); 121 | } 122 | 123 | 124 | /** 125 | * [!] Method is generated. Documentation taken from corresponding module. 126 | * 127 | * Checks that actual is greater or equal than expected 128 | * 129 | * @param $expected 130 | * @param $actual 131 | * @param string $message 132 | * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual() 133 | */ 134 | public function assertGreaterThanOrEqual($expected, $actual, $message = null) { 135 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThanOrEqual', func_get_args())); 136 | } 137 | 138 | 139 | /** 140 | * [!] Method is generated. Documentation taken from corresponding module. 141 | * 142 | * Checks that actual is less than expected 143 | * 144 | * @param $expected 145 | * @param $actual 146 | * @param string $message 147 | * @see \Codeception\Module\Asserts::assertLessThan() 148 | */ 149 | public function assertLessThan($expected, $actual, $message = null) { 150 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThan', func_get_args())); 151 | } 152 | 153 | 154 | /** 155 | * [!] Method is generated. Documentation taken from corresponding module. 156 | * 157 | * Checks that actual is less or equal than expected 158 | * 159 | * @param $expected 160 | * @param $actual 161 | * @param string $message 162 | * @see \Codeception\Module\Asserts::assertLessThanOrEqual() 163 | */ 164 | public function assertLessThanOrEqual($expected, $actual, $message = null) { 165 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThanOrEqual', func_get_args())); 166 | } 167 | 168 | 169 | /** 170 | * [!] Method is generated. Documentation taken from corresponding module. 171 | * 172 | * Checks that haystack contains needle 173 | * 174 | * @param $needle 175 | * @param $haystack 176 | * @param string $message 177 | * @see \Codeception\Module\Asserts::assertContains() 178 | */ 179 | public function assertContains($needle, $haystack, $message = null) { 180 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertContains', func_get_args())); 181 | } 182 | 183 | 184 | /** 185 | * [!] Method is generated. Documentation taken from corresponding module. 186 | * 187 | * Checks that haystack doesn't contain needle. 188 | * 189 | * @param $needle 190 | * @param $haystack 191 | * @param string $message 192 | * @see \Codeception\Module\Asserts::assertNotContains() 193 | */ 194 | public function assertNotContains($needle, $haystack, $message = null) { 195 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotContains', func_get_args())); 196 | } 197 | 198 | 199 | /** 200 | * [!] Method is generated. Documentation taken from corresponding module. 201 | * 202 | * Checks that string match with pattern 203 | * 204 | * @param string $pattern 205 | * @param string $string 206 | * @param string $message 207 | * @see \Codeception\Module\Asserts::assertRegExp() 208 | */ 209 | public function assertRegExp($pattern, $string, $message = null) { 210 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertRegExp', func_get_args())); 211 | } 212 | 213 | 214 | /** 215 | * [!] Method is generated. Documentation taken from corresponding module. 216 | * 217 | * Checks that string not match with pattern 218 | * 219 | * @param string $pattern 220 | * @param string $string 221 | * @param string $message 222 | * @see \Codeception\Module\Asserts::assertNotRegExp() 223 | */ 224 | public function assertNotRegExp($pattern, $string, $message = null) { 225 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotRegExp', func_get_args())); 226 | } 227 | 228 | 229 | /** 230 | * [!] Method is generated. Documentation taken from corresponding module. 231 | * 232 | * Checks that variable is empty. 233 | * 234 | * @param $actual 235 | * @param string $message 236 | * @see \Codeception\Module\Asserts::assertEmpty() 237 | */ 238 | public function assertEmpty($actual, $message = null) { 239 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEmpty', func_get_args())); 240 | } 241 | 242 | 243 | /** 244 | * [!] Method is generated. Documentation taken from corresponding module. 245 | * 246 | * Checks that variable is not empty. 247 | * 248 | * @param $actual 249 | * @param string $message 250 | * @see \Codeception\Module\Asserts::assertNotEmpty() 251 | */ 252 | public function assertNotEmpty($actual, $message = null) { 253 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEmpty', func_get_args())); 254 | } 255 | 256 | 257 | /** 258 | * [!] Method is generated. Documentation taken from corresponding module. 259 | * 260 | * Checks that variable is NULL 261 | * 262 | * @param $actual 263 | * @param string $message 264 | * @see \Codeception\Module\Asserts::assertNull() 265 | */ 266 | public function assertNull($actual, $message = null) { 267 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNull', func_get_args())); 268 | } 269 | 270 | 271 | /** 272 | * [!] Method is generated. Documentation taken from corresponding module. 273 | * 274 | * Checks that variable is not NULL 275 | * 276 | * @param $actual 277 | * @param string $message 278 | * @see \Codeception\Module\Asserts::assertNotNull() 279 | */ 280 | public function assertNotNull($actual, $message = null) { 281 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotNull', func_get_args())); 282 | } 283 | 284 | 285 | /** 286 | * [!] Method is generated. Documentation taken from corresponding module. 287 | * 288 | * Checks that condition is positive. 289 | * 290 | * @param $condition 291 | * @param string $message 292 | * @see \Codeception\Module\Asserts::assertTrue() 293 | */ 294 | public function assertTrue($condition, $message = null) { 295 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertTrue', func_get_args())); 296 | } 297 | 298 | 299 | /** 300 | * [!] Method is generated. Documentation taken from corresponding module. 301 | * 302 | * Checks that condition is negative. 303 | * 304 | * @param $condition 305 | * @param string $message 306 | * @see \Codeception\Module\Asserts::assertFalse() 307 | */ 308 | public function assertFalse($condition, $message = null) { 309 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFalse', func_get_args())); 310 | } 311 | 312 | 313 | /** 314 | * [!] Method is generated. Documentation taken from corresponding module. 315 | * 316 | * Checks if file exists 317 | * 318 | * @param string $filename 319 | * @param string $message 320 | * @see \Codeception\Module\Asserts::assertFileExists() 321 | */ 322 | public function assertFileExists($filename, $message = null) { 323 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileExists', func_get_args())); 324 | } 325 | 326 | 327 | /** 328 | * [!] Method is generated. Documentation taken from corresponding module. 329 | * 330 | * Checks if file doesn't exist 331 | * 332 | * @param string $filename 333 | * @param string $message 334 | * @see \Codeception\Module\Asserts::assertFileNotExists() 335 | */ 336 | public function assertFileNotExists($filename, $message = null) { 337 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileNotExists', func_get_args())); 338 | } 339 | 340 | 341 | /** 342 | * [!] Method is generated. Documentation taken from corresponding module. 343 | * 344 | * @param $expected 345 | * @param $actual 346 | * @param $description 347 | * @see \Codeception\Module\Asserts::assertGreaterOrEquals() 348 | */ 349 | public function assertGreaterOrEquals($expected, $actual, $description = null) { 350 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterOrEquals', func_get_args())); 351 | } 352 | 353 | 354 | /** 355 | * [!] Method is generated. Documentation taken from corresponding module. 356 | * 357 | * @param $expected 358 | * @param $actual 359 | * @param $description 360 | * @see \Codeception\Module\Asserts::assertLessOrEquals() 361 | */ 362 | public function assertLessOrEquals($expected, $actual, $description = null) { 363 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessOrEquals', func_get_args())); 364 | } 365 | 366 | 367 | /** 368 | * [!] Method is generated. Documentation taken from corresponding module. 369 | * 370 | * @param $actual 371 | * @param $description 372 | * @see \Codeception\Module\Asserts::assertIsEmpty() 373 | */ 374 | public function assertIsEmpty($actual, $description = null) { 375 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsEmpty', func_get_args())); 376 | } 377 | 378 | 379 | /** 380 | * [!] Method is generated. Documentation taken from corresponding module. 381 | * 382 | * @param $key 383 | * @param $actual 384 | * @param $description 385 | * @see \Codeception\Module\Asserts::assertArrayHasKey() 386 | */ 387 | public function assertArrayHasKey($key, $actual, $description = null) { 388 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayHasKey', func_get_args())); 389 | } 390 | 391 | 392 | /** 393 | * [!] Method is generated. Documentation taken from corresponding module. 394 | * 395 | * @param $key 396 | * @param $actual 397 | * @param $description 398 | * @see \Codeception\Module\Asserts::assertArrayNotHasKey() 399 | */ 400 | public function assertArrayNotHasKey($key, $actual, $description = null) { 401 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayNotHasKey', func_get_args())); 402 | } 403 | 404 | 405 | /** 406 | * [!] Method is generated. Documentation taken from corresponding module. 407 | * 408 | * @param $expectedCount 409 | * @param $actual 410 | * @param $description 411 | * @see \Codeception\Module\Asserts::assertCount() 412 | */ 413 | public function assertCount($expectedCount, $actual, $description = null) { 414 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertCount', func_get_args())); 415 | } 416 | 417 | 418 | /** 419 | * [!] Method is generated. Documentation taken from corresponding module. 420 | * 421 | * @param $class 422 | * @param $actual 423 | * @param $description 424 | * @see \Codeception\Module\Asserts::assertInstanceOf() 425 | */ 426 | public function assertInstanceOf($class, $actual, $description = null) { 427 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInstanceOf', func_get_args())); 428 | } 429 | 430 | 431 | /** 432 | * [!] Method is generated. Documentation taken from corresponding module. 433 | * 434 | * @param $class 435 | * @param $actual 436 | * @param $description 437 | * @see \Codeception\Module\Asserts::assertNotInstanceOf() 438 | */ 439 | public function assertNotInstanceOf($class, $actual, $description = null) { 440 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotInstanceOf', func_get_args())); 441 | } 442 | 443 | 444 | /** 445 | * [!] Method is generated. Documentation taken from corresponding module. 446 | * 447 | * @param $type 448 | * @param $actual 449 | * @param $description 450 | * @see \Codeception\Module\Asserts::assertInternalType() 451 | */ 452 | public function assertInternalType($type, $actual, $description = null) { 453 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInternalType', func_get_args())); 454 | } 455 | 456 | 457 | /** 458 | * [!] Method is generated. Documentation taken from corresponding module. 459 | * 460 | * Fails the test with message. 461 | * 462 | * @param $message 463 | * @see \Codeception\Module\Asserts::fail() 464 | */ 465 | public function fail($message) { 466 | return $this->getScenario()->runStep(new \Codeception\Step\Action('fail', func_get_args())); 467 | } 468 | 469 | 470 | /** 471 | * [!] Method is generated. Documentation taken from corresponding module. 472 | * 473 | * Handles and checks exception called inside callback function. 474 | * Either exception class name or exception instance should be provided. 475 | * 476 | * ```php 477 | * expectException(MyException::class, function() { 479 | * $this->doSomethingBad(); 480 | * }); 481 | * 482 | * $I->expectException(new MyException(), function() { 483 | * $this->doSomethingBad(); 484 | * }); 485 | * ``` 486 | * If you want to check message or exception code, you can pass them with exception instance: 487 | * ```php 488 | * expectException(new MyException("Don't do bad things"), function() { 491 | * $this->doSomethingBad(); 492 | * }); 493 | * ``` 494 | * 495 | * @param $exception string or \Exception 496 | * @param $callback 497 | * @see \Codeception\Module\Asserts::expectException() 498 | */ 499 | public function expectException($exception, $callback) { 500 | return $this->getScenario()->runStep(new \Codeception\Step\Action('expectException', func_get_args())); 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit (internal) tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit -------------------------------------------------------------------------------- /tests/unit/DockerChromeTest.php: -------------------------------------------------------------------------------- 1 | [ 56 | 'config' => ['path' => __FILE__], 57 | 'expectedConfig' => ['path' => __FILE__] 58 | ], 59 | 'debug should set to false by default' => [ 60 | 'config' => [], 61 | 'expectedConfig' => ['debug' => false] 62 | ], 63 | 'debug should set to true by config' => [ 64 | 'config' => ['debug' => true], 65 | 'expectedConfig' => ['debug' => true] 66 | ], 67 | 'http_proxy should set to empty string by default' => [ 68 | 'config' => [], 69 | 'expectedConfig' => ['http_proxy' => false] 70 | ], 71 | 'http_proxy should set to specific string by config' => [ 72 | 'config' => ['http_proxy' => 'http-proxy.sample.de:3128'], 73 | 'expectedConfig' => ['http_proxy' => 'http-proxy.sample.de:3128'] 74 | ], 75 | 'https_proxy should set to empty string by default' => [ 76 | 'config' => [], 77 | 'expectedConfig' => ['https_proxy' => false] 78 | ], 79 | 'https_proxy should set to specific string by config' => [ 80 | 'config' => ['https_proxy' => 'https-proxy.sample.de:3128'], 81 | 'expectedConfig' => ['https_proxy' => 'https-proxy.sample.de:3128'] 82 | ] 83 | ]; 84 | } 85 | 86 | /** 87 | * testInitConfigDefaults 88 | * 89 | * @dataProvider configDefaultsDataProvider 90 | * @param array $config 91 | * @param array $expectedConfig 92 | * @return void 93 | */ 94 | public function testInitConfigDefaults(array $config, array $expectedConfig) 95 | { 96 | $this->dockerChrome = new Codeception\Extension\DockerChrome(array_merge(['path' => vfsStream::url('docker/usr/local/bin/docker-composes')], $config), [], $this->processProphecy->reveal()); 97 | $this->assertArraySubset($expectedConfig, $this->dockerChrome->getConfig()); 98 | } 99 | 100 | /** 101 | * testInitConfigPathDefaultShouldThrowExceptionIfPathNotExist 102 | * 103 | * @return void 104 | */ 105 | public function testInitConfigPathDefaultShouldThrowExceptionIfPathNotExist() 106 | { 107 | $this->expectException(ExtensionException::class); 108 | $this->expectExceptionMessage('File not found: /some/missing/path'); 109 | $this->dockerChrome = new Codeception\Extension\DockerChrome(['path' => '/some/missing/path'], [], $this->processProphecy->reveal()); 110 | } 111 | 112 | /** 113 | * testInitConfigPathDefaultShouldThrowExceptionIfDefaultPathNotExist 114 | * 115 | * @return void 116 | */ 117 | public function testInitConfigPathDefaultShouldThrowExceptionIfDefaultPathNotExist() 118 | { 119 | $this->expectException(ExtensionException::class); 120 | $this->expectExceptionMessage('File not found: /some/missing/path'); 121 | $this->dockerChrome = new Codeception\Extension\DockerChrome([], [], $this->processProphecy->reveal(), '/some/missing/path'); 122 | } 123 | 124 | /** 125 | * testModuleInitShouldNotCreateDockerComposeYamlIfSuiteAreNotAllowed 126 | * 127 | * @return void 128 | */ 129 | public function testModuleInitShouldNotCreateDockerComposeYamlIfSuiteAreNotAllowed() 130 | { 131 | $this->dockerChrome = new Codeception\Extension\DockerChrome(['suites' => 'acceptance', 'path' => vfsStream::url('docker/usr/local/bin/docker-composes')], [], $this->processProphecy->reveal()); 132 | $this->suiteEventProphecy->getSettings()->willReturn([ 133 | 'modules' => [ 134 | 'enabled' => [] 135 | ] 136 | ]); 137 | $this->suiteEventProphecy->getSuite()->willReturn(new Suite()); 138 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 139 | $this->assertFileNotExists($this->dockerComposeFilePath); 140 | } 141 | 142 | /** 143 | * testModuleInitShouldThrowAnExceptionIfProcessIsNotRunning 144 | *suites 145 | * @return void 146 | */ 147 | public function testModuleInitShouldThrowAnExceptionIfProcessIsNotRunning() 148 | { 149 | $this->expectExceptionMessage('Failed to start Docker server.'); 150 | $this->expectException(ExtensionException::class); 151 | 152 | $this->processProphecy->start(Argument::any())->shouldBeCalled(); 153 | $this->processProphecy->getCommandLine(Argument::any())->shouldBeCalled(); 154 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(false); 155 | $this->processProphecy->signal(Argument::type('int'))->shouldNotBeCalled(); 156 | $this->processProphecy->wait()->shouldNotBeCalled(); 157 | 158 | $this->suiteEventProphecy->getSettings()->willReturn([ 159 | 'modules' => [ 160 | 'enabled' => [] 161 | ] 162 | ]); 163 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 164 | $this->assertFileExists($this->dockerComposeFilePath); 165 | } 166 | 167 | /** 168 | * testModuleInitShouldCreateDockerComposeYamlIfNoSuiteAreSet 169 | * 170 | * @return void 171 | */ 172 | public function testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSet() 173 | { 174 | $this->processProphecy->start(Argument::type('callable'))->shouldBeCalled()->will(function ($args) { 175 | $args[0]('info', 'Registered a node'); 176 | }); 177 | $this->processProphecy->getCommandLine(Argument::any())->shouldBeCalled(); 178 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(true); 179 | $this->processProphecy->signal(Argument::type('int'))->shouldNotBeCalled(); 180 | $this->processProphecy->wait()->shouldNotBeCalled(); 181 | 182 | $this->suiteEventProphecy->getSettings()->willReturn([ 183 | 'modules' => [ 184 | 'enabled' => [] 185 | ] 186 | ]); 187 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 188 | $this->assertFileExists($this->dockerComposeFilePath); 189 | } 190 | 191 | /** 192 | * testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithProxy 193 | * 194 | * @return void 195 | */ 196 | public function testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithProxy() 197 | { 198 | $this->processProphecy->start(Argument::type('callable'))->shouldBeCalled()->will(function ($args) { 199 | $args[0]('info', 'Registered a node'); 200 | }); 201 | $this->processProphecy->getCommandLine(Argument::any())->shouldBeCalled(); 202 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(true); 203 | $this->processProphecy->signal(Argument::type('int'))->shouldNotBeCalled(); 204 | $this->processProphecy->wait()->shouldNotBeCalled(); 205 | 206 | $this->suiteEventProphecy->getSettings()->willReturn([ 207 | 'modules' => [ 208 | 'enabled' => [ 209 | [ 210 | 'WebDriver' => [ 211 | 'port' => 2222, 212 | 'capabilities' => [ 213 | 'httpProxy' => 'http-proxy:3128', 214 | 'sslProxy' => 'https-proxy:3128', 215 | ] 216 | ] 217 | ] 218 | ] 219 | ] 220 | ])->shouldBeCalled(); 221 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 222 | $this->assertFileExists($this->dockerComposeFilePath); 223 | $this->assertEquals([ 224 | 'hub' => [ 225 | 'image' => 'selenium/hub', 226 | 'ports' => ['2222:4444'], 227 | 'environment' => [ 228 | 'http_proxy=http-proxy:3128', 229 | 'https_proxy=https-proxy:3128', 230 | ], 231 | ], 232 | 'chrome' => [ 233 | 'volumes' => [ 234 | '/dev/shm:/dev/shm' 235 | ], 236 | 'image' => 'selenium/node-chrome', 237 | 'links' => ['hub'], 238 | 'environment' => [ 239 | 'http_proxy=http-proxy:3128', 240 | 'https_proxy=https-proxy:3128', 241 | ], 242 | ] 243 | ], Yaml::parse(file_get_contents($this->dockerComposeFilePath))); 244 | } 245 | 246 | /** 247 | * testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithNoProxy 248 | * 249 | * @return void 250 | */ 251 | public function testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithNoProxy() 252 | { 253 | $this->processProphecy->start(Argument::type('callable'))->shouldBeCalled()->will(function ($args) { 254 | $args[0]('info', 'Registered a node'); 255 | }); 256 | $this->processProphecy->getCommandLine(Argument::any())->shouldBeCalled(); 257 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(true); 258 | $this->processProphecy->signal(Argument::type('int'))->shouldNotBeCalled(); 259 | $this->processProphecy->wait()->shouldNotBeCalled(); 260 | 261 | $this->suiteEventProphecy->getSettings()->willReturn([ 262 | 'modules' => [ 263 | 'enabled' => [ 264 | [ 265 | 'WebDriver' => [ 266 | 'port' => 2222, 267 | 'capabilities' => [ 268 | 'httpProxy' => 'http-proxy:3128', 269 | 'sslProxy' => 'https-proxy:3128', 270 | 'noProxy' => 'domain.loc', 271 | ] 272 | ] 273 | ] 274 | ] 275 | ] 276 | ])->shouldBeCalled(); 277 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 278 | $this->assertFileExists($this->dockerComposeFilePath); 279 | $this->assertEquals([ 280 | 'hub' => [ 281 | 'image' => 'selenium/hub', 282 | 'ports' => ['2222:4444'], 283 | 'environment' => [ 284 | 'http_proxy=http-proxy:3128', 285 | 'https_proxy=https-proxy:3128', 286 | 'no_proxy=domain.loc', 287 | ], 288 | ], 289 | 'chrome' => [ 290 | 'volumes' => [ 291 | '/dev/shm:/dev/shm' 292 | ], 293 | 'image' => 'selenium/node-chrome', 294 | 'links' => ['hub'], 295 | 'environment' => [ 296 | 'http_proxy=http-proxy:3128', 297 | 'https_proxy=https-proxy:3128', 298 | 'no_proxy=domain.loc', 299 | ], 300 | ] 301 | ], Yaml::parse(file_get_contents($this->dockerComposeFilePath))); 302 | } 303 | 304 | /** 305 | * testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithExtraHosts 306 | * 307 | * @return void 308 | */ 309 | public function testModuleInitShouldCreateDockerComposeYamlAndStartProcessIfNoSuiteAreSetWithExtraHosts() 310 | { 311 | $this->dockerChrome = new Codeception\Extension\DockerChrome(['extra_hosts' => ['someDomain.loc:127.0.0.1'], 'debug' => true, 'path' => vfsStream::url('docker/usr/local/bin/docker-composes')], [], $this->processProphecy->reveal()); 312 | 313 | $this->processProphecy->start(Argument::type('callable'))->shouldBeCalled()->will(function ($args) { 314 | $args[0]('info', 'Registered a node'); 315 | }); 316 | $this->processProphecy->getCommandLine(Argument::any())->shouldBeCalled(); 317 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(true); 318 | $this->processProphecy->signal(Argument::type('int'))->shouldNotBeCalled(); 319 | $this->processProphecy->wait()->shouldNotBeCalled(); 320 | 321 | $this->suiteEventProphecy->getSettings()->willReturn([ 322 | 'modules' => [ 323 | 'enabled' => [ 324 | [ 325 | 'WebDriver' => [ 326 | 'port' => 2222 327 | ] 328 | ] 329 | ] 330 | ] 331 | ])->shouldBeCalled(); 332 | $this->dockerChrome->moduleInit($this->suiteEventProphecy->reveal()); 333 | $this->assertFileExists($this->dockerComposeFilePath); 334 | $this->assertEquals([ 335 | 'hub' => [ 336 | 'image' => 'selenium/hub', 337 | 'ports' => ['2222:4444'], 338 | 'environment' => [], 339 | 'extra_hosts' => ['someDomain.loc:127.0.0.1'] 340 | ], 341 | 'chrome' => [ 342 | 'volumes' => [ 343 | '/dev/shm:/dev/shm' 344 | ], 345 | 'image' => 'selenium/node-chrome', 346 | 'links' => ['hub'], 347 | 'environment' => [], 348 | 'extra_hosts' => ['someDomain.loc:127.0.0.1'] 349 | ] 350 | ], Yaml::parse(file_get_contents($this->dockerComposeFilePath))); 351 | } 352 | 353 | /** 354 | * testDestructShouldStopServer 355 | * 356 | * @return void 357 | */ 358 | public function testDestructShouldStopServer() 359 | { 360 | $this->processProphecy->isRunning()->shouldBeCalled()->willReturn(true); 361 | $this->processProphecy->signal(2)->shouldBeCalled(); 362 | $this->processProphecy->wait()->shouldBeCalled(); 363 | $this->dockerChrome->__destruct(); 364 | } 365 | 366 | protected function initExtension() 367 | { 368 | vfsStream::setup('docker', null, [ 369 | 'usr' => [ 370 | 'local' => [ 371 | 'bin' => [ 372 | 'docker-composes' => ' true, 'path' => vfsStream::url('docker/usr/local/bin/docker-composes')]; 378 | $options = []; 379 | $this->processProphecy = $this->prophesize(Process::class); 380 | $this->dockerChrome = new Codeception\Extension\DockerChrome($config, $options, $this->processProphecy->reveal()); 381 | 382 | $this->suiteEventProphecy = $this->prophesize(SuiteEvent::class); 383 | 384 | //defaults for clean run 385 | $this->processProphecy->start(Argument::any()); 386 | $this->processProphecy->isRunning(); 387 | $this->processProphecy->signal(Argument::type('int')); 388 | $this->processProphecy->wait(); 389 | } 390 | 391 | /** 392 | * _before 393 | * 394 | * @return void 395 | */ 396 | protected function _before() 397 | { 398 | $this->initExtension(); 399 | } 400 | 401 | /** 402 | * _after 403 | * 404 | * @return void 405 | */ 406 | protected function _after() 407 | { 408 | if (file_exists($this->dockerComposeFilePath)) { 409 | unlink($this->dockerComposeFilePath); 410 | } 411 | } 412 | } -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 |