Ver código fonte

[tests] refactor tests (#3743)

* [tests] refactored client side tests

* [tests] refactored client side tests

* [tests] refactored client side tests
version-14
Rushabh Mehta 8 anos atrás
committed by GitHub
pai
commit
5577442251
34 arquivos alterados com 262 adições e 821 exclusões
  1. +10
    -5
      frappe/contacts/doctype/address/address.js
  2. +0
    -0
      frappe/core/doctype/doctype/boilerplate/test_controller.js
  3. +3
    -1
      frappe/core/doctype/doctype/doctype.py
  4. +23
    -0
      frappe/core/doctype/test_runner/_test_test_runner.js
  5. +15
    -18
      frappe/core/doctype/test_runner/test_runner.js
  6. +31
    -1
      frappe/core/doctype/test_runner/test_runner.json
  7. +6
    -20
      frappe/core/doctype/test_runner/test_runner.py
  8. +8
    -3
      frappe/docs/user/en/guides/automated-testing/qunit-testing.md
  9. +1
    -7
      frappe/modules/utils.py
  10. +1
    -0
      frappe/public/build.json
  11. +1
    -8
      frappe/public/js/frappe/dom.js
  12. +3
    -3
      frappe/public/js/frappe/form/footer/timeline.js
  13. +41
    -0
      frappe/public/js/frappe/misc/test_utils.js
  14. +0
    -64
      frappe/tests/ui/_test_desktop.js
  15. +0
    -60
      frappe/tests/ui/_test_gantt_view.js
  16. +0
    -56
      frappe/tests/ui/_test_print_format_builder.js
  17. +0
    -16
      frappe/tests/ui/data/test_data_for_views.js
  18. +22
    -93
      frappe/tests/ui/data/test_lib.js
  19. +0
    -44
      frappe/tests/ui/global_search/_test_list_document.js
  20. +0
    -64
      frappe/tests/ui/global_search/_test_math.js
  21. +0
    -55
      frappe/tests/ui/global_search/_test_new_record.js
  22. +0
    -42
      frappe/tests/ui/global_search/_test_open_module.js
  23. +0
    -88
      frappe/tests/ui/global_search/_test_search_document.js
  24. +3
    -3
      frappe/tests/ui/test_calendar_view.js
  25. +0
    -62
      frappe/tests/ui/test_list/_test_list_delete.js
  26. +0
    -16
      frappe/tests/ui/test_list/_test_list_values.js
  27. +0
    -27
      frappe/tests/ui/test_list/_test_quick_entry.js
  28. +21
    -17
      frappe/tests/ui/test_list/test_list_filter.js
  29. +14
    -7
      frappe/tests/ui/test_list/test_list_paging.js
  30. +0
    -26
      frappe/tests/ui/test_module/test_module_menu.js
  31. +41
    -0
      frappe/tests/ui/test_module_view.js
  32. +0
    -0
      frappe/tests/ui/test_number_format.js
  33. +13
    -15
      frappe/tests/ui/test_test_runner.py
  34. +5
    -0
      frappe/tests/ui/tests.txt

+ 10
- 5
frappe/contacts/doctype/address/address.js Ver arquivo

@@ -32,10 +32,15 @@ frappe.ui.form.on("Address", {
}
},
after_save: function() {
var last_route = frappe.route_history.slice(-2, -1)[0];
if(frappe.dynamic_link && frappe.dynamic_link.doc
&& frappe.dynamic_link.doc.name == last_route[2]){
frappe.set_route(last_route[0], last_route[1], last_route[2]);
}
frappe.run_serially([
() => frappe.timeout(1),
() => {
var last_route = frappe.route_history.slice(-2, -1)[0];
if(frappe.dynamic_link && frappe.dynamic_link.doc
&& frappe.dynamic_link.doc.name == last_route[2]){
frappe.set_route(last_route[0], last_route[1], last_route[2]);
}
}
]);
}
});

frappe/core/doctype/doctype/boilerplate/_test_controller.js → frappe/core/doctype/doctype/boilerplate/test_controller.js Ver arquivo


+ 3
- 1
frappe/core/doctype/doctype/doctype.py Ver arquivo

@@ -338,7 +338,9 @@ class DocType(Document):
if not self.istable:
make_boilerplate("controller.js", self.as_dict())
#make_boilerplate("controller_list.js", self.as_dict())
make_boilerplate("_test_controller.js", self.as_dict())
if not os.path.exists(frappe.get_module_path(frappe.scrub(self.module),
'doctype', frappe.scrub(self.name), 'tests')):
make_boilerplate("test_controller.js", self.as_dict())

if self.has_web_view:
templates_path = frappe.get_module_path(frappe.scrub(self.module), 'doctype', frappe.scrub(self.name), 'templates')


+ 23
- 0
frappe/core/doctype/test_runner/_test_test_runner.js Ver arquivo

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line

