diff --git a/client/src/components/Workflow/WorkflowDropdown.test.js b/client/src/components/Workflow/WorkflowDropdown.test.js index e15b58ffdf7..e853721cd25 100644 --- a/client/src/components/Workflow/WorkflowDropdown.test.js +++ b/client/src/components/Workflow/WorkflowDropdown.test.js @@ -4,6 +4,7 @@ import { getLocalVue } from "jest/helpers"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import flushPromises from "flush-promises"; +import { ROOT_COMPONENT } from "utils/navigation"; const localVue = getLocalVue(true); @@ -41,8 +42,8 @@ describe("WorkflowDropdown.vue", () => { }); it("should not display source metadata if not present", async () => { - expect(wrapper.find(".workflow-trs-icon").exists()).toBeFalsy(); - expect(wrapper.find(".workflow-external-link").exists()).toBeFalsy(); + expect(wrapper.find(ROOT_COMPONENT.workflows.trs_icon.selector).exists()).toBeFalsy(); + expect(wrapper.find(ROOT_COMPONENT.workflows.external_link.selector).exists()).toBeFalsy(); }); it("should display name and description", async () => { diff --git a/client/src/components/Workflow/WorkflowIndexActions.test.js b/client/src/components/Workflow/WorkflowIndexActions.test.js index 54318a174ad..e59eac231ed 100644 --- a/client/src/components/Workflow/WorkflowIndexActions.test.js +++ b/client/src/components/Workflow/WorkflowIndexActions.test.js @@ -1,10 +1,11 @@ import WorkflowIndexActions from "./WorkflowIndexActions"; import { shallowMount } from "@vue/test-utils"; import { getLocalVue } from "jest/helpers"; +import { ROOT_COMPONENT } from "utils/navigation"; import "jest-location-mock"; -const localVue = getLocalVue(); +const localVue = getLocalVue(true); describe("WorkflowIndexActions.vue", () => { let wrapper; @@ -21,13 +22,18 @@ describe("WorkflowIndexActions.vue", () => { describe("naviation", () => { it("should create a workflow when create is clicked", async () => { - await wrapper.find("#workflow-create").trigger("click"); + await wrapper.find(ROOT_COMPONENT.workflows.new_button.selector).trigger("click"); expect(window.location).toBeAt("/root/workflows/create"); }); it("should import a workflow when create is clicked", async () => { - await wrapper.find("#workflow-import").trigger("click"); + await wrapper.find(ROOT_COMPONENT.workflows.import_button.selector).trigger("click"); expect(window.location).toBeAt("/root/workflows/import"); }); + + it("should localize button text", async () => { + expect(wrapper.find(ROOT_COMPONENT.workflows.new_button.selector).text()).toBeLocalizationOf("Create"); + expect(wrapper.find(ROOT_COMPONENT.workflows.import_button.selector).text()).toBeLocalizationOf("Import"); + }); }); }); diff --git a/client/src/components/Workflow/WorkflowRunButton.test.js b/client/src/components/Workflow/WorkflowRunButton.test.js new file mode 100644 index 00000000000..7a07f20fed1 --- /dev/null +++ b/client/src/components/Workflow/WorkflowRunButton.test.js @@ -0,0 +1,29 @@ +import WorkflowRunButton from "./WorkflowRunButton"; +import { shallowMount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import { ROOT_COMPONENT } from "utils/navigation"; + +import "jest-location-mock"; + +const localVue = getLocalVue(true); +const WORKFLOW_ID = "workflow_id123"; + +describe("WorkflowRunButton.vue", () => { + let wrapper; + + beforeEach(async () => { + const propsData = { + root: "/root/", + id: WORKFLOW_ID, + }; + wrapper = shallowMount(WorkflowRunButton, { + propsData, + localVue, + }); + }); + + it("should localize title text", async () => { + const selector = ROOT_COMPONENT.workflows.run_button({ id: WORKFLOW_ID }).selector; + expect(wrapper.find(selector).attributes("title")).toBeLocalizationOf("Run workflow"); + }); +}); diff --git a/client/src/components/Workflow/WorkflowRunButton.vue b/client/src/components/Workflow/WorkflowRunButton.vue index 7e01211fb7d..53f411c40ae 100644 --- a/client/src/components/Workflow/WorkflowRunButton.vue +++ b/client/src/components/Workflow/WorkflowRunButton.vue @@ -2,6 +2,7 @@ diff --git a/client/src/utils/navigation/index.js b/client/src/utils/navigation/index.js new file mode 100644 index 00000000000..a9f1cf9a84c --- /dev/null +++ b/client/src/utils/navigation/index.js @@ -0,0 +1,123 @@ +import NAVIGATION_DATA from "./navigation.yml"; + +function interpolate(template, properties) { + let parsed = template; + Object.keys(properties).forEach((key) => { + const value = properties[key]; + parsed = parsed.replace("${" + key + "}", value); + }); + return parsed; +} + +class SelectorTemplate extends Function { + constructor(selector, selectorType, props = {}, children = {}) { + super(); + if (selectorType == "data-description") { + selectorType = "css"; + selector = `[data-description="${selector}"]`; + } + this._selector = selector; + this.selectorType = selectorType; + this._props = props || {}; + return new Proxy(this, { + apply: (target, thisArg, args) => target._call(args), + }); + } + + _call(args) { + const inputProps = args[0]; + const allProps = { ...this._props, ...inputProps }; + return new SelectorTemplate(this._selector, this.selectorType, allProps, this.children); + } + + get description() { + const selector = this.selector; + let desc; + if (this.selectorType == "css") { + desc = `CSS selector [${selector}]`; + } else if (this.selectorType == "xpath") { + desc = `XPATH selector [${selector}]`; + } else if (this.selectorType == "id") { + desc = `DOM element with id [${selector}]`; + } + return desc; + } + + get selector() { + return interpolate(this._selector, this._props); + } +} + +function selectorTemplateFromObject(object, children = {}) { + if (typeof object === "string" || object instanceof String) { + const selector = object; + return new SelectorTemplate(selector, "css", {}, children); + } else { + const selectorType = object.type || "css"; + const selector = object.selector; + return new SelectorTemplate(selector, selectorType, {}, children); + } +} + +function componentFromObject(name, object) { + const selectors = {}; + const subComponents = {}; + + Object.keys(object).forEach((key) => { + const value = object[key]; + if (key == "selectors") { + let baseSelector = null; + const hasBaseSelector = value["_"] !== undefined; + if (hasBaseSelector) { + baseSelector = value["_"]; + delete value["_"]; + } + Object.keys(value).forEach((selectorKey) => { + const selectorValue = value[selectorKey]; + if (selectorValue == undefined) { + throw `Problem with selectors value ${selectorValue}`; + } + selectors[selectorKey] = selectorTemplateFromObject(selectorValue); + }); + if (baseSelector) { + selectors["_"] = selectorTemplateFromObject(baseSelector, selectors); + } + } else if (key == "labels") { + // implement as needed + } else if (key == "text") { + // implement as needed + } else { + const component = componentFromObject(key, value); + subComponents[key] = component; + } + }); + return new Component(name, subComponents, selectors); +} + +class Component { + constructor(name, subComponents, selectors) { + self._name = name; + self._subComponents = subComponents; + self._selectors = selectors; + const component = this; + + Object.keys(subComponents).forEach((key) => { + const value = subComponents[key]; + component[key] = value; + }); + Object.keys(selectors).forEach((key) => { + const value = selectors[key]; + component[key] = value; + }); + } + + get selector() { + if (hasOwnProperty("_")) { + return this["_"]; + } else { + throw `No _ selector for [${this}]`; + } + } +} + +export const ROOT_COMPONENT = componentFromObject("root", NAVIGATION_DATA); diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml new file mode 100644 index 00000000000..ddeaaa08032 --- /dev/null +++ b/client/src/utils/navigation/navigation.yml @@ -0,0 +1,918 @@ +# top_level: {global: , workflows: , histories: } +# components := {: } | +# component := {selectors: , labels: , ids: } +# selectors_map := {: } +# selector := | {type: , selector: } +# selector_type := css|xpath|sizzle|id +# labels_map := {: