@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/README.md +21 -15
- package/package.json +27 -4
- package/src/{form.js → form/index.js} +6 -6
- package/src/index.js +1 -6
- package/src/{list.js → list/index.js} +2 -2
- package/src/{router.js → router/index.js} +15 -3
- package/src/select/index.js +7 -0
- package/src/table/filters.js +5 -5
- package/src/table/index.js +7 -0
- package/src/table/rendering.js +2 -2
- package/src/table/theme.css +83 -83
- package/src/form.test.js +0 -372
- package/src/list.test.js +0 -228
- package/src/router.test.js +0 -208
- package/src/select.js +0 -7
- package/src/select.test.js +0 -415
- package/src/table.js +0 -7
- package/src/table.test.js +0 -393
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
|
-
})
|