QUnit.test("test: Test Runner", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(1);

frappe.run_serially('Test Runner', [
// insert a new Test Runner
() => frappe.tests.make([
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);

});

+ 15
- 18
frappe/core/doctype/test_runner/test_runner.js Ver arquivo

@@ -32,31 +32,28 @@ frappe.ui.form.on('Test Runner', {
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
};
// 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));

details.assertions.map(a => {
// eslint-disable-next-line
console.log(`${a.result ? '✔' : '✗'} ${a.message}`);
});

// eslint-disable-next-line
console.log(JSON.stringify(result, null, 2));
});
QUnit.load();



+ 31
- 1
frappe/core/doctype/test_runner/test_runner.json Ver arquivo

@@ -42,6 +42,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "app",
"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": "App",
"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,
@@ -83,7 +113,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-12 23:16:15.910891",
"modified": "2017-07-19 03:22:33.221169",
"modified_by": "Administrator",
"module": "Core",
"name": "Test Runner",


+ 6
- 20
frappe/core/doctype/test_runner/test_runner.py Ver arquivo

@@ -13,37 +13,23 @@ class TestRunner(Document):
def get_test_js():
'''Get test + data for app, example: app/tests/ui/test_name.js'''
test_path = frappe.db.get_single_value('Test Runner', 'module_path')
test_js = []

# split
app, test_path = test_path.split(os.path.sep, 1)
test_js = get_test_data(app)

# full path
# now full path
test_path = frappe.get_app_path(app, test_path)

with open(test_path, 'r') as fileobj:
test_js.append(dict(
script = fileobj.read()
))
return test_js

def get_test_data(app):
'''Get the test fixtures from all js files in app/tests/ui/data'''
test_js = []

def add_file(path):
with open(path, 'r') as fileobj:
test_js.append(dict(
script = fileobj.read()
))

data_path = frappe.get_app_path(app, 'tests', 'ui', 'data')
if os.path.exists(data_path):
for fname in os.listdir(data_path):
if fname.endswith('.js'):
add_file(os.path.join(data_path, fname))

if app != 'frappe':
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js'))
# add test_lib.js
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js'))
add_file(test_path)

return test_js


+ 8
- 3
frappe/docs/user/en/guides/automated-testing/qunit-testing.md Ver arquivo

@@ -22,11 +22,16 @@ To run a Test Runner based test, use the `run-ui-tests` bench command by passing

This will pass the filename to `test_test_runner.py` that will load the required JS in the browser and execute the tests

### Adding Fixtures / Test Data
### Debugging Tests

You can also add data that you require for all tests in the `tests/ui/data` folder of your app. All the files in this folder will be loaded in the browser before running the test.
To debug a test, you can open it in the **Test Runner** from your UI and run it manually to see where it is exactly failing.

The file `frappe/tests/ui/data/test_lib.js`, which contains library functions for testing is always loaded.
### Test Sequence

In Frappé UI tests are run in a fixed sequence to ensure dependencies.

The sequence in which the tests will be run will be in `tests/ui/tests.txt`
file.

### Running All UI Tests



+ 1
- 7
frappe/modules/utils.py Ver arquivo

@@ -208,17 +208,11 @@ def make_boilerplate(template, doc, opts=None):
template_name = template.replace("controller", scrub(doc.name))
target_file_path = os.path.join(target_path, template_name)

# allow alternate file paths beginning with _ (e.g. for _test_controller.js)
if template_name.startswith('_'):
alt_target_file_path = os.path.join(target_path, template_name[1:])
else:
alt_target_file_path = target_file_path

if not doc: doc = {}

app_publisher = get_app_publisher(doc.module)

if not os.path.exists(target_file_path) and not os.path.exists(alt_target_file_path):
if not os.path.exists(target_file_path):
if not opts:
opts = {}



+ 1
- 0
frappe/public/build.json Ver arquivo

@@ -125,6 +125,7 @@
"public/js/frappe/misc/common.js",
"public/js/frappe/misc/pretty_date.js",
"public/js/frappe/misc/utils.js",
"public/js/frappe/misc/test_utils.js",
"public/js/frappe/misc/tools.js",
"public/js/frappe/misc/datetime.js",
"public/js/frappe/misc/number_format.js",


+ 1
- 8
frappe/public/js/frappe/dom.js Ver arquivo

@@ -219,13 +219,6 @@ frappe.get_modal = function(title, content) {
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body);
};

frappe._in = function(source, target) {
// returns true if source is in target and both are not empty / falsy
if(!source) return false;
if(!target) return false;
return (target.indexOf(source) !== -1);
};

// add <option> list to <select>
(function($) {
$.fn.add_options = function(options_list) {
@@ -306,4 +299,4 @@ frappe._in = function(source, target) {
}
return this;
}
})(jQuery);
})(jQuery);

+ 3
- 3
frappe/public/js/frappe/form/footer/timeline.js Ver arquivo

