[ui-tests] python is back! (#3565)

* [ui-tests] python is back!

* [minor] remove old test

* [test] dont test test_runner

* [tests] try firefox

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome 1

* [tests] try chrome 2

* [tests] try chrome 3

* [tests] try phantomJS

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] try chrome

* [tests] login click button

* [tests] login click button

* [tests] show log

* [test] test with start_maximized

* [test] test only login

* [travis] test another port for selenium

* [travis] try running ui tests after unittests are done

* [travis] pring body_div if fails

* [tests] complete setup wizard for frappe

* [minor] move ui tests to frappe/ui/tests

* [tests] ui tests in public and codacy fixes

* [fix] tests + eslint

* [minor] move tests to tests/ui folder and print console after print

* [fix] linting

* [tests] added documentation and better integration testing

* [promise] form triggering is now promise based

* [test]

* [test]

* [test]

* [test]

* [test] print output

* [minor] default empty in select and print console

* [cleanup] more minor fixes

* [enhance] first-cut done!

* [minor] frappe.run_serially to pass arguments while chaining
This commit is contained in:
Rushabh Mehta 2017-07-03 11:53:00 +05:30 committed by GitHub
parent b72dded0f7
commit f409fd7358
52 changed files with 6886 additions and 5727 deletions

View file

@ -117,6 +117,7 @@
"set_field_options": true, "set_field_options": true,
"getCookie": true, "getCookie": true,
"getCookies": true, "getCookies": true,
"get_url_arg": true "get_url_arg": true,
"QUnit": true
} }
} }

View file

@ -2,23 +2,22 @@ language: python
dist: trusty dist: trusty
group: deprecated-2017Q2 group: deprecated-2017Q2
python:
- "2.7"
addons: addons:
apt: apt:
sources: sources:
- google-chrome - google-chrome
packages: packages:
- google-chrome-stable - google-chrome-stable
# sauce_connect:
# username: "rmehta1"
# access_key: "a80640ec-24c8-44ad-9398-1b6f123ae4a1"
python:
- "2.7"
services: services:
- mysql - mysql
before_install:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
install: install:
- sudo apt-get purge -y mysql-common mysql-server mysql-client - sudo apt-get purge -y mysql-common mysql-server mysql-client
- nvm install v7.10.0 - nvm install v7.10.0
@ -31,10 +30,19 @@ install:
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
before_script: before_script:
- wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- sudo apt-get install libnss3
- sudo apt-get --only-upgrade install google-chrome-stable
- sudo cp chromedriver /usr/local/bin/.
- sudo chmod +x /usr/local/bin/chromedriver
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
- mysql -u root -ptravis -e 'create database test_frappe' - mysql -u root -ptravis -e 'create database test_frappe'
- echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis - echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis
- echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis - echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis
- cd ~/frappe-bench - cd ~/frappe-bench
- bench use test_site - bench use test_site
- bench reinstall --yes - bench reinstall --yes
@ -44,5 +52,5 @@ before_script:
script: script:
- set -e - set -e
- bench --verbose run-tests - bench --verbose run-tests
- bench reinstall --yes - sleep 5
- bench run-ui-tests --ci - bench --verbose run-tests --ui-tests

View file

@ -298,11 +298,13 @@ def console(context):
@click.option('--doctype', help="For DocType") @click.option('--doctype', help="For DocType")
@click.option('--test', multiple=True, help="Specific test") @click.option('--test', multiple=True, help="Specific test")
@click.option('--driver', help="For Travis") @click.option('--driver', help="For Travis")
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
@click.option('--module', help="Run tests in a module") @click.option('--module', help="Run tests in a module")
@click.option('--profile', is_flag=True, default=False) @click.option('--profile', is_flag=True, default=False)
@click.option('--junit-xml-output', help="Destination file path for junit xml report") @click.option('--junit-xml-output', help="Destination file path for junit xml report")
@pass_context @pass_context
def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False): def run_tests(context, app=None, module=None, doctype=None, test=(),
driver=None, profile=False, junit_xml_output=False, ui_tests = False):
"Run tests" "Run tests"
import frappe.test_runner import frappe.test_runner
tests = test tests = test
@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None
frappe.init(site=site) frappe.init(site=site)
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
force=context.force, profile=profile, junit_xml_output=junit_xml_output) force=context.force, profile=profile, junit_xml_output=junit_xml_output,
ui_tests = ui_tests)
if len(ret.failures) == 0 and len(ret.errors) == 0: if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0 ret = 0

View file

@ -0,0 +1,63 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Test Runner', {
refresh: (frm) => {
frm.disable_save();
frm.page.set_primary_action(__("Run Tests"), () => {
return new Promise(resolve => {
let wrapper = $(frm.fields_dict.output.wrapper).empty();
$("<p>Loading...</p>").appendTo(wrapper);
// all tests
frappe.call({
method: 'frappe.core.doctype.test_runner.test_runner.get_all_tests'
}).always((data) => {
$("<div id='qunit'></div>").appendTo(wrapper.empty());
frm.events.run_tests(frm, data.message);
resolve();
});
});
});
},
run_tests: function(frm, files) {
let require_list = [
"assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css"
].concat();
frappe.require(require_list, () => {
files.forEach((f) => {
frappe.dom.eval(f.script);
});
// if(frm.doc.module_name) {
// QUnit.module.only(frm.doc.module_name);
// }
QUnit.testDone(function(details) {
var result = {
"Module name": details.module,
"Test name": details.name,
"Assertions": {
"Total": details.total,
"Passed": details.passed,
"Failed": details.failed
},
"Skipped": details.skipped,
"Todo": details.todo,
"Runtime": details.runtime
};
// eslint-disable-next-line
console.log(JSON.stringify(result, null, 2));
});
QUnit.load();
QUnit.done(() => {
frappe.set_route('Form', 'Test Runner', 'Test Runner');
});
});
}
});

View file

@ -0,0 +1,122 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-06-26 10:57:19.976624",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "module_path",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Module Path",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "output",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Output",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-06-26 10:57:19.976624",
"modified_by": "Administrator",
"module": "Core",
"name": "Test Runner",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, os
from frappe.model.document import Document
class TestRunner(Document):
pass
@frappe.whitelist()
def get_all_tests():
tests = []
for app in frappe.get_installed_apps():
tests_path = frappe.get_app_path(app, 'tests', 'ui')
if os.path.exists(tests_path):
for basepath, folders, files in os.walk(tests_path): # pylint: disable=unused-variable
for fname in files:
if fname.startswith('test') and fname.endswith('.js'):
path = os.path.join(basepath, fname)
with open(path, 'r') as fileobj:
tests.append(dict(
path = os.path.relpath(frappe.get_app_path(app), path),
script = fileobj.read()
))
return tests

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View file

@ -0,0 +1,7 @@
# Automated Testing
Frappé Provides you a test framework to write and execute tests that can be run directly on a Continuous Integration Tool like Travis
You can write server-side unit tests or UI tests
{index}

View file

@ -0,0 +1,3 @@
unit-testing
integration-testing
qunit-testing

View file

@ -0,0 +1,49 @@
# UI Integration Testing
You can write integration tests using the Selenium Driver. `frappe.utils.selenium_driver` gives you a friendly API to write selenium based tests
To write integration tests, create a standard test case by creating a python file starting with `test_`
All integration tests will be run at the end of the unittests.
### Example
Here is an example of an integration test to check insertion of a To Do
from __future__ import print_function
from frappe.utils.selenium_testdriver import TestDriver
import unittest
import time
class TestToDo(unittest.TestCase):
def setUp(self):
self.driver = TestDriver()
def test_todo(self):
self.driver.login()
# list view
self.driver.set_route('List', 'ToDo')
time.sleep(2)
# new
self.driver.click_primary_action()
time.sleep(2)
# set input
self.driver.set_text_editor('description', 'hello')
# save
self.driver.click_modal_primary_action()
time.sleep(2)
self.assertTrue(self.driver.get_visible_element('.result-list')
.find_element_by_css_selector('.list-item')
.find_element_by_css_selector('.list-id').text=='hello')
def tearDown(self):
self.driver.close()

View file

