@inglorious/web 3.0.0 → 4.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.
@@ -1,208 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
5
-
6
- import { router } from "."
7
-
8
- describe("router", () => {
9
- let entity
10
- let api
11
-
12
- beforeEach(() => {
13
- entity = {
14
- id: "router",
15
- type: "router",
16
- routes: {
17
- "/": "homePage",
18
- "/users": "userListPage",
19
- "/users/:id": "userPage",
20
- "/users/:id/posts/:postId": "postPage",
21
- "*": "notFoundPage",
22
- },
23
- }
24
-
25
- api = {
26
- getEntity: vi.fn().mockReturnValue(entity),
27
- notify: vi.fn(),
28
- }
29
-
30
- // Mock window.location and history
31
- vi.spyOn(window, "location", "get").mockReturnValue({
32
- pathname: "/",
33
- search: "",
34
- hash: "",
35
- origin: "http://localhost:3000",
36
- })
37
- vi.spyOn(history, "pushState").mockImplementation(() => {})
38
- vi.spyOn(history, "replaceState").mockImplementation(() => {})
39
- vi.spyOn(history, "go").mockImplementation(() => {})
40
- })
41
-
42
- afterEach(() => {
43
- vi.restoreAllMocks()
44
- })
45
-
46
- describe("init()", () => {
47
- it("should initialize with the current window.location, set up a popstate listener, and set up a click listener for link interception", () => {
48
- vi.spyOn(window, "location", "get").mockReturnValue({
49
- pathname: "/users/123",
50
- search: "?sort=asc",
51
- hash: "#details",
52
- origin: "http://localhost:3000",
53
- })
54
- const windowSpy = vi.spyOn(window, "addEventListener")
55
- const documentSpy = vi.spyOn(document, "addEventListener")
56
-
57
- router.init(entity, undefined, api)
58
-
59
- expect(api.notify).toHaveBeenCalledWith("#router:navigate", {
60
- to: "/users/123?sort=asc",
61
- params: { id: "123" },
62
- replace: true,
63
- })
64
- expect(windowSpy).toHaveBeenCalledWith("popstate", expect.any(Function))
65
- expect(documentSpy).toHaveBeenCalledWith("click", expect.any(Function))
66
- })
67
- })
68
-
69
- describe("navigate()", () => {
70
- it("should navigate to a new path and update the entity", () => {
71
- router.navigate(entity, "/users/456?q=test", api)
72
-
73
- expect(entity.path).toBe("/users/456")
74
- expect(entity.route).toBe("userPage")
75
- expect(entity.params).toEqual({ id: "456" })
76
- expect(entity.query).toEqual({ q: "test" })
77
- expect(history.pushState).toHaveBeenCalledWith(
78
- expect.any(Object),
79
- "",
80
- "/users/456?q=test",
81
- )
82
- expect(api.notify).toHaveBeenCalledWith("routeChange", expect.any(Object))
83
- })
84
-
85
- it("should use replaceState when replace is true", () => {
86
- router.navigate(entity, { to: "/users", replace: true }, api)
87
- expect(history.replaceState).toHaveBeenCalled()
88
- expect(history.pushState).not.toHaveBeenCalled()
89
- })
90
-
91
- it("should handle numeric navigation", () => {
92
- router.navigate(entity, -1, api)
93
- expect(history.go).toHaveBeenCalledWith(-1)
94
- })
95
-
96
- it("should build path from params", () => {
97
- router.navigate(
98
- entity,
99
- { to: "/users/:id/posts/:postId", params: { id: 1, postId: 2 } },
100
- api,
101
- )
102
- expect(history.pushState).toHaveBeenCalledWith(
103
- expect.any(Object),
104
- "",
105
- "/users/1/posts/2",
106
- )
107
- expect(entity.route).toBe("postPage")
108
- expect(entity.params).toEqual({ id: "1", postId: "2" })
109
- })
110
-
111
- it("should use the fallback route for unknown paths", () => {
112
- router.navigate(entity, "/some/unknown/path", api)
113
- expect(entity.route).toBe("notFoundPage")
114
- expect(entity.params).toEqual({})
115
- })
116
-
117
- it("should not navigate if the path is identical", () => {
118
- entity.path = "/users"
119
- vi.spyOn(window, "location", "get").mockReturnValue({
120
- pathname: "/users",
121
- search: "",
122
- hash: "",
123
- })
124
-
125
- router.navigate(entity, "/users", api)
126
-
127
- expect(history.pushState).not.toHaveBeenCalled()
128
- expect(api.notify).not.toHaveBeenCalledWith(
129
- "routeChange",
130
- expect.any(Object),
131
- )
132
- })
133
-
134
- it("should navigate if the path is identical but force is true", () => {
135
- entity.path = "/users"
136
- vi.spyOn(window, "location", "get").mockReturnValue({
137
- pathname: "/users",
138
- search: "",
139
- hash: "",
140
- })
141
-
142
- router.navigate(entity, { to: "/users", force: true }, api)
143
-
144
- expect(history.pushState).toHaveBeenCalled()
145
- expect(api.notify).toHaveBeenCalledWith("routeChange", expect.any(Object))
146
- })
147
- })
148
-
149
- describe("routeSync()", () => {
150
- it("should update the entity state from a payload", () => {
151
- const payload = {
152
- path: "/new?a=1",
153
- entityType: "newPage",
154
- params: {},
155
- }
156
-
157
- vi.spyOn(window, "location", "get").mockReturnValue({ hash: "#section" })
158
-
159
- router.routeSync(entity, payload)
160
-
161
- expect(entity.path).toBe("/new")
162
- expect(entity.route).toBe("newPage")
163
- expect(entity.query).toEqual({ a: "1" })
164
- expect(entity.hash).toBe("#section")
165
- })
166
- })
167
-
168
- describe("loadSuccess()", () => {
169
- it("should handle lazy loaded modules", () => {
170
- const module = { myPage: { render: () => {} } }
171
- const route = { pattern: "/lazy", params: {} }
172
- const payload = {
173
- module,
174
- route,
175
- path: "/lazy",
176
- replace: false,
177
- state: {},
178
- }
179
-
180
- router.loadSuccess(entity, payload, api)
181
-
182
- expect(api.notify).toHaveBeenCalledWith("morph", {
183
- name: "myPage",
184
- type: module.myPage,
185
- })
186
- expect(entity.routes["/lazy"]).toBe("myPage")
187
- expect(entity.loading).toBe(false)
188
- expect(entity.route).toBe("myPage")
189
- expect(history.pushState).toHaveBeenCalled()
190
- })
191
- })
192
-
193
- describe("loadError()", () => {
194
- it("should handle load errors", () => {
195
- const error = new Error("Failed")
196
- const payload = { error, path: "/lazy" }
197
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
198
-
199
- router.loadError(entity, payload)
200
-
201
- expect(entity.path).toBe("/lazy")
202
- expect(entity.loading).toBe(false)
203
- expect(entity.error).toBe(error)
204
-
205
- consoleSpy.mockRestore()
206
- })
207
- })
208
- })
@@ -1,415 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { beforeEach, describe, expect, it } from "vitest"
5
-
6
- import { select } from "."
7
- import {
8
- filterOptions,
9
- findOptionIndex,
10
- formatOption,
11
- getOptionLabel,
12
- getOptionValue,
13
- groupOptions,
14
- isOptionSelected,
15
- } from "./logic.js"
16
-
17
- const sampleOptions = [
18
- { value: "br", label: "Brazil" },
19
- { value: "it", label: "Italy" },
20
- { value: "ca", label: "Canada" },
21
- { value: "us", label: "United States" },
22
- { value: "uk", label: "United Kingdom" },
23
- { value: "fr", label: "France" },
24
- ]
25
-
26
- describe("select", () => {
27
- let entity
28
-
29
- beforeEach(() => {
30
- entity = {
31
- id: "test-select",
32
- type: "select",
33
- options: JSON.parse(JSON.stringify(sampleOptions)),
34
- }
35
- })
36
-
37
- describe("logic", () => {
38
- describe("create()", () => {
39
- it("should initialize with default state", () => {
40
- select.create(entity)
41
-
42
- expect(entity.isOpen).toBe(false)
43
- expect(entity.searchTerm).toBe("")
44
- expect(entity.focusedIndex).toBe(-1)
45
- expect(entity.isMulti).toBe(false)
46
- expect(entity.selectedValue).toBe(null)
47
- expect(entity.options).toEqual(sampleOptions)
48
- expect(entity.isLoading).toBe(false)
49
- expect(entity.isDisabled).toBe(false)
50
- expect(entity.isSearchable).toBe(true)
51
- expect(entity.isClearable).toBe(true)
52
- expect(entity.isCreatable).toBe(false)
53
- expect(entity.placeholder).toBe("Select...")
54
- expect(entity.noOptionsMessage).toBe("No options")
55
- expect(entity.loadingMessage).toBe("Loading...")
56
- expect(entity.groupBy).toBe(null)
57
- })
58
-
59
- it("should initialize multi-select with empty array", () => {
60
- entity.isMulti = true
61
- select.create(entity)
62
- expect(entity.selectedValue).toEqual([])
63
- })
64
- })
65
-
66
- describe("open() and close()", () => {
67
- beforeEach(() => {
68
- select.create(entity)
69
- })
70
-
71
- it("open: should open the dropdown", () => {
72
- select.open(entity)
73
- expect(entity.isOpen).toBe(true)
74
- })
75
-
76
- it("open: should not open if disabled", () => {
77
- entity.isDisabled = true
78
- select.open(entity)
79
- expect(entity.isOpen).toBe(false)
80
- })
81
-
82
- it("open: should focus first option if available", () => {
83
- select.open(entity)
84
- expect(entity.focusedIndex).toBe(0)
85
- })
86
-
87
- it("open: should not focus if no options", () => {
88
- entity.options = []
89
- select.open(entity)
90
- expect(entity.focusedIndex).toBe(-1)
91
- })
92
-
93
- it("close: should close the dropdown", () => {
94
- entity.isOpen = true
95
- entity.focusedIndex = 2
96
- select.close(entity)
97
- expect(entity.isOpen).toBe(false)
98
- expect(entity.focusedIndex).toBe(-1)
99
- })
100
- })
101
-
102
- describe("toggle()", () => {
103
- beforeEach(() => {
104
- select.create(entity)
105
- })
106
-
107
- it("should open if closed", () => {
108
- select.toggle(entity)
109
- expect(entity.isOpen).toBe(true)
110
- })
111
-
112
- it("should close if open", () => {
113
- entity.isOpen = true
114
- select.toggle(entity)
115
- expect(entity.isOpen).toBe(false)
116
- })
117
- })
118
-
119
- describe("optionSelect()", () => {
120
- beforeEach(() => {
121
- select.create(entity)
122
- })
123
-
124
- it("should select an option in single-select mode", () => {
125
- const option = sampleOptions[0]
126
- select.optionSelect(entity, option)
127
- expect(entity.selectedValue).toBe("br")
128
- expect(entity.isOpen).toBe(false)
129
- })
130
-
131
- it("should add option in multi-select mode", () => {
132
- entity.isMulti = true
133
- select.create(entity)
134
- entity.isOpen = true // Open dropdown first
135
- const option = sampleOptions[0]
136
- select.optionSelect(entity, option)
137
- expect(entity.selectedValue).toContain("br")
138
- expect(entity.isOpen).toBe(true) // Multi-select doesn't close
139
- })
140
-
141
- it("should remove option in multi-select mode if already selected", () => {
142
- entity.isMulti = true
143
- select.create(entity)
144
- const option = sampleOptions[0]
145
- select.optionSelect(entity, option) // Add
146
- select.optionSelect(entity, option) // Remove
147
- expect(entity.selectedValue).not.toContain("br")
148
- })
149
-
150
- it("should not select if disabled", () => {
151
- entity.isDisabled = true
152
- select.optionSelect(entity, sampleOptions[0])
153
- expect(entity.selectedValue).toBe(null)
154
- })
155
- })
156
-
157
- describe("clear()", () => {
158
- beforeEach(() => {
159
- select.create(entity)
160
- })
161
-
162
- it("should clear selection in single-select mode", () => {
163
- entity.selectedValue = "br"
164
- select.clear(entity)
165
- expect(entity.selectedValue).toBe(null)
166
- })
167
-
168
- it("should clear selection in multi-select mode", () => {
169
- entity.isMulti = true
170
- select.create(entity)
171
- entity.selectedValue = ["br", "us"]
172
- select.clear(entity)
173
- expect(entity.selectedValue).toEqual([])
174
- })
175
-
176
- it("should not clear if disabled", () => {
177
- entity.selectedValue = "br"
178
- entity.isDisabled = true
179
- select.clear(entity)
180
- expect(entity.selectedValue).toBe("br")
181
- })
182
- })
183
-
184
- describe("searchChange()", () => {
185
- beforeEach(() => {
186
- select.create(entity)
187
- })
188
-
189
- it("should update searchTerm and filter options", () => {
190
- select.searchChange(entity, "bra")
191
- expect(entity.searchTerm).toBe("bra")
192
- })
193
-
194
- it("should reset focusedIndex when search changes", () => {
195
- entity.focusedIndex = 2
196
- select.searchChange(entity, "bra")
197
- expect(entity.focusedIndex).toBe(0)
198
- })
199
-
200
- it("should set focusedIndex to -1 if no results", () => {
201
- select.searchChange(entity, "xyz")
202
- expect(entity.focusedIndex).toBe(-1)
203
- })
204
-
205
- it("should show all options if searchTerm is empty", () => {
206
- select.searchChange(entity, "bra")
207
- select.searchChange(entity, "")
208
- expect(entity.focusedIndex).toBe(0)
209
- })
210
- })
211
-
212
- describe("keyboard navigation", () => {
213
- beforeEach(() => {
214
- select.create(entity)
215
- })
216
-
217
- it("focusNext: should move to next option", () => {
218
- entity.focusedIndex = 0
219
- select.focusNext(entity)
220
- expect(entity.focusedIndex).toBe(1)
221
- })
222
-
223
- it("focusNext: should not go past last option", () => {
224
- entity.focusedIndex = sampleOptions.length - 1
225
- select.focusNext(entity)
226
- expect(entity.focusedIndex).toBe(sampleOptions.length - 1)
227
- })
228
-
229
- it("focusPrev: should move to previous option", () => {
230
- entity.focusedIndex = 2
231
- select.focusPrev(entity)
232
- expect(entity.focusedIndex).toBe(1)
233
- })
234
-
235
- it("focusPrev: should not go before first option", () => {
236
- entity.focusedIndex = 0
237
- select.focusPrev(entity)
238
- expect(entity.focusedIndex).toBe(-1)
239
- })
240
-
241
- it("focusFirst: should move to first option", () => {
242
- entity.focusedIndex = 3
243
- select.focusFirst(entity)
244
- expect(entity.focusedIndex).toBe(0)
245
- })
246
-
247
- it("focusLast: should move to last option", () => {
248
- entity.focusedIndex = 0
249
- select.focusLast(entity)
250
- expect(entity.focusedIndex).toBe(sampleOptions.length - 1)
251
- })
252
-
253
- it("focusNext: should not move if no options", () => {
254
- entity.options = []
255
- entity.focusedIndex = -1
256
- select.focusNext(entity)
257
- expect(entity.focusedIndex).toBe(-1)
258
- })
259
- })
260
- })
261
-
262
- describe("helpers", () => {
263
- describe("getOptionValue()", () => {
264
- it("should return value from object", () => {
265
- expect(getOptionValue({ value: "test", label: "Test" })).toBe("test")
266
- })
267
-
268
- it("should return the option itself if not an object", () => {
269
- expect(getOptionValue("test")).toBe("test")
270
- expect(getOptionValue(123)).toBe(123)
271
- })
272
-
273
- it("should return the object itself if it has no value property", () => {
274
- const option = { label: "Test" }
275
- expect(getOptionValue(option)).toBe(option)
276
- })
277
- })
278
-
279
- describe("getOptionLabel()", () => {
280
- it("should return label from object", () => {
281
- expect(getOptionLabel({ value: "test", label: "Test" })).toBe("Test")
282
- })
283
-
284
- it("should return value as string if no label", () => {
285
- expect(getOptionLabel({ value: "test" })).toBe("test")
286
- })
287
-
288
- it("should return stringified option if not an object", () => {
289
- expect(getOptionLabel("test")).toBe("test")
290
- expect(getOptionLabel(123)).toBe("123")
291
- })
292
- })
293
-
294
- describe("isOptionSelected()", () => {
295
- it("should return true for selected option in single-select", () => {
296
- const option = { value: "br", label: "Brazil" }
297
- expect(isOptionSelected(option, "br", false)).toBe(true)
298
- expect(isOptionSelected(option, "us", false)).toBe(false)
299
- })
300
-
301
- it("should return true for selected option in multi-select", () => {
302
- const option = { value: "br", label: "Brazil" }
303
- expect(isOptionSelected(option, ["br", "us"], true)).toBe(true)
304
- expect(isOptionSelected(option, ["us"], true)).toBe(false)
305
- })
306
-
307
- it("should handle string/number options", () => {
308
- expect(isOptionSelected("test", "test", false)).toBe(true)
309
- expect(isOptionSelected(123, 123, false)).toBe(true)
310
- })
311
- })
312
-
313
- describe("filterOptions()", () => {
314
- it("should return all options if searchTerm is empty", () => {
315
- const result = filterOptions(sampleOptions, "")
316
- expect(result).toEqual(sampleOptions)
317
- })
318
-
319
- it("should filter options by label (case-insensitive)", () => {
320
- const result = filterOptions(sampleOptions, "bra")
321
- expect(result).toHaveLength(1)
322
- expect(result[0].label).toBe("Brazil")
323
- })
324
-
325
- it("should filter options case-insensitively", () => {
326
- const result = filterOptions(sampleOptions, "BRA")
327
- expect(result).toHaveLength(1)
328
- expect(result[0].label).toBe("Brazil")
329
- })
330
-
331
- it("should return empty array if no matches", () => {
332
- const result = filterOptions(sampleOptions, "xyz")
333
- expect(result).toHaveLength(0)
334
- })
335
-
336
- it("should trim searchTerm", () => {
337
- const result = filterOptions(sampleOptions, " bra ")
338
- expect(result).toHaveLength(1)
339
- })
340
- })
341
-
342
- describe("findOptionIndex()", () => {
343
- it("should find index of option by value", () => {
344
- expect(findOptionIndex(sampleOptions, "br")).toBe(0)
345
- expect(findOptionIndex(sampleOptions, "it")).toBe(1)
346
- })
347
-
348
- it("should return -1 if not found", () => {
349
- expect(findOptionIndex(sampleOptions, "xyz")).toBe(-1)
350
- })
351
- })
352
-
353
- describe("groupOptions()", () => {
354
- const groupedOptions = [
355
- { value: "a", label: "A", category: "letters" },
356
- { value: "b", label: "B", category: "letters" },
357
- { value: "1", label: "1", category: "numbers" },
358
- { value: "2", label: "2", category: "numbers" },
359
- ]
360
-
361
- it("should group options by property", () => {
362
- const result = groupOptions(groupedOptions, "category")
363
- expect(result).toHaveLength(2)
364
- expect(result[0].label).toBe("letters")
365
- expect(result[0].options).toHaveLength(2)
366
- expect(result[1].label).toBe("numbers")
367
- expect(result[1].options).toHaveLength(2)
368
- })
369
-
370
- it("should return null if groupBy is not provided", () => {
371
- expect(groupOptions(sampleOptions, null)).toBeNull()
372
- expect(groupOptions(sampleOptions, "")).toBeNull()
373
- })
374
-
375
- it("should handle options without group property", () => {
376
- const mixedOptions = [
377
- ...groupedOptions,
378
- { value: "x", label: "X" }, // No category
379
- ]
380
- const result = groupOptions(mixedOptions, "category")
381
- expect(result).toBeDefined()
382
- const ungrouped = result.find((g) => g.label === "Ungrouped")
383
- expect(ungrouped).toBeDefined()
384
- expect(ungrouped.options).toContainEqual({ value: "x", label: "X" })
385
- })
386
- })
387
-
388
- describe("formatOption()", () => {
389
- it("should format string as option", () => {
390
- const result = formatOption("test")
391
- expect(result).toEqual({ value: "test", label: "test" })
392
- })
393
-
394
- it("should format number as option", () => {
395
- const result = formatOption(123)
396
- expect(result).toEqual({ value: 123, label: "123" })
397
- })
398
-
399
- it("should preserve object option", () => {
400
- const option = { value: "test", label: "Test", disabled: true }
401
- const result = formatOption(option)
402
- expect(result).toEqual({
403
- value: "test",
404
- label: "Test",
405
- disabled: true,
406
- })
407
- })
408
-
409
- it("should create label from value if missing", () => {
410
- const result = formatOption({ value: "test" })
411
- expect(result).toEqual({ value: "test", label: "test" })
412
- })
413
- })
414
- })
415
- })