View | Details | Raw Unified | Return to bug 40174
Collapse All | Expand All

(-)a/t/cypress/integration/KohaTable/Holdings_spec.ts (-412 / +189 lines)
Lines 1-5 Link Here
1
const RESTdefaultPageSize = "20"; // FIXME Mock this
1
const RESTdefaultPageSize = "20"; // FIXME Mock this
2
const baseTotalCount = "42";
2
const baseTotalCount = "21";
3
3
4
describe("catalogue/detail/holdings_table with items", () => {
4
describe("catalogue/detail/holdings_table with items", () => {
5
    const table_id = "holdings_table";
5
    const table_id = "holdings_table";
Lines 10-117 describe("catalogue/detail/holdings_table with items", () => { Link Here
10
            win.localStorage.clear();
10
            win.localStorage.clear();
11
        });
11
        });
12
12
13
        // FIXME All the following code should not be reused as it
14
        // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio"
15
        let generated_objects = {};
16
        const objects = [{ object: "library" }, { object: "item_type" }];
17
        cy.wrap(Promise.resolve())
18
            .then(() => {
19
                return objects.reduce((chain, { object }) => {
20
                    return chain.then(() => {
21
                        return cy
22
                            .task("buildSampleObject", { object })
23
                            .then(attributes => {
24
                                generated_objects[object] = attributes;
25
                            });
26
                    });
27
                }, Promise.resolve());
28
            })
29
            .then(() => {
30
                const library = generated_objects["library"];
31
                const item_type = generated_objects["item_type"];
32
                const queries = [
33
                    {
34
                        query: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
35
                        values: [library.library_id, library.name],
36
                    },
37
                    {
38
                        query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
39
                        values: [item_type.item_type_id, item_type.description],
40
                    },
41
                ];
42
                cy.wrap(Promise.resolve())
43
                    .then(() => {
44
                        return queries.reduce((chain, { query, values }) => {
45
                            return chain.then(() =>
46
                                cy.task("query", { sql: query, values })
47
                            );
48
                        }, Promise.resolve());
49
                    })
50
                    .then(() => {
51
                        let biblio = {
52
                            leader: "     nam a22     7a 4500",
53
                            fields: [
54
                                { "005": "20250120101920.0" },
55
                                {
56
                                    "245": {
57
                                        ind1: "",
58
                                        ind2: "",
59
                                        subfields: [{ a: "Some boring read" }],
60
                                    },
61
                                },
62
                                {
63
                                    "100": {
64
                                        ind1: "",
65
                                        ind2: "",
66
                                        subfields: [
67
                                            { c: "Some boring author" },
68
                                        ],
69
                                    },
70
                                },
71
                                {
72
                                    "942": {
73
                                        ind1: "",
74
                                        ind2: "",
75
                                        subfields: [
76
                                            { c: item_type.item_type_id },
77
                                        ],
78
                                    },
79
                                },
80
                            ],
81
                        };
82
                        cy.request({
83
                            method: "POST",
84
                            url: "/api/v1/biblios",
85
                            headers: {
86
                                "Content-Type": "application/marc-in-json",
87
                                "x-confirm-not-duplicate": 1,
88
                            },
89
                            body: biblio,
90
                        }).then(response => {
91
                            const biblio_id = response.body.id;
92
                            cy.wrap(biblio_id).as("biblio_id");
93
                            cy.request({
94
                                method: "POST",
95
                                url: `/api/v1/biblios/${biblio_id}/items`,
96
                                headers: {
97
                                    "Content-Type": "application/json",
98
                                },
99
                                body: {
100
                                    home_library_id: library.library_id,
101
                                    holding_library_id: library.library_id,
102
                                },
103
                            });
104
                        });
105
                    });
106
            });
107
        cy.task("query", {
13
        cy.task("query", {
108
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
14
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
109
        }).then(value => {
15
        }).then(value => {
110
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
16
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
111
        });
17
        });
18
19
        cy.task("insertSampleBiblio", { item_count: baseTotalCount }).then(
20
            objects => {
21
                cy.wrap(objects).as("objects");
22
            }
23
        );
112
    });
24
    });