@ -0,0 +1,46 @@
# UI Testing with Frappe API
You can either write integration tests, or directly write tests in Javascript using [QUnit](http://api.qunitjs.com/)
QUnit helps you write UI tests using the UQuit framework and native frappe API. As you might have guessed, this is a much faster way of writing tests.
### Test Runner
To write QUnit based tests, add your tests in the `tests/ui` folder of your application. Your test files must begin with `test_` and end with `.js` extension.
To run your files, you can use the **Test Runner**. The **Test Runner** gives a user interface to load all your QUnit tests and run them in the browser.
In the CI, all QUnit tests are run by the **Test Runner** using `frappe/tests/test_test_runner.py`
<img src="{{docs_base_url}}/assets/img/app-development/test-runner.png" class="screenshot">
### Example QUnit Test
Here is the example of the To Do test in QUnit
QUnit.test("test quick entry", function(assert) {
assert.expect(2);
let done = assert.async();
let random = frappe.utils.get_random(10);
frappe.set_route('List', 'ToDo')
.then(() => {
return frappe.new_doc('ToDo');
})
.then(() => {
frappe.quick_entry.dialog.set_value('description', random);
return frappe.quick_entry.insert();
})
.then((doc) => {
assert.ok(doc && !doc.__islocal);
return frappe.set_route('Form', 'ToDo', doc.name);
})
.then(() => {
assert.ok(cur_frm.doc.description.includes(random));
done();
});
});
### Writing Test Friendly Code with Promises
Promises are a great way to write test-friendly code. If your method calls an aysnchronous call (ajax), then you should return an `Promise` object. While writing tests, if you encounter a function that does not return a `Promise` object, you should update the code to return a `Promise` object.

View file

@ -1,4 +1,4 @@
# Writing Tests Guide # Unit Testing
## 1.Introduction ## 1.Introduction
@ -16,12 +16,12 @@ Frappe provides some basic tooling to quickly write automated tests. There are s
This function will build all the test dependencies and run your tests. This function will build all the test dependencies and run your tests.
You should run tests from "frappe_bench" folder. Without options all tests will be run. You should run tests from "frappe_bench" folder. Without options all tests will be run.
bench run-tests bench run-tests
If you need more information about test execution - you can use verbose log level for bench. If you need more information about test execution - you can use verbose log level for bench.
bench --verbose run-tests bench --verbose run-tests
### Options: ### Options:
--app <AppName> --app <AppName>
@ -30,9 +30,9 @@ If you need more information about test execution - you can use verbose log leve
--module <Module> (Run a particular module that has tests) --module <Module> (Run a particular module that has tests)
--profile (Runs a Python profiler on the test) --profile (Runs a Python profiler on the test)
--junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format) --junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format)
#### 2.1. Example for app: #### 2.1. Example for app:
All applications are located in folder: "~/frappe-bench/apps". All applications are located in folder: "~/frappe-bench/apps".
We can run tests for each application. We can run tests for each application.
- frappe-bench/apps/erpnext/ - frappe-bench/apps/erpnext/
@ -50,7 +50,7 @@ We can run tests for each application.
. .
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 1 test in 0.008s Ran 1 test in 0.008s
OK OK
#### 2.3. Example for test: #### 2.3. Example for test:
@ -60,44 +60,44 @@ Run a specific case in User:
. .
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 1 test in 0.005s Ran 1 test in 0.005s
OK OK
#### 2.4. Example for module: #### 2.4. Example for module:
If we want to run tests in the module: If we want to run tests in the module:
/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py /home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py
We should use module name like this (related to application folder) We should use module name like this (related to application folder)
erpnext.support.doctype.issue.test_issue erpnext.support.doctype.issue.test_issue
#####EXAMPLE: #####EXAMPLE:
frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry" frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry"
........................... ...........................
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 27 tests in 30.549s Ran 27 tests in 30.549s
#### 2.5. Example for profile: #### 2.5. Example for profile:
frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile
. .
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 1 test in 0.010s Ran 1 test in 0.010s
OK OK
9133 function calls (8912 primitive calls) in 0.011 seconds 9133 function calls (8912 primitive calls) in 0.011 seconds
Ordered by: cumulative time Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function) ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert) 2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate) 1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql) 13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get) 255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
12 0.000 0.000 0.002 0.000 12 0.000 0.000 0.002 0.000
#### 2.6. Example for XUnit XML: #### 2.6. Example for XUnit XML:
@ -118,7 +118,7 @@ We should use module name like this (related to application folder)
Its designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results. Its designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results.
#### Jenkins configuration support: #### Jenkins configuration support:
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin 1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin
2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report: 2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report:
(Example: _reports/*.xml_) (Example: _reports/*.xml_)
@ -197,9 +197,3 @@ Its designed for the CI Jenkins, but will work for anything else that underst
self.assertTrue("_Test Event 3" in subjects) self.assertTrue("_Test Event 3" in subjects)
self.assertFalse("_Test Event 2" in subjects) self.assertFalse("_Test Event 2" in subjects)
## 4. Client Side Testing (Using Selenium)
This feature is still under development.
For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py)

View file

@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""):
try: try:
if key not in doctype_python_modules: if key not in doctype_python_modules:
doctype_python_modules[key] = frappe.get_module(module_name) doctype_python_modules[key] = frappe.get_module(module_name)
except ImportError: except ImportError, e:
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name)) raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e)))
return doctype_python_modules[key] return doctype_python_modules[key]

View file

@ -1,12 +0,0 @@
var chromedriver = require('chromedriver');
module.exports = {
before: function (done) {
chromedriver.start();
done();
},
after: function (done) {
chromedriver.stop();
done();
}
};

View file

@ -1,96 +0,0 @@
const fs = require('fs');
const ci_mode = get_cli_arg('env') === 'ci_server';
const site_name = get_cli_arg('site');
let app_name = get_cli_arg('app');
if(!app_name) {
console.log('Please specify app to run tests');
return;
}
if(!ci_mode && !site_name) {
console.log('Please specify site to run tests');
return;
}
// site url
let site_url;
if(site_name) {
site_url = 'http://' + site_name + ':' + get_port();
}
// multiple apps
if(app_name.includes(',')) {
app_name = app_name.split(',');
} else {
app_name = [app_name];
}
let test_folders = [];
let page_objects = [];
for(const app of app_name) {
const test_folder = `apps/${app}/${app}/tests/ui`;
const page_object = test_folder + '/page_objects';
if(!fs.existsSync(test_folder)) {
console.log(`No test folder found for "${app}"`);
continue;
}
test_folders.push(test_folder);
if(fs.existsSync(page_object)) {
page_objects.push();
}
}
const config = {
"src_folders": test_folders,
"globals_path" : 'apps/frappe/frappe/nightwatch.global.js',
"page_objects_path": page_objects,
"selenium": {
"start_process": false
},
"test_settings": {
"default": {
"launch_url": site_url,
"selenium_port": 9515,
"selenium_host": "127.0.0.1",
"default_path_prefix": "",
"silent": true,
// "screenshots": {
// "enabled": true,
// "path": SCREENSHOT_PATH
// },
"globals": {
"waitForConditionTimeout": 15000
},
"desiredCapabilities": {
"browserName": "chrome",
"chromeOptions": {
"args": ["--no-sandbox", "--start-maximized"]
},
"javascriptEnabled": true,
"acceptSslCerts": true
}
},
"ci_server": {
"launch_url": 'http://localhost:8000'
}
}
}
module.exports = config;
function get_cli_arg(key) {
var args = process.argv;
var i = args.indexOf('--' + key);
if(i === -1) {
return null;
}
return args[i + 1];
}
function get_port() {
var bench_config = JSON.parse(fs.readFileSync('sites/common_site_config.json'));
return bench_config.webserver_port;
}

View file

@ -101,6 +101,7 @@
"public/js/frappe/ui/page.html", "public/js/frappe/ui/page.html",
"public/js/frappe/ui/page.js", "public/js/frappe/ui/page.js",
"public/js/frappe/ui/find.js",
"public/js/frappe/ui/iconbar.js", "public/js/frappe/ui/iconbar.js",
"public/js/frappe/form/layout.js", "public/js/frappe/form/layout.js",
"public/js/frappe/ui/field_group.js", "public/js/frappe/ui/field_group.js",
@ -194,6 +195,8 @@
"public/js/frappe/form/save.js", "public/js/frappe/form/save.js",
"public/js/frappe/form/script_manager.js", "public/js/frappe/form/script_manager.js",
"public/js/frappe/form/grid.js", "public/js/frappe/form/grid.js",
"public/js/frappe/form/grid_row.js",
"public/js/frappe/form/grid_row_form.js",
"public/js/frappe/form/linked_with.js", "public/js/frappe/form/linked_with.js",
"public/js/frappe/form/workflow.js", "public/js/frappe/form/workflow.js",
"public/js/frappe/form/print.js", "public/js/frappe/form/print.js",

View file

@ -195,6 +195,23 @@ frappe.ellipsis = function(text, max) {
return text; return text;
}; };
frappe.run_serially = function(tasks) {
var result = Promise.resolve();
tasks.forEach(task => {
if(task) {
result = result.then ? result.then(task) : Promise.resolve();
}
});
return result;
};
frappe.timeout = seconds => {
return new Promise((resolve) => {
setTimeout(() => resolve(), seconds * 1000);
});
};
frappe.get_modal = function(title, content) { frappe.get_modal = function(title, content) {
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body);
}; };

View file

@ -98,29 +98,44 @@ frappe.ui.form.Control = Class.extend({
} }
}, },
set_value: function(value) { set_value: function(value) {
this.parse_validate_and_set_in_model(value); return this.parse_validate_and_set_in_model(value);
}, },
parse_validate_and_set_in_model: function(value, e) { parse_validate_and_set_in_model: function(value, e) {
var me = this; var me = this;
if(this.inside_change_event) return; return new Promise(resolve => {
this.inside_change_event = true; if(this.inside_change_event) {
if(this.parse) value = this.parse(value); resolve();
return;
var set = function(value) { }
me.set_model_value(value); this.inside_change_event = true;
me.inside_change_event = false; if(this.parse) {
me.set_mandatory && me.set_mandatory(value); value = this.parse(value);
if(me.df.change || me.df.onchange) {
// onchange event specified in df
(me.df.change || me.df.onchange).apply(me, [e]);
} }
}
this.validate ? this.validate(value, set) : set(value); var set = function(value) {
me.inside_change_event = false;
me.set_model_value(value)
.then(() => {
me.set_mandatory && me.set_mandatory(value);
if(me.df.change || me.df.onchange) {
// onchange event specified in df
let _promise = (me.df.change || me.df.onchange).apply(me, [e]);
if(_promise && _promise.then) {
_promise.then(() => { resolve(); });
} else {
resolve();
}
} else {
resolve();
}
});
}
this.validate ? this.validate(value, set) : set(value);
})
}, },
get_parsed_value: function() { get_parsed_value: function() {
var me = this;
if(this.get_status()==='Write') { if(this.get_status()==='Write') {
return this.get_value ? return this.get_value ?
(this.parse ? this.parse(this.get_value()) : this.get_value()) : (this.parse ? this.parse(this.get_value()) : this.get_value()) :
@ -132,17 +147,20 @@ frappe.ui.form.Control = Class.extend({
} }
}, },
set_model_value: function(value) { set_model_value: function(value) {
if(this.doctype && this.docname) { return new Promise(resolve => {
if(frappe.model.set_value(this.doctype, this.docname, this.df.fieldname, if(this.doctype && this.docname) {
value, this.df.fieldtype)) { frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
value, this.df.fieldtype)
.then(() => resolve());
this.last_value = value; this.last_value = value;
} else {
if(this.doc) {
this.doc[this.df.fieldname] = value;
}
this.set_input(value);
resolve();
} }
} else { });
if(this.doc) {
this.doc[this.df.fieldname] = value;
}
this.set_input(value);
}
}, },
set_focus: function() { set_focus: function() {
if(this.$input) { if(this.$input) {
@ -878,7 +896,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
}, },
onclick: function() { onclick: function() {
if(this.frm && this.frm.doc) { if(this.frm && this.frm.doc) {
if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) { if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname); this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
} else { } else {
this.frm.runscript(this.df.options, this); this.frm.runscript(this.df.options, this);
@ -1290,14 +1308,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
frappe._from_link = this; frappe._from_link = this;
frappe._from_link_scrollY = $(document).scrollTop(); frappe._from_link_scrollY = $(document).scrollTop();
var trimmed_doctype = doctype.replace(/ /g, ''); frappe.ui.form.make_quick_entry(doctype, (doc) => {
var controller_name = "QuickEntryForm";
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
controller_name = trimmed_doctype + "QuickEntryForm";
}
new frappe.ui.form[controller_name](doctype, function(doc) {
if(me.frm) { if(me.frm) {
me.parse_validate_and_set_in_model(doc.name); me.parse_validate_and_set_in_model(doc.name);
} else { } else {

View file

@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({
select_row: function(name) { select_row: function(name) {
this.grid_rows_by_docname[name].select(); this.grid_rows_by_docname[name].select();
}, },
remove_all: function() {
this.grid_rows.forEach(row => {
row.remove();
});
},
refresh_remove_rows_button: function() { refresh_remove_rows_button: function() {
this.remove_rows_button.toggleClass('hide', this.remove_rows_button.toggleClass('hide',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({
if (this.frm && this.frm.docname) { if (this.frm && this.frm.docname) {
// use doc specific docfield object // use doc specific docfield object
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname,
this.frm.docname); this.frm.docname);
} else { } else {
// use non-doc specific docfield // use non-doc specific docfield
if(this.df.options) { if(this.df.options) {
@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({
get_docfield: function(fieldname) { get_docfield: function(fieldname) {
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null);
}, },
get_grid_row: function(docname) { get_row: function(key) {
return this.grid_rows_by_docname[docname]; if(typeof key == 'number') {
if(key < 0) {
return this.grid_rows[this.grid_rows.length + key];
} else {
return this.grid_rows[key];
}
} else {
return this.grid_rows_by_docname[key];
}
},
get_grid_row: function(key) {
return this.get_row(key);
}, },
get_field: function(fieldname) { get_field: function(fieldname) {
// Note: workaround for get_query // Note: workaround for get_query
@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) && (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm)
&& !in_list(frappe.model.layout_fields, df.fieldtype)) { && !in_list(frappe.model.layout_fields, df.fieldtype)) {
if(df.columns) { if(df.columns) {
df.colsize=df.columns; df.colsize=df.columns;
} }
else { else {
var colsize=2; var colsize=2;
switch(df.fieldtype){ switch(df.fieldtype) {
case"Text": case"Text":
case"Small Text": case"Small Text":
colsize=3; colsize=3;
break; break;
case"Check": case"Check":
colsize=1 colsize=1
}
df.colsize=colsize
} }
df.colsize=colsize;
}
if(df.columns) { if(df.columns) {
df.colsize=df.columns; df.colsize=df.columns;
@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({
// hide all custom buttons // hide all custom buttons
this.grid_buttons.find('.btn-custom').addClass('hidden'); this.grid_buttons.find('.btn-custom').addClass('hidden');
} }
}); });
frappe.ui.form.GridRow = Class.extend({
init: function(opts) {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.columns = {};
this.columns_list = [];
$.extend(this, opts);
this.make();
},
make: function() {
var me = this;
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
.on("click", function(e) {
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
return;
}
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
// pass
} else {
me.toggle_view();
return false;
}
});
// no checkboxes if too small
// if(this.is_too_small()) {
// this.row_check_html = '';
// }
if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row();
}
if(this.doc) {
this.set_data();
}
},
set_data: function() {
this.wrapper.data({
"doc": this.doc
})
},
set_row_index: function() {
if(this.doc) {
this.wrapper
.attr('data-name', this.doc.name)
.attr("data-idx", this.doc.idx)
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)
}
},
select: function(checked) {
this.doc.__checked = checked ? 1 : 0;
},
refresh_check: function() {
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
this.grid.refresh_remove_rows_button();
},
remove: function() {
var me = this;
if(this.grid.is_editable()) {
if(this.frm) {
if(this.get_open_form()) {
this.hide_form();
}
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
//this.wrapper.toggle(false);
frappe.model.clear_doc(this.doc.doctype, this.doc.name);
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
this.frm.dirty();
} else {
this.grid.df.data = this.grid.df.data.filter(function(d) {
return d.name !== me.doc.name;
})
// remap idxs
this.grid.df.data.forEach(function(d, i) {
d.idx = i+1;
});
}
this.grid.refresh();
}
},
insert: function(show, below) {
var idx = this.doc.idx;
if(below) idx ++;
this.toggle_view(false);
this.grid.add_new_row(idx, null, show);
},
refresh: function() {
if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name];
}
// re write columns
this.visible_columns = null;
if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row(true);
}
// refersh form fields
if(this.grid_form) {
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
}
},
render_template: function() {
this.set_row_index();
if(this.row_display) {
this.row_display.remove();
}
var index_html = '';
// row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
this.row_index.find('span').html(this.doc.idx);
}
this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
.html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm,
row: this
}));
},
render_row: function(refresh) {
var me = this;
this.set_row_index();
// index (1, 2, 3 etc)
if(!this.row_index) {
var txt = (this.doc ? this.doc.idx : "&nbsp;");
this.row_index = $(
`<div class="row-index sortable-handle col col-xs-1">
${this.row_check_html}
<span>${txt}</span></div>`)
.appendTo(this.row)
.on('click', function(e) {
if(!$(e.target).hasClass('grid-row-check')) {
me.toggle_view();
}
});
} else {
this.row_index.find('span').html(txt);
}
this.setup_columns();
this.add_open_form_button();
this.refresh_check();
if(this.frm && this.doc) {
$(this.frm.wrapper).trigger("grid-row-render", [this]);
}
},
make_editable: function() {
this.row.toggleClass('editable-row', this.grid.is_editable());
},
is_too_small: function() {
return this.row.width() ? this.row.width() < 300 : false;
},
add_open_form_button: function() {
var me = this;
if(this.doc && !this.grid.df.in_place_edit) {
// remove row
if(!this.open_form_button) {
this.open_form_button = $('<a class="close btn-open-row">\
<span class="octicon octicon-triangle-down"></span></a>')
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
.on('click', function() { me.toggle_view(); return false; });
if(this.is_too_small()) {
// narrow
this.open_form_button.css({'margin-right': '-2px'});
}
}
}
},
setup_columns: function() {
var me = this;
this.focus_set = false;
this.grid.setup_visible_columns();
for(var ci in this.grid.visible_columns) {
var df = this.grid.visible_columns[ci][0],
colsize = this.grid.visible_columns[ci][1],
txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);
if(this.doc && df.fieldtype === "Select") {
txt = __(txt);
}
if(!this.columns[df.fieldname]) {
var column = this.make_column(df, colsize, txt, ci);
} else {
var column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
}
// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
column.addClass('bold');
}
}
}
},
make_column: function(df, colsize, txt, ci) {
var me = this;
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
" grid-overflow-no-ellipsis" : "");
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
" text-right": "";
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
" text-center": "";
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
.data("df", df)
.appendTo(this.row)
.on('click', function() {
if(frappe.ui.form.editable_row===me) {
return;
}
var out = me.toggle_editable_row();
var col = this;
setTimeout(function() {
$(col).find('input[type="Text"]:first').focus();
}, 500);
return out;
});
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
$col.df = df;
$col.column_index = ci;
this.columns[df.fieldname] = $col;
this.columns_list.push($col);
return $col;
},
toggle_editable_row: function(show) {
var me = this;
// show static for field based on
// whether grid is editable
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {
// disable other editale row
if(frappe.ui.form.editable_row
&& frappe.ui.form.editable_row !== this) {
frappe.ui.form.editable_row.toggle_editable_row(false);
}
this.row.toggleClass('editable-row', true);
// setup controls
this.columns_list.forEach(function(column) {
me.make_control(column);
column.static_area.toggle(false);
column.field_area.toggle(true);
});
frappe.ui.form.editable_row = this;
return false;
} else {
this.row.toggleClass('editable-row', false);
this.columns_list.forEach(function(column) {
column.static_area.toggle(true);
column.field_area && column.field_area.toggle(false);
});
frappe.ui.form.editable_row = null;
}
},
make_control: function(column) {
if(column.field) return;
var me = this,
parent = column.field_area,
df = column.df;
// no text editor in grid
if (df.fieldtype=='Text Editor') {
df.fieldtype = 'Text';
}
var field = frappe.ui.form.make_control({
df: df,
parent: parent,
only_input: true,
with_link_btn: true,
doc: this.doc,
doctype: this.doc.doctype,
docname: this.doc.name,
frm: this.grid.frm,
grid: this.grid,
grid_row: this,
value: this.doc[df.fieldname]
});
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
field.refresh();
if(field.$input) {
field.$input
.addClass('input-sm')
.attr('data-col-idx', column.column_index)
.attr('placeholder', __(df.label));
// flag list input
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
field.$input.attr('data-last-input', 1);
}
}
this.set_arrow_keys(field);
column.field = field;
this.on_grid_fields_dict[df.fieldname] = field;
this.on_grid_fields.push(field);
},
set_arrow_keys: function(field) {
var me = this;
if(field.$input) {
field.$input.on('keydown', function(e) {
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
return;
}
var values = me.grid.get_data();
var fieldname = $(this).attr('data-fieldname');
var fieldtype = $(this).attr('data-fieldtype');
var move_up_down = function(base) {
if(in_list(['Text', 'Small Text'], fieldtype)) {
return;
}
base.toggle_editable_row();
setTimeout(function() {
var input = base.columns[fieldname].field.$input;
if(input) {
input.focus();
}
}, 400)
}
// TAB
if(e.which==TAB && !e.shiftKey) {
// last column
if($(this).attr('data-last-input') ||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
setTimeout(function() {
if(me.doc.idx === values.length) {
// last row
me.grid.add_new_row(null, null, true);
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
me.grid.set_focus_on_row();
} else {
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
me.grid.set_focus_on_row(me.doc.idx+1);
}
}, 500);
}
} else if(e.which==UP_ARROW) {
if(me.doc.idx > 1) {
var prev = me.grid.grid_rows[me.doc.idx-2];
move_up_down(prev);
}
} else if(e.which==DOWN_ARROW) {
if(me.doc.idx < values.length) {
var next = me.grid.grid_rows[me.doc.idx];
move_up_down(next);
}
}
});
}
},
get_open_form: function() {
return frappe.ui.form.get_open_grid_form();
},
toggle_view: function(show, callback) {
if(!this.doc) {
return this;
}
if(this.frm) {
// reload doc
this.doc = locals[this.doc.doctype][this.doc.name];
}
// hide other
var open_row = this.get_open_form();
if (show===undefined) show = !!!open_row;
// call blur
document.activeElement && document.activeElement.blur();
if(show && open_row) {
if(open_row==this) {
// already open, do nothing
callback && callback();
return;
} else {
// close other views
open_row.toggle_view(false);
}
}
if(show) {
this.show_form();
} else {
this.hide_form();
}
callback && callback();
return this;
},
show_form: function() {
if(!this.grid_form) {
this.grid_form = new frappe.ui.form.GridRowForm({
row: this
});
}
this.grid_form.render();
this.row.toggle(false);
// this.form_panel.toggle(true);
frappe.dom.freeze("", "dark");
cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
frappe.utils.scroll_to(this.wrapper, true, 15);
}
if(this.frm) {
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
}
},
hide_form: function() {
frappe.dom.unfreeze();
this.row.toggle(true);
this.refresh();
cur_frm.cur_grid = null;
this.wrapper.removeClass("grid-row-open");
},
open_prev: function() {
if(this.grid.grid_rows[this.doc.idx-2]) {
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
}
},
open_next: function() {
if(this.grid.grid_rows[this.doc.idx]) {
this.grid.grid_rows[this.doc.idx].toggle_view(true);
} else {
this.grid.add_new_row(null, null, true);
}
},
refresh_field: function(fieldname, txt) {
var df = this.grid.get_docfield(fieldname) || undefined;
// format values if no frm
if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
}
if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}
// reset static value
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
if(df && df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
}
}
// reset field value
var field = this.on_grid_fields_dict[fieldname];
if(field) {
field.docname = this.doc.name;
field.refresh();
}
// in form
if(this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
},
get_visible_columns: function(blacklist) {
var me = this;
var visible_columns = $.map(this.docfields, function(df) {
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);
return visible ? df : null;
});
return visible_columns;
},
set_field_property: function(fieldname, property, value) {
// set a field property for open form / grid form
var me = this;
var set_property = function(field) {
if(!field) return;
field.df[property] = value;
field.refresh();
}
// set property in grid form
if(this.grid_form) {
set_property(this.grid_form.fields_dict[fieldname]);
this.grid_form.layout && this.grid_form.layout.refresh_sections();
}
// set property in on grid fields
set_property(this.on_grid_fields_dict[fieldname]);
},
toggle_reqd: function(fieldname, reqd) {
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
},
toggle_display: function(fieldname, show) {
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
},
toggle_editable: function(fieldname, editable) {
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
},
});
frappe.ui.form.GridRowForm = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.wrapper = $('<div class="form-in-grid"></div>')
.appendTo(this.row.wrapper);
},
render: function() {
var me = this;
this.make_form();
this.form_area.empty();
this.layout = new frappe.ui.form.Layout({
fields: this.row.docfields,
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
});
this.layout.make();
this.fields = this.layout.fields;
this.fields_dict = this.layout.fields_dict;
this.layout.refresh(this.row.doc);
// copy get_query to fields
for(var fieldname in (this.row.grid.fieldinfo || {})) {
var fi = this.row.grid.fieldinfo[fieldname];
$.extend(me.fields_dict[fieldname], fi);
}
this.toggle_add_delete_button_display(this.wrapper);
this.row.grid.open_grid_row = this;
this.set_focus();
},
make_form: function() {
if(!this.form_area) {
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
this.form_area = this.wrapper.find(".form-area");
this.row.set_row_index();
this.set_form_events();
}
},
set_form_events: function() {
var me = this;
this.wrapper.find(".grid-delete-row")
.on('click', function() {
me.row.remove(); return false;
});
this.wrapper.find(".grid-insert-row")
.on('click', function() {
me.row.insert(true); return false;
});
this.wrapper.find(".grid-insert-row-below")
.on('click', function() {
me.row.insert(true, true); return false;
});
this.wrapper.find(".grid-append-row")
.on('click', function() {
me.row.toggle_view(false);
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
return false;
});
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
me.row.toggle_view();
return false;
});
},
toggle_add_delete_button_display: function($parent) {
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
.toggle(this.row.grid.is_editable());
},
refresh_field: function(fieldname) {
if(this.fields_dict[fieldname]) {
this.fields_dict[fieldname].refresh();
this.layout && this.layout.refresh_dependency();
}
},
set_focus: function() {
// wait for animation and then focus on the first row
var me = this;
setTimeout(function() {
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
var first = me.form_area.find("input:first");
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
try {
first.get(0).focus();
} catch(e) {
//
}
}
}
}, 500);
},
});

View file

@ -0,0 +1,586 @@
frappe.ui.form.GridRow = Class.extend({
init: function(opts) {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.columns = {};
this.columns_list = [];
$.extend(this, opts);
this.make();
},
make: function() {
var me = this;
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
.on("click", function(e) {
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
return;
}
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
// pass
} else {
me.toggle_view();
return false;
}
});
// no checkboxes if too small
// if(this.is_too_small()) {
// this.row_check_html = '';
// }
if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row();
}
if(this.doc) {
this.set_data();
}
},
set_data: function() {
this.wrapper.data({
"doc": this.doc
})
},
set_row_index: function() {
if(this.doc) {
this.wrapper
.attr('data-name', this.doc.name)
.attr("data-idx", this.doc.idx)
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)
}
},
select: function(checked) {
this.doc.__checked = checked ? 1 : 0;
},
refresh_check: function() {
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
this.grid.refresh_remove_rows_button();
},
remove: function() {
var me = this;
if(this.grid.is_editable()) {
if(this.frm) {
if(this.get_open_form()) {
this.hide_form();
}
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
//this.wrapper.toggle(false);
frappe.model.clear_doc(this.doc.doctype, this.doc.name);
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
this.frm.dirty();
} else {
this.grid.df.data = this.grid.df.data.filter(function(d) {
return d.name !== me.doc.name;
})
// remap idxs
this.grid.df.data.forEach(function(d, i) {
d.idx = i+1;
});
}
this.grid.refresh();
}
},
insert: function(show, below) {
var idx = this.doc.idx;
if(below) idx ++;
this.toggle_view(false);
this.grid.add_new_row(idx, null, show);
},
refresh: function() {
if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name];
}
// re write columns
this.visible_columns = null;
if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row(true);
}
// refersh form fields
if(this.grid_form) {
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
}
},
render_template: function() {
this.set_row_index();
if(this.row_display) {
this.row_display.remove();
}
var index_html = '';
// row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
this.row_index.find('span').html(this.doc.idx);
}
this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
.html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm,
row: this
}));
},
render_row: function(refresh) {
var me = this;
this.set_row_index();
// index (1, 2, 3 etc)
if(!this.row_index) {
var txt = (this.doc ? this.doc.idx : "&nbsp;");
this.row_index = $(
`<div class="row-index sortable-handle col col-xs-1">
${this.row_check_html}
<span>${txt}</span></div>`)
.appendTo(this.row)
.on('click', function(e) {
if(!$(e.target).hasClass('grid-row-check')) {
me.toggle_view();
}
});
} else {
this.row_index.find('span').html(txt);
}
this.setup_columns();
this.add_open_form_button();
this.refresh_check();
if(this.frm && this.doc) {
$(this.frm.wrapper).trigger("grid-row-render", [this]);
}
},
make_editable: function() {
this.row.toggleClass('editable-row', this.grid.is_editable());
},
is_too_small: function() {
return this.row.width() ? this.row.width() < 300 : false;
},
add_open_form_button: function() {
var me = this;
if(this.doc && !this.grid.df.in_place_edit) {
// remove row
if(!this.open_form_button) {
this.open_form_button = $('<a class="close btn-open-row">\
<span class="octicon octicon-triangle-down"></span></a>')
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
.on('click', function() { me.toggle_view(); return false; });
if(this.is_too_small()) {
// narrow
this.open_form_button.css({'margin-right': '-2px'});
}
}
}
},
setup_columns: function() {
var me = this;
this.focus_set = false;
this.grid.setup_visible_columns();
for(var ci in this.grid.visible_columns) {
var df = this.grid.visible_columns[ci][0],
colsize = this.grid.visible_columns[ci][1],
txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);
if(this.doc && df.fieldtype === "Select") {
txt = __(txt);
}
if(!this.columns[df.fieldname]) {
var column = this.make_column(df, colsize, txt, ci);
} else {
var column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
}
// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
column.addClass('bold');
}
}
}
},
make_column: function(df, colsize, txt, ci) {
var me = this;
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
" grid-overflow-no-ellipsis" : "");
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
" text-right": "";
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
" text-center": "";
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
.data("df", df)
.appendTo(this.row)
.on('click', function() {
if(frappe.ui.form.editable_row===me) {
return;
}
var out = me.toggle_editable_row();
var col = this;
setTimeout(function() {
$(col).find('input[type="Text"]:first').focus();
}, 500);
return out;
});
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
$col.df = df;
$col.column_index = ci;
this.columns[df.fieldname] = $col;
this.columns_list.push($col);
return $col;
},
activate: function() {
this.toggle_editable_row(true);
return this;
},
toggle_editable_row: function(show) {
var me = this;
// show static for field based on
// whether grid is editable
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {
// disable other editale row
if(frappe.ui.form.editable_row
&& frappe.ui.form.editable_row !== this) {
frappe.ui.form.editable_row.toggle_editable_row(false);
}
this.row.toggleClass('editable-row', true);
// setup controls
this.columns_list.forEach(function(column) {
me.make_control(column);
column.static_area.toggle(false);
column.field_area.toggle(true);
});
frappe.ui.form.editable_row = this;
return false;
} else {
this.row.toggleClass('editable-row', false);
this.columns_list.forEach(function(column) {
column.static_area.toggle(true);
column.field_area && column.field_area.toggle(false);
});
frappe.ui.form.editable_row = null;
}
},
make_control: function(column) {
if(column.field) return;
var me = this,
parent = column.field_area,
df = column.df;
// no text editor in grid
if (df.fieldtype=='Text Editor') {
df.fieldtype = 'Text';
}
var field = frappe.ui.form.make_control({
df: df,
parent: parent,
only_input: true,
with_link_btn: true,
doc: this.doc,
doctype: this.doc.doctype,
docname: this.doc.name,
frm: this.grid.frm,
grid: this.grid,
grid_row: this,
value: this.doc[df.fieldname]
});
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
field.refresh();
if(field.$input) {
field.$input
.addClass('input-sm')
.attr('data-col-idx', column.column_index)
.attr('placeholder', __(df.label));
// flag list input
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
field.$input.attr('data-last-input', 1);
}
}
this.set_arrow_keys(field);
column.field = field;
this.on_grid_fields_dict[df.fieldname] = field;
this.on_grid_fields.push(field);
},
set_arrow_keys: function(field) {
var me = this;
if(field.$input) {
field.$input.on('keydown', function(e) {
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
return;
}
var values = me.grid.get_data();
var fieldname = $(this).attr('data-fieldname');
var fieldtype = $(this).attr('data-fieldtype');
var move_up_down = function(base) {
if(in_list(['Text', 'Small Text'], fieldtype)) {
return;
}
base.toggle_editable_row();
setTimeout(function() {
var input = base.columns[fieldname].field.$input;
if(input) {
input.focus();
}
}, 400)
}
// TAB
if(e.which==TAB && !e.shiftKey) {
// last column
if($(this).attr('data-last-input') ||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
setTimeout(function() {
if(me.doc.idx === values.length) {
// last row
me.grid.add_new_row(null, null, true);
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
me.grid.set_focus_on_row();
} else {
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
me.grid.set_focus_on_row(me.doc.idx+1);
}
}, 500);
}
} else if(e.which==UP_ARROW) {
if(me.doc.idx > 1) {
var prev = me.grid.grid_rows[me.doc.idx-2];
move_up_down(prev);
}
} else if(e.which==DOWN_ARROW) {
if(me.doc.idx < values.length) {
var next = me.grid.grid_rows[me.doc.idx];
move_up_down(next);
}
}
});
}
},
get_open_form: function() {
return frappe.ui.form.get_open_grid_form();
},
toggle_view: function(show, callback) {
if(!this.doc) {
return this;
}
if(this.frm) {
// reload doc
this.doc = locals[this.doc.doctype][this.doc.name];
}
// hide other
var open_row = this.get_open_form();
if (show===undefined) show = !!!open_row;
// call blur
document.activeElement && document.activeElement.blur();
if(show && open_row) {
if(open_row==this) {
// already open, do nothing
callback && callback();
return;
} else {
// close other views
open_row.toggle_view(false);
}
}
if(show) {
this.show_form();
} else {
this.hide_form();
}
callback && callback();
return this;
},
show_form: function() {
if(!this.grid_form) {
this.grid_form = new frappe.ui.form.GridRowForm({
row: this
});
}
this.grid_form.render();
this.row.toggle(false);
// this.form_panel.toggle(true);
frappe.dom.freeze("", "dark");
cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
frappe.utils.scroll_to(this.wrapper, true, 15);
}
if(this.frm) {
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
}
},
hide_form: function() {
frappe.dom.unfreeze();
this.row.toggle(true);
this.refresh();
cur_frm.cur_grid = null;
this.wrapper.removeClass("grid-row-open");
},
open_prev: function() {
if(this.grid.grid_rows[this.doc.idx-2]) {
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
}
},
open_next: function() {
if(this.grid.grid_rows[this.doc.idx]) {
this.grid.grid_rows[this.doc.idx].toggle_view(true);
} else {
this.grid.add_new_row(null, null, true);
}
},
refresh_field: function(fieldname, txt) {
var df = this.grid.get_docfield(fieldname) || undefined;
// format values if no frm
if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
}
if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}
// reset static value
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
if(df && df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
}
}
// reset field value
var field = this.on_grid_fields_dict[fieldname];
if(field) {
field.docname = this.doc.name;
field.refresh();
}
// in form
if(this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
},
get_field: function(fieldname) {
let field = this.on_grid_fields_dict[fieldname];
if (field) {
return field;
} else if(this.grid_form) {
return this.grid_form.fields_dict[fieldname];
} else {
throw `fieldname ${fieldname} not found`;
}
},
get_visible_columns: function(blacklist) {
var me = this;
var visible_columns = $.map(this.docfields, function(df) {
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);
return visible ? df : null;
});
return visible_columns;
},
set_field_property: function(fieldname, property, value) {
// set a field property for open form / grid form
var me = this;
var set_property = function(field) {
if(!field) return;
field.df[property] = value;
field.refresh();
}
// set property in grid form
if(this.grid_form) {
set_property(this.grid_form.fields_dict[fieldname]);
this.grid_form.layout && this.grid_form.layout.refresh_sections();
}
// set property in on grid fields
set_property(this.on_grid_fields_dict[fieldname]);
},
toggle_reqd: function(fieldname, reqd) {
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
},
toggle_display: function(fieldname, show) {
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
},
toggle_editable: function(fieldname, editable) {
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
},
});

View file

@ -0,0 +1,97 @@
frappe.ui.form.GridRowForm = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.wrapper = $('<div class="form-in-grid"></div>')
.appendTo(this.row.wrapper);
},
render: function() {
var me = this;
this.make_form();
this.form_area.empty();
this.layout = new frappe.ui.form.Layout({
fields: this.row.docfields,
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
});
this.layout.make();
this.fields = this.layout.fields;
this.fields_dict = this.layout.fields_dict;
this.layout.refresh(this.row.doc);
// copy get_query to fields
for(var fieldname in (this.row.grid.fieldinfo || {})) {
var fi = this.row.grid.fieldinfo[fieldname];
$.extend(me.fields_dict[fieldname], fi);
}
this.toggle_add_delete_button_display(this.wrapper);
this.row.grid.open_grid_row = this;
this.set_focus();
},
make_form: function() {
if(!this.form_area) {
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
this.form_area = this.wrapper.find(".form-area");
this.row.set_row_index();
this.set_form_events();
}
},
set_form_events: function() {
var me = this;
this.wrapper.find(".grid-delete-row")
.on('click', function() {
me.row.remove(); return false;
});
this.wrapper.find(".grid-insert-row")
.on('click', function() {
me.row.insert(true); return false;
});
this.wrapper.find(".grid-insert-row-below")
.on('click', function() {
me.row.insert(true, true); return false;
});
this.wrapper.find(".grid-append-row")
.on('click', function() {
me.row.toggle_view(false);
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
return false;
});
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
me.row.toggle_view();
return false;
});
},
toggle_add_delete_button_display: function($parent) {
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
.toggle(this.row.grid.is_editable());
},
refresh_field: function(fieldname) {
if(this.fields_dict[fieldname]) {
this.fields_dict[fieldname].refresh();
this.layout && this.layout.refresh_dependency();
}
},
set_focus: function() {
// wait for animation and then focus on the first row
var me = this;
setTimeout(function() {
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
var first = me.form_area.find("input:first");
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
try {
first.get(0).focus();
} catch(e) {
//
}
}
}
}, 500);
},
});

View file

@ -1,20 +1,37 @@
frappe.provide('frappe.ui.form'); frappe.provide('frappe.ui.form');
frappe.ui.form.make_quick_entry = (doctype, after_insert) => {
var trimmed_doctype = doctype.replace(/ /g, '');
var controller_name = "QuickEntryForm";
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
controller_name = trimmed_doctype + "QuickEntryForm";
}
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert);
return frappe.quick_entry.setup();
};
frappe.ui.form.QuickEntryForm = Class.extend({ frappe.ui.form.QuickEntryForm = Class.extend({
init: function(doctype, success_function){ init: function(doctype, after_insert){
this.doctype = doctype; this.doctype = doctype;
this.success_function = success_function; this.after_insert = after_insert;
this.setup();
}, },
setup: function(){ setup: function() {
var me = this; let me = this;
frappe.model.with_doctype(this.doctype, function() { return new Promise(resolve => {
me.set_meta_and_mandatory_fields(); frappe.model.with_doctype(this.doctype, function() {
var validate_flag = me.validate_quick_entry(); me.set_meta_and_mandatory_fields();
if(!validate_flag){ if(me.is_quick_entry()) {
me.render_dialog(); me.render_dialog();
} resolve(me);
} else {
frappe.quick_entry = null;
frappe.set_route('Form', me.doctype, me.doc.name)
.then(() => resolve(me));
}
});
}); });
}, },
@ -25,34 +42,34 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.doc = frappe.model.get_new_doc(this.doctype, null, null, true); this.doc = frappe.model.get_new_doc(this.doctype, null, null, true);
}, },
validate_quick_entry: function(){ is_quick_entry: function(){
if(this.meta.quick_entry != 1) { if(this.meta.quick_entry != 1) {
frappe.set_route('Form', this.doctype, this.doc.name); return false;
return true;
} }
var mandatory_flag = this.validate_mandatory_length();
var child_table_flag = this.validate_for_child_table();
if (mandatory_flag || child_table_flag){ if (this.too_many_mandatory_fields() || this.has_child_table()) {
return true; return false;
} }
this.validate_for_prompt_autoname(); this.validate_for_prompt_autoname();
return true;
}, },
validate_mandatory_length: function(){ too_many_mandatory_fields: function(){
if(this.mandatory.length > 7) { if(this.mandatory.length > 7) {
// too many fields, show form // too many fields, show form
frappe.set_route('Form', this.doctype, this.doc.name);
return true; return true;
} }
return false;
}, },
validate_for_child_table: function(){ has_child_table: function(){
if($.map(this.mandatory, function(d) { return d.fieldtype==='Table' ? d : null; }).length) { if($.map(this.mandatory, function(d) {
return d.fieldtype==='Table' ? d : null; }).length) {
// has mandatory table, quit! // has mandatory table, quit!
frappe.set_route('Form', this.doctype, this.doc.name);
return true; return true;
} }
return false;
}, },
validate_for_prompt_autoname: function(){ validate_for_prompt_autoname: function(){
@ -86,6 +103,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
} }
}); });
this.dialog.onhide = () => frappe.quick_entry = null;
this.dialog.show(); this.dialog.show();
this.set_defaults(); this.set_defaults();
}, },
@ -93,44 +111,62 @@ frappe.ui.form.QuickEntryForm = Class.extend({
register_primary_action: function(){ register_primary_action: function(){
var me = this; var me = this;
this.dialog.set_primary_action(__('Save'), function() { this.dialog.set_primary_action(__('Save'), function() {
if(me.dialog.working) return; if(me.dialog.working) {
return;
}
var data = me.dialog.get_values(); var data = me.dialog.get_values();
if(data) { if(data) {
me.dialog.working = true; me.dialog.working = true;
var values = me.update_doc(); me.insert();
me.insert_document(values);
} }
}); });
}, },
insert_document: function(values){ insert: function() {
var me = this; let me = this;
frappe.call({ return new Promise(resolve => {
method: "frappe.client.insert", me.update_doc();
args: { frappe.call({
doc: values method: "frappe.client.insert",
}, args: {
callback: function(r) { doc: me.dialog.doc
me.dialog.hide(); },
// delete the old doc callback: function(r) {
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); me.dialog.hide();
var doc = r.message; // delete the old doc
if(me.success_function) { frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.success_function(doc); me.dialog.doc = r.message;
} if(frappe._from_link) {
frappe.ui.form.update_calling_link(doc.name); frappe.ui.form.update_calling_link(me.dialog.doc.name);
}, } else {
error: function() { if(me.after_insert) {
me.open_doc(); me.after_insert(me.dialig.doc);
}, } else {
always: function() { me.open_from_if_not_list();
me.dialog.working = false; }
}, }
freeze: true },
error: function() {
me.open_doc();
},
always: function() {
me.dialog.working = false;
resolve(me.dialog.doc);
},
freeze: true
});
}); });
}, },
open_from_if_not_list: function() {
let route = frappe.get_route();
let doc = this.dialog.doc;
if(route && !(route[0]==='List' && route[1]===doc.doctype)) {
frappe.set_route('Form', doc.doctype, doc.name);
}
},
update_doc: function(){ update_doc: function(){
var me = this; var me = this;
var data = this.dialog.get_values(true); var data = this.dialog.get_values(true);

View file

@ -226,8 +226,11 @@ frappe.ui.form.update_calling_link = function (newdoc) {
// if from form, switch // if from form, switch
if (frappe._from_link.frm) { if (frappe._from_link.frm) {
frappe.set_route("Form", frappe._from_link.frm.doctype, frappe._from_link.frm.docname); frappe.set_route("Form",
setTimeout(function () { frappe.utils.scroll_to(frappe._from_link_scrollY); }, 100); frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
});
} }
frappe._from_link = null; frappe._from_link = null;

View file

@ -55,8 +55,8 @@ frappe.ui.form.off = function(doctype, fieldname, handler) {
} }
frappe.ui.form.trigger = function(doctype, fieldname, callback) { frappe.ui.form.trigger = function(doctype, fieldname) {
cur_frm.script_manager.trigger(fieldname, doctype, null, callback); cur_frm.script_manager.trigger(fieldname, doctype);
} }
frappe.ui.form.ScriptManager = Class.extend({ frappe.ui.form.ScriptManager = Class.extend({
@ -64,32 +64,76 @@ frappe.ui.form.ScriptManager = Class.extend({
$.extend(this, opts); $.extend(this, opts);
}, },
make: function(ControllerClass) { make: function(ControllerClass) {
this.frm.cscript = $.extend(this.frm.cscript, new ControllerClass({frm: this.frm})); this.frm.cscript = $.extend(this.frm.cscript,
new ControllerClass({frm: this.frm}));
}, },
trigger: function(event_name, doctype, name, callback) { trigger: function(event_name, doctype, name) {
var me = this; // trigger all the form level events that
doctype = doctype || this.frm.doctype; // are bound to this event_name
name = name || this.frm.docname; let me = this;
var handlers = this.get_handlers(event_name, doctype, name, callback); return new Promise(resolve => {
if(callback) handlers.push(callback); doctype = doctype || this.frm.doctype;
name = name || this.frm.docname;
this.frm.selected_doc = frappe.get_doc(doctype, name); let tasks = [];
let handlers = this.get_handlers(event_name, doctype);
return $.when.apply($, $.map(handlers, function(fn) { return fn(); })); // helper for child table
this.frm.selected_doc = frappe.get_doc(doctype, name);
let runner = (_function, is_old_style) => {
let _promise = null;
if(is_old_style) {
// old style arguments (doc, cdt, cdn)
_promise = me.frm.cscript[_function](me.frm.doc, doctype, name);
} else {
// new style (frm, doctype, name)
_promise = _function(me.frm, doctype, name);
}
// if the trigger returns a promise, return it,
// or use the default promise frappe.after_ajax
if (_promise && _promise.then) {
return _promise;
} else {
return frappe.after_server_call();
}
};
// make list of functions to be run serially
handlers.new_style.forEach((_function) => {
tasks.push(() => runner(_function, false));
});
handlers.old_style.forEach((_function) => {
tasks.push(() => runner(_function, true));
});
// run them serially
frappe.run_serially(tasks).then(resolve());
});
}, },
get_handlers: function(event_name, doctype, name, callback) { has_handlers: function(event_name, doctype) {
var handlers = []; let handlers = this.get_handlers(event_name, doctype);
var me = this; return handlers && (handlers.old_style.length || handlers.new_style.length);
},
get_handlers: function(event_name, doctype) {
// returns list of all functions to be called (old style and new style)
let me = this;
let handlers = {
old_style: [],
new_style: []
};
if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) { if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) {
$.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) { $.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) {
handlers.push(function() { return fn(me.frm, doctype, name) }); handlers.new_style.push(fn);
}); });
} }
if(this.frm.cscript[event_name]) { if(this.frm.cscript[event_name]) {
handlers.push(function() { return me.frm.cscript[event_name](me.frm.doc, doctype, name); }); handlers.old_style.push(event_name);
} }
if(this.frm.cscript["custom_" + event_name]) { if(this.frm.cscript["custom_" + event_name]) {
handlers.push(function() { return me.frm.cscript["custom_" + event_name](me.frm.doc, doctype, name); }); handlers.old_style.push("custom_" + event_name);
} }
return handlers; return handlers;
}, },

View file

@ -313,32 +313,20 @@ $.extend(frappe.model, {
frappe.create_routes = {}; frappe.create_routes = {};
frappe.new_doc = function (doctype, opts) { frappe.new_doc = function (doctype, opts) {
if(opts && $.isPlainObject(opts)) { frappe.route_options = opts; } return new Promise(resolve => {
frappe.model.with_doctype(doctype, function() { if(opts && $.isPlainObject(opts)) {
if(frappe.create_routes[doctype]) { frappe.route_options = opts;
frappe.set_route(frappe.create_routes[doctype]);
} else {
var trimmed_doctype = doctype.replace(/ /g, '');
var controller_name = "QuickEntryForm";
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
controller_name = trimmed_doctype + "QuickEntryForm";
}
new frappe.ui.form[controller_name](doctype, function(doc) {
//frappe.set_route('List', doctype);
var title = doc.name;
var title_field = frappe.get_meta(doc.doctype).title_field;
if (title_field) {
title = doc[title_field];
}
var route = frappe.get_route();
if(route && !(route[0]==='List' && route[1]===doc.doctype)) {
frappe.set_route('Form', doc.doctype, doc.name);
}
});
} }
frappe.model.with_doctype(doctype, function() {
if(frappe.create_routes[doctype]) {
frappe.set_route(frappe.create_routes[doctype])
.then(() => resolve());
} else {
frappe.ui.form.make_quick_entry(doctype)
.then(() => resolve());
}
});
}); });
} }

View file

@ -164,7 +164,8 @@ $.extend(frappe.meta, {
}); });
if(!out) { if(!out) {
frappe.msgprint(__('Warning: Unable to find {0} in any table related to {1}', [ // eslint-disable-next-line
console.log(__('Warning: Unable to find {0} in any table related to {1}', [
key, __(doctype)])); key, __(doctype)]));
} }
} }

View file

@ -327,9 +327,11 @@ $.extend(frappe.model, {
set_value: function(doctype, docname, fieldname, value, fieldtype) { set_value: function(doctype, docname, fieldname, value, fieldtype) {
/* help: Set a value locally (if changed) and execute triggers */ /* help: Set a value locally (if changed) and execute triggers */
var doc = locals[doctype] && locals[doctype][docname]; var doc = locals[doctype] && locals[doctype][docname];
var to_update = fieldname; var to_update = fieldname;
let tasks = [];
if(!$.isPlainObject(to_update)) { if(!$.isPlainObject(to_update)) {
to_update = {}; to_update = {};
to_update[fieldname] = value; to_update[fieldname] = value;
@ -343,14 +345,16 @@ $.extend(frappe.model, {
} }
doc[key] = value; doc[key] = value;
frappe.model.trigger(key, value, doc); tasks.push(() => frappe.model.trigger(key, value, doc));
} else { } else {
// execute link triggers (want to reselect to execute triggers) // execute link triggers (want to reselect to execute triggers)
if(fieldtype=="Link" && doc) { if(fieldtype=="Link" && doc) {
frappe.model.trigger(key, value, doc); tasks.push(() => frappe.model.trigger(key, value, doc));
} }
} }
}); });
return frappe.run_serially(tasks);
}, },
on: function(doctype, fieldname, fn) { on: function(doctype, fieldname, fn) {
@ -371,21 +375,34 @@ $.extend(frappe.model, {
}, },
trigger: function(fieldname, value, doc) { trigger: function(fieldname, value, doc) {
let tasks = [];
var run = function(events, event_doc) { var runner = function(events, event_doc) {
$.each(events || [], function(i, fn) { $.each(events || [], function(i, fn) {
fn && fn(fieldname, value, event_doc || doc); if(fn) {
let _promise = fn(fieldname, value, event_doc || doc);
// if the trigger returns a promise, return it,
// or use the default promise frappe.after_ajax
if (_promise && _promise.then) {
return _promise;
} else {
return frappe.after_server_call();
}
}
}); });
}; };
if(frappe.model.events[doc.doctype]) { if(frappe.model.events[doc.doctype]) {
tasks.push(() => {
return runner(frappe.model.events[doc.doctype][fieldname]);
});
// field-level tasks.push(() => {
run(frappe.model.events[doc.doctype][fieldname]); runner(frappe.model.events[doc.doctype]['*']);
});
// doctype-level
run(frappe.model.events[doc.doctype]['*']);
} }
frappe.run_serially(tasks);
}, },
get_doc: function(doctype, name) { get_doc: function(doctype, name) {

View file

@ -26,4 +26,4 @@ frappe.provide("frappe.utils");
frappe.provide("frappe.ui"); frappe.provide("frappe.ui");
frappe.provide("frappe.modules"); frappe.provide("frappe.modules");
frappe.provide("frappe.templates"); frappe.provide("frappe.templates");
frappe.provide("frappe.test_data");

View file

@ -10,8 +10,9 @@ frappe.request.waiting_for_ajax = [];
// generic server call (call page, object) // generic server call (call page, object)
frappe.call = function(opts) { frappe.call = function(opts) {
if(opts.quiet) if(opts.quiet) {
opts.no_spinner = true; opts.no_spinner = true;
}
var args = $.extend({}, opts.args); var args = $.extend({}, opts.args);
// cmd // cmd
@ -302,13 +303,31 @@ frappe.request.cleanup = function(opts, r) {
} }
} }
frappe.after_ajax = function(fn) { frappe.after_server_call = () => {
if(frappe.request.ajax_count) { if(frappe.request.ajax_count) {
frappe.request.waiting_for_ajax.push(fn); return new Promise(resolve => {
frappe.request.waiting_for_ajax.push(() => {
resolve();
});
});
} else { } else {
fn(); return null;
} }
} };
frappe.after_ajax = function(fn) {
return new Promise(resolve => {
if(frappe.request.ajax_count) {
frappe.request.waiting_for_ajax.push(() => {
if(fn) fn();
resolve();
});
} else {
if(fn) fn();
resolve();
}
});
};
frappe.request.report_error = function(xhr, request_opts) { frappe.request.report_error = function(xhr, request_opts) {
var data = JSON.parse(xhr.responseText); var data = JSON.parse(xhr.responseText);

View file

@ -123,24 +123,31 @@ frappe.get_route_str = function(route) {
} }
frappe.set_route = function() { frappe.set_route = function() {
var params = arguments; return new Promise(resolve => {
if(params.length===1 && $.isArray(params[0])) { var params = arguments;
params = params[0]; if(params.length===1 && $.isArray(params[0])) {
} params = params[0];
var route = $.map(params, function(a) {
if($.isPlainObject(a)) {
frappe.route_options = a;
return null;
} else {
return a;
// return a ? encodeURIComponent(a) : null;
} }
}).join('/'); var route = $.map(params, function(a) {
if($.isPlainObject(a)) {
frappe.route_options = a;
return null;
} else {
return a;
// return a ? encodeURIComponent(a) : null;
}
}).join('/');
window.location.hash = route; window.location.hash = route;
// Set favicon (app.js) // Set favicon (app.js)
frappe.app.set_favicon && frappe.app.set_favicon(); frappe.app.set_favicon && frappe.app.set_favicon();
setTimeout(() => {
frappe.after_ajax(() => {
resolve();
});
}, 100);
});
} }
frappe.set_re_route = function() { frappe.set_re_route = function() {

View file

@ -192,7 +192,6 @@ frappe.ui.BaseList = Class.extend({
onchange: () => { me.refresh(true); } onchange: () => { me.refresh(true); }
}); });
var has_standard_filters = false;
this.meta.fields.forEach(function(df) { this.meta.fields.forEach(function(df) {
if(df.in_standard_filter) { if(df.in_standard_filter) {
if(df.fieldtype == "Select" && df.options) { if(df.fieldtype == "Select" && df.options) {
@ -205,17 +204,13 @@ frappe.ui.BaseList = Class.extend({
me.page.add_field({ me.page.add_field({
fieldtype: df.fieldtype, fieldtype: df.fieldtype,
label: __(df.label), label: __(df.label),
options: df.options, options: options,
fieldname: df.fieldname, fieldname: df.fieldname,
onchange: () => {me.refresh(true);} onchange: () => { me.refresh(true); }
}); });
} }
}); });
this.page.page_form.on('change', ':input', function() {
me.refresh(true);
});
this.standard_filters_added = true; this.standard_filters_added = true;
}, },

View file

@ -89,11 +89,18 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
return f && (f.get_parsed_value ? f.get_parsed_value() : null); return f && (f.get_parsed_value ? f.get_parsed_value() : null);
}, },
set_value: function(key, val){ set_value: function(key, val){
var f = this.fields_dict[key]; return new Promise(resolve => {
if(f) { var f = this.fields_dict[key];
f.set_input(val); if(f) {
this.refresh_dependency(); f.set_value(val).then(() => {
} f.set_input(val);
this.refresh_dependency();
resolve();
});
} else {
resolve();
}
});
}, },
set_input: function(key, val) { set_input: function(key, val) {
return this.set_value(key, val); return this.set_value(key, val);

View file

@ -19,8 +19,11 @@
<div class="col-sm-6 col-xs-12"> <div class="col-sm-6 col-xs-12">
<div class="filter_field pull-left" style="width: calc(100% - 70px)"></div> <div class="filter_field pull-left" style="width: calc(100% - 70px)"></div>
<div class="filter-actions pull-left"> <div class="filter-actions pull-left">
<a class="set-filter-and-run btn btn-primary pull-left"><i class=" fa fa-check"></i></a> <a class="set-filter-and-run btn btn-sm btn-primary pull-left">
<a class="small grey remove-filter pull-left"><i class="octicon octicon-trashcan visible-xs"></i> <i class=" fa fa-check visible-xs"></i>
<span class="hidden-xs">{%= __("Apply") %}</span></a>
<a class="small grey remove-filter pull-left">
<i class="octicon octicon-trashcan visible-xs"></i>
<span class="hidden-xs">{%= __("Remove") %}</span></a> <span class="hidden-xs">{%= __("Remove") %}</span></a>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>

View file

@ -0,0 +1,16 @@
frappe.find = {
page_primary_action: () => {
return $('.page-actions:visible .btn-primary');
},
field: (fieldname, value) => {
return new Promise(resolve => {
let input = $(`[data-fieldname="${fieldname}"] :input`);
if(value) {
input.val(value).trigger('change');
frappe.after_ajax(() => { resolve(input); });
} else {
resolve(input);
}
});
}
};

View file

@ -1,27 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.standard_pages["test-runner"] = function() {
var wrapper = frappe.container.add_page('test-runner');
frappe.ui.make_app_page({
parent: wrapper,
single_column: true,
title: __("Test Runner")
});
$("<div id='qunit'></div>").appendTo($(wrapper).find(".layout-main"));
var route = frappe.get_route();
if(route.length < 2) {
frappe.msgprint(__("To run a test add the module name in the route after '{0}'. For example, {1}", ['test-runner/', '#test-runner/lib/js/frappe/test_app.js']));
return;
}
var requires = ["assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css"].concat(route.splice(1).join("/"));
frappe.require(requires, function() {
QUnit.load();
});
}

View file

@ -368,13 +368,13 @@ _f.Frm.prototype.set_read_only = function() {
} }
_f.Frm.prototype.trigger = function(event) { _f.Frm.prototype.trigger = function(event) {
this.script_manager.trigger(event); return this.script_manager.trigger(event);
}; };
_f.Frm.prototype.get_formatted = function(fieldname) { _f.Frm.prototype.get_formatted = function(fieldname) {
return frappe.format(this.doc[fieldname], return frappe.format(this.doc[fieldname],
frappe.meta.get_docfield(this.doctype, fieldname, this.docname), frappe.meta.get_docfield(this.doctype, fieldname, this.docname),
{no_icon:true}, this.doc); {no_icon:true}, this.doc);
} }
_f.Frm.prototype.open_grid_row = function() { _f.Frm.prototype.open_grid_row = function() {

View file

@ -606,18 +606,19 @@ _f.Frm.prototype.setnewdoc = function() {
var me = this; var me = this;
// hide any open grid // hide any open grid
this.script_manager.trigger("before_load", this.doctype, this.docname, function() { this.script_manager.trigger("before_load", this.doctype, this.docname)
me.script_manager.trigger("onload"); .then(() => {
me.opendocs[me.docname] = true; me.script_manager.trigger("onload");
me.render_form(); me.opendocs[me.docname] = true;
me.render_form();
frappe.after_ajax(function() { frappe.after_ajax(function() {
me.trigger_link_fields(); me.trigger_link_fields();
});
frappe.breadcrumbs.add(me.meta.module, me.doctype)
}); });
frappe.breadcrumbs.add(me.meta.module, me.doctype)
});
// update seen // update seen
if(this.meta.track_seen) { if(this.meta.track_seen) {
$('.list-id[data-name="'+ me.docname +'"]').addClass('seen'); $('.list-id[data-name="'+ me.docname +'"]').addClass('seen');
@ -705,17 +706,21 @@ Object.defineProperty(window, 'validated', {
}); });
_f.Frm.prototype.save = function(save_action, callback, btn, on_error) { _f.Frm.prototype.save = function(save_action, callback, btn, on_error) {
btn && $(btn).prop("disabled", true); let me = this;
$(document.activeElement).blur(); return new Promise(resolve => {
btn && $(btn).prop("disabled", true);
$(document.activeElement).blur();
frappe.ui.form.close_grid_form(); frappe.ui.form.close_grid_form();
// let any pending js process finish // let any pending js process finish
var me = this; setTimeout(function() {
setTimeout(function() { me._save(save_action, callback, btn, on_error) }, 100); me._save(save_action, callback, btn, on_error, resolve);
}, 100);
});
} }
_f.Frm.prototype._save = function(save_action, callback, btn, on_error) { _f.Frm.prototype._save = function(save_action, callback, btn, on_error, resolve) {
var me = this; var me = this;
if(!save_action) save_action = "Save"; if(!save_action) save_action = "Save";
this.validate_form_action(save_action); this.validate_form_action(save_action);
@ -736,26 +741,29 @@ _f.Frm.prototype._save = function(save_action, callback, btn, on_error) {
on_error(); on_error();
} }
callback && callback(r); callback && callback(r);
resolve();
} }
if(save_action != "Update") { if(save_action != "Update") {
// validate // validate
frappe.validated = true; frappe.validated = true;
$.when(this.script_manager.trigger("validate"), this.script_manager.trigger("before_save")) Promise.all([
.done(function() { this.script_manager.trigger("validate"),
// done is called after all ajaxes in validate & before_save are completed :) this.script_manager.trigger("before_save")
]).then(() => {
// done is called after all ajaxes in validate & before_save are completed :)
if(!frappe.validated) { if(!frappe.validated) {
btn && $(btn).prop("disabled", false); btn && $(btn).prop("disabled", false);
if(on_error) { if(on_error) {
on_error(); on_error();
}
return;
} }
resolve();
return;
}
frappe.ui.form.save(me, save_action, after_save, btn); frappe.ui.form.save(me, save_action, after_save, btn);
}); });
} else { } else {
frappe.ui.form.save(me, save_action, after_save, btn); frappe.ui.form.save(me, save_action, after_save, btn);
} }
@ -767,7 +775,7 @@ _f.Frm.prototype.savesubmit = function(btn, callback, on_error) {
this.validate_form_action("Submit"); this.validate_form_action("Submit");
frappe.confirm(__("Permanently Submit {0}?", [this.docname]), function() { frappe.confirm(__("Permanently Submit {0}?", [this.docname]), function() {
frappe.validated = true; frappe.validated = true;
me.script_manager.trigger("before_submit").done(function() { me.script_manager.trigger("before_submit").then(function() {
if(!frappe.validated) { if(!frappe.validated) {
if(on_error) if(on_error)
on_error(); on_error();
@ -964,10 +972,6 @@ _f.Frm.prototype.validate_form_action = function(action) {
} }
}; };
_f.Frm.prototype.get_handlers = function(fieldname, doctype, docname) {
return this.script_manager.get_handlers(fieldname, doctype || this.doctype, docname || this.docname)
}
_f.Frm.prototype.has_perm = function(ptype) { _f.Frm.prototype.has_perm = function(ptype) {
return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc); return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc);
} }

View file

@ -1,12 +1,12 @@
/*! /*!
* QUnit 2.0.0 * QUnit 2.3.3
* https://qunitjs.com/ * https://qunitjs.com/
* *
* Copyright jQuery Foundation and other contributors * Copyright jQuery Foundation and other contributors
* Released under the MIT license * Released under the MIT license
* https://jquery.org/license * https://jquery.org/license
* *
* Date: 2016-06-16T17:09Z * Date: 2017-06-02T14:07Z
*/ */
/** Font Family and Sizes */ /** Font Family and Sizes */
@ -226,7 +226,8 @@
#qunit-tests li.running, #qunit-tests li.running,
#qunit-tests li.pass, #qunit-tests li.pass,
#qunit-tests li.fail, #qunit-tests li.fail,
#qunit-tests li.skipped { #qunit-tests li.skipped,
#qunit-tests li.aborted {
display: list-item; display: list-item;
} }
@ -235,7 +236,7 @@
} }
#qunit-tests.hidepass li.running, #qunit-tests.hidepass li.running,
#qunit-tests.hidepass li.pass { #qunit-tests.hidepass li.pass:not(.todo) {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
width: 0; width: 0;
@ -374,12 +375,16 @@
#qunit-banner.qunit-fail { background-color: #EE5757; } #qunit-banner.qunit-fail { background-color: #EE5757; }
/*** Aborted tests */
#qunit-tests .aborted { color: #000; background-color: orange; }
/*** Skipped tests */ /*** Skipped tests */
#qunit-tests .skipped { #qunit-tests .skipped {
background-color: #EBECE9; background-color: #EBECE9;
} }
#qunit-tests .qunit-todo-label,
#qunit-tests .qunit-skipped-label { #qunit-tests .qunit-skipped-label {
background-color: #F4FF77; background-color: #F4FF77;
display: inline-block; display: inline-block;
@ -390,19 +395,35 @@
margin: -0.4em 0.4em -0.4em 0; margin: -0.4em 0.4em -0.4em 0;
} }
#qunit-tests .qunit-todo-label {
background-color: #EEE;
}
/** Result */ /** Result */
#qunit-testresult { #qunit-testresult {
padding: 0.5em 1em 0.5em 1em;
color: #2B81AF; color: #2B81AF;
background-color: #D2E0E6; background-color: #D2E0E6;
border-bottom: 1px solid #FFF; border-bottom: 1px solid #FFF;
} }
#qunit-testresult .clearfix {
height: 0;
clear: both;
}
#qunit-testresult .module-name { #qunit-testresult .module-name {
font-weight: 700; font-weight: 700;
} }
#qunit-testresult-display {
padding: 0.5em 1em 0.5em 1em;
width: 85%;
float:left;
}
#qunit-testresult-controls {
padding: 0.5em 1em 0.5em 1em;
width: 10%;
float:left;
}
/** Fixture */ /** Fixture */
@ -412,4 +433,4 @@
left: -10000px; left: -10000px;
width: 1000px; width: 1000px;
height: 1000px; height: 1000px;
} }

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,8 @@ def xmlrunner_wrapper(output):
return xmlrunner.XMLTestRunner(*args, **kwargs) return xmlrunner.XMLTestRunner(*args, **kwargs)
return _runner return _runner
def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=False, profile=False, junit_xml_output=None): def main(app=None, module=None, doctype=None, verbose=False, tests=(),
force=False, profile=False, junit_xml_output=None, ui_tests=False):
global unittest_runner global unittest_runner
xmloutput_fh = None xmloutput_fh = None
@ -57,7 +58,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=Fal
elif module: elif module:
ret = run_tests_for_module(module, verbose, tests, profile) ret = run_tests_for_module(module, verbose, tests, profile)
else: else:
ret = run_all_tests(app, verbose, profile) ret = run_all_tests(app, verbose, profile, ui_tests)
frappe.db.commit() frappe.db.commit()
@ -80,7 +81,7 @@ def set_test_email_config():
"admin_password": "admin" "admin_password": "admin"
}) })
def run_all_tests(app=None, verbose=False, profile=False): def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False):
import os import os
apps = [app] if app else frappe.get_installed_apps() apps = [app] if app else frappe.get_installed_apps()
@ -95,9 +96,11 @@ def run_all_tests(app=None, verbose=False, profile=False):
# print path # print path
for filename in files: for filename in files:
filename = cstr(filename) filename = cstr(filename)
if filename.startswith("test_") and filename.endswith(".py"): if filename.startswith("test_") and filename.endswith(".py")\
and filename != 'test_runner.py':
# print filename[:-3] # print filename[:-3]
_add_test(app, path, filename, verbose, test_suite=test_suite) _add_test(app, path, filename, verbose,
test_suite, ui_tests)
if profile: if profile:
pr = cProfile.Profile() pr = cProfile.Profile()
@ -163,7 +166,7 @@ def _run_unittest(module, verbose=False, tests=(), profile=False):
return out return out
def _add_test(app, path, filename, verbose, test_suite=None): def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
import os import os
if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path: if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path:
@ -179,8 +182,9 @@ def _add_test(app, path, filename, verbose, test_suite=None):
relative_path=relative_path.replace('/', '.'), module_name=filename[:-3]) relative_path=relative_path.replace('/', '.'), module_name=filename[:-3])
module = frappe.get_module(module_name) module = frappe.get_module(module_name)
is_ui_test = True if hasattr(module, 'TestDriver') else False
if getattr(module, "selenium_tests", False) and not frappe.conf.run_selenium_tests: if is_ui_test != ui_tests:
return return
if not test_suite: if not test_suite:
@ -325,3 +329,5 @@ def print_mandatory_fields(doctype):
for d in meta.get("fields", {"reqd":1}): for d in meta.get("fields", {"reqd":1}):
print(d.parent + ":" + d.fieldname + " | " + d.fieldtype + " | " + (d.options or "")) print(d.parent + ":" + d.fieldname + " | " + d.fieldtype + " | " + (d.options or ""))
print() print()

