├── telenium ├── mods │ ├── __init__.py │ └── telenium_client.py ├── static │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── css │ │ ├── app.css │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.css │ ├── js │ │ ├── reconnecting-websocket.min.js │ │ ├── mustache.min.js │ │ ├── ui.js │ │ ├── jquery-sortable.js │ │ └── bootstrap.min.js │ └── index.html ├── __init__.py ├── execute.py ├── client.py ├── tests.py ├── xpath.py └── web.py ├── MANIFEST.in ├── .gitignore ├── Makefile ├── LICENSE ├── setup.py └── README.md /telenium/mods/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.md 4 | 5 | recursive-include telenium/static * 6 | -------------------------------------------------------------------------------- /telenium/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/telenium/HEAD/telenium/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /telenium/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/telenium/HEAD/telenium/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /telenium/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/telenium/HEAD/telenium/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /telenium/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/telenium/HEAD/telenium/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .noseids 3 | *.so 4 | *.pyc 5 | *.pyo 6 | *.py# 7 | *~ 8 | *.swp 9 | *.DS_Store 10 | *.kpf 11 | *.egg-info 12 | session.dat 13 | build 14 | dist 15 | .vscode/settings.json 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: release 2 | 3 | 4 | check-release: 5 | rm -rf dist build *.egg-info 6 | python setup.py sdist 7 | python setup.py bdist_wheel 8 | twine check dist/* 9 | 10 | release: 11 | rm -rf dist build *.egg-info 12 | python setup.py sdist 13 | python setup.py bdist_wheel 14 | twine upload dist/* 15 | -------------------------------------------------------------------------------- /telenium/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | __version__ = "0.5.2.dev0" 4 | 5 | 6 | def install(): 7 | """Install the kivy telenium module into the kivy instance 8 | """ 9 | from .mods.telenium_client import install as mod_install 10 | return mod_install() 11 | 12 | 13 | def connect(host="localhost", port=9901, timeout=5): 14 | from .client import TeleniumHttpClient 15 | """Connect to a remote telenium kivy module 16 | """ 17 | return TeleniumHttpClient(f"http://{host}:{port}/jsonrpc", timeout=timeout) 18 | -------------------------------------------------------------------------------- /telenium/execute.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | def run_executable(executable_name): 4 | # insert the telenium module path 5 | # we do the import here to be able to load kivy args 6 | from kivy.modules import Modules 7 | from kivy.config import Config 8 | from os.path import dirname, join 9 | import runpy 10 | 11 | Modules.add_path(join(dirname(__file__), "mods")) 12 | Config.set("modules", "telenium_client", "") 13 | runpy.run_path(executable_name, run_name="__main__") 14 | 15 | 16 | if __name__ == "__main__": 17 | import sys 18 | executable_name = sys.argv[1] 19 | sys.argv = sys.argv[1:] # pop the first one 20 | run_executable(executable_name) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Mathieu Virbel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /telenium/static/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | @keyframes clockwise { 3 | to { 4 | transform: rotate(360deg) translatez(0); 5 | } 6 | } 7 | @keyframes counter-clockwise { 8 | to { 9 | transform: rotate(-360deg) translatez(0); 10 | } 11 | } 12 | 13 | body { 14 | padding-top: 50px; 15 | } 16 | 17 | button.loading div.hidden { 18 | display: block !important; 19 | } 20 | 21 | button.loading span { 22 | display: none; 23 | } 24 | 25 | .toolbar-right { 26 | margin-top: -60px; 27 | float: right; 28 | } 29 | 30 | 31 | .progress-spinner { 32 | height: 32px; 33 | width: 32px; 34 | border-width: 3px; 35 | border-style: solid; 36 | border-color: rgba(112, 112, 112, 0.75) rgba(36, 36, 36, 0.25) rgba(36, 36, 36, 0.25) rgba(36, 36, 36, 0.25); 37 | border-radius: 100%; 38 | animation: clockwise 0.75s linear infinite; 39 | } 40 | 41 | .progress-spinner-tiny { 42 | width: 18px; 43 | height: 18px; 44 | } 45 | 46 | .test-status { 47 | font-size: 24px; 48 | } 49 | 50 | .test-status-success { 51 | color: #5cb85c; 52 | } 53 | .test-status-error { 54 | color: #d9534f; 55 | } 56 | 57 | #modal-export .modal-dialog { 58 | max-width: 90%; 59 | width: 1024px; 60 | } 61 | #modal-export pre { 62 | max-height: 500px; 63 | } 64 | 65 | #tl-tests input.step-selector, 66 | #tl-tests input.step-arg { 67 | font-family: monospace; 68 | } 69 | 70 | .dragged { 71 | position: absolute; 72 | top: 0; 73 | opacity: 0.5; 74 | z-index: 2000; } 75 | .sorted-table tr { 76 | cursor: pointer; } 77 | /* line 96, /Users/jonasvonandrian/jquery-sortable/source/css/application.css.sass */ 78 | .sorted-table tr.placeholder { 79 | display: block; 80 | background: red; 81 | position: relative; 82 | margin: 0; 83 | padding: 0; 84 | border: none; } 85 | /* line 103, /Users/jonasvonandrian/jquery-sortable/source/css/application.css.sass */ 86 | .sorted-table tr.placeholder:before { 87 | content: ""; 88 | position: absolute; 89 | width: 0; 90 | height: 0; 91 | border: 5px solid transparent; 92 | border-left-color: red; 93 | margin-top: -5px; 94 | left: -5px; 95 | border-right: none; } 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Telenium: automation for Kivy application 3 | """ 4 | 5 | import re 6 | import codecs 7 | import os 8 | try: 9 | from ez_setup import use_setuptools 10 | use_setuptools() 11 | except: 12 | pass 13 | from setuptools import setup 14 | 15 | here = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | try: 18 | import pypandoc 19 | long_description = pypandoc.convert("README.md", "rst") 20 | except ImportError: 21 | long_description = open("README.md").read() 22 | 23 | 24 | def find_version(*file_paths): 25 | with codecs.open(os.path.join(here, *file_paths), "r", "latin1") as f: 26 | version_file = f.read() 27 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 28 | version_file, re.M) 29 | if version_match: 30 | return version_match.group(1) 31 | raise RuntimeError("Unable to find version string.") 32 | 33 | 34 | setup( 35 | name="telenium", 36 | version=find_version("telenium", "__init__.py"), 37 | url="http://github.com/tito/telenium", 38 | license="MIT License", 39 | author="Mathieu Virbel", 40 | author_email="mat@meltingrocks.com", 41 | description=("Kivy automation, can be used to do GUI tests."), 42 | long_description=long_description, 43 | long_description_content_type="text/markdown", 44 | keywords=["kivy", "automate", "unittest", "wait", "condition"], 45 | packages=["telenium", "telenium.mods"], 46 | entry_points={ 47 | "console_scripts": [ 48 | "telenium=telenium.web:run", 49 | "telenium-cli=telenium.client:run" 50 | ] 51 | }, 52 | install_requires=[ 53 | "Mako>=1.0.6", 54 | "CherryPy>=10.2.1", 55 | "ws4py>=0.4.2", 56 | "json-rpc>=1.10.3", 57 | "Werkzeug>=0.12.2" 58 | ], 59 | include_package_data=True, 60 | zip_safe=False, 61 | platforms="any", 62 | classifiers=[ 63 | "Development Status :: 4 - Beta", "Intended Audience :: Developers", 64 | "Intended Audience :: End Users/Desktop", 65 | "Intended Audience :: Information Technology", 66 | "Intended Audience :: Science/Research", 67 | "Intended Audience :: System Administrators", 68 | "License :: OSI Approved :: MIT License", "Operating System :: POSIX", 69 | "Operating System :: MacOS", "Operating System :: Unix", 70 | "Programming Language :: Python", 71 | "Programming Language :: Python :: 2", 72 | "Topic :: Software Development :: Libraries :: Python Modules", 73 | "Topic :: Software Development :: Build Tools", "Topic :: Internet", 74 | "Topic :: System :: Systems Administration", 75 | "Topic :: System :: Monitoring" 76 | ]) 77 | -------------------------------------------------------------------------------- /telenium/static/js/reconnecting-websocket.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); 2 | -------------------------------------------------------------------------------- /telenium/client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import argparse 4 | import requests 5 | import json 6 | from time import time, sleep 7 | 8 | 9 | class TeleniumHttpException(Exception): 10 | pass 11 | 12 | 13 | class TeleniumHttpClientMethod(object): 14 | _id = 0 15 | def __init__(self, client, method): 16 | self.client = client 17 | self.method = method 18 | super(TeleniumHttpClientMethod, self).__init__() 19 | 20 | def __call__(self, *args): 21 | TeleniumHttpClientMethod._id += 1 22 | _id = TeleniumHttpClientMethod._id 23 | payload = { 24 | "method": self.method, 25 | "params": args, 26 | "jsonrpc": "2.0", 27 | "id": _id 28 | } 29 | headers = {"Content-Type": "application/json"} 30 | response = None 31 | response_json = None 32 | 33 | print(f"> {self.method}: {args}") 34 | 35 | try: 36 | response = requests.post( 37 | self.client.url, data=json.dumps(payload), 38 | headers=headers) 39 | 40 | if response.headers.get('content-type') == 'application/json': 41 | response_json = response.json() 42 | 43 | except requests.exceptions.ConnectionError: 44 | # Closing the app, unsurprisingly, often causes a disconnection 45 | if self.method != 'app_quit': 46 | raise 47 | 48 | except: 49 | if response and response.headers.get( 50 | 'content-type') == 'application/json': 51 | raise TeleniumHttpException( 52 | response.json()["error"]["message"]) 53 | raise 54 | 55 | # The JSON response doesn't always contain a "result" value. 56 | return response_json.get('result', None) if response_json else None 57 | 58 | 59 | class TeleniumHttpClient(object): 60 | def __init__(self, url, timeout): 61 | self.url = url 62 | self.timeout = timeout 63 | super(TeleniumHttpClient, self).__init__() 64 | 65 | def wait(self, selector, timeout=-1): 66 | start = time() 67 | while True: 68 | matches = self.select(selector) 69 | if matches: 70 | return True 71 | if timeout == -1: 72 | return False 73 | if timeout > 0 and time() - start > timeout: 74 | raise Exception("Timeout") 75 | sleep(0.1) 76 | 77 | def wait_click(self, selector, timeout=-1): 78 | if self.wait(selector, timeout=timeout): 79 | self.click_on(selector) 80 | 81 | def wait_drag(self, selector, target, duration, timeout): 82 | if ( 83 | self.wait(selector, timeout=timeout) and 84 | self.wait(target, timeout=timeout) 85 | ): 86 | self.drag(selector, target, duration) 87 | 88 | def screenshot(self, filename=None): 89 | import base64 90 | ret = TeleniumHttpClientMethod(self, "screenshot")() 91 | if ret: 92 | ret["data"] = base64.b64decode(ret["data"].encode("utf-8")) 93 | if filename: 94 | ret["filename"] = filename 95 | with open(filename, "wb") as fd: 96 | fd.write(ret["data"]) 97 | return ret 98 | 99 | def execute(self, code): 100 | from textwrap import dedent 101 | return TeleniumHttpClientMethod(self, "execute")(dedent(code)) 102 | 103 | def sleep(self, seconds): 104 | print(f"> sleep {seconds} seconds") 105 | sleep(seconds) 106 | 107 | def __getattr__(self, attr): 108 | return TeleniumHttpClientMethod(self, attr) 109 | 110 | 111 | def run_client(host="localhost", port=9901): 112 | import code 113 | import readline 114 | import rlcompleter 115 | url = "http://{host}:{port}/jsonrpc".format(host=host, port=port) 116 | cli = TeleniumHttpClient(url=url, timeout=5) 117 | 118 | print("Connecting to {}".format(url)) 119 | while not cli.ping(): 120 | sleep(.1) 121 | print("Connected!") 122 | 123 | vars = globals() 124 | vars.update(locals()) 125 | readline.set_completer(rlcompleter.Completer(vars).complete) 126 | readline.parse_and_bind("tab: complete") 127 | shell = code.InteractiveConsole(vars) 128 | shell.interact() 129 | 130 | 131 | def run(): 132 | parser = argparse.ArgumentParser(description="Telenium Client") 133 | parser.add_argument( 134 | "host", type=str, default="localhost", help="Telenium Host IP") 135 | parser.add_argument( 136 | "--port", type=int, default=9901, help="Telenium Host Port") 137 | args = parser.parse_args() 138 | run_client(host=args.host, port=args.port) 139 | 140 | 141 | if __name__ == "__main__": 142 | run() 143 | -------------------------------------------------------------------------------- /telenium/tests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | 4 | import unittest 5 | import subprocess 6 | import os 7 | from telenium.client import TeleniumHttpClient 8 | from time import time, sleep 9 | from uuid import uuid4 10 | 11 | 12 | class TeleniumTestCase(unittest.TestCase): 13 | """Telenium unittest TestCase, that can be run with any runner like pytest. 14 | """ 15 | 16 | #: Telenium url to connect to 17 | telenium_url = "http://localhost:9901/jsonrpc" 18 | 19 | #: Timeout of the process to start, in seconds 20 | process_start_timeout = 5 21 | 22 | #: Environment variables that can be passed to the process 23 | cmd_env = {} 24 | 25 | #: Entrypoint of the process 26 | cmd_entrypoint = ["main.py"] 27 | 28 | #: Command to start the process (cmd_entrypoint is appended to this) 29 | cmd_process = ["python", "-m", "telenium.execute"] 30 | 31 | _telenium_init = False 32 | 33 | @classmethod 34 | def start_process(cls): 35 | host = os.environ.get("TELENIUM_HOST", "localhost") 36 | if "TELENIUM_HOST" in os.environ: 37 | url = "http://{}:{}/jsonrpc".format( 38 | os.environ.get("TELENIUM_HOST", "localhost"), 39 | int(os.environ.get("TELENIUM_PORT", "9901"))) 40 | else: 41 | url = cls.telenium_url 42 | 43 | cls.telenium_token = str(uuid4()) 44 | cls.cli = TeleniumHttpClient(url=url, timeout=5) 45 | 46 | # prior test, close any possible previous telenium application 47 | # to ensure this one might be executed correctly. 48 | try: 49 | cls.cli.app_quit() 50 | sleep(2) 51 | except: 52 | pass 53 | 54 | # prepare the environment of the application to start 55 | env = os.environ.copy() 56 | env["TELENIUM_TOKEN"] = cls.telenium_token 57 | for key, value in cls.cmd_env.items(): 58 | env[key] = str(value) 59 | cmd = cls.cmd_process + cls.cmd_entrypoint 60 | 61 | # start the application 62 | if os.environ.get("TELENIUM_TARGET", None) == "android": 63 | cls.start_android_process(env=env) 64 | else: 65 | cls.start_desktop_process(cmd=cmd, env=env) 66 | 67 | # wait for telenium server to be online 68 | start = time() 69 | while True: 70 | try: 71 | cls.cli.ping() 72 | break 73 | except Exception: 74 | if time() - start > cls.process_start_timeout: 75 | raise Exception("timeout") 76 | sleep(1) 77 | 78 | # ensure the telenium we are connected are the same as the one we 79 | # launched here 80 | if cls.cli.get_token() != cls.telenium_token: 81 | raise Exception("Connected to another telenium server") 82 | 83 | @classmethod 84 | def start_desktop_process(cls, cmd, env): 85 | cwd = os.path.dirname(os.path.abspath(cls.cmd_entrypoint[0])) 86 | cls.process = subprocess.Popen(cmd, env=env, cwd=cwd) 87 | 88 | @classmethod 89 | def start_android_process(cls, env): 90 | import subprocess 91 | import json 92 | package = os.environ.get("TELENIUM_ANDROID_PACKAGE", None) 93 | entry = os.environ.get("TELENIUM_ANDROID_ENTRY", 94 | "org.kivy.android.PythonActivity") 95 | telenium_env = cls.cmd_env.copy() 96 | telenium_env["TELENIUM_TOKEN"] = env["TELENIUM_TOKEN"] 97 | cmd = [ 98 | "adb", "shell", "am", "start", "-n", 99 | "{}/{}".format(package, entry), "-a", entry 100 | ] 101 | 102 | filename = "/tmp/telenium_env.json" 103 | with open(filename, "w") as fd: 104 | fd.write(json.dumps(telenium_env)) 105 | cmd_env = ["adb", "push", filename, "/sdcard/telenium_env.json"] 106 | print("Execute: {}".format(cmd_env)) 107 | subprocess.Popen(cmd_env).communicate() 108 | print("Execute: {}".format(cmd)) 109 | cls.process = subprocess.Popen(cmd) 110 | print(cls.process.communicate()) 111 | 112 | @classmethod 113 | def stop_process(cls): 114 | cls.cli.app_quit() 115 | cls.process.wait() 116 | 117 | @classmethod 118 | def setUpClass(cls): 119 | TeleniumTestCase._telenium_init = False 120 | cls.start_process() 121 | super(TeleniumTestCase, cls).setUpClass() 122 | 123 | @classmethod 124 | def tearDownClass(cls): 125 | TeleniumTestCase._telenium_init = False 126 | cls.stop_process() 127 | super(TeleniumTestCase, cls).tearDownClass() 128 | 129 | def setUp(self): 130 | if not TeleniumTestCase._telenium_init: 131 | if hasattr(self, "init"): 132 | self.init() 133 | TeleniumTestCase._telenium_init = True 134 | 135 | def assertExists(self, selector, timeout=-1): 136 | self.assertTrue(self.cli.wait(selector, timeout=timeout)) 137 | 138 | def assertNotExists(self, selector, timeout=-1): 139 | start = time() 140 | while True: 141 | matches = self.cli.select(selector) 142 | if not matches: 143 | return True 144 | if timeout == -1: 145 | raise AssertionError("selector matched elements") 146 | if timeout > 0 and time() - start > timeout: 147 | raise Exception("Timeout") 148 | sleep(0.1) 149 | -------------------------------------------------------------------------------- /telenium/xpath.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import re 4 | import json 5 | 6 | 7 | class Selector(object): 8 | def __init__(self, **kwargs): 9 | super(Selector, self).__init__() 10 | for key, value in kwargs.items(): 11 | setattr(self, key, value) 12 | 13 | def traverse_tree(self, root): 14 | if not root: 15 | return 16 | yield root 17 | for child in root.children: 18 | for result in self.traverse_tree(child): 19 | yield result 20 | 21 | def match_class(self, widget, classname): 22 | if not classname.startswith("~"): 23 | return widget.__class__.__name__ == classname 24 | bases = [widget.__class__] + list(self.get_bases(widget.__class__)) 25 | bases = [cls.__name__ for cls in bases] 26 | return classname[1:] in bases 27 | 28 | def get_bases(self, cls): 29 | for base in cls.__bases__: 30 | if base.__name__ == 'object': 31 | break 32 | yield base 33 | if base.__name__ == 'Widget': 34 | break 35 | for cbase in self.get_bases(base): 36 | yield cbase 37 | 38 | def execute(self, root): 39 | return list(self.filter(root, [root])) 40 | 41 | def __add__(self, other): 42 | return SequenceSelector(first=self, second=other) 43 | 44 | 45 | class SequenceSelector(Selector): 46 | first = None 47 | second = None 48 | 49 | def filter(self, root, items): 50 | items = self.first.filter(root, items) 51 | items = self.second.filter(root, items) 52 | return items 53 | 54 | def __repr__(self): 55 | return "Sequence({}, {})".format(self.first, self.second) 56 | 57 | 58 | class AllClassSelector(Selector): 59 | classname = None 60 | 61 | def filter(self, root, items): 62 | if not items: 63 | items = [self.root] 64 | for item in items: 65 | for match_item in self.traverse_tree(item): 66 | if self.match_class(match_item, self.classname): 67 | yield match_item 68 | 69 | def __repr__(self): 70 | return "AllClass(classname={})".format(self.classname) 71 | 72 | 73 | class ChildrenClassSelector(Selector): 74 | classname = None 75 | 76 | def filter(self, root, items): 77 | items = list(items) 78 | for item in items: 79 | for child in item.children: 80 | if self.match_class(child, self.classname): 81 | yield child 82 | 83 | def __repr__(self): 84 | return "ChildrenClass(classname={})".format(self.classname) 85 | 86 | 87 | class IndexSelector(Selector): 88 | index = None 89 | 90 | def filter(self, root, items): 91 | try: 92 | for index, item in enumerate(reversed(list(items))): 93 | if index == self.index: 94 | yield item 95 | return 96 | except IndexError: 97 | return 98 | 99 | def __repr__(self): 100 | return "Index({})".format(self.index) 101 | 102 | 103 | class AttrExistSelector(Selector): 104 | attr = None 105 | 106 | def filter(self, root, items): 107 | for item in items: 108 | if hasattr(item, self.attr): 109 | yield item 110 | 111 | def __repr__(self): 112 | return "AttrExists({})".format(self.attr) 113 | 114 | 115 | class AttrOpSelector(Selector): 116 | attr = None 117 | op = None 118 | value = None 119 | 120 | def filter(self, root, items): 121 | op = self.op 122 | attr = self.attr 123 | value = json.loads(self.value) 124 | for item in items: 125 | if not hasattr(item, attr): 126 | continue 127 | value_item = getattr(item, attr) 128 | if op == "=" and value_item == value: 129 | yield item 130 | elif op == "!=" and value_item != value: 131 | yield item 132 | elif op == "~=" and value in value_item: 133 | yield item 134 | elif op == "!~=" and value not in value_item: 135 | yield item 136 | 137 | def __repr__(self): 138 | return "AttrOp(attr={}, op={}, value={})".format(self.attr, self.op, 139 | self.value) 140 | 141 | 142 | class XpathParser(object): 143 | WORD = re.compile("^([~\w]+)") 144 | 145 | def parse(self, expr): 146 | root = None 147 | while expr: 148 | selector = None 149 | if expr.startswith("//"): 150 | expr = expr[2:] 151 | match = re.match(self.WORD, expr) 152 | if not match: 153 | raise Exception("Missing classname for //") 154 | classname = expr[match.start():match.end()] 155 | expr = expr[match.end():] 156 | selector = AllClassSelector(classname=classname) 157 | 158 | elif expr.startswith("/"): 159 | expr = expr[1:] 160 | match = re.match(self.WORD, expr) 161 | if not match: 162 | raise Exception("Missing classname for /") 163 | classname = expr[match.start():match.end()] 164 | expr = expr[match.end():] 165 | selector = ChildrenClassSelector(classname=classname) 166 | 167 | elif expr.startswith("["): 168 | index_nbr = expr.index("]") 169 | try: 170 | # index ? 171 | index = int(expr[1:index_nbr]) 172 | selector = IndexSelector(index=index) 173 | except: 174 | for item in expr[1:index_nbr].split(","): 175 | item_selector = self.parse_attr(item) 176 | if selector: 177 | selector = SequenceSelector(first=selector, 178 | second=item_selector) 179 | else: 180 | selector = item_selector 181 | expr = expr[index_nbr + 1:] 182 | 183 | else: 184 | raise Exception("Left over during parsing: {}".format(expr)) 185 | 186 | if selector: 187 | if not root: 188 | root = selector 189 | else: 190 | root = SequenceSelector(first=root, second=selector) 191 | 192 | return root 193 | 194 | def parse_attr(self, expr): 195 | if expr.startswith("@"): 196 | return self.parse_attr_op(expr) 197 | else: 198 | #return self.parse_attr_func(expr) 199 | # why not ? 200 | raise Exception("Invalid syntax at {}".format(expr)) 201 | 202 | def parse_attr_op(self, expr): 203 | info = re.split(r"(=|!=|~=|!~=)", expr, 1) 204 | attr = info[0][1:] 205 | if len(info) == 1: 206 | return AttrExistSelector(attr=attr) 207 | elif len(info) == 3: 208 | return AttrOpSelector(attr=attr, op=info[1], value=info[2]) 209 | else: 210 | raise Exception("Invalid syntax in {}".format(expr)) 211 | 212 | 213 | if __name__ == "__main__": 214 | from kivy.lang import Builder 215 | root = Builder.load_string(""" 216 | BoxLayout: 217 | TextInput 218 | BoxLayout: 219 | AnchorLayout: 220 | TextInput 221 | TextInput 222 | Button: 223 | text: "Hello" 224 | Button: 225 | text: "World" 226 | """) 227 | parser = XpathParser() 228 | print(parser.parse("//BoxLayout/TextInput").execute(root)) 229 | print(parser.parse("//TextInput").execute(root)) 230 | p = parser.parse("//BoxLayout/Button[@text=\"World\"]") 231 | print(p) 232 | print(p.execute(root)) 233 | -------------------------------------------------------------------------------- /telenium/static/js/mustache.min.js: -------------------------------------------------------------------------------- 1 | (function defineMustache(global,factory){if(typeof exports==="object"&&exports&&typeof exports.nodeName!=="string"){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{global.Mustache={};factory(global.Mustache)}})(this,function mustacheFactory(mustache){var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){value=context.view;names=name.split(".");index=0;while(value!=null&&index")value=this.renderPartial(token,context,partials,originalTemplate);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j>> id = cli.pick() # then click somewhere on the UI 115 | >>> cli.click_at(id) 116 | True 117 | >>> cli.setattr("//Label", "color", (0, 1, 0, 1)) 118 | True 119 | ``` 120 | 121 | If a command returns True, it means it has been successful, otherwise it 122 | returns None. 123 | 124 | # Create unit tests 125 | 126 | Telenium have a module you can use that ease unit tests: it launch the app 127 | and execute tests. For now, it has been tested and coded to work only 128 | locally using subprocess. 129 | 130 | Additionnal methods: 131 | - `assertExists(selector, timeout=-1)` and 132 | `assertNotExists(selector, timeout=-1)` to check if a selector exists or not 133 | in the app. They both have a `timeout` parameter that, if it reach, will fail 134 | the test. 135 | - `cli.wait_click(selector, timeout=-1)`: easy way to wait a selector to match, 136 | then click on the first widget. 137 | 138 | Here is a real example that launch an app (default is "main.py"): 139 | 140 | - It first go in the menu to click where it need to save a CSV (`SaveButton`, `CascadeSaveButton` then `SaveCSVButton`) 141 | - Then wait at maximum 2s the popup to show with a label "Export to CSV" 142 | - Then click on the "Close" button in the popup 143 | - Then ensure the popup is closed by checking the label is gone. 144 | 145 | Example: 146 | 147 | ```python 148 | from telenium.tests import TeleniumTestCase 149 | 150 | class UITestCase(TeleniumTestCase): 151 | cmd_entrypoint = ["main.py"] 152 | 153 | def test_export_csv(self): 154 | self.cli.wait_click("//SaveButton") 155 | self.cli.wait_click("//CascadeSaveButton") 156 | self.cli.wait_click("//SaveCSVButton") 157 | self.assertExists("//Label[@text~=\"Export to CSV\"]", timeout=2) 158 | self.cli.wait_click("//FitButton[@text=\"Close\"]", timeout=2) 159 | self.assertNotExists("//Label[@text~=\"Export to CSV\"]", timeout=2) 160 | ``` 161 | 162 | Each new TeleniumTestCase will close and start the application, so you always 163 | run from a clean app. If you always need to do something before starting the 164 | test, you can overload the `init`. This will be executed once before any 165 | tests in the class starts: 166 | 167 | ```python 168 | class UITestCase(TeleniumTestCase): 169 | def init(self): 170 | self.cli.wait_click("//PresetSelectionItem[@text!~=\"ttyUSB0 on mintel\"]", 171 | timeout=10) 172 | self.cli.wait_click("//Button[@text=\"Connect\"]") 173 | self.cli.wait("//BottomLabel[@text=\"Done\"]", timeout=10) 174 | ``` 175 | 176 | You can also change few parameters to change/add things in your application for 177 | unit testing if needed: 178 | 179 | ```python 180 | class UITestCase(TeleniumTestCase): 181 | process_start_timeout = 5 182 | cmd_env = {"I_AM_RUNNING_TEST": 1} 183 | ``` 184 | 185 | # Telenium commands 186 | 187 | ## `version()` 188 | 189 | Return the current API version. You can use it to know which methods are 190 | available. 191 | 192 | ```python 193 | >>> cli.version() 194 | 1 195 | ``` 196 | 197 | ## `select(selector)` 198 | 199 | Return unique selectors for all widgets that matches the `selector`. 200 | 201 | ```python 202 | >>> cli.select("//Label") 203 | [u"/WindowSDL/GridLayout/Label[0]", u"/WindowSDL/GridLayout/Label[1]"] 204 | ``` 205 | 206 | ## `getattr(selector, key)` 207 | 208 | Return the value of an attribute on the first widget found by the `selector`. 209 | 210 | ```python 211 | >>> cli.getattr("//Label", "text") 212 | u"Hello world" 213 | ``` 214 | 215 | ## `setattr(selector, key, value)` 216 | 217 | Set an attribute named by `key` to `value` for all widgets that matches the 218 | `selector`. 219 | 220 | ```python 221 | >>> cli.setattr("//Label", "text", "Plop") 222 | True 223 | ``` 224 | 225 | ## `element(selector)` 226 | 227 | Return `True` if at least one widget match the `selector`. 228 | 229 | ```python 230 | >>> cli.element("//Label") 231 | True 232 | >>> cli.element("//InvalidButton") 233 | False 234 | ``` 235 | 236 | ## `execute(code)` 237 | 238 | Execute python code in the application. Only the "app" symbol that point to the 239 | current running application is available. Return True if the code executed, or 240 | False if the code failed. Exception will be print withing the application logs. 241 | 242 | ```python 243 | >>> cli.execute("app.call_one_app_method") 244 | True 245 | ``` 246 | 247 | ## `pick(all=False)` 248 | 249 | Return either the first widget selector you touch on the screen (`all=False`, 250 | the default), either it return the list of all the wigdets that are traversed 251 | where you touch the screen. 252 | 253 | ```python 254 | >>> cli.pick() 255 | u'/WindowSDL/Button[0]' 256 | >>> cli.pick(all=True) 257 | [u'/WindowSDL/Button[0]',u'/WindowSDL'] 258 | ``` 259 | 260 | ## `click_on(selector)` 261 | 262 | Simulate a touch down/up on the first widget that match the `selector`. Return 263 | True if it worked. 264 | 265 | ```python 266 | >>> cli.click_on("//Button[0]") 267 | True 268 | ``` 269 | 270 | ## `screenshot(filename=None)` (>= 0.5.0) 271 | 272 | Take a screenshot of the current application in a PNG format. 273 | Data will be saved into filename if passed, or you can have data in the result. 274 | 275 | ```python 276 | >>> cli.screenshot("hello.png") 277 | {"filename": "hello.png", "data": "base64 utf-8 encoded data..."} 278 | ``` 279 | 280 | ## `evaluate(expr)` (>= 0.5.0) 281 | 282 | Evaluate an expression, and return the result. Only serializable result can be 283 | fetched, if an object is sent back, you'll receive None. 284 | 285 | ```python 286 | >>> cli.evaluate("len(app.my_list)") 287 | 123 288 | ``` 289 | 290 | ## `evaluate_and_store(key, expr)` (>= 0.5.0) 291 | 292 | Evaluate an expression, and store the result in a id-map, used by `execute` and `evaluate` method. 293 | 294 | ```python 295 | >>> cli.evaluate_and_store("root", "app.root.children[0]") 296 | True 297 | >>> cli.execute("root.do_something()") 298 | ``` 299 | 300 | ## `select_and_store(key, selector)` (>= 0.5.0) 301 | 302 | Select the first widget returned by a selector, and store the result in a id-map, used by `execute` and `evaluate` method. 303 | 304 | ```python 305 | >>> cli.select_and_store("btn", "//Button[@title~='Login']") 306 | True 307 | >>> cli.execute("btn.disabled = True") 308 | ``` 309 | 310 | 311 | # Telenium selector syntax (XPATH) 312 | 313 | Cheat sheet about telenium XPATH-based selector implementation. 314 | 315 | - Select any widget that match the widget class in the hierarchy: `//CLASS` 316 | - Select a widget that match the tree: `/CLASS` 317 | - Select a widget with attributes `/CLASS[,...]` 318 | - Index selector if there is multiple match: `/CLASS[INDEX]` 319 | - Attribute exists: `@attr` 320 | - Attribute equal to a value: `@attr=VALUE` 321 | - Attribute not equal to a value: `@attr!=VALUE` 322 | - Attribute contain a value: `@attr~=VALUE` 323 | - Attribute does not contain a value: `@attr!~=VALUE` 324 | - Value can be a string, but must be escaped within double quote only. 325 | 326 | Some examples: 327 | 328 | ``` 329 | # Select all the boxlayout in the app 330 | //BoxLayout 331 | 332 | # Take the first boxlayout 333 | //BoxLayout[0] 334 | 335 | # Get the Button as a direct descendant of the BoxLayout 336 | //BoxLayout[0]/Button 337 | 338 | # Or get the 5th Button that are anywhere under the BoxLayout (may or may 339 | # not a direct descandant) 340 | //BoxLayout[0]//Button 341 | 342 | # Select the button that is written "Close" 343 | //BoxLayout[0]//Button[@text="Close"] 344 | 345 | # Select the button that contain "Close" 346 | //BoxLayout[0]//Button[@text~="Close"] 347 | ``` 348 | 349 | # Real life examples 350 | 351 | ## Automate screenshots of an app 352 | 353 | I was having an app where content is displayed randomly. But the client want to review all the texts and position of every content. Easy peasy:: 354 | 355 | ```python 356 | from telenium import connect 357 | prefix = "screenshots/myapp-" 358 | 359 | cli = connect() 360 | cli.wait("//MyAppContainer") 361 | cli.select_and_store("root", "//MyAppContainer") 362 | 363 | animals_count = cli.evaluate("len(root.animals)") 364 | for index in range(animals_count): 365 | # get one animal 366 | cli.evaluate_and_store("animal", f"root.animals[{index}]") 367 | animal_id = cli.evaluate("animal['id']") 368 | animal_name = cli.evaluate("animal['title_en']") 369 | # focus the animal 370 | cli.execute("root.focus_animal(animal)") 371 | cli.sleep(3) 372 | # take a screenshot 373 | cli.screenshot(f"{prefix}{animal_id}-{animal_name}.png") 374 | cli.sleep(3) 375 | ``` 376 | 377 | ## Automate login and go directly to the right screen 378 | 379 | If you code, close your app, restart, login, do many action before reaching the screen you are currently working on, you could automate the first part. 380 | 381 | ```python 382 | from telenium import connect 383 | cli = connect() 384 | cli.wait('//IntroUI') 385 | cli.execute("app.username = 'username'") 386 | cli.execute("app.userpwd = 'fake'") 387 | cli.click_on('//LoginButton') 388 | cli.wait('//IntroUI') 389 | cli.click_on('//StartButton') 390 | cli.wait('//GameUI') 391 | ``` -------------------------------------------------------------------------------- /telenium/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 51 | 52 |
53 |