113
25
114
    afterEach(function () {
26
    afterEach(function () {
27
        cy.task("deleteSampleObjects", this.objects);
115
        cy.set_syspref(
28
        cy.set_syspref(
116
            "AlwaysShowHoldingsTableFilters",
29
            "AlwaysShowHoldingsTableFilters",
117
            this.syspref_AlwaysShowHoldingsTableFilters
30
            this.syspref_AlwaysShowHoldingsTableFilters
Lines 119-151 describe("catalogue/detail/holdings_table with items", () => { Link Here
119
    });
32
    });
120
33
121
    it("Correctly init the table", function () {
34
    it("Correctly init the table", function () {
122
        // Do not use `() => {` or this.biblio_id won't be retrieved
35
        // Do not use `() => {` or this.objets won't be retrieved
123
        const biblio_id = this.biblio_id;
36
        const biblio_id = this.objects.biblio.biblio_id;
124
        cy.task("buildSampleObjects", {
37
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
125
            object: "item",
126
            count: RESTdefaultPageSize,
127
            values: {
128
                biblio_id,
129
                checkout: null,
130
                transfer: null,
131
                lost_status: 0,
132
                withdrawn: 0,
133
                damaged_status: 0,
134
                not_for_loan_status: 0,
135
                course_item: null,
136
                cover_image_ids: [],
137
                _status: ["available"],
138
            },
139
        }).then(items => {
140
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
141
                statuscode: 200,
142
                body: items,
143
                headers: {
144
                    "X-Base-Total-Count": baseTotalCount,
145
                    "X-Total-Count": baseTotalCount,
146
                },
147
            });
148
149
            cy.visit(
38
            cy.visit(
150
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
39
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
151
            );
40
            );
Lines 162-409 describe("catalogue/detail/holdings_table with items", () => { Link Here
162
    });
51
    });
163
52
164
    it("Show filters", function () {
53
    it("Show filters", function () {
165
        // Do not use `() => {` or this.biblio_id won't be retrieved
54
        // Do not use `() => {` or this.objects won't be retrieved
166
        const biblio_id = this.biblio_id;
55
        const biblio_id = this.objects.biblio.biblio_id;
167
        cy.task("buildSampleObjects", {
168
            object: "item",
169
            count: RESTdefaultPageSize,
170
            values: {
171
                biblio_id,
172
                checkout: null,
173
                transfer: null,
174
                lost_status: 0,
175
                withdrawn: 0,
176
                damaged_status: 0,
177
                not_for_loan_status: 0,
178
                course_item: null,
179
                cover_image_ids: [],
180
                _status: ["available"],
181
            },
182
        }).then(items => {
183
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
184
                statuscode: 200,
185
                body: items,
186
                headers: {
187
                    "X-Base-Total-Count": baseTotalCount,
188
                    "X-Total-Count": baseTotalCount,
189
                },
190
            });
191
56
192
            cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => {
57
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => {
193
                cy.visit(
58
            cy.visit(
194
                    "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" +
59
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
195
                        biblio_id
60
            );
196
                );
197
61
198
                // Hide the 'URL' column
62
            // Hide the 'URL' column
199
                cy.mock_table_settings(
63
            cy.mock_table_settings(
200
                    {
64
                {
201
                        columns: { uri: { is_hidden: 1 } },
65
                    columns: { uri: { is_hidden: 1 } },
202
                    },
66
                },
203
                    "items_table_settings.holdings"
67
                "items_table_settings.holdings"
204
                );
68
            );
205
69
206
                cy.get("@columns").then(columns => {
70
            cy.get("@columns").then(columns => {
207
                    cy.get(`#${table_id}_wrapper tbody tr`).should(
71
                cy.get(`#${table_id}_wrapper tbody tr`).should(
208
                        "have.length",
72
                    "have.length",
209
                        RESTdefaultPageSize
73
                    RESTdefaultPageSize
210
                    );
74
                );
211
75
212
                    // Filters are not displayed
76
                // Filters are not displayed
213
                    cy.get(`#${table_id} thead tr`).should("have.length", 1);
77
                cy.get(`#${table_id} thead tr`).should("have.length", 1);
214
78
215
                    cy.get(`#${table_id} th`).contains("Status");
79
                cy.get(`#${table_id} th`).contains("Status");
216
                    cy.get(`#${table_id} th`)
80
                cy.get(`#${table_id} th`).contains("URL").should("not.exist");
217
                        .contains("URL")
81
                cy.get(`#${table_id} th`)
218
                        .should("not.exist");
82
                    .contains("Course reserves")
219
                    cy.get(`#${table_id} th`)
83
                    .should("not.exist");
220
                        .contains("Course reserves")
221
                        .should("not.exist");
222
84
223
                    cy.get(`.${table_id}_table_controls .show_filters`).click();
85
                cy.get(`.${table_id}_table_controls .show_filters`).click();
224
                    cy.get(`#${table_id}_wrapper .dt-info`).contains(
86
                cy.get(`#${table_id}_wrapper .dt-info`).contains(
225
                        `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries`
87
                    `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries`
226
                    );
88
                );
227
                    // Filters are displayed
89
                // Filters are displayed
228
                    cy.get(`#${table_id} thead tr`).should("have.length", 2);
90
                cy.get(`#${table_id} thead tr`).should("have.length", 2);
229
91
230
                    cy.get(`#${table_id} th`).contains("Status");
92
                cy.get(`#${table_id} th`).contains("Status");
231
                    cy.get(`#${table_id} th`)
93
                cy.get(`#${table_id} th`).contains("URL").should("not.exist");
232
                        .contains("URL")
94
                cy.get(`#${table_id} th`)
233
                        .should("not.exist");
95
                    .contains("Course reserves")
234
                    cy.get(`#${table_id} th`)
96
                    .should("not.exist");
235
                        .contains("Course reserves")
236
                        .should("not.exist");
237
                });
238
            });
97
            });
98
        });
239
99
240
            cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
100
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
241
                cy.visit(
101
            cy.visit(
242
                    "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" +
102
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
243
                        biblio_id
103
            );
244
                );
245
104
246
                // Hide the 'URL' column
105
            // Hide the 'URL' column
247
                cy.mock_table_settings(
106
            cy.mock_table_settings(
248
                    {
107
                {
249
                        columns: { uri: { is_hidden: 1 } },
108
                    columns: { uri: { is_hidden: 1 } },
250
                    },
109
                },
251
                    "items_table_settings.holdings"
110
                "items_table_settings.holdings"
252
                );
111
            );
253
112
254
                cy.get("@columns").then(columns => {
113
            cy.get("@columns").then(columns => {
255
                    cy.get(`#${table_id}_wrapper tbody tr`).should(
114
                cy.get(`#${table_id}_wrapper tbody tr`).should(
256
                        "have.length",
115
                    "have.length",
257
                        RESTdefaultPageSize
116
                    RESTdefaultPageSize
258
                    );
117
                );
259
118
260
                    // Filters are displayed
119
                // Filters are displayed
261
                    cy.get(`#${table_id} thead tr`).should("have.length", 2);
120
                cy.get(`#${table_id} thead tr`).should("have.length", 2);
262
121
263
                    cy.get(`.${table_id}_table_controls .hide_filters`).click();
122
                cy.get(`.${table_id}_table_controls .hide_filters`).click();
264
123
265
                    // Filters are not displayed
124
                // Filters are not displayed
266
                    cy.get(`#${table_id} thead tr`).should("have.length", 1);
125
                cy.get(`#${table_id} thead tr`).should("have.length", 1);
267
                });
268
            });
126
            });
269
        });
127
        });
270
    });
