@cable8mm/jquery-infinite-with-template 1.0.4 → 1.1.2
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/.editorconfig +14 -0
- package/.github/workflows/publish.yml +24 -0
- package/.github/workflows/static.yml +49 -0
- package/.github/workflows/test.yml +33 -0
- package/.vscode/settings.json +4 -0
- package/AGENTS.md +216 -0
- package/README.md +266 -180
- package/examples/data_sources.json +84 -0
- package/examples/generate +55 -11
- package/examples/index.html +3 -3
- package/jest.config.js +8 -0
- package/jquery.infiniteScrollWithTemplate.d.ts +65 -0
- package/jquery.infiniteScrollWithTemplate.js +95 -10
- package/jquery.infiniteScrollWithTemplate.min.js +1 -1
- package/package.json +20 -2
- package/scripts/prepare-dist.js +72 -0
- package/tests/jquery.infiniteScrollWithTemplate.test.js +310 -0
- package/tests/setup.js +16 -0
- package/examples/data_sources.ajax +0 -84
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const $ = require("jquery");
|
|
6
|
+
require("../jquery.infiniteScrollWithTemplate.js");
|
|
7
|
+
|
|
8
|
+
describe("jQuery Infinite With Template Plugin", () => {
|
|
9
|
+
let $container;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Setup DOM
|
|
13
|
+
document.body.innerHTML = `
|
|
14
|
+
<div id="test-container"></div>
|
|
15
|
+
<div id="load-button">Load More</div>
|
|
16
|
+
<script id="test-template" type="text/x-jsrender">
|
|
17
|
+
<div class="item">{{:id}}: {{:title}}</div>
|
|
18
|
+
</script>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
$container = $("#test-container");
|
|
22
|
+
$container.empty();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Cleanup
|
|
27
|
+
$(window).off(".infiniteTemplate");
|
|
28
|
+
$(document).off(".infiniteTemplate");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("Initialization", () => {
|
|
32
|
+
test("should return jQuery object for chaining", () => {
|
|
33
|
+
$.ajax = jest.fn((options) => {
|
|
34
|
+
options.success({ data: [] });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = $container.infiniteTemplate({
|
|
38
|
+
templateSelector: "#test-template",
|
|
39
|
+
dataPath: "/api/test",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result).toBeInstanceOf($);
|
|
43
|
+
expect(result).toEqual($container);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should require templateSelector", () => {
|
|
47
|
+
const consoleError = jest.spyOn(console, "error").mockImplementation();
|
|
48
|
+
|
|
49
|
+
$.ajax = jest.fn();
|
|
50
|
+
|
|
51
|
+
$container.infiniteTemplate({
|
|
52
|
+
dataPath: "/api/test",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
56
|
+
"infiniteTemplate: templateSelector is required",
|
|
57
|
+
);
|
|
58
|
+
expect($.ajax).not.toHaveBeenCalled();
|
|
59
|
+
|
|
60
|
+
consoleError.mockRestore();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("should require dataPath", () => {
|
|
64
|
+
const consoleError = jest.spyOn(console, "error").mockImplementation();
|
|
65
|
+
|
|
66
|
+
$.ajax = jest.fn();
|
|
67
|
+
|
|
68
|
+
$container.infiniteTemplate({
|
|
69
|
+
templateSelector: "#test-template",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
73
|
+
"infiniteTemplate: dataPath is required",
|
|
74
|
+
);
|
|
75
|
+
expect($.ajax).not.toHaveBeenCalled();
|
|
76
|
+
|
|
77
|
+
consoleError.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("Data Loading", () => {
|
|
82
|
+
test("should load data via AJAX", () => {
|
|
83
|
+
$.ajax = jest.fn((options) => {
|
|
84
|
+
options.success({
|
|
85
|
+
data: [
|
|
86
|
+
{ id: 1, title: "Item 1" },
|
|
87
|
+
{ id: 2, title: "Item 2" },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
$container.infiniteTemplate({
|
|
93
|
+
templateSelector: "#test-template",
|
|
94
|
+
dataPath: "/api/test",
|
|
95
|
+
loadAtStart: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect($.ajax).toHaveBeenCalled();
|
|
99
|
+
expect($.ajax).toHaveBeenCalledWith(
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
url: expect.stringContaining("/api/test"),
|
|
102
|
+
method: "GET",
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should pass data to template", () => {
|
|
108
|
+
$.ajax = jest.fn((options) => {
|
|
109
|
+
options.success({
|
|
110
|
+
data: [{ id: 1, title: "Item 1" }],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
$container.infiniteTemplate({
|
|
115
|
+
templateSelector: "#test-template",
|
|
116
|
+
dataPath: "/api/test",
|
|
117
|
+
loadAtStart: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Verify AJAX was called with correct URL
|
|
121
|
+
expect($.ajax).toHaveBeenCalled();
|
|
122
|
+
const call = $.ajax.mock.calls[0];
|
|
123
|
+
expect(call[0].url).toContain("page=1");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("Callbacks", () => {
|
|
128
|
+
test("should call zeroCallback when no data", () => {
|
|
129
|
+
const zeroCallback = jest.fn();
|
|
130
|
+
|
|
131
|
+
$.ajax = jest.fn((options) => {
|
|
132
|
+
options.success({ data: [] });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
$container.infiniteTemplate({
|
|
136
|
+
templateSelector: "#test-template",
|
|
137
|
+
dataPath: "/api/test",
|
|
138
|
+
zeroCallback: zeroCallback,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(zeroCallback).toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("should call errorCallback on AJAX error", () => {
|
|
145
|
+
const errorCallback = jest.fn();
|
|
146
|
+
|
|
147
|
+
$.ajax = jest.fn((options) => {
|
|
148
|
+
options.error(new Error("Test error"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
$container.infiniteTemplate({
|
|
152
|
+
templateSelector: "#test-template",
|
|
153
|
+
dataPath: "/api/test",
|
|
154
|
+
errorCallback: errorCallback,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(errorCallback).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should call errorCallback on JSON parse error", () => {
|
|
161
|
+
const errorCallback = jest.fn();
|
|
162
|
+
|
|
163
|
+
$.ajax = jest.fn((options) => {
|
|
164
|
+
options.success("invalid json{");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
$container.infiniteTemplate({
|
|
168
|
+
templateSelector: "#test-template",
|
|
169
|
+
dataPath: "/api/test",
|
|
170
|
+
errorCallback: errorCallback,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(errorCallback).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("should call loadingCallback", () => {
|
|
177
|
+
const loadingCallback = jest.fn();
|
|
178
|
+
|
|
179
|
+
$.ajax = jest.fn((options) => {
|
|
180
|
+
options.success({ data: [] });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
$container.infiniteTemplate({
|
|
184
|
+
templateSelector: "#test-template",
|
|
185
|
+
dataPath: "/api/test",
|
|
186
|
+
loadingCallback: loadingCallback,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(loadingCallback).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("should call loadedCallback", () => {
|
|
193
|
+
const loadedCallback = jest.fn();
|
|
194
|
+
|
|
195
|
+
$.ajax = jest.fn((options) => {
|
|
196
|
+
options.success({ data: [] });
|
|
197
|
+
if (options.complete) {
|
|
198
|
+
options.complete();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
$container.infiniteTemplate({
|
|
203
|
+
templateSelector: "#test-template",
|
|
204
|
+
dataPath: "/api/test",
|
|
205
|
+
loadedCallback: loadedCallback,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(loadedCallback).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("Click Loading", () => {
|
|
213
|
+
test("should load data on button click", () => {
|
|
214
|
+
$.ajax = jest.fn((options) => {
|
|
215
|
+
options.success({ data: [{ id: 1, title: "Item 1" }] });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
$container.infiniteTemplate({
|
|
219
|
+
templateSelector: "#test-template",
|
|
220
|
+
dataPath: "/api/test",
|
|
221
|
+
loadAtStart: false,
|
|
222
|
+
loadSelector: "#load-button",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect($.ajax).not.toHaveBeenCalled();
|
|
226
|
+
|
|
227
|
+
$("#load-button").click();
|
|
228
|
+
|
|
229
|
+
expect($.ajax).toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("URL Building", () => {
|
|
234
|
+
test("should include page parameter", () => {
|
|
235
|
+
$.ajax = jest.fn((options) => {
|
|
236
|
+
expect(options.url).toContain("page=1");
|
|
237
|
+
options.success({ data: [] });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
$container.infiniteTemplate({
|
|
241
|
+
templateSelector: "#test-template",
|
|
242
|
+
dataPath: "/api/test",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("should include query parameters", () => {
|
|
247
|
+
$.ajax = jest.fn((options) => {
|
|
248
|
+
expect(options.url).toContain("word=test");
|
|
249
|
+
options.success({ data: [] });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
$container.infiniteTemplate({
|
|
253
|
+
templateSelector: "#test-template",
|
|
254
|
+
dataPath: "/api/test",
|
|
255
|
+
query: "word=test",
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("should add cache buster when preventCache is true", () => {
|
|
260
|
+
$.ajax = jest.fn((options) => {
|
|
261
|
+
expect(options.url).toMatch(/[?&]t=\d+/);
|
|
262
|
+
options.success({ data: [] });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
$container.infiniteTemplate({
|
|
266
|
+
templateSelector: "#test-template",
|
|
267
|
+
dataPath: "/api/test",
|
|
268
|
+
preventCache: true,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Edge Cases", () => {
|
|
274
|
+
test("should handle empty data array", () => {
|
|
275
|
+
const zeroCallback = jest.fn();
|
|
276
|
+
|
|
277
|
+
$.ajax = jest.fn((options) => {
|
|
278
|
+
options.success({ data: [] });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
$container.infiniteTemplate({
|
|
282
|
+
templateSelector: "#test-template",
|
|
283
|
+
dataPath: "/api/test",
|
|
284
|
+
zeroCallback: zeroCallback,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect($container.find(".item").length).toBe(0);
|
|
288
|
+
expect(zeroCallback).toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("should handle JSON string response", () => {
|
|
292
|
+
$.ajax = jest.fn((options) => {
|
|
293
|
+
options.success(JSON.stringify({ data: [{ id: 1, title: "Item 1" }] }));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
$container.infiniteTemplate({
|
|
297
|
+
templateSelector: "#test-template",
|
|
298
|
+
dataPath: "/api/test",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Verify AJAX was called successfully
|
|
302
|
+
expect($.ajax).toHaveBeenCalled();
|
|
303
|
+
expect($.ajax).toHaveBeenCalledWith(
|
|
304
|
+
expect.objectContaining({
|
|
305
|
+
url: expect.any(String),
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
package/tests/setup.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Mock jQuery and jsRender for testing
|
|
2
|
+
global.jQuery = require("jquery");
|
|
3
|
+
global.$ = global.jQuery;
|
|
4
|
+
|
|
5
|
+
// Use actual jsrender
|
|
6
|
+
const jsrender = require("jsrender");
|
|
7
|
+
global.jsrender = jsrender;
|
|
8
|
+
global.$.templates = jsrender.templates;
|
|
9
|
+
|
|
10
|
+
// Mock window.location
|
|
11
|
+
Object.defineProperty(window, "location", {
|
|
12
|
+
value: {
|
|
13
|
+
origin: "http://localhost:8080",
|
|
14
|
+
},
|
|
15
|
+
writable: true,
|
|
16
|
+
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"data": [
|
|
3
|
-
{
|
|
4
|
-
"id": 886,
|
|
5
|
-
"title": "ZjLn9jCi1w8nsFSye6A2q9cuu78U88(word=None)"
|
|
6
|
-
},
|
|
7
|
-
{
|
|
8
|
-
"id": 91,
|
|
9
|
-
"title": "VVTFOrWminUAi8E3whuFiyi76sf6L3(word=None)"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"id": 654,
|
|
13
|
-
"title": "Roc3yFmOHpm4Koj8hhuRr5fBNAUcxZ(word=None)"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": 788,
|
|
17
|
-
"title": "zR7rzbUz81pPYFxvm3NBDFGCUqB0cy(word=None)"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": 7,
|
|
21
|
-
"title": "1OHxyIMH9DahoskhqVbCFINH3aVysn(word=None)"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"id": 915,
|
|
25
|
-
"title": "9kX2B97YL0onvcE4nRETuACESX9YIM(word=None)"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"id": 125,
|
|
29
|
-
"title": "jc5fTJ5Y1QzYOtQy6XhiMlRxq05KAc(word=None)"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"id": 362,
|
|
33
|
-
"title": "rsoNXzq590AQzf6Hp0Gi8rHQfaz9KI(word=None)"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"id": 101,
|
|
37
|
-
"title": "1msDlWisPm0Q9oUofPdhgjOHuZtlep(word=None)"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"id": 206,
|
|
41
|
-
"title": "nev0CvpOKc1E0zUuvTrgMUluSowoz5(word=None)"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"id": 544,
|
|
45
|
-
"title": "aIMdZ9LAf5SDl9hmeFIqzCMgU2joOx(word=None)"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"id": 415,
|
|
49
|
-
"title": "ikYBbV7nA4S2LPCWk2TYXQ4H5Vp7ia(word=None)"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"id": 717,
|
|
53
|
-
"title": "H6u32LKXqbgU8rx7UTjsALjKfUPZru(word=None)"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"id": 536,
|
|
57
|
-
"title": "JuyIiHNlLn0H97APMtB5ng72GAVzBr(word=None)"
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"id": 554,
|
|
61
|
-
"title": "e8cbqBh9ifoEI2nFqyryDBwE87Boy2(word=None)"
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
"id": 111,
|
|
65
|
-
"title": "yCJNeoMlqkuNazccgeKwMoj2d9xC6v(word=None)"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"id": 134,
|
|
69
|
-
"title": "2qZpgg3lJ3djS5RG5IZzZjdtf7Y9IJ(word=None)"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"id": 523,
|
|
73
|
-
"title": "LC84RteVTWGuAgDDhknpb7jREpqdfY(word=None)"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": 946,
|
|
77
|
-
"title": "q0NnpnSWosOwbxIYQE3ZiMbuqJKG40(word=None)"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"id": 30,
|
|
81
|
-
"title": "sHc5R2u1W0T45AMxQRx73gAFbGHXk0(word=None)"
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
}
|