diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index d3343a56a3..aaf7137ea9 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -93,6 +93,7 @@ context("Dynamic Link", () => { it("Creating a dynamic link and verifying it in a dialog", () => { get_dialog_with_dynamic_link().as("dialog"); + cy.wait(500); cy.get_field("doc_type").clear(); cy.fill_field("doc_type", "User", "Link"); cy.get_field("doc_id").click(); @@ -106,7 +107,7 @@ context("Dynamic Link", () => { cy.get(".btn-modal-close").click({ force: true, multiple: true }); }); - it("Creating a dynamic link and verifying it", () => { + it("Shows dynamic link options in list filters", () => { cy.visit("/desk/test-dynamic-link"); //Clicking on the Document ID field @@ -122,7 +123,9 @@ context("Dynamic Link", () => { .find("div") .its("length") .should("be.gte", 0); + }); + it("Shows dynamic link options in new form", () => { //Opening a new form for dynamic link doctype cy.new_form("Test Dynamic Link"); cy.get_field("doc_type").clear(); @@ -138,14 +141,14 @@ context("Dynamic Link", () => { .its("length") .should("be.gte", 0); cy.get_field("doc_type").clear(); + }); + + it("Shows error when invalid DocType is passed", () => { + cy.new_form("Test Dynamic Link"); + cy.get_field("doc_type").clear(); //Entering System Settings in the Doctype field - cy.intercept("/api/method/frappe.desk.search.search_link").as("search_query"); cy.fill_field("doc_type", "System Settings", "Link", { delay: 500 }); - cy.wait("@search_query"); - cy.get(`[data-fieldname="doc_type"] ul:visible div:first-child`).click({ - scrollBehavior: false, - }); cy.get_field("doc_id").click(); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 2633a5bca8..2e2cf92deb 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -62,12 +62,13 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("todo for link", { delay: 200 }); + cy.wait(500); + cy.get("@input").type("todo for link", { delay: 100 }); cy.wait("@search_link"); cy.wait(500); - cy.get(".frappe-control[data-fieldname=link]").findByRole("listbox").should("be.visible"); - cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); - cy.get(".frappe-control[data-fieldname=link] input").blur(); + cy.get("@input").parent().findByRole("listbox").should("be.visible"); + cy.get("@input").type("{enter}"); + cy.get("@input").blur(); cy.get("@dialog").then((dialog) => { cy.get("@todos").then((todos) => { let value = dialog.get_value("link"); @@ -80,24 +81,27 @@ context("Control Link", () => { get_dialog_with_link().as("dialog"); cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link"); - - cy.get(".frappe-control[data-fieldname=link] input") - .type("invalid value", { delay: 100 }) - .blur(); - cy.wait("@validate_link"); + cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); + cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); cy.wait(500); - cy.get(".frappe-control[data-fieldname=link] input").should("have.value", ""); + cy.get("@input").type("invalid value", { delay: 100 }).blur(); + cy.wait("@validate_link"); + cy.get("@input").should("have.value", ""); }); it("should be possible set empty value explicitly", () => { get_dialog_with_link().as("dialog"); cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link"); + cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); - cy.get(".frappe-control[data-fieldname=link] input").type(" ", { delay: 100 }).blur(); - cy.wait("@validate_link"); + cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); cy.wait(500); - cy.get(".frappe-control[data-fieldname=link] input").should("have.value", ""); + cy.get("@input").type(" ", { delay: 100 }).blur(); + cy.wait("@validate_link"); + cy.get("@input").should("have.value", ""); cy.window() .its("cur_dialog") .then((dialog) => { @@ -111,10 +115,10 @@ context("Control Link", () => { cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); cy.get("@todos").then((todos) => { - cy.get(".frappe-control[data-fieldname=link] input").as("input"); - cy.get("@input").focus(); + cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type(todos[0]).blur(); + cy.wait(500); + cy.get("@input").type(todos[0], { delay: 100 }).blur(); // not waiting for validate_link because it will not get called cy.get("@input").trigger("mouseover"); cy.get(".frappe-control[data-fieldname=link] .btn-open") @@ -156,11 +160,13 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("todo for link", { delay: 200 }); + cy.wait(500); + cy.get("@input").type("todo for link", { delay: 100 }); cy.wait("@search_link"); + cy.wait(500); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); - cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); - cy.get(".frappe-control[data-fieldname=link] input").blur(); + cy.get("@input").type("{enter}"); + cy.get("@input").blur(); cy.get("@dialog").then((dialog) => { cy.get("@todos").then((todos) => { let field = dialog.get_field("link"); @@ -179,13 +185,18 @@ context("Control Link", () => { cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link"); - cy.get(".frappe-control[data-fieldname=assigned_by] input").focus().as("input"); - cy.get("@input").clear().type(cy.config("testUser"), { delay: 300 }).blur(); - cy.wait("@validate_link"); - cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should( - "contain", - "Frappe" - ); + cy.fill_field("assigned_by", cy.config("testUser"), "Link"); + cy.call("frappe.client.get_value", { + doctype: "User", + filters: { + name: cy.config("testUser"), + }, + fieldname: "full_name", + }).then((r) => { + cy.get( + ".frappe-control[data-fieldname=assigned_by_full_name] .control-value" + ).should("contain", r.message.full_name); + }); cy.window().its("cur_frm.doc.assigned_by").should("eq", cy.config("testUser")); @@ -234,10 +245,19 @@ context("Control Link", () => { cy.new_form("ToDo"); cy.fill_field("description", "new", "Text Editor").blur().wait(200); cy.save(); - cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should( - "contain", - "Frappe" - ); + cy.call("frappe.client.get_value", { + doctype: "User", + filters: { + name: cy.config("testUser"), + }, + fieldname: "full_name", + }).then((r) => { + cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + r.message.full_name + ); + }); + // if user clears default value explicitly, system should not reset default again cy.get_field("assigned_by").clear().blur(); cy.save(); @@ -264,11 +284,11 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("Sonstiges", { delay: 200 }); + cy.get("@input").type("Sonstiges", { delay: 100 }); cy.wait("@search_link"); cy.wait(500); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); - cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".frappe-control[data-fieldname=link] input").type("{enter}"); cy.get(".frappe-control[data-fieldname=link] input").blur(); cy.get("@dialog").then((dialog) => { let field = dialog.get_field("link"); @@ -296,11 +316,11 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("Non-Conforming", { delay: 200 }); + cy.get("@input").type("Non-Conforming", { delay: 100 }); cy.wait("@search_link"); cy.wait(500); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); - cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".frappe-control[data-fieldname=link] input").type("{enter}"); cy.get(".frappe-control[data-fieldname=link] input").blur(); cy.get("@dialog").then((dialog) => { let field = dialog.get_field("link"); diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js index a8cd4f767f..a072240993 100644 --- a/cypress/integration/customize_form.js +++ b/cypress/integration/customize_form.js @@ -4,8 +4,7 @@ context("Customize Form", () => { cy.visit("/desk/customize-form"); }); it("Changing to naming rule should update autoname", () => { - cy.fill_field("doc_type", "ToDo", "Link").blur(); - cy.wait(2000); + cy.fill_field("doc_type", "ToDo", "Link"); cy.findByRole("tab", { name: "Details" }).click(); cy.click_form_section("Naming"); const naming_rule_default_autoname_map = { diff --git a/cypress/integration/dashboard_chart.js b/cypress/integration/dashboard_chart.js index 487a0ea42e..8a2fb91d08 100644 --- a/cypress/integration/dashboard_chart.js +++ b/cypress/integration/dashboard_chart.js @@ -8,13 +8,13 @@ context("Dashboard Chart", () => { cy.new_form("Dashboard Chart"); cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); - cy.get_field("document_type", "Link"); - cy.fill_field("document_type", "Workspace Link", "Link").focus().blur(); - cy.get_field("document_type", "Link").should("have.value", "Workspace Link"); - + cy.get_field("chart_name", "Data").should("be.visible"); cy.fill_field("chart_name", "Test Chart", "Data"); + cy.fill_field("document_type", "Workspace Link", "Link"); + + cy.get('[data-fieldname="filters_json"]').click(); + cy.get(".modal-dialog", { timeout: 500 }).should("be.visible"); - cy.get('[data-fieldname="filters_json"]').click().wait(200); cy.get(".modal-body .filter-action-buttons .add-filter").click(); cy.get(".modal-body .fieldname-select-area").click(); cy.get(".modal-actions .btn-modal-close").click(); diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js index da4b9e75fb..8677710bc4 100644 --- a/cypress/integration/form_builder.js +++ b/cypress/integration/form_builder.js @@ -49,23 +49,34 @@ context("Form Builder", () => { cy.get(".modal-body .clear-filters").click(); cy.get(".modal-body .filter-action-buttons .add-filter").click(); cy.wait(100); - cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input").type( - "Male" - ); + + cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); + cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input") + .focus() + .as("input"); + cy.wait("@search_link"); + cy.wait(500); + cy.get("@input").type("Male", { delay: 100 }); + cy.wait("@search_link"); + cy.wait(500); + cy.get("@input").type("{enter}", { delay: 100 }); + cy.get("@input").blur(); + cy.get(".btn-modal-primary").click(); + cy.wait(500); // Save the document cy.click_doc_primary_button("Save"); + cy.wait(1000); - // Open a new Form - cy.new_form(doctype_name); - // Click on the "salutation" field - cy.get_field("gender").clear().click(); - - cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); - cy.wait("@search_link").then((data) => { - expect(data.response.body.message.length).to.eq(1); - expect(data.response.body.message[0].value).to.eq("Male"); + cy.compare_document({ + fields: [ + {}, + { + fieldname: "gender", + link_filters: '[["Gender","name","=","Male"]]', + }, + ], }); }); diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js index c3e3edcf5e..fd7b6a9d9d 100644 --- a/cypress/integration/number_card.js +++ b/cypress/integration/number_card.js @@ -9,7 +9,7 @@ context("Number Card", () => { cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); cy.get_field("document_type", "Link"); - cy.fill_field("document_type", "Workspace Link", "Link").focus().blur(); + cy.fill_field("document_type", "Workspace Link", "Link"); cy.get_field("document_type", "Link").should("have.value", "Workspace Link"); cy.fill_field("label", "Test Number Card", "Data"); diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index 03bd244c94..e56c420f3e 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -19,12 +19,18 @@ context("Web Form", () => { cy.fill_field("doc_type", "Note", "Link"); cy.fill_field("module", "Website", "Link"); cy.click_custom_action_button("Get Fields"); + // wait until Get Fields finishes populating the grid + cy.get('[data-fieldname="web_form_fields"] .grid-row').should(($rows) => { + expect($rows.length, "web form fields").to.be.greaterThan(0); + }); cy.click_custom_action_button("Publish"); cy.wait("@save_form"); cy.get_field("route").should("have.value", "note"); - cy.get(".title-area .indicator-pill").contains("Published"); + cy.get(".title-area .indicator-pill") + .should("contain.text", "Published") + .should("have.class", "green"); }); it("Open Web Form", () => { diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js index 05e03903ae..842103ec83 100644 --- a/cypress/integration/workspace_blocks.js +++ b/cypress/integration/workspace_blocks.js @@ -72,7 +72,7 @@ context("Workspace Blocks", () => { cy.get(".block-list-container .block-list-item").contains("Quick List").click(); cy.fill_field("label", "ToDo", "Data"); - cy.fill_field("document_type", "ToDo", "Link").blur(); + cy.fill_field("document_type", "ToDo", "Link"); cy.wait("@get_doctype"); cy.get_open_dialog().find(".filter-edit-area").should("contain", "No filters selected"); @@ -143,6 +143,7 @@ context("Workspace Blocks", () => { }); it("Number Card Block", () => { + cy.visit("/app/private/test-block-page"); cy.create_records([ { doctype: "Number Card", @@ -155,12 +156,16 @@ context("Workspace Blocks", () => { cy.get(".codex-editor__redactor .ce-block"); cy.get(".btn-edit-workspace").click(); - cy.get(".ce-block").first().click({ force: true }).type("{enter}"); + cy.get(".ce-block") + .first() + .realHover() + .find(".new-block-button") + .should("be.visible") + .click(); cy.get(".block-list-container .block-list-item").contains("Number Card").click(); // add number card cy.fill_field("number_card_name", "Test Number Card", "Link"); - cy.get('[data-fieldname="number_card_name"] ul div').contains("Test Number Card").click(); cy.click_modal_primary_button("Add"); cy.get(".ce-block .number-widget-box").first().as("number_card"); cy.get("@number_card").find(".widget-title").should("contain", "Test Number Card"); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5731a0050c..f774dc9872 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -171,7 +171,24 @@ Cypress.Commands.add("fill_field", (fieldname, value, fieldtype = "Data") => { cy.get("@input").clear().wait(200); } - if (fieldtype === "Select") { + if (["Link", "Dynamic Link"].includes(fieldtype)) { + cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); + cy.get("@input").clear().focus(); + cy.wait("@search_link"); + cy.get("@input").parent().findByRole("listbox").as("dropdown"); + cy.get("@dropdown").should("be.visible"); + cy.get("@input").type(value, { delay: 100 }); + cy.wait("@search_link"); + cy.get("@dropdown") + .should("be.visible") + .find("div[role='option']") + .first() + .should("include.text", value); + cy.get("@input").type("{enter}"); + cy.get("@input").blur(); + cy.get("@dropdown").should("not.exist"); + cy.get("@input").should("have.value", value); + } else if (fieldtype === "Select") { cy.get("@input").select(value); } else { cy.get("@input").type(value, { diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index ba5bf18022..abb5e6ae21 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -26,7 +26,14 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat this.set_input_attributes(); this.$input.on("focus", function () { if (!me.$input.val()) { - me.$input.val("").trigger("input"); + me.$input.val(""); + + // Create a fake input event + const e = $.Event("input"); + e.target = me.$input[0]; + + // Pass it to on_input directly, bypassing debounce, so the dropdown opens immediately + me.on_input(e); } me.show_link_and_clear_buttons(); @@ -259,107 +266,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat this.custom_awesomplete_filter && this.custom_awesomplete_filter(this.awesomplete); - this.$input.on( - "input", - frappe.utils.debounce(function (e) { - var doctype = me.get_options(); - if (!doctype) return; - if (!me.$input.cache[doctype]) { - me.$input.cache[doctype] = {}; - } - - var term = e.target.value; - - if (me.$input.cache[doctype][term] != null) { - // immediately show from cache - me.awesomplete.list = me.$input.cache[doctype][term]; - } - var args = { - txt: term, - doctype: doctype, - ignore_user_permissions: me.df.ignore_user_permissions, - reference_doctype: me.get_reference_doctype() || "", - page_length: cint(frappe.boot.sysdefaults?.link_field_results_limit) || 10, - form_doctype: me.doctype, - link_fieldname: me.df.fieldname, - }; - - me.set_custom_query(args); - - frappe.call({ - type: "POST", - method: "frappe.desk.search.search_link", - no_spinner: true, - args: args, - callback: function (r) { - if (!window.Cypress && !me.$input.is(":focus")) { - return; - } - r.message = me.merge_duplicates(r.message); - - // show filter description in awesomplete - let filter_string = me.df.filter_description - ? me.df.filter_description - : args.filters - ? me.get_filter_description(args.filters) - : null; - if (filter_string) { - r.message.push({ - html: `${filter_string}`, - value: "", - action: () => {}, - }); - } - - if (!me.df.only_select) { - if (frappe.model.can_create(doctype)) { - // new item - r.message.push({ - html: - "" + - " " + - __("Create a new {0}", [__(me.get_options())]) + - "", - label: __("Create a new {0}", [__(me.get_options())]), - value: "create_new__link_option", - action: me.new_doc, - }); - } - - //custom link actions - let custom__link_options = - frappe.ui.form.ControlLink.link_options && - frappe.ui.form.ControlLink.link_options(me); - - if (custom__link_options) { - r.message = r.message.concat(custom__link_options); - } - - // advanced search - if (locals && locals["DocType"]) { - // not applicable in web forms - r.message.push({ - html: - "" + - " " + - __("Advanced Search") + - "", - label: __("Advanced Search"), - value: "advanced_search__link_option", - action: me.open_advanced_search, - }); - } - } - me.$input.cache[doctype][term] = r.message; - me.awesomplete.list = me.$input.cache[doctype][term]; - me.toggle_href(doctype); - r.message.forEach((item) => { - frappe.utils.add_link_title(doctype, item.value, item.label); - }); - }, - }); - }, 500) - ); + this.$input.on("input", frappe.utils.debounce(this.on_input.bind(this), 500)); this.$input.on("blur", function () { if (me.selected) { @@ -433,6 +340,105 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat }); } + on_input(e) { + var doctype = this.get_options(); + if (!doctype) return; + if (!this.$input.cache[doctype]) { + this.$input.cache[doctype] = {}; + } + + var term = e.target.value; + + if (this.$input.cache[doctype][term] != null) { + // immediately show from cache + this.awesomplete.list = this.$input.cache[doctype][term]; + } + var args = { + txt: term, + doctype: doctype, + ignore_user_permissions: this.df.ignore_user_permissions, + reference_doctype: this.get_reference_doctype() || "", + page_length: cint(frappe.boot.sysdefaults?.link_field_results_limit) || 10, + form_doctype: this.doctype, + link_fieldname: this.df.fieldname, + }; + + this.set_custom_query(args); + + frappe.call({ + type: "POST", + method: "frappe.desk.search.search_link", + no_spinner: true, + args: args, + callback: (r) => { + if (!window.Cypress && !this.$input.is(":focus")) { + return; + } + r.message = this.merge_duplicates(r.message); + + // show filter description in awesomplete + let filter_string = this.df.filter_description + ? this.df.filter_description + : args.filters + ? this.get_filter_description(args.filters) + : null; + if (filter_string) { + r.message.push({ + html: `${filter_string}`, + value: "", + action: () => {}, + }); + } + + if (!this.df.only_select) { + if (frappe.model.can_create(doctype)) { + // new item + r.message.push({ + html: + "" + + " " + + __("Create a new {0}", [__(this.get_options())]) + + "", + label: __("Create a new {0}", [__(this.get_options())]), + value: "create_new__link_option", + action: this.new_doc, + }); + } + + //custom link actions + let custom__link_options = + frappe.ui.form.ControlLink.link_options && + frappe.ui.form.ControlLink.link_options(this); + + if (custom__link_options) { + r.message = r.message.concat(custom__link_options); + } + + // advanced search + if (locals && locals["DocType"]) { + // not applicable in web forms + r.message.push({ + html: + "" + + " " + + __("Advanced Search") + + "", + label: __("Advanced Search"), + value: "advanced_search__link_option", + action: this.open_advanced_search, + }); + } + } + this.$input.cache[doctype][term] = r.message; + this.awesomplete.list = this.$input.cache[doctype][term]; + this.toggle_href(doctype); + r.message.forEach((item) => { + frappe.utils.add_link_title(doctype, item.value, item.label); + }); + }, + }); + } + show_untranslated() { let value = this.get_input_value(); this.is_translatable() && this.set_input_value(value);