@yuneta/gobj-ui 2.2.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/gobj-ui.cjs.js +702 -1024
- package/dist/gobj-ui.es.js +703 -1025
- package/package.json +1 -1
- package/src/c_yui_form.js +322 -51
- package/src/c_yui_map.js +7 -4
- package/src/c_yui_nav.js +90 -1
- package/src/c_yui_shell.css +68 -2
- package/src/c_yui_shell.js +103 -18
- package/src/c_yui_treedb_topic_with_form.js +216 -1171
- package/src/nav_cards_helpers.js +71 -0
- package/src/nav_cards_helpers.test.js +122 -0
- package/src/shell_section_index.js +106 -0
- package/src/shell_section_index.test.js +151 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* nav_cards_helpers.js
|
|
3
|
+
*
|
|
4
|
+
* Pure descriptor builders for C_YUI_NAV's "cards" layout — a
|
|
5
|
+
* grid of tappable cards, one per item, used as the section-index
|
|
6
|
+
* landing of a submenu (list → detail pattern). Split out of
|
|
7
|
+
* c_yui_nav.js so it can be unit-tested without a DOM: the
|
|
8
|
+
* functions return createElement2 node descriptors, never
|
|
9
|
+
* HTMLElements.
|
|
10
|
+
*
|
|
11
|
+
* Copyright (c) 2026, ArtGins.
|
|
12
|
+
* All Rights Reserved.
|
|
13
|
+
***********************************************************************/
|
|
14
|
+
|
|
15
|
+
/************************************************************
|
|
16
|
+
* Descriptor of one card: an <a> carrying the same data-*
|
|
17
|
+
* contract as every other nav item (data-route/data-item-id/
|
|
18
|
+
* data-disabled), so C_YUI_NAV's delegated click handler and
|
|
19
|
+
* active-route highlight work unchanged.
|
|
20
|
+
************************************************************/
|
|
21
|
+
export function card_descriptor(it, show_label)
|
|
22
|
+
{
|
|
23
|
+
let children = [];
|
|
24
|
+
let label = it.name || "";
|
|
25
|
+
|
|
26
|
+
if(it.icon) {
|
|
27
|
+
children.push(["span", {class: "icon is-medium"},
|
|
28
|
+
["i", {class: it.icon, "aria-hidden": "true"}]]);
|
|
29
|
+
}
|
|
30
|
+
if(show_label && label) {
|
|
31
|
+
children.push(["span", {class: "yui-nav-label", i18n: label}, label]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let a_attrs = {
|
|
35
|
+
class: "yui-nav-item yui-nav-card",
|
|
36
|
+
href: it.route ? "#" + it.route : "#",
|
|
37
|
+
"data-item-id": it.id,
|
|
38
|
+
"data-route": it.route || "",
|
|
39
|
+
"data-disabled": it.disabled ? "1" : "0",
|
|
40
|
+
"aria-label": label || it.id
|
|
41
|
+
};
|
|
42
|
+
if(label) {
|
|
43
|
+
a_attrs["data-i18n-aria-label"] = label;
|
|
44
|
+
}
|
|
45
|
+
let tip = it.tooltip || it.aria_label;
|
|
46
|
+
if(tip) {
|
|
47
|
+
a_attrs.title = tip;
|
|
48
|
+
a_attrs["data-i18n-title"] = tip;
|
|
49
|
+
}
|
|
50
|
+
if(it.disabled) {
|
|
51
|
+
a_attrs["aria-disabled"] = "true";
|
|
52
|
+
a_attrs["tabindex"] = "-1";
|
|
53
|
+
}
|
|
54
|
+
return ["a", a_attrs, children];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/************************************************************
|
|
58
|
+
* Descriptor of the whole grid. Decorative items (`header`/
|
|
59
|
+
* `divider`) are dropped, same policy as the tabs layout.
|
|
60
|
+
************************************************************/
|
|
61
|
+
export function cards_grid_descriptor(items, show_label)
|
|
62
|
+
{
|
|
63
|
+
let cards = [];
|
|
64
|
+
for(let it of (items || [])) {
|
|
65
|
+
if(!it || it.type === "header" || it.type === "divider") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
cards.push(card_descriptor(it, show_label));
|
|
69
|
+
}
|
|
70
|
+
return ["div", {}, cards];
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* nav_cards_helpers.test.js
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the pure "cards" layout descriptor builders.
|
|
5
|
+
* Descriptors are plain createElement2 arrays, so no DOM needed.
|
|
6
|
+
***********************************************************************/
|
|
7
|
+
import { test, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
card_descriptor,
|
|
10
|
+
cards_grid_descriptor,
|
|
11
|
+
} from "./nav_cards_helpers.js";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/***************************************************************
|
|
15
|
+
* card_descriptor
|
|
16
|
+
***************************************************************/
|
|
17
|
+
test("card carries the nav item data-* contract", () => {
|
|
18
|
+
let [tag, attrs, children] = card_descriptor(
|
|
19
|
+
{id: "budgets", name: "budgets", icon: "wzi-coins", route: "/reports/budgets"},
|
|
20
|
+
true
|
|
21
|
+
);
|
|
22
|
+
expect(tag).toBe("a");
|
|
23
|
+
expect(attrs["data-item-id"]).toBe("budgets");
|
|
24
|
+
expect(attrs["data-route"]).toBe("/reports/budgets");
|
|
25
|
+
expect(attrs["data-disabled"]).toBe("0");
|
|
26
|
+
expect(attrs.href).toBe("#/reports/budgets");
|
|
27
|
+
expect(attrs["aria-label"]).toBe("budgets");
|
|
28
|
+
expect(attrs["data-i18n-aria-label"]).toBe("budgets");
|
|
29
|
+
expect(attrs.class).toContain("yui-nav-card");
|
|
30
|
+
|
|
31
|
+
let [icon_tag, icon_attrs] = children[0];
|
|
32
|
+
expect(icon_tag).toBe("span");
|
|
33
|
+
expect(icon_attrs.class).toContain("icon");
|
|
34
|
+
|
|
35
|
+
let [label_tag, label_attrs, label_text] = children[1];
|
|
36
|
+
expect(label_tag).toBe("span");
|
|
37
|
+
expect(label_attrs.i18n).toBe("budgets");
|
|
38
|
+
expect(label_text).toBe("budgets");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("card without icon renders label only", () => {
|
|
42
|
+
let [, , children] = card_descriptor(
|
|
43
|
+
{id: "x", name: "x label", route: "/x"},
|
|
44
|
+
true
|
|
45
|
+
);
|
|
46
|
+
expect(children.length).toBe(1);
|
|
47
|
+
expect(children[0][0]).toBe("span");
|
|
48
|
+
expect(children[0][2]).toBe("x label");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("show_label=false drops the label span", () => {
|
|
52
|
+
let [, attrs, children] = card_descriptor(
|
|
53
|
+
{id: "x", name: "x label", icon: "yi-gear", route: "/x"},
|
|
54
|
+
false
|
|
55
|
+
);
|
|
56
|
+
expect(children.length).toBe(1);
|
|
57
|
+
expect(children[0][1].class).toContain("icon");
|
|
58
|
+
/* aria still carries the name for icon-only cards. */
|
|
59
|
+
expect(attrs["aria-label"]).toBe("x label");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("disabled card is marked for the click handler and a11y", () => {
|
|
63
|
+
let [, attrs] = card_descriptor(
|
|
64
|
+
{id: "x", name: "x", route: "/x", disabled: true},
|
|
65
|
+
true
|
|
66
|
+
);
|
|
67
|
+
expect(attrs["data-disabled"]).toBe("1");
|
|
68
|
+
expect(attrs["aria-disabled"]).toBe("true");
|
|
69
|
+
expect(attrs["tabindex"]).toBe("-1");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("tooltip mirrors into title + data-i18n-title", () => {
|
|
73
|
+
let [, attrs] = card_descriptor(
|
|
74
|
+
{id: "x", name: "x", route: "/x", tooltip: "the tip"},
|
|
75
|
+
true
|
|
76
|
+
);
|
|
77
|
+
expect(attrs.title).toBe("the tip");
|
|
78
|
+
expect(attrs["data-i18n-title"]).toBe("the tip");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("item without route degrades to inert href '#'", () => {
|
|
82
|
+
let [, attrs] = card_descriptor({id: "x", name: "x"}, true);
|
|
83
|
+
expect(attrs.href).toBe("#");
|
|
84
|
+
expect(attrs["data-route"]).toBe("");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/***************************************************************
|
|
89
|
+
* cards_grid_descriptor
|
|
90
|
+
***************************************************************/
|
|
91
|
+
test("grid renders one card per navigable item", () => {
|
|
92
|
+
let [tag, , cards] = cards_grid_descriptor(
|
|
93
|
+
[
|
|
94
|
+
{id: "a", name: "a", route: "/s/a"},
|
|
95
|
+
{id: "b", name: "b", route: "/s/b"},
|
|
96
|
+
],
|
|
97
|
+
true
|
|
98
|
+
);
|
|
99
|
+
expect(tag).toBe("div");
|
|
100
|
+
expect(cards.length).toBe(2);
|
|
101
|
+
expect(cards[0][1]["data-route"]).toBe("/s/a");
|
|
102
|
+
expect(cards[1][1]["data-route"]).toBe("/s/b");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("grid drops decorative items (same policy as tabs)", () => {
|
|
106
|
+
let [, , cards] = cards_grid_descriptor(
|
|
107
|
+
[
|
|
108
|
+
{type: "header", name: "group"},
|
|
109
|
+
{id: "a", name: "a", route: "/s/a"},
|
|
110
|
+
{type: "divider"},
|
|
111
|
+
{id: "b", name: "b", route: "/s/b"},
|
|
112
|
+
null,
|
|
113
|
+
],
|
|
114
|
+
true
|
|
115
|
+
);
|
|
116
|
+
expect(cards.length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("grid tolerates empty/missing items", () => {
|
|
120
|
+
expect(cards_grid_descriptor([], true)[2].length).toBe(0);
|
|
121
|
+
expect(cards_grid_descriptor(null, true)[2].length).toBe(0);
|
|
122
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_section_index.js
|
|
3
|
+
*
|
|
4
|
+
* Pure helper for C_YUI_SHELL: synthesize the stage target of a
|
|
5
|
+
* section-index landing route.
|
|
6
|
+
*
|
|
7
|
+
* A level-1 menu item that declares `submenu.index` opts out of
|
|
8
|
+
* the redirect-to-default-child behaviour: its own route becomes
|
|
9
|
+
* a real resting, deep-linkable route whose view is the submenu
|
|
10
|
+
* itself rendered as a "cards" C_YUI_NAV (the shell's view
|
|
11
|
+
* contract only requires a $container by the end of mt_create,
|
|
12
|
+
* which C_YUI_NAV already satisfies). Config:
|
|
13
|
+
*
|
|
14
|
+
* "submenu": {
|
|
15
|
+
* "render": { "top-sub": "tabs" },
|
|
16
|
+
* "index": true — landing in stage "main"
|
|
17
|
+
* "index": {"stage": "x"} — landing in stage "x"
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Precedence: an explicit inline `target` on the item wins (this
|
|
21
|
+
* helper returns null then). When the target is synthesized,
|
|
22
|
+
* `submenu.default` becomes inert for the section: navigate_to()
|
|
23
|
+
* only redirects to the default child while the entry has NO
|
|
24
|
+
* target.
|
|
25
|
+
*
|
|
26
|
+
* Tabs and cards never coexist (DRY of navigation): while the
|
|
27
|
+
* index is on stage the shell collapses the secondary zone, and
|
|
28
|
+
* for index sections the tab strip defaults to desktop/tablet
|
|
29
|
+
* only — on mobile a "backbar" nav (← <section>) takes its place
|
|
30
|
+
* inside a child view. See secondary_nav_renders().
|
|
31
|
+
*
|
|
32
|
+
* Split out of c_yui_shell.js so it can be unit-tested without
|
|
33
|
+
* a DOM.
|
|
34
|
+
*
|
|
35
|
+
* Copyright (c) 2026, ArtGins.
|
|
36
|
+
* All Rights Reserved.
|
|
37
|
+
***********************************************************************/
|
|
38
|
+
|
|
39
|
+
/************************************************************
|
|
40
|
+
* Return the synthesized target for `item`'s section route,
|
|
41
|
+
* or null when the item doesn't opt in.
|
|
42
|
+
************************************************************/
|
|
43
|
+
export function section_index_target(menu_id, item)
|
|
44
|
+
{
|
|
45
|
+
if(!item || !item.route || item.target) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let sub = item.submenu;
|
|
49
|
+
if(!sub || !Array.isArray(sub.items) || !sub.index) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
let index_cfg = (typeof sub.index === "object") ? sub.index : {};
|
|
53
|
+
return {
|
|
54
|
+
stage: index_cfg.stage || "main",
|
|
55
|
+
gclass: "C_YUI_NAV",
|
|
56
|
+
kw: {
|
|
57
|
+
menu_id: `index.${menu_id}.${item.id}`,
|
|
58
|
+
nav_label: item.name || item.id || "",
|
|
59
|
+
menu_items: sub.items,
|
|
60
|
+
layout: "cards",
|
|
61
|
+
level: "secondary"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/************************************************************
|
|
67
|
+
* Return the list of render configs for one (submenu, zone)
|
|
68
|
+
* pair. `layout` is the raw submenu.render[zone] value
|
|
69
|
+
* (string or object).
|
|
70
|
+
*
|
|
71
|
+
* Without submenu.index: the declared render, unchanged.
|
|
72
|
+
* With submenu.index (list → detail):
|
|
73
|
+
* - the declared nav defaults to show_on ">=tablet" (an
|
|
74
|
+
* explicit show_on in the render object wins);
|
|
75
|
+
* - a "backbar" nav (show_on "<tablet") is appended so a
|
|
76
|
+
* child view offers "← <section>" on mobile instead of
|
|
77
|
+
* the duplicated tab strip. Disable with
|
|
78
|
+
* index: {backbar: false}; tune its breakpoints with
|
|
79
|
+
* index: {backbar: {show_on: "..."}}.
|
|
80
|
+
************************************************************/
|
|
81
|
+
export function secondary_nav_renders(item, layout)
|
|
82
|
+
{
|
|
83
|
+
let cfg = (layout && typeof layout === "object")
|
|
84
|
+
? Object.assign({}, layout)
|
|
85
|
+
: {layout: String(layout || "vertical")};
|
|
86
|
+
|
|
87
|
+
let sub = (item && item.submenu) || {};
|
|
88
|
+
if(!sub.index) {
|
|
89
|
+
return [cfg];
|
|
90
|
+
}
|
|
91
|
+
let index_cfg = (typeof sub.index === "object") ? sub.index : {};
|
|
92
|
+
if(!cfg.show_on) {
|
|
93
|
+
cfg.show_on = ">=tablet";
|
|
94
|
+
}
|
|
95
|
+
if(index_cfg.backbar === false) {
|
|
96
|
+
return [cfg];
|
|
97
|
+
}
|
|
98
|
+
let backbar_cfg = (typeof index_cfg.backbar === "object" && index_cfg.backbar)
|
|
99
|
+
? index_cfg.backbar
|
|
100
|
+
: {};
|
|
101
|
+
return [cfg, {
|
|
102
|
+
layout: "backbar",
|
|
103
|
+
show_on: backbar_cfg.show_on || "<tablet",
|
|
104
|
+
back_route: (item && item.route) || ""
|
|
105
|
+
}];
|
|
106
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_section_index.test.js
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the section-index target synthesis.
|
|
5
|
+
***********************************************************************/
|
|
6
|
+
import { test, expect } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
section_index_target,
|
|
9
|
+
secondary_nav_renders,
|
|
10
|
+
} from "./shell_section_index.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const SUB_ITEMS = [
|
|
14
|
+
{id: "insights", name: "insights", route: "/reports/insights"},
|
|
15
|
+
{id: "budgets", name: "budgets", route: "/reports/budgets"},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function reports_item(extra_sub, extra_item)
|
|
19
|
+
{
|
|
20
|
+
return Object.assign({
|
|
21
|
+
id: "reports",
|
|
22
|
+
name: "reports",
|
|
23
|
+
route: "/reports",
|
|
24
|
+
submenu: Object.assign({
|
|
25
|
+
render: {"top-sub": "tabs"},
|
|
26
|
+
items: SUB_ITEMS,
|
|
27
|
+
}, extra_sub || {}),
|
|
28
|
+
}, extra_item || {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/***************************************************************
|
|
33
|
+
* Opt-in gate
|
|
34
|
+
***************************************************************/
|
|
35
|
+
test("no submenu.index → null (redirect-to-default preserved)", () => {
|
|
36
|
+
expect(section_index_target("primary", reports_item())).toBe(null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("index=false → null", () => {
|
|
40
|
+
expect(section_index_target("primary", reports_item({index: false}))).toBe(null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("item without route → null", () => {
|
|
44
|
+
let item = reports_item({index: true});
|
|
45
|
+
delete item.route;
|
|
46
|
+
expect(section_index_target("primary", item)).toBe(null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("explicit inline target wins over index", () => {
|
|
50
|
+
let item = reports_item(
|
|
51
|
+
{index: true},
|
|
52
|
+
{target: {stage: "main", gclass: "C_APP_VIEW"}}
|
|
53
|
+
);
|
|
54
|
+
expect(section_index_target("primary", item)).toBe(null);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("submenu without items array → null", () => {
|
|
58
|
+
let item = reports_item({index: true});
|
|
59
|
+
item.submenu.items = null;
|
|
60
|
+
expect(section_index_target("primary", item)).toBe(null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("no submenu at all → null", () => {
|
|
64
|
+
expect(section_index_target("primary", {id: "x", route: "/x"})).toBe(null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/***************************************************************
|
|
69
|
+
* Synthesis
|
|
70
|
+
***************************************************************/
|
|
71
|
+
test("index=true synthesizes a cards C_YUI_NAV target in stage 'main'", () => {
|
|
72
|
+
let t = section_index_target("primary", reports_item({index: true}));
|
|
73
|
+
expect(t).not.toBe(null);
|
|
74
|
+
expect(t.stage).toBe("main");
|
|
75
|
+
expect(t.gclass).toBe("C_YUI_NAV");
|
|
76
|
+
expect(t.kw.layout).toBe("cards");
|
|
77
|
+
expect(t.kw.level).toBe("secondary");
|
|
78
|
+
expect(t.kw.menu_id).toBe("index.primary.reports");
|
|
79
|
+
expect(t.kw.nav_label).toBe("reports");
|
|
80
|
+
/* Same array reference: a later yui_shell_set_submenu() refresh
|
|
81
|
+
* of kw.menu_items must be observable by the next mount. */
|
|
82
|
+
expect(t.kw.menu_items).toBe(SUB_ITEMS);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("index={stage} overrides the stage", () => {
|
|
86
|
+
let t = section_index_target("primary", reports_item({index: {stage: "aux"}}));
|
|
87
|
+
expect(t.stage).toBe("aux");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("nav_label falls back to the item id", () => {
|
|
91
|
+
let item = reports_item({index: true});
|
|
92
|
+
delete item.name;
|
|
93
|
+
let t = section_index_target("primary", item);
|
|
94
|
+
expect(t.kw.nav_label).toBe("reports");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
/***************************************************************
|
|
99
|
+
* secondary_nav_renders
|
|
100
|
+
***************************************************************/
|
|
101
|
+
test("no index → the declared render, unchanged, alone", () => {
|
|
102
|
+
let renders = secondary_nav_renders(reports_item(), {layout: "tabs"});
|
|
103
|
+
expect(renders).toEqual([{layout: "tabs"}]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("index → tabs constrained to >=tablet + mobile backbar", () => {
|
|
107
|
+
let renders = secondary_nav_renders(
|
|
108
|
+
reports_item({index: true}), {layout: "tabs"}
|
|
109
|
+
);
|
|
110
|
+
expect(renders.length).toBe(2);
|
|
111
|
+
expect(renders[0]).toEqual({layout: "tabs", show_on: ">=tablet"});
|
|
112
|
+
expect(renders[1]).toEqual({
|
|
113
|
+
layout: "backbar",
|
|
114
|
+
show_on: "<tablet",
|
|
115
|
+
back_route: "/reports"
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("explicit show_on on the declared render wins", () => {
|
|
120
|
+
let renders = secondary_nav_renders(
|
|
121
|
+
reports_item({index: true}), {layout: "tabs", show_on: ">=desktop"}
|
|
122
|
+
);
|
|
123
|
+
expect(renders[0].show_on).toBe(">=desktop");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("index {backbar:false} suppresses the backbar", () => {
|
|
127
|
+
let renders = secondary_nav_renders(
|
|
128
|
+
reports_item({index: {backbar: false}}), {layout: "tabs"}
|
|
129
|
+
);
|
|
130
|
+
expect(renders.length).toBe(1);
|
|
131
|
+
expect(renders[0].show_on).toBe(">=tablet");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("index {backbar:{show_on}} tunes the backbar breakpoints", () => {
|
|
135
|
+
let renders = secondary_nav_renders(
|
|
136
|
+
reports_item({index: {backbar: {show_on: "<desktop"}}}),
|
|
137
|
+
{layout: "tabs"}
|
|
138
|
+
);
|
|
139
|
+
expect(renders[1].show_on).toBe("<desktop");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("string layout shorthand is normalized", () => {
|
|
143
|
+
let renders = secondary_nav_renders(reports_item(), "tabs");
|
|
144
|
+
expect(renders).toEqual([{layout: "tabs"}]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("input render object is not mutated", () => {
|
|
148
|
+
let declared = {layout: "tabs"};
|
|
149
|
+
secondary_nav_renders(reports_item({index: true}), declared);
|
|
150
|
+
expect(declared).toEqual({layout: "tabs"});
|
|
151
|
+
});
|