@@ -320,9 +320,9 @@ frappe.ui.form.Timeline = Class.extend({
c.show_subject = false;
if(c.subject
&& c.communication_type==="Communication"
&& !frappe._in(this.frm.doc.subject, c.subject)
&& !frappe._in(this.frm.doc.name, c.subject)
&& !frappe._in(this.frm.doc[this.frm.meta.title_field || "name"], c.subject)) {
&& !this.frm.doc.subject.includes(c.subject)
&& !this.frm.doc.name.includes(c.subject)
&& !this.frm.doc[this.frm.meta.title_field || "name"].includes(c.subject)) {
c.show_subject = true;
}
},


+ 41
- 0
frappe/public/js/frappe/misc/test_utils.js Ver arquivo

@@ -0,0 +1,41 @@
// for testing
frappe.click_button = function(text, idx) {
let container = '';
if(typeof idx === 'string') {
container = idx + ' ';
idx = 0;
}
let element = $(`${container}.btn:contains("${text}"):visible`);
if(!element.length) {
throw `did not find any button containing ${text}`;
}
element.get(idx || 0).click();
return frappe.timeout(0.5);
};

frappe.click_link = function(text, idx) {
let element = $(`a:contains("${text}"):visible`);
if(!element.length) {
throw `did not find any link containing ${text}`;
}
element.get(idx || 0).click();
return frappe.timeout(0.5);
};

frappe.set_control= function(fieldname, value) {
let control = $(`.form-control[data-fieldname="${fieldname}"]:visible`);
if(!control.length) {
throw `did not find any control with fieldname ${fieldname}`;
}
control.val(value).trigger('change');
return frappe.timeout(0.5);
};

frappe.click_check = function(label, idx) {
let check = $(`.checkbox:contains("${label}") input`);
if(!check.length) {
throw `did not find any checkbox with label ${label}`;
}
check.get(idx || 0).click();
return frappe.timeout(0.5);
};

+ 0
- 64
frappe/tests/ui/_test_desktop.js Ver arquivo

@@ -1,64 +0,0 @@
QUnit.module('views');

QUnit.test("Verification of navbar menu links", function(assert) {
assert.expect(14);
let done = assert.async();
let navbar_user_items = ['Set Desktop Icons', 'My Settings', 'Reload', 'View Website', 'Background Jobs', 'Logout'];
let modal_and_heading = ['Documentation', 'About'];

frappe.run_serially([
// Goto Desk using button click to check if its working
() => frappe.tests.click_navbar_item('Home'),
() => assert.deepEqual([""], frappe.get_route(), "Routed correctly"),

// Click username on the navbar (Adminisrator) and verify visibility of all elements
() => frappe.tests.click_navbar_item('navbar_user'),
() => navbar_user_items.forEach(function(navbar_user_item) {
assert.ok(frappe.tests.is_visible(navbar_user_item), "Visibility of "+navbar_user_item+" verified");
}),

// Click Help and verify visibility of all elements
() => frappe.tests.click_navbar_item('Help'),
() => modal_and_heading.forEach(function(modal) {
assert.ok(frappe.tests.is_visible(modal), "Visibility of "+modal+" modal verified");
}),

// Goto Desk
() => frappe.tests.click_navbar_item('Home'),
() => frappe.timeout(1),

// Click navbar-username and verify links of all menu items
// Check if clicking on 'Set Desktop Icons' redirects you to the correct page
() => frappe.tests.click_navbar_item('navbar_user'),
() => frappe.tests.click_dropdown_item('Set Desktop Icons'),
() => assert.deepEqual(["modules_setup"], frappe.get_route(), "Routed to 'modules_setup' by clicking on 'Set Desktop Icons'"),
() => frappe.tests.click_navbar_item('Home'),

// Check if clicking on 'My Settings' redirects you to the correct page
() => frappe.tests.click_navbar_item('navbar_user'),
() => frappe.tests.click_dropdown_item('My Settings'),
() => assert.deepEqual(["Form", "User", "Administrator"], frappe.get_route(), "Routed to 'Form, User, Administrator' by clicking on 'My Settings'"),
() => frappe.tests.click_navbar_item('Home'),

// Check if clicking on 'Background Jobs' redirects you to the correct page
() => frappe.tests.click_navbar_item('navbar_user'),
() => frappe.tests.click_dropdown_item('Background Jobs'),
() => assert.deepEqual(["background_jobs"], frappe.get_route(), "Routed to 'background_jobs' by clicking on 'Background Jobs'"),
() => frappe.tests.click_navbar_item('Home'),

// Click Help and check both modals
// Check if clicking 'Documentation' opens the right modal
() => frappe.tests.click_navbar_item('Help'),
() => frappe.tests.click_dropdown_item('Documentation'),
() => assert.ok(frappe.tests.is_visible('Documentation', 'span'), "Documentation modal popped"),
() => frappe.tests.click_button('Close'),

// Check if clicking 'About' opens the right modal
() => frappe.tests.click_navbar_item('Help'),
() => frappe.tests.click_dropdown_item('About'),
() => assert.ok(frappe.tests.is_visible('Frappe Framework', 'div'), "Frappe Framework[About] modal popped"),
() => frappe.tests.click_button('Close'),

() => done()
]);
});

+ 0
- 60
frappe/tests/ui/_test_gantt_view.js Ver arquivo

@@ -1,60 +0,0 @@
QUnit.module('views');

QUnit.test("Gantt View Tests", function(assert) {
assert.expect(2);
let done = assert.async();
let random_text = frappe.utils.get_random(10);
let start_date = frappe.datetime.get_today()+" 16:20:35"; // arbitrary value taken to prevent cases like 12a for 12:00am and 12h to 24h conversion
let end_date = frappe.datetime.get_today()+" 18:30:45"; //arbitrary value taken to prevent cases like 12a for 12:00am and 12h to 24h conversion
let event_id = (text) => {
// Method to acquire the ID of the event created. This is needed to redirect to the event page
$('.bar-label').each(function() {
if ($(this).text().includes(text)){
let init = $(this).text().indexOf('(');
let fin = $(this).text().indexOf(')');
return ($(this).text().substr(init+1,fin-init-1));
}
});
};
let event_title_text = () => {
// Method to check the name of the event created. This is needed to verify the creation and deletion of the event
return $('#bar > g > g.bar-group > text:visible').text();
};

frappe.run_serially([
// Create an event using the Frapee API
() => {
return frappe.tests.make("Event", [
{subject: random_text},
{starts_on: start_date},
{ends_on: end_date},
{event_type: 'Private'}
]);
},

// Check if event is created
() => frappe.set_route(["List", "Event", "Gantt"]),
() => frappe.tests.click_page_head_item("Refresh"),
() => frappe.timeout(1),
() => assert.ok(event_title_text().includes(random_text), "Event title verified"),

// Delete event
() => frappe.set_route(["List", "Event", "Gantt"]),
() => frappe.timeout(1),
// Redirect to the event page to delete the event
() => frappe.set_route(["Form", "Event", event_id(random_text)]),
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),
() => frappe.timeout(1),
() => frappe.set_route(["List", "Event", "Gantt"]),
() => frappe.timeout(1),

// Check if event is deleted
() => frappe.tests.click_page_head_item("Refresh"),
() => frappe.timeout(1),
() => assert.notOk(event_title_text().includes(random_text), "Event deleted"),

() => done()
]);
});

+ 0
- 56
frappe/tests/ui/_test_print_format_builder.js Ver arquivo

@@ -1,56 +0,0 @@
QUnit.module('views');

