@inglorious/web 3.0.0 → 3.0.1

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,228 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { html } from "lit-html"
5
- import { beforeEach, describe, expect, it, vi } from "vitest"
6
-
7
- import { list } from "."
8
-
9
- describe("list", () => {
10
- let entity
11
-
12
- beforeEach(() => {
13
- entity = {
14
- id: "test-list",
15
- type: "test-item-type",
16
- items: Array.from({ length: 100 }, (_, i) => ({
17
- id: i,
18
- name: `Item ${i}`,
19
- })),
20
- }
21
- })
22
-
23
- describe("and create()", () => {
24
- it("should set default list properties on init", () => {
25
- list.create(entity)
26
- expect(entity.scrollTop).toBe(0)
27
- expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
28
- expect(entity.viewportHeight).toBe(600)
29
- expect(entity.bufferSize).toBe(5)
30
- expect(entity.itemHeight).toBeNull()
31
- expect(entity.estimatedHeight).toBe(50)
32
- })
33
-
34
- it("should not overwrite existing properties on init (except scrollTop)", () => {
35
- entity.viewportHeight = 800
36
- entity.visibleRange = { start: 10, end: 30 }
37
-
38
- list.create(entity)
39
-
40
- expect(entity.viewportHeight).toBe(800)
41
- expect(entity.visibleRange).toEqual({ start: 10, end: 30 })
42
- expect(entity.scrollTop).toBe(0) // scrollTop is always reset
43
- })
44
-
45
- it("should reset the list on create", () => {
46
- list.create(entity)
47
- expect(entity.scrollTop).toBe(0)
48
- expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
49
- })
50
- })
51
-
52
- describe("scroll()", () => {
53
- beforeEach(() => {
54
- list.create(entity)
55
- })
56
-
57
- it("should calculate visible range based on itemHeight", () => {
58
- entity.itemHeight = 20
59
- const containerEl = { scrollTop: 200 } // 10 items down
60
-
61
- list.scroll(entity, containerEl)
62
-
63
- // start = floor(200 / 20) - 5 = 10 - 5 = 5
64
- // visibleCount = ceil(600 / 20) = 30
65
- // end = min(5 + 30 + 5, 100) = 40
66
- expect(entity.visibleRange).toEqual({ start: 5, end: 40 })
67
- expect(entity.scrollTop).toBe(200)
68
- })
69
-
70
- it("should calculate visible range based on estimatedHeight if itemHeight is null", () => {
71
- entity.estimatedHeight = 50
72
- const containerEl = { scrollTop: 500 } // 10 items down
73
-
74
- list.scroll(entity, containerEl)
75
-
76
- // start = floor(500 / 50) - 5 = 10 - 5 = 5
77
- // visibleCount = ceil(600 / 50) = 12
78
- // end = min(5 + 12 + 5, 100) = 22
79
- expect(entity.visibleRange).toEqual({ start: 5, end: 22 })
80
- })
81
-
82
- it("should not update if the visible range has not changed", () => {
83
- entity.itemHeight = 20
84
- entity.visibleRange = { start: 5, end: 40 }
85
- const originalRange = { ...entity.visibleRange }
86
-
87
- const containerEl = { scrollTop: 201 } // Still within the same range calculation
88
- list.scroll(entity, containerEl)
89
-
90
- expect(entity.visibleRange).toEqual(originalRange) // Should not have changed
91
- })
92
-
93
- it("should handle scrolling to the top", () => {
94
- entity.itemHeight = 20
95
- const containerEl = { scrollTop: 0 }
96
-
97
- list.scroll(entity, containerEl)
98
-
99
- // start = max(0, floor(0/20) - 5) = 0
100
- // end = min(0 + 30 + 5, 100) = 35
101
- expect(entity.visibleRange).toEqual({ start: 0, end: 35 })
102
- })
103
-
104
- it("should handle scrolling near the bottom", () => {
105
- entity.itemHeight = 20
106
- const containerEl = { scrollTop: 1900 } // 95 items down
107
-
108
- list.scroll(entity, containerEl)
109
-
110
- // start = floor(1900 / 20) - 5 = 95 - 5 = 90
111
- // visibleCount = 30
112
- // end = min(90 + 30 + 5, 100) = 100
113
- expect(entity.visibleRange).toEqual({ start: 90, end: 100 })
114
- })
115
- })
116
-
117
- describe("mount()", () => {
118
- it("should measure and set itemHeight and update visibleRange", () => {
119
- list.create(entity)
120
- const itemEl = document.createElement("div")
121
- vi.spyOn(itemEl, "offsetHeight", "get").mockReturnValue(40)
122
-
123
- const containerEl = {
124
- querySelector: vi.fn().mockReturnValue(itemEl),
125
- }
126
-
127
- list.mount(entity, containerEl)
128
-
129
- expect(containerEl.querySelector).toHaveBeenCalledWith("[data-index]")
130
- expect(entity.itemHeight).toBe(40)
131
- // end = ceil(600 / 40) = 15
132
- expect(entity.visibleRange).toEqual({ start: 0, end: 15 })
133
- })
134
-
135
- it("should do nothing if no item element is found", () => {
136
- list.create(entity)
137
- const originalEntity = { ...entity }
138
- const containerEl = {
139
- querySelector: vi.fn().mockReturnValue(null),
140
- }
141
-
142
- list.mount(entity, containerEl)
143
-
144
- expect(entity).toEqual(originalEntity)
145
- })
146
- })
147
-
148
- describe("render()", () => {
149
- let api
150
-
151
- beforeEach(() => {
152
- list.create(entity)
153
- api = {
154
- notify: vi.fn(),
155
- getType: vi.fn().mockReturnValue({
156
- renderItem: (item, index) => html`<div>${index}: ${item.name}</div>`,
157
- }),
158
- }
159
- })
160
-
161
- it("should return a lit-html TemplateResult", () => {
162
- const result = list.render(entity, api)
163
- // Duck-typing for TemplateResult, as it's not a public class
164
- expect(result).toHaveProperty("strings")
165
- expect(result).toHaveProperty("values")
166
- })
167
-
168
- it("should warn and return empty if items are missing", () => {
169
- const spy = vi.spyOn(console, "warn").mockImplementation(() => {})
170
- delete entity.items
171
- const result = list.render(entity, api)
172
- expect(spy).toHaveBeenCalledWith(`list entity ${entity.id} needs 'items'`)
173
- expect(result.strings.join("").trim()).toBe("")
174
- spy.mockRestore()
175
- })
176
-
177
- it("should warn and return empty if type.renderItem is missing", () => {
178
- const spy = vi.spyOn(console, "warn").mockImplementation(() => {})
179
- api.getType.mockReturnValue({}) // No renderItem
180
- const result = list.render(entity, api)
181
- expect(spy).toHaveBeenCalledWith(
182
- `type ${entity.type} needs 'renderItem' method`,
183
- )
184
- expect(result.strings.join("").trim()).toBe("")
185
- spy.mockRestore()
186
- })
187
-
188
- it("should render only the visible items", () => {
189
- entity.itemHeight = 50
190
- entity.visibleRange = { start: 10, end: 15 } // 5 visible items
191
-
192
- const result = list.render(entity, api)
193
- const renderedItems = result.values.find(Array.isArray)
194
-
195
- expect(renderedItems).toHaveLength(5) // 5 visible items
196
- // Check if the first rendered item is indeed item 10
197
- const innerTemplate = renderedItems[0].values[2]
198
- const renderedText = `${innerTemplate.values[0]}: ${innerTemplate.values[1]}`
199
- expect(renderedText).toBe("10: Item 10")
200
- })
201
-
202
- it("should calculate total height correctly", () => {
203
- entity.itemHeight = 40
204
- const result = list.render(entity, api)
205
- // The styleMap for the inner div is at result.values[3].values[0]
206
- const styleValue = result.values[3].values[0]
207
- const expectedHeight = 100 * 40 // items.length * itemHeight
208
- expect(styleValue.height).toBe(`${expectedHeight}px`)
209
- })
210
- })
211
-
212
- describe("renderItem()", () => {
213
- it("should render a default item view", () => {
214
- const item = { id: 1, value: "test" }
215
- const result = list.renderItem(item, 5)
216
- // Duck-typing for TemplateResult
217
- expect(result).toHaveProperty("strings")
218
- expect(result).toHaveProperty("values")
219
- const fullText =
220
- result.strings[0] +
221
- result.values[0] +
222
- result.strings[1] +
223
- result.values[1] +
224
- result.strings[2]
225
- expect(fullText).toBe(`<div>6. ${JSON.stringify(item)}</div>`)
226
- })
227
- })
228
- })
@@ -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
- })