View file

@ -1,27 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import unittest, frappe
from frappe.utils import sel
selenium_tests = True
class TestLogin(unittest.TestCase):
def setUp(self):
return
sel.login()
def test_login(self):
return
self.assertEquals(sel._driver.current_url, sel.get_localhost() + "/desk")
def test_to_do(self):
return
# too unpredictable in travis
sel.go_to_module("ToDo")
sel.primary_action()
sel.wait_for_page("Form/ToDo")
sel.set_field("description", "test description", "textarea")
sel.primary_action()
self.assertTrue(sel.wait_for_state("clean"))

View file

View file

@ -1,19 +0,0 @@
module.exports = {
beforeEach: browser => {
browser
.url(browser.launch_url + '/login')
.waitForElementVisible('body', 5000)
},
'Login': browser => {
browser
.assert.title('Login')
.assert.visible('#login_email', 'Check if login box is visible')
.setValue("#login_email", "Administrator")
.setValue("#login_password", "admin")
.click(".btn-login")
.waitForElementVisible("#body_div", 15000);
},
after: browser => {
browser.end();
},
};

View file

@ -0,0 +1,93 @@
frappe.tests = {
data: {},
get_fixture_names: (doctype) => {
return Object.keys(frappe.test_data[doctype]);
},
make: function(doctype, data) {
return frappe.run_serially([
() => frappe.set_route('List', doctype),
() => frappe.new_doc(doctype),
() => {
let frm = frappe.quick_entry ? frappe.quick_entry.dialog : cur_frm;
return frappe.tests.set_form_values(frm, data);
},
() => frappe.timeout(1),
() => (frappe.quick_entry ? frappe.quick_entry.insert() : cur_frm.save())
]);
},
set_form_values: (frm, data) => {
let tasks = [];
data.forEach(item => {
for (let key in item) {
let task = () => {
let value = item[key];
if ($.isArray(value)) {
return frappe.tests.set_grid_values(frm, key, value);
} else {
// single value
return frm.set_value(key, value);
}
};
tasks.push(task);
}
});
// set values
return frappe.run_serially(tasks);
},
set_grid_values: (frm, key, value) => {
// set value in grid
let grid = frm.get_field(key).grid;
grid.remove_all();
let grid_row_tasks = [];
// build tasks for each row
value.forEach(d => {
grid_row_tasks.push(() => {
grid.add_new_row();
let grid_row = grid.get_row(-1).toggle_view(true);
let grid_value_tasks = [];
// build tasks to set each row value
d.forEach(child_value => {
for (let child_key in child_value) {
grid_value_tasks.push(() => {
return frappe.model.set_value(grid_row.doc.doctype,
grid_row.doc.name, child_key, child_value[child_key]);
});
}
});
return frappe.run_serially(grid_value_tasks);
});
});
return frappe.run_serially(grid_row_tasks);
},
setup_doctype: (doctype) => {
return frappe.set_route('List', doctype)
.then(() => {
frappe.tests.data[doctype] = [];
let expected = frappe.tests.get_fixture_names(doctype);
cur_list.data.forEach((d) => {
frappe.tests.data[doctype].push(d.name);
if(expected.indexOf(d.name) !== -1) {
expected[expected.indexOf(d.name)] = null;
}
});
let tasks = [];
expected.forEach(function(d) {
if(d) {
tasks.push(() => frappe.tests.make(doctype,
frappe.test_data[doctype][d]));
}
});
return frappe.run_serially(tasks);
});
}
};

