Merge branch 'develop' into asset-repair-refactor
diff --git a/.eslintrc b/.eslintrc
index e40502a..cb45ce5 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,11 +147,14 @@
 		"Chart": true,
 		"Cypress": true,
 		"cy": true,
+		"describe": true,
+		"expect": true,
 		"it": true,
 		"context": true,
 		"before": true,
 		"beforeEach": true,
 		"onScan": true,
-		"extend_cscript": true
+		"extend_cscript": true,
+		"localforage": true,
 	}
 }
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
new file mode 100644
index 0000000..4bc55da
--- /dev/null
+++ b/.github/workflows/ui-tests.yml
@@ -0,0 +1,104 @@
+name: UI
+
+on:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  test:
+    runs-on: ubuntu-18.04
+
+    strategy:
+      fail-fast: false
+
+    name: UI Tests (Cypress)
+
+    services:
+      mysql:
+        image: mariadb:10.3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: YES
+        ports:
+          - 3306:3306
+        options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+
+    steps:
+      - name: Clone
+        uses: actions/checkout@v2
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+
+      - uses: actions/setup-node@v2
+        with:
+          node-version: 14
+          check-latest: true
+
+      - name: Add to Hosts
+        run: |
+          echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
+
+      - name: Cache pip
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+            ${{ runner.os }}-
+
+      - name: Cache node modules
+        uses: actions/cache@v2
+        env:
+          cache-name: cache-node-modules
+        with:
+          path: ~/.npm
+          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+          restore-keys: |
+            ${{ runner.os }}-build-${{ env.cache-name }}-
+            ${{ runner.os }}-build-
+            ${{ runner.os }}-
+
+      - name: Get yarn cache directory path
+        id: yarn-cache-dir-path
+        run: echo "::set-output name=dir::$(yarn cache dir)"
+
+      - uses: actions/cache@v2
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-
+
+      - name: Cache cypress binary
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache
+          key: ${{ runner.os }}-cypress-
+          restore-keys: |
+            ${{ runner.os }}-cypress-
+            ${{ runner.os }}-
+
+      - name: Install
+        run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+        env:
+          DB: mariadb
+          TYPE: ui
+
+      - name: Site Setup
+        run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
+
+      - name: cypress pre-requisites
+        run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile
+
+
+      - name: Build Assets
+        run: cd ~/frappe-bench/ && bench build
+
+      - name: UI Tests
+        run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
+        env:
+          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000..02b10d8
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,11 @@
+{
+  "baseUrl": "http://test_site:8000/",
+  "projectId": "da59y9",
+  "adminPassword": "admin",
+  "defaultCommandTimeout": 20000,
+  "pageLoadTimeout": 15000,
+  "retries": {
+    "runMode": 2,
+    "openMode": 2
+  }
+}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 0000000..da18d93
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+  "name": "Using fixtures to represent data",
+  "email": "hello@cypress.io",
+  "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js
new file mode 100644
index 0000000..3d6ed5d
--- /dev/null
+++ b/cypress/integration/test_customer.js
@@ -0,0 +1,13 @@
+
+context('Customer', () => {
+	before(() => {
+		cy.login();
+	});
+	it('Check Customer Group', () => {
+		cy.visit(`app/customer/`);
+		cy.get('.primary-action').click();
+		cy.wait(500);
+		cy.get('.custom-actions > .btn').click();
+		cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
+	});
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..07d9804
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,17 @@
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+module.exports = () => {
+	// `on` is used to hook into various events Cypress emits
+	// `config` is the resolved Cypress config
+};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..7929a2e
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... });
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
+//
+//
+// -- This is will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..72070cc
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,26 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+import '../../../frappe/cypress/support/commands' // eslint-disable-line
+
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+Cypress.Cookies.defaults({
+	preserve: 'sid'
+});
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 0000000..d90ebf6
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "allowJs": true,
+        "baseUrl": "../node_modules",
+        "types": [
+            "cypress"
+        ]
+    },
+    "include": [
+        "**/*.*"
+    ]
+}
\ No newline at end of file
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index e092b07..dd6d647 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -26,9 +26,6 @@
 
 		self.set_lead_contact(self.raised_by)
 
