├── kirk.jpg ├── .gitignore ├── src └── Sauce │ └── Sausage │ ├── MobileTestCase.php │ ├── WebDriverTestCase.php │ ├── SauceTestCommon.php │ ├── SeleniumRCDriver.php │ ├── SauceConfig.php │ ├── SauceAPI.php │ ├── SeleniumRCTestCase.php │ ├── SauceMethods.php │ └── TestCase.php ├── .travis.yml ├── bin └── sauce_config ├── LICENSE.APACHE2 ├── tests └── SauceConfigTest.php ├── composer.json ├── MobileDemo.php ├── WebDriverFileUploadDemo.php ├── sauce_api_test.php ├── SeleniumRCDemo.php ├── WebDriverNologinLinkDemo.php ├── WebDriverDemo.php ├── WebDriverDemoShootout.php └── README.md /kirk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlipps/sausage/HEAD/kirk.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | vendor/ 3 | composer.lock 4 | .sauce_config 5 | PHPUnitDemo.php 6 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/MobileTestCase.php: -------------------------------------------------------------------------------- 1 | assertFalse(SAUCE_VERIFY_CERTS); 17 | } 18 | 19 | /** 20 | * @runInSeparateProcess 21 | * @preserveGlobalState disabled 22 | */ 23 | public function testLoadConfig_DontVerifyCertsPresentButEmpty_VerifyCertsTrue() { 24 | putenv('SAUCE_DONT_VERIFY_CERTS='); 25 | Sauce\Sausage\SauceConfig::LoadConfig(false); 26 | 27 | $this->assertTrue(SAUCE_VERIFY_CERTS); 28 | } 29 | 30 | /** 31 | * @runInSeparateProcess 32 | * @preserveGlobalState disabled 33 | */ 34 | public function testLoadConfig_VerifyCertsNotPresent_VerifyCertsTrue() { 35 | putenv('SAUCE_DONT_VERIFY_CERTS'); 36 | Sauce\Sausage\SauceConfig::LoadConfig(false); 37 | $this->assertTrue(SAUCE_VERIFY_CERTS); 38 | } 39 | 40 | } 41 | 42 | ?> -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sauce/sausage", 3 | "type": "sauce-sausage", 4 | "description": "PHP version of the Sauce Labs API", 5 | "keywords": ["sauce", "saucelabs", "api", "testing", "phpunit", "selenium", "appium"], 6 | "homepage": "http://github.com/jlipps/sausage", 7 | "license": "Apache-2.0", 8 | "authors": [{ 9 | "name": "Jonathan Lipps", 10 | "email": "jlipps@saucelabs.com", 11 | "homepage": "http://www.saucelabs.com", 12 | "role": "Lead" 13 | }], 14 | "repositories": [ 15 | { 16 | "type": "vcs", 17 | "url": "https://github.com/appium/php-client" 18 | } 19 | ], 20 | "require-dev": { 21 | "phpunit/phpunit": ">=4.5.1" 22 | }, 23 | "require": { 24 | "php": ">=5.4.0", 25 | "sauce/sausage-installer": ">=0.1.0", 26 | "phpunit/phpunit-selenium": ">=1.4.1", 27 | "brianium/paratest": ">=0.12.1", 28 | "appium/php-client": ">=0.1.0" 29 | }, 30 | "suggest": { 31 | "sauce/connect": ">=3.1" 32 | }, 33 | "autoload": { 34 | "psr-0": {"Sauce": "src/"} 35 | }, 36 | "support": { 37 | "email": "help@saucelabs.com", 38 | "irc": "irc://irc.freenode.org/saucelabs" 39 | }, 40 | "bin": ["bin/sauce_config"] 41 | } 42 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/SauceTestCommon.php: -------------------------------------------------------------------------------- 1 | updateJob($session_id, array('passed'=>$passed)); 18 | } 19 | 20 | public static function SpinAssert($msg, $test, $args=array(), $timeout=10) 21 | { 22 | $num_tries = 0; 23 | $result = false; 24 | while ($num_tries < $timeout && !$result) { 25 | try { 26 | $result = call_user_func_array($test, $args); 27 | } catch (\Exception $e) { 28 | $result = false; 29 | } 30 | 31 | if (!$result) 32 | sleep(1); 33 | 34 | $num_tries++; 35 | } 36 | 37 | $msg .= " (Failed after $num_tries tries)"; 38 | 39 | return array($result, $msg); 40 | } 41 | 42 | 43 | public static function ReportBuild($session_id, $build) 44 | { 45 | self::RequireSauceConfig(); 46 | $api = new SauceAPI(SAUCE_USERNAME, SAUCE_ACCESS_KEY); 47 | $api->updateJob($session_id, array('build'=>$build)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /MobileDemo.php: -------------------------------------------------------------------------------- 1 | '', 16 | 'desiredCapabilities' => array( 17 | 'appium-version' => '1.0', 18 | 'platformName' => 'iOS', 19 | 'platformVersion' => '7.0', 20 | 'deviceName' => 'iPhone Simulator', 21 | 'name' => 'Appium/Sauce iOS Test, PHP', 22 | 'app' => APP_URL 23 | ) 24 | ) 25 | ); 26 | 27 | public function elemsByClassName($klass) 28 | { 29 | return $this->elements($this->using('class name')->value($klass)); 30 | } 31 | 32 | protected function populate() 33 | { 34 | $elems = $this->elemsByClassName('UIATextField'); 35 | foreach ($elems as $elem) { 36 | $randNum = rand(0, 10); 37 | $elem->value($randNum); 38 | $this->numValues[] = $randNum; 39 | } 40 | } 41 | 42 | public function testUiComputation() 43 | { 44 | $this->populate(); 45 | $buttons = $this->elemsByClassName('UIAButton'); 46 | $buttons[0]->click(); 47 | $texts = $this->elemsByClassName('UIAStaticText'); 48 | $this->assertEquals(array_sum($this->numValues), (int)($texts[0]->text())); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WebDriverFileUploadDemo.php: -------------------------------------------------------------------------------- 1 | 'firefox', 11 | 'desiredCapabilities' => array( 12 | 'version' => '15', 13 | 'platform' => 'Windows 2012', 14 | ) 15 | )//, 16 | // run Mobile Safari on iOS 17 | //array( 18 | //'browserName' => '', 19 | //'desiredCapabilities' => array( 20 | //'app' => 'safari', 21 | //'device' => 'iPhone Simulator', 22 | //'version' => '6.1', 23 | //'platform' => 'Mac 10.8', 24 | //) 25 | //)//, 26 | // run Chrome on Linux on Sauce 27 | //array( 28 | //'browserName' => 'chrome', 29 | //'desiredCapabilities' => array( 30 | //'platform' => 'Linux' 31 | //) 32 | //), 33 | // run Chrome locally 34 | // array( 35 | // 'browserName' => 'chrome', 36 | // 'local' => true, 37 | // 'sessionStrategy' => 'shared' 38 | // ) 39 | ); 40 | 41 | public function setUpPage() 42 | { 43 | $this->url("http://sl-test.herokuapp.com/guinea_pig/file_upload"); 44 | 45 | // set the method which knows if this is a file we're trying to upload 46 | $this->fileDetector(function($filename) { 47 | if(file_exists($filename)) { 48 | return $filename; 49 | } else { 50 | return NULL; 51 | } 52 | }); 53 | } 54 | 55 | public function testFileUpload() 56 | { 57 | // for some reason byId('myfile') doesn't want to work 58 | $filebox = $this->byName('myfile'); 59 | $this->sendKeys($filebox, "./kirk.jpg"); 60 | 61 | $this->byId('submit')->submit(); 62 | 63 | // $image = $this->byTag('img'); 64 | 65 | $this->assertTextPresent("kirk.jpg (image/jpeg)"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sauce_api_test.php: -------------------------------------------------------------------------------- 1 | updateJob('0e2ae11933664d0ba26948d379fc67a6', array('passed'=>TRUE)); 12 | print_r($res); 13 | //$res = $s->getAccountDetails(); 14 | //print_r($res); 15 | //$res = $s->getAccountLimits(); 16 | //print_r($res); 17 | //$res = $s->createSubaccount(array('username'=>'jlippstest', 'email'=>'jlipps2@adsf.com', 'password'=>'testpass', 'name'=>"New Guy")); 18 | //print_r($res); 19 | //$res = $s->setSubaccountSubscription('sah', 'free'); 20 | //print_r($res); 21 | //$res = $s->login('testpass', 'jlippstest'); 22 | //print_r($res); 23 | //$res = $s->getUsage(null, null, 'jlippstest'); 24 | //print_r($res); 25 | //$res = $s->getUsage('2012-03-01', '2012-05-01'); 26 | //print_r($res); 27 | //$res = $s->deleteSubaccountSubscription('jlippstest'); 28 | //print_r($res); 29 | //$res = $s->getAllBrowsers(); 30 | //print_r($res); 31 | //$res = $s->getSeleniumRCBrowsers(); 32 | //print_r($res); 33 | //$res = $s->getWebDriverBrowsers(); 34 | //print_r($res); 35 | //$res = $s->getStatus(); 36 | //print_r($res); 37 | //$res = $s->getSauceTestsCount(); 38 | //print_r($res); 39 | //$res = $s->getScoutBrowsers(); 40 | //print_r($res); 41 | //$res = $s->getScoutBrowsers(true); 42 | //print_r($res); 43 | //$res = $s->getJobs(null, null, 1); 44 | //print_r($res); 45 | //$res = $s->updateJob('0213180a592449948a0f46b3b2c23cdb', array('build'=>'1234')); 46 | //$res = $s->getJobsForBuild('1234'); 47 | //print_r($res); 48 | //$res = $s->getActivity(); 49 | //print_r($res); 50 | //$res = $s->getUpdatedJobs(0); 51 | //print_r($res); 52 | //$res = $s->getJob('0e2ae11933664d0ba26948d379fc67a6'); 53 | //print_r($res); 54 | //$res = $s->stopJob('0e2ae11933664d0ba26948d379fc67a6'); 55 | //print_r($res); 56 | //$res = $s->createErrorReport("This is an error report, woot!"); 57 | //print_r($res); 58 | //$res = $s->getTunnels(); 59 | //print_r($res); 60 | //$res = $s->getTunnel('00af7b9575f6405b82176d983ac6aa94'); 61 | //print_r($res); 62 | //$res = $s->deleteTunnel('00af7b9575f6405b82176d983ac6aa94'); 63 | //print_r($res); 64 | -------------------------------------------------------------------------------- /SeleniumRCDemo.php: -------------------------------------------------------------------------------- 1 | 'firefox', 11 | 'browserVersion' => '11', 12 | 'os' => 'Windows 2003' 13 | )//, 14 | //Chrome on Linux on Sauce 15 | //array( 16 | //'browser' => 'googlechrome', 17 | //'browserVersion' => '', 18 | //'os' => 'Linux' 19 | //), 20 | //Chrome on local machine 21 | //array( 22 | //'browser' => 'googlechrome', 23 | //'local' => true 24 | //) 25 | ); 26 | 27 | public function setUp() 28 | { 29 | $this->setBrowserUrl('http://saucelabs.com/test/guinea-pig'); 30 | } 31 | 32 | public function postSessionSetUp() 33 | { 34 | $this->open('http://saucelabs.com/test/guinea-pig'); 35 | } 36 | 37 | public function testTitle() 38 | { 39 | $this->assertTitle("I am a page title - Sauce Labs"); 40 | } 41 | 42 | public function testLink() 43 | { 44 | $this->click('id=i am a link'); 45 | $driver = $this; 46 | $title_test = function() use ($driver) { 47 | return ($driver->getTitle() == "I am another page title - Sauce Labs"); 48 | }; 49 | $this->spinAssert("Title never matched!", $title_test); 50 | } 51 | 52 | public function testTextbox() 53 | { 54 | $test_text = "This is some text"; 55 | $this->click('id=i_am_a_textbox'); 56 | $this->type('id=i_am_a_textbox', $test_text); 57 | $this->assertElementValueEquals('id=i_am_a_textbox', $test_text); 58 | } 59 | 60 | public function testSubmitComments() 61 | { 62 | $comment = "This comment rocks lots of rocks"; 63 | $this->type('id=comments', $comment); 64 | $this->click('id=submit'); 65 | $driver = $this; 66 | 67 | $comment_test = 68 | function() use ($comment, $driver) 69 | { 70 | $text = $driver->getText('id=your_comments'); 71 | return ($text == "Your comments: $comment"); 72 | } 73 | ; 74 | 75 | $this->spinAssert("Comment never showed up!", $comment_test); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /WebDriverNologinLinkDemo.php: -------------------------------------------------------------------------------- 1 | 'firefox', 11 | 'desiredCapabilities' => array( 12 | 'version' => '15', 13 | 'platform' => 'Windows 2012', 14 | ) 15 | ), 16 | // run Mobile Safari on iOS 17 | array( 18 | 'browserName' => '', 19 | 'desiredCapabilities' => array( 20 | 'app' => 'safari', 21 | 'device' => 'iPhone Simulator', 22 | 'version' => '6.1', 23 | 'platform' => 'Mac 10.8', 24 | ) 25 | ), 26 | // run Chrome on Linux on Sauce 27 | array( 28 | 'browserName' => 'chrome', 29 | 'desiredCapabilities' => array( 30 | 'platform' => 'Linux' 31 | ) 32 | ), 33 | // run Chrome locally 34 | array( 35 | 'browserName' => 'chrome', 36 | 'local' => true, 37 | 'sessionStrategy' => 'shared' 38 | ) 39 | ); 40 | 41 | public function setUpPage() 42 | { 43 | $this->url("http://saucelabs.com/test/guinea-pig"); 44 | } 45 | 46 | public function testTitle() 47 | { 48 | $this->assertContains("I am not a page title", $this->title()); 49 | } 50 | 51 | public function testLink() 52 | { 53 | $link = $this->byId('i am a link'); 54 | $link->click(); 55 | $this->assertContains("I am another page title", $this->title()); 56 | } 57 | 58 | public function testTextbox() 59 | { 60 | $test_text = "This is some text"; 61 | $textbox = $this->byId('i_am_a_textbox'); 62 | $textbox->click(); 63 | $this->keys($test_text); 64 | $this->assertEquals($textbox->value(), $test_text); 65 | } 66 | 67 | public function testSubmitComments() 68 | { 69 | $comment = "This is a very insightful comment."; 70 | $this->byId('comments')->value($comment); 71 | $this->byId('submit')->submit(); 72 | $driver = $this; 73 | 74 | $comment_test = function() use ($comment, $driver) { 75 | $text = $driver->byId('your_comments')->text(); 76 | return $text == "Your comments : $comment"; 77 | }; 78 | 79 | $this->spinAssert("Comment never showed up!", $comment_test); 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /WebDriverDemo.php: -------------------------------------------------------------------------------- 1 | 'firefox', 14 | 'desiredCapabilities' => array( 15 | 'version' => '15', 16 | 'platform' => 'Windows 2012', 17 | ) 18 | ), 19 | // run Chrome on Linux on Sauce 20 | array( 21 | 'browserName' => 'chrome', 22 | 'desiredCapabilities' => array( 23 | 'platform' => 'Linux' 24 | ) 25 | ), 26 | // run Mobile Safari on iOS 27 | //array( 28 | //'browserName' => '', 29 | //'desiredCapabilities' => array( 30 | //'app' => 'safari', 31 | //'device' => 'iPhone Simulator', 32 | //'version' => '6.1', 33 | //'platform' => 'Mac 10.8', 34 | //) 35 | //)//, 36 | // run Chrome locally 37 | //array( 38 | //'browserName' => 'chrome', 39 | //'local' => true, 40 | //'sessionStrategy' => 'shared' 41 | //) 42 | ); 43 | 44 | public function testTitle() 45 | { 46 | $this->assertContains("I am a page title", $this->title()); 47 | } 48 | 49 | public function testLink() 50 | { 51 | $link = $this->byId('i am a link'); 52 | $link->click(); 53 | $this->assertContains("I am another page title", $this->title()); 54 | } 55 | 56 | public function testTextbox() 57 | { 58 | $test_text = "This is some text"; 59 | $textbox = $this->byId('i_am_a_textbox'); 60 | $textbox->click(); 61 | $textbox->clear(); 62 | $this->keys($test_text); 63 | $this->assertEquals($textbox->value(), $test_text); 64 | } 65 | 66 | public function testSubmitComments() 67 | { 68 | $comment = "This is a very insightful comment."; 69 | $this->byId('comments')->value($comment); 70 | $this->byId('submit')->submit(); 71 | $driver = $this; 72 | 73 | $comment_test = function() use ($comment, $driver) { 74 | $text = $driver->byId('your_comments')->text(); 75 | return $text == "Your comments: $comment"; 76 | }; 77 | 78 | $this->spinAssert("Comment never showed up!", $comment_test); 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/SeleniumRCDriver.php: -------------------------------------------------------------------------------- 1 | $data === NULL) { 24 | throw new \PHPUnit_Framework_Exception("set$data() needs to be called before start()."); 25 | } 26 | } 27 | 28 | $data = array( 29 | 'username' => $this->username, 30 | 'access-key' => $this->access_key, 31 | 'os' => $this->os, 32 | 'browser' => $this->browser, 33 | 'browser-version' => $this->browser_version, 34 | 'name' => $this->name, 35 | 'record-video' => $this->record_video, 36 | 'video-upload-on-pass' => $this->video_upload_on_pass, 37 | ); 38 | 39 | $this->sessionId = $this->getString( 40 | 'getNewBrowserSession', 41 | array(json_encode($data), $this->browserUrl) 42 | ); 43 | 44 | $this->doCommand('setTimeout', array($this->seleniumTimeout * 1000)); 45 | 46 | return $this->sessionId; 47 | } 48 | 49 | public function setRecordVideo($record) 50 | { 51 | if (!is_bool($record)) { 52 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'bool'); 53 | } 54 | 55 | $this->record_video = $record; 56 | } 57 | 58 | public function setUploadVideoOnPass($record) 59 | { 60 | if (!is_bool($record)) { 61 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'bool'); 62 | } 63 | 64 | $this->video_upload_on_pass = $record; 65 | } 66 | 67 | public function setUsername($username) 68 | { 69 | if (!is_string($username)) { 70 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); 71 | } 72 | 73 | $this->username = $username; 74 | } 75 | 76 | public function setAccessKey($key) 77 | { 78 | if (!is_string($key)) { 79 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); 80 | } 81 | 82 | $this->access_key = $key; 83 | } 84 | 85 | public function setBrowserVersion($ver) 86 | { 87 | if (!is_string($ver)) { 88 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); 89 | } 90 | 91 | $this->browser_version = $ver; 92 | } 93 | 94 | public function setOs($os) 95 | { 96 | if (!is_string($os)) { 97 | throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); 98 | } 99 | 100 | $this->os = $os; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/SauceConfig.php: -------------------------------------------------------------------------------- 1 | username = $username; 19 | $this->access_key = $access_key; 20 | $this->verify_certs = $verify_certs; 21 | $this->methods = new SauceMethods($this->username); 22 | 23 | $sauce_host = 'saucelabs.com'; 24 | if ($use_european_host){ 25 | $sauce_host = 'eu-central-1.saucelabs.com'; 26 | } 27 | 28 | define('SAUCE_HOST', $sauce_host); 29 | } 30 | 31 | 32 | protected function buildUrl($endpoint) 33 | { 34 | if ($endpoint[0] != '/') 35 | $endpoint = '/'.$endpoint; 36 | 37 | return 'https://'.SAUCE_HOST.$endpoint; 38 | } 39 | 40 | protected function makeRequest($url, $type="GET", $params=false) 41 | { 42 | $ch = curl_init(); 43 | if($this->verify_certs == false) { 44 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 45 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 46 | } 47 | 48 | curl_setopt($ch, CURLOPT_URL, $url); 49 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 50 | curl_setopt($ch, CURLOPT_USERPWD, $this->username.":".$this->access_key); 51 | 52 | if ($type == "POST") 53 | curl_setopt($ch, CURLOPT_POST, 1); 54 | elseif ($type == "PUT") 55 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); 56 | elseif ($type == "DELETE") 57 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 58 | 59 | // If user has ca bundle location specified in environment 60 | // set that same path on php curl connection 61 | if (getenv('CURL_CA_BUNDLE')) { 62 | curl_setopt($ch, CURLOPT_CAINFO, getenv('CURL_CA_BUNDLE')); 63 | } 64 | 65 | // If user has requested it, be extremely verbose when making requests. 66 | // This is primarily intended to help Sauce Support staff figure out what's 67 | // busted. 68 | if (getenv('SAUCE_DIAGNOSE_SSL')) { 69 | curl_setopt($ch, CURLOPT_CERTINFO, true); 70 | curl_setopt($ch, CURLOPT_VERBOSE, true); 71 | curl_setopt($ch, CURLOPT_STDERR, getenv('SAUCE_DIAGNOSE_SSL')); 72 | } 73 | 74 | $headers = array(); 75 | $headers[] = 'Content-Type: text/json'; 76 | 77 | $data = ''; 78 | if ($params) { 79 | $data = json_encode($params); 80 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 81 | } 82 | $headers[] = 'Content-length:'.strlen($data); 83 | 84 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 85 | 86 | $response = curl_exec($ch); 87 | 88 | if (curl_errno($ch)) 89 | throw new \Exception("Got an error while making a request: ".curl_error($ch)); 90 | 91 | curl_close($ch); 92 | 93 | return $this->convertResult($response); 94 | } 95 | 96 | public function __call($command, $args) 97 | { 98 | $res = call_user_func_array(array($this->methods, $command), $args); 99 | 100 | if (sizeof($res) < 1) 101 | throw new \Exception("Got a bad API call format from $command"); $endpoint = $res[0]; 102 | 103 | $request_args = array_slice($res, 1); 104 | 105 | $url = $this->buildUrl($endpoint); 106 | 107 | array_unshift($request_args, $url); 108 | 109 | return call_user_func_array(array($this, 'makeRequest'), $request_args); 110 | } 111 | 112 | protected function convertResult($response) 113 | { 114 | $result = json_decode($response); 115 | 116 | if (!$result) { 117 | throw new \Exception("An error occurred parsing the response. ". 118 | "Please check your parameters and try again"); 119 | } 120 | 121 | $result = $this->convertObjToArray($result); 122 | 123 | return $result; 124 | } 125 | 126 | protected function convertObjToArray($obj) 127 | { 128 | if (is_object($obj)) 129 | $obj = get_object_vars($obj); 130 | if (is_array($obj)) 131 | foreach ($obj as $key => $val) 132 | $obj[$key] = $this->convertObjToArray($val); 133 | 134 | return $obj; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /WebDriverDemoShootout.php: -------------------------------------------------------------------------------- 1 | 'firefox', 14 | 'desiredCapabilities' => array( 15 | 'version' => '15', 16 | 'platform' => 'VISTA' 17 | ) 18 | ) 19 | ); 20 | 21 | protected function randomUser() 22 | { 23 | $id = uniqid(); 24 | return array( 25 | 'username' => "fakeuser_$id", 26 | 'password' => 'testpass', 27 | 'name' => "Fake $id", 28 | 'email' => "$id@fake.com" 29 | ); 30 | } 31 | 32 | protected function doLogin($username, $password) 33 | { 34 | $this->url('/'); 35 | $this->byName('login')->value($username); 36 | $this->byName('password')->value($password); 37 | $this->byCss('input.login')->click(); 38 | 39 | $this->assertTextPresent("Logged in successfully", $this->byId('message')); 40 | } 41 | 42 | protected function doLogout() 43 | { 44 | $this->url('/logout'); 45 | $this->assertTextPresent("Logged out successfully", $this->byId('message')); 46 | } 47 | 48 | protected function doRegister($user, $logout = false) 49 | { 50 | $user['confirm_password'] = isset($user['confirm_password']) ? 51 | $user['confirm_password'] : $user['password']; 52 | $this->url('/register'); 53 | $this->byId('username')->value($user['username']); 54 | $this->byId('password')->value($user['password']); 55 | $this->byId('confirm_password')->value($user['confirm_password']); 56 | $this->byId('name')->value($user['name']); 57 | $this->byId('email')->value($user['email']); 58 | $this->byId('form.submitted')->click(); 59 | 60 | if ($logout) 61 | $this->doLogout(); 62 | } 63 | 64 | public function setUpPage() 65 | { 66 | $this->url('http://tutorialapp.saucelabs.com'); 67 | } 68 | 69 | public function testLoginFailsWithBadCredentials() 70 | { 71 | $fake_username = uniqid(); 72 | $fake_password = uniqid(); 73 | 74 | $this->url('/'); 75 | $this->byName('login')->value($fake_username); 76 | $this->byName('password')->value($fake_password); 77 | $this->byCss('input.login')->click(); 78 | 79 | $this->assertTextPresent("Failed to login.", $this->byId('message')); 80 | } 81 | 82 | public function testLogin() 83 | { 84 | $user = $this->randomUser(); 85 | $this->doRegister($user, true); 86 | $this->doLogin($user['username'], $user['password']); 87 | } 88 | 89 | public function testLogout() 90 | { 91 | $this->doRegister($this->randomUser(), true); 92 | } 93 | 94 | public function testRegister() 95 | { 96 | $user = $this->randomUser(); 97 | $this->doRegister($user); 98 | $logged_in_text = "You are logged in as {$user['username']}"; 99 | $this->assertTextPresent($logged_in_text); 100 | } 101 | 102 | public function testRegisterFailsWithoutUsername() 103 | { 104 | $user = $this->randomUser(); 105 | $user['username'] = ''; 106 | $this->doRegister($user); 107 | $this->assertTextPresent("Please enter a value"); 108 | } 109 | 110 | public function testRegisterFailsWithoutName() 111 | { 112 | $user = $this->randomUser(); 113 | $user['name'] = ''; 114 | $this->doRegister($user); 115 | $this->assertTextPresent("Please enter a value"); 116 | } 117 | 118 | public function testRegisterFailsWithMismatchedPasswords() 119 | { 120 | $user = $this->randomUser(); 121 | $user['confirm_password'] = uniqid(); 122 | $this->doRegister($user); 123 | $this->assertTextPresent("Fields do not match"); 124 | } 125 | 126 | public function testRegisterFailsWithBadEmail() 127 | { 128 | $user = $this->randomUser(); 129 | $user['email'] = 'test'; 130 | $this->doRegister($user); 131 | $this->assertTextPresent("An email address must contain a single @"); 132 | $this->byId('email')->clear(); 133 | $this->byId('email')->value('@bob.com'); 134 | $this->byId('form.submitted')->click(); 135 | $this->assertTextPresent("The username portion of the email address is invalid"); 136 | $this->byId('email')->clear(); 137 | $this->byId('email')->value('test@bob'); 138 | $this->byId('form.submitted')->click(); 139 | $this->assertTextPresent("The domain portion of the email address is invalid"); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/SeleniumRCTestCase.php: -------------------------------------------------------------------------------- 1 | getDriver($browser); 24 | self::ShareSession(false); 25 | } 26 | 27 | protected function getDriver(array $browser) 28 | { 29 | $local = isset($browser['local']) && $browser['local']; 30 | $this->is_local_test = $local; 31 | 32 | if (!$local) { 33 | SauceTestCommon::RequireSauceConfig(); 34 | $build = SauceConfig::GetBuild(); 35 | } else { 36 | unset($browser['local']); 37 | } 38 | 39 | $defaults = array( 40 | 'browser' => 'firefox', 41 | 'browserVersion' => '11', 42 | 'os' => 'Windows 2008', 43 | 'timeout' => 60, 44 | 'httpTimeout' => 45, 45 | 'host' => 'ondemand.saucelabs.com', 46 | 'port' => 80, 47 | 'name' => get_called_class().'::'.$this->getName(), 48 | 'record-video' => true, 49 | 'video-upload-on-pass' => true, 50 | ); 51 | 52 | $local_defaults = array( 53 | 'browser' => 'firefox', 54 | 'timeout' => 30, 55 | 'httpTimeout' => 45, 56 | 'host' => 'localhost', 57 | 'port' => 4444, 58 | 'name' => get_called_class().'::'.$this->getName(), 59 | ); 60 | 61 | if ($local) 62 | $browser = array_merge($local_defaults, $browser); 63 | else 64 | $browser = array_merge($defaults, $browser); 65 | 66 | $checks = array( 67 | 'name' => 'string', 68 | 'browser' => 'string', 69 | 'browserVersion' => 'string', 70 | 'timeout' => 'int', 71 | 'httpTimeout' => 'int', 72 | 'os' => 'string' 73 | ); 74 | if ($local) { 75 | unset($checks['browserVersion']); 76 | unset($checks['os']); 77 | } 78 | 79 | foreach ($checks as $key => $type) { 80 | $func = 'is_'.$type; 81 | if (!$func($browser[$key])) { 82 | throw new InvalidArgumentException( 83 | 'Array element "'.$key.'" is no '.$type.'.' 84 | ); 85 | } 86 | } 87 | 88 | if ($local) 89 | $driver = new \PHPUnit_Extensions_SeleniumTestCase_Driver(); 90 | else 91 | $driver = new SeleniumRCDriver(); 92 | if (!$local) { 93 | $driver->setUsername(SAUCE_USERNAME); 94 | $driver->setAccessKey(SAUCE_ACCESS_KEY); 95 | $driver->setOs($browser['os']); 96 | $driver->setBrowserVersion($browser['browserVersion']); 97 | $driver->setRecordVideo($browser['record-video']); 98 | $driver->setUploadVideoOnPass($browser['video-upload-on-pass']); 99 | $build = isset($browser['build']) ? $browser['build'] : $build; 100 | if ($build) 101 | $this->build = $build; 102 | } 103 | $driver->setHost($browser['host']); 104 | $driver->setPort($browser['port']); 105 | $driver->setName($browser['name']); 106 | $driver->setBrowser($browser['browser']); 107 | $driver->setTimeout($browser['timeout']); 108 | $driver->setHttpTimeout($browser['httpTimeout']); 109 | $driver->setTestCase($this); 110 | $driver->setTestId($this->testId); 111 | 112 | 113 | $this->drivers[0] = $driver; 114 | 115 | return $driver; 116 | } 117 | 118 | protected function prepareTestSession() 119 | { 120 | $this->job_id = parent::prepareTestSession(); 121 | if ($this->build) 122 | SauceTestCommon::ReportBuild($this->job_id, $this->build); 123 | $this->postSessionSetUp(); 124 | return $this->job_id; 125 | } 126 | 127 | protected function postSessionSetUp() 128 | { 129 | } 130 | 131 | 132 | public function tearDown() 133 | { 134 | if (!$this->is_local_test) { 135 | SauceTestCommon::ReportStatus($this->job_id, !$this->hasFailed()); 136 | 137 | if(getenv('JENKINS_HOME')) { 138 | printf("\nSauceOnDemandSessionID=%s job-name=%s", $this->job_id, get_called_class().'.'.$this->getName()."\n"); 139 | } 140 | } 141 | } 142 | 143 | public function spinAssert($msg, $test, $args=array(), $timeout=10) 144 | { 145 | list($result, $msg) = SauceTestCommon::SpinAssert($msg, $test, $args, $timeout); 146 | $this->assertTrue($result, $msg); 147 | } 148 | 149 | public static function browsers() { 150 | $json = getenv('bamboo_SAUCE_ONDEMAND_BROWSERS'); 151 | 152 | if (!$json) { 153 | $json = getenv('SAUCE_ONDEMAND_BROWSERS'); 154 | } 155 | 156 | if ($json) { 157 | $jsonMapFn = function($options) { 158 | return array( 159 | 'browser' => $options->browser, 160 | 'browserVersion' => $options->{'browser-version'}, 161 | 'os' => $options->os 162 | ); 163 | }; 164 | $jsonDecode = json_decode($json); 165 | 166 | if ($jsonDecode) { 167 | return array_map($jsonMapFn, $jsonDecode); 168 | } 169 | } 170 | 171 | //Check for set browsers from child test case 172 | if (!empty(static::$browsers) && is_array(static::$browsers)) { 173 | return static::$browsers; 174 | } 175 | 176 | throw new \Exception('No browsers found.'); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/SauceMethods.php: -------------------------------------------------------------------------------- 1 | username = $username; 22 | } 23 | 24 | protected function requireParam($param_name, $param_val, 25 | $check_truthiness=true) 26 | { 27 | if($param_val === NULL || ($check_truthiness && !$param_val)) 28 | throw new \Exception("$param_name is required"); 29 | } 30 | 31 | protected function requireParams(array $params) 32 | { 33 | foreach ($params as $param_set) 34 | call_user_func_array(array($this, 'requireParam'), $param_set); 35 | } 36 | 37 | /* user methods */ 38 | 39 | public function getAccountDetails() 40 | { 41 | return array(SAUCE_API_PREFIX.'users/'.$this->username); 42 | } 43 | 44 | public function getAccountLimits() 45 | { 46 | return array(SAUCE_API_PREFIX.$this->username.'/limits'); 47 | } 48 | 49 | public function createUser($user_details) 50 | { 51 | throw new \Exception("Create user is only for authorized partners"); 52 | } 53 | 54 | public function login($password, $username = null) 55 | { 56 | $this->requireParam("password", $password); 57 | $username = $username ? $username : $this->username; 58 | 59 | return array( 60 | SAUCE_API_PREFIX.'users/'.$username.'/login', 61 | "POST", 62 | array('password'=>$password) 63 | ); 64 | } 65 | 66 | public function createSubaccount(array $subacct_details) 67 | { 68 | $this->requireParam("subacct_details", $subacct_details); 69 | 70 | foreach ($subacct_details as $key => $val) 71 | if (!in_array($key, self::$user_fields)) 72 | throw new \Exception("$key is not a valid subaccount field"); 73 | 74 | foreach (self::$user_fields as $key) 75 | if (!isset($subacct_details[$key])) 76 | throw new \Exception("$key is a required subaccount field"); 77 | 78 | return array( 79 | SAUCE_API_PREFIX.'users/'.$this->username, 80 | "POST", 81 | $subacct_details 82 | ); 83 | } 84 | 85 | public function setSubaccountSubscription($username, $plan) 86 | { 87 | $this->requireParam("username", $username); 88 | $this->requireParam("plan", $plan); 89 | 90 | return array( 91 | SAUCE_API_PREFIX.'users/'.$username.'/subscription', 92 | "POST", 93 | array('plan' => $plan) 94 | ); 95 | } 96 | 97 | public function deleteSubaccountSubscription($username) 98 | { 99 | $this->requireParam("username", $username); 100 | 101 | return array( 102 | SAUCE_API_PREFIX.'users/'.$username.'/subscription', 103 | "DELETE" 104 | ); 105 | } 106 | 107 | /* usage and activity methods */ 108 | 109 | public function getUsage($start = null, $end = null, $username = null) 110 | { 111 | $username = $username ? $username : $this->username; 112 | 113 | $q = http_build_query(array('start' => $start, 'end' => $end)); 114 | 115 | return array(SAUCE_API_PREFIX.'users/'.$username.'/usage?'.$q); 116 | } 117 | 118 | /* job methods */ 119 | 120 | public function getJobs($from = null, $to = null, $limit = null, 121 | $skip = null, $username = null, $full = false) 122 | { 123 | $username = $username ? $username : $this->username; 124 | $q = http_build_query(array( 125 | 'from' => $from, 126 | 'to' => $to, 127 | 'limit' => $limit, 128 | 'skip' => $skip, 129 | 'full' => $full 130 | )); 131 | return array(SAUCE_API_PREFIX.$username.'/jobs_safe?'.$q); 132 | } 133 | 134 | public function getJobsForBuild($build, $limit = null, $skip = null, $username = null, $full = false) 135 | { 136 | $this->requireParam("build", $build); 137 | $username = $username ? $username : $this->username; 138 | $q = http_build_query(array( 139 | 'limit' => $limit, 140 | 'skip' => $skip, 141 | 'full' => $full 142 | )); 143 | return array(SAUCE_API_PREFIX.$username.'/build/'.$build.'/jobs?'.$q); 144 | } 145 | 146 | public function getUpdatedJobs($since, $username = null) 147 | { 148 | $this->requireParam("since", $since, false); 149 | $username = $username ? $username : $this->username; 150 | $q = http_build_query(array('since' => $since)); 151 | return array(SAUCE_API_PREFIX.$username.'/updated_jobs_safe?'.$q); 152 | } 153 | 154 | public function getJob($job_id) 155 | { 156 | $this->requireParam('job_id', $job_id); 157 | return array(SAUCE_API_PREFIX.$this->username.'/jobs/'.$job_id); 158 | } 159 | 160 | public function getActivity($username = null) 161 | { 162 | $username = $username ? $username : $this->username; 163 | return array(SAUCE_API_PREFIX.$username.'/activity'); 164 | } 165 | 166 | public function updateJob($job_id, $job_details) 167 | { 168 | $this->requireParams(array( 169 | array("job_id", $job_id), 170 | array("job_details", $job_details) 171 | )); 172 | 173 | return array( 174 | SAUCE_API_PREFIX.$this->username.'/jobs/'.$job_id, 175 | "PUT", 176 | $job_details 177 | ); 178 | 179 | } 180 | 181 | public function stopJob($job_id) 182 | { 183 | $this->requireParam("job_id", $job_id); 184 | return array( 185 | SAUCE_API_PREFIX.$this->username.'/jobs/'.$job_id.'/stop', 186 | "PUT" 187 | ); 188 | } 189 | 190 | public function getJobAssets($job_id, $username = null) 191 | { 192 | $this->requireParam('job_id', $job_id); 193 | $username = $username ? $username : $this->username; 194 | return array(SAUCE_API_PREFIX . $username . '/jobs/' . $job_id . '/assets'); 195 | } 196 | 197 | /* tunnel methods */ 198 | 199 | public function getTunnels($username = null) 200 | { 201 | $username = $username ? $username : $this->username; 202 | return array(SAUCE_API_PREFIX.$username.'/tunnels'); 203 | } 204 | 205 | public function getTunnel($tunnel_id, $username = null) 206 | { 207 | $this->requireParam("tunnel_id", $tunnel_id); 208 | $username = $username ? $username : $this->username; 209 | return array(SAUCE_API_PREFIX.$username.'/tunnels/'.$tunnel_id); 210 | } 211 | 212 | public function deleteTunnel($tunnel_id, $username = null) 213 | { 214 | $this->requireParam("tunnel_id", $tunnel_id); 215 | $username = $username ? $username : $this->username; 216 | return array( 217 | SAUCE_API_PREFIX.$username.'/tunnels/'.$tunnel_id, 218 | "DELETE" 219 | ); 220 | } 221 | 222 | /* Reporting methods */ 223 | 224 | public function createErrorReport($info, $tunnel = null, $username = null) 225 | { 226 | $username = $username ? $username : $this->username; 227 | $params = array('info' => $info); 228 | if ($tunnel !== null) 229 | $params['Tunnel'] = $tunnel; 230 | 231 | return array(SAUCE_API_PREFIX.$username.'/errors', "POST", $params); 232 | } 233 | 234 | /* Sauce Labs informational methods */ 235 | 236 | public function getAllBrowsers() 237 | { 238 | return array(SAUCE_API_PREFIX.'info/browsers/all'); 239 | } 240 | 241 | public function getSeleniumRCBrowsers() 242 | { 243 | return array(SAUCE_API_PREFIX.'info/browsers/selenium-rc'); 244 | } 245 | 246 | public function getWebDriverBrowsers() 247 | { 248 | return array(SAUCE_API_PREFIX.'info/browsers/webdriver'); 249 | } 250 | 251 | public function getStatus() 252 | { 253 | return array(SAUCE_API_PREFIX.'info/status'); 254 | } 255 | 256 | public function getSauceTestsCount() 257 | { 258 | return array(SAUCE_API_PREFIX.'info/counter'); 259 | } 260 | 261 | public function getScoutBrowsers($sanitized=false) 262 | { 263 | $endpoint = SAUCE_API_PREFIX.'info/scout'; 264 | $endpoint .= $sanitized ? '/sanitized' : ''; 265 | return array($endpoint); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Sauce/Sausage/TestCase.php: -------------------------------------------------------------------------------- 1 | getDesiredCapabilities(); 24 | $this->setBrowserUrl(''); 25 | if (!isset($caps['name'])) { 26 | $caps['name'] = get_called_class().'::'.$this->getName(); 27 | $this->setDesiredCapabilities($caps); 28 | } 29 | 30 | $tunnelId = getenv('SAUCE_TUNNEL_IDENTIFIER'); 31 | if ($tunnelId) { 32 | $caps = $this->getDesiredCapabilities(); 33 | $caps['tunnel-identifier'] = $tunnelId; 34 | $this->setDesiredCapabilities($caps); 35 | } 36 | } 37 | 38 | public function setUpPage() 39 | { 40 | if ($this->start_url) 41 | $this->url($this->start_url); 42 | } 43 | 44 | public function setupSpecificBrowser($params) 45 | { 46 | // Setting 'local' gives us nice defaults of localhost:4444 47 | $local = (isset($params['local']) && $params['local']); 48 | $this->is_local_test = $local; 49 | 50 | if (!$local) 51 | SauceTestCommon::RequireSauceConfig(); 52 | 53 | // Give some nice defaults 54 | if (!isset($params['seleniumServerRequestsTimeout'])) 55 | $params['seleniumServerRequestsTimeout'] = 60; 56 | 57 | if (!isset($params['browserName'])) { 58 | $params['browserName'] = 'chrome'; 59 | $params['desiredCapabilities'] = array( 60 | 'version' => '', 61 | 'platform' => 'VISTA' 62 | ); 63 | } 64 | 65 | 66 | // Set up host 67 | 68 | $host = isset($params['host']) ? $params['host'] : false; 69 | if ($local) { 70 | $this->setHost($host ? $host : 'localhost'); 71 | } else { 72 | $sauce_host = SAUCE_USERNAME.':'.SAUCE_ACCESS_KEY.'@ondemand.saucelabs.com'; 73 | $this->setHost($host ? $host : $sauce_host); 74 | } 75 | 76 | // Set up port 77 | $port = isset($params['port']) ? $params['port'] : false; 78 | $this->setPort($port ? $port : ($local ? 4444 : 80)); 79 | 80 | // Set up other params 81 | $this->setBrowser($params['browserName']); 82 | $caps = isset($params['desiredCapabilities']) ? $params['desiredCapabilities'] : array(); 83 | $this->setSeleniumServerRequestsTimeout( 84 | $params['seleniumServerRequestsTimeout']); 85 | $build = isset($params['build']) ? $params['build'] : SauceConfig::GetBuild(); 86 | if ($build && !isset($caps['build'])) 87 | $caps['build'] = $build; 88 | $this->setDesiredCapabilities($caps); 89 | 90 | // If we're using Sauce, make sure we don't try to share browsers 91 | if (!$local && !$host && isset($params['sessionStrategy'])) { 92 | $params['sessionStrategy'] = 'isolated'; 93 | } 94 | 95 | $this->setUpSessionStrategy($params); 96 | } 97 | 98 | public function isTextPresent($text, \PHPUnit_Extensions_Selenium2TestCase_Element $element = NULL) 99 | { 100 | $element = $element ?: $this->byCssSelector('body'); 101 | $el_text = str_replace("\n", " ", $element->text()); 102 | return strpos($el_text, $text) !== false; 103 | } 104 | 105 | public function waitForText($text, \PHPUnit_Extensions_Selenium2TestCase_Element $element = NULL, 106 | $timeout = 10) 107 | { 108 | $test = function() use ($element, $text) { 109 | $el_text = $element ? $element->text() : $this->byCssSelector('body')->text(); 110 | $el_text = str_replace("\n", " ", $el_text); 111 | return strpos($el_text, $text) !== false; 112 | }; 113 | $this->spinWait("Text $text never appeared!", $test, array(), $timeout); 114 | } 115 | 116 | 117 | public function assertTextPresent($text, \PHPUnit_Extensions_Selenium2TestCase_Element $element = NULL) 118 | { 119 | $this->spinAssert("$text was never found", function() use ($text, $element) { 120 | $el_text = $element ? $element->text() : $this->byCssSelector('body')->text(); 121 | return strpos($el_text, $text) !== false; 122 | }); 123 | } 124 | 125 | public function assertTextNotPresent($text, \PHPUnit_Extensions_Selenium2TestCase_Element $element = NULL) 126 | { 127 | $this->spinAssert("$text was found", function() use ($text, $element) { 128 | $el_text = $element ? $element->text() : $this->byCssSelector('body')->text(); 129 | return strpos($el_text, $text) === false; 130 | }); 131 | } 132 | 133 | public function byCss($selector) 134 | { 135 | return parent::byCssSelector($selector); 136 | } 137 | 138 | public function fileDetector($fileDetectorFunction) 139 | { 140 | $this->fileDetectorFunction = $fileDetectorFunction; 141 | } 142 | 143 | public function sendKeys(\PHPUnit_Extensions_Selenium2TestCase_Element $element, $keys) 144 | { 145 | if($this->fileDetectorFunction && 146 | call_user_func($this->fileDetectorFunction, $keys)) { 147 | $remote_file = $this->file($keys); 148 | $element->value($remote_file); 149 | } else { 150 | $element->click(); 151 | $this->keys($keys); 152 | } 153 | } 154 | 155 | public function tearDown() 156 | { 157 | if (!$this->is_local_test) { 158 | SauceTestCommon::ReportStatus($this->getSessionId(), !$this->hasFailed()); 159 | 160 | if(getenv('JENKINS_HOME')) { 161 | printf("\nSauceOnDemandSessionID=%s job-name=%s", $this->getSessionId(), get_called_class().'.'.$this->getName()."\n"); 162 | } 163 | } 164 | } 165 | 166 | public function spinAssert($msg, $test, $args=array(), $timeout=10) 167 | { 168 | list($result, $msg) = SauceTestCommon::SpinAssert($msg, $test, $args, $timeout); 169 | $this->assertTrue($result, $msg); 170 | } 171 | 172 | public function spinWait($msg, $test, $args=array(), $timeout=10) 173 | { 174 | list($result, $msg) = SauceTestCommon::SpinAssert($msg, $test, $args, $timeout); 175 | if (!$result) 176 | throw new \Exception($msg); 177 | } 178 | 179 | protected function buildUrl($url) 180 | { 181 | if ($url !== NULL && $this->base_url !== NULL && !preg_match("/^https?:/", $url)) { 182 | if (strlen($url) && $url[0] == '/') { 183 | $sep = ''; 184 | } else { 185 | $sep = '/'; 186 | } 187 | $url = trim($this->base_url, '/').$sep.$url; 188 | } 189 | return $url; 190 | } 191 | 192 | public function url($url = NULL) 193 | { 194 | return parent::url($this->buildUrl($url)); 195 | } 196 | 197 | public function setBrowserUrl($url = '') 198 | { 199 | return parent::setBrowserUrl($this->buildUrl($url)); 200 | } 201 | 202 | public function createNoLoginLink() 203 | { 204 | // generate as per http://saucelabs.com/docs/integration#public-job-links 205 | $job_id = $this->getSessionId(); 206 | $key = SAUCE_USERNAME.':'.SAUCE_ACCESS_KEY; 207 | $auth_token = hash_hmac("md5", $job_id, $key); 208 | 209 | return "https://saucelabs.com/jobs/".$job_id."?auth=".$auth_token; 210 | } 211 | 212 | public function toString() 213 | { 214 | if(!$this->is_local_test && $this->hasFailed()) 215 | return parent::toString()."\nReport link: ".$this->createNoLoginLink()."\n"; 216 | return parent::toString(); 217 | } 218 | 219 | public static function browsers() { 220 | $json = getenv('bamboo_SAUCE_ONDEMAND_BROWSERS'); 221 | 222 | if (!$json) { 223 | $json = getenv('SAUCE_ONDEMAND_BROWSERS'); 224 | } 225 | 226 | if ($json) { 227 | $jsonMapFn = function($options) { 228 | return array( 229 | 'browserName' => $options->browser, 230 | 'desiredCapabilities' => array( 231 | 'platform' => $options->os, 232 | 'version' => $options->{'browser-version'}, 233 | ), 234 | ); 235 | }; 236 | $jsonDecode = json_decode($json); 237 | 238 | if ($jsonDecode) { 239 | return array_map($jsonMapFn, $jsonDecode); 240 | } 241 | } 242 | 243 | //Check for set browsers from child test case 244 | if (!empty(static::$browsers) && is_array(static::$browsers)) { 245 | return static::$browsers; 246 | } 247 | 248 | throw new \Exception('No browsers found.'); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jlipps/sausage.svg?branch=master)](https://travis-ci.org/jlipps/sausage) 2 | 3 | Sausage 4 | ======= 5 | 6 | Your one-stop shop for everything Selenium + Sauce Labs + PHP. This is a set of 7 | classes and libraries that make it easy to run your Selenium tests, either 8 | locally or on Sauce Labs. You run the tests with [PHPUnit](http://phpunit.de). 9 | 10 | Sausage comes bundled with [Paratest](http://github.com/brianium/paratest) (for 11 | running your PHPUnit tests in parallel) and optionally 12 | [Sauce Connect](http://saucelabs.com/docs/connect) (for testing locally-hosted 13 | sites with Sauce). 14 | 15 | Read the rest of this page for installation and usage instructions designed 16 | to help you get the most out of your sausage. 17 | 18 | License 19 | ------- 20 | Sausage is available under the Apache 2 license. See `LICENSE.APACHE2` for more 21 | details. 22 | 23 | Quickstart 24 | ---------- 25 | Check out [sausage-bun](http://github.com/jlipps/sausage-bun). It's a one-line 26 | script you can run via curl and PHP to get everything going. For example: 27 | 28 | ``` 29 | curl -sL https://raw.githubusercontent.com/jlipps/sausage-bun/master/givememysausage.php | php 30 | ``` 31 | 32 | _Note_: if you are a Windows user who's not using Cygwin, it'll take a little 33 | extra work to set you up---please see the [sausage-bun 34 | README](http://github.com/jlipps/sausage-bun) 35 | 36 | Manual Install 37 | ------------ 38 | Sausage is distributed as a [Composer](http://getcomposer.org) package via 39 | [Packagist](http://packagist.org/), 40 | under the package `sauce/sausage`. To get it, add (or update) the `composer.json` 41 | file in your project root. A minimal example composer.json would look like: 42 | 43 | ```json 44 | { 45 | "require": { 46 | "sauce/sausage": ">=0.15.2" 47 | } 48 | } 49 | ``` 50 | 51 | If you haven't already got Composer installed, get it thusly (for *nix/Mac): 52 | 53 | curl -sL http://getcomposer.org/installer | php 54 | 55 | Then, install the packages (or `update` if you've already set up Composer): 56 | 57 | php composer.phar install 58 | 59 | This will install Sausage and all its dependences (like PHPUnit, etc...). If 60 | you didn't already have the `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment 61 | variables set, you'll now need to configure Sausage for use with your Sauce 62 | account: 63 | 64 | vendor/bin/sauce_config YOUR_SAUCE_USERNAME YOUR_SAUCE_ACCESS_KEY 65 | 66 | (Or for Windows): 67 | 68 | vendor\bin\sauce_config.bat YOUR_SAUCE_USERNAME YOUR_SAUCE_ACCESS_KEY 69 | 70 | (It's a Composer convention for package binaries to be located in `vendor/bin`; 71 | you can always symlink things elsewhere if it's more convenient). 72 | 73 | Requirements 74 | --- 75 | * Sausage will work on any modern (>= 5.4) PHP installation 76 | * Composer's requirements must also be satisfied (unfortunately, I could not 77 | find these documented anywhere). Suffice it to say they're normal requirements 78 | like the cURL extension, `safe_mode` off, `allow_url_fopen`, etc... 79 | * If you're on a Windows machine, you might want to set up all your PHP stuff 80 | in [Cygwin](http://cygwin.com) 81 | 82 | Getting Started 83 | ---- 84 | If everything's set up correctly, you should be able to run this: 85 | 86 | vendor/bin/phpunit vendor/sauce/sausage/WebDriverDemo.php 87 | 88 | (Or for Windows): 89 | 90 | vendor\bin\phpunit.bat vendor\sauce\sausage\WebDriverDemo.php 91 | 92 | And start seeing tests pass. (While the tests are running, you can check on 93 | their progress by going to your [Sauce tests 94 | page](http://saucelabs.com/tests)) 95 | 96 | Getting Started with Mobile 97 | ---- 98 | Running tests on Mobile uses [Appium](http://appium.io). If everything is set up 99 | correctly, you should be able to run this: 100 | 101 | vendor/bin/phpunit vendor/sauce.sausage/MobileDemo.php 102 | 103 | (Or for Windows): 104 | 105 | vendor\bin\phpunit.bat vendor\sauce\sausage\AppiumDemo.php 106 | 107 | And start seeing tests pass. (While the tests are running, you can check on 108 | their progress by going to your [Sauce tests 109 | page](http://saucelabs.com/tests)) 110 | 111 | Running tests in parallel 112 | --- 113 | Running Selenium tests one at a time is like eating one cookie at a time. Let's 114 | do it all at once! Try this: 115 | 116 | vendor/bin/paratest -p 2 -f --phpunit=vendor/bin/phpunit vendor/sauce/sausage/WebDriverDemo.php 117 | 118 | (Or for Windows): 119 | 120 | vendor\bin\paratest.bat -p 2 -f --phpunit=vendor\bin\phpunit.bat vendor\sauce\sausage\WebDriverDemo.php 121 | 122 | Now they'll finish twice as fast! (And if you get a [Sauce Labs 123 | account](http://saucelabs.com/pricing), you can 124 | bump up that concurrency to 4, 10, 20, 30, or more!) 125 | 126 | Writing WebDriver tests 127 | --- 128 | Writing tests for Selenium 2 (WebDriver) is easy and straightforward. Sausage 129 | is by default built on top of 130 | [PHPUnit_Selenium](http://github.com/sebastianbergmann/phpunit-selenium). All 131 | commands that work in `PHPUnit_Extensions_Selenium2TestCase` also work in 132 | Sausage's `WebDriverTestCase`. Here's a simple example: 133 | 134 | ```php 135 | 'firefox', 147 | 'desiredCapabilities' => array( 148 | 'version' => '15', 149 | 'platform' => 'VISTA' 150 | ) 151 | ), 152 | // run Chrome on Linux on Sauce 153 | array( 154 | 'browserName' => 'chrome', 155 | 'desiredCapabilities' => array( 156 | 'platform' => 'Linux' 157 | ) 158 | ) 159 | ); 160 | 161 | public function testLink() 162 | { 163 | $link = $this->byId('i am a link'); 164 | $link->click(); 165 | $this->assertContains("I am another page title", $this->title()); 166 | } 167 | } 168 | ``` 169 | 170 | In this example, we define a set of browsers to use, and run a simple check 171 | to make sure that clicking on a link gets us to the expected new page. 172 | 173 | For more examples, check out: 174 | * The `WebDriverDemo.php` file in this repository 175 | * The documentation for [PHPUnit_Extensions_Selenium2TestCase](http://www.phpunit.de/manual/3.7/en/selenium.html#selenium.selenium2testcase) 176 | 177 | If you're into Selenium 1 (Selenium RC), instead take a look at 178 | `SeleniumRCDemo.php` 179 | 180 | Writing Mobile tests 181 | --- 182 | Writing tests for mobile devices is easy and straightforward. Sausage 183 | is by default built on top of [Appium](http://appium.io) and the [Appium PHP 184 | Client](https://github.com/appium/php-client) and 185 | [PHPUnit_Selenium](http://github.com/sebastianbergmann/phpunit-selenium). All 186 | commands that work in `PHPUnit_Extensions_Selenium2TestCase` also work in 187 | Sausage's `MobileTestCase`. Here's a simple example: 188 | 189 | ```php 190 | '', 201 | 'desiredCapabilities' => array( 202 | 'appium-version' => '1.0', 203 | 'platformName' => 'iOS', 204 | 'platformVersion' => '7.0', 205 | 'deviceName' => 'iPhone Simulator', 206 | 'name' => 'Appium/Sauce iOS Test, PHP', 207 | 'app' => APP_URL 208 | ) 209 | ) 210 | ); 211 | 212 | public function elemsByClassName($klass) 213 | { 214 | return $this->elements($this->using('class name')->value($klass)); 215 | } 216 | 217 | protected function populate() 218 | { 219 | $elems = $this->elemsByClassName('UIATextField'); 220 | foreach ($elems as $elem) { 221 | $randNum = rand(0, 10); 222 | $elem->value($randNum); 223 | $this->numValues[] = $randNum; 224 | } 225 | } 226 | 227 | public function testUiComputation() 228 | { 229 | $this->populate(); 230 | $buttons = $this->elemsByClassName('UIAButton'); 231 | $buttons[0]->click(); 232 | $texts = $this->elemsByClassName('UIAStaticText'); 233 | $this->assertEquals(array_sum($this->numValues), (int)($texts[0]->text())); 234 | } 235 | } 236 | ``` 237 | 238 | Here we define a the device capabilities we want to use, and run a simple test 239 | of finding elements and interacting with them. 240 | 241 | Sauce Labs API 242 | --- 243 | Sausage comes bundled with a nice PHP interface to the [Sauce Labs API](https://saucelabs.com/docs/rest): 244 | 245 | ```php 246 | getAccountDetails(); 251 | 252 | $most_recent_test = $s->getJobs(0)['jobs'][0]; 253 | $s->updateJob($most_recent_test['id'], array('passed' => true)); 254 | 255 | $browser_list = $s->getAllBrowsers(); 256 | foreach ($browser_list as $browser) { 257 | $name = $browser['long_name']; 258 | $ver = $browser['short_version']; 259 | $os = $browser['os']; 260 | echo "$name $ver $os\n"; 261 | } 262 | ``` 263 | 264 | See `Sauce/Sausage/SauceMethods.php` for the list of Sauce API functions (currently 265 | boasting 100% support). Also check out `sauce_api_test.php` for other examples. 266 | 267 | Automatic Test Naming 268 | --- 269 | By default, Sauce Labs doesn't know how to display the name of your test. Sausage 270 | comes up with a good name (`TestClass::testFunction`) and reports it with your 271 | test so it's easy to find on your [tests page](http://saucelabs.com/tests). 272 | 273 | Automatic Test Status Reporting 274 | --- 275 | Since Selenium commands might be successful but your test still fails because 276 | of an assertion, there is in principle no way for Sauce Labs to know whether a 277 | particular run was a pass or fail. Sausage catches any failed assertions and 278 | makes sure to report the status of the test to Sauce after it's complete, so 279 | as you're looking at your log of tests you can easily see which passed and which 280 | failed. 281 | 282 | Automatic Authorized Link Generation 283 | --- 284 | Upon test failure, Sausage generates a authorized link to the failed job report 285 | on the Sauce Labs website, to facilitate reporting to people who need to know 286 | the details of the test. The job remains private (unless you change the status 287 | yourself), but others can follow the link without needing to log in with your 288 | credentials. 289 | 290 | Build IDs 291 | ---- 292 | If you're running your tests as part of your build, you can define a build id, 293 | either by updating the browser arrays to include a 'build' parameter, or 294 | (more reasonably), defining an environment variable `SAUCE_BUILD`, like so: 295 | 296 | SAUCE_BUILD=build-1234 vendor/bin/phpunit MyAwesomeTestCase.php 297 | 298 | 299 | SpinAsserts 300 | --- 301 | SpinAsserts are awesome and [you should really use them](http://sauceio.com/index.php/2011/04/how-to-lose-races-and-win-at-selenium/). Luckily, Sausage comes with a SpinAssert framework built in. 302 | Let's say we want to perform a check and we're not exactly sure how quickly the 303 | state will change to what we want. We can do this: 304 | 305 | ```php 306 | public function testSubmitComments() 307 | { 308 | $comment = "This is a very insightful comment."; 309 | $this->byId('comments')->click(); 310 | $this->keys($comment); 311 | $this->byId('submit')->submit(); 312 | $driver = $this; 313 | 314 | $comment_test = function() use ($comment, $driver) { 315 | return ($driver->byId('your_comments')->text() == "Your comments: $comment"); 316 | }; 317 | 318 | $this->spinAssert("Comment never showed up!", $comment_test); 319 | } 320 | ``` 321 | 322 | This will submit a comment and wait for up to 10 seconds for the comment to show 323 | up before declaring the test failed. 324 | 325 | The `spinWait` function is similar and allows you to wait for a certain 326 | condition without necessarily asserting anything of it. 327 | 328 | Sauce Connect 329 | --- 330 | Sauce Connect is a special tunnel-creating binary application (see the [Sauce 331 | Connect Docs](http://saucelabs.com/docs/connect)). It is bundled as another 332 | composer package (`sauce/connect`), which you can add to your `composer.json` 333 | requirements: 334 | 335 | ```json 336 | { 337 | "require": { 338 | "sauce/sausage": ">=0.5", 339 | "sauce/connect": ">=3.0" 340 | } 341 | } 342 | ``` 343 | 344 | If you've already run `vendor/bin/sauce_config` or otherwise set your Sauce 345 | credentials, starting sauce connect is as easy as: 346 | 347 | vendor/bin/sauce_connect 348 | 349 | (Or for Windows): 350 | 351 | vendor\bin\sauce_connect.bat 352 | 353 | Run that and you'll be testing against your local test server in no time! 354 | 355 | Ignoring certificate validation 356 | --- 357 | 358 | To connect to saucelabs, cURL is used. Sometimes certificate validation may fail, resulting in an error similar to this: 359 | 360 | ``` 361 | Exception: Got an error while making a request: server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none 362 | ``` 363 | 364 | You can manually disable curl certificate validation if needed by setting an environment variable `SAUCE_DONT_VERIFY_CERTS`. If any value is set, validation is skipped completely. 365 | 366 | Travis-ci and tunnel-identifier 367 | --- 368 | 369 | Travis use tunnel identifier to parallelize unit testing. You have to set the tunnel-identifier for your tests. 370 | 371 | To do so, just add this line to your .travis.yml file in install section 372 | 373 | ``` 374 | - export SAUCE_TUNNEL_IDENTIFIER=$TRAVIS_JOB_NUMBER 375 | ``` 376 | 377 | It's also recomanded to add the line below for travis-ci (see previous section) 378 | ``` 379 | - export SAUCE_DONT_VERIFY_CERTS=1 380 | ``` 381 | 382 | 383 | Contributors 384 | --- 385 | * Jonathan Lipps ([jlipps](http://github.com/jlipps)) (Author) 386 | 387 | If you have any ideas for Sausage, put them in code and send them my way! 388 | 389 | 390 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/jlipps/sausage/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 391 | 392 | --------------------------------------------------------------------------------