View file

@ -0,0 +1,33 @@
QUnit.module('views');
QUnit.test("test quick entry", function(assert) {
assert.expect(2);
let done = assert.async();
let random = frappe.utils.get_random(10);
frappe.run_serially([
() => frappe.set_route('List', 'ToDo'),
() => frappe.new_doc('ToDo'),
() => frappe.quick_entry.dialog.set_value('description', random),
() => frappe.quick_entry.insert(),
(doc) => {
assert.ok(doc && !doc.__islocal);
return frappe.set_route('Form', 'ToDo', doc.name);
},
() => {
assert.ok(cur_frm.doc.description.includes(random));
return done();
}
]);
});
QUnit.test("test list values", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.set_route('List', 'DocType')
.then(() => {
assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route());
assert.ok($('.list-item:visible').length > 10);
done();
});
});

View file

@ -0,0 +1,17 @@
from __future__ import print_function
from frappe.utils.selenium_testdriver import TestDriver
import unittest
class TestLogin(unittest.TestCase):
def setUp(self):
self.driver = TestDriver()
def test_test_runner(self):
self.driver.login()
self.driver.set_route('Form', 'Test Runner')
self.driver.click_primary_action()
self.driver.wait_for('#qunit-testresult-display', timeout=60)
self.driver.print_console()
def tearDown(self):
self.driver.close()