QUnit.only("Print Format Builder", function(assert) {
assert.expect(0);
let random_text = frappe.utils.get_random(10);
let done = assert.async();
let click_custoize = () => {
return $(`.btn-print-edit`).click();
};

frappe.run_serially([
() => frappe.set_route(["List", "ToDo", "List"]),
() => frappe.timeout(0.3),

() => frappe.new_doc('ToDo'),
() => frappe.quick_entry.dialog.set_value('description', random_text),
() => frappe.quick_entry.insert(),
() => frappe.timeout(0.5),

() => frappe.tests.click_page_head_item('Refresh'),
() => frappe.timeout(0.3),
() => frappe.tests.click_generic_text(random_text),
() => frappe.tests.click_print_logo(),

() => click_custoize(),
() => frappe.timeout(1),
() => $(`div.control-input > input:visible`).val('custom_todo'),
() => frappe.timeout(0.3),

() => frappe.tests.click_generic_text('Start', 'button'),
() => frappe.timeout(1),

() => frappe.tests.click_page_head_item('Save'),

() => frappe.tests.click_generic_text('Edit Properties', 'button'),
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),
() => frappe.timeout(1),

() => frappe.set_route(["List", "ToDo", "List"]),
() => frappe.timeout(0.3),
() => frappe.tests.click_generic_text(random_text),
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),

() => frappe.timeout(2),

() => done()
]);
});


// when you do a pull, hopefully after a merge ... make sure edit the options method in global_search

+ 0
- 16
frappe/tests/ui/data/test_data_for_views.js Ver arquivo

@@ -1,16 +0,0 @@
$.extend(frappe.test_data, {
'User': {
'user1@mail.com': [
{first_name: 'User 1'},
{email: 'user1@mail.com'},
{send_welcome_email: 0}
]
},
'Kanban Board': {
'kanban 1': [
{kanban_board_name: 'kanban 1'},
{reference_doctype: 'ToDo'},
{field_name: 'status'}
]
}
});

+ 22
- 93
frappe/tests/ui/data/test_lib.js Ver arquivo

@@ -100,57 +100,6 @@ frappe.tests = {
return frappe.run_serially(tasks);
}]);
},
click_and_wait: (button, obj=0.1) => {
return frappe.run_serially([
() => {
//check if obj value is passed
if (obj == 0.1)
$(button).click();
else
$(button)[obj].click();
},
() => frappe.timeout(0.5)
]);
},
create_todo: (todo_needed) => {
let status_list = ['Closed', 'Open'];
let priority_list = ['Low', 'Medium', 'High'];
let date_list = ['2017-05-05', '2017-06-06', '2017-07-07', '2017-08-08'];
let owner_list = ['Administrator', 'user1@mail.com'];
let i;
let num_of_todo;
let tasks = [];

return frappe.run_serially([
() => frappe.set_route('List', 'ToDo', 'List'),
() => {
//remove todo filters
for (i=1; i<=5; i++)
$('.col-md-2:nth-child('+i+') .input-sm').val('');
},
() => cur_list.page.btn_secondary.click(),
() => frappe.timeout(0.5),
() => num_of_todo = cur_list.data.length,//todo present
() => {
if (num_of_todo < todo_needed)
{
for (i=0; i<(todo_needed-num_of_todo); i+=1)
{
tasks.push(() => frappe.tests.make("ToDo", [
{description: 'ToDo for testing'},
{status: status_list[i%2]},
{priority: priority_list[i%3]},
{date: date_list[i%4]},
{owner: owner_list[i%2]}
]));
tasks.push(() => i+=1);
}
i=0;
}
},
() => frappe.run_serially(tasks)
]);
},
click_page_head_item: (text) => {
// Method to items present on the page header like New, Save, Delete etc.
let possible_texts = ["New", "Delete", "Save", "Yes"];
@@ -177,33 +126,6 @@ frappe.tests = {
() => frappe.timeout(1)
]);
},
click_navbar_item: (text) => {
// Method to click an elements present on the navbar
return frappe.run_serially([
() => {
if (text == "Help"){
$(`.dropdown-help .dropdown-toggle:visible`).click();
}
else if (text == "navbar_user"){
$(`.dropdown-navbar-user .dropdown-toggle:visible`).click();
}
else if (text == "Notification"){
$(`.navbar-new-comments`).click();
}
else if (text == "Home"){
$(`.erpnext-icon`).click();
}
},
() => frappe.timeout(1)
]);
},
click_generic_text: (text, tag='a') => {
// Method to click an element by its name
return frappe.run_serially([
() => $(`${tag}:contains("${text}"):visible`)[0].click(),
() => frappe.timeout(1)
]);
},
click_desktop_icon: (text) => {
// Method to click the desktop icons on the Desk, by their name
return frappe.run_serially([
@@ -215,22 +137,29 @@ frappe.tests = {
// Method to check the visibility of an element
return $(`${tag}:contains("${text}")`).is(`:visible`);
},
close_modal: () => {
// Close the modal on the screen
$(`a.close`).click();
},
click_print_logo: () => {
return frappe.run_serially([
() => $(`.fa-print`).click(),
() => frappe.timeout(1)
]);
},
click_button: function(text) {
$(`.btn:contains("${text}"):visible`).click();
return frappe.timeout(1);
let element = $(`.btn:contains("${text}"):visible`);
if(!element.length) {
throw `did not find any button containing ${text}`;
}
element.click();
return frappe.timeout(0.5);
},
click_link: function(text) {
$(`a:contains("${text}"):visible`).click();
return frappe.timeout(1);
}
let element = $(`a:contains("${text}"):visible`);
if(!element.length) {
throw `did not find any link containing ${text}`;
}
element.get(0).click();
return frappe.timeout(0.5);
},
set_control: function(fieldname, value) {
let control = $(`.form-control[data-fieldname="${fieldname}"]:visible`);
if(!control.length) {
throw `did not find any control with fieldname ${fieldname}`;
}
control.val(value).trigger('change');
return frappe.timeout(0.5);
},

};

+ 0
- 44
frappe/tests/ui/global_search/_test_list_document.js Ver arquivo

@@ -1,44 +0,0 @@
QUnit.module('views');

QUnit.test("List a document type", function(assert) {
assert.expect(2);
let done = assert.async();
let option_number=0;
let awesome_search = $('#navbar-search').get(0);
let options = () => {
// Method to return the available options after search
return $('body > div.main-section > header > div > div > div.hidden-xs > form > div > div > ul > li').each(function (){});
};

frappe.run_serially([
// Goto Home using button click to check if its working
() => frappe.set_route(),
() => frappe.timeout(1),

() => $('#navbar-search').focus(),
() => $('#navbar-search').val('customer'),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
assert.ok(frappe.tests.is_visible("Search for 'customer'"), "'Search for 'customer'' is visible!");
if (frappe.tests.is_visible("Search for 'customer'")){
let search_options = options();
// Iterate over all available options till you reach "Search for 'customer'""
for (option_number=0; option_number<search_options.length; option_number++)
if ($(search_options[option_number]).text().includes("Search for 'customer'"))
break;
}
},
// Highlight the "Search for 'customer'" option
() => awesome_search.awesomplete.goto(option_number),
// Click the highlighted option
() => awesome_search.awesomplete.select(),
() => frappe.timeout(1),
// Verify if the modal is correct
() => assert.ok(frappe.tests.is_visible('Customer will have a table tabCustomer associated with it', 'p'), "Correct modal for 'customer' was called"),
() => frappe.timeout(1),
() => frappe.tests.close_modal(),

() => done()
]);
});

