Browse Source

Reports: Dashboard & Summary

aguryev 3 years ago
parent
commit
a1ac07cc40

+ 1 - 0
1TestResults/.~lock.baangt_baangt_signup.xlsx_20200531_144012.xlsx#

@@ -0,0 +1 @@
+,aguryev,aguryev-VivoBook,31.05.2020 20:25,file:///home/aguryev/.config/libreoffice/4;

+ 1 - 0
1TestResults/.~lock.baangt_example_googleImages.xlsx_20200531_121313.xlsx#

@@ -0,0 +1 @@
+,aguryev,aguryev-VivoBook,31.05.2020 12:14,file:///home/aguryev/.config/libreoffice/4;

+ 1 - 0
1TestResults/.~lock.baangt_paypal_secondform2.xlsx_20200529_231358.xlsx#

@@ -0,0 +1 @@
+,aguryev,aguryev-VivoBook,31.05.2020 20:52,file:///home/aguryev/.config/libreoffice/4;

+ 22 - 5
baangt/base/CliAndInteractive.py

@@ -7,7 +7,7 @@ from PyQt5 import QtWidgets
 # from baangt.ui.pyqt.uimain import MainWindow
 from baangt.ui.pyqt.uimain import MainController
 from baangt.base.RuntimeStatistics import Statistic
