Merge branch 'develop' into asset-repair-refactor

This commit is contained in:
Saqib 2021-07-02 11:19:47 +05:30 committed by GitHub
commit f9c58934d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 218 additions and 105 deletions

View File

@ -147,11 +147,14 @@
"Chart": true, "Chart": true,
"Cypress": true, "Cypress": true,
"cy": true, "cy": true,
"describe": true,
"expect": true,
"it": true, "it": true,
"context": true, "context": true,
"before": true, "before": true,
"beforeEach": true, "beforeEach": true,
"onScan": true, "onScan": true,
"extend_cscript": true "extend_cscript": true,
"localforage": true,
} }
} }

104
.github/workflows/ui-tests.yml vendored Normal file
View File

@ -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 }}

11
cypress.json Normal file
View File

@ -0,0 +1,11 @@
{
"baseUrl": "http://test_site:8000/",
"projectId": "da59y9",
"adminPassword": "admin",
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000,
"retries": {
"runMode": 2,
"openMode": 2
}
}

View File

@ -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"
}

View File

@ -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');
});
});

17
cypress/plugins/index.js Normal file
View File

@ -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
};

View File

@ -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) => { ... });

26
cypress/support/index.js Normal file
View File

@ -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'
});

12
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

View File

@ -26,9 +26,6 @@ class Issue(Document):
self.set_lead_contact(self.raised_by) self.set_lead_contact(self.raised_by)
if not self.service_level_agreement:
self.reset_sla_fields()
def on_update(self): def on_update(self):
# Add a communication in the issue timeline # Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal: if self.flags.create_communication and self.via_customer_portal:
@ -54,106 +51,6 @@ class Issue(Document):
self.company = frappe.db.get_value("Lead", self.lead, "company") or \ self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company") 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): def create_communication(self):
communication = frappe.new_doc("Communication") communication = frappe.new_doc("Communication")
communication.update({ communication.update({
@ -318,4 +215,4 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals
def get_holidays(holiday_list_name): def get_holidays(holiday_list_name):
holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
holidays = [holiday.holiday_date for holiday in holiday_list.holidays] holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
return holidays return holidays