+ 0
- 64
frappe/tests/ui/global_search/_test_math.js Ver arquivo

@@ -1,64 +0,0 @@
QUnit.module('views');

QUnit.test("Math Calculations", function(assert) {
assert.expect(5);
let done = assert.async();
let awesome_search = $('#navbar-search').get(0);
let random_number_1 = (Math.random()*100).toFixed(2);
let random_number_2 = (Math.random()*100).toFixed(2);
let operations = ['+', '-', '/', '*'];

frappe.run_serially([
// Goto Home using button click to check if its working
() => frappe.set_route(),
() => frappe.timeout(1),

() => $('#navbar-search').focus(),
() => $('#navbar-search').val(random_number_1+operations[0]+random_number_2),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
let num_str = random_number_1+operations[0]+random_number_2+' = '+parseFloat(parseFloat(random_number_1)+parseFloat(random_number_2));
assert.ok(frappe.tests.is_visible(num_str), "Math operation '"+operations[0]+"' is correct");
},

() => $('#navbar-search').focus(),
() => $('#navbar-search').val(random_number_1+operations[1]+random_number_2),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
let num_str = random_number_1+operations[1]+random_number_2+' = '+parseFloat(parseFloat(random_number_1)-parseFloat(random_number_2));
assert.ok(frappe.tests.is_visible(num_str), "Math operation '"+operations[1]+"' is correct");
},

() => $('#navbar-search').focus(),
() => $('#navbar-search').val(random_number_1+operations[2]+random_number_2),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
let num_str = random_number_1+operations[2]+random_number_2+' = '+parseFloat(parseFloat(random_number_1)/parseFloat(random_number_2));
assert.ok(frappe.tests.is_visible(num_str), "Math operation '"+operations[2]+"' is correct");
},

() => $('#navbar-search').focus(),
() => $('#navbar-search').val(random_number_1+operations[3]+random_number_2),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
let num_str = random_number_1+operations[3]+random_number_2+' = '+parseFloat(parseFloat(random_number_1)*parseFloat(random_number_2));
assert.ok(frappe.tests.is_visible(num_str), "Math operation '"+operations[3]+"' is correct");
},

() => $('#navbar-search').focus(),
() => $('#navbar-search').val("=Math.sin(Math.PI/2)"),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => assert.ok(frappe.tests.is_visible("Math.sin(Math.PI/2) = 1"), "Math operation 'sin' evaluated correctly"),

// Close the modal
() => awesome_search.awesomplete.select(),
() => frappe.tests.close_modal(),

() => done()
]);
});

+ 0
- 55
frappe/tests/ui/global_search/_test_new_record.js Ver arquivo

@@ -1,55 +0,0 @@
QUnit.module('views');

QUnit.test("Make a new record", function(assert) {
assert.expect(3);
let done = assert.async();
let option_number=0;
let awesome_search = $('#navbar-search').get(0);
let random_text = frappe.utils.get_random(10);
let options = () => {
// Method to return the available options after search
return $('body > div.main-section > header > div > div > div.hidden-xs > form > div > div > ul > li').each(function (){});
};
let todo_title_text = () => {
// Method to return the title of the todo visible
return $("div.list-item__content.ellipsis.list-item__content--flex-2 > a:visible").text();
};

frappe.run_serially([
// Goto Home using button click to check if its working
() => frappe.set_route(),
() => frappe.timeout(1),

() => $('#navbar-search').focus(),
() => $('#navbar-search').val('ToDo'),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
assert.ok(frappe.tests.is_visible('New ToDo'), "'New ToDo' is visible!");
if (frappe.tests.is_visible('New ToDo')){
let search_options = options();
// Iterate over all available options till you reach 'New ToDo'
for (option_number=0; option_number<search_options.length; option_number++)
if ($(search_options[option_number]).text().includes('New ToDo'))
break;
}
},
// Highlight the 'New ToDo' option
() => awesome_search.awesomplete.goto(option_number),
// Click the highlighted option
() => awesome_search.awesomplete.select(),
() => frappe.timeout(1),
() => frappe.quick_entry.dialog.set_value('description', random_text),
() => frappe.quick_entry.insert(),
() => frappe.timeout(1),
() => frappe.set_route(["List", "ToDo", "List"]),
() => frappe.timeout(1),
// Verify if the todo is created
() => frappe.tests.click_page_head_item("Refresh"),
() => frappe.timeout(1),
() => assert.ok(todo_title_text().includes(random_text), "New ToDo was created successfully"),
() => assert.deepEqual(["List", "ToDo", "List"], frappe.get_route(), "Successfully routed to 'ToDo List'"),

() => done()
]);
});

