├── .gitignore ├── CliController.php ├── Commands.php ├── Commands2.php ├── Converter.php ├── LICENSE ├── README.md ├── Selenium2TestCaseTpl.tpl ├── SeleniumTestCaseTpl.tpl ├── libs ├── arguments.php └── simple_html_dom.php └── selenium2php.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject -------------------------------------------------------------------------------- /CliController.php: -------------------------------------------------------------------------------- 1 | _converter = new Converter; 38 | } 39 | 40 | protected function _printTitle() { 41 | print "Selenium2php converts Selenium HTML tests into PHPUnit test case code."; 42 | print "\n"; 43 | print "\n"; 44 | } 45 | 46 | protected function _printHelp() { 47 | print "Usage: selenium2php [switches] Test.html [Test.php]"; 48 | print "\n"; 49 | print " selenium2php [switches] "; 50 | print "\n"; 51 | print "\n"; 52 | print " --dest= Destination folder.\n"; 53 | print " --selenium2 Use Selenium2 tests format.\n"; 54 | print " --php-prefix= Add prefix to php filenames.\n"; 55 | print " --php-postfix= Add postfix to php filenames.\n"; 56 | print " --browser= Set browser for tests.\n"; 57 | print " --browser-url= Set URL for tests.\n"; 58 | print " --remote-host= Set Selenium server address for tests.\n"; 59 | print " --remote-port= Set Selenium server port for tests.\n"; 60 | print " -r|--recursive Use subdirectories for converting.\n"; 61 | print " --class-prefix= Set TestCase class prefix.\n"; 62 | print " --use-hash-postfix Add hash part to output filename.\n"; 63 | print " --files-pattern= Glob pattern for input test files (*.html).\n"; 64 | print " --output-tpl= Template for result file. See TestExampleTpl.\n"; 65 | print " --custom-param1= Assign value to \$customParam1 in template.\n"; 66 | print " --custom-param2= Assign value to \$customParam2 in template.\n"; 67 | } 68 | 69 | protected function _applyOptionsAndFlags($options, $flags){ 70 | if (is_array($options)){ 71 | foreach ($options as $opt){ 72 | if (is_string($opt)){ 73 | switch ($opt){ 74 | case 'recursive': 75 | $this->_recursive = true; 76 | break; 77 | case 'use-hash-postfix': 78 | $this->_useHashFilePostfix = true; 79 | break; 80 | case 'selenium2': 81 | $this->_converter->useSelenium2(); 82 | break; 83 | default: 84 | print "Unknown option \"$opt\".\n"; 85 | exit(1); 86 | } 87 | } else if (is_array($opt)){ 88 | switch ($opt[0]){ 89 | case 'php-prefix': 90 | $this->_phpFilePrefix = $opt[1]; 91 | break; 92 | case 'php-postfix': 93 | $this->_phpFilePostfix = $opt[1]; 94 | break; 95 | case 'browser': 96 | $this->_converter->setBrowser($opt[1]); 97 | break; 98 | case 'browser-url': 99 | $this->_converter->setTestUrl($opt[1]); 100 | break; 101 | case 'remote-host': 102 | $this->_converter->setRemoteHost($opt[1]); 103 | break; 104 | case 'remote-port': 105 | $this->_converter->setRemotePort($opt[1]); 106 | break; 107 | case 'dest': 108 | $this->_destFolder = $opt[1]; 109 | break; 110 | case 'class-prefix': 111 | $this->_converter->setTplClassPrefix($opt[1]); 112 | break; 113 | case 'use-hash-postfix': 114 | $this->_useHashFilePostfix = true; 115 | break; 116 | case 'files-pattern': 117 | $this->_htmlPattern = $opt[1]; 118 | break; 119 | case 'output-tpl': 120 | $this->_tplFile = $opt[1]; 121 | break; 122 | case 'custom-param1': 123 | $this->_converter->setTplCustomParam1($opt[1]); 124 | break; 125 | case 'custom-param2': 126 | $this->_converter->setTplCustomParam2($opt[1]); 127 | break; 128 | default: 129 | print "Unknown option \"{$opt[0]}\".\n"; 130 | exit(1); 131 | } 132 | } 133 | } 134 | } 135 | 136 | if (is_array($flags)){ 137 | foreach ($flags as $flag){ 138 | switch($flag){ 139 | case 'r': 140 | $this->_recursive = true; 141 | break; 142 | default: 143 | print "Unknown flag \"$flag\".\n"; 144 | exit(1); 145 | } 146 | } 147 | } 148 | } 149 | 150 | public function run($arguments, $options, $flags) { 151 | $this->_printTitle(); 152 | $this->_applyOptionsAndFlags($options, $flags); 153 | if (empty($arguments)) { 154 | $this->_printHelp(); 155 | } else if (!empty($arguments)) { 156 | $first = array_shift($arguments); 157 | $second = array_shift($arguments); 158 | if ($first && is_string($first)) { 159 | if (is_file($first)) { 160 | $htmlFileName = $first; 161 | if (is_readable($htmlFileName)) { 162 | if ($second && is_string($second)) { 163 | $phpFileName = $second; 164 | } else { 165 | $phpFileName = ''; 166 | } 167 | $this->_sourceBaseDir = rtrim(dirname($htmlFileName), "\\/")."/"; 168 | $this->convertFile($htmlFileName, $phpFileName); 169 | print "OK.\n"; 170 | exit(0); 171 | } else { 172 | print "Cannot open file \"$htmlFileName\".\n"; 173 | exit(1); 174 | } 175 | } else if (is_dir($first)) { 176 | $dir = rtrim($first, "\\/")."/"; 177 | $this->_sourceBaseDir = $dir; 178 | $res = $this->convertFilesInDirectory($dir); 179 | if ($res){ 180 | print "OK.\n"; 181 | exit(0); 182 | } else { 183 | exit(1); 184 | } 185 | } else { 186 | print "\"$first\" is not existing file or directory.\n"; 187 | exit(1); 188 | } 189 | } 190 | } 191 | } 192 | 193 | protected function convertFilesInDirectory($dir){ 194 | if ($this->_recursive){ 195 | $files = $this->globRecursive($dir . $this->_htmlPattern, GLOB_NOSORT); 196 | } else { 197 | $files = glob($dir . $this->_htmlPattern, GLOB_NOSORT); 198 | } 199 | if (count($files)){ 200 | foreach ($files as $htmlFile){ 201 | $this->convertFile($htmlFile); 202 | } 203 | return true; 204 | } else { 205 | print "Files \"{$this->_htmlPattern}\" not found in \"$dir\"."; 206 | } 207 | return false; 208 | } 209 | 210 | protected function _makeOutputFilename($htmlFileName, $htmlContent) { 211 | $fileName = $this->_makeTestName($htmlFileName); 212 | 213 | if ($this->_destFolder) { 214 | $filePath = rtrim($this->_destFolder, "\\/") . "/"; 215 | if (!realpath($filePath)) { 216 | //path is not absolute 217 | $filePath = $this->_sourceBaseDir . $filePath; 218 | if (!realpath($filePath)) { 219 | print "Directory \"$filePath\" not found.\n"; 220 | exit(1); 221 | } 222 | } 223 | } else { 224 | $filePath = dirname($htmlFileName) . "/"; 225 | } 226 | 227 | if ($this->_useHashFilePostfix) { 228 | $hashPostfix = '_' . substr(md5($htmlContent), 0, 8) . '_'; 229 | } else { 230 | $hashPostfix = ''; 231 | } 232 | 233 | $phpFileName = $filePath . $this->_phpFilePrefix 234 | . preg_replace("/\..+$/", '', $fileName) 235 | . $hashPostfix 236 | . $this->_phpFilePostfix . ".php"; 237 | return $phpFileName; 238 | } 239 | 240 | /** 241 | * Makes output test name considering path. 242 | * 243 | * If destination folder is not defined 244 | * returns base name of html file without extension. 245 | * Example: 246 | * auth/login/simple.html -> simple 247 | * 248 | * If destination folder is defined 249 | * returns base name of html file prefixed with 250 | * name of folder accordingly to destination folder. 251 | * Example: 252 | * auth/login/simple.html -> Auth_login_simple 253 | * 254 | * @param string $htmlFileName input file name 255 | * @return string output test name 256 | */ 257 | protected function _makeTestName($htmlFileName){ 258 | /* get from file if this is empty */ 259 | $testName = basename($htmlFileName); 260 | 261 | if ($this->_destFolder) { 262 | $absPath = str_replace('\\', '_', $htmlFileName); 263 | $absPath = str_replace('/', '_', $absPath); 264 | $destPath = str_replace('\\', '_', $this->_sourceBaseDir); 265 | $destPath = str_replace('/', '_', $destPath); 266 | $testName= str_replace($destPath, '', $absPath); 267 | } 268 | 269 | $testName = ucfirst(preg_replace("/\..+$/", '', $testName)); 270 | return $testName; 271 | } 272 | 273 | public function convertFile($htmlFileName, $phpFileName = '') { 274 | $htmlContent = file_get_contents($htmlFileName); 275 | if ($htmlContent) { 276 | if (!$phpFileName) { 277 | $phpFileName = $this->_makeOutputFilename($htmlFileName, $htmlContent); 278 | } 279 | $result = $this->_converter->convert($htmlContent, $this->_makeTestName($htmlFileName), $this->_tplFile); 280 | file_put_contents($phpFileName, $result); 281 | print $phpFileName."\n"; 282 | } 283 | } 284 | 285 | protected function globRecursive($pattern, $flags) { 286 | 287 | $files = glob($pattern, $flags); 288 | foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { 289 | $files = array_merge($files, $this->globRecursive($dir . '/' . basename($pattern), $flags)); 290 | } 291 | 292 | return $files; 293 | } 294 | } -------------------------------------------------------------------------------- /Commands.php: -------------------------------------------------------------------------------- 1 | waitForElementPresent("css=div.route-view > span") 25 | * converts into: 26 | * 27 | * for ($second = 0;; $second++) { 28 | * if ($second >= 60) $this->fail("timeout"); 29 | * try { 30 | * if ($this->isElementPresent("css=div.route-view > span")) break; 31 | * } catch (Exception $e) {} 32 | * sleep(1); 33 | * } 34 | * 35 | * 36 | * Magic method __call leaves unmentioned commads as is. 37 | */ 38 | class Commands { 39 | 40 | protected $_obj = '$this'; 41 | 42 | /** 43 | * 44 | * @param string $name 45 | * @param string $arguments 46 | * @return string 47 | */ 48 | public function __call($name, $arguments) { 49 | if (isset($arguments[1]) && false !== $arguments[1]){ 50 | $line = "{$this->_obj}->$name(\"{$arguments[0]}\", \"{$arguments[1]}\");"; 51 | } else if (false !== $arguments[0]) { 52 | $line = "{$this->_obj}->$name(\"{$arguments[0]}\");"; 53 | } else { 54 | $line = "{$this->_obj}->$name();"; 55 | } 56 | return $line; 57 | } 58 | 59 | /** 60 | * 61 | * @param string $target 62 | * @return array 63 | */ 64 | public function clickAndWait($target) { 65 | $lines = array(); 66 | $lines[] = "{$this->_obj}->click(\"$target\");"; 67 | $lines[] = "{$this->_obj}->waitForPageToLoad(\"30000\");"; 68 | return $lines; 69 | } 70 | 71 | /** 72 | * 73 | * @param string $target 74 | * @param string $value 75 | * @return string 76 | */ 77 | public function assertText($target, $value){ 78 | return "{$this->_obj}->assertEquals(\"$value\", {$this->_obj}->getText(\"$target\"));"; 79 | } 80 | 81 | /** 82 | * 83 | * @param string $target 84 | * @return string 85 | */ 86 | public function assertElementPresent($target){ 87 | return $this->_assertTrue("{$this->_obj}->isElementPresent(\"$target\")"); 88 | } 89 | 90 | /** 91 | * 92 | * @param string $target 93 | * @return string 94 | */ 95 | public function assertElementNotPresent($target){ 96 | return $this->_assertFalse("{$this->_obj}->isElementPresent(\"$target\")"); 97 | } 98 | 99 | 100 | /** 101 | * 102 | * @param string $target 103 | * @return array 104 | */ 105 | public function waitForElementPresent($target){ 106 | return $this->_waitWrapper("{$this->_obj}->isElementPresent(\"$target\")"); 107 | } 108 | 109 | /** 110 | * 111 | * @param string $target 112 | * @return array 113 | */ 114 | public function waitForElementNotPresent($target){ 115 | return $this->_waitWrapper("!{$this->_obj}->isElementPresent(\"$target\")"); 116 | } 117 | 118 | /** 119 | * 120 | * @param string $target 121 | * @return array 122 | */ 123 | public function waitForTextPresent($target){ 124 | return $this->_waitWrapper("{$this->_obj}->isTextPresent(\"$target\")"); 125 | } 126 | 127 | /** 128 | * 129 | * @param string $target 130 | * @return array 131 | */ 132 | public function waitForTextNotPresent($target){ 133 | return $this->_waitWrapper("!{$this->_obj}->isTextPresent(\"$target\")"); 134 | } 135 | 136 | /** 137 | * 138 | * @param string $expression 139 | * @return array 140 | */ 141 | protected function _waitWrapper($expression){ 142 | $lines = array(); 143 | $lines[] = 'for ($second = 0; ; $second++) {'; 144 | $lines[] = ' if ($second >= 60) '.$this->_obj.'->fail("timeout");'; 145 | $lines[] = ' try {'; 146 | $lines[] = " if ($expression) break;"; 147 | $lines[] = ' } catch (Exception $e) {}'; 148 | $lines[] = ' sleep(1);'; 149 | $lines[] = '}'; 150 | return $lines; 151 | } 152 | 153 | /** 154 | * 155 | * @param string $expression 156 | * @return string 157 | */ 158 | protected function _assertFalse($expression){ 159 | return "{$this->_obj}->assertFalse($expression);"; 160 | } 161 | 162 | /** 163 | * 164 | * @param string $expression 165 | * @return string 166 | */ 167 | protected function _assertTrue($expression){ 168 | return "{$this->_obj}->assertTrue($expression);"; 169 | } 170 | 171 | protected function _assertPattern($target, $string){ 172 | $target = str_replace("?", "[\s\S]", $target); 173 | $expression = "(bool)preg_match('/^$target$/', " . $string . ")"; 174 | return $expression; 175 | } 176 | 177 | /** 178 | * 179 | * @param string $target 180 | * @return string 181 | */ 182 | public function assertConfirmation($target){ 183 | $target = str_replace("?", "[\s\S]", $target); 184 | $expression = $this->_assertPattern($target, '$this->getConfirmation()'); 185 | return $this->_assertTrue($expression); 186 | } 187 | 188 | /** 189 | * 190 | * @return array 191 | */ 192 | public function verifyConfirmation($target) { 193 | $expression = $this->_assertTrue($this->_assertPattern($target, '$this->getConfirmation()')); 194 | $lines = array(); 195 | $lines[] = 'try {'; 196 | $lines[] = ' '.$expression; 197 | $lines[] = '} catch (PHPUnit_Framework_AssertionFailedError $e) {'; 198 | $lines[] = ' array_push($this->verificationErrors, $e->toString());'; 199 | $lines[] = '}'; 200 | return $lines; 201 | } 202 | 203 | public function assertTextPresent($target){ 204 | return $this->_assertTrue("{$this->_obj}->isTextPresent(\"$target\")"); 205 | } 206 | 207 | public function assertTextNotPresent($target){ 208 | return $this->_assertFalse("{$this->_obj}->isTextPresent(\"$target\")"); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Commands2.php: -------------------------------------------------------------------------------- 1 | _obj}->$name(\"{$arguments[0]}\", \"{$arguments[1]}\");"; 41 | $this->_addNote('Unknown command', $name, $arguments); 42 | } else if (false !== $arguments[0]) { 43 | $line = "{$this->_obj}->$name(\"{$arguments[0]}\")"; 44 | } else { 45 | $line = "{$this->_obj}->$name()"; 46 | } 47 | return $line; 48 | } 49 | 50 | protected function _addNote($noteText, $commandName, $arguments = array()){ 51 | if (is_string($arguments)){ 52 | $arguments = array($arguments); 53 | } 54 | echo "$noteText - $commandName('" . implode("', '", $arguments). "')\n"; 55 | } 56 | 57 | public function open($target) { 58 | return "{$this->_obj}->url(\"$target\");"; 59 | } 60 | 61 | public function type($selector, $value) { 62 | $lines = array(); 63 | $lines[] = '$input = ' . $this->_byQuery($selector) . ';'; 64 | $lines[] = '$input->clear();'; 65 | $lines[] = '$input->value("' . $value . '");'; 66 | return $lines; 67 | } 68 | 69 | /** 70 | * Sends key by key 71 | * 72 | * In PHPUnit 3.7.27 $this->keys() is not implemented, 73 | * but $this->value() does it key by key. 74 | * 75 | * @param type $selector 76 | * @param type $value 77 | * @return string 78 | */ 79 | public function sendKeys($selector, $value){ 80 | $lines = array(); 81 | $lines[] = '$input = ' . $this->_byQuery($selector) . ';'; 82 | $lines[] = '$input->value("' . $value . '");'; 83 | return $lines; 84 | } 85 | 86 | protected function _byQuery($selector) { 87 | if (preg_match('/^\/\/(.+)/', $selector)) { 88 | /* "//a[contains(@href, '?logout')]" */ 89 | return $this->byXPath($selector); 90 | } else if (preg_match('/^([a-z]+)=(.+)/', $selector, $match)) { 91 | /* "id=login_name" */ 92 | switch ($match[1]) { 93 | case 'id': 94 | return $this->byId($match[2]); 95 | break; 96 | case 'name': 97 | return $this->byName($match[2]); 98 | break; 99 | case 'link': 100 | return $this->byLinkText($match[2]); 101 | break; 102 | case 'xpath': 103 | return $this->byXPath($match[2]); 104 | break; 105 | case 'css': 106 | $cssSelector = str_replace('..', '.', $match[2]); 107 | return $this->byCssSelector($cssSelector); 108 | break; 109 | } 110 | } 111 | throw new \Exception("Unknown selector '$selector'"); 112 | } 113 | 114 | public function click($selector) { 115 | $lines = array(); 116 | $lines[] = '$input = ' . $this->_byQuery($selector) . ';'; 117 | $lines[] = '$input->click();'; 118 | return $lines; 119 | } 120 | 121 | public function select($selectSelector, $optionSelector) { 122 | $lines = array(); 123 | $lines[] = '$element = ' . $this->_byQuery($selectSelector) . ';'; 124 | $lines[] = '$selectElement = ' . $this->_obj . '->select($element);'; 125 | 126 | if (preg_match('/label=(.+)/', $optionSelector, $match)) { 127 | $lines[] = '$selectElement->selectOptionByLabel("' . $match[1] . '");'; 128 | } else if (preg_match('/value=(.+)/', $optionSelector, $match)) { 129 | $lines[] = '$selectElement->selectOptionByValue("' . $match[1] . '");'; 130 | } else { 131 | throw new \Exception("Unknown option selector '$optionSelector'"); 132 | } 133 | 134 | return $lines; 135 | } 136 | 137 | /** 138 | * 139 | * @param string $target 140 | * @return array 141 | */ 142 | public function clickAndWait($target) { 143 | return $this->click($target); 144 | } 145 | 146 | /** 147 | * 148 | * @param string $target 149 | * @param string $value 150 | * @return string 151 | */ 152 | public function assertText($target, $value) { 153 | $lines = array(); 154 | $lines[] = '$input = ' . $this->_byQuery($target) . ';'; 155 | 156 | if (strpos($value, '*')) { 157 | $value = '/' . str_replace('*', '.+', $value) . '/'; 158 | $lines[] = "{$this->_obj}->assertRegExp(\"$value\", \$input->text());"; 159 | } else { 160 | $lines[] = "{$this->_obj}->assertEquals(\"$value\", \$input->text());"; 161 | } 162 | 163 | return $lines; 164 | } 165 | 166 | /** 167 | * 168 | * @param string $target 169 | * @param string $value 170 | * @return string 171 | */ 172 | public function assertNotText($target, $value) { 173 | $lines = array(); 174 | $lines[] = '$input = ' . $this->_byQuery($target) . ';'; 175 | 176 | if (strpos($value, '*')) { 177 | $value = '/' . str_replace('*', '.+', $value) . '/'; 178 | $lines[] = "{$this->_obj}->assertNotRegExp(\"$value\", \$input->text());"; 179 | } else { 180 | $lines[] = "{$this->_obj}->assertNotEquals(\"$value\", \$input->text());"; 181 | } 182 | 183 | return $lines; 184 | } 185 | 186 | /** 187 | * 188 | * @param string $target 189 | * @return string 190 | */ 191 | public function assertElementPresent($target) { 192 | $lines = array(); 193 | $lines[] = 'try {'; 194 | $lines[] = " " . $this->_byQuery($target) . ';'; 195 | $lines[] = " {$this->_obj}->assertTrue(true);"; 196 | $lines[] = '} catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {'; 197 | $lines[] = " if (PHPUnit_Extensions_Selenium2TestCase_WebDriverException::NoSuchElement === \$e->getCode()) {"; 198 | $lines[] = " {$this->_obj}->assertTrue(false, \"Element $target not found\");"; 199 | $lines[] = " } else { "; 200 | $lines[] = " throw \$e;"; 201 | $lines[] = " }"; 202 | $lines[] = '}'; 203 | return $lines; 204 | } 205 | 206 | /** 207 | * 208 | * @param string $target 209 | * @return string 210 | */ 211 | public function assertElementNotPresent($target) { 212 | $lines = array(); 213 | $lines[] = 'try {'; 214 | $lines[] = " " . $this->_byQuery($target) . ';'; 215 | $lines[] = " {$this->_obj}->assertTrue(false, \"Element $target was found\");"; 216 | $lines[] = '} catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {'; 217 | $lines[] = " {$this->_obj}->assertEquals(PHPUnit_Extensions_Selenium2TestCase_WebDriverException::NoSuchElement, \$e->getCode());"; 218 | $lines[] = '}'; 219 | return $lines; 220 | } 221 | 222 | /** 223 | * 224 | * @param string $target 225 | * @return array 226 | */ 227 | public function waitForElementPresent($target) { 228 | $localExpression = str_replace($this->_obj, '$testCase', $this->_byQuery($target)); 229 | 230 | /* 231 | * In Selenium 2 we can not interact with invisible elements. 232 | */ 233 | 234 | $lines = array(); 235 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 236 | $lines[] = ' try {'; 237 | $lines[] = " \$element = $localExpression;"; 238 | $lines[] = " if (\$element->displayed()) {"; 239 | $lines[] = " return true;"; 240 | $lines[] = " }"; 241 | $lines[] = ' } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {}'; 242 | $lines[] = '}, 8000);'; 243 | return $lines; 244 | } 245 | 246 | public function waitForElementNotPresent($target) { 247 | $localExpression = str_replace($this->_obj, '$testCase', $this->_byQuery($target)); 248 | $lines = array(); 249 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 250 | $lines[] = " try {"; 251 | $lines[] = " $localExpression;"; 252 | $lines[] = ' } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {'; 253 | $lines[] = " if (PHPUnit_Extensions_Selenium2TestCase_WebDriverException::NoSuchElement == \$e->getCode()) {"; 254 | $lines[] = " return true;"; 255 | $lines[] = " }"; 256 | $lines[] = ' }'; 257 | $lines[] = '}, 8000);'; 258 | return $lines; 259 | } 260 | 261 | /** 262 | * SELENIUM DEPRECATED 263 | * 264 | * @param string $target 265 | * @return array 266 | */ 267 | public function waitForTextPresent($text) { 268 | $this->_addNote('Deprecated command', 'waitForTextPresent', $text); 269 | $lines = array(); 270 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 271 | $lines[] = " if (strpos(\$testCase->byTag('body')->text(), \"$text\") !== false) {"; 272 | $lines[] = " return true;"; 273 | $lines[] = ' }'; 274 | $lines[] = '}, 8000);'; 275 | return $lines; 276 | } 277 | 278 | /** 279 | * 280 | * @param string $expression 281 | * @return array 282 | */ 283 | protected function _waitWrapper($expression) { 284 | $localExpression = str_replace($this->_obj, '$testCase', $expression); 285 | $lines = array(); 286 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 287 | $lines[] = ' try {'; 288 | $lines[] = " $localExpression"; 289 | $lines[] = ' } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {}'; 290 | $lines[] = '}, 8000);'; 291 | return $lines; 292 | } 293 | 294 | /** 295 | * 296 | * @param string $expression 297 | * @return string 298 | */ 299 | protected function _assertFalse($expression) { 300 | return "{$this->_obj}->assertFalse($expression);"; 301 | } 302 | 303 | /** 304 | * 305 | * @param string $expression 306 | * @return string 307 | */ 308 | protected function _assertTrue($expression) { 309 | return "{$this->_obj}->assertTrue($expression);"; 310 | } 311 | 312 | protected function _assertPattern($target, $string) { 313 | $target = str_replace("?", "[\s\S]", $target); 314 | $expression = "(bool)preg_match('/^$target$/', " . $string . ")"; 315 | return $expression; 316 | } 317 | 318 | protected function _isTextPresent($text) { 319 | return "(bool)(strpos({$this->_obj}->byTag('body')->text(), \"$text\") !== false)"; 320 | } 321 | 322 | /** 323 | * SELENIUM DEPRECATED 324 | * 325 | * @param type $target 326 | * @return type 327 | */ 328 | public function assertTextPresent($target) { 329 | $this->_addNote('Deprecated command', 'assertTextPresent', $target); 330 | return $this->_assertTrue($this->_isTextPresent($target)); 331 | } 332 | 333 | /** 334 | * SELENIUM DEPRECATED 335 | * 336 | * @param type $target 337 | * @return type 338 | */ 339 | public function assertTextNotPresent($target) { 340 | $this->_addNote('Deprecated command', 'assertTextNotPresent', $target); 341 | return $this->_assertFalse($this->_isTextPresent($target)); 342 | } 343 | 344 | 345 | public function waitForText($target, $value){ 346 | $localExpression = '$input = ' . str_replace($this->_obj, '$testCase', $this->_byQuery($target)); 347 | $lines = array(); 348 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 349 | $lines[] = " $localExpression;"; 350 | $lines[] = " if (('$value' === '' && \$input->text() === '') || strpos(\$input->text(), \"$value\") !== false) {"; 351 | $lines[] = " return true;"; 352 | $lines[] = ' }'; 353 | $lines[] = '}, 8000);'; 354 | return $lines; 355 | } 356 | 357 | public function waitForNotText($target, $value){ 358 | $localExpression = '$input = ' . str_replace($this->_obj, '$testCase', $this->_byQuery($target)); 359 | $lines = array(); 360 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 361 | $lines[] = " try {"; 362 | $lines[] = " $localExpression;"; 363 | $lines[] = " if (('$value' === '' && \$input->text() !== '') || strpos(\$input->text(), \"$value\") === false) {"; 364 | $lines[] = " return true;"; 365 | $lines[] = ' }'; 366 | $lines[] = ' } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {'; 367 | $lines[] = " if (PHPUnit_Extensions_Selenium2TestCase_WebDriverException::NoSuchElement == \$e->getCode()) {"; 368 | $lines[] = " return true;"; 369 | $lines[] = " }"; 370 | $lines[] = ' }'; 371 | $lines[] = '}, 8000);'; 372 | return $lines; 373 | } 374 | 375 | public function assertConfirmation($text){ 376 | return $this->assertAlert($text); 377 | } 378 | 379 | public function assertAlert($text){ 380 | $lines = array(); 381 | $lines[] = "if ( !is_null({$this->_obj}->alertText()) ) {"; 382 | $lines[] = " {$this->_obj}->assertEquals(\"$text\", {$this->_obj}->alertText());"; 383 | $lines[] = "}"; 384 | $lines[] = "{$this->_obj}->acceptAlert();"; 385 | return $lines; 386 | } 387 | 388 | public function runScript($script) { 389 | $lines = array(); 390 | $lines[] = "\$script = \"$script\";"; 391 | $lines[] = "\$result = {$this->_obj}->execute(array("; 392 | $lines[] = " 'script' => \$script,"; 393 | $lines[] = " 'args' => array()"; 394 | $lines[] = "));"; 395 | return $lines; 396 | } 397 | 398 | /** 399 | * 400 | * @param string $target 401 | * @param string $varName 402 | * @return string 403 | */ 404 | public function storeAttribute($target, $varName) { 405 | $this->_checkVarName($varName); 406 | $line = "\$$varName = " . $this->_getAttributeByLocator($target) . ';'; 407 | return $line; 408 | } 409 | 410 | /** 411 | * 412 | * @param string $target 413 | * @param string $value 414 | * @return string 415 | */ 416 | public function assertAttribute($target, $value) { 417 | $line = "{$this->_obj}->assertEquals(\"$value\", " . $this->_getAttributeByLocator($target) . ');'; 418 | return $line; 419 | } 420 | 421 | /** 422 | * Returns value of dom attribute 423 | * 424 | * @param string $locator - locator ending with @attr. For example css=.some-link@href 425 | * @return string expression 426 | */ 427 | protected function _getAttributeByLocator($locator) { 428 | /* 429 | * We dont have a $this->getAttribute($locator) 430 | */ 431 | /* 432 | * /(.+)\/@([\S])+$/ ~ //div/a/@href -> //div/a 433 | */ 434 | $elementTarget = preg_replace('/(.+?)\/?@([\S]+)$/', '$1', $locator); 435 | $attribute = preg_replace('/(.+?)\/?@([\S]+)$/', '$2', $locator); 436 | $line = $this->_byQuery($elementTarget) . "->attribute('$attribute')"; 437 | return $line; 438 | } 439 | 440 | protected function _checkVarName($varName) { 441 | $reservedWords = array( 442 | 'element', 443 | 'input', 444 | 'script', 445 | 'result', 446 | 'selectElement', 447 | ); 448 | if (in_array($varName, $reservedWords)) { 449 | $this->_addNote("'$varName' is bad name for variable, converter uses it for other commands", 'store*', $varName); 450 | } 451 | } 452 | 453 | public function storeText($target, $varName) { 454 | $this->_checkVarName($varName); 455 | $lines = array(); 456 | $lines[] = '$element = ' . $this->_byQuery($target) . ';'; 457 | $lines[] = "\$$varName = \$element->text();"; 458 | return $lines; 459 | } 460 | 461 | /** 462 | * 463 | * @param type $target 464 | * @return type 465 | */ 466 | public function mouseOver($target) { 467 | $lines = array(); 468 | $lines[] = '$element = ' . $this->_byQuery($target) . ';'; 469 | $lines[] = "{$this->_obj}->moveto(\$element);"; 470 | return $lines; 471 | } 472 | 473 | /** 474 | * Waits for any response 475 | * 476 | * @param int $timeout ms 477 | * @return type 478 | */ 479 | public function waitForPageToLoad($timeout) { 480 | $timeout = intval($timeout); 481 | $lines = array(); 482 | $lines[] = $this->_obj . '->waitUntil(function($testCase) {'; 483 | $lines[] = ' if (strlen($testCase->source()) > 0) {'; 484 | $lines[] = ' return true;'; 485 | $lines[] = ' }'; 486 | $lines[] = " }, $timeout);"; 487 | return $lines; 488 | } 489 | 490 | } 491 | -------------------------------------------------------------------------------- /Converter.php: -------------------------------------------------------------------------------- 1 | find('link')){ 90 | 91 | if (!$this->_testUrl){ 92 | $this->_testUrl = $html->find('link', 0)->href; 93 | } 94 | if (!$this->_testName) { 95 | $title = $html->find('title', 0)->innertext; 96 | $this->_testName = preg_replace('/[^A-Za-z0-9]/', '_', ucwords($title)); 97 | } 98 | 99 | foreach ($html->find('table tr') as $row) { 100 | if ($row->find('td', 2)) { 101 | $command = $row->find('td', 0)->innertext; 102 | $target = $row->find('td', 1)->innertext; 103 | $value = $row->find('td', 2)->innertext; 104 | 105 | $this->_commands[] = array( 106 | 'command' => $command, 107 | 'target' => $target, 108 | 'value' => $value 109 | ); 110 | } 111 | } 112 | 113 | } else { 114 | throw new \Exception("HTML parse error"); 115 | } 116 | } 117 | 118 | /** 119 | * Converts HTML text of Selenium test case into PHP code 120 | * 121 | * @param string $htmlStr content of html file with Selenium test case 122 | * @param string $testName test class name (leave blank for auto) 123 | * @return string PHP test case file content 124 | */ 125 | public function convert($htmlStr, $testName = '', $tplFile = ''){ 126 | $this->_testName = $testName; 127 | $this->_commands = array(); 128 | $this->_parseHtml($htmlStr); 129 | if ($tplFile){ 130 | if (is_file($tplFile)){ 131 | return $this->_convertToTpl($tplFile); 132 | } else { 133 | echo "Template file $tplFile is not accessible."; 134 | exit; 135 | } 136 | } else { 137 | $lines = $this->_composeLines(); 138 | return $this->_composeStr($lines); 139 | } 140 | } 141 | 142 | /** 143 | * Implodes lines of file into one string 144 | * 145 | * @param array $lines 146 | * @return string 147 | */ 148 | protected function _composeStr($lines){ 149 | return implode($this->_tplEOL, $lines); 150 | } 151 | 152 | /** 153 | * Adds indents to each line except first 154 | * and implodes lines into one string 155 | * 156 | * @param array $lines array of strings 157 | * @param int $indentSize 158 | * @return string 159 | */ 160 | protected function _composeStrWithIndents($lines, $indentSize){ 161 | foreach ($lines as $i=>$line){ 162 | if ($i != 0){ 163 | $lines[$i] = $this->_indent($indentSize) . $line; 164 | } 165 | } 166 | return $this->_composeStr($lines); 167 | } 168 | 169 | /** 170 | * Uses tpl file for output result. 171 | * 172 | * @param string $tplFile filepath 173 | * @return string output content 174 | */ 175 | protected function _convertToTpl($tplFile){ 176 | $tpl = file_get_contents($tplFile); 177 | $replacements = array( 178 | '{$comment}' => $this->_composeComment(), 179 | '{$className}' => $this->_composeClassName(), 180 | '{$browser}' => $this->_browser, 181 | '{$testUrl}' => $this->_testUrl ? $this->_testUrl : $this->_defaultTestUrl, 182 | '{$remoteHost}' => $this->_remoteHost ? $this->_remoteHost : '127.0.0.1', 183 | '{$remotePort}' => $this->_remotePort ? $this->_remotePort : '4444', 184 | '{$testMethodName}' => $this->_composeTestMethodName(), 185 | '{$testMethodContent}' => $this->_composeStrWithIndents($this->_composeTestMethodContent(), 8), 186 | '{$customParam1}' => $this->_tplCustomParam1, 187 | '{$customParam2}' => $this->_tplCustomParam2, 188 | ); 189 | foreach ($replacements as $s=>$r){ 190 | $tpl = str_replace($s, $r, $tpl); 191 | } 192 | return $tpl; 193 | } 194 | 195 | protected function _composeLines() { 196 | $lines = array(); 197 | 198 | $lines[] = $this->_tplFirstLine; 199 | $lines[] = $this->_composeComment(); 200 | 201 | if (count($this->_tplPreClass)) { 202 | $lines[] = ""; 203 | foreach ($this->_tplPreClass as $mLine) { 204 | $lines[] = $mLine; 205 | } 206 | $lines[] = ""; 207 | } 208 | 209 | $lines[] = "class " . $this->_composeClassName() . " extends " . $this->_tplParentClass . "{"; 210 | $lines[] = ""; 211 | 212 | if (count($this->_tplAdditionalClassContent)) { 213 | foreach ($this->_tplAdditionalClassContent as $mLine) { 214 | $lines[] = $this->_indent(4) . $mLine; 215 | } 216 | $lines[] = ""; 217 | } 218 | 219 | 220 | $lines[] = $this->_indent(4) . "function setUp(){"; 221 | foreach ($this->_composeSetupMethodContent() as $mLine){ 222 | $lines[] = $this->_indent(8) . $mLine; 223 | } 224 | $lines[] = $this->_indent(4) . "}"; 225 | $lines[] = ""; 226 | 227 | 228 | $lines[] = $this->_indent(4) . "function " . $this->_composeTestMethodName() . "(){"; 229 | foreach ($this->_composeTestMethodContent() as $mLine){ 230 | $lines[] = $this->_indent(8) . $mLine; 231 | } 232 | $lines[] = $this->_indent(4) . "}"; 233 | $lines[] = ""; 234 | 235 | 236 | $lines[] = "}"; 237 | 238 | return $lines; 239 | } 240 | 241 | protected function _indent($size){ 242 | return str_repeat(" ", $size); 243 | } 244 | 245 | protected function _composeClassName(){ 246 | return $this->_tplClassPrefix . $this->_testName . "Test"; 247 | } 248 | 249 | protected function _composeTestMethodName(){ 250 | return "test" . $this->_testName; 251 | } 252 | 253 | protected function _composeSetupMethodContent(){ 254 | $mLines = array(); 255 | $mLines[] = '$this->setBrowser("' . $this->_browser . '");'; 256 | if ($this->_testUrl){ 257 | $mLines[] = '$this->setBrowserUrl("' . $this->_testUrl . '");'; 258 | } else{ 259 | $mLines[] = '$this->setBrowserUrl("' . $this->_defaultTestUrl . '");'; 260 | } 261 | if ($this->_remoteHost) { 262 | $mLines[] = '$this->setHost("' . $this->_remoteHost . '");'; 263 | } 264 | if ($this->_remotePort) { 265 | $mLines[] = '$this->setPort("' . $this->_remotePort . '");'; 266 | } 267 | return $mLines; 268 | } 269 | 270 | protected function _composeTestMethodContent(){ 271 | if ($this->_selenium2){ 272 | require_once 'Commands2.php'; 273 | $commands = new Commands2; 274 | } else { 275 | require_once 'Commands.php'; 276 | $commands = new Commands; 277 | } 278 | 279 | $mLines = array(); 280 | 281 | 282 | foreach ($this->_commands as $row){ 283 | $command = $row['command']; 284 | $target = $this->_prepareHtml($row['target']); 285 | $value = $this->_prepareHtml($row['value']); 286 | $res = $commands->$command($target, $value); 287 | if (is_string($res)){ 288 | if ($this->_tplCommandEOL !== ''){ 289 | $res .= $this->_tplCommandEOL; 290 | } 291 | $mLines[] = $res; 292 | } else if (is_array($res)){ 293 | $size = count($res); 294 | $i = 0; 295 | foreach ($res as $subLine){ 296 | $i++; 297 | if ($size === $i && $this->_tplCommandEOL !== ''){ 298 | $subLine .= $this->_tplCommandEOL; 299 | } 300 | 301 | $mLines[] = $subLine; 302 | } 303 | } 304 | 305 | } 306 | 307 | return $mLines; 308 | } 309 | 310 | protected function _prepareHtml($html){ 311 | $res = $html; 312 | $res = str_replace(' ', ' ', $res); 313 | $res = html_entity_decode($res); 314 | $res = str_replace('
', '\n', $res); 315 | $res = str_replace('"', '\\"', $res); 316 | return $res; 317 | } 318 | 319 | protected function _composeComment(){ 320 | $lines = array(); 321 | $lines[] = "/*"; 322 | $lines[] = "* Autogenerated from Selenium html test case by Selenium2php."; 323 | $lines[] = "* " . date("Y-m-d H:i:s"); 324 | $lines[] = "*/"; 325 | $line = implode($this->_tplEOL, $lines); 326 | return $line; 327 | } 328 | 329 | public function setTestUrl($testUrl){ 330 | $this->_testUrl = $testUrl; 331 | } 332 | 333 | public function setRemoteHost($host){ 334 | $this->_remoteHost = $host; 335 | } 336 | 337 | public function setRemotePort($port){ 338 | $this->_remotePort = $port; 339 | } 340 | 341 | /** 342 | * Sets browser where test runs 343 | * 344 | * @param string $browser example: *firefox 345 | */ 346 | public function setBrowser($browser){ 347 | $this->_browser = $browser; 348 | } 349 | 350 | /** 351 | * Sets lines of text before test class defenition 352 | * @param string $text 353 | */ 354 | public function setTplPreClass($linesOfText){ 355 | $this->_tplPreClass = $linesOfText; 356 | } 357 | 358 | public function setTplEOL($tplEOL){ 359 | $this->_tplEOL = $tplEOL; 360 | } 361 | 362 | /** 363 | * Sets lines of text into test class 364 | * 365 | * @param array $content - array of strings with methods or properties 366 | */ 367 | public function setTplAdditionalClassContent($linesOfText){ 368 | $this->_tplAdditionalClassContent = $linesOfText; 369 | } 370 | 371 | /** 372 | * Sets name of class as parent for test class 373 | * Default: PHPUnit_Extensions_SeleniumTestCase 374 | * 375 | * @param string $className 376 | */ 377 | public function setTplParentClass($className){ 378 | $this->_tplParentClass = $className; 379 | } 380 | 381 | public function setTplClassPrefix($prefix){ 382 | $this->_tplClassPrefix = $prefix; 383 | } 384 | 385 | public function useSelenium2(){ 386 | $this->_selenium2 = true; 387 | $this->setTplParentClass('PHPUnit_Extensions_Selenium2TestCase'); 388 | $this->_tplCommandEOL = PHP_EOL; 389 | } 390 | 391 | /** 392 | * Passes value to template file 393 | * 394 | * @param type $value 395 | */ 396 | public function setTplCustomParam1($value){ 397 | $this->_tplCustomParam1 = $value; 398 | } 399 | 400 | /** 401 | * Passes value to template file 402 | * 403 | * @param type $value 404 | */ 405 | public function setTplCustomParam2($value){ 406 | $this->_tplCustomParam2 = $value; 407 | } 408 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Selenium2php 2 | ========================== 3 | 4 | ### Description 5 | Converts HTML text of Selenium test case recorded from Selenium IDE into 6 | PHP code for PHPUnit_Extensions_SeleniumTestCase or PHPUnit_Extensions_Selenium2TestCase as TestCase file. 7 | Check [Commands2.php](https://github.com/rnixik/selenium2php/blob/master/Commands2.php) for Selenium2 and [Commands.php](https://github.com/rnixik/selenium2php/blob/master/Commands1.php) for Selenium RC implemented commands. 8 | 9 | Basic workflow: 10 | 11 | 1. Record HTML tests with [Selenum IDE](http://docs.seleniumhq.org/projects/ide/) 12 | 2. Store them in some directory 13 | 3. Use selenium2php [switches] 14 | 4. Use [PhantomJS](http://phantomjs.org/) or other [webdrivers](http://docs.seleniumhq.org/projects/webdriver/) for testing 15 | 5. Edit test if necessary and go to step 2 16 | 17 | It is easier if you use continuous integration, for example, [Jenkins](http://jenkins-ci.org/). 18 | 19 | ### Usage 20 | selenium2php [switches] Test.html [Test.php] 21 | selenium2php [switches] 22 | 23 | --dest= Destination folder. 24 | --selenium2 Use Selenium2 tests format. 25 | --php-prefix= Add prefix to php filenames. 26 | --php-postfix= Add postfix to php filenames. 27 | --browser= Set browser for tests. 28 | --browser-url= Set URL for tests. 29 | --remote-host= Set Selenium server address for tests. 30 | --remote-port= Set Selenium server port for tests. 31 | -r|--recursive Use subdirectories for converting. 32 | --class-prefix= Set TestCase class prefix. 33 | --use-hash-postfix Add hash part to output filename 34 | --files-pattern= Glob pattern for input test files (*.html). 35 | --output-tpl= Template for result file. See Selenium2TestCaseTpl. 36 | --custom-param1= Assign value to $customParam1 in template. 37 | --custom-param2= Assign value to $customParam2 in template. 38 | 39 | ### Selenium2 features 40 | If you are going to use Selenium 2, you should know: 41 | * You do not wait for simple page loading (links, form submits). 42 | * You have to wait for some elements if you use ajax calls or changing visiblity via javascripts. 43 | Use waitFor*, for example, waitForElementPresent after click command. 44 | * You can nott manipulate invisible elements. 45 | 46 | ### Selenium2 (PhantomJS, GhostDriver) example 47 | You have google.html recorded in Selenium IDE: 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | google 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 |
google
open/
waitForElementPresentcss=form input[autocomplete="off"]
typecss=form input[autocomplete="off"]github
clickcss=form input[type="submit"]
waitForElementPresentid=res
assertTextPresentBuild software better, together
93 | 94 | 95 | 96 | You run 97 | 98 | php selenium2php.php --selenium2 --browser=phantomjs --output-tpl=Selenium2TestCaseTpl.tpl google.html 99 | 100 | And you get GoogleTest.php 101 | 102 | setBrowser("phantomjs"); 111 | $this->setBrowserUrl("https://www.google.com/"); 112 | $this->setHost("127.0.0.1"); 113 | $this->setPort(4444); 114 | } 115 | 116 | function testGoogle(){ 117 | $this->url('/'); 118 | 119 | $this->waitUntil(function($testCase) { 120 | try { 121 | $testCase->byCssSelector("form input[autocomplete=\"off\"]"); 122 | return true; 123 | } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {} 124 | }, 8000); 125 | 126 | $input = $this->byCssSelector("form input[autocomplete=\"off\"]"); 127 | $input->value("github"); 128 | 129 | $input = $this->byCssSelector("form input[type=\"submit\"]"); 130 | $input->click(); 131 | 132 | $this->waitUntil(function($testCase) { 133 | try { 134 | $testCase->byId("res"); 135 | return true; 136 | } catch (PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) {} 137 | }, 8000); 138 | 139 | $this->assertTrue((bool)(strpos($this->byTag('body')->text(), 'Build software better, together') !== false)); 140 | 141 | } 142 | 143 | } 144 | 145 | Your test case is ready. 146 | Prepare your selenim hub + phantomjs webdriver like [here](https://github.com/detro/ghostdriver#register-ghostdriver-with-a-selenium-grid-hub): 147 | 148 | java -jar selenium.jar -role hub 149 | phantomjs --webdriver=1408 --webdriver-selenium-grid-hub=http://127.0.0.1:4444 150 | 151 | And run phpunit: 152 | 153 | php phpunit.phar GoogleTest.php 154 | 155 | ### Selenium1 (RC) example 156 | You have google.html recorded in Selenium IDE: 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | google 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
google
open/
typeid=gbqfqgithub
waitForElementPresentid=res
assertTextPresentBuild software better, together
192 | 193 | 194 | 195 | You run 196 | 197 | php selenium2php.php google.html 198 | 199 | And you get GoogleTest.php 200 | 201 | setBrowser("*firefox"); 209 | $this->setBrowserUrl("https://www.google.ru/"); 210 | } 211 | function testGoogle(){ 212 | $this->open("/"); 213 | $this->type("id=gbqfq", "github"); 214 | for ($second = 0; ; $second++) { 215 | if ($second >= 60) $this->fail("timeout"); 216 | try { 217 | if ($this->isElementPresent("id=res")) break; 218 | } catch (Exception $e) {} 219 | sleep(1); 220 | } 221 | $this->assertTrue($this->isTextPresent("Build software better, together")); 222 | } 223 | } 224 | 225 | 226 | ### PHPUnit built-in method for Selenium1 (RC) 227 | If you don't need php test files, see [PHPUnit Example](http://phpunit.de/manual/3.8/en/selenium.html#selenium.seleniumtestcase.examples.WebTest4.php) 228 | 229 | 237 | 238 | ### TestCase templates 239 | You can add you php code to every TestCase file. For example, save screenshot on not successful test. 240 | 241 | File Selenium2TestCaseScreenshotTpl.tpl: 242 | 243 | setBrowser("{$browser}"); 249 | $this->setBrowserUrl("{$testUrl}"); 250 | $this->setHost("{$remoteHost}"); 251 | $this->setPort({$remotePort}); 252 | } 253 | 254 | function {$testMethodName}(){ 255 | {$testMethodContent} 256 | } 257 | 258 | public function onNotSuccessfulTest(Exception $e){ 259 | file_put_contents("D:/t/test_screenshots/{$className}.png", $this->currentScreenshot()); 260 | throw $e; 261 | } 262 | 263 | } 264 | 265 | Run converting with option --output-tpl 266 | 267 | php selenium2php.php --selenium2 --browser=phantomjs --output-tpl=Selenium2TestCaseScreenshotTpl.tpl google.html 268 | 269 | ### License 270 | Copyright 2013 Roman Nix 271 | Licensed under the Apache License, Version 2.0 (the "License"); 272 | you may not use this file except in compliance with the License. 273 | You may obtain a copy of the License at 274 | 275 | http://www.apache.org/licenses/LICENSE-2.0 276 | 277 | Unless required by applicable law or agreed to in writing, software 278 | distributed under the License is distributed on an "AS IS" BASIS, 279 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 280 | See the License for the specific language governing permissions and 281 | limitations under the License. 282 | 283 | -------------------------------------------------------------------------------- /Selenium2TestCaseTpl.tpl: -------------------------------------------------------------------------------- 1 | setBrowser("{$browser}"); 7 | $this->setBrowserUrl("{$testUrl}"); 8 | $this->setHost("{$remoteHost}"); 9 | $this->setPort({$remotePort}); 10 | } 11 | 12 | function {$testMethodName}(){ 13 | {$testMethodContent} 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /SeleniumTestCaseTpl.tpl: -------------------------------------------------------------------------------- 1 | setBrowser("{$browser}"); 7 | $this->setBrowserUrl("{$testUrl}"); 8 | $this->setHost("{$remoteHost}"); 9 | $this->setPort({$remotePort}); 10 | } 11 | 12 | function {$testMethodName}(){ 13 | {$testMethodContent} 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /libs/arguments.php: -------------------------------------------------------------------------------- 1 | '', 11 | 'options' => array(), 12 | 'flags' => array(), 13 | 'arguments' => array(), 14 | ); 15 | 16 | $ret['exec'] = array_shift($args); 17 | 18 | while (($arg = array_shift($args)) != NULL) { 19 | // Is it a option? (prefixed with --) 20 | if (substr($arg, 0, 2) === '--') { 21 | $option = substr($arg, 2); 22 | 23 | // is it the syntax '--option=argument'? 24 | if (strpos($option, '=') !== FALSE) 25 | array_push($ret['options'], explode('=', $option, 2)); 26 | else 27 | array_push($ret['options'], $option); 28 | 29 | continue; 30 | } 31 | 32 | // Is it a flag or a serial of flags? (prefixed with -) 33 | if (substr($arg, 0, 1) === '-') { 34 | for ($i = 1; isset($arg[$i]); $i++) 35 | $ret['flags'][] = $arg[$i]; 36 | 37 | continue; 38 | } 39 | 40 | // finally, it is not option, nor flag 41 | $ret['arguments'][] = $arg; 42 | continue; 43 | } 44 | return $ret; 45 | } 46 | -------------------------------------------------------------------------------- /libs/simple_html_dom.php: -------------------------------------------------------------------------------- 1 | size is the "real" number of bytes the dom was created from. 17 | * but for most purposes, it's a really good estimation. 18 | * Paperg - Added the forceTagsClosed to the dom constructor. Forcing tags closed is great for malformed html, but it CAN lead to parsing errors. 19 | * Allow the user to tell us how much they trust the html. 20 | * Paperg add the text and plaintext to the selectors for the find syntax. plaintext implies text in the innertext of a node. text implies that the tag is a text node. 21 | * This allows for us to find tags based on the text they contain. 22 | * Create find_ancestor_tag to see if a tag is - at any level - inside of another specific tag. 23 | * Paperg: added parse_charset so that we know about the character set of the source document. 24 | * NOTE: If the user's system has a routine called get_last_retrieve_url_contents_content_type availalbe, we will assume it's returning the content-type header from the 25 | * last transfer or curl_exec, and we will parse that and use it in preference to any other method of charset detection. 26 | * 27 | * Licensed under The MIT License 28 | * Redistributions of files must retain the above copyright notice. 29 | * 30 | * @author S.C. Chen 31 | * @author John Schlick 32 | * @author Rus Carroll 33 | * @version 1.11 ($Rev: 184 $) 34 | * @package PlaceLocalInclude 35 | * @subpackage simple_html_dom 36 | */ 37 | 38 | /** 39 | * All of the Defines for the classes below. 40 | * @author S.C. Chen 41 | */ 42 | define('HDOM_TYPE_ELEMENT', 1); 43 | define('HDOM_TYPE_COMMENT', 2); 44 | define('HDOM_TYPE_TEXT', 3); 45 | define('HDOM_TYPE_ENDTAG', 4); 46 | define('HDOM_TYPE_ROOT', 5); 47 | define('HDOM_TYPE_UNKNOWN', 6); 48 | define('HDOM_QUOTE_DOUBLE', 0); 49 | define('HDOM_QUOTE_SINGLE', 1); 50 | define('HDOM_QUOTE_NO', 3); 51 | define('HDOM_INFO_BEGIN', 0); 52 | define('HDOM_INFO_END', 1); 53 | define('HDOM_INFO_QUOTE', 2); 54 | define('HDOM_INFO_SPACE', 3); 55 | define('HDOM_INFO_TEXT', 4); 56 | define('HDOM_INFO_INNER', 5); 57 | define('HDOM_INFO_OUTER', 6); 58 | define('HDOM_INFO_ENDSPACE',7); 59 | define('DEFAULT_TARGET_CHARSET', 'UTF-8'); 60 | define('DEFAULT_BR_TEXT', "\r\n"); 61 | // helper functions 62 | // ----------------------------------------------------------------------------- 63 | // get html dom from file 64 | // $maxlen is defined in the code as PHP_STREAM_COPY_ALL which is defined as -1. 65 | function file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) 66 | { 67 | // We DO force the tags to be terminated. 68 | $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $defaultBRText); 69 | // For sourceforge users: uncomment the next line and comment the retreive_url_contents line 2 lines down if it is not already done. 70 | $contents = file_get_contents($url, $use_include_path, $context, $offset); 71 | // Paperg - use our own mechanism for getting the contents as we want to control the timeout. 72 | // $contents = retrieve_url_contents($url); 73 | if (empty($contents)) 74 | { 75 | return false; 76 | } 77 | // The second parameter can force the selectors to all be lowercase. 78 | $dom->load($contents, $lowercase, $stripRN); 79 | return $dom; 80 | } 81 | 82 | // get html dom from string 83 | function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) 84 | { 85 | $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $defaultBRText); 86 | if (empty($str)) 87 | { 88 | $dom->clear(); 89 | return false; 90 | } 91 | $dom->load($str, $lowercase, $stripRN); 92 | return $dom; 93 | } 94 | 95 | // dump html dom tree 96 | function dump_html_tree($node, $show_attr=true, $deep=0) 97 | { 98 | $node->dump($node); 99 | } 100 | 101 | /** 102 | * simple html dom node 103 | * PaperG - added ability for "find" routine to lowercase the value of the selector. 104 | * PaperG - added $tag_start to track the start position of the tag in the total byte index 105 | * 106 | * @package PlaceLocalInclude 107 | */ 108 | class simple_html_dom_node { 109 | public $nodetype = HDOM_TYPE_TEXT; 110 | public $tag = 'text'; 111 | public $attr = array(); 112 | public $children = array(); 113 | public $nodes = array(); 114 | public $parent = null; 115 | public $_ = array(); 116 | public $tag_start = 0; 117 | private $dom = null; 118 | 119 | function __construct($dom) 120 | { 121 | $this->dom = $dom; 122 | $dom->nodes[] = $this; 123 | } 124 | 125 | function __destruct() 126 | { 127 | $this->clear(); 128 | } 129 | 130 | function __toString() 131 | { 132 | return $this->outertext(); 133 | } 134 | 135 | // clean up memory due to php5 circular references memory leak... 136 | function clear() 137 | { 138 | $this->dom = null; 139 | $this->nodes = null; 140 | $this->parent = null; 141 | $this->children = null; 142 | } 143 | 144 | // dump node's tree 145 | function dump($show_attr=true, $deep=0) 146 | { 147 | $lead = str_repeat(' ', $deep); 148 | 149 | echo $lead.$this->tag; 150 | if ($show_attr && count($this->attr)>0) 151 | { 152 | echo '('; 153 | foreach ($this->attr as $k=>$v) 154 | echo "[$k]=>\"".$this->$k.'", '; 155 | echo ')'; 156 | } 157 | echo "\n"; 158 | 159 | foreach ($this->nodes as $c) 160 | $c->dump($show_attr, $deep+1); 161 | } 162 | 163 | 164 | // Debugging function to dump a single dom node with a bunch of information about it. 165 | function dump_node() 166 | { 167 | echo $this->tag; 168 | if (count($this->attr)>0) 169 | { 170 | echo '('; 171 | foreach ($this->attr as $k=>$v) 172 | { 173 | echo "[$k]=>\"".$this->$k.'", '; 174 | } 175 | echo ')'; 176 | } 177 | if (count($this->attr)>0) 178 | { 179 | echo ' $_ ('; 180 | foreach ($this->_ as $k=>$v) 181 | { 182 | if (is_array($v)) 183 | { 184 | echo "[$k]=>("; 185 | foreach ($v as $k2=>$v2) 186 | { 187 | echo "[$k2]=>\"".$v2.'", '; 188 | } 189 | echo ")"; 190 | } else { 191 | echo "[$k]=>\"".$v.'", '; 192 | } 193 | } 194 | echo ")"; 195 | } 196 | 197 | if (isset($this->text)) 198 | { 199 | echo " text: (" . $this->text . ")"; 200 | } 201 | 202 | echo " children: " . count($this->children); 203 | echo " nodes: " . count($this->nodes); 204 | echo " tag_start: " . $this->tag_start; 205 | echo "\n"; 206 | 207 | } 208 | 209 | // returns the parent of node 210 | function parent() 211 | { 212 | return $this->parent; 213 | } 214 | 215 | // returns children of node 216 | function children($idx=-1) 217 | { 218 | if ($idx===-1) return $this->children; 219 | if (isset($this->children[$idx])) return $this->children[$idx]; 220 | return null; 221 | } 222 | 223 | // returns the first child of node 224 | function first_child() 225 | { 226 | if (count($this->children)>0) return $this->children[0]; 227 | return null; 228 | } 229 | 230 | // returns the last child of node 231 | function last_child() 232 | { 233 | if (($count=count($this->children))>0) return $this->children[$count-1]; 234 | return null; 235 | } 236 | 237 | // returns the next sibling of node 238 | function next_sibling() 239 | { 240 | if ($this->parent===null) return null; 241 | $idx = 0; 242 | $count = count($this->parent->children); 243 | while ($idx<$count && $this!==$this->parent->children[$idx]) 244 | ++$idx; 245 | if (++$idx>=$count) return null; 246 | return $this->parent->children[$idx]; 247 | } 248 | 249 | // returns the previous sibling of node 250 | function prev_sibling() 251 | { 252 | if ($this->parent===null) return null; 253 | $idx = 0; 254 | $count = count($this->parent->children); 255 | while ($idx<$count && $this!==$this->parent->children[$idx]) 256 | ++$idx; 257 | if (--$idx<0) return null; 258 | return $this->parent->children[$idx]; 259 | } 260 | 261 | // function to locate a specific ancestor tag in the path to the root. 262 | function find_ancestor_tag($tag) 263 | { 264 | global $debugObject; 265 | if (is_object($debugObject)) 266 | { 267 | $debugObject->debugLogEntry(1); 268 | } 269 | 270 | // Start by including ourselves in the comparison. 271 | $returnDom = $this; 272 | 273 | while (!is_null($returnDom)) 274 | { 275 | if (is_object($debugObject)) 276 | { 277 | $debugObject->debugLog(2, "Current tag is: " . $returnDom->tag); 278 | } 279 | 280 | if ($returnDom->tag == $tag) 281 | { 282 | break; 283 | } 284 | $returnDom = $returnDom->parent; 285 | } 286 | return $returnDom; 287 | } 288 | 289 | // get dom node's inner html 290 | function innertext() 291 | { 292 | if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; 293 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 294 | 295 | $ret = ''; 296 | foreach ($this->nodes as $n) 297 | $ret .= $n->outertext(); 298 | return $ret; 299 | } 300 | 301 | // get dom node's outer text (with tag) 302 | function outertext() 303 | { 304 | global $debugObject; 305 | if (is_object($debugObject)) 306 | { 307 | $text = ''; 308 | if ($this->tag == 'text') 309 | { 310 | if (!empty($this->text)) 311 | { 312 | $text = " with text: " . $this->text; 313 | } 314 | } 315 | $debugObject->debugLog(1, 'Innertext of tag: ' . $this->tag . $text); 316 | } 317 | 318 | if ($this->tag==='root') return $this->innertext(); 319 | 320 | // trigger callback 321 | if ($this->dom && $this->dom->callback!==null) 322 | { 323 | call_user_func_array($this->dom->callback, array($this)); 324 | } 325 | 326 | if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER]; 327 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 328 | 329 | // render begin tag 330 | if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]) 331 | { 332 | $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup(); 333 | } else { 334 | $ret = ""; 335 | } 336 | 337 | // render inner text 338 | if (isset($this->_[HDOM_INFO_INNER])) 339 | { 340 | // If it's a br tag... don't return the HDOM_INNER_INFO that we may or may not have added. 341 | if ($this->tag != "br") 342 | { 343 | $ret .= $this->_[HDOM_INFO_INNER]; 344 | } 345 | } else { 346 | if ($this->nodes) 347 | { 348 | foreach ($this->nodes as $n) 349 | { 350 | $ret .= $this->convert_text($n->outertext()); 351 | } 352 | } 353 | } 354 | 355 | // render end tag 356 | if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0) 357 | $ret .= 'tag.'>'; 358 | return $ret; 359 | } 360 | 361 | // get dom node's plain text 362 | function text() 363 | { 364 | if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; 365 | switch ($this->nodetype) 366 | { 367 | case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 368 | case HDOM_TYPE_COMMENT: return ''; 369 | case HDOM_TYPE_UNKNOWN: return ''; 370 | } 371 | if (strcasecmp($this->tag, 'script')===0) return ''; 372 | if (strcasecmp($this->tag, 'style')===0) return ''; 373 | 374 | $ret = ''; 375 | // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed for some span tags, and some p tags) $this->nodes is set to NULL. 376 | // NOTE: This indicates that there is a problem where it's set to NULL without a clear happening. 377 | // WHY is this happening? 378 | if (!is_null($this->nodes)) 379 | { 380 | foreach ($this->nodes as $n) 381 | { 382 | $ret .= $this->convert_text($n->text()); 383 | } 384 | } 385 | return $ret; 386 | } 387 | 388 | function xmltext() 389 | { 390 | $ret = $this->innertext(); 391 | $ret = str_ireplace('', '', $ret); 393 | return $ret; 394 | } 395 | 396 | // build node's text with tag 397 | function makeup() 398 | { 399 | // text, comment, unknown 400 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 401 | 402 | $ret = '<'.$this->tag; 403 | $i = -1; 404 | 405 | foreach ($this->attr as $key=>$val) 406 | { 407 | ++$i; 408 | 409 | // skip removed attribute 410 | if ($val===null || $val===false) 411 | continue; 412 | 413 | $ret .= $this->_[HDOM_INFO_SPACE][$i][0]; 414 | //no value attr: nowrap, checked selected... 415 | if ($val===true) 416 | $ret .= $key; 417 | else { 418 | switch ($this->_[HDOM_INFO_QUOTE][$i]) 419 | { 420 | case HDOM_QUOTE_DOUBLE: $quote = '"'; break; 421 | case HDOM_QUOTE_SINGLE: $quote = '\''; break; 422 | default: $quote = ''; 423 | } 424 | $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote; 425 | } 426 | } 427 | $ret = $this->dom->restore_noise($ret); 428 | return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>'; 429 | } 430 | 431 | // find elements by css selector 432 | //PaperG - added ability for find to lowercase the value of the selector. 433 | function find($selector, $idx=null, $lowercase=false) 434 | { 435 | $selectors = $this->parse_selector($selector); 436 | if (($count=count($selectors))===0) return array(); 437 | $found_keys = array(); 438 | 439 | // find each selector 440 | for ($c=0; $c<$count; ++$c) 441 | { 442 | // The change on the below line was documented on the sourceforge code tracker id 2788009 443 | // used to be: if (($levle=count($selectors[0]))===0) return array(); 444 | if (($levle=count($selectors[$c]))===0) return array(); 445 | if (!isset($this->_[HDOM_INFO_BEGIN])) return array(); 446 | 447 | $head = array($this->_[HDOM_INFO_BEGIN]=>1); 448 | 449 | // handle descendant selectors, no recursive! 450 | for ($l=0; $l<$levle; ++$l) 451 | { 452 | $ret = array(); 453 | foreach ($head as $k=>$v) 454 | { 455 | $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k]; 456 | //PaperG - Pass this optional parameter on to the seek function. 457 | $n->seek($selectors[$c][$l], $ret, $lowercase); 458 | } 459 | $head = $ret; 460 | } 461 | 462 | foreach ($head as $k=>$v) 463 | { 464 | if (!isset($found_keys[$k])) 465 | $found_keys[$k] = 1; 466 | } 467 | } 468 | 469 | // sort keys 470 | ksort($found_keys); 471 | 472 | $found = array(); 473 | foreach ($found_keys as $k=>$v) 474 | $found[] = $this->dom->nodes[$k]; 475 | 476 | // return nth-element or array 477 | if (is_null($idx)) return $found; 478 | else if ($idx<0) $idx = count($found) + $idx; 479 | return (isset($found[$idx])) ? $found[$idx] : null; 480 | } 481 | 482 | // seek for given conditions 483 | // PaperG - added parameter to allow for case insensitive testing of the value of a selector. 484 | protected function seek($selector, &$ret, $lowercase=false) 485 | { 486 | global $debugObject; 487 | if (is_object($debugObject)) 488 | { 489 | $debugObject->debugLogEntry(1); 490 | } 491 | 492 | list($tag, $key, $val, $exp, $no_key) = $selector; 493 | 494 | // xpath index 495 | if ($tag && $key && is_numeric($key)) 496 | { 497 | $count = 0; 498 | foreach ($this->children as $c) 499 | { 500 | if ($tag==='*' || $tag===$c->tag) { 501 | if (++$count==$key) { 502 | $ret[$c->_[HDOM_INFO_BEGIN]] = 1; 503 | return; 504 | } 505 | } 506 | } 507 | return; 508 | } 509 | 510 | $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0; 511 | if ($end==0) { 512 | $parent = $this->parent; 513 | while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) { 514 | $end -= 1; 515 | $parent = $parent->parent; 516 | } 517 | $end += $parent->_[HDOM_INFO_END]; 518 | } 519 | 520 | for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) { 521 | $node = $this->dom->nodes[$i]; 522 | 523 | $pass = true; 524 | 525 | if ($tag==='*' && !$key) { 526 | if (in_array($node, $this->children, true)) 527 | $ret[$i] = 1; 528 | continue; 529 | } 530 | 531 | // compare tag 532 | if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;} 533 | // compare key 534 | if ($pass && $key) { 535 | if ($no_key) { 536 | if (isset($node->attr[$key])) $pass=false; 537 | } else { 538 | if (($key != "plaintext") && !isset($node->attr[$key])) $pass=false; 539 | } 540 | } 541 | // compare value 542 | if ($pass && $key && $val && $val!=='*') { 543 | // If they have told us that this is a "plaintext" search then we want the plaintext of the node - right? 544 | if ($key == "plaintext") { 545 | // $node->plaintext actually returns $node->text(); 546 | $nodeKeyValue = $node->text(); 547 | } else { 548 | // this is a normal search, we want the value of that attribute of the tag. 549 | $nodeKeyValue = $node->attr[$key]; 550 | } 551 | if (is_object($debugObject)) {$debugObject->debugLog(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);} 552 | 553 | //PaperG - If lowercase is set, do a case insensitive test of the value of the selector. 554 | if ($lowercase) { 555 | $check = $this->match($exp, strtolower($val), strtolower($nodeKeyValue)); 556 | } else { 557 | $check = $this->match($exp, $val, $nodeKeyValue); 558 | } 559 | if (is_object($debugObject)) {$debugObject->debugLog(2, "after match: " . ($check ? "true" : "false"));} 560 | 561 | // handle multiple class 562 | if (!$check && strcasecmp($key, 'class')===0) { 563 | foreach (explode(' ',$node->attr[$key]) as $k) { 564 | // Without this, there were cases where leading, trailing, or double spaces lead to our comparing blanks - bad form. 565 | if (!empty($k)) { 566 | if ($lowercase) { 567 | $check = $this->match($exp, strtolower($val), strtolower($k)); 568 | } else { 569 | $check = $this->match($exp, $val, $k); 570 | } 571 | if ($check) break; 572 | } 573 | } 574 | } 575 | if (!$check) $pass = false; 576 | } 577 | if ($pass) $ret[$i] = 1; 578 | unset($node); 579 | } 580 | // It's passed by reference so this is actually what this function returns. 581 | if (is_object($debugObject)) {$debugObject->debugLog(1, "EXIT - ret: ", $ret);} 582 | } 583 | 584 | protected function match($exp, $pattern, $value) { 585 | global $debugObject; 586 | if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} 587 | 588 | switch ($exp) { 589 | case '=': 590 | return ($value===$pattern); 591 | case '!=': 592 | return ($value!==$pattern); 593 | case '^=': 594 | return preg_match("/^".preg_quote($pattern,'/')."/", $value); 595 | case '$=': 596 | return preg_match("/".preg_quote($pattern,'/')."$/", $value); 597 | case '*=': 598 | if ($pattern[0]=='/') { 599 | return preg_match($pattern, $value); 600 | } 601 | return preg_match("/".$pattern."/i", $value); 602 | } 603 | return false; 604 | } 605 | 606 | protected function parse_selector($selector_string) { 607 | global $debugObject; 608 | if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} 609 | 610 | // pattern of CSS selectors, modified from mootools 611 | // Paperg: Add the colon to the attrbute, so that it properly finds like google does. 612 | // Note: if you try to look at this attribute, yo MUST use getAttribute since $dom->x:y will fail the php syntax check. 613 | // Notice the \[ starting the attbute? and the @? following? This implies that an attribute can begin with an @ sign that is not captured. 614 | // This implies that an html attribute specifier may start with an @ sign that is NOT captured by the expression. 615 | // farther study is required to determine of this should be documented or removed. 616 | // $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; 617 | $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; 618 | preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER); 619 | if (is_object($debugObject)) {$debugObject->debugLog(2, "Matches Array: ", $matches);} 620 | 621 | $selectors = array(); 622 | $result = array(); 623 | //print_r($matches); 624 | 625 | foreach ($matches as $m) { 626 | $m[0] = trim($m[0]); 627 | if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue; 628 | // for browser generated xpath 629 | if ($m[1]==='tbody') continue; 630 | 631 | list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false); 632 | if (!empty($m[2])) {$key='id'; $val=$m[2];} 633 | if (!empty($m[3])) {$key='class'; $val=$m[3];} 634 | if (!empty($m[4])) {$key=$m[4];} 635 | if (!empty($m[5])) {$exp=$m[5];} 636 | if (!empty($m[6])) {$val=$m[6];} 637 | 638 | // convert to lowercase 639 | if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);} 640 | //elements that do NOT have the specified attribute 641 | if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;} 642 | 643 | $result[] = array($tag, $key, $val, $exp, $no_key); 644 | if (trim($m[7])===',') { 645 | $selectors[] = $result; 646 | $result = array(); 647 | } 648 | } 649 | if (count($result)>0) 650 | $selectors[] = $result; 651 | return $selectors; 652 | } 653 | 654 | function __get($name) { 655 | if (isset($this->attr[$name])) 656 | { 657 | return $this->convert_text($this->attr[$name]); 658 | } 659 | switch ($name) { 660 | case 'outertext': return $this->outertext(); 661 | case 'innertext': return $this->innertext(); 662 | case 'plaintext': return $this->text(); 663 | case 'xmltext': return $this->xmltext(); 664 | default: return array_key_exists($name, $this->attr); 665 | } 666 | } 667 | 668 | function __set($name, $value) { 669 | switch ($name) { 670 | case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value; 671 | case 'innertext': 672 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value; 673 | return $this->_[HDOM_INFO_INNER] = $value; 674 | } 675 | if (!isset($this->attr[$name])) { 676 | $this->_[HDOM_INFO_SPACE][] = array(' ', '', ''); 677 | $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE; 678 | } 679 | $this->attr[$name] = $value; 680 | } 681 | 682 | function __isset($name) { 683 | switch ($name) { 684 | case 'outertext': return true; 685 | case 'innertext': return true; 686 | case 'plaintext': return true; 687 | } 688 | //no value attr: nowrap, checked selected... 689 | return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]); 690 | } 691 | 692 | function __unset($name) { 693 | if (isset($this->attr[$name])) 694 | unset($this->attr[$name]); 695 | } 696 | 697 | // PaperG - Function to convert the text from one character set to another if the two sets are not the same. 698 | function convert_text($text) { 699 | global $debugObject; 700 | if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} 701 | 702 | $converted_text = $text; 703 | 704 | $sourceCharset = ""; 705 | $targetCharset = ""; 706 | if ($this->dom) { 707 | $sourceCharset = strtoupper($this->dom->_charset); 708 | $targetCharset = strtoupper($this->dom->_target_charset); 709 | } 710 | if (is_object($debugObject)) {$debugObject->debugLog(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);} 711 | 712 | if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0)) 713 | { 714 | // Check if the reported encoding could have been incorrect and the text is actually already UTF-8 715 | if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text))) 716 | { 717 | $converted_text = $text; 718 | } 719 | else 720 | { 721 | $converted_text = iconv($sourceCharset, $targetCharset, $text); 722 | } 723 | } 724 | 725 | return $converted_text; 726 | } 727 | 728 | function is_utf8($string) 729 | { 730 | return (utf8_encode(utf8_decode($string)) == $string); 731 | } 732 | 733 | // camel naming conventions 734 | function getAllAttributes() {return $this->attr;} 735 | function getAttribute($name) {return $this->__get($name);} 736 | function setAttribute($name, $value) {$this->__set($name, $value);} 737 | function hasAttribute($name) {return $this->__isset($name);} 738 | function removeAttribute($name) {$this->__set($name, null);} 739 | function getElementById($id) {return $this->find("#$id", 0);} 740 | function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);} 741 | function getElementByTagName($name) {return $this->find($name, 0);} 742 | function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);} 743 | function parentNode() {return $this->parent();} 744 | function childNodes($idx=-1) {return $this->children($idx);} 745 | function firstChild() {return $this->first_child();} 746 | function lastChild() {return $this->last_child();} 747 | function nextSibling() {return $this->next_sibling();} 748 | function previousSibling() {return $this->prev_sibling();} 749 | } 750 | 751 | /** 752 | * simple html dom parser 753 | * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector. 754 | * Paperg - change $size from protected to public so we can easily access it 755 | * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it. 756 | * 757 | * @package PlaceLocalInclude 758 | */ 759 | class simple_html_dom { 760 | public $root = null; 761 | public $nodes = array(); 762 | public $callback = null; 763 | public $lowercase = false; 764 | public $size; 765 | protected $pos; 766 | protected $doc; 767 | protected $char; 768 | protected $cursor; 769 | protected $parent; 770 | protected $noise = array(); 771 | protected $token_blank = " \t\r\n"; 772 | protected $token_equal = ' =/>'; 773 | protected $token_slash = " />\r\n\t"; 774 | protected $token_attr = ' >'; 775 | protected $_charset = ''; 776 | protected $_target_charset = ''; 777 | protected $default_br_text = ""; 778 | 779 | // use isset instead of in_array, performance boost about 30%... 780 | protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1); 781 | protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1); 782 | // Known sourceforge issue #2977341 783 | // B tags that are not closed cause us to return everything to the end of the document. 784 | protected $optional_closing_tags = array( 785 | 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1), 786 | 'th'=>array('th'=>1), 787 | 'td'=>array('td'=>1), 788 | 'li'=>array('li'=>1), 789 | 'dt'=>array('dt'=>1, 'dd'=>1), 790 | 'dd'=>array('dd'=>1, 'dt'=>1), 791 | 'dl'=>array('dd'=>1, 'dt'=>1), 792 | 'p'=>array('p'=>1), 793 | 'nobr'=>array('nobr'=>1), 794 | 'b'=>array('b'=>1), 795 | ); 796 | 797 | function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) { 798 | if ($str) { 799 | if (preg_match("/^http:\/\//i",$str) || is_file($str)) 800 | $this->load_file($str); 801 | else 802 | $this->load($str, $lowercase, $stripRN, $defaultBRText); 803 | } 804 | // Forcing tags to be closed implies that we don't trust the html, but it can lead to parsing errors if we SHOULD trust the html. 805 | if (!$forceTagsClosed) { 806 | $this->optional_closing_array=array(); 807 | } 808 | $this->_target_charset = $target_charset; 809 | } 810 | 811 | function __destruct() { 812 | $this->clear(); 813 | } 814 | 815 | // load html from string 816 | function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) { 817 | global $debugObject; 818 | 819 | // prepare 820 | $this->prepare($str, $lowercase, $stripRN, $defaultBRText); 821 | // strip out comments 822 | $this->remove_noise("''is"); 823 | // strip out cdata 824 | $this->remove_noise("''is", true); 825 | // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 826 | // Script tags removal now preceeds style tag removal. 827 | // strip out