Sahi is a mature, business-ready tool for automation of web application testing.
Sahi is available as an Open Source free product and as Sahi Pro, the commercial version.
Sahi is especially suited for cross-browser/multi-browser testing of complex web 2.0 applications with lots of AJAX
and dynamic content. Sahi works well in Agile development environments, enabling rapid automation and maintenance
and easily integrating with build systems. Sahi saves time and effort with faster development, less maintenance
and fast distributed playback. Sahi runs on any modern browser which supports javascript.
Current version Sahi Pro v6.0.1
https://sahipro.com/docs/using-sahi/index.html
The injected javascript
Example:
_setValue(_textbox("user"), "test"); _setValue(_password("password"), "secret"); _click(_submit("Login")); _setValue(_textbox("q"), "2"); _setValue(_textbox("q[1]"), "1"); _setValue(_textbox("q[2]"), "1"); _click(_button("Add")); _click(_cell("Rs.200[1]")); _assertExists(_textbox("total")); _assert(_isVisible(_textbox("total"))); _assertEqual("1150", _textbox("total").value); _click(_button("Logout"));
create Functions:
_include("library.sah"); login("test", "secret"); addBooks(2, 1, 1); verifyTotal(1150); _click(_button("Logout"));
function login($username, $password){ _setValue(_textbox("user"), $username); _setValue(_password("password"), $password); _click(_submit("Login")); } function addBooks($numJava, $numRuby, $numPython){ _setValue(_textbox("q"), $numJava); _setValue(_textbox("q[1]"), $numRuby); _setValue(_textbox("q[2]"), $numPython); _click(_button("Add")); } function verifyTotal($total){ _assertEqual($total, _textbox("total").value); }
Sahi uses Rhino engine so Sahi script can call any java code in Sahi's classpath.
Rhino seamlessly translates variables between Java and Javascript.
The scripts are stored on the location where sahi is installed.
The folder Sahi_pro contains all the files to use sahi.
Under this folder you find userdata where all the scripts are stored
Double click on install_sahi_pro_xxx.jar to start the installer. If Java is configured properly, it will launch the installer.
https://sahipro.com/docs/using-sahi/quick-tutorial.html
on MAC
start sahi with \sahi_pro\userdata\bin\start_dashboard.sh
Add license to \sahi_pro\userdata\config folder
The Sahi Dashboard automatically starts the Sahi proxy and lets you start your configured browsers.
If you don't see your browser click configure and change the default location to your browser location.
Click on a browser. Now you should see a browser window like this
Press ALT and double click on the browser window. Sahi's Controller window will pop up.