View file

@ -0,0 +1,50 @@
from __future__ import print_function
from frappe.utils.selenium_testdriver import TestDriver
import unittest
import time, os
class TestToDo(unittest.TestCase):
def setUp(self):
self.driver = TestDriver()
def test_todo(self):
self.driver.login()
# list view
self.driver.set_route('List', 'ToDo')
time.sleep(2)
# new
self.driver.click_primary_action()
time.sleep(2)
# set input
self.driver.set_text_editor('description', 'hello')
# save
self.driver.click_modal_primary_action()
time.sleep(2)
# refresh
self.driver.click_secondary_action()
time.sleep(2)
result_list = self.driver.get_visible_element('.result-list')
first_element_text = (result_list
.find_element_by_css_selector('.list-item')
.find_element_by_css_selector('.list-id').text)
# if os.environ.get('CI'):
# # we don't run this test in Travis as it always fails
# # reinforcing why we use Unit Testing instead of integration
# # testing
# return
self.assertTrue('hello' in first_element_text)
def tearDown(self):
self.driver.close()

View file

@ -94,6 +94,22 @@ def before_tests():
frappe.db.commit() frappe.db.commit()
frappe.clear_cache() frappe.clear_cache()
# complete setup if missing
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
setup_complete({
"language" :"english",
"email" :"test@erpnext.com",
"full_name" :"Test User",
"password" :"test",
"country" :"United States",
"timezone" :"America/New_York",
"currency" :"USD"
})
frappe.db.commit()
frappe.clear_cache()
def import_country_and_currency(): def import_country_and_currency():
from frappe.geo.country_info import get_all from frappe.geo.country_info import get_all
from frappe.utils import update_progress_bar from frappe.utils import update_progress_bar

