1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?php
namespace Codeception\Module;
use Codeception\Step;
use Codeception\TestInterface;
use Facebook\WebDriver\WebDriverBy;
/**
* Module for AngularJS testing, based on [WebDriver module](http://codeception.com/docs/modules/WebDriver) and [Protractor](http://angular.github.io/protractor/).
*
* Performs **synchronization to ensure that page content is fully rendered**.
* Uses Angular's and Protractor internals methods to synchronize with the page.
*
* ## Configuration
*
* The same as for [WebDriver](http://codeception.com/docs/modules/WebDriver#Configuration), but few new options added:
*
* * `el` - element where Angular application is defined (default: `body`)
* * `script_timeout` - for how long in seconds to wait for angular operations to finish (default: 5)
*
* ### Example (`acceptance.suite.yml`)
*
* modules:
* enabled:
* - AngularJS:
* url: 'http://localhost/'
* browser: firefox
* script_timeout: 10
*
*
* ### Additional Features
*
* Can perform matching elements by model. In this case you should provide a strict locator with `model` set.
*
* Example:
*
* ```php
* $I->selectOption(['model' => 'customerId'], '3');
* ```
*/
class AngularJS extends WebDriver
{
protected $insideApplication = true;
protected $defaultAngularConfig = [
'script_timeout' => 5,
'el' => 'body',
];
protected $waitForAngular = <<<EOF
var rootSelector = arguments[0];
var callback = arguments[1];
var el = document.querySelector(rootSelector);
try {
if (window.getAngularTestability) {
window.getAngularTestability(el).whenStable(callback);
return;
}
if (!window.angular) {
throw new Error('window.angular is undefined. This could be either ' +
'because this is a non-angular page or because your test involves ' +
'client-side navigation, which can interfere with Protractor\'s ' +
'bootstrapping. See http://git.io/v4gXM for details');
}
if (angular.getTestability) {
angular.getTestability(el).whenStable(callback);
} else {
if (!angular.element(el).injector()) {
throw new Error('root element (' + rootSelector + ') has no injector.' +
' this may mean it is not inside ng-app.');
}
angular.element(el).injector().get('\$browser').
notifyWhenNoOutstandingRequests(callback);
}
} catch (err) {
callback(err.message);
}
EOF;
public function _setConfig($config)
{
parent::_setConfig(array_merge($this->defaultAngularConfig, $config));
}
public function _before(TestInterface $test)
{
parent::_before($test);
$this->webDriver->manage()->timeouts()->setScriptTimeout($this->config['script_timeout']);
}
/**
* Enables Angular mode (enabled by default).
* Waits for Angular to finish rendering after each action.
*/
public function amInsideAngularApp()
{
$this->insideApplication = true;
}
/**
* Disabled Angular mode.
*
* Falls back to original WebDriver, in case web page does not contain Angular app.
*/
public function amOutsideAngularApp()
{
$this->insideApplication = false;
}
public function _afterStep(Step $step)
{
if (!$this->insideApplication) {
return;
}
$actions = [
'amOnPage',
'click',
'fillField',
'selectOption',
'checkOption',
'uncheckOption',
'unselectOption',
'doubleClick',
'appendField',
'clickWithRightButton',
'dragAndDrop'
];
if (in_array($step->getAction(), $actions)) {
$this->webDriver->executeAsyncScript($this->waitForAngular, [$this->config['el']]);
}
}
protected function getStrictLocator(array $by)
{
$type = key($by);
$value = $by[$type];
if ($type === 'model') {
return WebDriverBy::cssSelector(sprintf('[ng-model="%s"]', $value));
}
return parent::getStrictLocator($by);
}
}