Project

54 |
55 | 56 | 57 |

58 | Name of your test, used for generating class name 59 |

60 |
61 |
62 | 63 | 64 |

65 | Path to your main.py (or any other entrypoint) 66 |

67 |
68 |
69 | 70 | 71 |

72 | Argument of your application 73 |

74 |
75 |
76 | 77 | 78 |

79 | Maximum time to wait the application to spin up 80 |

81 |
82 |
83 | 84 | 85 |

86 | Maximum time to wait for a selector or a command to work 87 |

88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
KeyValueAction
101 |
102 |
103 | 104 |
105 |
106 | 107 | 154 | 155 | 167 | 168 | 179 | 180 | 196 | 197 | 203 | 204 | 271 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /telenium/mods/telenium_client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | VERSION = 2 4 | 5 | import sys 6 | import os 7 | import re 8 | import threading 9 | import traceback 10 | import json 11 | from kivy.logger import Logger 12 | from kivy.app import App 13 | from kivy.clock import Clock 14 | from werkzeug.wrappers import Request, Response 15 | from werkzeug.serving import run_simple 16 | from jsonrpc import JSONRPCResponseManager, dispatcher 17 | from telenium.xpath import XpathParser 18 | from kivy.input.motionevent import MotionEvent 19 | from kivy.input.provider import MotionEventProvider 20 | from kivy.compat import unichr 21 | from kivy.utils import platform 22 | from itertools import count 23 | from time import time 24 | 25 | nextid = count() 26 | telenium_input = None 27 | 28 | 29 | def kivythread(f): 30 | def f2(*args, **kwargs): 31 | ev = threading.Event() 32 | ev_value = threading.Event() 33 | 34 | def custom_call(dt): 35 | if f(*args, **kwargs): 36 | ev_value.set() 37 | ev.set() 38 | 39 | Clock.schedule_once(custom_call, 0) 40 | ev.wait() 41 | return ev_value.is_set() 42 | 43 | return f2 44 | 45 | 46 | def pick_widget(widget, x, y): 47 | ret = None 48 | # try to filter widgets that are not visible (invalid inspect target) 49 | if (hasattr(widget, 'visible') and not widget.visible): 50 | return ret 51 | if widget.collide_point(x, y): 52 | ret = widget 53 | x2, y2 = widget.to_local(x, y) 54 | # reverse the loop - look at children on top first 55 | for child in reversed(widget.children): 56 | ret = pick_widget(child, x2, y2) or ret 57 | return ret 58 | 59 | 60 | def collide_at(widget, x, y): 61 | if widget.collide_point(x, y): 62 | x2, y2 = widget.to_local(x, y) 63 | have_results = False 64 | for child in reversed(widget.children): 65 | for ret in collide_at(child, x2, y2): 66 | yield ret 67 | have_results = True 68 | if not have_results: 69 | yield widget 70 | 71 | 72 | class TeleniumMotionEvent(MotionEvent): 73 | def depack(self, args): 74 | self.is_touch = True 75 | self.sx, self.sy = args[:2] 76 | super(TeleniumMotionEvent, self).depack(args) 77 | 78 | 79 | class TeleniumInputProvider(MotionEventProvider): 80 | events = [] 81 | 82 | def update(self, dispatch_fn): 83 | while self.events: 84 | event = self.events.pop(0) 85 | dispatch_fn(*event) 86 | 87 | 88 | def selectAll(selector, root=None): 89 | if root is None: 90 | root = App.get_running_app().root.parent 91 | parser = XpathParser() 92 | matches = parser.parse(selector) 93 | matches = matches.execute(root) 94 | return matches or [] 95 | 96 | 97 | def selectFirst(selector, root=None): 98 | matches = selectAll(selector, root=root) 99 | if matches: 100 | return matches[0] 101 | 102 | 103 | def rpc_getattr(selector, key): 104 | widget = selectFirst(selector) 105 | if widget: 106 | return getattr(widget, key) 107 | 108 | 109 | def path_to(widget): 110 | from kivy.core.window import Window 111 | root = Window 112 | if widget.parent is root or widget.parent == widget or not widget.parent: 113 | return "/{}".format(widget.__class__.__name__) 114 | return "{}/{}[{}]".format( 115 | path_to(widget.parent), widget.__class__.__name__, 116 | widget.parent.children.index(widget)) 117 | 118 | 119 | def rpc_ping(): 120 | return True 121 | 122 | 123 | def rpc_version(): 124 | return VERSION 125 | 126 | 127 | def rpc_get_token(): 128 | return os.environ.get("TELENIUM_TOKEN") 129 | 130 | 131 | @kivythread 132 | def rpc_app_quit(): 133 | App.get_running_app().stop() 134 | return True 135 | 136 | 137 | def rpc_app_ready(): 138 | app = App.get_running_app() 139 | if app is None: 140 | return False 141 | if app.root is None: 142 | return False 143 | return True 144 | 145 | 146 | def rpc_select(selector, with_bounds=False): 147 | if not with_bounds: 148 | return list(map(path_to, selectAll(selector))) 149 | 150 | results = [] 151 | for widget in selectAll(selector): 152 | left, bottom = widget.to_window(widget.x, widget.y) 153 | right, top = widget.to_window(widget.x + widget.width, widget.y + widget.height) 154 | bounds = (left, bottom, right, top) 155 | path = path_to(widget) 156 | results.append((path, bounds)) 157 | return results 158 | 159 | 160 | def rpc_highlight(selector): 161 | if not selector: 162 | results = [] 163 | else: 164 | try: 165 | results = rpc_select(selector, with_bounds=True) 166 | except: 167 | _highlight([]) 168 | raise 169 | _highlight(results) 170 | return results 171 | 172 | 173 | @kivythread 174 | def _highlight(results): 175 | from kivy.graphics import Color, Rectangle, Canvas 176 | from kivy.core.window import Window 177 | if not hasattr(Window, "_telenium_canvas"): 178 | Window._telenium_canvas = Canvas() 179 | _canvas = Window._telenium_canvas 180 | 181 | Window.canvas.remove(_canvas) 182 | Window.canvas.add(_canvas) 183 | 184 | _canvas.clear() 185 | with _canvas: 186 | Color(1, 0, 0, 0.5) 187 | for widget, bounds in results: 188 | left, bottom, right, top = bounds 189 | Rectangle(pos=(left, bottom), size=(right-left, top-bottom)) 190 | 191 | 192 | @kivythread 193 | def rpc_setattr(selector, key, value): 194 | ret = False 195 | for widget in selectAll(selector): 196 | setattr(widget, key, value) 197 | ret = True 198 | return ret 199 | 200 | 201 | @kivythread 202 | def rpc_element(selector): 203 | if selectFirst(selector): 204 | return True 205 | 206 | 207 | idmap = {} 208 | 209 | @kivythread 210 | def rpc_execute(cmd): 211 | app = App.get_running_app() 212 | idmap["app"] = app 213 | print("execute", cmd) 214 | try: 215 | exec(cmd, idmap, idmap) 216 | except Exception: 217 | traceback.print_exc() 218 | return False 219 | return True 220 | 221 | 222 | def rpc_evaluate(cmd): 223 | ev = threading.Event() 224 | result = [] 225 | _rpc_evaluate(cmd, ev, result) 226 | ev.wait() 227 | return result[0] 228 | 229 | 230 | @kivythread 231 | def _rpc_evaluate(cmd, ev, result): 232 | app = App.get_running_app() 233 | idmap["app"] = app 234 | res = None 235 | try: 236 | res = eval(cmd, idmap, idmap) 237 | result.append(res) 238 | finally: 239 | if not result: 240 | result.append(None) 241 | ev.set() 242 | 243 | 244 | def rpc_evaluate_and_store(key, cmd): 245 | ev = threading.Event() 246 | result = [] 247 | _rpc_evaluate(cmd, ev, result) 248 | ev.wait() 249 | idmap[key] = result[0] 250 | return True 251 | 252 | 253 | def rpc_select_and_store(key, selector): 254 | idmap[key] = result = selectFirst(selector) 255 | return result is not None 256 | 257 | 258 | def rpc_pick(all=False): 259 | from kivy.core.window import Window 260 | widgets = [] 261 | ev = threading.Event() 262 | 263 | def on_touch_down(touch): 264 | root = App.get_running_app().root 265 | for widget in Window.children: 266 | if all: 267 | widgets.extend(list(collide_at(root, touch.x, touch.y))) 268 | else: 269 | widget = pick_widget(root, touch.x, touch.y) 270 | widgets.append(widget) 271 | ev.set() 272 | return True 273 | 274 | orig_on_touch_down = Window.on_touch_down 275 | Window.on_touch_down = on_touch_down 276 | ev.wait() 277 | Window.on_touch_down = orig_on_touch_down 278 | if widgets: 279 | if all: 280 | ret = list(map(path_to, widgets)) 281 | else: 282 | ret = path_to(widgets[0]) 283 | return ret 284 | 285 | 286 | @kivythread 287 | def rpc_click_on(selector): 288 | w = selectFirst(selector) 289 | if w: 290 | from kivy.core.window import Window 291 | cx, cy = w.to_window(w.center_x, w.center_y) 292 | sx = cx / float(Window.width) 293 | sy = cy / float(Window.height) 294 | me = TeleniumMotionEvent("telenium", 295 | id=next(nextid), 296 | args=[sx, sy]) 297 | telenium_input.events.append(("begin", me)) 298 | telenium_input.events.append(("end", me)) 299 | return True 300 | 301 | 302 | @kivythread 303 | def rpc_drag(selector, target, duration): 304 | from kivy.base import EventLoop 305 | w1 = selectFirst(selector) 306 | w2 = selectFirst(target) 307 | duration = float(duration) 308 | if w1 and w2: 309 | from kivy.core.window import Window 310 | cx1, cy1 = w1.to_window(w1.center_x, w1.center_y) 311 | sx1 = cx1 / float(Window.width) 312 | sy1 = cy1 / float(Window.height) 313 | 314 | me = TeleniumMotionEvent("telenium", 315 | id=next(nextid), 316 | args=[sx1, sy1]) 317 | 318 | telenium_input.events.append(("begin", me)) 319 | if not duration: 320 | telenium_input.events.append(("end", me)) 321 | 322 | else: 323 | d = 0 324 | while d < duration: 325 | t = time() 326 | EventLoop.idle() 327 | dt = time() - t 328 | # need to compute that ever frame, it could have moved 329 | cx2, cy2 = w2.to_window(w2.center_x, w2.center_y) 330 | sx2 = cx2 / float(Window.width) 331 | sy2 = cy2 / float(Window.height) 332 | 333 | dsx = dt * (sx2 - me.sx) / (duration - d) 334 | dsy = dt * (sy2 - me.sy) / (duration - d) 335 | 336 | me.sx += dsx 337 | me.sy += dsy 338 | 339 | telenium_input.events.append(("update", me)) 340 | d += dt 341 | 342 | telenium_input.events.append(("end", me)) 343 | return True 344 | 345 | 346 | def rpc_send_keycode(keycodes): 347 | # very hard to get it right, not fully tested and fail proof. 348 | # just the basics. 349 | from kivy.core.window import Keyboard 350 | keys = keycodes.split("+") 351 | scancode = 0 352 | key = None 353 | sym = "" 354 | modifiers = [] 355 | for el in keys: 356 | if re.match("^[A-Z]", el): 357 | lower_el = el.lower() 358 | # modifier detected ? add it 359 | if lower_el in ("ctrl", "meta", "alt", "shift"): 360 | modifiers.append(lower_el) 361 | continue 362 | # not a modifier, convert to scancode 363 | sym = lower_el 364 | key = Keyboard.keycodes.get(lower_el, 0) 365 | else: 366 | # may fail, so nothing would be done. 367 | try: 368 | key = int(el) 369 | sym = unichr(key) 370 | except: 371 | traceback.print_exc() 372 | return False 373 | _send_keycode(key, scancode, sym, modifiers) 374 | return True 375 | 376 | 377 | @kivythread 378 | def _send_keycode(key, scancode, sym, modifiers): 379 | from kivy.core.window import Window 380 | print("Telenium: send key key={!r} scancode={} sym={!r} modifiers={}".format( 381 | key, scancode, sym, modifiers 382 | )) 383 | if not Window.dispatch("on_key_down", key, scancode, sym, modifiers): 384 | Window.dispatch("on_keyboard", key, scancode, sym, modifiers) 385 | Window.dispatch("on_key_up", key, scancode) 386 | return True 387 | 388 | 389 | def rpc_screenshot(): 390 | ev = threading.Event() 391 | result = [] 392 | _rpc_screenshot(ev, result) 393 | ev.wait() 394 | return result[0] 395 | 396 | 397 | @kivythread 398 | def _rpc_screenshot(ev, result): 399 | import base64 400 | filename = None 401 | data = None 402 | try: 403 | from kivy.core.window import Window 404 | filename = Window.screenshot() 405 | with open(filename, "rb") as fd: 406 | data = fd.read() 407 | os.unlink(filename) 408 | return True 409 | finally: 410 | result.append({ 411 | "filename": filename, 412 | "data": base64.b64encode(data).decode("utf-8") 413 | }) 414 | ev.set() 415 | 416 | 417 | def register_input_provider(): 418 | global telenium_input 419 | telenium_input = TeleniumInputProvider("telenium", None) 420 | from kivy.base import EventLoop 421 | EventLoop.add_input_provider(telenium_input) 422 | 423 | 424 | def load_android_env_var_file(): 425 | """ 426 | When using a TeleniumTestCase is to launch an app on an android device, 427 | environment variables need to be set (even if no additional ones are 428 | specified by the user). Unfortunately, adb doesn't seem to allow setting 429 | environment variables in any straight-forward manner. Instead, 430 | TeleniumTestCase provides the key-value pairs needed in the form of a file 431 | (/sdcard/telenium_env.json) to be handled by the remote telenium client. 432 | Said file is deleted once loaded. Note that this implies that your app is 433 | going to need the right permissions to access your storage. 434 | """ 435 | file_path = '/sdcard/telenium_env.json' 436 | if platform == 'android' and os.path.exists(file_path): 437 | with open(file_path, 'r') as filestream: 438 | file_contents = json.load(filestream) 439 | os.environ.update(file_contents) 440 | os.remove(file_path) 441 | 442 | 443 | @Request.application 444 | def application(request): 445 | try: 446 | response = JSONRPCResponseManager.handle( 447 | request.data, dispatcher) 448 | except Exception as e: 449 | raise 450 | return Response(response.json, mimetype='application/json') 451 | 452 | 453 | def run_telenium(): 454 | Logger.info("TeleniumClient: Started at 0.0.0.0:9901") 455 | load_android_env_var_file() 456 | register_input_provider() 457 | 458 | dispatcher.add_method(rpc_version, "version") 459 | dispatcher.add_method(rpc_ping, "ping") 460 | dispatcher.add_method(rpc_get_token, "get_token") 461 | dispatcher.add_method(rpc_app_quit, "app_quit") 462 | dispatcher.add_method(rpc_app_ready, "app_ready") 463 | dispatcher.add_method(rpc_select, "select") 464 | dispatcher.add_method(rpc_highlight, "highlight") 465 | dispatcher.add_method(rpc_getattr, "getattr") 466 | dispatcher.add_method(rpc_setattr, "setattr") 467 | dispatcher.add_method(rpc_element, "element") 468 | dispatcher.add_method(rpc_execute, "execute") 469 | dispatcher.add_method(rpc_evaluate, "evaluate") 470 | dispatcher.add_method(rpc_evaluate_and_store, "evaluate_and_store") 471 | dispatcher.add_method(rpc_select_and_store, "select_and_store") 472 | dispatcher.add_method(rpc_pick, "pick") 473 | dispatcher.add_method(rpc_click_on, "click_on") 474 | dispatcher.add_method(rpc_drag, "drag") 475 | dispatcher.add_method(rpc_send_keycode, "send_keycode") 476 | dispatcher.add_method(rpc_screenshot, "screenshot") 477 | 478 | run_simple("0.0.0.0", 9901, application) 479 | 480 | 481 | def install(): 482 | thread = threading.Thread(target=run_telenium) 483 | thread.daemon = True 484 | thread.start() 485 | 486 | 487 | def start(win, ctx): 488 | Logger.info("TeleniumClient: Start") 489 | ctx.thread = threading.Thread(target=run_telenium) 490 | ctx.thread.daemon = True 491 | ctx.thread.start() 492 | 493 | 494 | def stop(win, ctx): 495 | pass 496 | -------------------------------------------------------------------------------- /telenium/static/js/ui.js: -------------------------------------------------------------------------------- 1 | var url = "ws://" + document.location.hostname + ":" + document.location.port + "/ws"; 2 | var socket = null; 3 | var current_el = null; 4 | var app_status = "stopped"; 5 | var test = null; 6 | var current_test_id = null; 7 | var latest_export = null; 8 | 9 | function telenium_execute() { 10 | console.log("app_status", app_status) 11 | $("#btn-execute") 12 | .prop("disabled", true) 13 | .addClass("loading"); 14 | if (app_status == "stopped") 15 | telenium_send("execute", {}); 16 | else if (app_status == "running") 17 | telenium_send("stop", {}); 18 | } 19 | 20 | function telenium_save_local() { 21 | telenium_send("save_local", {}); 22 | } 23 | 24 | function telenium_add_env() { 25 | var template = $("#tpl-env-new").html(); 26 | var rendered = Mustache.render(template, {}); 27 | $("#tl-env").append(rendered); 28 | } 29 | 30 | function telenium_add_step() { 31 | var template = $("#tpl-step-new").html(); 32 | var rendered = Mustache.render(template, {}); 33 | $("#tl-steps").append(rendered); 34 | telenium_sync_test(); 35 | } 36 | 37 | function telenium_remove_env(ev) { 38 | $($(ev).parents()[1]).detach(); 39 | telenium_sync_env(); 40 | } 41 | 42 | function _telenium_ev_step_to_row(ev) { 43 | var parents = $(ev).parentsUntil("tbody"); 44 | return parents[parents.length - 1]; 45 | } 46 | 47 | function telenium_remove_step(ev) { 48 | $(_telenium_ev_step_to_row(ev)).detach(); 49 | telenium_sync_test(); 50 | } 51 | 52 | function telenium_duplicate_step(ev) { 53 | var parent = _telenium_ev_step_to_row(ev); 54 | $(parent).clone().insertAfter(parent); 55 | } 56 | 57 | function telenium_connect() { 58 | socket = new ReconnectingWebSocket(url, null, { 59 | automaticOpen: false 60 | }) 61 | socket.onmessage = function(event) { 62 | var msg = JSON.parse(event.data); 63 | telenium_process(msg); 64 | }; 65 | socket.onopen = function() { 66 | telenium_send("recover") 67 | } 68 | socket.open(); 69 | } 70 | 71 | function telenium_send(command, options) { 72 | var data = { 73 | "cmd": command, 74 | "options": options || {} 75 | }; 76 | socket.send(JSON.stringify(data)); 77 | } 78 | 79 | function telenium_pick(el) { 80 | current_el = el; 81 | $("#modal-pick-wait").modal(); 82 | telenium_send("pick", {}); 83 | } 84 | 85 | function telenium_pick_use(selector) { 86 | $("#modal-pick").modal("hide"); 87 | $(current_el).parent().find("input.selector").val(selector); 88 | telenium_sync_test(); 89 | } 90 | 91 | function telenium_play_step(el) { 92 | var index = $(el).parents("tr")[0].rowIndex - 1; 93 | telenium_send("run_step", { 94 | "id": current_test_id, 95 | "index": index 96 | }); 97 | } 98 | 99 | function telenium_run_steps() { 100 | $(".test-status").hide(); 101 | telenium_send("run_steps", { 102 | "id": current_test_id 103 | }); 104 | } 105 | 106 | function telenium_run_tests() { 107 | $(".test-status").hide(); 108 | telenium_send("run_tests", {}); 109 | } 110 | 111 | function telenium_add_test() { 112 | current_test_id = null; 113 | telenium_send("add_test", {}); 114 | } 115 | 116 | function telenium_clone_test() { 117 | telenium_send("clone_test", {"test_id": current_test_id}); 118 | current_test_id = null; 119 | } 120 | 121 | function telenium_process(msg) { 122 | cmd = msg[0]; 123 | console.log(msg) 124 | if (cmd == "settings") { 125 | $.each(msg[1], function(key, value) { 126 | $("input[data-settings-key='" + key + "']").val(value); 127 | }) 128 | 129 | } else if (cmd == "env") { 130 | $("#tl-env").empty(); 131 | template = $("#tpl-env-new").html(); 132 | for (var i = 0; i < msg[1].length; i++) { 133 | var entry = msg[1][i]; 134 | tpl = Mustache.render(template, { 135 | "key": entry[0], 136 | "value": entry[1] 137 | }); 138 | $("#tl-env").append(tpl); 139 | } 140 | 141 | } else if (cmd == "tests") { 142 | if (current_test_id === null) 143 | current_test_id = msg[1][msg[1].length - 1]["id"]; 144 | app_sync_tests_choice(msg[1]); 145 | telenium_select_test(current_test_id); 146 | 147 | } else if (cmd == "test") { 148 | test = msg[1]; 149 | current_test_id = test["id"]; 150 | app_sync_test(); 151 | 152 | } else if (cmd == "status") { 153 | app_status = msg[1]; 154 | if (msg[1] == "running") { 155 | $("#btn-execute") 156 | .prop("disabled", false) 157 | .removeClass("loading") 158 | .find("span") 159 | .removeClass("glyphicon-play") 160 | .addClass("glyphicon-stop"); 161 | } else if (msg[1] == "stopped") { 162 | $("#btn-execute") 163 | .prop("disabled", false) 164 | .removeClass("loading") 165 | .find("span") 166 | .removeClass("glyphicon-stop") 167 | .addClass("glyphicon-play"); 168 | } 169 | 170 | } else if (cmd == "pick") { 171 | $("#modal-pick-wait").modal("hide"); 172 | if (msg[1] == "error") { 173 | alert("Application is not running"); 174 | } else { 175 | var selectors = msg[2]; 176 | if (selectors.length == 1) { 177 | telenium_pick_use(selectors[0]); 178 | } else if (selectors.length > 1) { 179 | template = $("#tpl-pick-list").html(); 180 | tpl = Mustache.render(template, { 181 | "selectors": selectors 182 | }); 183 | $("#modal-pick .modal-body").html(tpl); 184 | $("#modal-pick").modal("show"); 185 | } 186 | } 187 | 188 | } else if (cmd == "run_test") { 189 | 190 | } else if (cmd == "run_step") { 191 | var test_id = msg[1]; 192 | var rowindex = msg[2]; 193 | var status = msg[3]; 194 | if (test_id != current_test_id) 195 | return; 196 | var tr = $("#tl-steps tr").eq(rowindex); 197 | tr.find(".test-status").hide(); 198 | tr.find(".test-status-" + status).show(); 199 | 200 | } else if (cmd == "export") { 201 | latest_export = { 202 | "data": msg[1], 203 | "mimetype": msg[2], 204 | "filename": msg[3], 205 | "type": msg[4] 206 | } 207 | $("#modal-export").modal("show"); 208 | $("#modal-export .modal-body pre").html(latest_export["data"]); 209 | 210 | } else if (cmd == "progress") { 211 | if (msg[1] == "started") { 212 | $(".progress-box").removeClass("hidden"); 213 | app_set_progress("0"); 214 | } else if (msg[1] == "update") { 215 | var count = msg[2]; 216 | var total = msg[3] 217 | if (total > 0) 218 | app_set_progress("" + Math.round(count * 100 / total)); 219 | } else { 220 | $(".progress-box").addClass("hidden"); 221 | } 222 | } else if (cmd == "changed") { 223 | $("#btn-save").prop("disabled", !msg[1]); 224 | } else if (cmd == "is_local") { 225 | $("#btn-save").show(); 226 | } 227 | } 228 | 229 | function telenium_sync_env() { 230 | var keys = $.map($("#tl-env input[name='key[]']"), function(item) { 231 | return $(item).val(); 232 | }) 233 | var values = $.map($("#tl-env input[name='value[]']"), function(item) { 234 | return $(item).val(); 235 | }) 236 | var env = {}; 237 | for (var i = 0; i < keys.length; i++) { 238 | if (keys[i] == "") 239 | continue; 240 | env[keys[i]] = values[i]; 241 | } 242 | telenium_send("sync_env", { 243 | "env": env 244 | }); 245 | } 246 | 247 | 248 | function telenium_sync_settings() { 249 | var settings = {}; 250 | $("input[data-settings-key]").each(function(index, item) { 251 | settings[$(this).data("settings-key")] = $(this).val(); 252 | }) 253 | telenium_send("sync_settings", { 254 | "settings": settings 255 | }); 256 | } 257 | 258 | 259 | function telenium_sync_test() { 260 | var t_types = $.map($("#tl-steps select"), function(item) { 261 | return $(item).val(); 262 | }) 263 | var t_selectors = $.map($("#tl-steps input.step-selector"), function(item) { 264 | return $(item).val(); 265 | }) 266 | var t_args1 = $.map($("#tl-steps input.step-arg1"), function(item) { 267 | return $(item).val(); 268 | }) 269 | var t_args2 = $.map($("#tl-steps input.step-arg2"), function(item) { 270 | return $(item).val(); 271 | }) 272 | var steps = []; 273 | for (var i = 0; i < t_types.length; i++) { 274 | steps.push([t_types[i], t_selectors[i], t_args1[i], t_args2[i]]); 275 | } 276 | telenium_send("sync_test", { 277 | "id": current_test_id, 278 | "name": $("input[data-test-key='name']").val(), 279 | "steps": steps 280 | }); 281 | } 282 | 283 | function telenium_delete_test() { 284 | if (current_test_id === null) 285 | return; 286 | telenium_send("delete_test", { 287 | "id": current_test_id 288 | }); 289 | current_test_id = null; 290 | } 291 | 292 | function telenium_select(selector) { 293 | telenium_send("select", { 294 | "selector": selector 295 | }); 296 | } 297 | 298 | function telenium_select_test(test_id) { 299 | current_test_id = test_id; 300 | telenium_send("select_test", { 301 | "id": test_id 302 | }); 303 | } 304 | 305 | function telenium_export_python() { 306 | telenium_send("export", { 307 | "type": "python" 308 | }); 309 | } 310 | 311 | function telenium_export_json() { 312 | telenium_send("export", { 313 | "type": "json" 314 | }); 315 | } 316 | 317 | function app_show_page(page) { 318 | $(".navpage").removeClass("active"); 319 | $(".navpage[data-page=" + page + "]").addClass("active"); 320 | $(".page").addClass("hidden"); 321 | $("#page-" + page).removeClass("hidden"); 322 | } 323 | 324 | function app_sync_test() { 325 | if (current_test_id === null) { 326 | current_test_id = test["id"]; 327 | } 328 | $("#tl-steps").empty(); 329 | $("input[data-test-key='name']").val(test["name"]); 330 | 331 | var steps = test["steps"]; 332 | template = $("#tpl-step-new").html(); 333 | for (var i = 0; i < steps.length; i++) { 334 | var entry = steps[i]; 335 | var tpl = $(Mustache.render(template, { 336 | "key": entry[0], 337 | "selector": entry[1], 338 | "arg1": entry[2], 339 | "arg2": entry[3] 340 | })); 341 | tpl.find("option[value=" + entry[0] + "]") 342 | .attr("selected", true); 343 | $("#tl-steps").append(tpl); 344 | $("#tl-steps select").change(); 345 | } 346 | } 347 | 348 | function app_sync_tests_choice(tests) { 349 | $("#tl-tests").empty(); 350 | for (var i = 0; i < tests.length; i++) { 351 | var option = $("") 352 | .val(tests[i]["id"]) 353 | .html(tests[i]["name"]); 354 | if (tests[i]["id"] == current_test_id) { 355 | option.prop("selected", true); 356 | } 357 | option.appendTo($("#tl-tests")); 358 | } 359 | } 360 | 361 | function app_set_progress(value) { 362 | $(".progress-box .progress-bar").css("width", "" + value + "%"); 363 | } 364 | 365 | var textFile = null; 366 | 367 | function makeTextFile(text, mimetype) { 368 | var data = new Blob([text], { 369 | type: mimetype 370 | }); 371 | if (textFile !== null) { 372 | window.URL.revokeObjectURL(textFile); 373 | } 374 | textFile = window.URL.createObjectURL(data); 375 | return textFile; 376 | } 377 | 378 | function app_export_save() { 379 | var link = document.createElement("a"); 380 | link.setAttribute("download", latest_export["filename"]); 381 | link.href = makeTextFile(latest_export["data"], latest_export["mimetype"]); 382 | window.requestAnimationFrame(function() { 383 | var event = new MouseEvent("click"); 384 | link.dispatchEvent(event); 385 | document.body.removeChild(link); 386 | }); 387 | } 388 | 389 | $(document).ready(function() { 390 | $(".navpage").click(function(ev, el) { 391 | app_show_page($(this).data("page")); 392 | }) 393 | $("#btn-execute").click(telenium_execute); 394 | $("#btn-add-test").click(telenium_add_test); 395 | $("#btn-clone-test").click(telenium_clone_test); 396 | $("#btn-add-step").click(telenium_add_step); 397 | $("#btn-add-env").click(telenium_add_env); 398 | $("#btn-save").click(telenium_save_local); 399 | $("#btn-run-steps").click(telenium_run_steps); 400 | $("#btn-run-tests").click(telenium_run_tests); 401 | $("#btn-delete-test").click(telenium_delete_test); 402 | $("#btn-export-python").click(telenium_export_python); 403 | $("#btn-export-json").click(telenium_export_json); 404 | $("#btn-export-save").click(app_export_save); 405 | $("#tl-env").on("blur", "input", function() { 406 | telenium_sync_env(); 407 | }); 408 | $("#tl-tests").on("change", function() { 409 | telenium_select_test($(this).val()); 410 | }); 411 | $("input[data-settings-key]").on("blur", function() { 412 | telenium_sync_settings(); 413 | }); 414 | $("input[data-test-key]").on("change", function() { 415 | $("option[value='" + current_test_id + "']").html($(this).val()); 416 | telenium_sync_test(); 417 | }); 418 | $("#tl-steps").on("blur", "select,input", function() { 419 | telenium_sync_test(); 420 | }); 421 | $("#tl-steps").on("change", "select", function() { 422 | var parent = $($(this).parents()[1]); 423 | var container = parent.find(".step-arg-container"); 424 | var selected = $(this).find(":selected"); 425 | if (typeof selected.data("arg0") === "undefined") { 426 | parent.find(".step-selector") 427 | .prop("placeholder", 'XPATH selector like //Button[@text~="Hello"]'); 428 | } else { 429 | parent.find(".step-selector") 430 | .prop("placeholder", selected.data("arg0")); 431 | } 432 | 433 | if (typeof selected.data("arg1") === "undefined") { 434 | container.hide(); 435 | return; 436 | } 437 | container.find(".step-arg1").prop("placeholder", selected.data("arg1")); 438 | if (typeof selected.data("arg2") === "undefined") { 439 | container.find(".step-arg2").hide(); 440 | } else { 441 | container.find(".step-arg2") 442 | .prop("placeholder", selected.data("arg2")) 443 | .show(); 444 | } 445 | 446 | container.show(); 447 | 448 | }).on("input", "input.step-selector", function(ev) { 449 | current_el = ev.target; 450 | telenium_select($(current_el).val()); 451 | }).on("focus", "input.step-selector", function(ev) { 452 | current_el = ev.target; 453 | telenium_select($(current_el).val()); 454 | }).on("blur", "input.step-selector", function(ev) { 455 | current_el = ev.target; 456 | telenium_select(""); 457 | }); 458 | 459 | $("#tl-steps-container").sortable({ 460 | containerSelector: "table", 461 | itemPath: "> tbody", 462 | itemSelector: "tr", 463 | placeholder: "", 464 | onDrop: function($item, container, _super) { 465 | _super($item, container); 466 | telenium_sync_test() 467 | } 468 | }); 469 | 470 | telenium_connect(); 471 | }); 472 | -------------------------------------------------------------------------------- /telenium/static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /telenium/web.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import re 5 | import sys 6 | import functools 7 | import threading 8 | import cherrypy 9 | import json 10 | import subprocess 11 | import traceback 12 | import webbrowser 13 | import argparse 14 | import shlex 15 | from mako.template import Template 16 | from uuid import uuid4 17 | import telenium 18 | from telenium.client import TeleniumHttpClient 19 | from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool 20 | from ws4py.websocket import WebSocket 21 | from os.path import dirname, join, realpath 22 | from time import time, sleep 23 | 24 | SESSION_FN = ".telenium.dat" 25 | 26 | TPL_EXPORT_UNITTEST = u"""<%! 27 | def capitalize(text): 28 | return text.capitalize() 29 | def camelcase(text): 30 | return "".join([x.strip().capitalize() for x in text.split()]) 31 | def funcname(text): 32 | if text == "init": 33 | return "init" 34 | import re 35 | suffix = re.sub(r"[^a-z0-9_]", "_", text.lower().strip()) 36 | return "test_{}".format(suffix) 37 | def getarg(text): 38 | import re 39 | return re.match("^(\w+)", text).groups()[0] 40 | %># coding=utf-8 41 | 42 | import time 43 | from telenium.tests import TeleniumTestCase 44 | 45 | 46 | class ${settings["project"]|camelcase}TestCase(TeleniumTestCase): 47 | % if env: 48 | cmd_env = ${ env } 49 | % endif 50 | cmd_entrypoint = [u'${ settings["entrypoint"] }'] 51 | % for test in tests: 52 | % if test["name"] == "setUpClass": 53 | <% vself = "cls" %> 54 | @classmethod 55 | def setUpClass(cls): 56 | super(${settings["project"]|camelcase}TestCase, cls).setUpClass() 57 | % else: 58 | <% vself = "self" %> 59 | def ${test["name"]|funcname}(self): 60 | % if not test["steps"]: 61 | pass 62 | % endif 63 | % endif 64 | % for key, value, arg1, arg2 in test["steps"]: 65 | % if key == "wait": 66 | ${vself}.cli.wait('${value}', timeout=${settings["command-timeout"]}) 67 | % elif key == "wait_click": 68 | ${vself}.cli.wait_click('${value}', timeout=${settings["command-timeout"]}) 69 | % elif key == "assertExists": 70 | ${vself}.assertExists('${value}', timeout=${settings["command-timeout"]}) 71 | % elif key == "assertNotExists": 72 | ${vself}.assertNotExists('${value}', timeout=${settings["command-timeout"]}) 73 | % elif key == "assertAttributeValue": 74 | attr_name = '${arg1|getarg}' 75 | attr_value = ${vself}.cli.getattr('${value}', attr_name) 76 | ${vself}.assertTrue(eval('${arg1}', {attr_name: attr_value})) 77 | % elif key == "setAttribute": 78 | ${vself}.cli.setattr('${value}', '${arg1}', ${arg2}) 79 | % elif key == "sendKeycode": 80 | ${vself}.cli.send_keycode('${value}') 81 | % elif key == "sleep": 82 | time.sleep(${value}) 83 | % elif key == "executeCode": 84 | ${vself}.assertTrue(self.cli.execute('${value}')) 85 | % endif 86 | % endfor 87 | % endfor 88 | """ 89 | 90 | FILE_API_VERSION = 3 91 | local_filename = None 92 | 93 | 94 | def threaded(f): 95 | @functools.wraps(f) 96 | def _threaded(*args, **kwargs): 97 | thread = threading.Thread(target=f, args=args, kwargs=kwargs) 98 | thread.daemon = True 99 | thread.start() 100 | return thread 101 | 102 | return _threaded 103 | 104 | 105 | def funcname(text): 106 | return text.lower().replace(" ", "_").strip() 107 | 108 | 109 | def getarg(text): 110 | return re.match("^(\w+)", text).groups()[0] 111 | 112 | 113 | class ApiWebSocket(WebSocket): 114 | t_process = None 115 | cli = None 116 | progress_count = 0 117 | progress_total = 0 118 | session = { 119 | "settings": { 120 | "project": "Test", 121 | "entrypoint": "main.py", 122 | "application-timeout": "10", 123 | "command-timeout": "5", 124 | "args": "" 125 | }, 126 | "env": {}, 127 | "tests": [{ 128 | "id": str(uuid4()), 129 | "name": "New test", 130 | "steps": [] 131 | }] 132 | } 133 | 134 | def opened(self): 135 | super(ApiWebSocket, self).opened() 136 | self.load() 137 | 138 | def closed(self, code, reason=None): 139 | pass 140 | 141 | def received_message(self, message): 142 | msg = json.loads(message.data) 143 | try: 144 | getattr(self, "cmd_{}".format(msg["cmd"]))(msg["options"]) 145 | except: 146 | traceback.print_exc() 147 | 148 | def send_object(self, obj): 149 | data = json.dumps(obj) 150 | self.send(data, False) 151 | 152 | def save(self): 153 | self.session["version_format"] = FILE_API_VERSION 154 | 155 | # check if the file changed 156 | if local_filename is not None: 157 | changed = False 158 | try: 159 | with open(local_filename) as fd: 160 | data = json.loads(fd.read()) 161 | changed = data != self.session 162 | except: 163 | changed = True 164 | self.send_object(["changed", changed]) 165 | 166 | with open(SESSION_FN, "w") as fd: 167 | fd.write(json.dumps(self.session)) 168 | 169 | def load(self): 170 | try: 171 | with open(SESSION_FN) as fd: 172 | session = json.loads(fd.read()) 173 | session = upgrade_version(session) 174 | self.session.update(session) 175 | except: 176 | pass 177 | 178 | def get_test(self, test_id): 179 | for test in self.session["tests"]: 180 | if test["id"] == test_id: 181 | return test 182 | 183 | def get_test_by_name(self, name): 184 | for test in self.session["tests"]: 185 | if test["name"] == name: 186 | return test 187 | 188 | @property 189 | def is_running(self): 190 | return self.t_process is not None 191 | 192 | # command implementation 193 | 194 | def cmd_recover(self, options): 195 | if local_filename: 196 | self.send_object(["is_local", True]) 197 | self.send_object(["settings", self.session["settings"]]) 198 | self.send_object(["env", dict(self.session["env"].items())]) 199 | tests = [{ 200 | "name": x["name"], 201 | "id": x["id"] 202 | } for x in self.session["tests"]] 203 | self.send_object(["tests", tests]) 204 | if self.t_process is not None: 205 | self.send_object(["status", "running"]) 206 | 207 | def cmd_save_local(self, options): 208 | try: 209 | assert local_filename is not None 210 | # save json source 211 | data = self.export("json") 212 | with open(local_filename, "w") as fd: 213 | fd.write(data) 214 | 215 | # auto export to python 216 | filename = local_filename.replace(".json", ".py") 217 | data = self.export("python") 218 | with open(filename, "w") as fd: 219 | fd.write(data) 220 | self.send_object(["save_local", "ok"]) 221 | self.send_object(["changed", False]) 222 | except Exception as e: 223 | traceback.print_exc() 224 | self.send_object(["save_local", "error", repr(e)]) 225 | 226 | def cmd_sync_env(self, options): 227 | while self.session["env"]: 228 | self.session["env"].pop(self.session["env"].keys()[0]) 229 | for key, value in options.get("env", {}).items(): 230 | self.session["env"][key] = value 231 | self.save() 232 | 233 | def cmd_sync_settings(self, options): 234 | self.session["settings"] = options["settings"] 235 | self.save() 236 | 237 | def cmd_sync_test(self, options): 238 | uid = options["id"] 239 | for test in self.session["tests"]: 240 | if test["id"] == uid: 241 | test["name"] = options["name"] 242 | test["steps"] = options["steps"] 243 | self.save() 244 | 245 | def cmd_add_test(self, options): 246 | self.session["tests"].append({ 247 | "id": str(uuid4()), 248 | "name": "New test", 249 | "steps": [] 250 | }) 251 | self.save() 252 | self.send_object(["tests", self.session["tests"]]) 253 | 254 | def cmd_clone_test(self, options): 255 | for test in self.session["tests"]: 256 | if test["id"] != options["test_id"]: 257 | continue 258 | clone_test = test.copy() 259 | clone_test["id"] = str(uuid4()) 260 | clone_test["name"] += " (1)" 261 | self.session["tests"].append(clone_test) 262 | break 263 | self.save() 264 | self.send_object(["tests", self.session["tests"]]) 265 | 266 | def cmd_delete_test(self, options): 267 | for test in self.session["tests"][:]: 268 | if test["id"] == options["id"]: 269 | self.session["tests"].remove(test) 270 | if not self.session["tests"]: 271 | self.cmd_add_test(None) 272 | self.save() 273 | self.send_object(["tests", self.session["tests"]]) 274 | 275 | def cmd_select(self, options): 276 | if not self.cli: 277 | status = "error" 278 | results = "Application not running" 279 | else: 280 | try: 281 | results = self.cli.highlight(options["selector"]) 282 | status = "ok" 283 | except Exception as e: 284 | status = "error" 285 | results = u"{}".format(e) 286 | self.send_object(["select", options["selector"], status, results]) 287 | 288 | def cmd_select_test(self, options): 289 | test = self.get_test(options["id"]) 290 | self.send_object(["test", test]) 291 | 292 | @threaded 293 | def cmd_pick(self, options): 294 | if not self.cli: 295 | return self.send_object(["pick", "error", "App is not started"]) 296 | objs = self.cli.pick(True) 297 | return self.send_object(["pick", "success", objs]) 298 | 299 | @threaded 300 | def cmd_execute(self, options): 301 | self.execute() 302 | 303 | def cmd_run_step(self, options): 304 | self.run_step(options["id"], options["index"]) 305 | 306 | @threaded 307 | def cmd_run_steps(self, options): 308 | test = self.get_test(options["id"]) 309 | if test is None: 310 | self.send_object(["alert", "Test not found"]) 311 | return 312 | if not self.is_running: 313 | ev_start, ev_stop = self.execute() 314 | ev_start.wait() 315 | if ev_stop.is_set(): 316 | return 317 | self.run_test(test) 318 | 319 | @threaded 320 | def cmd_run_tests(self, options): 321 | # restart always from scratch 322 | self.send_object(["progress", "started"]) 323 | 324 | # precalculate the number of steps to run 325 | count = sum([len(x["steps"]) for x in self.session["tests"]]) 326 | self.progress_count = 0 327 | self.progress_total = count 328 | 329 | try: 330 | ev_start, ev_stop = self.execute() 331 | ev_start.wait() 332 | if ev_stop.is_set(): 333 | return 334 | setup = self.get_test_by_name("setUpClass") 335 | if setup: 336 | if not self.run_test(setup): 337 | return 338 | setup = self.get_test_by_name("init") 339 | if setup: 340 | if not self.run_test(setup): 341 | return 342 | for test in self.session["tests"]: 343 | if test["name"] in ("setUpClass", "init"): 344 | continue 345 | if not self.run_test(test): 346 | return 347 | finally: 348 | self.send_object(["progress", "finished"]) 349 | 350 | def cmd_stop(self, options): 351 | if self.t_process: 352 | self.t_process.terminate() 353 | 354 | def cmd_export(self, options): 355 | try: 356 | dtype = options["type"] 357 | mimetype = { 358 | "python": "text/plain", 359 | "json": "application/json" 360 | }[dtype] 361 | ext = {"python": "py", "json": "json"}[dtype] 362 | key = funcname(self.session["settings"]["project"]) 363 | filename = "test_ui_{}.{}".format(key, ext) 364 | export = self.export(options["type"]) 365 | self.send_object(["export", export, mimetype, filename, dtype]) 366 | except Exception as e: 367 | self.send_object(["export", "error", u"{}".format(e)]) 368 | 369 | def export(self, kind): 370 | if kind == "python": 371 | return Template(TPL_EXPORT_UNITTEST).render( 372 | session=self.session, **self.session) 373 | elif kind == "json": 374 | self.session["version_format"] = FILE_API_VERSION 375 | return json.dumps( 376 | self.session, sort_keys=True, indent=4, separators=(',', ': ')) 377 | 378 | def execute(self): 379 | ev_start = threading.Event() 380 | ev_stop = threading.Event() 381 | self._execute(ev_start=ev_start, ev_stop=ev_stop) 382 | return ev_start, ev_stop 383 | 384 | @threaded 385 | def _execute(self, ev_start, ev_stop): 386 | self.t_process = None 387 | try: 388 | self.start_process() 389 | ev_start.set() 390 | self.t_process.communicate() 391 | self.send_object(["status", "stopped", None]) 392 | except Exception as e: 393 | try: 394 | self.t_process.terminate() 395 | except: 396 | pass 397 | try: 398 | self.send_object(["status", "stopped", u"{}".format(e)]) 399 | except: 400 | pass 401 | finally: 402 | self.t_process = None 403 | ev_stop.set() 404 | 405 | def start_process(self): 406 | url = "http://localhost:9901/jsonrpc" 407 | process_start_timeout = 10 408 | telenium_token = str(uuid4()) 409 | self.cli = cli = TeleniumHttpClient(url=url, timeout=10) 410 | 411 | # entry no any previous telenium is running 412 | try: 413 | cli.app_quit() 414 | sleep(2) 415 | except: 416 | pass 417 | 418 | # prepare the application 419 | entrypoint = self.session["settings"]["entrypoint"] 420 | print(self.session) 421 | args = shlex.split(self.session["settings"].get("args", "")) 422 | cmd = [sys.executable, "-m", "telenium.execute", entrypoint] + args 423 | cwd = dirname(entrypoint) 424 | if not os.path.isabs(cwd): 425 | cwd = os.getcwd() 426 | env = os.environ.copy() 427 | env.update(self.session["env"]) 428 | env["TELENIUM_TOKEN"] = telenium_token 429 | 430 | # start the application 431 | self.t_process = subprocess.Popen(cmd, env=env, cwd=cwd) 432 | 433 | # wait for telenium server to be online 434 | start = time() 435 | while True: 436 | try: 437 | if cli.app_ready(): 438 | break 439 | except Exception: 440 | if time() - start > process_start_timeout: 441 | raise Exception("timeout") 442 | sleep(1) 443 | 444 | # ensure the telenium we are connected are the same as the one we 445 | # launched here 446 | if cli.get_token() != telenium_token: 447 | raise Exception("Connected to another telenium server") 448 | 449 | self.send_object(["status", "running"]) 450 | 451 | def run_test(self, test): 452 | test_id = test["id"] 453 | try: 454 | self.send_object(["test", test]) 455 | self.send_object(["run_test", test_id, "running"]) 456 | for index, step in enumerate(test["steps"]): 457 | if not self.run_step(test_id, index): 458 | return 459 | return True 460 | except Exception as e: 461 | self.send_object(["run_test", test_id, "error", str(e)]) 462 | else: 463 | self.send_object(["run_test", test_id, "finished"]) 464 | 465 | def run_step(self, test_id, index): 466 | self.progress_count += 1 467 | self.send_object( 468 | ["progress", "update", self.progress_count, self.progress_total]) 469 | try: 470 | self.send_object(["run_step", test_id, index, "running"]) 471 | success = self._run_step(test_id, index) 472 | if success: 473 | self.send_object(["run_step", test_id, index, "success"]) 474 | return True 475 | else: 476 | self.send_object(["run_step", test_id, index, "error"]) 477 | except Exception as e: 478 | self.send_object(["run_step", test_id, index, "error", str(e)]) 479 | 480 | def _run_step(self, test_id, index): 481 | test = self.get_test(test_id) 482 | if not test: 483 | raise Exception("Unknown test") 484 | cmd, selector, arg1, arg2 = test["steps"][index] 485 | timeout = 5 486 | if cmd == "wait": 487 | return self.cli.wait(selector, timeout=timeout) 488 | elif cmd == "wait_click": 489 | self.cli.wait_click(selector, timeout=timeout) 490 | return True 491 | elif cmd == "wait_drag": 492 | self.cli.wait_drag( 493 | selector, target=arg1, duration=arg2, timeout=timeout) 494 | return True 495 | elif cmd == "assertExists": 496 | return self.cli.wait(selector, timeout=timeout) is True 497 | elif cmd == "assertNotExists": 498 | return self.assertNotExists(self.cli, selector, timeout=timeout) 499 | elif cmd == "assertAttributeValue": 500 | attr_name = getarg(arg1) 501 | attr_value = self.cli.getattr(selector, attr_name) 502 | return bool(eval(arg1, {attr_name: attr_value})) 503 | elif cmd == "setAttribute": 504 | return self.cli.setattr(selector, arg1, eval(arg2)) 505 | elif cmd == "sendKeycode": 506 | self.cli.send_keycode(selector) 507 | return True 508 | elif cmd == "sleep": 509 | sleep(float(selector)) 510 | return True 511 | elif cmd == "executeCode": 512 | return self.cli.execute(selector) 513 | 514 | def assertNotExists(self, cli, selector, timeout=-1): 515 | start = time() 516 | while True: 517 | matches = cli.select(selector) 518 | if not matches: 519 | return True 520 | if timeout == -1: 521 | raise AssertionError("selector matched elements") 522 | if timeout > 0 and time() - start > timeout: 523 | raise Exception("Timeout") 524 | sleep(0.1) 525 | 526 | 527 | class Root(object): 528 | @cherrypy.expose 529 | def index(self): 530 | raise cherrypy.HTTPRedirect("/static/index.html") 531 | 532 | @cherrypy.expose 533 | def ws(self): 534 | pass 535 | 536 | 537 | class WebSocketServer(object): 538 | def __init__(self, host="0.0.0.0", port=8080, open_webbrowser=True): 539 | super(WebSocketServer, self).__init__() 540 | self.host = host 541 | self.port = port 542 | self.daemon = True 543 | self.open_webbrowser = open_webbrowser 544 | 545 | def run(self): 546 | cherrypy.config.update({ 547 | "global": { 548 | "environment": "production" 549 | }, 550 | "server.socket_port": self.port, 551 | "server.socket_host": self.host, 552 | }) 553 | cherrypy.tree.mount( 554 | Root(), 555 | "/", 556 | config={ 557 | "/": { 558 | "tools.sessions.on": True 559 | }, 560 | "/ws": { 561 | "tools.websocket.on": True, 562 | "tools.websocket.handler_cls": ApiWebSocket 563 | }, 564 | "/static": { 565 | "tools.staticdir.on": 566 | True, 567 | "tools.staticdir.dir": 568 | join(realpath(dirname(__file__)), "static"), 569 | "tools.staticdir.index": 570 | "index.html" 571 | } 572 | }) 573 | cherrypy.engine.start() 574 | url = "http://{}:{}/".format(self.host, self.port) 575 | print("Telenium {} ready at {}".format(telenium.__version__, url)) 576 | if self.open_webbrowser: 577 | webbrowser.open(url) 578 | cherrypy.engine.block() 579 | 580 | def stop(self): 581 | cherrypy.engine.exit() 582 | cherrypy.server.stop() 583 | 584 | 585 | def preload_session(filename): 586 | global local_filename 587 | local_filename = filename 588 | if not local_filename.endswith(".json"): 589 | print("You can load only telenium-json files.") 590 | sys.exit(1) 591 | if not os.path.exists(filename): 592 | print("Create new file at {}".format(local_filename)) 593 | if os.path.exists(SESSION_FN): 594 | os.unlink(SESSION_FN) 595 | else: 596 | with open(filename) as fd: 597 | session = json.loads(fd.read()) 598 | session = upgrade_version(session) 599 | with open(SESSION_FN, "w") as fd: 600 | fd.write(json.dumps(session)) 601 | 602 | 603 | def upgrade_version(session): 604 | # automatically upgrade to latest version 605 | version_format = session.get("version_format") 606 | if version_format is None or version_format == FILE_API_VERSION: 607 | return session 608 | session["version_format"] += 1 609 | version_format = session["version_format"] 610 | print("Upgrade to version {}".format(version_format)) 611 | if version_format == 2: 612 | # arg added in steps, so steps must have 3 arguments not 2. 613 | for test in session["tests"]: 614 | for step in test["steps"]: 615 | if len(step) == 2: 616 | step.append(None) 617 | elif version_format == 3: 618 | # arg added in steps, so steps must have 4 arguments not 3. 619 | for test in session["tests"]: 620 | for step in test["steps"]: 621 | if len(step) == 3: 622 | step.append(None) 623 | return session 624 | 625 | 626 | WebSocketPlugin(cherrypy.engine).subscribe() 627 | cherrypy.tools.websocket = WebSocketTool() 628 | 629 | 630 | def run(): 631 | 632 | parser = argparse.ArgumentParser(description="Telenium IDE") 633 | parser.add_argument( 634 | "filename", 635 | type=str, 636 | default=None, 637 | nargs="?", 638 | help="Telenium JSON file") 639 | parser.add_argument( 640 | "--new", action="store_true", help="Start a new session") 641 | parser.add_argument( 642 | "--port", type=int, default=8080, help="Telenium IDE port") 643 | parser.add_argument( 644 | "--notab", 645 | action="store_true", 646 | help="Prevent opening the IDE in the browser") 647 | args = parser.parse_args() 648 | if args.new: 649 | if os.path.exists(SESSION_FN): 650 | os.unlink(SESSION_FN) 651 | if args.filename: 652 | preload_session(args.filename) 653 | server = WebSocketServer(port=args.port, open_webbrowser=not args.notab) 654 | server.run() 655 | server.stop() 656 | 657 | 658 | if __name__ == "__main__": 659 | run() 660 | -------------------------------------------------------------------------------- /telenium/static/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn-default .badge, 33 | .btn-primary .badge, 34 | .btn-success .badge, 35 | .btn-info .badge, 36 | .btn-warning .badge, 37 | .btn-danger .badge { 38 | text-shadow: none; 39 | } 40 | .btn:active, 41 | .btn.active { 42 | background-image: none; 43 | } 44 | .btn-default { 45 | text-shadow: 0 1px 0 #fff; 46 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 47 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 48 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 49 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 52 | background-repeat: repeat-x; 53 | border-color: #dbdbdb; 54 | border-color: #ccc; 55 | } 56 | .btn-default:hover, 57 | .btn-default:focus { 58 | background-color: #e0e0e0; 59 | background-position: 0 -15px; 60 | } 61 | .btn-default:active, 62 | .btn-default.active { 63 | background-color: #e0e0e0; 64 | border-color: #dbdbdb; 65 | } 66 | .btn-default.disabled, 67 | .btn-default:disabled, 68 | .btn-default[disabled] { 69 | background-color: #e0e0e0; 70 | background-image: none; 71 | } 72 | .btn-primary { 73 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 74 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 75 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 76 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #245580; 81 | } 82 | .btn-primary:hover, 83 | .btn-primary:focus { 84 | background-color: #265a88; 85 | background-position: 0 -15px; 86 | } 87 | .btn-primary:active, 88 | .btn-primary.active { 89 | background-color: #265a88; 90 | border-color: #245580; 91 | } 92 | .btn-primary.disabled, 93 | .btn-primary:disabled, 94 | .btn-primary[disabled] { 95 | background-color: #265a88; 96 | background-image: none; 97 | } 98 | .btn-success { 99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 101 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 102 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 103 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 104 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 105 | background-repeat: repeat-x; 106 | border-color: #3e8f3e; 107 | } 108 | .btn-success:hover, 109 | .btn-success:focus { 110 | background-color: #419641; 111 | background-position: 0 -15px; 112 | } 113 | .btn-success:active, 114 | .btn-success.active { 115 | background-color: #419641; 116 | border-color: #3e8f3e; 117 | } 118 | .btn-success.disabled, 119 | .btn-success:disabled, 120 | .btn-success[disabled] { 121 | background-color: #419641; 122 | background-image: none; 123 | } 124 | .btn-info { 125 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 126 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 127 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 128 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 129 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 130 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 131 | background-repeat: repeat-x; 132 | border-color: #28a4c9; 133 | } 134 | .btn-info:hover, 135 | .btn-info:focus { 136 | background-color: #2aabd2; 137 | background-position: 0 -15px; 138 | } 139 | .btn-info:active, 140 | .btn-info.active { 141 | background-color: #2aabd2; 142 | border-color: #28a4c9; 143 | } 144 | .btn-info.disabled, 145 | .btn-info:disabled, 146 | .btn-info[disabled] { 147 | background-color: #2aabd2; 148 | background-image: none; 149 | } 150 | .btn-warning { 151 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 152 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 153 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 154 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 155 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 156 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 157 | background-repeat: repeat-x; 158 | border-color: #e38d13; 159 | } 160 | .btn-warning:hover, 161 | .btn-warning:focus { 162 | background-color: #eb9316; 163 | background-position: 0 -15px; 164 | } 165 | .btn-warning:active, 166 | .btn-warning.active { 167 | background-color: #eb9316; 168 | border-color: #e38d13; 169 | } 170 | .btn-warning.disabled, 171 | .btn-warning:disabled, 172 | .btn-warning[disabled] { 173 | background-color: #eb9316; 174 | background-image: none; 175 | } 176 | .btn-danger { 177 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 178 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 179 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 180 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 182 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 183 | background-repeat: repeat-x; 184 | border-color: #b92c28; 185 | } 186 | .btn-danger:hover, 187 | .btn-danger:focus { 188 | background-color: #c12e2a; 189 | background-position: 0 -15px; 190 | } 191 | .btn-danger:active, 192 | .btn-danger.active { 193 | background-color: #c12e2a; 194 | border-color: #b92c28; 195 | } 196 | .btn-danger.disabled, 197 | .btn-danger:disabled, 198 | .btn-danger[disabled] { 199 | background-color: #c12e2a; 200 | background-image: none; 201 | } 202 | .thumbnail, 203 | .img-thumbnail { 204 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 205 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 206 | } 207 | .dropdown-menu > li > a:hover, 208 | .dropdown-menu > li > a:focus { 209 | background-color: #e8e8e8; 210 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 211 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 212 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 213 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 214 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 215 | background-repeat: repeat-x; 216 | } 217 | .dropdown-menu > .active > a, 218 | .dropdown-menu > .active > a:hover, 219 | .dropdown-menu > .active > a:focus { 220 | background-color: #2e6da4; 221 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 222 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 223 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 224 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 226 | background-repeat: repeat-x; 227 | } 228 | .navbar-default { 229 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 230 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 231 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 232 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 233 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 234 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 235 | background-repeat: repeat-x; 236 | border-radius: 4px; 237 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 238 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 239 | } 240 | .navbar-default .navbar-nav > .open > a, 241 | .navbar-default .navbar-nav > .active > a { 242 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 243 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 244 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 245 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 246 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 247 | background-repeat: repeat-x; 248 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 249 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 250 | } 251 | .navbar-brand, 252 | .navbar-nav > li > a { 253 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 254 | } 255 | .navbar-inverse { 256 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 257 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 258 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 259 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 260 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 261 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 262 | background-repeat: repeat-x; 263 | } 264 | .navbar-inverse .navbar-nav > .open > a, 265 | .navbar-inverse .navbar-nav > .active > a { 266 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 267 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 268 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 269 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 270 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 271 | background-repeat: repeat-x; 272 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 273 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 274 | } 275 | .navbar-inverse .navbar-brand, 276 | .navbar-inverse .navbar-nav > li > a { 277 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 278 | } 279 | .navbar-static-top, 280 | .navbar-fixed-top, 281 | .navbar-fixed-bottom { 282 | border-radius: 0; 283 | } 284 | @media (max-width: 767px) { 285 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 286 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 287 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 288 | color: #fff; 289 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 290 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 291 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 292 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 294 | background-repeat: repeat-x; 295 | } 296 | } 297 | .alert { 298 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 299 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .alert-success { 303 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 304 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 305 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 306 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 307 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 308 | background-repeat: repeat-x; 309 | border-color: #b2dba1; 310 | } 311 | .alert-info { 312 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 313 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 314 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 315 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 316 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 317 | background-repeat: repeat-x; 318 | border-color: #9acfea; 319 | } 320 | .alert-warning { 321 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 322 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 323 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 324 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 325 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 326 | background-repeat: repeat-x; 327 | border-color: #f5e79e; 328 | } 329 | .alert-danger { 330 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 331 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 332 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 333 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 334 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 335 | background-repeat: repeat-x; 336 | border-color: #dca7a7; 337 | } 338 | .progress { 339 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 340 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 342 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 344 | background-repeat: repeat-x; 345 | } 346 | .progress-bar { 347 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 348 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 349 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 350 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 352 | background-repeat: repeat-x; 353 | } 354 | .progress-bar-success { 355 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 356 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 357 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 358 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 360 | background-repeat: repeat-x; 361 | } 362 | .progress-bar-info { 363 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 364 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 365 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 366 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 368 | background-repeat: repeat-x; 369 | } 370 | .progress-bar-warning { 371 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 372 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 374 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 376 | background-repeat: repeat-x; 377 | } 378 | .progress-bar-danger { 379 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 380 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 381 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 382 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 383 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 384 | background-repeat: repeat-x; 385 | } 386 | .progress-bar-striped { 387 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 388 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 389 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 390 | } 391 | .list-group { 392 | border-radius: 4px; 393 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 394 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 395 | } 396 | .list-group-item.active, 397 | .list-group-item.active:hover, 398 | .list-group-item.active:focus { 399 | text-shadow: 0 -1px 0 #286090; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 405 | background-repeat: repeat-x; 406 | border-color: #2b669a; 407 | } 408 | .list-group-item.active .badge, 409 | .list-group-item.active:hover .badge, 410 | .list-group-item.active:focus .badge { 411 | text-shadow: none; 412 | } 413 | .panel { 414 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 415 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 416 | } 417 | .panel-default > .panel-heading { 418 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 419 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 420 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 421 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 422 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 423 | background-repeat: repeat-x; 424 | } 425 | .panel-primary > .panel-heading { 426 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 427 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 428 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 429 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 430 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 431 | background-repeat: repeat-x; 432 | } 433 | .panel-success > .panel-heading { 434 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 435 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 436 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 437 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 438 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 439 | background-repeat: repeat-x; 440 | } 441 | .panel-info > .panel-heading { 442 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 443 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 444 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 445 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 447 | background-repeat: repeat-x; 448 | } 449 | .panel-warning > .panel-heading { 450 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 451 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 453 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .panel-danger > .panel-heading { 458 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 459 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 461 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .well { 466 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 467 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 469 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 471 | background-repeat: repeat-x; 472 | border-color: #dcdcdc; 473 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 474 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 475 | } 476 | /*# sourceMappingURL=bootstrap-theme.css.map */ 477 | -------------------------------------------------------------------------------- /telenium/static/js/jquery-sortable.js: -------------------------------------------------------------------------------- 1 | /* =================================================== 2 | * jquery-sortable.js v0.9.13 3 | * http://johnny.github.com/jquery-sortable/ 4 | * =================================================== 5 | * Copyright (c) 2012 Jonas von Andrian 6 | * All rights reserved. 7 | * 8 | * Redistribution and use in source and binary forms, with or without 9 | * modification, are permitted provided that the following conditions are met: 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * * Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * * The name of the author may not be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * ========================================================== */ 29 | 30 | 31 | !function ( $, window, pluginName, undefined){ 32 | var containerDefaults = { 33 | // If true, items can be dragged from this container 34 | drag: true, 35 | // If true, items can be droped onto this container 36 | drop: true, 37 | // Exclude items from being draggable, if the 38 | // selector matches the item 39 | exclude: "", 40 | // If true, search for nested containers within an item.If you nest containers, 41 | // either the original selector with which you call the plugin must only match the top containers, 42 | // or you need to specify a group (see the bootstrap nav example) 43 | nested: true, 44 | // If true, the items are assumed to be arranged vertically 45 | vertical: true 46 | }, // end container defaults 47 | groupDefaults = { 48 | // This is executed after the placeholder has been moved. 49 | // $closestItemOrContainer contains the closest item, the placeholder 50 | // has been put at or the closest empty Container, the placeholder has 51 | // been appended to. 52 | afterMove: function ($placeholder, container, $closestItemOrContainer) { 53 | }, 54 | // The exact css path between the container and its items, e.g. "> tbody" 55 | containerPath: "", 56 | // The css selector of the containers 57 | containerSelector: "ol, ul", 58 | // Distance the mouse has to travel to start dragging 59 | distance: 0, 60 | // Time in milliseconds after mousedown until dragging should start. 61 | // This option can be used to prevent unwanted drags when clicking on an element. 62 | delay: 0, 63 | // The css selector of the drag handle 64 | handle: "", 65 | // The exact css path between the item and its subcontainers. 66 | // It should only match the immediate items of a container. 67 | // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div" 68 | itemPath: "", 69 | // The css selector of the items 70 | itemSelector: "li", 71 | // The class given to "body" while an item is being dragged 72 | bodyClass: "dragging", 73 | // The class giving to an item while being dragged 74 | draggedClass: "dragged", 75 | // Check if the dragged item may be inside the container. 76 | // Use with care, since the search for a valid container entails a depth first search 77 | // and may be quite expensive. 78 | isValidTarget: function ($item, container) { 79 | return true 80 | }, 81 | // Executed before onDrop if placeholder is detached. 82 | // This happens if pullPlaceholder is set to false and the drop occurs outside a container. 83 | onCancel: function ($item, container, _super, event) { 84 | }, 85 | // Executed at the beginning of a mouse move event. 86 | // The Placeholder has not been moved yet. 87 | onDrag: function ($item, position, _super, event) { 88 | $item.css(position) 89 | }, 90 | // Called after the drag has been started, 91 | // that is the mouse button is being held down and 92 | // the mouse is moving. 93 | // The container is the closest initialized container. 94 | // Therefore it might not be the container, that actually contains the item. 95 | onDragStart: function ($item, container, _super, event) { 96 | $item.css({ 97 | height: $item.outerHeight(), 98 | width: $item.outerWidth() 99 | }) 100 | $item.addClass(container.group.options.draggedClass) 101 | $("body").addClass(container.group.options.bodyClass) 102 | }, 103 | // Called when the mouse button is being released 104 | onDrop: function ($item, container, _super, event) { 105 | $item.removeClass(container.group.options.draggedClass).removeAttr("style") 106 | $("body").removeClass(container.group.options.bodyClass) 107 | }, 108 | // Called on mousedown. If falsy value is returned, the dragging will not start. 109 | // Ignore if element clicked is input, select or textarea 110 | onMousedown: function ($item, _super, event) { 111 | if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) { 112 | event.preventDefault() 113 | return true 114 | } 115 | }, 116 | // The class of the placeholder (must match placeholder option markup) 117 | placeholderClass: "placeholder", 118 | // Template for the placeholder. Can be any valid jQuery input 119 | // e.g. a string, a DOM element. 120 | // The placeholder must have the class "placeholder" 121 | placeholder: '
  • ', 122 | // If true, the position of the placeholder is calculated on every mousemove. 123 | // If false, it is only calculated when the mouse is above a container. 124 | pullPlaceholder: true, 125 | // Specifies serialization of the container group. 126 | // The pair $parent/$children is either container/items or item/subcontainers. 127 | serialize: function ($parent, $children, parentIsContainer) { 128 | var result = $.extend({}, $parent.data()) 129 | 130 | if(parentIsContainer) 131 | return [$children] 132 | else if ($children[0]){ 133 | result.children = $children 134 | } 135 | 136 | delete result.subContainers 137 | delete result.sortable 138 | 139 | return result 140 | }, 141 | // Set tolerance while dragging. Positive values decrease sensitivity, 142 | // negative values increase it. 143 | tolerance: 0 144 | }, // end group defaults 145 | containerGroups = {}, 146 | groupCounter = 0, 147 | emptyBox = { 148 | left: 0, 149 | top: 0, 150 | bottom: 0, 151 | right:0 152 | }, 153 | eventNames = { 154 | start: "touchstart.sortable mousedown.sortable", 155 | drop: "touchend.sortable touchcancel.sortable mouseup.sortable", 156 | drag: "touchmove.sortable mousemove.sortable", 157 | scroll: "scroll.sortable" 158 | }, 159 | subContainerKey = "subContainers" 160 | 161 | /* 162 | * a is Array [left, right, top, bottom] 163 | * b is array [left, top] 164 | */ 165 | function d(a,b) { 166 | var x = Math.max(0, a[0] - b[0], b[0] - a[1]), 167 | y = Math.max(0, a[2] - b[1], b[1] - a[3]) 168 | return x+y; 169 | } 170 | 171 | function setDimensions(array, dimensions, tolerance, useOffset) { 172 | var i = array.length, 173 | offsetMethod = useOffset ? "offset" : "position" 174 | tolerance = tolerance || 0 175 | 176 | while(i--){ 177 | var el = array[i].el ? array[i].el : $(array[i]), 178 | // use fitting method 179 | pos = el[offsetMethod]() 180 | pos.left += parseInt(el.css('margin-left'), 10) 181 | pos.top += parseInt(el.css('margin-top'),10) 182 | dimensions[i] = [ 183 | pos.left - tolerance, 184 | pos.left + el.outerWidth() + tolerance, 185 | pos.top - tolerance, 186 | pos.top + el.outerHeight() + tolerance 187 | ] 188 | } 189 | } 190 | 191 | function getRelativePosition(pointer, element) { 192 | var offset = element.offset() 193 | return { 194 | left: pointer.left - offset.left, 195 | top: pointer.top - offset.top 196 | } 197 | } 198 | 199 | function sortByDistanceDesc(dimensions, pointer, lastPointer) { 200 | pointer = [pointer.left, pointer.top] 201 | lastPointer = lastPointer && [lastPointer.left, lastPointer.top] 202 | 203 | var dim, 204 | i = dimensions.length, 205 | distances = [] 206 | 207 | while(i--){ 208 | dim = dimensions[i] 209 | distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)] 210 | } 211 | distances = distances.sort(function (a,b) { 212 | return b[1] - a[1] || b[2] - a[2] || b[0] - a[0] 213 | }) 214 | 215 | // last entry is the closest 216 | return distances 217 | } 218 | 219 | function ContainerGroup(options) { 220 | this.options = $.extend({}, groupDefaults, options) 221 | this.containers = [] 222 | 223 | if(!this.options.rootGroup){ 224 | this.scrollProxy = $.proxy(this.scroll, this) 225 | this.dragProxy = $.proxy(this.drag, this) 226 | this.dropProxy = $.proxy(this.drop, this) 227 | this.placeholder = $(this.options.placeholder) 228 | 229 | if(!options.isValidTarget) 230 | this.options.isValidTarget = undefined 231 | } 232 | } 233 | 234 | ContainerGroup.get = function (options) { 235 | if(!containerGroups[options.group]) { 236 | if(options.group === undefined) 237 | options.group = groupCounter ++ 238 | 239 | containerGroups[options.group] = new ContainerGroup(options) 240 | } 241 | 242 | return containerGroups[options.group] 243 | } 244 | 245 | ContainerGroup.prototype = { 246 | dragInit: function (e, itemContainer) { 247 | this.$document = $(itemContainer.el[0].ownerDocument) 248 | 249 | // get item to drag 250 | var closestItem = $(e.target).closest(this.options.itemSelector); 251 | // using the length of this item, prevents the plugin from being started if there is no handle being clicked on. 252 | // this may also be helpful in instantiating multidrag. 253 | if (closestItem.length) { 254 | this.item = closestItem; 255 | this.itemContainer = itemContainer; 256 | if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) { 257 | return; 258 | } 259 | this.setPointer(e); 260 | this.toggleListeners('on'); 261 | this.setupDelayTimer(); 262 | this.dragInitDone = true; 263 | } 264 | }, 265 | drag: function (e) { 266 | if(!this.dragging){ 267 | if(!this.distanceMet(e) || !this.delayMet) 268 | return 269 | 270 | this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e) 271 | this.item.before(this.placeholder) 272 | this.dragging = true 273 | } 274 | 275 | this.setPointer(e) 276 | // place item under the cursor 277 | this.options.onDrag(this.item, 278 | getRelativePosition(this.pointer, this.item.offsetParent()), 279 | groupDefaults.onDrag, 280 | e) 281 | 282 | var p = this.getPointer(e), 283 | box = this.sameResultBox, 284 | t = this.options.tolerance 285 | 286 | if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left) 287 | if(!this.searchValidTarget()){ 288 | this.placeholder.detach() 289 | this.lastAppendedItem = undefined 290 | } 291 | }, 292 | drop: function (e) { 293 | this.toggleListeners('off') 294 | 295 | this.dragInitDone = false 296 | 297 | if(this.dragging){ 298 | // processing Drop, check if placeholder is detached 299 | if(this.placeholder.closest("html")[0]){ 300 | this.placeholder.before(this.item).detach() 301 | } else { 302 | this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e) 303 | } 304 | this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e) 305 | 306 | // cleanup 307 | this.clearDimensions() 308 | this.clearOffsetParent() 309 | this.lastAppendedItem = this.sameResultBox = undefined 310 | this.dragging = false 311 | } 312 | }, 313 | searchValidTarget: function (pointer, lastPointer) { 314 | if(!pointer){ 315 | pointer = this.relativePointer || this.pointer 316 | lastPointer = this.lastRelativePointer || this.lastPointer 317 | } 318 | 319 | var distances = sortByDistanceDesc(this.getContainerDimensions(), 320 | pointer, 321 | lastPointer), 322 | i = distances.length 323 | 324 | while(i--){ 325 | var index = distances[i][0], 326 | distance = distances[i][1] 327 | 328 | if(!distance || this.options.pullPlaceholder){ 329 | var container = this.containers[index] 330 | if(!container.disabled){ 331 | if(!this.$getOffsetParent()){ 332 | var offsetParent = container.getItemOffsetParent() 333 | pointer = getRelativePosition(pointer, offsetParent) 334 | lastPointer = getRelativePosition(lastPointer, offsetParent) 335 | } 336 | if(container.searchValidTarget(pointer, lastPointer)) 337 | return true 338 | } 339 | } 340 | } 341 | if(this.sameResultBox) 342 | this.sameResultBox = undefined 343 | }, 344 | movePlaceholder: function (container, item, method, sameResultBox) { 345 | var lastAppendedItem = this.lastAppendedItem 346 | if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0]) 347 | return; 348 | 349 | item[method](this.placeholder) 350 | this.lastAppendedItem = item 351 | this.sameResultBox = sameResultBox 352 | this.options.afterMove(this.placeholder, container, item) 353 | }, 354 | getContainerDimensions: function () { 355 | if(!this.containerDimensions) 356 | setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent()) 357 | return this.containerDimensions 358 | }, 359 | getContainer: function (element) { 360 | return element.closest(this.options.containerSelector).data(pluginName) 361 | }, 362 | $getOffsetParent: function () { 363 | if(this.offsetParent === undefined){ 364 | var i = this.containers.length - 1, 365 | offsetParent = this.containers[i].getItemOffsetParent() 366 | 367 | if(!this.options.rootGroup){ 368 | while(i--){ 369 | if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){ 370 | // If every container has the same offset parent, 371 | // use position() which is relative to this parent, 372 | // otherwise use offset() 373 | // compare #setDimensions 374 | offsetParent = false 375 | break; 376 | } 377 | } 378 | } 379 | 380 | this.offsetParent = offsetParent 381 | } 382 | return this.offsetParent 383 | }, 384 | setPointer: function (e) { 385 | var pointer = this.getPointer(e) 386 | 387 | if(this.$getOffsetParent()){ 388 | var relativePointer = getRelativePosition(pointer, this.$getOffsetParent()) 389 | this.lastRelativePointer = this.relativePointer 390 | this.relativePointer = relativePointer 391 | } 392 | 393 | this.lastPointer = this.pointer 394 | this.pointer = pointer 395 | }, 396 | distanceMet: function (e) { 397 | var currentPointer = this.getPointer(e) 398 | return (Math.max( 399 | Math.abs(this.pointer.left - currentPointer.left), 400 | Math.abs(this.pointer.top - currentPointer.top) 401 | ) >= this.options.distance) 402 | }, 403 | getPointer: function(e) { 404 | var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0] 405 | return { 406 | left: e.pageX || o.pageX, 407 | top: e.pageY || o.pageY 408 | } 409 | }, 410 | setupDelayTimer: function () { 411 | var that = this 412 | this.delayMet = !this.options.delay 413 | 414 | // init delay timer if needed 415 | if (!this.delayMet) { 416 | clearTimeout(this._mouseDelayTimer); 417 | this._mouseDelayTimer = setTimeout(function() { 418 | that.delayMet = true 419 | }, this.options.delay) 420 | } 421 | }, 422 | scroll: function (e) { 423 | this.clearDimensions() 424 | this.clearOffsetParent() // TODO is this needed? 425 | }, 426 | toggleListeners: function (method) { 427 | var that = this, 428 | events = ['drag','drop','scroll'] 429 | 430 | $.each(events,function (i,event) { 431 | that.$document[method](eventNames[event], that[event + 'Proxy']) 432 | }) 433 | }, 434 | clearOffsetParent: function () { 435 | this.offsetParent = undefined 436 | }, 437 | // Recursively clear container and item dimensions 438 | clearDimensions: function () { 439 | this.traverse(function(object){ 440 | object._clearDimensions() 441 | }) 442 | }, 443 | traverse: function(callback) { 444 | callback(this) 445 | var i = this.containers.length 446 | while(i--){ 447 | this.containers[i].traverse(callback) 448 | } 449 | }, 450 | _clearDimensions: function(){ 451 | this.containerDimensions = undefined 452 | }, 453 | _destroy: function () { 454 | containerGroups[this.options.group] = undefined 455 | } 456 | } 457 | 458 | function Container(element, options) { 459 | this.el = element 460 | this.options = $.extend( {}, containerDefaults, options) 461 | 462 | this.group = ContainerGroup.get(this.options) 463 | this.rootGroup = this.options.rootGroup || this.group 464 | this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector 465 | 466 | var itemPath = this.rootGroup.options.itemPath 467 | this.target = itemPath ? this.el.find(itemPath) : this.el 468 | 469 | this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this)) 470 | 471 | if(this.options.drop) 472 | this.group.containers.push(this) 473 | } 474 | 475 | Container.prototype = { 476 | dragInit: function (e) { 477 | var rootGroup = this.rootGroup 478 | 479 | if( !this.disabled && 480 | !rootGroup.dragInitDone && 481 | this.options.drag && 482 | this.isValidDrag(e)) { 483 | rootGroup.dragInit(e, this) 484 | } 485 | }, 486 | isValidDrag: function(e) { 487 | return e.which == 1 || 488 | e.type == "touchstart" && e.originalEvent.touches.length == 1 489 | }, 490 | searchValidTarget: function (pointer, lastPointer) { 491 | var distances = sortByDistanceDesc(this.getItemDimensions(), 492 | pointer, 493 | lastPointer), 494 | i = distances.length, 495 | rootGroup = this.rootGroup, 496 | validTarget = !rootGroup.options.isValidTarget || 497 | rootGroup.options.isValidTarget(rootGroup.item, this) 498 | 499 | if(!i && validTarget){ 500 | rootGroup.movePlaceholder(this, this.target, "append") 501 | return true 502 | } else 503 | while(i--){ 504 | var index = distances[i][0], 505 | distance = distances[i][1] 506 | if(!distance && this.hasChildGroup(index)){ 507 | var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer) 508 | if(found) 509 | return true 510 | } 511 | else if(validTarget){ 512 | this.movePlaceholder(index, pointer) 513 | return true 514 | } 515 | } 516 | }, 517 | movePlaceholder: function (index, pointer) { 518 | var item = $(this.items[index]), 519 | dim = this.itemDimensions[index], 520 | method = "after", 521 | width = item.outerWidth(), 522 | height = item.outerHeight(), 523 | offset = item.offset(), 524 | sameResultBox = { 525 | left: offset.left, 526 | right: offset.left + width, 527 | top: offset.top, 528 | bottom: offset.top + height 529 | } 530 | if(this.options.vertical){ 531 | var yCenter = (dim[2] + dim[3]) / 2, 532 | inUpperHalf = pointer.top <= yCenter 533 | if(inUpperHalf){ 534 | method = "before" 535 | sameResultBox.bottom -= height / 2 536 | } else 537 | sameResultBox.top += height / 2 538 | } else { 539 | var xCenter = (dim[0] + dim[1]) / 2, 540 | inLeftHalf = pointer.left <= xCenter 541 | if(inLeftHalf){ 542 | method = "before" 543 | sameResultBox.right -= width / 2 544 | } else 545 | sameResultBox.left += width / 2 546 | } 547 | if(this.hasChildGroup(index)) 548 | sameResultBox = emptyBox 549 | this.rootGroup.movePlaceholder(this, item, method, sameResultBox) 550 | }, 551 | getItemDimensions: function () { 552 | if(!this.itemDimensions){ 553 | this.items = this.$getChildren(this.el, "item").filter( 554 | ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")" 555 | ).get() 556 | setDimensions(this.items, this.itemDimensions = [], this.options.tolerance) 557 | } 558 | return this.itemDimensions 559 | }, 560 | getItemOffsetParent: function () { 561 | var offsetParent, 562 | el = this.el 563 | // Since el might be empty we have to check el itself and 564 | // can not do something like el.children().first().offsetParent() 565 | if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed") 566 | offsetParent = el 567 | else 568 | offsetParent = el.offsetParent() 569 | return offsetParent 570 | }, 571 | hasChildGroup: function (index) { 572 | return this.options.nested && this.getContainerGroup(index) 573 | }, 574 | getContainerGroup: function (index) { 575 | var childGroup = $.data(this.items[index], subContainerKey) 576 | if( childGroup === undefined){ 577 | var childContainers = this.$getChildren(this.items[index], "container") 578 | childGroup = false 579 | 580 | if(childContainers[0]){ 581 | var options = $.extend({}, this.options, { 582 | rootGroup: this.rootGroup, 583 | group: groupCounter ++ 584 | }) 585 | childGroup = childContainers[pluginName](options).data(pluginName).group 586 | } 587 | $.data(this.items[index], subContainerKey, childGroup) 588 | } 589 | return childGroup 590 | }, 591 | $getChildren: function (parent, type) { 592 | var options = this.rootGroup.options, 593 | path = options[type + "Path"], 594 | selector = options[type + "Selector"] 595 | 596 | parent = $(parent) 597 | if(path) 598 | parent = parent.find(path) 599 | 600 | return parent.children(selector) 601 | }, 602 | _serialize: function (parent, isContainer) { 603 | var that = this, 604 | childType = isContainer ? "item" : "container", 605 | 606 | children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () { 607 | return that._serialize($(this), !isContainer) 608 | }).get() 609 | 610 | return this.rootGroup.options.serialize(parent, children, isContainer) 611 | }, 612 | traverse: function(callback) { 613 | $.each(this.items || [], function(item){ 614 | var group = $.data(this, subContainerKey) 615 | if(group) 616 | group.traverse(callback) 617 | }); 618 | 619 | callback(this) 620 | }, 621 | _clearDimensions: function () { 622 | this.itemDimensions = undefined 623 | }, 624 | _destroy: function() { 625 | var that = this; 626 | 627 | this.target.off(eventNames.start, this.handle); 628 | this.el.removeData(pluginName) 629 | 630 | if(this.options.drop) 631 | this.group.containers = $.grep(this.group.containers, function(val){ 632 | return val != that 633 | }) 634 | 635 | $.each(this.items || [], function(){ 636 | $.removeData(this, subContainerKey) 637 | }) 638 | } 639 | } 640 | 641 | var API = { 642 | enable: function() { 643 | this.traverse(function(object){ 644 | object.disabled = false 645 | }) 646 | }, 647 | disable: function (){ 648 | this.traverse(function(object){ 649 | object.disabled = true 650 | }) 651 | }, 652 | serialize: function () { 653 | return this._serialize(this.el, true) 654 | }, 655 | refresh: function() { 656 | this.traverse(function(object){ 657 | object._clearDimensions() 658 | }) 659 | }, 660 | destroy: function () { 661 | this.traverse(function(object){ 662 | object._destroy(); 663 | }) 664 | } 665 | } 666 | 667 | $.extend(Container.prototype, API) 668 | 669 | /** 670 | * jQuery API 671 | * 672 | * Parameters are 673 | * either options on init 674 | * or a method name followed by arguments to pass to the method 675 | */ 676 | $.fn[pluginName] = function(methodOrOptions) { 677 | var args = Array.prototype.slice.call(arguments, 1) 678 | 679 | return this.map(function(){ 680 | var $t = $(this), 681 | object = $t.data(pluginName) 682 | 683 | if(object && API[methodOrOptions]) 684 | return API[methodOrOptions].apply(object, args) || this 685 | else if(!object && (methodOrOptions === undefined || 686 | typeof methodOrOptions === "object")) 687 | $t.data(pluginName, new Container($t, methodOrOptions)) 688 | 689 | return this 690 | }); 691 | }; 692 | 693 | }(jQuery, window, 'sortable'); 694 | -------------------------------------------------------------------------------- /telenium/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------