+ 0
- 42
frappe/tests/ui/global_search/_test_open_module.js Ver arquivo

@@ -1,42 +0,0 @@
QUnit.module('views');

QUnit.test("Open a module or tool", function(assert) {
assert.expect(2);
let done = assert.async();
let option_number=0;
let awesome_search = $('#navbar-search').get(0);
let options = () => {
// Method to return the available options after search
return $('body > div.main-section > header > div > div > div.hidden-xs > form > div > div > ul > li').each(function (){});
};

frappe.run_serially([
// Goto Home using button click to check if its working
() => frappe.set_route(),
() => frappe.timeout(1),

() => $('#navbar-search').focus(),
() => $('#navbar-search').val('ToDo'),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
assert.ok(frappe.tests.is_visible('ToDo List'), "'ToDo List' is visible!");
if (frappe.tests.is_visible('ToDo List')){
let search_options = options();
// Iterate over all available options till you reach 'ToDo List'
for (option_number=0; option_number<search_options.length; option_number++)
if ($(search_options[option_number]).text().includes('ToDo List'))
break;
}
},
// Highlight the 'ToDo List' option
() => awesome_search.awesomplete.goto(option_number),
// Click the highlighted option
() => awesome_search.awesomplete.select(),
() => frappe.timeout(1),
// Verify if the redirected route is correct
() => assert.deepEqual(["List", "ToDo", "List"], frappe.get_route(), "Successfully routed to 'ToDo List'"),

() => done()
]);
});

+ 0
- 88
frappe/tests/ui/global_search/_test_search_document.js Ver arquivo

@@ -1,88 +0,0 @@
QUnit.module('views');

QUnit.test("Search in a document type", function(assert) {
assert.expect(3);
let done = assert.async();
let option_number=0;
let awesome_search = $('#navbar-search').get(0);
let random_text = "argo1234";
let options = () => {
// Method to return the available options after search
return $('body > div.main-section > header > div > div > div.hidden-xs > form > div > div > ul > li').each(function (){});
};
let todo_title_text = () => {
// Method to return the title of the todo visible
return $("div.list-item__content.ellipsis.list-item__content--flex-2 > a:visible").text();
};
let select_all_todo = () => {
$('div.list-item__content.ellipsis.text-muted.list-item__content--flex-2 > input:visible').click();
};
let remove_all_filters = () => {
$('button.remove-filter > i').click();
};

frappe.run_serially([
// Goto Home using button click to check if its working
() => frappe.set_route(),
() => frappe.timeout(1),

() => $('#navbar-search').focus(),
() => $('#navbar-search').val('ToDo'),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
assert.ok(frappe.tests.is_visible('New ToDo'), "'New ToDo' is visible!");
if (frappe.tests.is_visible('New ToDo')){
let search_options = options();
// Iterate over all available options till you reach 'New ToDo'
for (option_number=0; option_number<search_options.length; option_number++)
if ($(search_options[option_number]).text().includes('New ToDo'))
break;
}
},
// Highlight the 'New ToDo' option
() => awesome_search.awesomplete.goto(option_number),
// Click the highlighted option
() => awesome_search.awesomplete.select(),
() => frappe.timeout(1),
() => frappe.quick_entry.dialog.set_value('description', random_text),
() => frappe.quick_entry.insert(),
() => frappe.timeout(1),

// Search for the created ToDo in global search
() => frappe.set_route(["List", "ToDo", "List"]),
() => frappe.timeout(1),
() => $('#navbar-search').focus(),
() => $('#navbar-search').val('argo1234'),
() => $('#navbar-search').focus(),
() => frappe.timeout(1),
() => {
assert.ok(frappe.tests.is_visible('Find argo1234 in ToDo'), "'Find argo1234 in ToDo' is visible!");
if (frappe.tests.is_visible('Find argo1234 in ToDo')){
let search_options = options();
// Iterate over all available options till you reach 'New ToDo'
for (option_number=0; option_number<search_options.length; option_number++)
if ($(search_options[option_number]).text().includes('Find argo1234 in ToDo'))
break;
}
},
// Highlight the 'Find argo1234 in ToDo' option
() => awesome_search.awesomplete.goto(option_number),
// Click the highlighted option
() => awesome_search.awesomplete.select(),
() => frappe.timeout(1),
// Verify if the 'argo1234' is the only ToDo
() => assert.ok(todo_title_text().includes('argo1234'), "'argo1234' is the only visible ToDo"),

// Remove all filters
() => remove_all_filters(),
() => frappe.timeout(1),
// Delete all ToDo
() => select_all_todo(),
() => frappe.timeout(1),
() => frappe.tests.click_page_head_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),

() => done()
]);
});

+ 3
- 3
frappe/tests/ui/test_calendar_view.js Ver arquivo