View file

@ -186,7 +186,7 @@ class NestedSet(Document):
self.validate_ledger() self.validate_ledger()
def on_trash(self): def on_trash(self):
if not self.nsm_parent_field: if not getattr(self, 'nsm_parent_field', None):
self.nsm_parent_field = frappe.scrub(self.doctype) + "_parent" self.nsm_parent_field = frappe.scrub(self.doctype) + "_parent"
parent = self.get(self.nsm_parent_field) parent = self.get(self.nsm_parent_field)

View file

@ -1,187 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
from selenium.webdriver.common.keys import Keys
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.select import Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from urllib import unquote
import time, subprocess
import signal
import sys
host = "http://localhost"
pipe = None
port = "8000"
_driver = None
_verbose = None
logged_in = False
cur_route = False
input_wait = 0
def get_localhost():
return "{host}:{port}".format(host=host, port=port)
def start(verbose=None, driver=None):
global _driver, _verbose
_verbose = verbose
_driver = getattr(webdriver, driver or "PhantomJS")()
_driver.set_window_size(1080,800)
signal.signal(signal.SIGINT, signal_handler)
def signal_handler(signal, frame):
close()
sys.exit(0)
def start_test_server(verbose):
global pipe
pipe = subprocess.Popen(["bench", "serve", "--port", port], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#time.sleep(5)
while not pipe.stderr.readline():
time.sleep(0.5)
if verbose:
print("Test server started")
def get(url):
_driver.get(url)
def login(wait_for_id="#page-desktop"):
global logged_in
if logged_in:
return
get(get_localhost() + "/login")
wait("#login_email")
set_input("#login_email", "Administrator")
set_input("#login_password", "admin", key=Keys.RETURN)
wait(wait_for_id)
logged_in = True
def go_to_module(module_name, item=None):
global cur_route
# desktop
find(".navbar-home", True)[0].click()
cur_route = None
wait("#page-desktop")
page = "Module/" + module_name
m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
if not m:
page = "List/" + module_name
m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
if not m:
raise Exception("Module {0} not found".format(module_name))
m[0].click()
wait_for_page(page)
if item:
elem = find('[data-label="{0}"]'.format(item))[0]
elem.click()
page = elem.get_attribute("data-route")
wait_for_page(page)
def new_doc(module, doctype):
go_to_module(module, doctype)
primary_action()
wait_for_page("Form/" + doctype)
def add_child(fieldname):
find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click()
wait('[data-fieldname="{0}"] .form-grid'.format(fieldname))
def done_add_child(fieldname):
selector = '[data-fieldname="{0}"] .grid-row-open .btn-success'.format(fieldname)
scroll_to(selector)
wait_till_clickable(selector).click()
def find(selector, everywhere=False):
if cur_route and not everywhere:
selector = cur_route + " " + selector
return _driver.find_elements_by_css_selector(selector)
def set_field(fieldname, value, fieldtype="input"):
_driver.switch_to.window(_driver.current_window_handle)
selector = '{0}[data-fieldname="{1}"]'.format(fieldtype, fieldname)
set_input(selector, value, key=Keys.TAB)
wait_for_ajax()
def set_select(fieldname, value):
select = Select(find('select[data-fieldname="{0}"]'.format(fieldname))[0])
select.select_by_value(value)
wait_for_ajax()
def primary_action():
selector = ".page-actions .primary-action"
scroll_to(selector)
wait_till_clickable(selector).click()
wait_for_ajax()
def wait_for_page(name):
global cur_route
cur_route = None
route = '[data-page-route="{0}"]'.format(name)
wait_for_ajax()
elem = wait(route)
wait_for_ajax()
cur_route = route
return elem
def wait_till_clickable(selector):
if cur_route:
selector = cur_route + " " + selector
return get_wait().until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector)))
def wait_till_visible(selector):
if cur_route:
selector = cur_route + " " + selector
return get_wait().until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector)))
def wait_for_ajax():
wait('body[data-ajax-state="complete"]', True)
def wait_for_state(state):
return wait(cur_route + '[data-state="{0}"]'.format(state), True)
def wait(selector, everywhere=False):
if cur_route and not everywhere:
selector = cur_route + " " + selector
time.sleep(0.5)
elem = get_wait().until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
return elem
def get_wait():
return WebDriverWait(_driver, 20)
def set_input(selector, text, key=None):
elem = find(selector)[0]
elem.clear()
elem.send_keys(text)
if key:
time.sleep(0.5)
elem.send_keys(key)
if input_wait:
time.sleep(input_wait)
def scroll_to(selector):
execute_script("frappe.ui.scroll('{0}')".format(selector))
def execute_script(js):
_driver.execute_script(js)
def close():
global _driver, pipe
if _driver:
_driver.quit()
if pipe:
pipe.kill()
_driver = pipe = None