128
    });
271
129
272
    it("Filters by code and description", function () {
130
    it("Filters by code and description", function () {
273
        // Do not use `() => {` or this.biblio_id won't be retrieved
131
        // Do not use `() => {` or this.objects won't be retrieved
274
        const biblio_id = this.biblio_id;
132
        const biblio_id = this.objects.biblio.biblio_id;
275
        cy.task("buildSampleObjects", {
276
            object: "item",
277
            count: RESTdefaultPageSize,
278
            values: {
279
                biblio_id,
280
                checkout: null,
281
                transfer: null,
282
                lost_status: 0,
283
                withdrawn: 0,
284
                damaged_status: 0,
285
                not_for_loan_status: 0,
286
                course_item: null,
287
                cover_image_ids: [],
288
                _status: ["available"],
289
            },
290
        }).then(items => {
291
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
292
                statuscode: 200,
293
                body: items,
294
                headers: {
295
                    "X-Base-Total-Count": baseTotalCount,
296
                    "X-Total-Count": baseTotalCount,
297
                },
298
            }).as("searchItems");
299
133
300
            cy.visit(
134
        cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`).as(
301
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
135
            "searchItems"
302
            );
136
        );
303
137
304
            cy.window().then(win => {
138
        cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id);
305
                win.coded_values.library = new Map(
139
306
                    items.map(i => [
140
        cy.wait("@searchItems");
307
                        i.home_library.name,
308
                        i.home_library.library_id,
309
                    ])
310
                );
311
                win.coded_values.item_type = new Map(
312
                    items.map(i => [
313
                        i.item_type.description,
314
                        i.item_type.item_type_id,
315
                    ])
316
                );
317
            });
318
            cy.wait("@searchItems");
319
320
            let library_id = items[0].home_library.library_id;
321
            let library_name = items[0].home_library.name;
322
            cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id);
323
324
            cy.wait("@searchItems").then(interception => {
325
                const q = interception.request.query.q;
326
                expect(q).to.match(
327
                    new RegExp(
328
                        `"me.home_library_id":{"like":"%${library_id}%"}`
329
                    )
330
                );
331
            });
332
141
333
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
142
        cy.task("query", {
334
            cy.wait("@searchItems");
143
            sql: "SELECT homebranch FROM items WHERE biblionumber=? LIMIT 1",
335
            cy.get(`#${table_id}_wrapper input.dt-input`).type(library_name);
144
            values: [biblio_id],
145
        }).then(result => {
146
            let library_id = result[0].homebranch;
147
            cy.task("query", {
148
                sql: "SELECT branchname FROM branches WHERE branchcode=?",
149
                values: [library_id],
150
            }).then(result => {
151
                let library_name = result[0].branchname;
152
                cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id);
153
154
                cy.wait("@searchItems").then(interception => {
155
                    const q = interception.request.query.q;
156
                    expect(q).to.match(
157
                        new RegExp(
158
                            `"me.home_library_id":{"like":"%${library_id}%"}`
159
                        )
160
                    );
161
                });
336
162
337
            cy.wait("@searchItems").then(interception => {
163
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
338
                const q = interception.request.query.q;
164
                cy.wait("@searchItems");
339
                expect(q).to.match(
165
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
340
                    new RegExp(`"me.home_library_id":\\["${library_id}"\\]`)
166
                    library_name
341
                );
167
                );
342
            });
343
168
344
            let item_type_id = items[0].item_type.item_type_id;
169
                cy.wait("@searchItems").then(interception => {
345
            let item_type_description = items[0].item_type.description;
170
                    const q = interception.request.query.q;
346
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
171
                    expect(q).to.match(
347
            cy.wait("@searchItems");
172
                        new RegExp(`"me.home_library_id":\\["${library_id}"\\]`)
348
            cy.get(`#${table_id}_wrapper input.dt-input`).type(item_type_id);
173
                    );
174
                });
175
            });
176
        });
349
177
350
            cy.wait("@searchItems").then(interception => {
178
        cy.task("query", {
351
                const q = interception.request.query.q;
179
            sql: "SELECT itype FROM items WHERE biblionumber=? LIMIT 1",
352
                expect(q).to.match(
180
            values: [biblio_id],
353
                    new RegExp(`"me.item_type_id":{"like":"%${item_type_id}%"}`)
181
        }).then(result => {
182
            let item_type_id = result[0].itype;
183
            cy.task("query", {
184
                sql: "SELECT description FROM itemtypes WHERE itemtype=?",
185
                values: [item_type_id],
186
            }).then(result => {
187
                let item_type_description = result[0].description;
188
189
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
190
                cy.wait("@searchItems");
191
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
192
                    item_type_id
354
                );
193
                );
355
            });
356
194
357
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
195
                cy.wait("@searchItems").then(interception => {
358
            cy.wait("@searchItems");
196
                    const q = interception.request.query.q;
359
            cy.get(`#${table_id}_wrapper input.dt-input`).type(
197
                    expect(q).to.match(
360
                item_type_description
198
                        new RegExp(
361
            );
199
                            `"me.item_type_id":{"like":"%${item_type_id}%"}`
200
                        )
201
                    );
202
                });
362
203
363
            cy.wait("@searchItems").then(interception => {
204
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
364
                const q = interception.request.query.q;
205
                cy.wait("@searchItems");
365
                expect(q).to.match(
206
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
366
                    new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`)
207
                    item_type_description
367
                );
208
                );
368
            });
369
209
370
            cy.viewport(2999, 2999);
210
                cy.wait("@searchItems").then(interception => {
371
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
211
                    const q = interception.request.query.q;
372
            cy.wait("@searchItems");
212
                    expect(q).to.match(
373
            // Show filters if not there already
213
                        new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`)
374
            cy.get(`.${table_id}_table_controls .show_filters`)
214
                    );
375
                .then(link => {
215
                });
376
                    if (link.is(":visible")) {
216
377
                        cy.wrap(link).click();
217
                cy.viewport(2999, 2999);
378
                        cy.wait("@searchItems");
218
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
379
                    }
219
                cy.wait("@searchItems");
380
                })
220
                // Show filters if not there already
381
                .then(() => {
221
                cy.get(`.${table_id}_table_controls .show_filters`)
382
                    // Select first (non-empty) option
222
                    .then(link => {
383
                    cy.get(
223
                        if (link.is(":visible")) {
384
                        `#${table_id}_wrapper th#holdings_itype select`
224
                            cy.wrap(link).click();
385
                    ).then(select => {
225
                            cy.wait("@searchItems");
386
                        const raw_value = select.find("option").eq(1).val();
226
                        }
387
                        expect(raw_value).to.match(/^\^/);
227
                    })
388
                        expect(raw_value).to.match(/\$$/);
228
                    .then(() => {
389
                        item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $
229
                        // Select first (non-empty) option
390
                    });
230
                        cy.get(
391
                    cy.get(
231
                            `#${table_id}_wrapper th#holdings_itype select`
392
                        `#${table_id}_wrapper th#holdings_itype select option`
232
                        ).then(select => {
393
                    )
233
                            const raw_value = select.find("option").eq(1).val();
394
                        .eq(1)
234
                            expect(raw_value).to.match(/^\^/);
395
                        .then(o => {
235
                            expect(raw_value).to.match(/\$$/);
396
                            cy.get(
236
                            item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $
397
                                `#${table_id}_wrapper th#holdings_itype select`
237
                        });
398
                            ).select(o.val(), { force: true });
238
                        cy.get(
239
                            `#${table_id}_wrapper th#holdings_itype select option`
240
                        )
241
                            .eq(1)
242
                            .then(o => {
243
                                cy.get(
244
                                    `#${table_id}_wrapper th#holdings_itype select`
245
                                ).select(o.val(), { force: true });
246
                            });
247
                        cy.wait("@searchItems").then(interception => {
248
                            const q = interception.request.query.q;
249
                            expect(q).to.match(
250
                                new RegExp(
251
                                    `{"me.item_type_id":"${item_type_id}"}`
252
                                )
253
                            );
399
                        });
254
                        });
400
                    cy.wait("@searchItems").then(interception => {
401
                        const q = interception.request.query.q;
402
                        expect(q).to.match(
403
                            new RegExp(`{"me.item_type_id":"${item_type_id}"}`)
404
                        );
405
                    });
255
                    });
406
                });
256
            });
407
        });
