@assistkick/create 1.0.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/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Graph Settings Side Sheet (feat_030, feat_037).
|
|
3
|
+
* Uses node:test built-in runner.
|
|
4
|
+
* Tests the settings sheet logic: toggle state management,
|
|
5
|
+
* open/close behavior, delegation to GraphRenderer, and
|
|
6
|
+
* localStorage persistence.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
const DEFAULTS = { edgeLabels: false, hideDefined: false, allNodeNames: false, featureNodeNames: true };
|
|
11
|
+
const STORAGE_KEY = 'graph-settings';
|
|
12
|
+
|
|
13
|
+
class GraphSettingsSheet {
|
|
14
|
+
sheetEl: any;
|
|
15
|
+
renderer: any;
|
|
16
|
+
doc: any;
|
|
17
|
+
settingsBtn: any;
|
|
18
|
+
storage: any;
|
|
19
|
+
isOpen = false;
|
|
20
|
+
settings: Record<string, boolean>;
|
|
21
|
+
|
|
22
|
+
constructor(sheetEl: any, renderer: any, doc?: any, settingsBtn?: any, storage?: any) {
|
|
23
|
+
this.sheetEl = sheetEl;
|
|
24
|
+
this.renderer = renderer;
|
|
25
|
+
this.doc = doc;
|
|
26
|
+
this.settingsBtn = settingsBtn;
|
|
27
|
+
this.storage = storage;
|
|
28
|
+
this.settings = { ...DEFAULTS };
|
|
29
|
+
if (storage) {
|
|
30
|
+
try {
|
|
31
|
+
const stored = storage.getItem(STORAGE_KEY);
|
|
32
|
+
if (stored) {
|
|
33
|
+
const parsed = JSON.parse(stored);
|
|
34
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
35
|
+
if (typeof parsed[key] === 'boolean') this.settings[key] = parsed[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
init() { this.bindToggles(); this.bindClose(); this.bindDocumentClick(); }
|
|
43
|
+
|
|
44
|
+
open() {
|
|
45
|
+
this.isOpen = true;
|
|
46
|
+
this.sheetEl.classList.add('open');
|
|
47
|
+
this.syncToggles();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
close() {
|
|
51
|
+
this.isOpen = false;
|
|
52
|
+
this.sheetEl.classList.remove('open');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toggle() {
|
|
56
|
+
if (this.isOpen) this.close(); else this.open();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
syncToggles() {
|
|
60
|
+
const toggles = this.sheetEl.querySelectorAll('.settings-toggle');
|
|
61
|
+
for (const t of toggles) {
|
|
62
|
+
const key = t.dataset?.setting;
|
|
63
|
+
if (key && key in this.settings) t.checked = this.settings[key];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
applyDefaults() {
|
|
68
|
+
const applied = new Set<string>();
|
|
69
|
+
for (const key of Object.keys(this.settings)) {
|
|
70
|
+
if (applied.has(key)) continue;
|
|
71
|
+
this.applySetting(key);
|
|
72
|
+
// allNodeNames and featureNodeNames share the same renderer call
|
|
73
|
+
if (key === 'allNodeNames' || key === 'featureNodeNames') {
|
|
74
|
+
applied.add('allNodeNames');
|
|
75
|
+
applied.add('featureNodeNames');
|
|
76
|
+
}
|
|
77
|
+
applied.add(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
applySetting(key: string) {
|
|
82
|
+
const value = this.settings[key];
|
|
83
|
+
const r = this.renderer;
|
|
84
|
+
if (!r) return;
|
|
85
|
+
switch (key) {
|
|
86
|
+
case 'edgeLabels':
|
|
87
|
+
if (r.edgeLabelsVisible !== value) r.toggleEdgeLabels();
|
|
88
|
+
break;
|
|
89
|
+
case 'hideDefined':
|
|
90
|
+
if (r.hideDefinedActive !== value) r.toggleHideDefined();
|
|
91
|
+
break;
|
|
92
|
+
case 'allNodeNames':
|
|
93
|
+
case 'featureNodeNames':
|
|
94
|
+
r.setNodeNameVisibility(this.settings.allNodeNames, this.settings.featureNodeNames);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getSettings() {
|
|
100
|
+
return { ...this.settings };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bindToggles() {
|
|
104
|
+
const toggles = this.sheetEl.querySelectorAll('.settings-toggle');
|
|
105
|
+
for (const t of toggles) {
|
|
106
|
+
t.addEventListener('change', (e: any) => {
|
|
107
|
+
const key = e.target.dataset.setting;
|
|
108
|
+
if (!(key in DEFAULTS)) return;
|
|
109
|
+
this.settings[key] = e.target.checked;
|
|
110
|
+
this.applySetting(key);
|
|
111
|
+
if (this.storage) {
|
|
112
|
+
try { this.storage.setItem(STORAGE_KEY, JSON.stringify(this.settings)); } catch {}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
bindClose() {
|
|
119
|
+
const btn = this.sheetEl.querySelector('.settings-close');
|
|
120
|
+
if (btn) btn.addEventListener('click', () => this.close());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_handleDocumentClick: any = null;
|
|
124
|
+
|
|
125
|
+
bindDocumentClick() {
|
|
126
|
+
this._handleDocumentClick = (e: any) => this.handleDocumentClick(e);
|
|
127
|
+
if (this.doc) this.doc.addEventListener('click', this._handleDocumentClick);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
handleDocumentClick(e: any) {
|
|
131
|
+
if (!this.isOpen) return;
|
|
132
|
+
if (this.sheetEl.contains(e.target)) return;
|
|
133
|
+
if (this.settingsBtn && (e.target === this.settingsBtn || this.settingsBtn.contains?.(e.target))) return;
|
|
134
|
+
this.close();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
destroy() {
|
|
138
|
+
if (this.doc && this._handleDocumentClick) {
|
|
139
|
+
this.doc.removeEventListener('click', this._handleDocumentClick);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Mock GraphRenderer — tracks calls to toggle/set methods
|
|
145
|
+
const createMockRenderer = () => ({
|
|
146
|
+
edgeLabelsVisible: true,
|
|
147
|
+
hideDefinedActive: false,
|
|
148
|
+
allNodeNamesVisible: true,
|
|
149
|
+
featureNodeNamesVisible: true,
|
|
150
|
+
toggleEdgeLabelsCalls: 0,
|
|
151
|
+
toggleHideDefinedCalls: 0,
|
|
152
|
+
setNodeNameVisibilityCalls: [],
|
|
153
|
+
|
|
154
|
+
toggleEdgeLabels() {
|
|
155
|
+
this.edgeLabelsVisible = !this.edgeLabelsVisible;
|
|
156
|
+
this.toggleEdgeLabelsCalls++;
|
|
157
|
+
return this.edgeLabelsVisible;
|
|
158
|
+
},
|
|
159
|
+
toggleHideDefined() {
|
|
160
|
+
this.hideDefinedActive = !this.hideDefinedActive;
|
|
161
|
+
this.toggleHideDefinedCalls++;
|
|
162
|
+
return this.hideDefinedActive;
|
|
163
|
+
},
|
|
164
|
+
setNodeNameVisibility(all, feature) {
|
|
165
|
+
this.allNodeNamesVisible = all;
|
|
166
|
+
this.featureNodeNamesVisible = feature;
|
|
167
|
+
this.setNodeNameVisibilityCalls.push({ all, feature });
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Minimal DOM element mock
|
|
172
|
+
const createMockElement = (attrs = {}) => {
|
|
173
|
+
const listeners = {};
|
|
174
|
+
const classList = new Set();
|
|
175
|
+
const children = [];
|
|
176
|
+
|
|
177
|
+
const el = {
|
|
178
|
+
dataset: attrs.dataset || {},
|
|
179
|
+
checked: attrs.checked || false,
|
|
180
|
+
type: attrs.type || 'div',
|
|
181
|
+
listeners,
|
|
182
|
+
classList: {
|
|
183
|
+
add: (c) => classList.add(c),
|
|
184
|
+
remove: (c) => classList.delete(c),
|
|
185
|
+
contains: (c) => classList.has(c),
|
|
186
|
+
toggle: (c, force) => {
|
|
187
|
+
if (force === undefined) {
|
|
188
|
+
classList.has(c) ? classList.delete(c) : classList.add(c);
|
|
189
|
+
} else if (force) {
|
|
190
|
+
classList.add(c);
|
|
191
|
+
} else {
|
|
192
|
+
classList.delete(c);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
_classList: classList,
|
|
197
|
+
addEventListener: (event, handler) => {
|
|
198
|
+
listeners[event] = listeners[event] || [];
|
|
199
|
+
listeners[event].push(handler);
|
|
200
|
+
},
|
|
201
|
+
removeEventListener: (event, handler) => {
|
|
202
|
+
if (listeners[event]) {
|
|
203
|
+
listeners[event] = listeners[event].filter(h => h !== handler);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
querySelectorAll: (selector) => {
|
|
207
|
+
if (selector === '.settings-toggle') return children.filter(c => c.type === 'checkbox');
|
|
208
|
+
return [];
|
|
209
|
+
},
|
|
210
|
+
querySelector: (selector) => {
|
|
211
|
+
if (selector === '.settings-close') return children.find(c => c._role === 'close') || null;
|
|
212
|
+
return null;
|
|
213
|
+
},
|
|
214
|
+
_addChild: (child) => children.push(child),
|
|
215
|
+
contains: (target) => target === el || children.includes(target),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return el;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Mock localStorage
|
|
222
|
+
const createMockStorage = (initialData = {}) => {
|
|
223
|
+
const store = { ...initialData };
|
|
224
|
+
return {
|
|
225
|
+
getItem: (key) => key in store ? store[key] : null,
|
|
226
|
+
setItem: (key, value) => { store[key] = String(value); },
|
|
227
|
+
removeItem: (key) => { delete store[key]; },
|
|
228
|
+
_store: store,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
describe('GraphSettingsSheet', () => {
|
|
233
|
+
|
|
234
|
+
describe('default settings', () => {
|
|
235
|
+
it('has edge labels off by default', () => {
|
|
236
|
+
const renderer = createMockRenderer();
|
|
237
|
+
const el = createMockElement();
|
|
238
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
239
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('has hide defined off by default', () => {
|
|
243
|
+
const renderer = createMockRenderer();
|
|
244
|
+
const el = createMockElement();
|
|
245
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
246
|
+
assert.equal(sheet.settings.hideDefined, false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('has all node names off by default', () => {
|
|
250
|
+
const renderer = createMockRenderer();
|
|
251
|
+
const el = createMockElement();
|
|
252
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
253
|
+
assert.equal(sheet.settings.allNodeNames, false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('has feature node names on by default', () => {
|
|
257
|
+
const renderer = createMockRenderer();
|
|
258
|
+
const el = createMockElement();
|
|
259
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
260
|
+
assert.equal(sheet.settings.featureNodeNames, true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('open/close', () => {
|
|
265
|
+
it('starts closed', () => {
|
|
266
|
+
const renderer = createMockRenderer();
|
|
267
|
+
const el = createMockElement();
|
|
268
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
269
|
+
assert.equal(sheet.isOpen, false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('opens when open() is called', () => {
|
|
273
|
+
const renderer = createMockRenderer();
|
|
274
|
+
const el = createMockElement();
|
|
275
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
276
|
+
sheet.open();
|
|
277
|
+
assert.equal(sheet.isOpen, true);
|
|
278
|
+
assert.ok(el._classList.has('open'));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('closes when close() is called', () => {
|
|
282
|
+
const renderer = createMockRenderer();
|
|
283
|
+
const el = createMockElement();
|
|
284
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
285
|
+
sheet.open();
|
|
286
|
+
sheet.close();
|
|
287
|
+
assert.equal(sheet.isOpen, false);
|
|
288
|
+
assert.ok(!el._classList.has('open'));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('toggles between open and closed', () => {
|
|
292
|
+
const renderer = createMockRenderer();
|
|
293
|
+
const el = createMockElement();
|
|
294
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
295
|
+
|
|
296
|
+
sheet.toggle();
|
|
297
|
+
assert.equal(sheet.isOpen, true);
|
|
298
|
+
|
|
299
|
+
sheet.toggle();
|
|
300
|
+
assert.equal(sheet.isOpen, false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('syncs checkbox states when opening', () => {
|
|
304
|
+
const renderer = createMockRenderer();
|
|
305
|
+
const el = createMockElement();
|
|
306
|
+
|
|
307
|
+
const checkbox1 = createMockElement({
|
|
308
|
+
type: 'checkbox',
|
|
309
|
+
dataset: { setting: 'edgeLabels' },
|
|
310
|
+
checked: false,
|
|
311
|
+
});
|
|
312
|
+
const checkbox2 = createMockElement({
|
|
313
|
+
type: 'checkbox',
|
|
314
|
+
dataset: { setting: 'allNodeNames' },
|
|
315
|
+
checked: false,
|
|
316
|
+
});
|
|
317
|
+
el._addChild(checkbox1);
|
|
318
|
+
el._addChild(checkbox2);
|
|
319
|
+
|
|
320
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
321
|
+
sheet.settings.edgeLabels = true;
|
|
322
|
+
sheet.settings.allNodeNames = true;
|
|
323
|
+
sheet.open();
|
|
324
|
+
|
|
325
|
+
assert.equal(checkbox1.checked, true);
|
|
326
|
+
assert.equal(checkbox2.checked, true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('applyDefaults', () => {
|
|
331
|
+
it('turns off edge labels on init when renderer has them on', () => {
|
|
332
|
+
const renderer = createMockRenderer();
|
|
333
|
+
renderer.edgeLabelsVisible = true;
|
|
334
|
+
const el = createMockElement();
|
|
335
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
336
|
+
sheet.applyDefaults();
|
|
337
|
+
|
|
338
|
+
// Should have toggled edge labels off
|
|
339
|
+
assert.equal(renderer.toggleEdgeLabelsCalls, 1);
|
|
340
|
+
assert.equal(renderer.edgeLabelsVisible, false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('does not toggle edge labels if already off', () => {
|
|
344
|
+
const renderer = createMockRenderer();
|
|
345
|
+
renderer.edgeLabelsVisible = false;
|
|
346
|
+
const el = createMockElement();
|
|
347
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
348
|
+
sheet.applyDefaults();
|
|
349
|
+
|
|
350
|
+
assert.equal(renderer.toggleEdgeLabelsCalls, 0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('calls setNodeNameVisibility with defaults (false, true)', () => {
|
|
354
|
+
const renderer = createMockRenderer();
|
|
355
|
+
const el = createMockElement();
|
|
356
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
357
|
+
sheet.applyDefaults();
|
|
358
|
+
|
|
359
|
+
assert.equal(renderer.setNodeNameVisibilityCalls.length, 1);
|
|
360
|
+
assert.deepEqual(renderer.setNodeNameVisibilityCalls[0], { all: false, feature: true });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('applies stored hideDefined=true on init', () => {
|
|
364
|
+
const renderer = createMockRenderer();
|
|
365
|
+
renderer.hideDefinedActive = false;
|
|
366
|
+
const el = createMockElement();
|
|
367
|
+
const storage = createMockStorage({
|
|
368
|
+
'graph-settings': JSON.stringify({ hideDefined: true }),
|
|
369
|
+
});
|
|
370
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
371
|
+
sheet.applyDefaults();
|
|
372
|
+
|
|
373
|
+
assert.equal(renderer.toggleHideDefinedCalls, 1);
|
|
374
|
+
assert.equal(renderer.hideDefinedActive, true);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('applySetting', () => {
|
|
379
|
+
it('toggles edge labels on when setting changes to true', () => {
|
|
380
|
+
const renderer = createMockRenderer();
|
|
381
|
+
renderer.edgeLabelsVisible = false;
|
|
382
|
+
const el = createMockElement();
|
|
383
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
384
|
+
sheet.settings.edgeLabels = true;
|
|
385
|
+
sheet.applySetting('edgeLabels');
|
|
386
|
+
|
|
387
|
+
assert.equal(renderer.toggleEdgeLabelsCalls, 1);
|
|
388
|
+
assert.equal(renderer.edgeLabelsVisible, true);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('does not toggle edge labels when state already matches', () => {
|
|
392
|
+
const renderer = createMockRenderer();
|
|
393
|
+
renderer.edgeLabelsVisible = true;
|
|
394
|
+
const el = createMockElement();
|
|
395
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
396
|
+
sheet.settings.edgeLabels = true;
|
|
397
|
+
sheet.applySetting('edgeLabels');
|
|
398
|
+
|
|
399
|
+
assert.equal(renderer.toggleEdgeLabelsCalls, 0);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('toggles hide defined on when setting changes to true', () => {
|
|
403
|
+
const renderer = createMockRenderer();
|
|
404
|
+
const el = createMockElement();
|
|
405
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
406
|
+
sheet.settings.hideDefined = true;
|
|
407
|
+
sheet.applySetting('hideDefined');
|
|
408
|
+
|
|
409
|
+
assert.equal(renderer.toggleHideDefinedCalls, 1);
|
|
410
|
+
assert.equal(renderer.hideDefinedActive, true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('calls setNodeNameVisibility for allNodeNames change', () => {
|
|
414
|
+
const renderer = createMockRenderer();
|
|
415
|
+
const el = createMockElement();
|
|
416
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
417
|
+
sheet.settings.allNodeNames = true;
|
|
418
|
+
sheet.applySetting('allNodeNames');
|
|
419
|
+
|
|
420
|
+
const lastCall = renderer.setNodeNameVisibilityCalls.at(-1);
|
|
421
|
+
assert.deepEqual(lastCall, { all: true, feature: true });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('calls setNodeNameVisibility for featureNodeNames change', () => {
|
|
425
|
+
const renderer = createMockRenderer();
|
|
426
|
+
const el = createMockElement();
|
|
427
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
428
|
+
sheet.settings.featureNodeNames = false;
|
|
429
|
+
sheet.applySetting('featureNodeNames');
|
|
430
|
+
|
|
431
|
+
const lastCall = renderer.setNodeNameVisibilityCalls.at(-1);
|
|
432
|
+
assert.deepEqual(lastCall, { all: false, feature: false });
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('getSettings', () => {
|
|
437
|
+
it('returns a copy of settings', () => {
|
|
438
|
+
const renderer = createMockRenderer();
|
|
439
|
+
const el = createMockElement();
|
|
440
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
441
|
+
|
|
442
|
+
const settings = sheet.getSettings();
|
|
443
|
+
settings.edgeLabels = true; // mutate the copy
|
|
444
|
+
|
|
445
|
+
// Original should be unchanged
|
|
446
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('reflects current state', () => {
|
|
450
|
+
const renderer = createMockRenderer();
|
|
451
|
+
const el = createMockElement();
|
|
452
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
453
|
+
sheet.settings.hideDefined = true;
|
|
454
|
+
sheet.settings.allNodeNames = true;
|
|
455
|
+
|
|
456
|
+
const settings = sheet.getSettings();
|
|
457
|
+
assert.deepEqual(settings, {
|
|
458
|
+
edgeLabels: false,
|
|
459
|
+
hideDefined: true,
|
|
460
|
+
allNodeNames: true,
|
|
461
|
+
featureNodeNames: true,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('bindToggles', () => {
|
|
467
|
+
it('updates setting and applies when checkbox changes', () => {
|
|
468
|
+
const renderer = createMockRenderer();
|
|
469
|
+
renderer.edgeLabelsVisible = false;
|
|
470
|
+
const el = createMockElement();
|
|
471
|
+
|
|
472
|
+
const checkbox = createMockElement({
|
|
473
|
+
type: 'checkbox',
|
|
474
|
+
dataset: { setting: 'edgeLabels' },
|
|
475
|
+
checked: true,
|
|
476
|
+
});
|
|
477
|
+
el._addChild(checkbox);
|
|
478
|
+
|
|
479
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
480
|
+
sheet.bindToggles();
|
|
481
|
+
|
|
482
|
+
// Simulate change event
|
|
483
|
+
const changeHandlers = checkbox.listeners['change'];
|
|
484
|
+
assert.ok(changeHandlers && changeHandlers.length > 0, 'should have change handler');
|
|
485
|
+
changeHandlers[0]({ target: { dataset: { setting: 'edgeLabels' }, checked: true } });
|
|
486
|
+
|
|
487
|
+
assert.equal(sheet.settings.edgeLabels, true);
|
|
488
|
+
assert.equal(renderer.toggleEdgeLabelsCalls, 1);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('ignores unknown setting keys', () => {
|
|
492
|
+
const renderer = createMockRenderer();
|
|
493
|
+
const el = createMockElement();
|
|
494
|
+
|
|
495
|
+
const checkbox = createMockElement({
|
|
496
|
+
type: 'checkbox',
|
|
497
|
+
dataset: { setting: 'unknownSetting' },
|
|
498
|
+
checked: true,
|
|
499
|
+
});
|
|
500
|
+
el._addChild(checkbox);
|
|
501
|
+
|
|
502
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
503
|
+
sheet.bindToggles();
|
|
504
|
+
|
|
505
|
+
const changeHandlers = checkbox.listeners['change'];
|
|
506
|
+
changeHandlers[0]({ target: { dataset: { setting: 'unknownSetting' }, checked: true } });
|
|
507
|
+
|
|
508
|
+
// Should not throw or modify settings
|
|
509
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('saves to localStorage on toggle change', () => {
|
|
513
|
+
const renderer = createMockRenderer();
|
|
514
|
+
renderer.edgeLabelsVisible = false;
|
|
515
|
+
const el = createMockElement();
|
|
516
|
+
const storage = createMockStorage();
|
|
517
|
+
|
|
518
|
+
const checkbox = createMockElement({
|
|
519
|
+
type: 'checkbox',
|
|
520
|
+
dataset: { setting: 'edgeLabels' },
|
|
521
|
+
checked: true,
|
|
522
|
+
});
|
|
523
|
+
el._addChild(checkbox);
|
|
524
|
+
|
|
525
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
526
|
+
sheet.bindToggles();
|
|
527
|
+
|
|
528
|
+
const changeHandlers = checkbox.listeners['change'];
|
|
529
|
+
changeHandlers[0]({ target: { dataset: { setting: 'edgeLabels' }, checked: true } });
|
|
530
|
+
|
|
531
|
+
const stored = JSON.parse(storage.getItem('graph-settings'));
|
|
532
|
+
assert.equal(stored.edgeLabels, true);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe('close button binding', () => {
|
|
537
|
+
it('closes sheet when close button is clicked', () => {
|
|
538
|
+
const renderer = createMockRenderer();
|
|
539
|
+
const el = createMockElement();
|
|
540
|
+
|
|
541
|
+
const closeBtn = createMockElement();
|
|
542
|
+
closeBtn._role = 'close';
|
|
543
|
+
el._addChild(closeBtn);
|
|
544
|
+
|
|
545
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
546
|
+
sheet.open();
|
|
547
|
+
sheet.bindClose();
|
|
548
|
+
|
|
549
|
+
// Simulate click
|
|
550
|
+
const clickHandlers = closeBtn.listeners['click'];
|
|
551
|
+
assert.ok(clickHandlers && clickHandlers.length > 0, 'should have click handler');
|
|
552
|
+
clickHandlers[0]();
|
|
553
|
+
|
|
554
|
+
assert.equal(sheet.isOpen, false);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('document click (outside click to close)', () => {
|
|
559
|
+
it('closes sheet when clicking outside the sheet and settings button', () => {
|
|
560
|
+
const renderer = createMockRenderer();
|
|
561
|
+
const el = createMockElement();
|
|
562
|
+
const mockDoc = createMockElement();
|
|
563
|
+
const settingsBtn = createMockElement();
|
|
564
|
+
|
|
565
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
566
|
+
sheet.bindDocumentClick();
|
|
567
|
+
sheet.open();
|
|
568
|
+
|
|
569
|
+
// Simulate click on an unrelated element
|
|
570
|
+
const outsideTarget = createMockElement();
|
|
571
|
+
sheet.handleDocumentClick({ target: outsideTarget });
|
|
572
|
+
|
|
573
|
+
assert.equal(sheet.isOpen, false);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('does not close sheet when clicking inside the sheet', () => {
|
|
577
|
+
const renderer = createMockRenderer();
|
|
578
|
+
const el = createMockElement();
|
|
579
|
+
const mockDoc = createMockElement();
|
|
580
|
+
const settingsBtn = createMockElement();
|
|
581
|
+
const childInSheet = createMockElement();
|
|
582
|
+
el._addChild(childInSheet);
|
|
583
|
+
|
|
584
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
585
|
+
sheet.bindDocumentClick();
|
|
586
|
+
sheet.open();
|
|
587
|
+
|
|
588
|
+
sheet.handleDocumentClick({ target: childInSheet });
|
|
589
|
+
|
|
590
|
+
assert.equal(sheet.isOpen, true);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('does not close sheet when clicking the settings button', () => {
|
|
594
|
+
const renderer = createMockRenderer();
|
|
595
|
+
const el = createMockElement();
|
|
596
|
+
const mockDoc = createMockElement();
|
|
597
|
+
const settingsBtn = createMockElement();
|
|
598
|
+
|
|
599
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
600
|
+
sheet.bindDocumentClick();
|
|
601
|
+
sheet.open();
|
|
602
|
+
|
|
603
|
+
sheet.handleDocumentClick({ target: settingsBtn });
|
|
604
|
+
|
|
605
|
+
assert.equal(sheet.isOpen, true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('does not close sheet when it is already closed', () => {
|
|
609
|
+
const renderer = createMockRenderer();
|
|
610
|
+
const el = createMockElement();
|
|
611
|
+
const mockDoc = createMockElement();
|
|
612
|
+
const settingsBtn = createMockElement();
|
|
613
|
+
|
|
614
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
615
|
+
sheet.bindDocumentClick();
|
|
616
|
+
|
|
617
|
+
const outsideTarget = createMockElement();
|
|
618
|
+
sheet.handleDocumentClick({ target: outsideTarget });
|
|
619
|
+
|
|
620
|
+
// Should still be closed, no error
|
|
621
|
+
assert.equal(sheet.isOpen, false);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('registers click listener on document via bindDocumentClick', () => {
|
|
625
|
+
const renderer = createMockRenderer();
|
|
626
|
+
const el = createMockElement();
|
|
627
|
+
const mockDoc = createMockElement();
|
|
628
|
+
const settingsBtn = createMockElement();
|
|
629
|
+
|
|
630
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
631
|
+
sheet.bindDocumentClick();
|
|
632
|
+
|
|
633
|
+
assert.ok(mockDoc.listeners['click'] && mockDoc.listeners['click'].length > 0,
|
|
634
|
+
'should register click listener on document');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('removes click listener on destroy', () => {
|
|
638
|
+
const renderer = createMockRenderer();
|
|
639
|
+
const el = createMockElement();
|
|
640
|
+
const mockDoc = createMockElement();
|
|
641
|
+
const settingsBtn = createMockElement();
|
|
642
|
+
|
|
643
|
+
const sheet = new GraphSettingsSheet(el, renderer, mockDoc, settingsBtn);
|
|
644
|
+
sheet.bindDocumentClick();
|
|
645
|
+
|
|
646
|
+
assert.equal(mockDoc.listeners['click'].length, 1);
|
|
647
|
+
|
|
648
|
+
sheet.destroy();
|
|
649
|
+
|
|
650
|
+
assert.equal(mockDoc.listeners['click'].length, 0);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe('localStorage persistence', () => {
|
|
655
|
+
it('loads settings from localStorage on construction', () => {
|
|
656
|
+
const renderer = createMockRenderer();
|
|
657
|
+
const el = createMockElement();
|
|
658
|
+
const storage = createMockStorage({
|
|
659
|
+
'graph-settings': JSON.stringify({
|
|
660
|
+
edgeLabels: true,
|
|
661
|
+
hideDefined: true,
|
|
662
|
+
allNodeNames: true,
|
|
663
|
+
featureNodeNames: false,
|
|
664
|
+
}),
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
668
|
+
|
|
669
|
+
assert.equal(sheet.settings.edgeLabels, true);
|
|
670
|
+
assert.equal(sheet.settings.hideDefined, true);
|
|
671
|
+
assert.equal(sheet.settings.allNodeNames, true);
|
|
672
|
+
assert.equal(sheet.settings.featureNodeNames, false);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('uses defaults when localStorage is empty', () => {
|
|
676
|
+
const renderer = createMockRenderer();
|
|
677
|
+
const el = createMockElement();
|
|
678
|
+
const storage = createMockStorage();
|
|
679
|
+
|
|
680
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
681
|
+
|
|
682
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
683
|
+
assert.equal(sheet.settings.hideDefined, false);
|
|
684
|
+
assert.equal(sheet.settings.allNodeNames, false);
|
|
685
|
+
assert.equal(sheet.settings.featureNodeNames, true);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('uses defaults when localStorage has corrupt data', () => {
|
|
689
|
+
const renderer = createMockRenderer();
|
|
690
|
+
const el = createMockElement();
|
|
691
|
+
const storage = createMockStorage({
|
|
692
|
+
'graph-settings': 'not-valid-json{{{',
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
696
|
+
|
|
697
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
698
|
+
assert.equal(sheet.settings.featureNodeNames, true);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('ignores unknown keys in stored data', () => {
|
|
702
|
+
const renderer = createMockRenderer();
|
|
703
|
+
const el = createMockElement();
|
|
704
|
+
const storage = createMockStorage({
|
|
705
|
+
'graph-settings': JSON.stringify({
|
|
706
|
+
edgeLabels: true,
|
|
707
|
+
unknownKey: 'should be ignored',
|
|
708
|
+
}),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
712
|
+
|
|
713
|
+
assert.equal(sheet.settings.edgeLabels, true);
|
|
714
|
+
assert.equal(sheet.settings.hideDefined, false); // default
|
|
715
|
+
assert.ok(!('unknownKey' in sheet.settings));
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('ignores non-boolean values in stored data', () => {
|
|
719
|
+
const renderer = createMockRenderer();
|
|
720
|
+
const el = createMockElement();
|
|
721
|
+
const storage = createMockStorage({
|
|
722
|
+
'graph-settings': JSON.stringify({
|
|
723
|
+
edgeLabels: 'true', // string, not boolean
|
|
724
|
+
allNodeNames: 42, // number, not boolean
|
|
725
|
+
}),
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
729
|
+
|
|
730
|
+
assert.equal(sheet.settings.edgeLabels, false); // default
|
|
731
|
+
assert.equal(sheet.settings.allNodeNames, false); // default
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('saves settings to localStorage on toggle', () => {
|
|
735
|
+
const renderer = createMockRenderer();
|
|
736
|
+
renderer.edgeLabelsVisible = false;
|
|
737
|
+
const el = createMockElement();
|
|
738
|
+
const storage = createMockStorage();
|
|
739
|
+
|
|
740
|
+
const checkbox = createMockElement({
|
|
741
|
+
type: 'checkbox',
|
|
742
|
+
dataset: { setting: 'allNodeNames' },
|
|
743
|
+
checked: true,
|
|
744
|
+
});
|
|
745
|
+
el._addChild(checkbox);
|
|
746
|
+
|
|
747
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
748
|
+
sheet.bindToggles();
|
|
749
|
+
|
|
750
|
+
const changeHandlers = checkbox.listeners['change'];
|
|
751
|
+
changeHandlers[0]({ target: { dataset: { setting: 'allNodeNames' }, checked: true } });
|
|
752
|
+
|
|
753
|
+
const stored = JSON.parse(storage.getItem('graph-settings'));
|
|
754
|
+
assert.equal(stored.allNodeNames, true);
|
|
755
|
+
assert.equal(stored.featureNodeNames, true); // other defaults preserved
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('works without localStorage (null)', () => {
|
|
759
|
+
const renderer = createMockRenderer();
|
|
760
|
+
const el = createMockElement();
|
|
761
|
+
|
|
762
|
+
// No localStorage passed — should not throw
|
|
763
|
+
const sheet = new GraphSettingsSheet(el, renderer);
|
|
764
|
+
assert.equal(sheet.settings.edgeLabels, false);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('partially loads from localStorage, using defaults for missing keys', () => {
|
|
768
|
+
const renderer = createMockRenderer();
|
|
769
|
+
const el = createMockElement();
|
|
770
|
+
const storage = createMockStorage({
|
|
771
|
+
'graph-settings': JSON.stringify({ edgeLabels: true }),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
775
|
+
|
|
776
|
+
assert.equal(sheet.settings.edgeLabels, true);
|
|
777
|
+
assert.equal(sheet.settings.hideDefined, false); // default
|
|
778
|
+
assert.equal(sheet.settings.allNodeNames, false); // default
|
|
779
|
+
assert.equal(sheet.settings.featureNodeNames, true); // default
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe('syncToggles', () => {
|
|
784
|
+
it('syncs checkbox checked state to match settings', () => {
|
|
785
|
+
const renderer = createMockRenderer();
|
|
786
|
+
const el = createMockElement();
|
|
787
|
+
|
|
788
|
+
const cb1 = createMockElement({ type: 'checkbox', dataset: { setting: 'edgeLabels' }, checked: false });
|
|
789
|
+
const cb2 = createMockElement({ type: 'checkbox', dataset: { setting: 'featureNodeNames' }, checked: false });
|
|
790
|
+
el._addChild(cb1);
|
|
791
|
+
el._addChild(cb2);
|
|
792
|
+
|
|
793
|
+
const storage = createMockStorage({
|
|
794
|
+
'graph-settings': JSON.stringify({ edgeLabels: true, featureNodeNames: false }),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const sheet = new GraphSettingsSheet(el, renderer, undefined, undefined, storage);
|
|
798
|
+
sheet.syncToggles();
|
|
799
|
+
|
|
800
|
+
assert.equal(cb1.checked, true);
|
|
801
|
+
assert.equal(cb2.checked, false);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
});
|