@@ -62,16 +62,16 @@ QUnit.test("Calendar View Tests", function(assert) {
() => frappe.set_route(["List", "Event", "Calendar"]),
() => frappe.timeout(1),
// delete event
() => frappe.tests.click_generic_text(random_text + ':Pub'),
() => frappe.click_link(random_text + ':Pub'),
() => {
frappe.tests.click_page_head_item('Menu');
frappe.tests.click_dropdown_item('Delete');
},
() => frappe.timeout(0.5),
() => frappe.tests.click_button('Yes'),
() => frappe.click_button('Yes'),
() => frappe.timeout(2),
() => frappe.set_route(["List", "Event", "Calendar"]),
() => frappe.tests.click_button("Refresh"),
() => frappe.click_button("Refresh"),
() => frappe.timeout(1),

// Check if event is deleted


+ 0
- 62
frappe/tests/ui/test_list/_test_list_delete.js Ver arquivo

@@ -1,62 +0,0 @@
QUnit.module('views');

QUnit.test("Test deletion of one list element [List view]", function(assert) {
assert.expect(3);
let done = assert.async();
let count;
let random;

frappe.run_serially([
() => frappe.tests.setup_doctype('User'),
() => frappe.tests.create_todo(2),
() => frappe.set_route('List', 'ToDo', 'List'),
() => frappe.timeout(0.5),
() => {
assert.deepEqual(['List', 'ToDo', 'List'], frappe.get_route(), "List opened successfully.");
//total list elements
count = cur_list.data.length;
random = Math.floor(Math.random() * (count) + 1);
//select one element randomly
$('div:nth-child('+random+')>div>div>.list-row-checkbox').click();
},
() => cur_list.page.btn_primary.click(),
() => frappe.timeout(0.5),
() => {
//check if asking for confirmation and click yes
assert.equal("Confirm", cur_dialog.title, "Asking for confirmation.");
cur_dialog.primary_action(frappe.confirm);
},
() => frappe.timeout(1),
//check if total elements decreased by one
() => assert.equal(cur_list.data.length, (count-1), "Only one element is deleted."),
() => done()
]);
});

QUnit.test("Test deletion of all list element [List view]", function(assert) {
assert.expect(3);
let done = assert.async();

frappe.run_serially([
() => frappe.tests.setup_doctype('User'),
() => frappe.tests.create_todo(5),
() => frappe.set_route('List', 'ToDo', 'List'),
() => frappe.timeout(0.5),
() => {
assert.deepEqual(['List', 'ToDo', 'List'], frappe.get_route(), "List opened successfully.");
//select all element
$('.list-select-all.hidden-xs').click();
},
() => cur_list.page.btn_primary.click(),
() => frappe.timeout(0.5),
() => {
assert.equal("Confirm", cur_dialog.title, "Asking for confirmation.");
//click yes for deletion
cur_dialog.primary_action(frappe.confirm);
},
() => frappe.timeout(2),
//check zero elements left
() => assert.equal(cur_list.data.length, 0, "No element is present in list."),
() => done()
]);
});

+ 0
- 16
frappe/tests/ui/test_list/_test_list_values.js Ver arquivo

@@ -1,16 +0,0 @@
QUnit.module('views');

QUnit.test("Test list values [List view]", function(assert) {
assert.expect(2);
let done = assert.async();

frappe.run_serially([
() => frappe.set_route('List', 'DocType'),
() => frappe.timeout(2),
() => {
assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route(), "Routed to DocType List");
assert.ok($('.list-item:visible').length > 10, "More than 10 items visible in DocType List");
},
() => done()
]);
});

+ 0
- 27
frappe/tests/ui/test_list/_test_quick_entry.js Ver arquivo

@@ -1,27 +0,0 @@
QUnit.module('views');

QUnit.only("Test quick entry [List view]", function(assert) {
assert.expect(2);
let done = assert.async();
let random_text = 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_text),
() => frappe.quick_entry.insert(),
(doc) => {
assert.ok(doc && !doc.__islocal, "Document exists");
return frappe.set_route('Form', 'ToDo', doc.name);
},
() => assert.ok(cur_frm.doc.description.includes(random_text), "ToDo created"),

// Delete the created ToDo
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),
() => frappe.timeout(2),

() => done()
]);
});

+ 21
- 17
frappe/tests/ui/test_list/test_list_filter.js Ver arquivo

@@ -1,34 +1,38 @@
QUnit.module('views');