-		if not self.service_level_agreement:
-			self.reset_sla_fields()
-
 	def on_update(self):
 		# Add a communication in the issue timeline
 		if self.flags.create_communication and self.via_customer_portal:
@@ -54,106 +51,6 @@
 				self.company = frappe.db.get_value("Lead", self.lead, "company") or \
 					frappe.db.get_default("Company")
 
-	def reset_sla_fields(self):
-		self.agreement_status = ""
-		self.response_by = ""
-		self.resolution_by = ""
-		self.response_by_variance = 0
-		self.resolution_by_variance = 0
-
-	def update_status(self):
-		status = frappe.db.get_value("Issue", self.name, "status")
-		if self.status != "Open" and status == "Open" and not self.first_responded_on:
-			self.first_responded_on = frappe.flags.current_time or now_datetime()
-
-		if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
-			self.resolution_date = frappe.flags.current_time or now_datetime()
-			if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
-				set_service_level_agreement_variance(issue=self.name)
-				self.update_agreement_status()
-			set_resolution_time(issue=self)
-			set_user_resolution_time(issue=self)
-
-		if self.status == "Open" and status != "Open":
-			# if no date, it should be set as None and not a blank string "", as per mysql strict config
-			self.resolution_date = None
-			self.reset_issue_metrics()
-			# enable SLA and variance on Reopen
-			self.agreement_status = "Ongoing"
-			set_service_level_agreement_variance(issue=self.name)
-
-		self.handle_hold_time(status)
-
-	def handle_hold_time(self, status):
-		if self.service_level_agreement:
-			# set response and resolution variance as None as the issue is on Hold
-			pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
-				filters={"parent": self.service_level_agreement})
-			hold_statuses = [entry.status for entry in pause_sla_on]
-			update_values = {}
-
-			if hold_statuses:
-				if self.status in hold_statuses and status not in hold_statuses:
-					update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
-					if not self.first_responded_on:
-						update_values['response_by'] = None
-						update_values['response_by_variance'] = 0
-					update_values['resolution_by'] = None
-					update_values['resolution_by_variance'] = 0
-
-				# calculate hold time when status is changed from any hold status to any non-hold status
-				if self.status not in hold_statuses and status in hold_statuses:
-					hold_time = self.total_hold_time if self.total_hold_time else 0
-					now_time = frappe.flags.current_time or now_datetime()
-					last_hold_time = 0
-					if self.on_hold_since:
-						# last_hold_time will be added to the sla variables
-						last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
-						update_values['total_hold_time'] = hold_time + last_hold_time
-
-					# re-calculate SLA variables after issue changes from any hold status to any non-hold status
-					# add hold time to SLA variables
-					start_date_time = get_datetime(self.service_level_agreement_creation)
-					priority = get_priority(self)
-					now_time = frappe.flags.current_time or now_datetime()
-
-					if not self.first_responded_on:
-						response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
-						response_by = add_to_date(response_by, seconds=round(last_hold_time))
-						response_by_variance = round(time_diff_in_seconds(response_by, now_time))
-						update_values['response_by'] = response_by
-						update_values['response_by_variance'] = response_by_variance + last_hold_time
-
-					resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
-					resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
-					resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
-					update_values['resolution_by'] = resolution_by
-					update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
-					update_values['on_hold_since'] = None
-
-				self.db_set(update_values)
-
-	def update_agreement_status(self):
-		if self.service_level_agreement and self.agreement_status == "Ongoing":
-			if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \
-				cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0:
-
-				self.agreement_status = "Failed"
-			else:
-				self.agreement_status = "Fulfilled"
-
-	def update_agreement_status_on_custom_status(self):
-		"""
-			Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
-		"""
-		if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
-			self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
-
-		if not self.resolution_date: # resolution_date set when issue has been closed
-			self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
-
-		self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
-
 	def create_communication(self):
 		communication = frappe.new_doc("Communication")
 		communication.update({
@@ -318,4 +215,4 @@
 def get_holidays(holiday_list_name):
 	holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
 	holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
-	return holidays
+	return holidays
\ No newline at end of file