257
        });
408
    });
258
    });
409
});
259
});
Lines 417-508 describe("catalogue/detail/holdings_table without items", () => { Link Here
417
            win.localStorage.clear();
267
            win.localStorage.clear();
418
        });
268
        });
419
269
420
        // FIXME All the following code should not be reused as it
421
        // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio"
422
        let generated_objects = {};
423
        const objects = [{ object: "library" }, { object: "item_type" }];
424
        cy.wrap(Promise.resolve())
425
            .then(() => {
426
                return objects.reduce((chain, { object }) => {
427
                    return chain.then(() => {
428
                        return cy
429
                            .task("buildSampleObject", { object })
430
                            .then(attributes => {
431
                                generated_objects[object] = attributes;
432
                            });
433
                    });
434
                }, Promise.resolve());
435
            })
436
            .then(() => {
437
                const item_type = generated_objects["item_type"];
438
                const queries = [
439
                    {
440
                        sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
441
                        values: [item_type.item_type_id, item_type.description],
442
                    },
443
                ];
444
                cy.wrap(Promise.resolve())
445
                    .then(() => {
446
                        return queries.reduce((chain, { sql, values }) => {
447
                            return chain.then(() =>
448
                                cy.task("query", { sql, values })
449
                            );
450
                        }, Promise.resolve());
451
                    })
452
                    .then(() => {
453
                        let biblio = {
454
                            leader: "     nam a22     7a 4500",
455
                            fields: [
456
                                { "005": "20250120101920.0" },
457
                                {
458
                                    "245": {
459
                                        ind1: "",
460
                                        ind2: "",
461
                                        subfields: [{ a: "Some boring read" }],
462
                                    },
463
                                },
464
                                {
465
                                    "100": {
466
                                        ind1: "",
467
                                        ind2: "",
468
                                        subfields: [
469
                                            { c: "Some boring author" },
470
                                        ],
471
                                    },
472
                                },
473
                                {
474
                                    "942": {
475
                                        ind1: "",
476
                                        ind2: "",
477
                                        subfields: [
478
                                            { c: item_type.item_type_id },
479
                                        ],
480
                                    },
481
                                },
482
                            ],
483
                        };
484
                        cy.request({
485
                            method: "POST",
486
                            url: "/api/v1/biblios",
487
                            headers: {
488
                                "Content-Type": "application/marc-in-json",
489
                                "x-confirm-not-duplicate": 1,
490
                            },
491
                            body: biblio,
492
                        }).then(response => {
493
                            const biblio_id = response.body.id;
494
                            cy.wrap(biblio_id).as("biblio_id");
495
                        });
496
                    });
497
            });