QUnit.test("Test filters [List view]", function(assert) {
assert.expect(2);
QUnit.test("Test list filters", function(assert) {
assert.expect(3);
let done = assert.async();

frappe.run_serially([
() => frappe.tests.setup_doctype('User'),
() => frappe.tests.create_todo(6),
() => {
return frappe.tests.make('ToDo', [
{description: 'low priority'},
{priority: 'Low'}
]);
},
() => {
return frappe.tests.make('ToDo', [
{description: 'high priority'},
{priority: 'High'}
]);
},
() => frappe.set_route('List', 'ToDo', 'List'),
() => frappe.timeout(0.5),
() => {
assert.deepEqual(['List', 'ToDo', 'List'], frappe.get_route(), "List opened successfully.");
assert.deepEqual(['List', 'ToDo', 'List'], frappe.get_route(),
"List opened successfully.");
//set filter values
$('.col-md-2:nth-child(2) .input-sm').val('Closed');
$('.col-md-2:nth-child(3) .input-sm').val('Low');
$('.col-md-2:nth-child(4) .input-sm').val('05-05-2017');
$('.col-md-2:nth-child(5) .input-sm').val('Administrator');
return frappe.set_control('priority', 'Low');
},
() => frappe.timeout(0.5),
() => cur_list.page.btn_secondary.click(),
() => frappe.timeout(1),
() => {
//get total list element
var count = cur_list.data.length;
//check if all elements are as per filter
var i=0;
for ( ; i < count ; i++)
if ((cur_list.data[i].status!='Closed')||(cur_list.data[i].priority!='Low')||(cur_list.data[i].owner!='Administrator')||(cur_list.data[i].date!='2017-05-05'))
break;
assert.equal(count, i, "Elements present have content according to filters.");
assert.equal(cur_list.data[0].priority, 'Low',
'visible element has low priority');
let non_low_items = cur_list.data.filter(d => d.priority != 'Low');
assert.equal(non_low_items.length, 0, 'no item without low priority');
},
() => done()
]);

+ 14
- 7
frappe/tests/ui/test_list/test_list_paging.js Ver arquivo

@@ -1,18 +1,25 @@
QUnit.module('views');

QUnit.test("Test paging in list [List view]", function(assert) {
assert.expect(3);
QUnit.test("Test paging in list view", function(assert) {
assert.expect(5);
let done = assert.async();

frappe.run_serially([
() => frappe.set_route('List', 'DocType'),
() => frappe.timeout(0.5),
() => assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route(), "List opened successfully."),
() => assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route(),
"List opened successfully."),
//check elements less then page length [20 in this case]
() => assert.ok(cur_list.data.length <= cur_list.page_length, "20 or less elements are visible."),
() => frappe.tests.click_and_wait('.btn-sm:contains("100"):visible'),
//check elements less then page length [100 in this case]
() => assert.ok(cur_list.data.length <= cur_list.page_length, "100 or less elements are visible."),
() => assert.equal(cur_list.data.length, 20, 'show 20 items'),
() => frappe.click_button('More'),
() => frappe.timeout(2),
() => assert.equal(cur_list.data.length, 40, 'show more items'),
() => frappe.click_button('100', '.btn-group-paging'),
() => frappe.timeout(2),
() => assert.ok(cur_list.data.length > 40, 'show 100 items'),
() => frappe.click_button('20', '.btn-group-paging'),
() => frappe.timeout(2),
() => assert.equal(cur_list.data.length, 20, 'show 20 items again'),
() => done()
]);
});

+ 0
- 26
frappe/tests/ui/test_module/test_module_menu.js Ver arquivo

@@ -1,26 +0,0 @@
QUnit.module('views');

QUnit.test("Test sidebar menu [Module view]", function(assert) {
assert.expect(2);
let done = assert.async();
let sidebar_opt = '.module-link:not(".active")';
let random_num;
let module_name;

frappe.run_serially([
//testing click on module name in side bar
() => frappe.set_route(['modules']),
() => frappe.timeout(1),
() => assert.deepEqual(['modules'], frappe.get_route(), "Module view opened successfully."),
() => {
//randomly choosing one module (not active)
var count = $(sidebar_opt).length;
random_num = Math.floor(Math.random() * (count) + 1);
module_name = $(sidebar_opt)[random_num].innerText;
},
() => frappe.tests.click_and_wait(sidebar_opt, random_num),
() => frappe.timeout(2),
() => assert.equal($('.title-text:visible')[0].innerText, module_name, "Module opened successfully using sidebar"),
() => done()
]);
});

+ 41
- 0
frappe/tests/ui/test_module_view.js Ver arquivo

@@ -0,0 +1,41 @@
QUnit.module('views');

QUnit.test("Test modules view", function(assert) {
assert.expect(4);
let done = assert.async();

frappe.run_serially([

//click Document Share Report in Permissions section [Report]
() => frappe.set_route("modules", "Setup"),
() => frappe.timeout(0.5),
() => frappe.click_link('Document Share Report'),
() => assert.deepEqual(frappe.get_route(), ["Report", "DocShare", "Document Share Report"],
'document share report'),

//click Print Setting in Printing section [Form]
() => frappe.set_route("modules", "Setup"),
() => frappe.timeout(0.5),
() => frappe.click_link('Print Settings'),
() => assert.deepEqual(frappe.get_route(), ["Form", "Print Settings"],
'print settings'),

//click Workflow Action in Workflow section [List]
() => frappe.set_route("modules", "Setup"),
() => frappe.timeout(0.5),
() => frappe.click_link('Workflow Action'),
() => assert.deepEqual(frappe.get_route(), ["List", "Workflow Action", "List"],
'workflow action'),

//click Workflow Action in Workflow section [List]
() => frappe.set_route("modules"),
() => frappe.timeout(0.5),
() => frappe.click_link('Tools'),
() => frappe.timeout(0.5),
() => frappe.click_link('To Do'),
() => assert.deepEqual(frappe.get_route(), ["List", "ToDo", "List"],
'todo list'),

() => done()
]);
});

frappe/public/js/frappe/misc/tests/test_number_format.js → frappe/tests/ui/test_number_format.js Ver arquivo


+ 13
- 15
frappe/tests/ui/test_test_runner.py Ver arquivo

@@ -7,6 +7,8 @@ class TestTestRunner(unittest.TestCase):
driver = TestDriver()
driver.login()
for test in get_tests():
if test.startswith('#'):
continue
print('Running {0}...'.format(test))
frappe.db.set_value('Test Runner', None, 'module_path', test)
frappe.db.commit()
@@ -26,11 +28,16 @@ class TestTestRunner(unittest.TestCase):

def get_tests():
'''Get tests base on flag'''
if frappe.flags.ui_test_app:
return get_tests_for(frappe.flags.ui_test_app)
elif frappe.flags.ui_test_path:
frappe.db.set_value('Test Runner', None, 'app', frappe.flags.ui_test_app or '')

if frappe.flags.ui_test_path:
# specific test
return (frappe.flags.ui_test_path,)
elif frappe.flags.ui_test_app:
# specific app
return get_tests_for(frappe.flags.ui_test_app)
else:
# all apps
tests = []
for app in frappe.get_installed_apps():
tests.extend(get_tests_for(app))
@@ -39,18 +46,9 @@ def get_tests():
def get_tests_for(app):
'''Get all tests for a particular app'''
tests = []
tests_path = frappe.get_app_path(app)
tests_path = frappe.get_app_path(app, 'tests', 'ui', 'tests.txt')
if os.path.exists(tests_path):
for basepath, folders, files in os.walk(tests_path): # pylint: disable=unused-variable
if os.path.join('ui', 'data') in basepath:
continue

for fname in files:
if (fname.startswith('test_') and fname.endswith('.js')
and fname != 'test_runner.js'):
path = os.path.join(basepath, fname)
path = os.path.relpath(path, frappe.get_app_path(app))
tests.append(os.path.join(app, path))

with open(tests_path, 'r') as fileobj:
tests = fileobj.read().strip().splitlines()
return tests


+ 5
- 0
frappe/tests/ui/tests.txt Ver arquivo

@@ -0,0 +1,5 @@
frappe/tests/ui/test_number_format.js
frappe/tests/ui/test_list/test_list_filter.js
frappe/tests/ui/test_list/test_list_paging.js
frappe/tests/ui/test_module_view.js
frappe/tests/ui/test_calendar_view.js

Carregando…
Cancelar
Salvar