Overview

Classes

  • VideoTestWebTestCaseDriver
  • VideoTestWebTestCaseDriverFunctions
  • Overview
  • Class
  1: <?php
  2: /** 
  3:  * Custom Selenium Driver, which adds some animation functions.
  4:  * 
  5:  * <code>
  6:  * usage: 
  7:  *   public function setUp(){
  8:  *     Yii::import('application.vendor.videotest.VideoTestWebTestCaseDriver');
  9:  *     $this->drivers[0]=VideoTestWebTestCaseDriver::attach($this->drivers[0], $this);
 10:  *     parent::setUp();
 11:  *   }
 12:  *
 13:  *   public function testABC(){
 14:  *     $this->videoInit();
 15:  *     $this->open('');
 16:  *     $this->videoStart('ABC');
 17:  *     $this->videoShowMessage('Test ABC');
 18:  *     ... 
 19:  *   }
 20:  * </code>
 21:  * Configuration: <br/>
 22:  * <code>
 23:  * Yii::app()->params['selenium-video']['fast']  
 24:  *   *  set to 1 to skip animation and make test run faster during development  
 25:  *   *  set to 2 to skip all visual effects completely  
 26:  *   *  set to 0 or false for default (slow, animated) behavior  
 27:  * </code>
 28:  * Inside the test you can override this value by using following functions:  
 29:  * <code>
 30:  *   *   $this->videoSlow() -- equal to setting configuration value to 0  
 31:  *   *   $this->videoFast() -- equal to setting configuration value to 1  
 32:  *   *   $this->videoSkip() -- equal to setting configuration value to 2  
 33:  *   *   $this->videoDefault() -- returns to default setting from configuration  
 34:  * </code>
 35:  * These functions can be configured to be ignored on CI server -- set
 36:  * Yii::app()->params['selenium-video']['ignore-fast-override'] to true in your local CI config
 37:  */
 38: 
 39: class VideoTestWebTestCaseDriver {
 40:     protected $driver;
 41:     protected $testCase; 
 42:     protected $functions;
 43: 
 44:     /**
 45:      * Creates driver object which wraps original one adding our functions
 46:      * @param PHPUnit_Extensions_SeleniumTestCase_Driver $driver current driver. See Usage above
 47:      * @param CWebTestCase $testCase current test case. See Usage above
 48:      * @return PHPUnit_Extensions_SeleniumTestCase_Driver driver to be placed to $this->drivers[0]
 49:      */
 50:     static public function attach($driver, $testCase){
 51:         if (is_a($driver, __CLASS__)) return $driver;
 52:         $class = __CLASS__;
 53:         $copy = new $class;
 54:         $copy->driver = $driver;
 55:         $copy->testCase = $testCase;
 56:         $copy->functions = new VideoTestWebTestCaseDriverFunctions();
 57:         $copy->functions->testCase = $testCase;
 58:         return $copy;
 59:     }
 60: 
 61:     /**
 62:      * @var boolean used to hide errors, that can be introduced by video driver
 63:      */
 64:     private $_lastCallWasVideoCall;
 65: 
 66:     /**
 67:      * Wrapper magic getter
 68:      * @param string $name
 69:      * @return mixed
 70:      */
 71:     public function __get($name){
 72:         return $this->driver->$name;
 73:     }
 74:     /**
 75:      * Wrapper magic setter
 76:      * @param string $name
 77:      * @param mixed $value
 78:      * @return void
 79:      */
 80:     public function __set($name, $value){
 81:         $this->driver->$name = $value;
 82:     }
 83:     /**
 84:      * Wrapper magic method
 85:      * Routes calls started with "video" to our driver, and hides errors, that can be 
 86:      * introduced by our driver
 87:      * @param string $fn
 88:      * @param array $args
 89:      * @return mixed
 90:      */
 91:     public function __call($fn, $args){
 92:         if (preg_match('/^video/', $fn)){
 93:             // this is our call
 94:             $result = call_user_func_array(array($this->functions, $fn), $args);
 95:             $this->_lastCallWasVideoCall = true;
 96:         } elseif (in_array($fn, array('getVerificationErrors', 'clearVerificationErrors'))){
 97:             if (!$this->_lastCallWasVideoCall){
 98:                 return $this->driver->$fn();
 99:             } else {
100:                 return array();
101:             }
102:         } else {
103:             $this->_lastCallWasVideoCall = false;
104:             return call_user_func_array(array($this->driver, $fn), $args);
105:         }
106:     }
107: 
108:     /** 
109:      * Trial-and-error method for writing video tests
110:      * 
111:      * Magic method to save some time when making video tests - it allows you change the code during 
112:      * test execution (thus keeping browser open) and retry until code works.
113:      * 
114:      * Usual usage  simply put following line at the appropriate place of your code:
115:      * <code>
116:      *        $this->doTry(get_defined_vars()); } function t(){
117:      * </code>
118:      * This line will split your test function into two functions - one will end with ->doTry() call,
119:      * and another one will go into the t() test function
120:      * 
121:      * This doTry() function will call test function t in an indefinite loop asking you to press Enter 
122:      * in the test console after each turn. All exceptions will be caught and displayed instead 
123:      * of making test fail.
124:      * 
125:      * As soon as a piece of code works (i.e. it clicks appropriate button or link) - you move working
126:      * code from t() function up to your test function.
127:      * 
128:      * After code works - you should have your t() function empty and simply remove the ->doTry() line.
129:      * @param $params array array of parameters to be passed to the test function 
130:      * @return void
131:      */
132:     public function doTry($params = array()){
133:         static $iteration = 0;
134:         static $preventDistructors = array();
135:         while (1){
136:             $d = debug_backtrace();
137:             $testFile = file_get_contents($d[4]['file']);
138:             preg_match_all($pattern = '/class\s+([^ ]+)\s+extends\s+/ui', $testFile, $m);
139:             if (!count($m[0])) throw new Exception($pattern.' not found');
140:             $testClassName = array();
141:             foreach ($m[0] as $k=>$v){
142:                 $className = $m[1][$k];
143:                 $newClassName = $className.$iteration;
144:                 $testFile = str_replace($v, 'class '.$newClassName.' extends ', $testFile);
145:                 $classNames[$className] = $newClassName;
146:                 if (is_a($className, 'CWebTestCase')) $testClassName = $newClassName;
147:             }
148:             $iteration ++;
149:             $testFile = preg_replace('/^\<\?(php)?/ui', '', $testFile);
150:             $paramNames = array();
151:             foreach ($params as $k=>$v){
152:                 $paramNames[]='$'.$k;
153:             }
154:             $testFile = preg_replace('/(public\s+)?function\s+t\s*\(\)[ \t\r\n]*\{/', 'public function t('.implode(', ', $paramNames).'){', $testFile);
155:             eval($testFile);
156:             $test = new $newClassName;
157:             $preventDistructors[] = $test;
158:             $reflection = new ReflectionClass($test);
159:             $property = $reflection->getProperty('drivers');
160:             $property->setAccessible(true);
161:             
162:             $oldReflection = new ReflectionClass($this->testCase);
163:             $oldProperty = $oldReflection->getProperty('drivers');
164:             $oldProperty->setAccessible(true);
165:             $property->setValue($test, $oldProperty->getValue($this->testCase));
166: 
167: 
168:             try {
169:                 call_user_func_array(array($test, 't'), $params);
170:             } catch (Exception $e){
171:                 print_r($e);
172:             }
173:             echo PHP_EOL.'press enter...'.PHP_EOL;
174:             @flush(); @ob_flush();
175:             fgets(STDIN);
176: 
177:         }        
178: 
179:     }
180:     /** 
181:      * just dumps values during test execution. To be used for debugging tests
182:      * @param mixed $msg can be of any type, since print_r will be used
183:      * @return void
184:      */
185:     public function say($msg){
186:         print_r($msg);
187:         echo PHP_EOL;
188:         if (strlen(print_r($msg, true)) < 100) var_dump($msg);
189:         flush(); @ob_flush();
190:     }
191: }
192: 
193: 
194: /**
195:  * Collection of video functions
196:  */
197: class VideoTestWebTestCaseDriverFunctions {
198:     public $overrideFastMode = false;
199:     public $testCase;
200:     const VIDEO_DEFAULT_MESSAGE_POSITION = 'top: 100px; left: 100px; right: 100px; height: 200px;';
201:     public $videoDefaultMessagePosition = self::VIDEO_DEFAULT_MESSAGE_POSITION;
202: 
203:     /** 
204:      * Makes sure that element is visible.
205:      * Scrolls page up or down if necessary. No horizontal scroll. 
206:      * @param string $element - same as element in Selenium methods
207:      * @return CWebTestCase $this for chaining
208:      */
209:     public function videoSetVisible($element){
210:         $x = $this->testCase->getElementPositionLeft($element);
211:         $y = $this->testCase->getElementPositionTop($element);
212:         // determine scroll step depending on mode.
213:         $step = $this->isFastModeOn()?50:15; // px
214:         $windowHeight = $this->testCase->getEval('window.innerHeight');
215:         $height = min($windowHeight * 80 / 100, $this->testCase->getElementHeight($element));
216:         // scroll up until element is visible and at least 50px below window top
217:         while ((($y - $height/2) < (($currentYScroll = $this->testCase->getEval('window.document.documentElement.scrollTop')) + 50)) && $currentYScroll){
218:             if ($this->isFastModeOn()){
219:                 $step = 50;
220:             } else {
221:                 $diff = ($currentYScroll + 50) - ($y - $height/2);
222:                 if ($diff > 150) {
223:                     $step = min(500, intval($diff/5));
224:                 } else {
225:                     $step = 15;
226:                 }
227:             }
228:             $this->testCase->runScript("window.scrollBy(0,-".$step.");");
229:             if (!$this->isFastModeOn()) usleep(10000);
230:         }
231:         $lastYScroll = -1;
232:         // scroll down until element is visible and at least 50px above window bottom
233:         while ((($y + $height/2) > (($currentYScroll = $this->testCase->getEval('window.document.documentElement.scrollTop')) + $windowHeight - 50)) && ($currentYScroll != $lastYScroll)){
234:             if ($this->isFastModeOn()){
235:                 $step = 50;
236:             } else {
237:                 $diff = ($y + $height/2) - ($currentYScroll + $windowHeight - 50);
238:                 if ($diff > 150) {
239:                     $step = min(500, intval($diff/5));
240:                 } else {
241:                     $step = 15;
242:                 }
243:             }
244:             $lastYScroll = $currentYScroll;
245:             $this->testCase->runScript("window.scrollBy(0,".$step.");");
246:             if (!$this->isFastModeOn()) usleep(10000);
247:         }
248:         return $this->testCase;
249:     }
250: 
251:     /** 
252:      * Draws mouse cursor which moves towards desired control, then invokes mouseOver
253:      * This method DOES NOT CLICK, this method only animates mouse cursor (arrow)
254:      * @param string $element - same as element in Selenium methods
255:      * @param true|array $nearTheLeftSide
256:      *   true -> mouse will move to the left side of the element (useful for labels of checkboxes)
257:      *   array(top,left) - exact coordinates (px) where to move
258:      *   array(+5, +0) - tweak coordinates - add 5 px to top and leave left as is
259:      * @return CWebTestCase $this for chaining
260:      */
261: 
262:     public function videoMouseClick($element, $nearTheLeftSide=false, $highlightCallback=false){
263:         $this->testCase->videoSetVisible($element);
264:         if ($this->isSkipModeOn()) return;
265:         $x = $this->testCase->getElementPositionLeft($element) + ($nearTheLeftSide?rand(5,20):($this->testCase->getElementWidth($element)*rand(20,80)/100));
266:         $y = ($a=$this->testCase->getElementPositionTop($element)) + ($b=$this->testCase->getElementHeight($element))*($c=rand(20,80)/100) - ($d=$this->testCase->getEval('window.document.documentElement.scrollTop'));
267:         if (is_array($nearTheLeftSide)){
268:             if (substr($nearTheLeftSide[1], 0, 1) == '+'){
269:                 $x += substr($nearTheLeftSide[1], 1);
270:             } elseif (substr($nearTheLeftSide[1], 0, 1) == '-'){
271:                 $x -= substr($nearTheLeftSide[1], 1);
272:             } else {
273:                 $x = $nearTheLeftSide[1];
274:             }
275:             if (substr($nearTheLeftSide[0], 0, 1) == '+'){
276:                 $y += substr($nearTheLeftSide[0], 1);
277:             } elseif (substr($nearTheLeftSide[0], 0, 1) == '-'){
278:                 $y -= substr($nearTheLeftSide[0], 1);
279:             } else {
280:                 $y = $nearTheLeftSide[0] - $this->testCase->getEval('window.document.documentElement.scrollTop');
281:             }
282:         } 
283: 
284:         $mouseStyle = ' background: url(\'data:image/gif;base64,R0lGODlhGQAdALMAAAAAAP///wD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAZAB0AAARGUMhJq7046827l8CXAaFoAUFpTmi6skGsmq381jYd766I7zPOD/gZEjtDkhIJzBVlraCwF/WpqsUK9ubk9rxS62tMLpsnEQA7\') no-repeat; position: fixed; z-index: 9999999; width: 25px; height: 29px;';
285:         $script = array();
286:         
287:         if ($this->testCase->getEval('window.document.getElementById("mouse-cursor")?1:0')){
288:             $screenCenterY = $this->testCase->getElementPositionTop('id=mouse-cursor')+4;
289:             $screenCenterX = $this->testCase->getElementPositionLeft('id=mouse-cursor')+7;
290:             $alreadyExists = true;
291:         } else {
292:             $windowHeight = $this->testCase->getEval('window.innerHeight');
293:             $screenCenterY = $windowHeight/2;
294:             $screenCenterX = 500;
295:             $alreadyExists = false;
296:         }
297:         $hops = 20;
298:         $opacity = $hops/4;
299:         $coords = array();
300:         for ($i = $this->isFastModeOn()?($hops-4):1; $i <= $hops; $i ++){
301:             $coords[]=array(
302:                 intval($y-($screenCenterY-$y)/$hops+($screenCenterY-$y)/$i-4), 
303:                 intval($x-($screenCenterX-$x)/$hops+($screenCenterX-$x)/$i-7),
304:                 (($i<$opacity)?(intval($i*10/$opacity)/10):1),
305:                 (!$this->isFastModeOn() && ($i < ($hops*3/4)))?40:0,
306:             );
307:         }
308:         if ($alreadyExists){
309:             $this->testCase->runScript('window.document.getElementById("mouse-cursor").setAttribute("data-done", 0);');
310:         } else {
311:             $this->testCase->runScript('var a = window.document.createElement("div"); a.setAttribute("id", "mouse-cursor"); a.setAttribute("style", "top:'.$screenCenterY.'px; left:'.$screenCenterX.'px; opacity: 0; '.$mouseStyle.'"); window.document.getElementsByTagName("body")[0].appendChild(a); ');
312:         }
313:         $this->testCase->runScript('(function(coords){ if (0 == coords.length) { window.document.getElementById("mouse-cursor").setAttribute("data-done", 1); } else { var nextItem = coords.shift(); var mouse = window.document.getElementById("mouse-cursor"); mouse.style.left = nextItem[1] + "px"; mouse.style.top = nextItem[0] + "px"; mouse.style.opacity = nextItem[2]; setTimeout(arguments.callee, nextItem[3], coords); }})('.CJSON::encode($coords).');');
314:         
315:         $t = time();
316:         while (time() < ($t + 10)){
317:             $done = $this->testCase->getEval('window.document.getElementById("mouse-cursor").getAttribute("data-done");');
318:             if (($done === "1") || ($done === 1)) break;
319:             usleep(100000);
320:         }
321:         $this->testCase->mouseOver($element);
322:         if ($highlightCallback) call_user_func_array($highlightCallback, array($element));
323:         if (!$this->isFastModeOn()) usleep(200000);
324:         return $this->testCase;
325:     }
326:     /** 
327:      * Just pause to let user see what's on the screen. 
328:      *
329:      * Pause is skipped in any of fast modes.
330:      * @param integer $millis time to sleep in milliseconds, default = 2000 (2 seconds)
331:      * @return CWebTestCase $this for chaining
332:      */
333:     public function videoSleep($millis = 2000){
334:         if (!$this->isFastModeOn()){
335:             usleep($millis * 1000);
336:         }
337:         return $this->testCase;
338:     }
339: 
340:     /**
341:      * Selects an item from drop-down select box, when custom selectbox is used
342:      *
343:      * @param string id id of select element
344:      * @param string value value to be selected. IMPORTANT: value should be visible initially, scrolling is not implemented yet
345:      * @return CWebTestCase $this for chaining
346:      */
347:     public function videoCustomSelectBoxSelect($id, $value){
348:         $this->videoMouseClick('xpath=//select[@id="'.$id.'"]/following-sibling::a[1]');
349:         for ($exceptionTries = 0; $exceptionTries < 10; $exceptionTries++){
350:             try{
351:                 $this->testCase->runScript($toggle='var e = document.createEvent("HTMLEvents"); e.initEvent("mousedown", false, true); e.which = 1; window.document.getElementById("'.$id.'").nextSibling.dispatchEvent(e); ');
352: 
353:                 for ($tries = 200; $tries; $tries--){
354:                     if ($this->testCase->isElementPresent($q='xpath=//ul[contains(@class, "selectBox-options") and not(contains(@style, "display: none"))]/li/a[@rel="'.$value.'"]')){
355:                         $this->videoMouseClick($q);
356:                         break;
357:                     }
358:                     usleep(50000);
359:                 }
360:                 $name = $this->testCase->getText($q);
361:                 $this->testCase->select('id='.$id, 'value='.$value);
362:                 $this->testCase->runScript('window.document.getElementById("'.$id.'").nextSibling.children[0].innerHTML='.CJSON::encode($name).';');
363:                 $this->testCase->runScript($toggle);
364:                 return; 
365:             } catch (Exception $e){
366:                 sleep(1);
367:             }
368:         }
369:         return $this->testCase;
370:     }
371: 
372:     /**
373:      * Removes DatePicker window, that will eventually appear after you focus on date text input
374:      * @return CWebTestCase $this for chaining
375:      */
376:     public function videoHideDatePicker(){
377:         $this->testCase->runScript('(function(){var a = window.document.getElementsByClassName("ui-datepicker"); var i; for (i = 0; i < a.length; i++){a[i].style.display="none";}})();');
378:         return $this->testCase;
379:     }
380: 
381:     /** 
382:      * Sets default position for message window.
383:      *
384:      * @param $position string a CSS style definition, e.g.: 'top: 100px; left: 100px; right: 100px; height: 200px;'. 
385:      * IMPORTANT: the ; at the end is mandatory
386:      * @return CWebTestCase $this for chaining
387:      */
388:     public function videoSetDefaultMessagePosition($position = self::VIDEO_DEFAULT_MESSAGE_POSITION){
389:         $this->videoDefaultMessagePosition = $position;
390:         return $this->testCase;
391:     }
392:     
393:     /**
394:      * Shows slideshow of images. Use it when animation is not possible. 
395:      * 
396:      * @param array $files array of filenames of images to be shown
397:      * @param integer|array $durations pause between sequental images, measured in seconds. If array
398:      * is specified, then it should have same keys and same number of items as $files
399:      * @return CWebTestCase $this for chaining
400:      */
401:     public function videoShowImage($files, $durations=5){
402:         if ($this->isSkipModeOn()) return;
403:         static $fnNameCounter = 1; 
404:         if (!is_array($files)) $files=array($files);
405:         if (!is_array($durations)) $durations=array($durations);
406: 
407:         $zindex = 10000000;
408:         $style = 'position: fixed; top: 100px; left: 100px; width:100%; height: 100%; display: block; z-index: '.$zindex.';';        
409: 
410:         $imgStyle = 'position: fixed; top: 100px; left: 100px;';
411:         $this->testCase->runScript('var a = window.document.createElement("div"); a.setAttribute("id", "video-image"); a.setAttribute("style", "'.$style.'"); a.innerHTML='.CJSON::encode('<img id="video-image-img-1" src="" style="'.$imgStyle.'; opacity: 0; z-index: '.$zindex.';"/><img id="video-image-img-2" src="" style="'.$imgStyle.'; opacity: 0; z-index: '.$zindex.';"/>').';  window.document.getElementsByTagName("body")[0].appendChild(a);');
412: 
413:         $step = 1;
414:         foreach ($files as $file){
415:             $zindex ++;
416:             $this->testCase->runScript('var a = window.document.getElementById("video-image-img-'.$step.'"); a.setAttribute("src", '.($file?CJSON::encode('data:image/png;base64,'.base64_encode(file_get_contents($file))):'""').'); var videoImageCounter'.$fnNameCounter.' = 0; function videoImageUpdate'.$fnNameCounter.'(){ a.setAttribute("style", "'.$imgStyle.' opacity: "+(videoImageCounter'.$fnNameCounter.'/100)+"; z-index: '.$zindex.';"); videoImageCounter'.$fnNameCounter.'+='.($this->isFastModeOn()?50:2).'; if (videoImageCounter'.$fnNameCounter.' <= 100) { window.setTimeout(videoImageUpdate'.$fnNameCounter.', 5); } } videoImageUpdate'.$fnNameCounter.'();');
417: 
418:             $step = 3 - $step;
419:             if (count($durations)){
420:                 $delay = array_shift($durations);
421:             } else {
422:                 if (!$delay) $delay = 5; 
423:             }
424: 
425:             if (!$this->isFastModeOn()) sleep($delay);
426:         }
427:         $this->testCase->runScript('window.document.getElementById("video-image-img-'.$step.'").setAttribute("style", "opacity: 0;"); var a = window.document.getElementById("video-image-img-'.(3-$step).'"); a.setAttribute("src", '.($file?CJSON::encode('data:image/png;base64,'.base64_encode(file_get_contents($file))):'""').'); var videoImageCounter'.$fnNameCounter.' = 0; function videoImageUpdate'.$fnNameCounter.'(){ a.setAttribute("style", "'.$imgStyle.' opacity: "+((100-videoImageCounter'.$fnNameCounter.')/100)+"; z-index: '.$zindex.';"); videoImageCounter'.$fnNameCounter.'+='.($this->isFastModeOn()?50:2).'; if (videoImageCounter'.$fnNameCounter.' <= 100) { window.setTimeout(videoImageUpdate'.$fnNameCounter.', 5); } } videoImageUpdate'.$fnNameCounter.'();');
428: 
429:         if (!$this->isFastModeOn()) sleep($delay);
430: 
431:         $this->testCase->runScript('var a = window.document.getElementById("video-image"); a.parentNode.removeChild(a);');
432:         return $this->testCase;
433:     }
434:     /** 
435:      * Shows message 
436:      * @param $text string - the text. Use "\n" to separate lines.
437:      * @param $position string see videoSetDefaultMessagePosition
438:      * @return CWebTestCase $this for chaining
439:      */
440:     public function videoShowMessage($text, $position=null, $moreMsToWait = 0){
441:         if ($this->isSkipModeOn()) return;
442:         if (!$position) $position = $this->videoDefaultMessagePosition;
443:         $pc = array('');
444:         $text .= ' ';
445:         $i=0;
446:         while (preg_match('/^(.)(.+)$/su', $text, $m)){
447:             if (in_array($m[1], array("\n", ' '))){
448:                 $pc[]=array(false, $m[1]);
449:                 $pc[]=array();
450:             } else {
451:                 $pc[max(array_keys($pc))][]=$m[1];
452:             }
453:             $text = $m[2];
454:         }
455:         $videoMessageStyle = 'position: fixed; '.$position.' display: block; background-color: #fff; padding: 10px; font-size: 20px; z-index: 10000000; border: #bbb solid 10px; border-radius: 20px; overflow: hidden;';
456: 
457:         $blinkingCursor = '<div id="bfwebtestcasevideodriverblinkerdiv">_</div>';
458:         $this->testCase->runScript('var a = window.document.createElement("div"); a.setAttribute("id", "video-message"); a.setAttribute("style", "'.$videoMessageStyle.'"); a.innerHTML='.CJSON::encode('<style>@keyframes bfwebtestcasevideodriverblinker { 0% { opacity: 1.0; } 50% { opacity: 0.0; } 100% { opacity: 1.0; }} #bfwebtestcasevideodriverblinkerdiv {display: inline-block; width: 10px; margin-right: -10px; animation-name: bfwebtestcasevideodriverblinker; animation-duration: 1s; animation-timing-function: step-end; font-weight: bold; animation-iteration-count: infinite;}</style><table style="position: absolute; border: none; bottom: 2px;"><tr style="border: none;"><td id="video-message-text" style="padding: 2px 20px 2px 2px; font-size: 22px; color: #6a6a6a; font-family: Verdana, Tahoma, Arial, Helvetica, sans-serif; border: none; text-align: left;">'.$blinkingCursor.'</td></tr></table>').';  window.document.getElementsByTagName("body")[0].appendChild(a);');
459: 
460: 
461:         $contents = '';
462:         $codeSequence = array();
463:         foreach ($pc as $piece){
464:             if (!is_array($piece) || !count($piece) || !isset($piece[0])) continue;
465:             if ($piece[0] === false){
466:                 $contents .= ($piece[1] == "\n")?'<br/>':htmlentities($piece[1]);
467:             } else {
468:                 for ($k = 0; $k <= count($piece); $k++) {
469:                     $visible = '';
470:                     $invisible = '';
471:                     for ($i=0; $i<$k; $i++){
472:                         $visible.=$piece[$i];
473:                     }
474:                     for ($i=$k; $i<count($piece); $i++){
475:                         $invisible.=$piece[$i];
476:                     }
477:                     $lastWord = '<nobr style="font-size: inherit;"><span style="font-size: inherit;">'.htmlentities($visible).'</span>'.$blinkingCursor.'<span style="color: #fff; font-size: inherit;">'.htmlentities($invisible).'</span></nobr>';
478:                     $lastWordNoBlink = '<nobr style="font-size: inherit;"><span style="font-size: inherit;">'.htmlentities($visible).'</span><span style="color: #fff; font-size: inherit;">'.htmlentities($invisible).'</span></nobr>';
479:                 
480:                     if ($this->isFastModeOn()) {
481:                     } else {
482:                         $codeSequence[] = array($contents.$lastWord, 50);
483:                     }
484:                 }
485:                 $contents .= $lastWordNoBlink;
486:             }
487:         }
488:         // TBD: this works slowly for large amounts of texts, we'd better optimize repeating $contents numerous times. 
489:         if ($this->isFastModeOn()) {
490:             $codeSequence[] = array($contents, 500+$moreMsToWait);
491:         } else {
492:             $lastItem = array_pop($codeSequence);
493:             $lastItem[1] += 2000+$moreMsToWait;
494:             array_push($codeSequence, $lastItem);
495:         }
496:         $this->testCase->runScript('(function(){var bftvvideomessage='.CJSON::encode($codeSequence).'; var bftvvideomessagefunction=function(){ if (0 == bftvvideomessage.length) { var a = window.document.getElementById("video-message"); a.parentNode.removeChild(a); } else { var nextItem = bftvvideomessage.shift(); window.document.getElementById("video-message-text").innerHTML = nextItem[0]; setTimeout(bftvvideomessagefunction, nextItem[1]); }}; bftvvideomessagefunction();})();');
497:         while ($this->testCase->isElementPresent('id=video-message')) usleep(100000);
498:         return $this->testCase;
499:     }
500:     /** 
501:      * Types text inside specified element 
502:      * @param string $element - same as element in Selenium methods
503:      * @param string $text - text to type
504:      * @return CWebTestCase $this for chaining
505:      */
506:     public function videoType($element, $text){
507:         if ($this->isFastModeOn()){
508:             $this->testCase->type($element, $text);
509:             return;
510:         }
511:         $existingId = false;
512:         try {
513:             $existingId = $this->testCase->getAttribute($element.'@id');
514:         } catch (Exception $e){}
515:         if ($existingId){
516:             $idToUse = $existingId;
517:         } else {
518:             $idToUse = 'videoTestDriverVideoTypeInput'.rand(100000,900000);
519:             $this->testCase->assignId($element, $idToUse);
520:         }
521:         $markElement = 'videotestdrivervideotypemark'.rand(100000, 900000);
522:         $this->testCase->runScript('(function(){ window.document.getElementsByTagName("body")[0].appendChild(window.document.createElement('.CJSON::encode($markElement).')); var text = '.CJSON::encode((string)$text.'').'; var putChar = function(i){ if (i > text.length) { var marks = window.document.getElementsByTagName('.CJSON::encode($markElement).'); var j; for (j = 0; j < marks.length; j++) { marks[j].parentNode.removeChild(marks[j]); } return; }  window.document.getElementById('.CJSON::encode($idToUse).').value = text.substr(0, i); setTimeout(function(){putChar(i+1);},100);}; putChar(1);})();');
523:         for ($t = time(); ($this->testCase->getEval('window.document.getElementsByTagName('.CJSON::encode($markElement).').length')) && (time() < $t + 60); ){
524:             usleep(300000);
525:         }
526:         if ($this->testCase->getEval('window.document.getElementsByTagName('.CJSON::encode($markElement).').length')){
527:             $this->testCase->fail(__CLASS__.' could not remove mark '.$markElement);
528:         }
529:         if ($idToUse !== $existingId){
530:             $this->testCase->assignId('id='.$idToUse, $existingId);
531:         }
532:         return $this->testCase;
533:     }
534:     /** 
535:      * Starts video recording 
536:      * @param string $name - name of video file
537:      * @return CWebTestCase $this for chaining
538:      */
539:     public function videoStart($name){
540:         if (!$this->isFastModeOn()){
541:             exec('selenium-video '.$name.' 2>&1');
542:         }
543:         return $this->testCase;
544:     }
545:     /** 
546:      * Finishes video
547:      * @return CWebTestCase $this for chaining
548:      */
549:     public function videoStop(){
550:         if (!$this->isFastModeOn()){
551:             sleep(5);
552:             exec('selenium-video stop 2>&1');
553:         }
554:         return $this->testCase;
555:     }
556: 
557:     /** 
558:      * Prepares for video recording -  
559:      * opens homepage, clears cookies,  maximizes window,
560:      * sets ENVIRONMENT=SELENIUM_TEST cookie to let backend know that test configuration should be used
561:      * @return CWebTestCase $this for chaining
562:      */
563:     public function videoInit(){
564:         $this->testCase->open('');
565:         $this->testCase->deleteAllVisibleCookies();
566:         $this->testCase->createCookie('ENVIRONMENT=SELENIUM_TEST', 'path=/');
567:         $this->testCase->open('');
568:         $this->testCase->windowMaximize();
569:         return $this->testCase;
570:     }
571: 
572:     /** 
573:      * Force fast mode -- used when debugging tests to save time
574:      * @return CWebTestCase $this for chaining
575:      */
576:     public function videoFast() {
577:         $this->overrideFastMode = 1;
578:         return $this->testCase;
579:     }
580: 
581:     /** 
582:      * Force slow mode -- used when debugging tests to save time
583:      * @return CWebTestCase $this for chaining
584:      */
585:     public function videoSlow() { 
586:         $this->overrideFastMode = 0;
587:         return $this->testCase;
588:     }
589: 
590:     /** 
591:      * Force skip mode -- used when debugging tests to save even more 
592:      * time - all visual effects will be turned off
593:      * @return CWebTestCase $this for chaining
594:      */
595:     public function videoSkip(){
596:         $this->overrideFastMode = 2;
597:         return $this->testCase;
598:     }
599: 
600:     /** 
601:      * Reset mode to default (defined by config) -- used when debugging tests to save time
602:      * @return CWebTestCase $this for chaining
603:      */
604:     public function videoDefault() {
605:         $this->overrideFastMode = false;
606:         return $this->testCase;
607:     }
608:     
609:     /**
610:      * @return boolean|integer 0 or false for slow mode, 1 for fast mode, 2 for skip mode
611:      */
612:     public function isFastModeOn(){
613:         if (!isset(Yii::app()->params['selenium-video']['ignore-fast-override']) || !Yii::app()->params['selenium-video']['ignore-fast-override']){
614:             if ($this->overrideFastMode !== false) {
615:                 return $this->overrideFastMode;
616:             }
617:         }
618:         return (isset(Yii::app()->params['selenium-video']['fast']) && Yii::app()->params['selenium-video']['fast'])?Yii::app()->params['selenium-video']['fast']:false;
619:     }
620:     /**
621:      * @return boolean true if skip mode is on
622:      */
623:     public function isSkipModeOn(){
624:         return ($this->isFastModeOn() === 2);
625:     }
626: }
627: 
628: 
API documentation generated by ApiGen