498
        cy.task("query", {
270
        cy.task("query", {
499
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
271
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
500
        }).then(value => {
272
        }).then(value => {
501
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
273
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
502
        });
274
        });
275
276
        cy.task("insertSampleBiblio", { item_count: 0 }).then(objects => {
277
            cy.wrap(objects).as("objects");
278
        });
503
    });
279
    });
504
280
505
    afterEach(function () {
281
    afterEach(function () {
282
        cy.task("deleteSampleObjects", this.objects);
506
        cy.set_syspref(
283
        cy.set_syspref(
507
            "AlwaysShowHoldingsTableFilters",
284
            "AlwaysShowHoldingsTableFilters",
508
            this.syspref_AlwaysShowHoldingsTableFilters
285
            this.syspref_AlwaysShowHoldingsTableFilters
Lines 510-517 describe("catalogue/detail/holdings_table without items", () => { Link Here
510
    });
287
    });
511
288
512
    it("Do not display the table", function () {
289
    it("Do not display the table", function () {
513
        // Do not use `() => {` or this.biblio_id won't be retrieved
290
        // Do not use `() => {` or this.objects won't be retrieved
514
        const biblio_id = this.biblio_id;
291
        const biblio_id = this.objects.biblio.biblio_id;
515
292
516
        cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id);
293
        cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id);
517
294
(-)a/t/cypress/integration/t/insertData.ts (+76 lines)
Line 0 Link Here
1
const { query } = require("./../../plugins/db.js");
2
3
describe("insertSampleBiblio", () => {
4
    it("should generate library and item type", () => {
5
        cy.task("insertSampleBiblio", { item_count: 3 }).then(objects => {
6
            const biblio_id = objects.biblio.biblio_id;
7
8
            expect(typeof biblio_id).to.be.equal("number");
9
10
            cy.task("query", {
11
                sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?",
12
                values: [biblio_id],
13
            }).then(result => {
14
                expect(result[0].count).to.be.equal(1);
15
            });
16
17
            cy.task("query", {
18
                sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?",
19
                values: [biblio_id],
20
            }).then(result => {
21
                expect(result[0].count).to.be.equal(3);
22
            });
23
24
            cy.task("query", {
25
                sql: "SELECT DISTINCT(itype) as count FROM items WHERE biblionumber=?",
26
                values: [biblio_id],
27
            }).then(result => {
28
                expect(result.length).to.be.equal(1);
29
            });
30
31
            cy.task("query", {
32
                sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
33
                values: [objects.library.library_id],
34
            }).then(result => {
35
                expect(result[0].count).to.be.equal(1);
36
            });
37
38
            cy.task("query", {
39
                sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?",
40
                values: [objects.item_type.item_type_id],
41
            }).then(result => {
42
                expect(result[0].count).to.be.equal(1);
43
            });
44
45
            cy.task("deleteSampleObjects", objects);
46
47
            cy.task("query", {
48
                sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?",
49
                values: [biblio_id],
50
            }).then(result => {
51
                expect(result[0].count).to.be.equal(0);
52
            });
53
54
            cy.task("query", {
55
                sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?",
56
                values: [biblio_id],
57
            }).then(result => {
58
                expect(result[0].count).to.be.equal(0);
59
            });
60
61
            cy.task("query", {
62
                sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
63
                values: [objects.library.library_id],
64
            }).then(result => {
65
                expect(result[0].count).to.be.equal(0);
66
            });
67
68
            cy.task("query", {
69
                sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?",
70
                values: [objects.item_type.item_type_id],
71
            }).then(result => {
72
                expect(result[0].count).to.be.equal(0);
73
            });
74
        });
75
    });
76
});
(-)a/t/cypress/plugins/index.js (+14 lines)
Lines 2-12 const { startDevServer } = require("@cypress/webpack-dev-server"); Link Here
2
2
3
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
3
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
4
4
5
const {
6
    insertSampleBiblio,
7
    insertObject,
8
    deleteSampleObjects,
9
} = require("./insertData.js");
10
5
const { query } = require("./db.js");
11
const { query } = require("./db.js");
6
12
7
const { apiGet, apiPost, apiPut, apiDelete } = require("./api-client.js");
13
const { apiGet, apiPost, apiPut, apiDelete } = require("./api-client.js");
8
14
9
module.exports = (on, config) => {
15
module.exports = (on, config) => {
16
    const baseUrl = config.baseUrl;
10
    on("dev-server:start", options =>
17
    on("dev-server:start", options =>
11
        startDevServer({
18
        startDevServer({
12
            options,
19
            options,
Lines 16-21 module.exports = (on, config) => { Link Here
16
    on("task", {
23
    on("task", {
17
        buildSampleObject,
24
        buildSampleObject,
18
        buildSampleObjects,
25
        buildSampleObjects,
26
        insertSampleBiblio({ item_count }) {
27
            return insertSampleBiblio(item_count, baseUrl);
28
        },
29
        insertObject({ type, object }) {
30
            return insertObject(type, object, baseUrl, authHeader);
31
        },
32
        deleteSampleObjects,
19
        query,
33
        query,
20
34
21
        apiGet(args) {
35
        apiGet(args) {
(-)a/t/cypress/plugins/insertData.js (+304 lines)
Line 0 Link Here
1
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
2
const { query } = require("./db.js");
3
4
const { APIClient } = require("./dist/api-client.cjs.js");
5
const { Buffer } = require("buffer");
6
7
const insertSampleBiblio = async (item_count, baseUrl) => {
8
    let client = APIClient.default;
9
    let generated_objects = {};
10
    const objects = [{ object: "library" }, { object: "item_type" }];
11
    for (const { object } of objects) {
12
        const attributes = await buildSampleObject({ object });
13
        generated_objects[object] = attributes;
14
    }
15
16
    const library = await insertObject(
17
        "library",
18
        generated_objects["library"],
19
        baseUrl,
20
        authHeader
21
    );
22
    const item_type = await insertObject(
23
        "item_type",
24
        generated_objects["item_type"],
25
        baseUrl,
26
        authHeader
27
    );
28
29
    let biblio = {
30
        leader: "     nam a22     7a 4500",
31
        fields: [
32
            { "005": "20250120101920.0" },
33
            {
34
                245: {
35
                    ind1: "",
36
                    ind2: "",
37
                    subfields: [{ a: "Some boring read" }],
38
                },
39
            },
40
            {
41
                100: {
42
                    ind1: "",
43
                    ind2: "",
44
                    subfields: [{ c: "Some boring author" }],
45
                },
46
            },
47
            {
48
                942: {
49
                    ind1: "",
50
                    ind2: "",
51
                    subfields: [{ c: item_type.item_type_id }],
52
                },
53
            },
54
        ],
55
    };
56
    const credentials = Buffer.from("koha:koha").toString("base64");
57
    let result = await client.koha.post({
58
        endpoint: `${baseUrl}/api/v1/biblios`,
59
        headers: {
60
            "Content-Type": "application/marc-in-json",
61
            "x-confirm-not-duplicate": 1,
62
            Authorization: "Basic " + credentials,
63
        },
64
        body: biblio,
65
    });
66
    const biblio_id = result.id;
67
    // We do not have a route to get a biblio as it is stored in DB
68
    // We might need to refine that in the future
69
    biblio = {
70
        biblio_id,
71
    };
72
73
    let items = buildSampleObjects({
74
        object: "item",
75
        count: item_count,
76
        values: {
77
            biblio_id,
78
            lost_status: 0,
79
            withdrawn: 0,
80
            damaged_status: 0,
81
            not_for_loan_status: 0,
82
            restricted_status: 0,
83
            new_status: null,
84
            issues: 0,
85
            item_type_id: item_type.item_type_id,
86
            home_library_id: library.library_id,
87
            holding_library_id: library.library_id,
88
        },
89
    });
90
    items = items.map(
91
        ({
92
            item_id,
93
            checkout,
94
            transfer,
95
            lost_date,
96
            withdrawn_date,
97
            damaged_date,
98
            course_item,
99
            _strings,
100
            biblio,
101
            bundle_host,
102
            item_group_item,
103
            recall,
104
            return_claim,
105
            return_claims,
106
            serial_item,
107
            first_hold,
108
            checkouts_count,
109
            renewals_count,
110
            holds_count,
111
            bundle_items_lost_count,
112
            analytics_count,
113
            effective_not_for_loan_status,
114
            effective_item_type_id,
115
            home_library,
116
            holding_library,
117
            bundle_items_not_lost_count,
118
            item_type,
119
            _status,
120
            effective_bookable,
121
            in_bundle,
122
            cover_image_ids,
123
            localuse,
124
            ...rest
125
        }) => rest
126
    );
127
    let createdItems = [];
128
    for (const item of items) {
129
        await client.koha
130
            .post({
131
                endpoint: `${baseUrl}/api/v1/biblios/${biblio_id}/items`,
132
                body: item,
133
                headers: {
134
                    "Content-Type": "application/json",
135
                    Authorization: "Basic " + credentials,
136
                },
137
            })
138
            .then(i => createdItems.push(i));
139
    }
140
    return { biblio, items: createdItems, library, item_type };
141
};
142
143
const deleteSampleObjects = async objects => {
144
    const deletionOrder = ["items", "biblio", "library", "item_type"];
145
    for (const type of deletionOrder) {
146
        if (!objects.hasOwnProperty(type)) {
147
            continue;
148
        }
149
        if (Array.isArray(objects[type]) && objects[type].length == 0) {
150
            // Empty array
151
            continue;
152
        }
153
        switch (type) {
154
            case "biblio":
155
                await query({
156
                    sql: "DELETE FROM biblio WHERE biblionumber=?",
157
                    values: [objects[type].biblio_id],
158
                });
159
                break;
160
            case "items":
161
                let item_ids = objects[type].map(i => i.item_id);
162
                await query({
163
                    sql: `DELETE FROM items WHERE itemnumber IN (${item_ids.map(() => "?").join(",")})`,
164
                    values: item_ids,
165
                });
166
                break;
167
            case "item":
168
                await query({
169
                    sql: "DELETE FROM items WHERE itemnumber = ?",
170
                    values: [objects[type].item_id],
171
                });
172
                break;
173
            case "library":
174
                await query({
175
                    sql: "DELETE FROM branches WHERE branchcode = ?",
176
                    values: [objects[type].library_id],
177
                });
178
                break;
179
            case "item_type":
180
                await query({
181
                    sql: "DELETE FROM itemtypes WHERE itemtype = ?",
182
                    values: [objects[type].item_type_id],
183
                });
184
                break;
185
        }
186
    }
187
    return true;
188
};
189
190
const insertLibrary = async (library, baseUrl, authHeader) => {
191
    const {
192
        pickup_items,
193
        smtp_server,
194
        cash_registers,
195
        desks,
196
        library_hours,
197
        needs_override,
198
        ...new_library
199
    } = library;
200
    let client = APIClient.default;
201
    return client.koha.post({
202
        endpoint: `${baseUrl}/api/v1/libraries`,
203
        body: new_library,
204
        headers: {
205
            "Content-Type": "application/json",
206
            Authorization: authHeader,
207
        },
208
    });
209
};
210
211
const insertObject = async (type, object, baseUrl, authHeader) => {
212
    let client = APIClient.default;
213
    if (type == "patron") {
214
        await query({
215
            sql: "SELECT COUNT(*) AS count FROM branches WHERE branchcode = ?",
216
            values: [object.library_id],
217
        }).then(result => {
218
            if (!result[0].count) {
219
                insertLibrary(object.library, baseUrl, authHeader);
220
            }
221
        });
222
        await query({
223
            sql: "SELECT COUNT(*) AS count FROM categories WHERE categorycode= ?",
224
            values: [object.category_id],
225
        }).then(result => {
226
            if (!result[0].count) {
227
                query({
228
                    sql: "INSERT INTO categories(categorycode, description) VALUES (?, ?)",
229
                    values: [
230
                        object.category_id,
231
                        `description for ${object.category_id}`,
232
                    ],
233
                });
234
            }
235
        });
236
        const {
237
            _strings,
238
            anonymized,
239
            restricted,
240
            expired,
241
            extended_attributes,
242
            library,
243
            checkouts_count,
244
            overdues_count,
245
            account_balance,
246
            lang,
247
            login_attempts,
248
            sms_provider_id,
249
            ...patron
250
        } = object;
251
252
        return client.koha.post({
253
            endpoint: `${baseUrl}/api/v1/patrons`,
254
            body: patron,
255
            headers: {
256
                "Content-Type": "application/json",
257
                Authorization: authHeader,
258
            },
259
        });
260
    } else if (type == "library") {
261
        const keysToKeep = ["library_id", "name"];
262
        const library = Object.fromEntries(
263
            Object.entries(object).filter(([key]) => keysToKeep.includes(key))
264
        );
265
        return client.koha.post({
266
            endpoint: `${baseUrl}/api/v1/libraries`,
267
            headers: {
268
                "Content-Type": "application/json",
269
                Authorization: authHeader,
270
            },
271
            body: library,
272
        });
273
    } else if (type == "item_type") {
274
        const keysToKeep = ["item_type_id", "description"];
275
        const item_type = Object.fromEntries(
276
            Object.entries(object).filter(([key]) => keysToKeep.includes(key))
277
        );
278
        return query({
279
            sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
280
            values: [item_type.item_type_id, item_type.description],
281
        })
282
            .then(result => {
283
                // FIXME We need /item_types/:item_type_id
284
                return client.koha.get({
285
                    endpoint: `${baseUrl}/api/v1/item_types?q={"item_type_id":"${item_type.item_type_id}"}`,
286
                    headers: {
287
                        "Content-Type": "application/json",
288
                        Authorization: authHeader,
289
                    },
290
                });
291
            })
292
            .then(item_types => item_types[0]);
293
    } else {
294
        return false;
295
    }
296
297
    return true;
298
};
299
300
module.exports = {
301
    insertSampleBiblio,
302
    insertObject,
303
    deleteSampleObjects,
304
};
(-)a/t/cypress/plugins/mockData.js (-9 / +57 lines)
Lines 1-19 Link Here
1
const { faker } = require("@faker-js/faker");
1
const { faker } = require("@faker-js/faker");
2
const { readYamlFile } = require("./../plugins/readYamlFile.js");
2
const { readYamlFile } = require("./../plugins/readYamlFile.js");
3
const { query } = require("./db.js");
3
const fs = require("fs");
4
const fs = require("fs");
4
5
6
const generatedDataCache = new Set();
7
5
const generateMockData = (type, properties) => {
8
const generateMockData = (type, properties) => {
6
    switch (type) {
9
    switch (type) {
7
        case "string":
10
        case "string":
8
            if (properties?.maxLength) {
11
            if (properties?.maxLength) {
9
                return faker.string.alpha({
12
                return (value = faker.string.alpha({
10
                    length: {
13
                    length: {
11
                        min: properties.minLength || 1,
14
                        min: properties.minLength || 1,
12
                        max: properties.maxLength,
15
                        max: properties.maxLength,
13
                    },
16
                    },
14
                });
17
                }));
15
            }
18
            }
16
            return faker.lorem.words(3);
19
            return (value = faker.lorem.words(3));
17
        case "integer":
20
        case "integer":
18
            return faker.number.int();
21
            return faker.number.int();
19
        case "boolean":
22
        case "boolean":
Lines 56-73 const generateDataFromSchema = (properties, values = {}) => { Link Here
56
                        data = buildSampleObject({ object: "library" });
59
                        data = buildSampleObject({ object: "library" });
57
                        fk_name = "library_id";
60
                        fk_name = "library_id";
58
                        break;
61
                        break;
62
                    case "pickup_library":
63
                        data = buildSampleObject({ object: "library" });
64
                        fk_name = "pickup_library_id";
65
                        break;
66
                    case "library":
67
                        data = buildSampleObject({ object: "library" });
68
                        fk_name = "library_id";
69
                        break;
59
                    case "item_type":
70
                    case "item_type":
60
                        data = buildSampleObject({ object: "item_type" });
71
                        data = buildSampleObject({ object: "item_type" });
61
                        fk_name = "item_type_id";
72
                        fk_name = "item_type_id";
62
                        break;
73
                        break;
74
                    case "item":
75
                        data = buildSampleObject({ object: "item" });
76
                        fk_name = "item_id";
77
                        break;
63
                    default:
78
                    default:
64
                        data = generateMockData(type, value);
79
                        try {
80
                            data = generateMockData(type, value);
81
                        } catch (e) {
82
                            throw new Error(
83
                                `Failed to generate data for (${key}): ${e}`
84
                            );
85
                        }
65
                }
86
                }
66
                if (typeof data === "object") {
87
                if (typeof data === "object") {
67
                    ids[key] = data[fk_name];
88
                    ids[key] = data[fk_name];
68
                }
89
                }
69
            } else {
90
            } else {
70
                data = generateMockData(type, value);
91
                try {
92
                    if (key.match(/_id$/)) {
93
                        let attempts = 0;
94
95
                        do {
96
                            data = generateMockData(type, value);
97
                            attempts++;
98
                            if (attempts > 10) {
99
                                throw new Error(
100
                                    "Could not generate unique string after 10 attempts"
101
                                );
102
                            }
103
                        } while (generatedDataCache.has(data));
104
105
                        generatedDataCache.add(data);
106
                    } else {
107
                        data = generateMockData(type, value);
108
                    }
109
                } catch (e) {
110
                    throw new Error(
111
                        `Failed to generate data for ${key} (${type}): ${e}`
112
                    );
113
                }
71
            }
114
            }
72
            mockData[key] = data;
115
            mockData[key] = data;
73
        }
116
        }
Lines 93-101 const buildSampleObjects = ({ object, values, count = 1 }) => { Link Here
93
        );
136
        );
94
    }
137
    }
95
    const schema = readYamlFile(yamlPath);
138
    const schema = readYamlFile(yamlPath);
96
    return Array.from({ length: count }, () =>
139
    let generatedObject;
97
        generateDataFromSchema(schema.properties, values)
140
    try {
98
    );
141
        generatedObject = Array.from({ length: count }, () =>
142
            generateDataFromSchema(schema.properties, values)
143
        );
144
    } catch (e) {
145
        throw new Error(`Failed to generate data for object '${object}': ${e}`);
146
    }
147
    return generatedObject;
99
};
148
};
100
149
101
const buildSampleObject = ({ object, values = {} }) => {
150
const buildSampleObject = ({ object, values = {} }) => {
102
- 

Return to bug 40174