1 module selenium.api; 2 3 import core.vararg; 4 import std.stdio; 5 6 import vibe.d; 7 8 import vibe.http.client; 9 import vibe.data.json; 10 import std.typecons; 11 12 //hack for development dub 13 alias Nint = Nullable!int; 14 Nint a; 15 16 /// https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/url 17 18 /** 19 * Aggregates all information about a model error status. 20 */ 21 class SeleniumException : Exception { 22 /** 23 * Create the exception 24 */ 25 this(string msg, string file = __FILE__, ulong line = cast(ulong)__LINE__, Throwable next = null) { 26 super(msg); 27 } 28 29 this(Json data, string file = __FILE__, ulong line = cast(ulong)__LINE__, Throwable next = null) { 30 super("Selenium server error: " ~ data["value"]["message"].to!string); 31 } 32 } 33 34 enum string[int] StatusDescription = [ 35 0 : "The command executed successfully.", 36 6 : "A session is either terminated or not started", 37 7 : "An element could not be located on the page using the given search parameters.", 38 8 : "A request to switch to a frame could not be satisfied because the frame could not be found.", 39 9 : "The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.", 40 10 : "An element command failed because the referenced element is no longer attached to the DOM.", 41 11 : "An element command could not be completed because the element is not visible on the page.", 42 12 : "An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element).", 43 13 : "An unknown server-side error occurred while processing the command.", 44 15 : "An attempt was made to select an element that cannot be selected.", 45 17 : "An error occurred while executing user supplied JavaScript.", 46 19 : "An error occurred while searching for an element by XPath.", 47 21 : "An operation did not complete before its timeout expired.", 48 23 : "A request to switch to a different window could not be satisfied because the window could not be found.", 49 24 : "An illegal attempt was made to set a cookie under a different domain than the current page.", 50 25 : "A request to set a cookie's value could not be satisfied.", 51 26 : "A modal dialog was open, blocking this operation", 52 27 : "An attempt was made to operate on a modal dialog when one was not open.", 53 28 : "A script did not complete before its timeout expired.", 54 29 : "The coordinates provided to an interactions operation are invalid.", 55 30 : "IME was not available.", 56 31 : "An IME engine could not be started.", 57 32 : "Argument was an invalid selector (e.g. XPath/CSS).", 58 33 : "A new session could not be created.", 59 34 : "Target provided for a move action is out of bounds." 60 ]; 61 62 enum LocatorStrategy : string { 63 ClassName = "class name", 64 CssSelector = "css selector", 65 Id = "id", 66 Name = "name", 67 LinkText = "link text", 68 PartialLinkText = "partial link text", 69 TagName = "tag name", 70 XPath = "xpath" 71 } 72 73 enum Browser: string { 74 android = "android", 75 chrome = "chrome", 76 firefox = "firefox", 77 htmlunit = "htmlunit", 78 internetExplorer = "internet explorer", 79 iPhone = "iPhone", 80 iPad = "iPad", 81 opera = "opera", 82 safari = "safari" 83 } 84 85 enum Platform: string { 86 windows = "WINDOWS", 87 xp = "XP", 88 vista = "VISTA", 89 mac = "MAC", 90 linux = "LINUX", 91 unix = "UNIX", 92 android = "ANDROID" 93 } 94 95 enum AlertBehaviour: string { 96 accept = "accept", 97 dismiss = "dismiss", 98 ignore = "ignore" 99 } 100 101 enum LogType: string { 102 client = "client", 103 driver = "driver", 104 browser = "browser", 105 server = "server" 106 } 107 108 enum TimeoutType: string { 109 script = "script", 110 implicit = "implicit", 111 pageLoad = "page load" 112 } 113 114 enum Orientation: string { 115 landscape = "LANDSCAPE", 116 portrait = "PORTRAIT" 117 } 118 119 enum MouseButton: int { 120 left = 0, 121 middle = 1, 122 right = 2 123 } 124 125 enum LogLevel: string { 126 ALL = "ALL", 127 DEBUG = "DEBUG", 128 INFO = "INFO", 129 WARNING = "WARNING", 130 SEVERE = "SEVERE", 131 OFF = "OFF" 132 } 133 134 enum CacheStatus { 135 uncached = 0, 136 idle = 1, 137 checking = 2, 138 downloading = 3, 139 update_ready = 4, 140 obsolete = 5 141 } 142 143 struct Capabilities { 144 @optional { 145 Browser browserName; 146 string browserVersion; 147 Platform platform; 148 149 bool takesScreenshot; 150 bool handlesAlerts; 151 bool cssSelectorsEnabled; 152 153 bool javascriptEnabled; 154 bool databaseEnabled; 155 bool locationContextEnabled; 156 bool applicationCacheEnabled; 157 bool browserConnectionEnabled; 158 bool webStorageEnabled; 159 bool acceptSslCerts; 160 bool rotatable; 161 bool nativeEvents; 162 //ProxyObject proxy; 163 AlertBehaviour unexpectedAlertBehaviour; 164 int elementScrollBehavior; 165 166 @name("webdriver.remote.sessionid") 167 string webdriver_remote_sessionid; 168 169 @name("webdriver.remote.quietExceptions") 170 bool webdriver_remote_quietExceptions; 171 } 172 173 static Capabilities chrome() { 174 auto capabilities = Capabilities(); 175 capabilities.browserName = Browser.chrome; 176 177 return capabilities; 178 } 179 } 180 181 struct SessionResponse(T) { 182 183 @optional { 184 string sessionId; 185 long hCode; 186 long status; 187 188 string state; 189 190 T value; 191 } 192 } 193 194 struct Size { 195 long width; 196 long height; 197 } 198 199 struct Position { 200 long x; 201 long y; 202 } 203 204 struct Cookie { 205 string name; 206 string value; 207 208 @optional { 209 string path; 210 string domain; 211 bool secure; 212 bool httpOnly; 213 long expiry; 214 } 215 } 216 217 struct ElementLocator { 218 LocatorStrategy using; 219 string value; 220 } 221 222 223 ElementLocator classLocator(string name) { 224 return ElementLocator(LocatorStrategy.ClassName, name); 225 } 226 227 ElementLocator cssLocator(string name) { 228 return ElementLocator(LocatorStrategy.CssSelector, name); 229 } 230 231 ElementLocator idLocator(string name) { 232 return ElementLocator(LocatorStrategy.Id, name); 233 } 234 235 ElementLocator nameLocator(string name) { 236 return ElementLocator(LocatorStrategy.Name, name); 237 } 238 239 ElementLocator linkTextLocator(string name) { 240 return ElementLocator(LocatorStrategy.LinkText, name); 241 } 242 243 ElementLocator partialLinkTextLocator(string name) { 244 return ElementLocator(LocatorStrategy.PartialLinkText, name); 245 } 246 247 ElementLocator tagLocator(string name) { 248 return ElementLocator(LocatorStrategy.TagName, name); 249 } 250 251 ElementLocator xpathLocator(string name) { 252 return ElementLocator(LocatorStrategy.XPath, name); 253 } 254 255 struct WebElement { 256 string ELEMENT; 257 } 258 259 struct GeoLocation(T) { 260 T latitude; 261 T longitude; 262 T altitude; 263 } 264 265 struct LogEntry { 266 long timestamp; 267 LogLevel level; 268 string message; 269 } 270 271 class SeleniumApiConnector { 272 const { 273 string serverUrl; 274 275 Capabilities desiredCapabilities; 276 Capabilities requiredCapabilities; 277 Capabilities session; 278 Capabilities responseSession; 279 } 280 281 immutable { 282 SeleniumApiConnection connection; 283 SeleniumApi api; 284 } 285 286 this(const string serverUrl, 287 const Capabilities desiredCapabilities, 288 const Capabilities requiredCapabilities = Capabilities(), 289 const Capabilities session = Capabilities()) { 290 291 this.serverUrl = serverUrl; 292 this.desiredCapabilities = desiredCapabilities; 293 this.requiredCapabilities = requiredCapabilities; 294 this.session = session; 295 296 responseSession = makeRequest(HTTPMethod.POST, serverUrl ~ "/session", ["desiredCapabilities": desiredCapabilities]) 297 .deserializeJson!(SessionResponse!Capabilities).value; 298 299 connection = new immutable SeleniumApiConnection(serverUrl, responseSession.webdriver_remote_sessionid.idup); 300 api = new immutable SeleniumApi(connection); 301 } 302 } 303 304 class SeleniumApiConnection { 305 immutable { 306 string sessionId; 307 string serverUrl; 308 } 309 310 this(immutable string serverUrl, string sessionId) immutable { 311 this.sessionId = sessionId; 312 this.serverUrl = serverUrl; 313 } 314 315 inout { 316 void disconnect() { 317 makeRequest(HTTPMethod.DELETE, 318 serverUrl ~ "/session/" ~ sessionId); 319 } 320 321 void DELETE(T)(string path, T values = null) { 322 makeRequest(HTTPMethod.DELETE, 323 serverUrl ~ "/session/" ~ sessionId ~ path, 324 values); 325 } 326 327 void DELETE(string path) { 328 makeRequest(HTTPMethod.DELETE, 329 serverUrl ~ "/session/" ~ sessionId ~ path); 330 } 331 332 void POST(T)(string path, T values) { 333 makeRequest(HTTPMethod.POST, 334 serverUrl ~ "/session/" ~ sessionId ~ path, 335 values); 336 } 337 338 void POST(string path) { 339 makeRequest(HTTPMethod.POST, 340 serverUrl ~ "/session/" ~ sessionId ~ path); 341 } 342 343 auto POST(U, T)(string path, T values) { 344 return makeRequest(HTTPMethod.POST, 345 serverUrl ~ "/session/" ~ sessionId ~ path, 346 values).deserializeJson!(SessionResponse!U).value; 347 } 348 349 auto POST(U)(string path) { 350 return makeRequest(HTTPMethod.POST, 351 serverUrl ~ "/session/" ~ sessionId ~ path) 352 .deserializeJson!(SessionResponse!U).value; 353 } 354 355 T GET(T)(string path) { 356 return makeRequest(HTTPMethod.GET, serverUrl ~ "/session/" ~ sessionId ~ path) 357 .deserializeJson!(SessionResponse!T).value; 358 } 359 } 360 } 361 362 class SeleniumApi { 363 const SeleniumApiConnection connection; 364 365 this(inout SeleniumApiConnection connection) inout { 366 this.connection = connection; 367 } 368 369 inout { 370 auto timeouts(TimeoutType type, long ms) { 371 connection.POST("/timeouts", ["type": Json(type), "ms": Json(ms)]); 372 return this; 373 } 374 375 auto timeoutsAsyncScript(long ms) { 376 connection.POST("/timeouts/async_script", ["ms": Json(ms)]); 377 return this; 378 } 379 380 auto timeoutsImplicitWait(long ms) { 381 connection.POST("/timeouts/implicit_wait", ["ms": Json(ms)]); 382 return this; 383 } 384 385 auto windowHandle() { 386 return connection.GET!string("/window_handle"); 387 } 388 389 auto windowHandles() { 390 return connection.GET!(string[])("/window_handles"); 391 } 392 393 auto url(string url) { 394 connection.POST("/url", ["url": Json(url)]); 395 return this; 396 } 397 398 auto url() { 399 return connection.GET!string("/url"); 400 } 401 402 auto forward() { 403 connection.POST("/forward"); 404 return this; 405 } 406 407 auto back() { 408 connection.POST("/back"); 409 return this; 410 } 411 412 auto refresh() { 413 connection.POST("/refresh"); 414 return this; 415 } 416 417 auto execute(T = string)(string script, Json args = Json.emptyArray) { 418 return connection.POST!T("/execute", [ "script": Json(script), "args": args ]); 419 } 420 421 auto executeAsync(T = string)(string script, Json args = Json.emptyArray) { 422 return connection.POST!T("/execute_async", [ "script": Json(script), "args": args ]); 423 } 424 425 auto screenshot() { 426 return connection.GET!string("/screenshot"); 427 } 428 429 auto imeAvailableEngines() { 430 return connection.GET!string("/ime/available_engines"); 431 } 432 433 auto imeActiveEngine() { 434 return connection.GET!string("/ime/active_engine"); 435 } 436 437 auto imeActivated() { 438 return connection.GET!bool("/ime/activated"); 439 } 440 441 auto imeDeactivate() { 442 connection.POST("/ime/deactivate"); 443 return this; 444 } 445 446 auto imeActivate(string engine) { 447 connection.POST("/ime/activate", ["engine": engine]); 448 return this; 449 } 450 451 auto frame(string id) { 452 connection.POST("/frame", ["id": id]); 453 return this; 454 } 455 456 auto frame(long id) { 457 connection.POST("/frame", ["id": id]); 458 return this; 459 } 460 461 auto frame(WebElement element) { 462 connection.POST("/frame", element); 463 return this; 464 } 465 466 auto frameParent() { 467 connection.POST("/frame/parent"); 468 return this; 469 } 470 471 auto selectWindow(string name) { 472 connection.POST("/window", ["name": name]); 473 return this; 474 } 475 476 auto windowClose() { 477 connection.DELETE("/window"); 478 return this; 479 } 480 481 auto windowSize(Size size) { 482 connection.POST("/window/size", size); 483 return this; 484 } 485 486 auto windowSize() { 487 return connection.GET!Size("/window/size"); 488 } 489 490 auto windowMaximize() { 491 connection.POST("/window/maximize"); 492 return this; 493 } 494 495 auto cookie() { 496 return connection.GET!(Cookie[])("/cookie"); 497 } 498 499 auto setCookie(Cookie cookie) { 500 struct Body { 501 Cookie cookie; 502 } 503 504 connection.POST("/cookie", Body(cookie)); 505 return this; 506 } 507 508 auto deleteAllCookies() { 509 connection.DELETE("/cookie"); 510 return this; 511 } 512 513 auto deleteCookie(string name) { 514 connection.DELETE("/cookie/" ~ name); 515 return this; 516 } 517 518 auto source() { 519 return connection.GET!string("/source"); 520 } 521 522 auto title() { 523 return connection.GET!string("/title"); 524 } 525 526 auto element(ElementLocator locator) { 527 return connection.POST!WebElement("/element", locator); 528 } 529 530 auto elements(ElementLocator locator) { 531 return connection.POST!(WebElement[])("/elements", locator); 532 } 533 534 auto activeElement() { 535 return connection.POST!WebElement("/element/active"); 536 } 537 538 auto elementFromElement(string initialElemId, ElementLocator locator) { 539 return connection.POST!WebElement("/element/" ~ initialElemId ~ "/element", locator); 540 } 541 542 auto elementsFromElement(string initialElemId, ElementLocator locator) { 543 return connection.POST!(WebElement[])("/element/" ~ initialElemId ~ "/elements", locator); 544 } 545 546 auto clickElement(string elementId) { 547 connection.POST("/element/" ~ elementId ~ "/click"); 548 return this; 549 } 550 551 auto submitElement(string elementId) { 552 connection.POST("/element/" ~ elementId ~ "/submit"); 553 return this; 554 } 555 556 auto elementText(string elementId) { 557 return connection.GET!string("/element/" ~ elementId ~ "/text"); 558 } 559 560 auto sendKeys(string elementId, string[] value) { 561 struct Body { 562 string[] value; 563 } 564 565 connection.POST("/element/" ~ elementId ~ "/value", Body(value)); 566 return this; 567 } 568 569 auto sendKeysToActiveElement(const(string[]) value) { 570 struct Body { 571 const(string[]) value; 572 } 573 574 connection.POST("/keys", Body(value)); 575 return this; 576 } 577 578 auto elementName(string elementId) { 579 return connection.GET!string("/element/" ~ elementId ~ "/name"); 580 } 581 582 auto clearElementValue(string elementId) { 583 connection.POST("/element/" ~ elementId ~ "/clear"); 584 return this; 585 } 586 587 auto elementSelected(string elementId) { 588 return connection.GET!bool("/element/" ~ elementId ~ "/selected"); 589 } 590 591 auto elementEnabled(string elementId) { 592 return connection.GET!bool("/element/" ~ elementId ~ "/enabled"); 593 } 594 595 auto elementValue(string elementId, string attribute) { 596 return connection.GET!string("/element/" ~ elementId ~ "/attribute/" ~ attribute); 597 } 598 599 auto elementEqualsOther(string firstElementId, string secondElementId) { 600 return connection.GET!bool("/element/" ~ firstElementId ~ "/equals/" ~ secondElementId); 601 } 602 603 auto elementDisplayed(string elementId) { 604 return connection.GET!bool("/element/" ~ elementId ~ "/displayed"); 605 } 606 607 auto elementLocation(string elementId) { 608 return connection.GET!Position("/element/" ~ elementId ~ "/location"); 609 } 610 611 auto elementLocationInView(string elementId) { 612 return connection.GET!Position("/element/" ~ elementId ~ "/location_in_view"); 613 } 614 615 auto elementSize(string elementId) { 616 return connection.GET!Size("/element/" ~ elementId ~ "/size"); 617 } 618 619 auto elementCssPropertyName(string elementId, string propertyName) { 620 return connection.GET!string("/element/" ~ elementId ~ "/css/" ~ propertyName); 621 } 622 623 auto orientation() { 624 return connection.GET!Orientation("/orientation"); 625 } 626 627 auto setOrientation(Orientation orientation) { 628 struct Body { 629 Orientation orientation; 630 } 631 632 return connection.POST("/orientation", Body(orientation)); 633 } 634 635 auto alertText() { 636 return connection.GET!string("/alert_text"); 637 } 638 639 auto setPromptText(string text) { 640 connection.POST("/alert_text", ["text": text]); 641 return this; 642 } 643 644 auto acceptAlert() { 645 connection.POST("/accept_alert"); 646 return this; 647 } 648 649 auto dismissAlert() { 650 connection.POST("/dismiss_alert"); 651 return this; 652 } 653 654 auto moveTo(Position position) { 655 connection.POST("/moveto", ["xoffset": position.x, "yoffset": position.y]); 656 return this; 657 } 658 659 auto moveTo(string elementId) { 660 connection.POST("/moveto", ["element": elementId]); 661 return this; 662 } 663 664 auto moveTo(string elementId, Position position) { 665 struct Body { 666 string element; 667 long xoffset; 668 long yoffset; 669 } 670 671 connection.POST("/moveto", Body(elementId, position.x, position.y)); 672 return this; 673 } 674 675 auto click(MouseButton button = MouseButton.left) { 676 connection.POST("/click", ["button": button]); 677 return this; 678 } 679 680 auto buttonDown(MouseButton button = MouseButton.left) { 681 connection.POST("/buttondown", ["button": button]); 682 return this; 683 } 684 685 auto buttonUp(MouseButton button = MouseButton.left) { 686 connection.POST("/buttonup", ["button": button]); 687 return this; 688 } 689 690 auto doubleClick() { 691 connection.POST("/doubleclick"); 692 return this; 693 } 694 695 auto touchClick(string elementId) { 696 connection.POST("/touch/click", ["element": elementId]); 697 return this; 698 } 699 700 auto touchDown(Position position) { 701 connection.POST("/touch/down", ["x": position.x, "y": position.y]); 702 return this; 703 } 704 705 auto touchUp(Position position) { 706 connection.POST("/touch/up", ["x": position.x, "y": position.y]); 707 return this; 708 } 709 710 auto touchMove(Position position) { 711 connection.POST("/touch/move", ["x": position.x, "y": position.y]); 712 return this; 713 } 714 715 auto touchScroll(string elementId, Position position) { 716 struct Body { 717 string element; 718 long xoffset; 719 long yoffset; 720 } 721 722 connection.POST("/touch/scroll", Body(elementId, position.x, position.y)); 723 return this; 724 } 725 726 auto touchScroll(Position position) { 727 connection.POST("/touch/scroll", ["xoffset": position.x, "yoffset": position.y]); 728 return this; 729 } 730 731 auto touchDoubleClick(string elementId) { 732 connection.POST("/touch/doubleclick", ["element": elementId]); 733 return this; 734 } 735 736 auto touchLongClick(string elementId) { 737 connection.POST("/touch/longclick", ["element": elementId]); 738 return this; 739 } 740 741 auto touchFlick(string elementId, Position position, long speed) { 742 struct Body { 743 string element; 744 long xoffset; 745 long yoffset; 746 long speed; 747 } 748 749 connection.POST("/touch/flick", Body(elementId, position.x, position.y, speed)); 750 return this; 751 } 752 753 auto touchFlick(long xSpeed, long ySpeed) { 754 connection.POST("/touch/flick", [ "xspeed": xSpeed, "yspeed": ySpeed ]); 755 return this; 756 } 757 758 auto geoLocation() { 759 return connection.GET!(GeoLocation!double)("/location"); 760 } 761 762 auto setGeoLocation(T)(T location) { 763 connection.POST("/location", ["location": location]); 764 return this; 765 } 766 767 auto localStorage() { 768 return connection.GET!(string[])("/local_storage"); 769 } 770 771 auto setLocalStorage(string key, string value) { 772 connection.POST("/local_storage", ["key": key, "value": value]); 773 return this; 774 } 775 776 auto deleteLocalStorage() { 777 connection.DELETE("/local_storage"); 778 return this; 779 } 780 781 auto localStorage(string key) { 782 return connection.GET!(string)("/local_storage/key/" ~ key); 783 } 784 785 auto deleteLocalStorage(string key) { 786 connection.DELETE("/local_storage/key/" ~ key); 787 return this; 788 } 789 790 auto localStorageSize() { 791 return connection.GET!(long)("/local_storage/size"); 792 } 793 794 auto sessionStorage() { 795 return connection.GET!(string[])("/session_storage"); 796 } 797 798 auto setSessionStorage(string key, string value) { 799 connection.POST("/session_storage", ["key": key, "value": value]); 800 return this; 801 } 802 803 auto deleteSessionStorage() { 804 connection.DELETE("/session_storage"); 805 return this; 806 } 807 808 auto sessionStorage(string key) { 809 return connection.GET!(string)("/session_storage/key/" ~ key); 810 } 811 812 auto deleteSessionStorage(string key) { 813 connection.DELETE("/session_storage/key/" ~ key); 814 return this; 815 } 816 817 auto sessionStorageSize() { 818 return connection.GET!(long)("/session_storage/size"); 819 } 820 821 auto log(LogType logType) { 822 return connection.POST!(LogEntry[])("/log", ["type": logType]); 823 } 824 825 auto logTypes() { 826 return connection.GET!(string[])("/log/types"); 827 } 828 829 auto applicationCacheStatus() { 830 return connection.GET!CacheStatus("/application_cache/status"); 831 } 832 833 /* 834 /session/:sessionId/element/:id - not yet implemented in Selenium 835 836 /session/:sessionId/log/types 837 /session/:sessionId/application_cache/status*/ 838 839 840 auto wait(long ms) { 841 sleep(ms.msecs); 842 return this; 843 } 844 } 845 } 846 847 private Json makeRequest(T)(HTTPMethod method, string path, T data) { 848 import vibe.core.core : sleep; 849 import core.time : msecs; 850 import std.conv : to; 851 852 Json result; 853 bool done = false; 854 855 requestHTTP(path, 856 (scope req) { 857 req.method = method; 858 req.writeJsonBody(data); 859 }, 860 (scope res) { 861 result = res.readJson; 862 863 if(res.statusCode == 500) { 864 throw new SeleniumException(result); 865 } 866 done = true; 867 } 868 ); 869 870 return result; 871 } 872 873 874 private Json makeRequest(HTTPMethod method, string path) { 875 import vibe.core.core : sleep; 876 import core.time : msecs; 877 import std.conv : to; 878 879 Json result; 880 bool done = false; 881 882 requestHTTP(path, 883 (scope req) { 884 req.method = method; 885 }, 886 (scope res) { 887 result = res.readJson; 888 889 if(res.statusCode == 500) { 890 throw new SeleniumException(result); 891 } 892 done = true; 893 } 894 ); 895 return result; 896 }