-from baangt.reports import Reports
+from baangt.reports import Dashboard, Summary
 
 def print_args():
     print("""
@@ -33,7 +33,9 @@ def args_read(l_search_parameter):
                                                 "globals=",
                                                 "reloadDrivers=",
                                                 "gui=",
-                                                "reports=",
+                                                "name=",
+                                                "stage=",
+                                                "id="
                                                 ])
     except getopt.GetoptError as err_det:
         print("Error in reading parameters:" + str(err_det))
@@ -81,9 +83,24 @@ def run():
         lDriver.downloadDriver(GC.BROWSER_FIREFOX)
         lDriver.downloadDriver(GC.BROWSER_CHROME)
         print("Latest versions of drivers for Firefox and Chrome were downloaded")
-    elif args_read("reports"):
-        r = Reports()
-        r.show_dashboard()
+
+    # Reports
+    elif args_read("name") or args_read("stage"):
+        try:
+            r = Dashboard(name=args_read("name"), stage=args_read("stage"))
+            r.show()
+        except ValueError as e:
+            print(f'ERROR: {e}')
+            sys.exit('Exiting...')
+    
+    elif args_read("id"):
+        try:
+            r = Summary(args_read("id"))
+            r.show()
+        except ValueError as e:
+            print(f'ERROR: {e}')
+            sys.exit('Exiting...')
+        
 
     else:
         app = QtWidgets.QApplication(sys.argv)

+ 2 - 1
baangt/base/GlobalConstants.py

@@ -118,7 +118,6 @@ OS_list_chrome = ['linux32', 'linux64', 'mac64', 'win32']
 GECKO_URL = 'https://api.github.com/repos/mozilla/geckodriver/releases/latest'
 CHROME_URL = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE'
 
-
 BROWSER_PROXY_PATH = '/browsermob-proxy/bin/browsermob-proxy'
 BROWSER_PROXY_URL = 'https://github.com/lightbody/browsermob-proxy/releases/download/browsermob-proxy/browsermob-proxy-bin.zip'
 
@@ -128,3 +127,5 @@ TESTDATAGENERATOR_OUTPUT_FORMAT = "xlsx"
 TESTDATAGENERATOR_INPUTFILE = "RawTestData.xlsx"
 TESTDATAGENERATOR_OUTPUTFILE_XLSX = "output.xlsx"
 TESTDATAGENERATOR_OUTPUTFILE_CSV = "output.csv"
+
+REPORT_PATH = 'reports'

+ 383 - 71
baangt/reports.py

@@ -1,81 +1,117 @@
 from datetime import datetime
 from sqlalchemy.orm import sessionmaker
-from sqlalchemy import desc
-from baangt.base.DataBaseORM import engine, TestrunLog
+from sqlalchemy import desc, and_
+from baangt.base.DataBaseORM import engine, TestrunLog, GlobalAttribute, TestCaseLog, TestCaseSequenceLog, TestCaseField
+import baangt.base.GlobalConstants as GC
 from jinja2 import Environment, FileSystemLoader
 import json
 import os
 import webbrowser
+import uuid
 
+# number of items on history charts
+history_items = 10
+template_dir = 'templates'
 
-class Reports:
-
-	path_to_reports = 'reports'
-
+class Report:
+	#
+	# Parent reports class
+	# defines constants and construcor
+	#
 
 	def __init__(self):
 		self.created = datetime.now()
-		
+		self.generate();
+
 	@property
-	def data(self):
+	def path(self):
+		return ''	
+
+	def generate(self):
 		#
-		# fetches TestrunLogs data
+		# report generator
 		#
 
-		db = sessionmaker(bind=engine)()
+		pass
 
-		data = {}
+	def show(self):
+		#
+		# shows html report
+		#
 
-		# get Testrun names
-		testrun_names = [item.testrunName for 
-			item in db.query(TestrunLog).order_by(TestrunLog.testrunName).group_by(TestrunLog.testrunName).all()]
+		webbrowser.open(f'file://{self.path}', new=2)
 
-		for name in testrun_names:
-			# build items by testruns
-			logs = db.query(TestrunLog).order_by(desc(TestrunLog.startTime)).filter_by(testrunName=name).all()[-10:]
-			empty = max(0, 10-len(logs))
 
-			# build charts
-			charts = {}
+	def template(self, template_name):
+		#
+		# returns jinja2 template
+		#
 
-			charts['figures'] = {
-				'records': logs[-1].recordCount,
-				'successful': logs[-1].statusOk,
-				'error': logs[-1].statusFailed,
-				'paused': logs[-1].statusPaused,
-			}
+		file_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), GC.REPORT_PATH, template_dir))
+		env = Environment(loader=file_loader)
 
-			# status chart
-			charts['status'] = json.dumps({
-				'type': 'doughnut',
-				'data': {
-					'datasets': [{
-						'data': [
-							logs[-1].statusOk,
-							logs[-1].statusFailed,
-							logs[-1].statusPaused,
-						],
-						'backgroundColor': [
-							'#52ff52',
-							'#ff5252',
-							'#bdbdbd',
-						],
-					}],
-					'labels': [
-						'Passed',
-						'Failed',
-						'Paused'
+		return env.get_template(template_name)
+
+
+	def chart_figures(self, log):
+		#
+		# builds json with log statistics
+		#
+
+		return {
+			'records': log.recordCount,
+			'successful': log.statusOk,
+			'error': log.statusFailed,
+			'paused': log.statusPaused,
+		}
+
+	def time_in_seconds(self, time):
+		time_int = time.split('.')[0]
+		factors = [3600, 60, 1]
+		return sum(t*f for t,f in zip(map(int, time_int.split(':')), factors))
+
+	def chart_status(self, log):
+		#
+		# builds json for Chart.js: log statistics
+		#
+
+		return json.dumps({
+			'type': 'doughnut',
+			'data': {
+				'datasets': [{
+					'data': [
+						log.statusOk,
+						log.statusFailed,
+						log.statusPaused,
 					],
+					'backgroundColor': [
+						'#52ff52',
+						'#ff5252',
+						'#bdbdbd',
+					],
+				}],
+				'labels': [
+					'Passed',
+					'Failed',
+					'Paused'
+				],
+			},
+			'options': {
+				'legend': {
+					'display': False,
 				},
-				'options': {
-					'legend': {
-						'display': False,
-					},
-				},
-			})
+			},
+		})
 
-			# results chart
-			charts['results'] = json.dumps({
+	def chart_results(self, logs):
+		#
+		# builds json for Chart.js: history of log results
+		#
+
+		logs = logs[-history_items:]
+		empty = history_items - len(logs)
+
+		return json.dumps({
 			'type': 'bar',
 				'data': {
 					'datasets': [
@@ -130,8 +166,15 @@ class Reports:
 				},
 			})
 
-			# duration chart
-			charts['duration'] = json.dumps({
+	def chart_duration(self, logs):
+		#
+		# builds json for Chart.js: history of log durations
+		#
+
+		logs = logs[-history_items:]
+		empty = history_items - len(logs)
+
+		return json.dumps({
 			'type': 'line',
 				'data': {
 					'datasets': [{
@@ -185,28 +228,297 @@ class Reports:
 			})
 
 
-			data['-'.join(name.split('.'))] = charts
+	def chart_testcases(self, logs):
+		#
+		# builds json for Chart.js: history of log results
+		#
+
+		tc = logs[-history_items:]
+		empty = history_items - len(tc)
+
+		return json.dumps({
+			'type': 'bar',
+				'data': {
+					'datasets': [
+						{
+							'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_SUCCESS else 0 for t in tc] + [None]*empty,
+							'backgroundColor': '#52ff52',
+							'label': 'PASSED',
+						},
+						{
+							'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_ERROR else 0 for t in tc] + [None]*empty,
+							'backgroundColor': '#ff5252',
+							'label': 'FAILED',
+						},
+						{
+							'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_WAITING else 0 for t in tc] + [None]*empty,
+							'backgroundColor': '#bdbdbd',
+							'label': 'PAUSED',
+						},
+					],
+					'labels': [t['duration'] for t in tc] + [None]*empty,
+				},
+				'options': {
+					'legend': {
+						'display': False,
+					},
+					'tooltips': {
+						'mode': 'index',
+						'intersect': False,
+					},
+					'scales': {
+						'xAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+							'stacked': True,
+						}],
+						'yAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+							'stacked': True,
+						}],
+					},
+				},
+			})
 
-		db.close()
 
-		return data
 
+class Dashboard(Report):
 
-	def show_dashboard(self):
+	def __init__(self, name=None, stage=None):
+		self.name = name
+		self.stage = stage
+		super().__init__()
+
+
+	@property
+	def path(self):
 		#
-		# shows html dashboard
+		# path to report
 		#
+		return os.path.join(
+			os.path.dirname(__file__),
+			GC.REPORT_PATH,
+			self.created.strftime('dashboard-%Y%m%d-%H%M%S.html'),
+		)
+
+
+	def generate(self):
+		#
+		# generates HTML report
+		#
+
+		# get jinja2 template
+		template = self.template('dashboard.html')
+
+		data = self.get_data() or None
+		if data is None:
+			error_msg = 'No TestrunLog maches:'
+			if self.name:
+				error_msg = f'{error_msg} Name "{self.name}"'
+				if self.stage:
+					  error_msg = f'{error_msg},'
+			if self.stage:
+				error_msg = f'{error_msg} Stage "{self.stage}"'
+			raise ValueError(error_msg)
+
+		# generate report
+		with open(self.path, 'w') as f:
+			f.write(template.render(
+				type='Dashboard',
+				data=data,
+				created=self.created.strftime('%Y-%m-%d %H:%M:%S'),
+				name=self.name,
+				stage=self.stage,
+			))
+
+
+	def get_data(self):
+		#
+		# get data from db for the report
+		#
+
+		db = sessionmaker(bind=engine)()
+		data = []
+
+		if self.name and self.stage:
+			logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=self.name)\
+				.filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage))).all()
+			return [self.build_charts(logs)]
+		
+		elif self.name:
+			# get Testrun stages
+			stages = db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==self.name))\
+			.filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
+			stages = [x[0] for x in stages]
+
+			for stage in stages:
+				logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=self.name)\
+					.filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
+				data.append(self.build_charts(logs, stage=stage))
+
+			return data
+
+		elif self.stage:
+			# get Testrun names
+			names = db.query(TestrunLog.testrunName)\
+			.filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage)))\
+			.group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
+			names = [x[0] for x in names]
+
+			for name in names:
+				logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
+					.filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage))).all()
+				data.append(self.build_charts(logs, name=name))
+
+			return data
+
+		else:
+			# get Testrun names
+			names = db.query(TestrunLog.testrunName)\
+			.group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
+			names = [x[0] for x in names]
+
+			for name in names:
+				# get Testrun stages
+				stages = db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==name))\
+				.filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
+				stages = [x[0] for x in stages]
+
+				for stage in stages:
+					logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
+						.filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
+					data.append(self.build_charts(logs, name=name, stage=stage))
+
+			return data
+
+
+	def build_charts(self, logs, name=None, stage=None):
+		#
+		# builds Chart.js data collections
+		#
+
+		name = self.name or name
+		stage = self.stage or stage
+		
+		if logs:
+			return {
+				'id': f'{"-".join(name.split("."))}-{stage}',
+				'name': name,
+				'stage': stage,
+				'figures': self.chart_figures(logs[-1]),
+				'status': self.chart_status(logs[-1]),
+				'results': self.chart_results(logs),
+				'duration': self.chart_duration(logs),
+			}
+		else:
+			error_msg = 'No TestrunLog maches:'
+			if self.name:
+				error_msg = f'{error_msg} Name "{self.name}"'
+				if self.stage:
+					  error_msg = f'{error_msg},'
+			if self.stage:
+				error_msg = f'{error_msg} Stage "{self.stage}"'
+			raise ValueError(error_msg)
+
+
+
+class Summary(Report):
+
+	def __init__(self, id):
+		self.id = id
+		super().__init__()
+
+	@property
+	def path(self):
+		#
+		# path to report
+		#
+		return os.path.join(
+			os.path.dirname(__file__),
+			GC.REPORT_PATH,
+			self.created.strftime('summary-%Y%m%d-%H%M%S.html'),
+		)
+
+	def generate(self):
+		#
+		# generates HTML report
+		#
+
+		# get jinja2 template
+		template = self.template('summary.html')
+
+		# generate report
+		with open(self.path, 'w') as f:
+			f.write(template.render(
+				type='Report',
+				data=self.get_data(),
+				created=self.created.strftime('%Y-%m-%d %H:%M:%S'),
+			))
+
+	def get_data(self):
+		#
+		# get data from db for the report
+		#
+
+		db = sessionmaker(bind=engine)()
+
+		log = db.query(TestrunLog).get(uuid.UUID(self.id).bytes)
+		if log is None:
+			raise ValueError(f'TestrunLog {self.id} does not exist')
+
+		# collect TestCases and screenshots
+		testcases = []
+		screenshots = []
+		for index, tc in enumerate(db.query(TestCaseLog).filter(TestCaseLog.testcase_sequence.has(TestCaseSequenceLog.testrun_id == log.id))):
+			testcases.append({
+				'status': db.query(TestCaseField.value).filter_by(name=GC.TESTCASESTATUS).filter(TestCaseField.testcase_id == tc.id).first()[0],
+				'duration': db.query(TestCaseField.value).filter_by(name=GC.TIMING_DURATION).filter(TestCaseField.testcase_id == tc.id).first()[0],
+			})
+			tc_screenshots = db.query(TestCaseField.value).filter_by(name=GC.SCREENSHOTS).filter(TestCaseField.testcase_id == tc.id).first()[0]
+			if tc_screenshots:
+				for shot in json.loads(tc_screenshots.replace("'", '"')):
+					screenshots.append({
+						'index': index + 1,
+						'path': shot,
+					})
+
+		# collect files
+		files = [
+			{
+				'name': 'Log',
+				'path': log.logfileName,
+			},
+			{
+				'name': 'Results',
+				'path': log.dataFile,
+			}
+		]
+
+		data = {
+			'id': self.id,
+			'time': log.startTime.strftime('%Y-%m-%d %H:%M:%S'),
+			'name': log.testrunName,
+			'figures': self.chart_figures(log),
+			'status': self.chart_status(log),
+			'testcases': self.chart_testcases(testcases),
+			'screenshots': screenshots,
+			'files': files,
+		}
+
+		return data
 
-		filename = os.path.join(os.path.dirname(__file__), self.path_to_reports, self.created.strftime('dashboard-%Y%m%d-%H%M%S.html'))
-		self.generate_dashboard(filename)
-		url = f'file://{filename}'
-		webbrowser.open(url, new=2)
 
-	def generate_dashboard(self, filename):
-		file_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), self.path_to_reports, 'templates'))
-		env = Environment(loader=file_loader)
 
-		template = env.get_template('dashboard.html')
 
-		with open(filename, 'w') as f:
-			f.write(template.render(data=self.data, created=self.created.strftime('%Y-%m-%d %H:%M:%S')))
+		

File diff suppressed because it is too large
+ 0 - 10224
baangt/reports/css/bootstrap.css


+ 0 - 89
baangt/reports/css/styles.css

@@ -1,89 +0,0 @@
-body {
-  min-width: 420px;
-}
-
-.hovered-item:hover {
-  background-color: #f5f5f5;
-}
-
-/* chips */
-.chip {
-  display: inline-block;
-  padding: 0 10px;
-  height: 30px;
-  font-size: 16px;
-  /*line-height: 50px;*/
-  border-radius: 15px;
-  background-color: #f1f1f1;
-}
-
-.closebtn {
-  padding-left: 10px;
-  color: #888;
-  font-weight: bold;
-  float: right;
-  font-size: 20px;
-  cursor: pointer;
-}
-
-/* input groups */
-.closebtn:hover {
-  color: #000;
-}
-
-.fixed-label {
-  width: 11rem;
-}
-
-.text-break {
-  word-wrap: break-word;
-}
-
-/* buttons */
-.btn-right {
-  width: 10rem;
-}
-
-/* dialog */
-.dialog {
-  justify-content: center !important;
-}
-
-
-/* widths */
-.w-20 {
-  width: 20%;
-}
-
-.w-40 {
-  width: 40%;
-}
-
-.w-60 {
-  width: 60%;
-}
-
-.w-80 {
-  width: 80%;
-}
-
-/* loading screen */
-#loading {
-  display: none;
-  position: absolute;
-  top: 0;
-  left: 0;
-  z-index: 100;
-  width: 100vw;
-  height: 150vh;
-  background-color: rgba(192, 192, 192, 0.8);
-}
-
-.spinner {
-  width: 3rem;
-  height: 3rem;
-}
-
-.h-80 {
-  height: 80vh;
-}

+ 0 - 32
baangt/reports/js/charts.js

@@ -1,32 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
-    
-    document.querySelectorAll('.hovered-item').forEach(item => {
-        console.log(item.id);
-        // get chart data
-        const request = new XMLHttpRequest();
-        request.responseType = 'json';
-        request.open('POST', `/chart/${item.id}`);
-        request.onload = () => {
-            // customize legend
-            var duration = request.response['duration'];
-            duration['options']['tooltips']['callbacks'] = {
-                label: function(item, data) {
-                    var label = data.datasets[item.datasetIndex].label || '';
-                    if (label) {
-                        label += ': ';
-                    }
-                    label += item.value;
-                    label += ' sec';
-
-                    return label;
-                }
-            };
-            //console.log(results['options']['tooltips']);
-            // draw charts 
-            var statusChart = new Chart(item.querySelector('.statusChart'), request.response['status']);
-            var resultsChart = new Chart(item.querySelector('.resultsChart'), request.response['results']);
-            var durationChart = new Chart(item.querySelector('.durationChart'), duration);
-        };
-        request.send();
-    });
-});

+ 0 - 332
baangt/reports/js/testrun.js

@@ -1,332 +0,0 @@
-
-    // get item
-    function load_item(item_type, item_id) {
-        const request = new XMLHttpRequest();
-        request.open('GET', `/${item_type}/${item_id}`);
-        request.onload = () => {
-            const response = request.responseText;
-            document.querySelector('main').innerHTML = response;
-        };
-        request.send();
-    }
-
-    // create new item
-    function new_item(item_type) {
-        const request = new XMLHttpRequest();
-        request.open('GET', `/${item_type}/new`);
-        request.onload = () => {
-            const response = request.responseText;
-            document.querySelector('main').innerHTML = response;
-        };
-        request.send();
-    }
-
-    // edit item
-    function edit_item(item_type, item_id) {
-        const request = new XMLHttpRequest();
-        request.open('GET', `/${item_type}/${item_id}/edit`);
-        request.onload = () => {
-            const response = request.responseText;
-            document.querySelector('main').innerHTML = response;
-        };
-        request.send();
-    }
-
-    // delete item
-    function set_delete_item(type_name, type, item_id) {
-        /* set data in delete modal */
-        document.querySelector('#deleteLabel').innerHTML = `Delete ${type_name} ID #${item_id}`;
-        const btns = document.querySelector('#deleteButtons');
-        btns.setAttribute('data-type', type);
-        btns.setAttribute('data-id', item_id);
-    }
-
-    function delete_item(e, cascade) {
-        /* delete item */
-        const request = new XMLHttpRequest();
-        if (cascade) {
-            // cascade delete: POST request
-            request.open('POST', `/${e.dataset['type']}/${e.dataset['id']}/delete`);
-        } else {
-            // single item delete: DELETE request
-            request.open('DELETE', `/${e.dataset['type']}/${e.dataset['id']}/delete`);
-        }
-        
-        request.onload = () => {
-            window.location.reload(true); 
-        }
-        request.send();
-    }
-
-    // push message
-    function push_message(msg, type) {
-        // create message
-        var alrt = document.createElement('div');
-        alrt.setAttribute('class', `alert alert-${type} alert-dismissible fade show`);
-        alrt.setAttribute('role', 'alert');
-        alrt.innerHTML = msg;
-        // create close button
-        var btn = document.createElement('button');
-        btn.setAttribute('type', 'button');
-        btn.setAttribute('class', 'close');
-        btn.setAttribute('data-dismiss', 'alert');
-        btn.setAttribute('aria-label', 'Close');
-        btn.innerHTML = `<span aria-hidden="true">&times;</span>`;
-        // add to main
-        alrt.appendChild(btn);
-        document.getElementById('flash_area').appendChild(alrt);
-    }
-
-    // add DataFile
-    function add_datafile() {
-        const message = document.getElementById('upload-message');
-        const modal = document.getElementById('uploadModal');
-        const files = document.getElementById('upload-file').files;
-        message.innerHTML = "";
-        if (files.length > 0) {
-            //console.log(files[0]);
-            const request = new XMLHttpRequest();
-            request.responseType = 'json';
-            request.open('POST', '/datafile/upload');
-            // get file
-            var datafile = new FormData();
-            datafile.append('datafile', files[0]);
-
-            request.onload = () => {
-                if (request.status === 200) {
-                    // add option to datalist
-                    var datalist_option = document.createElement('option');
-                    datalist_option.setAttribute('data-id', request.response['id']);
-                    datalist_option.innerText = request.response['name'];
-                    document.getElementById('datafilesOpt').appendChild(datalist_option);
-                    // add option to multi-select
-                    var select_option = document.createElement('option');
-                    select_option.innerText = request.response['name'];
-                    select_option.setAttribute('value', request.response['id']);
-                    document.getElementById('datafiles').appendChild(select_option);
-                    // message
-                    message.innerHTML = '<span class="text-success">DataFile successfully uploaded</span>'
-                    document.getElementById('upload-label').innerText = 'Choose a file';
-                } else {
-                    // push error message
-                    for (var key in request.response) {
-                        message.innerHTML = `<span class="text-danger">${key.toUpperCase()}: ${request.response[key]}</span>`
-                    }
-                }
-            }
-            request.send(datafile);
-        } else {
-            message.innerHTML = '<span class="text-success">Please select a DataFile</span>';
-        }
-    }
-
-    function add_datafile_new() {
-        const datafiles = document.getElementById('datafiles');
-        const files = document.getElementById('datafile-select').files;
-        const f = document.querySelector('form');
-        var datafile_list = new FormData();
-        if (files.length > 0) {
-            //datafiles.files.push.apply(files[0]);
-            datafile_list.append('file', files[0]);
-            //console.log(datafile_list.keys().length);
-            //console.log(datafile_list.keys().length);
-        } else {
-            push_message('No DataFile selected', 'warning');
-        }
-    }
-
-    function upload_datafile() {
-        //console.log('upload');
-        const datafiles = document.getElementById('datafiles');
-        //document.uploadForm.submit();
-    }
-
-    function create_chip(name, text, id) {
-        var chip_area = document.getElementById(name);
-        var new_chip = document.createElement('div');
-        var onclick_action;
-        new_chip.setAttribute('class', 'chip mr-1');
-        new_chip.setAttribute('data-id', id);
-        if (id >= 0) {        
-            onclick_action = `delete_chip(this.parentElement, '${name}')`;
-        } else {
-            onclick_action = 'this.parentElement.remove()';            
-        }
-
-        new_chip.innerHTML = `
-            <small>${text}</small>
-            <span class="closebtn" onclick="${onclick_action}">&times;</span>
-            `;
-        chip_area.appendChild(new_chip);
-    }
-
-    // select multiple with chips
-    function add_chip(e, name) {
-        // check if option exists
-        const text = e.value;
-        for (var i = 0; i < e.list.childElementCount; i++) {
-            //const opt_text = e.list.children[i].text.toUpperCase();
-            if (text === e.list.children[i].text) {
-                // create chip
-                //create_chip(`chips_${name}`, e.list.children[i].text, i);
-                
-                var chip_area = document.getElementById(`chips_${name}`);
-                var new_chip = document.createElement('div');
-                new_chip.setAttribute('class', 'chip mr-1');
-                new_chip.setAttribute('data-id', e.list.children[i].dataset['id']);
-                new_chip.innerHTML = `
-                    <small>${e.list.children[i].text}</small>
-                    <span class="closebtn" onclick="delete_chip(this.parentElement, '${name}')">&times;</span>
-                    `;
-                chip_area.appendChild(new_chip);
-                
-                // remove selected option from list
-                e.value = null;
-                e.list.children[i].disabled = true;
-                break;
-            }
-        }
-    }
-
-    function delete_chip(e, name) {
-        var list = document.getElementById(`${name}Opt`);
-        for (var i = 0; i < list.childElementCount; i++) {
-            if (e.dataset['id'] === list.children[i].dataset['id']) {
-                list.children[i].disabled = false;
-                e.parentElement.removeChild(e);
-                break;
-            }
-        }
-        
-    }
-
-    function get_chips() {
-        document.querySelectorAll('datalist').forEach(list => {
-            // create multyselect field
-            const name = list.id.substring(0, list.id.length - 3);
-            const selector = document.getElementById(name);
-            const chips_area = document.getElementById(`chips_${name}`);
-            //console.log(name);
-            chips_area.querySelectorAll('div').forEach(chip => {
-                //console.log(chip.dataset['id']);
-                //value = chip.dataset['id'];
-                for (var i = 0; i < selector.length; i++) {
-                    if (chip.dataset['id'] === selector.options[i].value) {
-                        selector.options[i].selected = true;
-                    }
-                }
-            });
-        });
-
-        return false;
-    }
-
-    function filter_options(e) {
-        const text = e.value.toUpperCase();
-        for (var i = 0; i < e.list.childElementCount; i++) {
-            const opt_text = e.list.children[i].text.toUpperCase();
-            if (!opt_text.includes(text)) {
-                e.list.children[i].style.display = opt_text ? 'list-item' : 'none';
-            }
-
-
-        }
-    }
-
-    function filter_items(e) {
-        const text = e.value.toUpperCase();
-        document.querySelectorAll('.testrun-item').forEach(item => {
-            var display = false;
-            item.querySelectorAll('.filtered').forEach(property => {
-                const value = property.innerHTML.toUpperCase();
-                if (value.includes(text)) {
-                    display = true;
-                }
-            });
-
-            item.style.display = display ? '' : 'none';
-
-        });
-    }
-
-
-    function get_file(e) {
-        // get filename
-        const filename = e.files[0].name;
-        const label = e.parentElement.querySelector('label');
-        label.innerText = filename;
-    }
-
-    function set_export_id(item_id) {
-        // set item_id in modal
-        const btn = document.querySelector('#exportButton')
-        btn.setAttribute('data-id', item_id);
-        btn.style.display = '';
-        // set modal body
-        document.querySelector('#exportRequest').style.display = '';
-        document.querySelector('#exportResponse').style.display = 'none';
-    }
-
-    function export_item(e) {
-        /*
-        exports a testrun
-        */
-        // get export format
-        const exportFormat = document.querySelector('input[name="formatRadio"]:checked').value;
-        const request = new XMLHttpRequest();
-        request.open('GET', `/testrun/${exportFormat}/${e.dataset['id']}`);
-        request.onload = () => {
-            // get response
-            const response = request.responseText;
-            // set export modal body 
-            const exportResponse = document.querySelector('#exportResponse');
-            exportResponse.innerHTML = response;
-            exportResponse.style.display = '';
-            document.querySelector('#exportRequest').style.display = 'none';
-            document.querySelector('#exportButton').style.display = 'none';
-        };
-        request.send();
-    }
-
-    function set_update_item(item_id) {
-        document.querySelector('#updateLabel').innerHTML = `Update Testrun ID #${item_id}`;
-        document.querySelector('#updateForm').setAttribute('action', `/testrun/${item_id}/import`);
-    }
-
-    function add_datafile_link(limit) {
-        const active = document.getElementById('activeDataFiles');
-        // display DataFile header
-        if (active.value == "0") {
-            document.getElementById('dataFileHeader').style.display = '';
-        }
-        // display DataFile field
-        document.getElementById(`dataDiv${++active.value}`).style.display = '';
-        // check if DataFile number limit achieved
-        if (active.value == limit) {
-            const link = document.getElementById('dataFileExpander');
-            link.setAttribute('class', 'text-muted');
-            link.setAttribute('onclick', '');
-        } 
-    }
-
-    function run_item(item_id) {
-        /*
-        run the testrun via API web-service
-        */
-        const request = new XMLHttpRequest();
-        request.open('GET', `/testrun/${item_id}/run`);
-        request.onload = () => {
-            // get response
-            const response = request.responseText;
-            //document.querySelector('#loading').style.display = 'none';
-            //alert(response);
-            document.documentElement.innerHTML = response;
-
-        }
-        scroll(0, 0);
-        document.querySelector('#titleLoading').innerHTML = `Running Testrun ID #${item_id}`;
-        document.querySelector('#loading').style.display = 'block';
-        
-        request.send();
-
-    }

+ 22 - 6
baangt/reports/templates/base.html

@@ -8,21 +8,37 @@
     <!-- Bootstrap CSS -->
     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
 
-    <!-- Local CSS -->
-    <link rel="stylesheet" href="css/styles.css">
+    <style>
+        .hovered-item:hover {
+            background-color: #f5f5f5;
+        }
+    </style>
 
     <title>
-        {% block title %}
-        {% endblock %}
+        BAANGT {{ type }}
     </title>
   </head>
   
   <body>
     <nav class="navbar navbar-expand navbar-dark bg-dark flex-md-nowrap p-1 shadow">
         <a class="navbar-brand ml-3 py-2" href="#">
-            {% block header %}
-            {% endblock %}
+            BAANGT {{ type }}
         </a>
+        <ul class="navbar-nav ml-auto mr-3">
+            {% if name %}
+                <li class="nav-item">
+                    <span class="nav-link">{{ name }}</span>
+                </li>
+            {% endif %} 
+            {% if stage %}
+                <li class="nav-item">
+                    <span class="nav-link">Stage: {{ stage }}</span>
+                </li>
+            {% endif %}
+            <li class="nav-item">
+                <span class="nav-link">{{ created }}</span>
+            </li>
+        </ul>
     </nav>
       
     {% block content %}

+ 16 - 19
baangt/reports/templates/dashboard.html

@@ -4,9 +4,6 @@
     BAANGT Dashboard
 {% endblock %}
 
-{% block header %}
-    BAANGT Dashboard {{ created }}
-{% endblock %}
 
 {% block content %}
 
@@ -24,11 +21,11 @@
     </div>
 
     <!-- testrun list -->
-    {% for key, value in data.items() %}
-        <div class="row hovered-item border-top py-3 mx-2" id="{{ key }}">
+    {% for item in data %}
+        <div class="row hovered-item border-top py-3 mx-2" id="{{ item.id }}">
             <div class="col-md-3 text-break">
-                <p class="text-primary">{{ key }}</p>
-                <!--p class="text-secondary">Stage</p--> 
+                <p class="text-primary">{{ item.name }}</p>
+                <p class="text-secondary">{{ item.stage }}</p> 
             </div>
             <div class="col-md-3">
                 <div class="row">
@@ -36,24 +33,24 @@
                         <canvas class="statusChart" width="100" height="100"></canvas>
                     </div>
                     <div class="d-flex flex-column col-2 border-right px-0">
-                        <h2 class="text-center">{{ value.figures.records }}</h2>
+                        <h2 class="text-center">{{ item.figures.records }}</h2>
                         <h6 class="text-center">TESTS</h6>
                     </div>
-                    {% if value.figures.successful > 0  %}
+                    {% if item.figures.successful > 0  %}
                         <div class="d-flex flex-column col-2 text-success px-0">
-                            <h2 class="text-center">{{ value.figures.successful }}</h2>
+                            <h2 class="text-center">{{ item.figures.successful }}</h2>
                             <h6 class="text-center">PASSED</h6>
                         </div>
                     {% endif %}
-                    {% if value.figures.error > 0  %}
+                    {% if item.figures.error > 0  %}
                         <div class="d-flex flex-column col-2 text-danger px-0">
-                            <h2 class="text-center">{{ value.figures.error }}</h2>
+                            <h2 class="text-center">{{ item.figures.error }}</h2>
                             <h6 class="text-center">FAILED</h6>
                         </div>
                     {% endif %}
-                    {% if value.figures.paused > 0  %}
+                    {% if item.figures.paused > 0  %}
                         <div class="d-flex flex-column col-2 text-secondary px-0">
-                            <h2 class="text-center">{{ value.figures.paused }}</h2>
+                            <h2 class="text-center">{{ item.figures.paused }}</h2>
                             <h6 class="text-center">PAUSED</h6>
                         </div>
                     {% endif %}
@@ -74,12 +71,12 @@
 
 <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
 <script type="text/javascript">
-    {% for key, value in data.items() %}
-        var item_{{ loop.index }} = document.querySelector('#{{ key }}')
+    {% for item in data %}
+        var item_{{ loop.index }} = document.querySelector('#{{ item.id }}')
 
-        var status_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.statusChart'), {{ value.status }});
-        var results_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.resultsChart'), {{ value.results }});
-        var duration_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.durationChart'), {{ value.duration }});
+        var status_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.statusChart'), {{ item.status }});
+        var results_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.resultsChart'), {{ item.results }});
+        var duration_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.durationChart'), {{ item.duration }});
     {% endfor %}
 </script>
 

+ 98 - 0
baangt/reports/templates/summary.html

@@ -0,0 +1,98 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+<main class="container">
+
+    <div class="d-flex align-items-center p-3 my-4 rounded shadow">
+        <div class="lh-100">
+            <div class="display-4 text-primary lh-100">{{ data.name }}</div>
+            <div class="mt-1 text-dark">Execution ID: {{ data.id }}</div>
+            <div class="text-secondary">Execution time: {{ data.time }}</div>
+        </div>
+    </div>
+    
+    <!-- Summary -->
+    <div class="display-4 p-2">Summary</div>
+    <div class="card mb-4 shadow">
+        <div class="row no-gutters">
+            <div class="col-md-5">
+                <canvas class="card-img my-3" id="statusChart"></canvas>
+            </div>
+            <div class="col-md-7">
+                <div class="card-body row h-100 align-items-center">
+                    <div class="d-flex flex-column col-3 border-right px-0">
+                        <div class="display-3 text-center">{{ data.figures.records }}</div>
+                        <h4 class="text-center">TESTS</h4>
+                    </div>
+                    {% if data.figures.successful > 0  %}
+                    <div class="d-flex flex-column col-3 text-success px-0">
+                        <div class="display-3 text-center">{{ data.figures.successful }}</div>
+                        <h4 class="text-center">PASSED</h4>
+                    </div>
+                    {% endif %}
+                    {% if data.figures.error > 0  %}
+                    <div class="d-flex flex-column col-3 text-danger px-0">
+                        <div class="display-3 text-center">{{ data.figures.error }}</div>
+                        <h4 class="text-center">FAILED</h4>
+                    </div>
+                    {% endif %}
+                    {% if data.figures.paused > 0  %}
+                    <div class="d-flex flex-column col-3 text-secondary px-0">
+                        <div class="display-3 text-center">{{ data.figures.paused }}</div>
+                        <h4 class="text-center">PAUSED</h4>
+                    </div>
+                    {% endif %}
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- TestCases -->
+    <div class="display-4 p-2">Test Cases</div>
+    <canvas class="rounded border w-100 mb-4 p-2 shadow" height="250" id="testcaseChart"></canvas>
+
+    <!-- Screenshots -->
+    {% if data.screenshots %}
+        <div class="display-4 p-2">Screenshots</div>
+        <div class="row row-cols-1 row-cols-md-3 mb-4">
+        {% for screenshot in data.screenshots %}
+            <div class="col mb-4">
+                <a href="file://{{ screenshot.path }}">
+                    <div class="card shadow">
+                        <img src="file://{{ screenshot.path }}" class="card-img" alt="Test Case {{ screenshot.index }}">
+                        <div class="card-body">
+                            <h5 class="card-title">Test Case {{ screenshot.index }}</h5>
+                        </div>
+                    </div>
+                </a>
+            </div>
+        {% endfor %}
+        </div>
+    {% endif %}
+
+    <!-- Files -->
+    <div class="display-4 p-2">Files</div>
+    <ul class="list-group w-100 mb-5 shadow">
+        {% for file in data.files %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-2 h5">
+                        {{ file.name }}
+                    </div>
+                    <div class="col-10 h6">
+                        <a href="file://{{ file.path }}">{{ file.path }}</a>
+                    </div>
+            </li>
+        {% endfor%}
+    </ul>
+
+</main>
+
+<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
+<script type="text/javascript">
+    var statusChart = new Chart(document.querySelector('#statusChart'), {{ data.status }});
+    var testcaseChart = new Chart(document.querySelector('#testcaseChart'), {{ data.testcases }});
+</script>
+
+{% endblock %}

+ 10 - 0
examples/globals_debug.json

@@ -0,0 +1,10 @@
+{
+    "exportFilesBasePath": "",
+    "Stage": "Debug",
+    "TC.Lines": "",
+    "TC.dontCloseBrowser": "True",
+    "TC.BrowserAttributes": "{'HEADLESS': 'True'}",
+    "TC.slowExecution": "True",
+    "TC.NetworkInfo": "True",
+    "TX.DEBUG": "True"
+}

+ 19 - 0
test_reports.py

@@ -0,0 +1,19 @@
+from baangt.reports import Dashboard, Summary
+
+name = 'example_googleImages.xlsx'
+#name = 'paypal_secondform2.xlsx'
+stage = 'Debug'
+#stage = 'Test'
+
+#d = Dashboard(name=name, stage=stage)
+#d = Dashboard(name=name)
+#d = Dashboard(stage=stage)
+#d = Dashboard()
+#d.show()
+
+#uuid = 'bdf3e0e2-e0e9-4289-810b-e3c773ebb7ce'
+#uuid = 'fd5d3557-1b7f-4995-bc10-73155e5a686e'
+uuid = '56ad6a1f-0641-4b2d-82f2-05e11a84d46c'
+
+r = Summary(uuid)
+r.show()