Click record and sahi records all your actions into the script.
When testing an Extjs application sahi sometimes takes the dynamic id of an element.
By using CTRL and hovering over an element the element will show in the sahi controller under Accessor.
You can choose alternatives and actions to use.
Using the arrow you can go to the parent node and you can anchor to this element by clicking the anchor button.
If you now select an other element sahi will use _in, _near API to find your element.
https://sahipro.com/docs/using-sahi/sahi-controller.html
You can use the sahi editor or notepadd ++ with autocomplete api
http://sahipro.com/alldocs/v5.1.0.0/faq/sahi-with-notepad++.html
The sahi editor allows you to edit scripts, make functions, add documentation, make suites, data driven test …
Run tests on multiple browsers using playback.
By rightclicking a test you can see all the logs from a test or suite.
https://sahipro.com/docs/using-sahi/sahi-script-editor.html
| _link(12) | Using index in the page; assuming it is the 13th link on the page. |
| _link(“sahi_link”) | Using id as string |
| _link(/.*_link/) | Using id as regular expression |
| _link(“Link to Sahi website”) | Using visible text as string |
| _link(/Link to .* website/) | Using visible text as regular expression |
| _link(”/Link to .* website/”) | Using visible text as regular expression |
| _link({id:“sahi_link”}) | Using an associative array with id |
| _link({sahiText:”/Link to .*/”}) | Using an associative array with visible text as regular expression |
| _link({className:“low”,sahiText:”/Link.*/”}) | Using an associative array with multiple attributes like className and sahiText |
| _link(“delete”) | points to the first delete link. This is the same as _link(“delete[0]”) |
| _link(“delete[1]”) | points to the second delete link; Note that indexing starts at 0 |
| _link(”/del/[1]”) | points to the second delete link; Note that indexing starts at 0 |
| _near is a DOM relation marker which specifies that the element should be searched near another element. | |
| _in is a DOM relation marker which specifies that the element should be searched within another element. | |
| _link(0, _near(_cell(“User Two”))) | points to the 0th link near cell with text “User Two”. Note that the index is 0 here since it is the nearest link. |
| _link(“delete”, _near(_cell(“User Two”))) | points to the nearest link with text “delete” near cell with text “User Two”. Note that we do not need to specify “delete[1]” since it is the delete. link nearest to User Two. |
| _link(/del/, _near(_cell(“User Two”))) | points to the nearest link with text which matches regular expression /del/ near cell with text “User Two”. |
| _link(“delete[2]”, _near(_cell(“User Two”))) | points to the 3rd nearest link with text “delete” near cell with text “User Two”. |
| _link(”/del/[2]”, _near(_cell(“User Two”))) | points to the 3rd nearest link with text matching /del/ near cell with text “User Two”. Note how the regular expression is appended with the index in square brackets and quoted to make it a string |
| _link(0, _in(_cell(“del2”))) | points to the 0th link in cell with id “del2” |
| _link(“delete”, _in(_cell(“del2”))) | points to the link with text “delete” within cell with id “del2” |
| _cell(0, _near(_cell(“User One”)), _under(_cell(“Status”))) | Finds first cell near User One and under Status |
| _cell(0, _rightOf(_cell(“User One”)), _under(_cell(“Status”))) | Finds first cell to the right of User One and under Status |
| _cell(“Inactive”, _under(_cell(“Status”))) | Finds first Inactive cell under Status |
Form Input Elements
Mouse events
Touch events
Keyboard events
More Actions: https://sahipro.com/docs/sahi-apis/action-apis.html
Focus Events
Text Selection
Page Navigation
Waits
Native Events
File Upload
File Download
Window open/close APIs
Calling Generic Code
More APIs: https://sahipro.com/docs/sahi-apis/index.html
Relation APIs
Screenshot APIs
Include APIs
Fetch APIs
Assertion APIs
Logging APIs
Debug Helper APIs
File APIs
Excel APIs
Database APIs
Data Drive APIs
URL APIs
Script/Suite Info APIs
Script Execution Control APIs
HTTP Header Manipulation APIs
URL Mock APIs
Javascript Dialogs
Rich Text Editors
Popup Windows
Multiple Domains
Frames and Domains
Multiple Browsers
Execute External Programs
Utility APIs
Rest APIs
Applet APIs
Flex APIs
Layout APIs
Documentation APIs
PageObject model
if(!_isDataPassed()){ // entered if executed from Controller // NOT entered if run from dd.csv $username = "jcpadmin"; $password = "test"; } function LoginPage() { this.$url = "login.html"; this.go = function() { _navigateTo(this.$url); } this.loginAs = function($username, $password){ _startLookInside(_div("loginWindow")); _setValue(_textbox(0), $username); //username _setValue(_password(0), $password); //password //_click(_button(0)); //remember me _click(_link(0)); // login _stopLookInside(); } this.logout = function(){ //_click(_span($username, _in(_div("/.* viewportToolbar .*/")))); //_link("/x-btn.*x-btn-toolbar x-box-item.*/[2]"); _click(_link(0,_rightOf(_link("/Feedback/")))); _click(_span("Abmelden")); } this.verifyPage = function($page){ _assertEqual("/.*"+$page+".*/",document.location.pathname); //_assertEqual($page,window.document.documentURI.split("/")[4]); } this.verifyError = function(){ _assertExists(_div("errors")); _assertVisible(_div("errors")); } this.verifyErrorMessage = function($message){ this.verifyError() _assertContainsText($message, _div("errors")); } }
function logout(){ $loginPage.logout(); $loginPage.verifyPage("login.html"); } function loginfail($username,$password,$errorMessage){ $loginPage.go(); $loginPage.verifyPage("login.html"); $loginPage.loginAs($username,$password); $loginPage.verifyPage("login.html"); $loginPage.verifyErrorMessage($errorMessage); } function login($username,$password){ $loginPage.go(); $loginPage.verifyPage("login.html"); $loginPage.loginAs($username,$password); $loginPage.verifyPage("analyse.html"); } /* --Functions Above-- */ _include("../pages/loginPage.sah"); $loginPage = new LoginPage();
_setStrictVisibilityCheck(true); function AnalysePage() { this.$url = "analyse.html"; this.go = function(){ _navigateTo(this.$url);} this.goMainMenu = function(){ this.clickTab("Hauptmenü");} this.clickTab = function($name){ _click(_span($name)); } this.closeTab = function($name){ _click(_span("/x-tab-close-btn/", _near(_span($name))));} this.clickNewClient = function(){ _click(_link("neueMandantButton"));} this.clickEditClient = function(){ _click(_link("ladeMandantButton")); } this.clientFilterByName = function($fname, $lname) { _setValue(_textbox("vorname"), $fname); _setValue(_textbox("nachname"), $lname); _click(_span("Suche")); _wait(2000,!_isVisible(_div("Lade Daten...")) ); } this.clientFilterByBerater = function($filter,$item) { _setValue(_textbox("beraterSearchField"), $filter); _click(_div({className: "/x-boundlist-item/", sahiText : $item})); _click(_span("Suche")); _wait(2000,!_isVisible(_div("Lade Daten...")) ); } this.clickClient = function($fname,$lname) { this.clientFilterByName($fname,$lname); this.clickClientByName($fname,$lname); } this.clickClientByName = function($fname,$lname) { $rows = _count("_row","/x-grid-row.*x-grid-data-row/"); // get all rows with clients visible if($rows <= 0) _fail("no mandanten found"); _log($rows + " rows found"); for(var $i=0; $i<$rows; $i++) { $row = _row("/x-grid-row.*x-grid-data-row/["+$i+"]"); if($fname == _getCellText(_cell(0, _in($row))) && $lname == _getCellText(_cell(1, _in($row))) ){ //check firstname and lastname _doubleClick(_cell(0, _in($row))); return true; } } _fail("Mandant not found"); } this.clickTabPersönlicheAngaben = function() {this.clickTab("Persönliche Angaben")} this.clickTabVerträgeAngebote = function() {this.clickTab("Verträge & Angebote")} this.clickTabBeratungHandlungsempfehlung = function() {this.clickTab("Beratung - Handlungsempfehlung")} this.clickTabAngebotsrechner = function() {this.clickTab("Angebotsrechner")} this.clickTabDokumente = function() {this.clickTab("Dokumente")} this.clickTabWiedervorlagen = function() {this.clickTab("Wiedervorlagen")} this.clickTabKommunikation = function() {this.clickTab("Kommunikation")} this.deleteMandant = function($fname,$lname){ //start from main menu this.clientFilterByName($fname,$lname); $rows = _count("_row","/x-grid-row.*x-grid-data-row/"); _log($rows + " rows found"); for(var $i=0; $i<$rows; $i++) { $row = _row("/x-grid-row.*x-grid-data-row/["+$i+"]"); if($fname == _getCellText(_cell(0, _in($row))) && $lname == _getCellText(_cell(1, _in($row))) ){ //check firstname and lastname _click(_image("user_delete.png", _in($row))); //delete row _assertVisible(_span("Achtung")); _click(_link("Ja")); } } _click(_image("x-tool-img x-tool-close")); // close window } } function clickFormTrigger($inputElName){ // dropdown or calender icon _click(_div("/ x-form-trigger /", _near(_textbox($inputElName)))); } function setDropdownItem($inputElName, $item){ clickFormTrigger($inputElName); _click(_listItem($item)); } function checkFieldRequired($inputElName) { _assertEqual("/x-form-required-field/", _textbox($inputElName).className); } function checkFieldDisabled($inputElName) { _assertEqual(true, _textbox($inputElName).disabled); }
_include("../../login/login_lib.sah"); login("jcpadmin","test"); _include("../../pages/analysePage.sah"); $ap = new AnalysePage(); $ap.go(); //------------------------------------------ // test create Mandant //------------------------------------------ //---delete all "Test Tester" if exists ---- $ap.clickEditClient(); $ap.deleteMandant("Test Tester","Sahi"); //-----------create new (required)----- $ap.clickNewClient(); checkFieldRequired("vorname"); _setValue(_textbox("vorname"), "Test Tester"); checkFieldRequired("nachname"); _setValue(_textbox("nachname"), "Sahi"); _setValue(_textbox("geburtsdatum"), "01.01.1990"); checkFieldRequired("geburtsdatum"); _click(_span("Speichern")); //-------------Haushalt---------------- //------Kunde------------------------ _click(_div("Kunde")); _setValue(_textbox("strasse"), "Sahistraße"); _setValue(_textbox("hausnummer"), "1"); _click(_span("Änderungen speichern")); //------Partner------------------------ _click(_div("Partner")); _setValue(_textbox("vorname"), "Test Partner"); _setValue(_textbox("nachname"), "Sahi"); _setValue(_textbox("geburtsdatum"), "02.01.1990"); _click(_span("Änderungen speichern")); //-----Kinder----------- _click(_div("Kinder")); _click(_span("Neue")); _setValue(_textbox("vorname"), "Test Junior"); _setValue(_textbox("nachname"), "Sahi"); _setValue(_textbox("geburtsdatum"), "01.01.2015"); _click(_span("Änderungen speichern")); _click(_span("Neue")); clickFormTrigger("anrede"); _click(_listItem("weiblich")); _setValue(_textbox("vorname"), "Test Kinder"); _setValue(_textbox("nachname"), "Sahi"); _setValue(_textbox("geburtsdatum"), "02.01.2015"); _click(_span("Änderungen speichern"));
_include("../../pages/analysePage.sah"); $ap = new AnalysePage(); $ap.go(); $ap.clickEditClient(); $ap.clickClient("Test Tester","Sahi"); $ap.clickTabPersönlicheAngaben(); //------------------------------------------ // test Kunde //------------------------------------------ //-------------Person---------------------- _click(_div("Kunde")); _assertEqual("Torsten B", _getValue(_textbox("beraterId"))); _assertEqual("Herr", _getValue(_textbox("anrede"))); checkFieldRequired("anrede"); _assertEqual("Test Tester", _getValue(_textbox("vorname"))); checkFieldRequired("vorname"); _assertEqual("Sahi", _getValue(_textbox("nachname"))); checkFieldRequired("nachname"); _assertEqual("01.01.1990", _getValue(_textbox("geburtsdatum"))); checkFieldRequired("geburtsdatum"); _assertEqual("Sahistraße", _getValue(_textbox("strasse"))); //-------------Ausbildung/Beruf------------ _click(_span("Ausbildung/Beruf")); //_assertEqual("Angestellte/r", _getValue(_textbox("beschaeftigungsverhaeltnis"))); //-------------Arbeitgeber------------------- _click(_span("Arbeitgeber")); //-------------Kommunikation--------------- _click(_span("Kommunikation", _near(_span("Kunde")))); //_assertEqual("VersicherungsAG", _getValue(_textbox("contactVersicherungen"))); //_assertEqual("BankAG", _getValue(_textbox("contactBank"))); //_assertEqual("SteuerAG", _getValue(_textbox("contactSteuern"))); //_assertEqual("RechtsAG", _getValue(_textbox("contactRecht"))); //-------------Legitimation------------------ _click(_span("Legitimation")); //-------------Bank-------------------------- _click(_span("Bank")); //_assertEqual("BankAG", _getValue(_textbox("bank0"))); //-------------Gesundheit-------------------- _click(_span("Gesundheit")); //_assertEqual("193", _getValue(_textbox("grosse"))); //_assertEqual("89", _getValue(_textbox("gewicht"))); //_assertEqual("Nein", _getValue(_textbox("motorradfahrer"))); //-------------Erweiterte Berufsfragen------ _click(_span("Erweiterte Berufsfragen")); _assertEqual("Sonstige Berufsausbildung", _getValue(_textbox("eb_abgeschlosseneAusbildung"))); //-------------Steuern------------------------ _click(_span("Steuern")); //_assertEqual("1", _getValue(_textbox("steuerklasse")));
TestSuite
test/login/testlogin.s.csv test/login/testlogin.dd.csv test/analyse/TestTabs.sah test/analyse/TestNewMandant.sah test/analyse/TestFormHundehalter.sah
Data driven test:
You can set the start url, add tags and pass parameters.
If you want to run only tests with a specific tag, you can configure that at playback.


You can see all the test that you ran, filter, compare test and screenshots, see details.
You can also make custom reports.
configure sahi proxy https://sahipro.com/docs/using-sahi/playback-mobile.html
Use the safari debug tools.
Because the app uses a local file for the app the proxy is not used.
We will need to change the location to your webserver and run the tests.
change the location to your proxy ex.: location.href = “http://192.168.4.6:9999”;
Now you see the sahi start page where you can insert the test you want to run and the url of you webserver.
you can also use the command line to start the test.
$("filebox").value = "mobile/test3.sah"; $("starturl").value = "http://192.168.4.6:8080/Finanzanalyse.app/www"; launch(); OR _sahi.sendToServer("_s_/dyn/Player_setScriptFile?dir=C%3A%2FUsers%2Fmathias%2Fsahi_pro%2Fuserdata%2Fscripts%2F&file="+"mobile/test3.sah"+"&starturl="+"http://192.168.4.6:8080/Finanzanalyse.app/www"+"&manual=0"+"&browserType=Netscape"); _sahi.setServerVar("sahi_paused", "0");