@inglorious/web 2.6.1 → 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.
package/src/form.test.js DELETED
@@ -1,372 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { beforeEach, describe, expect, it, vi } from "vitest"
5
-
6
- import { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
7
-
8
- describe("form", () => {
9
- let entity
10
-
11
- beforeEach(() => {
12
- entity = {
13
- id: "test-form",
14
- initialValues: {
15
- name: "John Doe",
16
- contact: {
17
- email: "john.doe@example.com",
18
- },
19
- tags: ["a", "b"],
20
- },
21
- }
22
- })
23
-
24
- describe("init()", () => {
25
- it("should add global event listeners for click and submit", () => {
26
- const spy = vi.spyOn(document, "addEventListener")
27
- form.init(entity)
28
- expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
29
- expect(spy).toHaveBeenCalledWith("submit", expect.any(Function))
30
- spy.mockRestore()
31
- })
32
- })
33
-
34
- describe("create()", () => {
35
- it("should initialize the form", () => {
36
- entity.values = {}
37
- form.create(entity)
38
- expect(entity.values).toEqual(entity.initialValues)
39
- })
40
-
41
- it("should reset the form to its initial state", () => {
42
- // mess up the state first
43
- entity.values = {}
44
- entity.isPristine = false
45
-
46
- form.create(entity)
47
-
48
- expect(entity.values).toEqual(entity.initialValues)
49
- expect(entity.values).not.toBe(entity.initialValues) // should be a clone
50
- expect(entity.isPristine).toBe(true)
51
- expect(entity.isValid).toBe(true)
52
- expect(entity.errors).toEqual({
53
- name: null,
54
- contact: { email: null },
55
- tags: [null, null],
56
- })
57
- expect(entity.touched).toEqual({
58
- name: null,
59
- contact: { email: null },
60
- tags: [null, null],
61
- })
62
- })
63
- })
64
-
65
- describe("reset()", () => {
66
- it("should reset the form to its initial state", () => {
67
- form.create(entity)
68
- entity.values.name = "Jane Doe"
69
- entity.isPristine = false
70
-
71
- form.reset(entity)
72
-
73
- expect(entity.values).toEqual(entity.initialValues)
74
- expect(entity.isPristine).toBe(true)
75
- })
76
- })
77
-
78
- describe("fieldChange()", () => {
79
- beforeEach(() => {
80
- form.create(entity)
81
- })
82
-
83
- it("should update a field's value, mark it as touched, and set form to dirty", () => {
84
- form.fieldChange(entity, { path: "name", value: "Jane Doe" })
85
-
86
- expect(entity.values.name).toBe("Jane Doe")
87
- expect(entity.touched.name).toBe(true)
88
- expect(entity.isPristine).toBe(false)
89
- })
90
-
91
- it("should update a nested field's value", () => {
92
- form.fieldChange(entity, {
93
- path: "contact.email",
94
- value: "jane.doe@example.com",
95
- })
96
-
97
- expect(entity.values.contact.email).toBe("jane.doe@example.com")
98
- expect(entity.touched.contact.email).toBe(true)
99
- })
100
-
101
- it("should validate the field if a validate function is provided", () => {
102
- const validate = (value) => (value.length < 10 ? "Too short" : null)
103
- form.fieldChange(entity, { path: "name", value: "Jane", validate })
104
-
105
- expect(entity.errors.name).toBe("Too short")
106
- expect(entity.isValid).toBe(false)
107
- })
108
-
109
- it("should clear a field's error when it becomes valid", () => {
110
- const validate = (value) => (value.length < 10 ? "Too short" : null)
111
- form.fieldChange(entity, { path: "name", value: "Jane", validate })
112
- expect(entity.isValid).toBe(false)
113
-
114
- form.fieldChange(entity, {
115
- path: "name",
116
- value: "Jane Doe The Great",
117
- validate,
118
- })
119
-
120
- expect(entity.errors.name).toBe(null)
121
- expect(entity.isValid).toBe(true)
122
- })
123
- })
124
-
125
- describe("fieldBlur()", () => {
126
- beforeEach(() => {
127
- form.create(entity)
128
- })
129
-
130
- it("should mark a field as touched", () => {
131
- form.fieldBlur(entity, { path: "name" })
132
- expect(entity.touched.name).toBe(true)
133
- })
134
-
135
- it("should validate the field if a validate function is provided", () => {
136
- const validate = (value) =>
137
- value === "John Doe" ? "Cannot be John" : null
138
- form.fieldBlur(entity, { path: "name", validate })
139
-
140
- expect(entity.errors.name).toBe("Cannot be John")
141
- expect(entity.isValid).toBe(false)
142
- })
143
- })
144
-
145
- describe("field array operations", () => {
146
- beforeEach(() => {
147
- form.create(entity)
148
- })
149
-
150
- it("fieldArrayAppend: should append a value and metadata", () => {
151
- form.fieldArrayAppend(entity, { path: "tags", value: "c" })
152
-
153
- expect(entity.values.tags).toEqual(["a", "b", "c"])
154
- expect(entity.errors.tags).toEqual([null, null, null])
155
- expect(entity.touched.tags).toEqual([null, null, null])
156
- expect(entity.isPristine).toBe(false)
157
- })
158
-
159
- it("fieldArrayRemove: should remove a value and metadata at an index", () => {
160
- form.fieldArrayRemove(entity, { path: "tags", index: 0 })
161
-
162
- expect(entity.values.tags).toEqual(["b"])
163
- expect(entity.errors.tags).toEqual([null])
164
- expect(entity.touched.tags).toEqual([null])
165
- expect(entity.isPristine).toBe(false)
166
- })
167
-
168
- it("fieldArrayInsert: should insert a value and metadata at an index", () => {
169
- form.fieldArrayInsert(entity, { path: "tags", index: 1, value: "c" })
170
-
171
- expect(entity.values.tags).toEqual(["a", "c", "b"])
172
- expect(entity.errors.tags).toEqual([null, null, null])
173
- expect(entity.touched.tags).toEqual([null, null, null])
174
- expect(entity.isPristine).toBe(false)
175
- })
176
-
177
- it("fieldArrayMove: should move a value and metadata", () => {
178
- entity.errors.tags[0] = "Error on A"
179
- entity.touched.tags[0] = true
180
-
181
- form.fieldArrayMove(entity, { path: "tags", fromIndex: 0, toIndex: 1 })
182
-
183
- expect(entity.values.tags).toEqual(["b", "a"])
184
- expect(entity.errors.tags).toEqual([null, "Error on A"])
185
- expect(entity.touched.tags).toEqual([null, true])
186
- expect(entity.isPristine).toBe(false)
187
- })
188
-
189
- it("fieldArray operations should warn if path is not an array", () => {
190
- const spy = vi.spyOn(console, "warn").mockImplementation(() => {})
191
-
192
- form.fieldArrayAppend(entity, { path: "name", value: "c" })
193
- expect(spy).toHaveBeenCalledWith("Field at path 'name' is not an array")
194
-
195
- spy.mockRestore()
196
- })
197
- })
198
-
199
- describe("validate()", () => {
200
- beforeEach(() => {
201
- form.create(entity)
202
- })
203
-
204
- it("should set errors and isValid based on the validation function", () => {
205
- const validate = (values) => {
206
- const errors = {}
207
- if (values.name !== "Jane Doe") {
208
- errors.name = "Name must be Jane Doe"
209
- }
210
- return errors
211
- }
212
-
213
- form.validate(entity, { validate })
214
-
215
- expect(entity.errors.name).toBe("Name must be Jane Doe")
216
- expect(entity.isValid).toBe(false)
217
- })
218
-
219
- it("should set isValid to true if there are no errors", () => {
220
- const validate = () => ({})
221
-
222
- entity.isValid = false
223
- form.validate(entity, { validate })
224
-
225
- expect(entity.errors).toEqual({})
226
- expect(entity.isValid).toBe(true)
227
- })
228
- })
229
-
230
- describe("validateAsync()", () => {
231
- let api
232
-
233
- beforeEach(() => {
234
- form.create(entity)
235
- api = { notify: vi.fn() }
236
- })
237
-
238
- it("should set isValidating to true and call the validation function", async () => {
239
- const validate = vi.fn().mockResolvedValue({})
240
- await form.validateAsync(entity, { validate }, api)
241
- expect(entity.isValidating).toBe(true)
242
- expect(validate).toHaveBeenCalledWith(entity.values)
243
- })
244
-
245
- it("should notify 'validationComplete' on successful validation", async () => {
246
- const errors = { name: "Required" }
247
- const validate = async () => errors
248
- await form.validateAsync(entity, { validate }, api)
249
-
250
- expect(api.notify).toHaveBeenCalledWith(
251
- `#${entity.id}:validationComplete`,
252
- {
253
- errors,
254
- isValid: false,
255
- },
256
- )
257
- })
258
-
259
- it("should notify 'validationError' on validation failure", async () => {
260
- const error = new Error("Validation failed")
261
- const validate = async () => {
262
- throw error
263
- }
264
- await form.validateAsync(entity, { validate }, api)
265
-
266
- expect(api.notify).toHaveBeenCalledWith(`#${entity.id}:validationError`, {
267
- error: "Validation failed",
268
- })
269
- })
270
- })
271
-
272
- describe("validationComplete()", () => {
273
- it("should update form state after async validation", () => {
274
- form.create(entity)
275
- entity.isValidating = true
276
- const errors = { name: "Error" }
277
-
278
- form.validationComplete(entity, { errors, isValid: false })
279
-
280
- expect(entity.isValidating).toBe(false)
281
- expect(entity.errors).toBe(errors)
282
- expect(entity.isValid).toBe(false)
283
- })
284
- })
285
-
286
- describe("validationError()", () => {
287
- it("should update form state after an async validation error", () => {
288
- form.create(entity)
289
- entity.isValidating = true
290
-
291
- form.validationError(entity, { error: "Network Error" })
292
-
293
- expect(entity.isValidating).toBe(false)
294
- expect(entity.submitError).toBe("Network Error")
295
- })
296
- })
297
- })
298
-
299
- describe("form helpers", () => {
300
- let testForm
301
-
302
- beforeEach(() => {
303
- testForm = {
304
- values: {
305
- user: { name: "Alex" },
306
- tags: ["one", "two"],
307
- },
308
- errors: {
309
- user: { name: "Too short" },
310
- tags: [null, "Invalid tag"],
311
- },
312
- touched: {
313
- user: { name: true },
314
- tags: [true, false],
315
- },
316
- }
317
- })
318
-
319
- describe("getFieldValue()", () => {
320
- it("should return the value of a field at a given path", () => {
321
- expect(getFieldValue(testForm, "user.name")).toBe("Alex")
322
- })
323
-
324
- it("should return an array value", () => {
325
- expect(getFieldValue(testForm, "tags")).toEqual(["one", "two"])
326
- })
327
-
328
- it("should return a default value if path does not exist", () => {
329
- expect(getFieldValue(testForm, "user.age", 30)).toBe(30)
330
- })
331
-
332
- it("should return undefined if path does not exist and no default is provided", () => {
333
- expect(getFieldValue(testForm, "user.age")).toBeUndefined()
334
- })
335
- })
336
-
337
- describe("getFieldError()", () => {
338
- it("should return the error of a field at a given path", () => {
339
- expect(getFieldError(testForm, "user.name")).toBe("Too short")
340
- })
341
-
342
- it("should return the error of a field in an array", () => {
343
- expect(getFieldError(testForm, "tags.1")).toBe("Invalid tag")
344
- })
345
-
346
- it("should return null for a valid field in an array", () => {
347
- expect(getFieldError(testForm, "tags.0")).toBeNull()
348
- })
349
-
350
- it("should return undefined if path does not exist", () => {
351
- expect(getFieldError(testForm, "user.age")).toBe(undefined)
352
- })
353
- })
354
-
355
- describe("isFieldTouched()", () => {
356
- it("should return true if a field has been touched", () => {
357
- expect(isFieldTouched(testForm, "user.name")).toBe(true)
358
- })
359
-
360
- it("should return true for a touched field in an array", () => {
361
- expect(isFieldTouched(testForm, "tags.0")).toBe(true)
362
- })
363
-
364
- it("should return false for an untouched field in an array", () => {
365
- expect(isFieldTouched(testForm, "tags.1")).toBe(false)
366
- })
367
-
368
- it("should return false if path does not exist", () => {
369
- expect(isFieldTouched(testForm, "user.age")).toBe(false)
370
- })
371
- })
372
- })
package/src/list.test.js DELETED
@@ -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 "./list.js"
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
- })