View file

@ -0,0 +1,260 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
#from selenium.webdriver.support.select import Select
from selenium.webdriver.support import expected_conditions as EC
#from selenium.common.exceptions import TimeoutException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import time
import signal
import os, sys
import frappe
from ast import literal_eval
class TestDriver(object):
def __init__(self, port='8000'):
self.port = port
chrome_options = Options()
capabilities = DesiredCapabilities.CHROME
if os.environ.get('CI'):
self.host = 'localhost'
else:
self.host = frappe.local.site
# enable browser logging
capabilities['loggingPrefs'] = {'browser':'ALL'}
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--start-maximized')
self.driver = webdriver.Chrome(chrome_options=chrome_options,
desired_capabilities=capabilities, port=9515)
self.driver.set_window_size(1080,800)
self.cur_route = None
self.logged_in = False
@property
def localhost(self):
return "http://{host}:{port}".format(host=self.host, port=self.port)
def get(self, url):
return self.driver.get(os.path.join(self.localhost, url))
def start(self):
def signal_handler(signal, frame):
self.close()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
def close(self):
if self.driver:
self.driver.quit()
self.driver = None
def login(self, wait_for_id="#page-desktop"):
if self.logged_in:
return
self.get('login')
self.wait_for("#login_email")
self.set_input("#login_email", "Administrator")
self.set_input("#login_password", "admin")
self.click('.btn-login')
self.wait_for(wait_for_id)
self.logged_in = True
def set_input(self, selector, text, key=None, xpath=None):
elem = self.find(selector, xpath=xpath)[0]
elem.clear()
elem.send_keys(text)
if key:
time.sleep(0.5)
elem.send_keys(key)
time.sleep(0.2)
def set_field(self, fieldname, text):
elem = self.find(xpath='//input[@data-fieldname="{0}"]'.format(fieldname))
elem[0].send_keys(text)
def set_text_editor(self, fieldname, text):
elem = self.find(xpath='//div[@data-fieldname="{0}"]//div[@contenteditable="true"]'.format(fieldname))
elem[0].send_keys(text)
def find(self, selector=None, everywhere=False, xpath=None):
if xpath:
return self.driver.find_elements_by_xpath(xpath)
else:
if self.cur_route and not everywhere:
selector = self.cur_route + " " + selector
return self.driver.find_elements_by_css_selector(selector)
def wait_for(self, selector=None, everywhere=False, timeout=20, xpath=None):
if self.cur_route and not everywhere:
selector = self.cur_route + " " + selector
time.sleep(0.5)
if selector:
_by = By.CSS_SELECTOR
if xpath:
_by = By.XPATH
selector = xpath
try:
elem = self.get_wait(timeout).until(
EC.presence_of_element_located((_by, selector)))
return elem
except Exception, e:
# body = self.driver.find_element_by_id('body_div')
# print(body.get_attribute('innerHTML'))
self.print_console()
raise e
def print_console(self):
for entry in self.driver.get_log('browser'):
source, line_no, message = entry.get('message').split(' ', 2)
if message[0] in ('"', "'"):
# message is a quoted/escaped string
message = literal_eval(message)
print(source + ' ' + line_no)
print(message)
print('-'*40)
def get_wait(self, timeout=20):
return WebDriverWait(self.driver, timeout)
def scroll_to(self, selector):
self.execute_script("frappe.ui.scroll('{0}')".format(selector))
def set_route(self, *args):
self.execute_script('frappe.set_route({0})'\
.format(', '.join(['"{0}"'.format(r) for r in args])))
self.wait_for(xpath='//div[@data-page-route="{0}"]'.format('/'.join(args)), timeout=4)
def click(self, css_selector, xpath=None):
self.wait_till_clickable(css_selector, xpath).click()
def click_primary_action(self):
selector = ".page-actions .primary-action"
#self.scroll_to(selector)
self.wait_till_clickable(selector).click()
self.wait_for_ajax()
def click_secondary_action(self):
selector = ".page-actions .btn-secondary"
#self.scroll_to(selector)
self.wait_till_clickable(selector).click()
self.wait_for_ajax()
def click_modal_primary_action(self):
self.get_visible_modal().find_element_by_css_selector('.btn-primary').click()
def get_visible_modal(self):
return self.get_visible_element('.modal-content')
def get_visible_element(self, selector=None, xpath=None):
for elem in self.find(selector=selector, xpath=xpath):
if elem.is_displayed():
return elem
def wait_till_clickable(self, selector=None, xpath=None):
if self.cur_route:
selector = self.cur_route + " " + selector
by = By.CSS_SELECTOR
if xpath:
by = By.XPATH
selector = xpath
return self.get_wait().until(EC.element_to_be_clickable(
(by, selector)))
def execute_script(self, js):
self.driver.execute_script(js)
def wait_for_ajax(self):
self.wait_for('body[data-ajax-state="complete"]', True)
# def go_to_module(module_name, item=None):
# global cur_route
#
# # desktop
# find(".navbar-home", True)[0].click()
# cur_route = None
# wait("#page-desktop")
#
# page = "Module/" + module_name
# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
# if not m:
# page = "List/" + module_name
# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
# if not m:
# raise Exception("Module {0} not found".format(module_name))
#
# m[0].click()
# wait_for_page(page)
#
# if item:
# elem = find('[data-label="{0}"]'.format(item))[0]
# elem.click()
# page = elem.get_attribute("data-route")
# wait_for_page(page)
#
# def new_doc(module, doctype):
# go_to_module(module, doctype)
# primary_action()
# wait_for_page("Form/" + doctype)
#
# def add_child(fieldname):
# find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click()
# wait('[data-fieldname="{0}"] .form-grid'.format(fieldname))
#
# def done_add_child(fieldname):
# selector = '[data-fieldname="{0}"] .grid-row-open .btn-success'.format(fieldname)
# scroll_to(selector)
# wait_till_clickable(selector).click()
#
# def set_field(fieldname, value, fieldtype="input"):
# _driver.switch_to.window(_driver.current_window_handle)
# selector = '{0}[data-fieldname="{1}"]'.format(fieldtype, fieldname)
# set_input(selector, value, key=Keys.TAB)
# wait_for_ajax()
#
# def set_select(fieldname, value):
# select = Select(find('select[data-fieldname="{0}"]'.format(fieldname))[0])
# select.select_by_value(value)
# wait_for_ajax()
#
#
# def wait_for_page(name):
# global cur_route
# cur_route = None
# route = '[data-page-route="{0}"]'.format(name)
# wait_for_ajax()
# elem = wait(route)
# wait_for_ajax()
# cur_route = route
# return elem
#
#
# def wait_till_visible(selector):
# if cur_route:
# selector = cur_route + " " + selector
# return get_wait().until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector)))
#
#
# def wait_for_state(state):
# return wait(cur_route + '[data-state="{0}"]'.format(state), True)
#
#