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 := {: