Sahi
Introduction
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
Features
- Recorder and object spy which works on Internet Explorer, Firefox, Chrome, Safari, Opera (and any modern browser).
- Playback on any desktop browser and even on mobile browsers.
- Robust object identification mechanism which works across browsers.
- Sahi automatically waits for page loads and ajax activity. There is no need for adding wait statements in 95% cases.
- Sahi does not need the browser to be in focus.
- Sahi can playback multiple scripts simultaneously reducing playback time.
- Sahi automatically builds rich reports without adding any extra code.
- …
Architecture
The injected javascript
- adds event handlers for recording
- adds Sahi's APIs for event simulation
- mocks some window dialogs like alert, prompt, confirm, print so that playback is not affected
Sahi script
Example:
- sample.sah
_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:
- sample.sah
_include("library.sah"); login("test", "secret"); addBooks(2, 1, 1); verifyTotal(1150); _click(_button("Logout"));
- library.sah
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
Using
Install
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
Dashboard
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.
Recording
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
Editor
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
API
Accessor API Basics
| _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 |
Accessor APIs
Form Input Elements
- _password
- _textbox
- _hidden
- _datebox
- _datetimebox
- _datetimelocalbox
- _emailbox
- _monthbox
- _numberbox
- _rangebox
- _searchbox
- _telephonebox
- _timebox
- _urlbox
- _weekbox
- _textarea
Action APIs
Mouse events
- _xy
- _click
- _doubleClick
- _rightClick
- _mouseDown
- _mouseUp
- _mouseOver
- _check
- _uncheck
- _setSelected
- _dragDrop
- _dragDropXY
Touch events
- _tap
- _touch
- _touchStart
- _touchEnd
- _touchCancel
- _touchMove
- _swipe
Keyboard events
- _setValue
- _keyDown
- _keyUp
- _keyPress
- _type
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
Script
PageObject model
- LoginPage.sah
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")); } }
- login_lib.sah
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();
- analysePage.sah
_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); }
- createMandant.sah
_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"));
- testKunde.sah
_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
- testsuite.suite
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.
Reports


You can see all the test that you ran, filter, compare test and screenshots, see details.
You can also make custom reports.
Mobile
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");
