5 * Copyright (c) 2002-2009, Sebastian Bergmann <sb@sebastian-bergmann.de>.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
12 * * Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * * Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
20 * * Neither the name of Sebastian Bergmann nor the names of his
21 * contributors may be used to endorse or promote products derived
22 * from this software without specific prior written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35 * POSSIBILITY OF SUCH DAMAGE.
39 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
40 * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
41 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
43 * @link http://www.phpunit.de/
44 * @since File available since Release 3.3.0
47 require_once 'PHPUnit/Util/Filter.php';
49 PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
52 * Implementation of the Selenium RC client/server protocol.
56 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
57 * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
58 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
59 * @version Release: 3.3.17
60 * @link http://www.phpunit.de/
61 * @since Class available since Release 3.3.0
63 class PHPUnit_Extensions_SeleniumTestCase_Driver
66 * @var PHPUnit_Extensions_SeleniumTestCase
88 protected $browserUrl;
93 protected $collectCodeCoverageInformation = FALSE;
98 protected $host = 'localhost';
103 protected $port = 4444;
108 protected $timeout = 30000;
113 protected $sessionId;
118 protected $sleep = 0;
123 protected $useWaitForPageToLoad = TRUE;
133 public function start()
135 if ($this->browserUrl == NULL) {
136 throw new RuntimeException(
137 'setBrowserUrl() needs to be called before start().'
141 if (!isset($this->sessionId)) {
142 $this->sessionId = $this->getString(
143 'getNewBrowserSession',
144 array($this->browser, $this->browserUrl)
147 $this->doCommand('setTimeout', array($this->timeout));
150 return $this->sessionId;
155 public function stop()
157 if (!isset($this->sessionId)) {
161 $this->doCommand('testComplete');
163 $this->sessionId = NULL;
167 * @param boolean $flag
168 * @throws InvalidArgumentException
170 public function setCollectCodeCoverageInformation($flag)
172 if (!is_bool($flag)) {
173 throw new InvalidArgumentException;
176 $this->collectCodeCoverageInformation = $flag;
180 * @param PHPUnit_Extensions_SeleniumTestCase $testCase
182 public function setTestCase(PHPUnit_Extensions_SeleniumTestCase $testCase)
184 $this->testCase = $testCase;
188 * @param integer $testId
190 public function setTestId($testId)
192 $this->testId = $testId;
196 * @param string $name
197 * @throws InvalidArgumentException
199 public function setName($name)
201 if (!is_string($name)) {
202 throw new InvalidArgumentException;
209 * @param string $browser
210 * @throws InvalidArgumentException
212 public function setBrowser($browser)
214 if (!is_string($browser)) {
215 throw new InvalidArgumentException;
218 $this->browser = $browser;
222 * @param string $browserUrl
223 * @throws InvalidArgumentException
225 public function setBrowserUrl($browserUrl)
227 if (!is_string($browserUrl)) {
228 throw new InvalidArgumentException;
231 $this->browserUrl = $browserUrl;
235 * @param string $host
236 * @throws InvalidArgumentException
238 public function setHost($host)
240 if (!is_string($host)) {
241 throw new InvalidArgumentException;
248 * @param integer $port
249 * @throws InvalidArgumentException
251 public function setPort($port)
253 if (!is_int($port)) {
254 throw new InvalidArgumentException;
261 * @param integer $timeout
262 * @throws InvalidArgumentException
264 public function setTimeout($timeout)
266 if (!is_int($timeout)) {
267 throw new InvalidArgumentException;
270 $this->timeout = $timeout;
274 * @param integer $seconds
275 * @throws InvalidArgumentException
277 public function setSleep($seconds)
279 if (!is_int($seconds)) {
280 throw new InvalidArgumentException;
283 $this->sleep = $seconds;
287 * Sets the number of seconds to sleep() after *AndWait commands
288 * when setWaitForPageToLoad(FALSE) is used.
290 * @param integer $seconds
291 * @throws InvalidArgumentException
293 public function setWait($seconds)
295 if (!is_int($seconds)) {
296 throw new InvalidArgumentException;
299 $this->wait = $seconds;
303 * Sets whether waitForPageToLoad (TRUE) or sleep() (FALSE)
304 * is used after *AndWait commands.
306 * @param boolean $flag
307 * @throws InvalidArgumentException
309 public function setWaitForPageToLoad($flag)
311 if (!is_bool($flag)) {
312 throw new InvalidArgumentException;
315 $this->useWaitForPageToLoad = $flag;
319 * This method implements the Selenium RC protocol.
321 * @param string $command
322 * @param array $arguments
324 * @method unknown addLocationStrategy()
325 * @method unknown addSelection()
326 * @method unknown addSelectionAndWait()
327 * @method unknown allowNativeXpath()
328 * @method unknown altKeyDown()
329 * @method unknown altKeyDownAndWait()
330 * @method unknown altKeyUp()
331 * @method unknown altKeyUpAndWait()
332 * @method unknown answerOnNextPrompt()
333 * @method unknown assignId()
334 * @method unknown break()
335 * @method unknown captureEntirePageScreenshot()
336 * @method unknown captureScreenshot()
337 * @method unknown check()
338 * @method unknown chooseCancelOnNextConfirmation()
339 * @method unknown chooseOkOnNextConfirmation()
340 * @method unknown click()
341 * @method unknown clickAndWait()
342 * @method unknown clickAt()
343 * @method unknown clickAtAndWait()
344 * @method unknown close()
345 * @method unknown contextMenu()
346 * @method unknown contextMenuAndWait()
347 * @method unknown contextMenuAt()
348 * @method unknown contextMenuAtAndWait()
349 * @method unknown controlKeyDown()
350 * @method unknown controlKeyDownAndWait()
351 * @method unknown controlKeyUp()
352 * @method unknown controlKeyUpAndWait()
353 * @method unknown createCookie()
354 * @method unknown createCookieAndWait()
355 * @method unknown deleteAllVisibleCookies()
356 * @method unknown deleteAllVisibleCookiesAndWait()
357 * @method unknown deleteCookie()
358 * @method unknown deleteCookieAndWait()
359 * @method unknown doubleClick()
360 * @method unknown doubleClickAndWait()
361 * @method unknown doubleClickAt()
362 * @method unknown doubleClickAtAndWait()
363 * @method unknown dragAndDrop()
364 * @method unknown dragAndDropAndWait()
365 * @method unknown dragAndDropToObject()
366 * @method unknown dragAndDropToObjectAndWait()
367 * @method unknown dragDrop()
368 * @method unknown dragDropAndWait()
369 * @method unknown echo()
370 * @method unknown fireEvent()
371 * @method unknown fireEventAndWait()
372 * @method unknown focus()
373 * @method string getAlert()
374 * @method array getAllButtons()
375 * @method array getAllFields()
376 * @method array getAllLinks()
377 * @method array getAllWindowIds()
378 * @method array getAllWindowNames()
379 * @method array getAllWindowTitles()
380 * @method string getAttribute()
381 * @method array getAttributeFromAllWindows()
382 * @method string getBodyText()
383 * @method string getConfirmation()
384 * @method string getCookie()
385 * @method integer getCursorPosition()
386 * @method integer getElementHeight()
387 * @method integer getElementIndex()
388 * @method integer getElementPositionLeft()
389 * @method integer getElementPositionTop()
390 * @method integer getElementWidth()
391 * @method string getEval()
392 * @method string getExpression()
393 * @method string getHtmlSource()
394 * @method string getLocation()
395 * @method string getLogMessages()
396 * @method integer getMouseSpeed()
397 * @method string getPrompt()
398 * @method array getSelectOptions()
399 * @method string getSelectedId()
400 * @method array getSelectedIds()
401 * @method string getSelectedIndex()
402 * @method array getSelectedIndexes()
403 * @method string getSelectedLabel()
404 * @method array getSelectedLabels()
405 * @method string getSelectedValue()
406 * @method array getSelectedValues()
407 * @method unknown getSpeed()
408 * @method unknown getSpeedAndWait()
409 * @method string getTable()
410 * @method string getText()
411 * @method string getTitle()
412 * @method string getValue()
413 * @method boolean getWhetherThisFrameMatchFrameExpression()
414 * @method boolean getWhetherThisWindowMatchWindowExpression()
415 * @method integer getXpathCount()
416 * @method unknown goBack()
417 * @method unknown goBackAndWait()
418 * @method unknown highlight()
419 * @method unknown highlightAndWait()
420 * @method unknown ignoreAttributesWithoutValue()
421 * @method boolean isAlertPresent()
422 * @method boolean isChecked()
423 * @method boolean isConfirmationPresent()
424 * @method boolean isEditable()
425 * @method boolean isElementPresent()
426 * @method boolean isOrdered()
427 * @method boolean isPromptPresent()
428 * @method boolean isSomethingSelected()
429 * @method boolean isTextPresent()
430 * @method boolean isVisible()
431 * @method unknown keyDown()
432 * @method unknown keyDownAndWait()
433 * @method unknown keyPress()
434 * @method unknown keyPressAndWait()
435 * @method unknown keyUp()
436 * @method unknown keyUpAndWait()
437 * @method unknown metaKeyDown()
438 * @method unknown metaKeyDownAndWait()
439 * @method unknown metaKeyUp()
440 * @method unknown metaKeyUpAndWait()
441 * @method unknown mouseDown()
442 * @method unknown mouseDownAndWait()
443 * @method unknown mouseDownAt()
444 * @method unknown mouseDownAtAndWait()
445 * @method unknown mouseMove()
446 * @method unknown mouseMoveAndWait()
447 * @method unknown mouseMoveAt()
448 * @method unknown mouseMoveAtAndWait()
449 * @method unknown mouseOut()
450 * @method unknown mouseOutAndWait()
451 * @method unknown mouseOver()
452 * @method unknown mouseOverAndWait()
453 * @method unknown mouseUp()
454 * @method unknown mouseUpAndWait()
455 * @method unknown mouseUpAt()
456 * @method unknown mouseUpAtAndWait()
457 * @method unknown mouseUpRight()
458 * @method unknown mouseUpRightAndWait()
459 * @method unknown mouseUpRightAt()
460 * @method unknown mouseUpRightAtAndWait()
461 * @method unknown open()
462 * @method unknown openWindow()
463 * @method unknown openWindowAndWait()
464 * @method unknown pause()
465 * @method unknown refresh()
466 * @method unknown refreshAndWait()
467 * @method unknown removeAllSelections()
468 * @method unknown removeAllSelectionsAndWait()
469 * @method unknown removeSelection()
470 * @method unknown removeSelectionAndWait()
471 * @method unknown runScript()
472 * @method unknown select()
473 * @method unknown selectAndWait()
474 * @method unknown selectFrame()
475 * @method unknown selectWindow()
476 * @method unknown setBrowserLogLevel()
477 * @method unknown setContext()
478 * @method unknown setCursorPosition()
479 * @method unknown setCursorPositionAndWait()
480 * @method unknown setMouseSpeed()
481 * @method unknown setMouseSpeedAndWait()
482 * @method unknown setSpeed()
483 * @method unknown setSpeedAndWait()
484 * @method unknown shiftKeyDown()
485 * @method unknown shiftKeyDownAndWait()
486 * @method unknown shiftKeyUp()
487 * @method unknown shiftKeyUpAndWait()
488 * @method unknown store()
489 * @method unknown storeAlert()
490 * @method unknown storeAlertPresent()
491 * @method unknown storeAllButtons()
492 * @method unknown storeAllFields()
493 * @method unknown storeAllLinks()
494 * @method unknown storeAllWindowIds()
495 * @method unknown storeAllWindowNames()
496 * @method unknown storeAllWindowTitle()s
497 * @method unknown storeAttribute()
498 * @method unknown storeAttributeFromAllWindows()
499 * @method unknown storeBodyText()
500 * @method unknown storeChecked()
501 * @method unknown storeConfirmation()
502 * @method unknown storeConfirmationPresent()
503 * @method unknown storeCookie()
504 * @method unknown storeCookieByName()
505 * @method unknown storeCookiePresent()
506 * @method unknown storeCursorPosition()
507 * @method unknown storeEditable()
508 * @method unknown storeElementHeight()
509 * @method unknown storeElementIndex()
510 * @method unknown storeElementPositionLeft()
511 * @method unknown storeElementPositionTop()
512 * @method unknown storeElementPresent()
513 * @method unknown storeElementWidth()
514 * @method unknown storeEval()
515 * @method unknown storeExpression()
516 * @method unknown storeHtmlSource()
517 * @method unknown storeLocation()
518 * @method unknown storeMouseSpeed()
519 * @method unknown storeOrdered()
520 * @method unknown storePrompt()
521 * @method unknown storePromptPresent()
522 * @method unknown storeSelectOptions()
523 * @method unknown storeSelectedId()
524 * @method unknown storeSelectedIds()
525 * @method unknown storeSelectedIndex()
526 * @method unknown storeSelectedIndexes()
527 * @method unknown storeSelectedLabel()
528 * @method unknown storeSelectedLabels()
529 * @method unknown storeSelectedValue()
530 * @method unknown storeSelectedValues()
531 * @method unknown storeSomethingSelected()
532 * @method unknown storeSpeed()
533 * @method unknown storeTable()
534 * @method unknown storeText()
535 * @method unknown storeTextPresent()
536 * @method unknown storeTitle()
537 * @method unknown storeValue()
538 * @method unknown storeVisible()
539 * @method unknown storeWhetherThisFrameMatchFrameExpression()
540 * @method unknown storeWhetherThisWindowMatchWindowExpression()
541 * @method unknown storeXpathCount()
542 * @method unknown submit()
543 * @method unknown submitAndWait()
544 * @method unknown type()
545 * @method unknown typeAndWait()
546 * @method unknown typeKeys()
547 * @method unknown typeKeysAndWait()
548 * @method unknown uncheck()
549 * @method unknown uncheckAndWait()
550 * @method unknown waitForCondition()
551 * @method unknown waitForPageToLoad()
552 * @method unknown waitForPopUp()
553 * @method unknown windowFocus()
554 * @method unknown windowMaximize()
556 public function __call($command, $arguments)
560 if (substr($command, -7, 7) == 'AndWait') {
561 $command = substr($command, 0, -7);
566 case 'addLocationStrategy':
568 case 'allowNativeXpath':
571 case 'answerOnNextPrompt':
574 case 'captureEntirePageScreenshot':
575 case 'captureScreenshot':
577 case 'chooseCancelOnNextConfirmation':
578 case 'chooseOkOnNextConfirmation':
583 case 'contextMenuAt':
584 case 'controlKeyDown':
587 case 'deleteAllVisibleCookies':
590 case 'doubleClickAt':
592 case 'dragAndDropToObject':
599 case 'ignoreAttributesWithoutValue':
614 case 'mouseUpRightAt':
619 case 'removeAllSelections':
620 case 'removeSelection':
625 case 'setBrowserLogLevel':
627 case 'setCursorPosition':
628 case 'setMouseSpeed':
634 case 'storeAlertPresent':
635 case 'storeAllButtons':
636 case 'storeAllFields':
637 case 'storeAllLinks':
638 case 'storeAllWindowIds':
639 case 'storeAllWindowNames':
640 case 'storeAllWindowTitles':
641 case 'storeAttribute':
642 case 'storeAttributeFromAllWindows':
643 case 'storeBodyText':
645 case 'storeConfirmation':
646 case 'storeConfirmationPresent':
648 case 'storeCookieByName':
649 case 'storeCookiePresent':
650 case 'storeCursorPosition':
651 case 'storeEditable':
652 case 'storeElementHeight':
653 case 'storeElementIndex':
654 case 'storeElementPositionLeft':
655 case 'storeElementPositionTop':
656 case 'storeElementPresent':
657 case 'storeElementWidth':
659 case 'storeExpression':
660 case 'storeHtmlSource':
661 case 'storeLocation':
662 case 'storeMouseSpeed':
665 case 'storePromptPresent':
666 case 'storeSelectOptions':
667 case 'storeSelectedId':
668 case 'storeSelectedIds':
669 case 'storeSelectedIndex':
670 case 'storeSelectedIndexes':
671 case 'storeSelectedLabel':
672 case 'storeSelectedLabels':
673 case 'storeSelectedValue':
674 case 'storeSelectedValues':
675 case 'storeSomethingSelected':
679 case 'storeTextPresent':
683 case 'storeWhetherThisFrameMatchFrameExpression':
684 case 'storeWhetherThisWindowMatchWindowExpression':
685 case 'storeXpathCount':
691 case 'windowMaximize': {
692 // Pre-Command Actions
696 if ($this->collectCodeCoverageInformation) {
697 $this->deleteCookie('PHPUNIT_SELENIUM_TEST_ID', 'path=/');
700 'PHPUNIT_SELENIUM_TEST_ID=' . $this->testId,
708 $this->doCommand($command, $arguments);
710 // Post-Command Actions
712 case 'addLocationStrategy':
713 case 'allowNativeXpath':
715 case 'captureScreenshot': {
716 // intentionally empty
722 if ($this->useWaitForPageToLoad) {
723 $this->waitForPageToLoad($this->timeout);
729 if ($this->sleep > 0) {
733 $this->testCase->runDefaultAssertions($command);
739 case 'getWhetherThisFrameMatchFrameExpression':
740 case 'getWhetherThisWindowMatchWindowExpression':
741 case 'isAlertPresent':
743 case 'isConfirmationPresent':
745 case 'isElementPresent':
747 case 'isPromptPresent':
748 case 'isSomethingSelected':
749 case 'isTextPresent':
751 return $this->getBoolean($command, $arguments);
755 case 'getCursorPosition':
756 case 'getElementHeight':
757 case 'getElementIndex':
758 case 'getElementPositionLeft':
759 case 'getElementPositionTop':
760 case 'getElementWidth':
761 case 'getMouseSpeed':
763 case 'getXpathCount': {
764 $result = $this->getNumber($command, $arguments);
767 $this->waitForPageToLoad($this->timeout);
777 case 'getConfirmation':
780 case 'getExpression':
781 case 'getHtmlSource':
783 case 'getLogMessages':
785 case 'getSelectedId':
786 case 'getSelectedIndex':
787 case 'getSelectedLabel':
788 case 'getSelectedValue':
793 $result = $this->getString($command, $arguments);
796 $this->waitForPageToLoad($this->timeout);
803 case 'getAllButtons':
806 case 'getAllWindowIds':
807 case 'getAllWindowNames':
808 case 'getAllWindowTitles':
809 case 'getAttributeFromAllWindows':
810 case 'getSelectedIds':
811 case 'getSelectedIndexes':
812 case 'getSelectedLabels':
813 case 'getSelectedValues':
814 case 'getSelectOptions': {
815 $result = $this->getStringArray($command, $arguments);
818 $this->waitForPageToLoad($this->timeout);
825 case 'waitForCondition':
826 case 'waitForFrameToLoad':
827 case 'waitForPopUp': {
828 if (count($arguments) == 1) {
829 $arguments[] = $this->timeout;
832 $this->doCommand($command, $arguments);
833 $this->testCase->runDefaultAssertions($command);
837 case 'waitForPageToLoad': {
838 if (empty($arguments)) {
839 $arguments[] = $this->timeout;
842 $this->doCommand($command, $arguments);
843 $this->testCase->runDefaultAssertions($command);
850 throw new BadMethodCallException(
851 "Method $command not defined."
858 * Send a command to the Selenium RC server.
860 * @param string $command
861 * @param array $arguments
863 * @author Shin Ohno <ganchiku@gmail.com>
864 * @author Bjoern Schotte <schotte@mayflower.de>
866 protected function doCommand($command, array $arguments = array())
868 if (!ini_get('allow_url_fopen')) {
869 throw new RuntimeException(
870 'Could not connect to the Selenium RC server because allow_url_fopen is disabled.'
875 'http://%s:%s/selenium-server/driver/?cmd=%s',
881 $numArguments = count($arguments);
883 for ($i = 0; $i < $numArguments; $i++) {
884 $argNum = strval($i + 1);
885 $url .= sprintf('&%s=%s', $argNum, urlencode(trim($arguments[$i])));
888 if (isset($this->sessionId)) {
889 $url .= sprintf('&%s=%s', 'sessionId', $this->sessionId);
892 $handle = @fopen($url, 'r');
895 throw new RuntimeException(
896 'Could not connect to the Selenium RC server.'
900 stream_set_blocking($handle, 1);
901 stream_set_timeout($handle, 0, $this->timeout);
903 $info = stream_get_meta_data($handle);
906 while (!$info['eof'] && !$info['timed_out']) {
907 $response .= fgets($handle, 4096);
908 $info = stream_get_meta_data($handle);
913 if (!preg_match('/^OK/', $response)) {
916 throw new RuntimeException(
917 'The response from the Selenium RC server is invalid: ' . $response
925 * Send a command to the Selenium RC server and treat the result
928 * @param string $command
929 * @param array $arguments
931 * @author Shin Ohno <ganchiku@gmail.com>
932 * @author Bjoern Schotte <schotte@mayflower.de>
934 protected function getBoolean($command, array $arguments)
936 $result = $this->getString($command, $arguments);
939 case 'true': return TRUE;
941 case 'false': return FALSE;
946 throw new RuntimeException(
947 'Result is neither "true" nor "false": ' . PHPUnit_Util_Type::toString($result, TRUE)
954 * Send a command to the Selenium RC server and treat the result
957 * @param string $command
958 * @param array $arguments
960 * @author Shin Ohno <ganchiku@gmail.com>
961 * @author Bjoern Schotte <schotte@mayflower.de>
963 protected function getNumber($command, array $arguments)
965 $result = $this->getString($command, $arguments);
967 if (!is_numeric($result)) {
970 throw new RuntimeException(
971 'Result is not numeric: ' . PHPUnit_Util_Type::toString($result, TRUE)
979 * Send a command to the Selenium RC server and treat the result
982 * @param string $command
983 * @param array $arguments
985 * @author Shin Ohno <ganchiku@gmail.com>
986 * @author Bjoern Schotte <schotte@mayflower.de>
988 protected function getString($command, array $arguments)
991 $result = $this->doCommand($command, $arguments);
994 catch (RuntimeException $e) {
1000 return (strlen($result) > 3) ? substr($result, 3) : '';
1004 * Send a command to the Selenium RC server and treat the result
1005 * as an array of strings.
1007 * @param string $command
1008 * @param array $arguments
1010 * @author Shin Ohno <ganchiku@gmail.com>
1011 * @author Bjoern Schotte <schotte@mayflower.de>
1013 protected function getStringArray($command, array $arguments)
1015 $csv = $this->getString($command, $arguments);
1018 $letters = preg_split('//', $csv, -1, PREG_SPLIT_NO_EMPTY);
1019 $count = count($letters);
1021 for ($i = 0; $i < $count; $i++) {
1022 $letter = $letters[$i];
1026 $letter = $letters[++$i];