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

(-)a/t/cypress/support/e2e.js (+3 lines)
Lines 24-29 Link Here
24
// -- This will overwrite an existing command --
24
// -- This will overwrite an existing command --
25
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
25
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26
26
27
// Import Select2 helpers
28
import "./select2";
29
27
function get_fallback_login_value(param) {
30
function get_fallback_login_value(param) {
28
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
31
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
29
32
(-)a/t/cypress/support/select2.js (-1 / +509 lines)
Line 0 Link Here
0
- 
1
// Select2Helpers.js - Reusable Cypress functions for Select2 dropdowns
2
3
/**
4
 * Helper functions for interacting with Select2 dropdown components in Cypress tests
5
 * Supports AJAX-based Select2s, triggering all standard Select2 events, and
6
 * multiple selection methods including index and text matching (partial/full)
7
 *
8
 * CHAINABILITY:
9
 * All Select2 helper commands are fully chainable. You can:
10
 * - Chain multiple Select2 operations (search, then select)
11
 * - Chain Select2 commands with standard Cypress commands
12
 * - Split complex interactions into multiple steps for better reliability
13
 *
14
 * Examples:
15
 *   cy.getSelect2('#mySelect')
16
 *     .select2({ search: 'foo' })
17
 *     .select2({ select: 'FooBar' });
18
 *
19
 *   cy.getSelect2('#mySelect')
20
 *     .select2({ search: 'bar' })
21
 *     .select2({ selectIndex: 0 })
22
 *     .should('have.value', 'bar_value');
23
 */
24
25
/**
26
 * Main Select2 interaction command to perform operations on Select2 dropdowns
27
 * @param {string|JQuery} [subject] - Optional jQuery element (when used with .select2())
28
 * @param {Object} options - Configuration options for the Select2 operation
29
 * @param {string} [options.search] - Search text to enter in the search box
30
 * @param {string|Object} [options.select] - Text to select or object with matcher options
31
 * @param {number} [options.selectIndex] - Index of the option to select (0-based)
32
 * @param {string} [options.selector] - CSS selector to find the select element (when not using subject)
33
 * @param {boolean} [options.clearSelection=false] - Whether to clear the current selection first
34
 * @param {Function} [options.matcher] - Custom matcher function for complex option selection
35
 * @param {number} [options.timeout=10000] - Timeout for AJAX responses in milliseconds
36
 * @param {boolean} [options.multiple=false] - Whether the select2 allows multiple selections
37
 * @param {number} [options.minSearchLength=3] - Minimum length of search text needed for Ajax select2
38
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
39
 *
40
 * @example
41
 * // Basic usage
42
 * cy.get('#mySelect').select2({ search: 'foo', select: 'FooBar' });
43
 *
44
 * @example
45
 * // Chained operations (more reliable, especially for AJAX Select2)
46
 * cy.getSelect2('#mySelect')
47
 *   .select2({ search: 'foo' })
48
 *   .select2({ select: 'FooBar' });
49
 *
50
 * @example
51
 * // With custom matcher
52
 * cy.getSelect2('#mySelect').select2({
53
 *   search: 'special',
54
 *   matcher: (option) => option.text.includes('Special Edition')
55
 * });
56
 */
57
Cypress.Commands.add(
58
    "select2",
59
    {
60
        prevSubject: "optional",
61
    },
62
    (subject, options) => {
63
        // Default configuration
64
        const defaults = {
65
            search: null, // Search text to enter in the search box
66
            select: null, // Text to select or object with matcher options
67
            selectIndex: null, // Index of the option to select
68
            selector: null, // CSS selector to find the select element (when not using subject)
69
            clearSelection: false, // Whether to clear the current selection first
70
            matcher: null, // Custom matcher function for complex option selection
71
            timeout: 10000, // Timeout for AJAX responses
72
            multiple: false, // Whether the select2 allows multiple selections
73
            minSearchLength: 3, // Minimum length of search text needed for Ajax select2
74
        };
75
76
        // Merge passed options with defaults
77
        const config = { ...defaults, ...options };
78
79
        // Handle selecting the target Select2 element
80
        let $originalSelect;
81
        if (subject) {
82
            $originalSelect = subject;
83
        } else if (config.selector) {
84
            $originalSelect = cy.get(config.selector);
85
        } else {
86
            throw new Error(
87
                "Either provide a subject or a selector to identify the Select2 element"
88
            );
89
        }
90
91
        return cy.wrap($originalSelect).then($el => {
92
            const selectId = $el.attr("id");
93
94
            // Find the Select2 container - based on the actual DOM structure
95
            // Look for a span.select2-container that's a sibling of the original select
96
            cy.get(`select#${selectId}`)
97
                .siblings(".select2-container")
98
                .first()
99
                .then($container => {
100
                    // Find the select2-selection element within the container and click it
101
                    cy.wrap($container)
102
                        .find(".select2-selection")
103
                        .first()
104
                        .click({ force: true });
105
106
                    // Handle search functionality
107
                    if (config.search !== null) {
108
                        // Find the search field in the dropdown (which is appended to body)
109
                        cy.get(
110
                            ".select2-search--dropdown .select2-search__field"
111
                        )
112
                            .first()
113
                            .should("be.visible")
114
                            .type(config.search, { force: true });
115
116
                        // If this is an Ajax select2 that requires minimum characters
117
                        if (config.search.length < config.minSearchLength) {
118
                            cy.log(
119
                                `Warning: Search text "${config.search}" may be too short for Ajax select2 (minimum typically ${config.minSearchLength} characters)`
120
                            );
121
122
                            // Check if we see the "Please enter X or more characters" message
123
                            cy.get(".select2-results__message").then(
124
                                $message => {
125
                                    if (
126
                                        $message.length > 0 &&
127
                                        $message.text().includes("character")
128
                                    ) {
129
                                        cy.log(
130
                                            "Detected minimum character requirement message. Search string may be too short."
131
                                        );
132
                                    }
133
                                }
134
                            );
135
                        }
136
137
                        // Wait for results to load - use a longer timeout for AJAX-based Select2
138
                        cy.get(".select2-results__options", {
139
                            timeout: config.timeout,
140
                        }).should("exist");
141
142
                        // Wait a moment for AJAX responses to complete
143
                        cy.wait(300); // Small wait to ensure AJAX response completes
144
145
                        // Check for no results to provide helpful error
146
                        cy.get(".select2-results__option").then($options => {
147
                            if (
148
                                $options.length === 0 ||
149
                                ($options.length === 1 &&
150
                                    $options
151
                                        .first()
152
                                        .text()
153
                                        .includes("No results")) ||
154
                                ($options.length === 1 &&
155
                                    $options
156
                                        .first()
157
                                        .text()
158
                                        .includes("Please enter"))
159
                            ) {
160
                                cy.log(
161
                                    `Warning: No results found for search: "${config.search}"`
162
                                );
163
                            }
164
                        });
165
                    }
166
167
                    // Handle selection based on the provided options
168
                    if (
169
                        config.select !== null ||
170
                        config.selectIndex !== null ||
171
                        config.matcher !== null
172
                    ) {
173
                        // Get the results options
174
                        cy.get(".select2-results__option").then($options => {
175
                            if (
176
                                $options.length === 0 ||
177
                                ($options.length === 1 &&
178
                                    $options
179
                                        .first()
180
                                        .text()
181
                                        .includes("No results")) ||
182
                                ($options.length === 1 &&
183
                                    $options
184
                                        .first()
185
                                        .text()
186
                                        .includes("Please enter"))
187
                            ) {
188
                                throw new Error(
189
                                    `No selectable options found for search: "${config.search}"`
190
                                );
191
                            }
192
193
                            let $optionToSelect;
194
195
                            // Select by index
196
                            if (config.selectIndex !== null) {
197
                                if (
198
                                    config.selectIndex < 0 ||
199
                                    config.selectIndex >= $options.length
200
                                ) {
201
                                    throw new Error(
202
                                        `Select2: index ${config.selectIndex} out of bounds (0-${$options.length - 1})`
203
                                    );
204
                                }
205
                                $optionToSelect = $options.eq(
206
                                    config.selectIndex
207
                                );
208
                            }
209
                            // Select by custom matcher function
210
                            else if (
211
                                config.matcher !== null &&
212
                                typeof config.matcher === "function"
213
                            ) {
214
                                for (let i = 0; i < $options.length; i++) {
215
                                    const $option = $options.eq(i);
216
                                    const optionContent = {
217
                                        text: $option.text().trim(),
218
                                        html: $option.html(),
219
                                        element: $option[0],
220
                                    };
221
222
                                    if (config.matcher(optionContent, i)) {
223
                                        $optionToSelect = $option;
224
                                        break;
225
                                    }
226
                                }
227
                            }
228
                            // Select by text (default)
229
                            else if (config.select !== null) {
230
                                // If config.select is a string, use default matching
231
                                if (typeof config.select === "string") {
232
                                    const selectText = config.select;
233
234
                                    // Try exact match first
235
                                    const exactMatch = $options.filter(
236
                                        (i, el) =>
237
                                            Cypress.$(el).text().trim() ===
238
                                            selectText
239
                                    );
240
241
                                    if (exactMatch.length) {
242
                                        $optionToSelect = exactMatch.first();
243
                                    } else {
244
                                        // Fall back to partial match
245
                                        const partialMatches = $options.filter(
246
                                            (i, el) =>
247
                                                Cypress.$(el)
248
                                                    .text()
249
                                                    .trim()
250
                                                    .includes(selectText)
251
                                        );
252
253
                                        if (partialMatches.length) {
254
                                            $optionToSelect =
255
                                                partialMatches.first();
256
                                        }
257
                                    }
258
                                }
259
                                // Handle object format for advanced matching
260
                                else if (typeof config.select === "object") {
261
                                    const matchType =
262
                                        config.select.matchType || "partial"; // 'exact', 'partial', 'startsWith', 'endsWith'
263
                                    const text = config.select.text;
264
265
                                    if (!text) {
266
                                        throw new Error(
267
                                            'When using object format for selection, "text" property is required'
268
                                        );
269
                                    }
270
271
                                    let matcher;
272
                                    switch (matchType) {
273
                                        case "exact":
274
                                            matcher = optText =>
275
                                                optText === text;
276
                                            break;
277
                                        case "startsWith":
278
                                            matcher = optText =>
279
                                                optText.startsWith(text);
280
                                            break;
281
                                        case "endsWith":
282
                                            matcher = optText =>
283
                                                optText.endsWith(text);
284
                                            break;
285
                                        case "partial":
286
                                        default:
287
                                            matcher = optText =>
288
                                                optText.includes(text);
289
                                    }
290
291
                                    for (let i = 0; i < $options.length; i++) {
292
                                        const $option = $options.eq(i);
293
                                        const optionText = $option
294
                                            .text()
295
                                            .trim();
296
297
                                        if (matcher(optionText)) {
298
                                            $optionToSelect = $option;
299
                                            break;
300
                                        }
301
                                    }
302
                                }
303
                            }
304
305
                            // If we found an option to select, click it
306
                            if ($optionToSelect) {
307
                                cy.wrap($optionToSelect).click({ force: true });
308
309
                                // Check that the original select has a value (to verify selection worked)
310
                                // We just check that it has some value, not a specific value
311
                                cy.get(`select#${selectId}`).should($el => {
312
                                    expect($el.val()).to.not.be.null;
313
                                    expect($el.val()).to.not.equal("");
314
                                });
315
                            } else {
316
                                throw new Error(
317
                                    `Could not find any option matching the selection criteria. Search: "${config.search}", Select: "${config.select}"`
318
                                );
319
                            }
320
                        });
321
                    }
322
                });
323
        });
324
    }
325
);
326
327
/**
328
 * Helper to get a Select2 dropdown by any jQuery-like selector
329
 * This is the recommended starting point for chainable Select2 operations
330
 *
331
 * @param {string} selector - jQuery-like selector for the original select element
332
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
333
 *
334
 * @example
335
 * // Chain multiple Select2 operations
336
 * cy.getSelect2('#bookSelect')
337
 *   .select2({ search: 'JavaScript' })
338
 *   .select2({ select: 'JavaScript: The Good Parts' });
339
 *
340
 * @example
341
 * // Chain with standard Cypress assertions
342
 * cy.getSelect2('#categorySelect')
343
 *   .select2({ select: 'Fiction' })
344
 *   .should('have.value', 'fiction');
345
 */
346
Cypress.Commands.add("getSelect2", selector => {
347
    return cy.get(selector);
348
});
349
350
/**
351
 * Helper to clear a Select2 selection
352
 * Can be used as a standalone command or as part of a chain
353
 *
354
 * @param {string} selector - jQuery-like selector for the original select element
355
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
356
 *
357
 * @example
358
 * // Standalone usage
359
 * cy.clearSelect2('#tagSelect');
360
 *
361
 * @example
362
 * // As part of a chain
363
 * cy.getSelect2('#tagSelect')
364
 *   .select2({ select: 'Mystery' })
365
 *   .clearSelect2('#tagSelect')
366
 *   .select2({ select: 'Fantasy' });
367
 */
368
Cypress.Commands.add("clearSelect2", selector => {
369
    return cy.getSelect2(selector).select2({ clearSelection: true });
370
});
371
372
/**
373
 * Helper to search in a Select2 dropdown without making a selection
374
 * Useful for testing search functionality or as part of a multi-step interaction
375
 *
376
 * @param {string} selector - jQuery-like selector for the original select element
377
 * @param {string} searchText - Text to search for
378
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
379
 *
380
 * @example
381
 * // Standalone usage to test search functionality
382
 * cy.searchSelect2('#authorSelect', 'Gaiman');
383
 *
384
 * @example
385
 * // Chained with selection - more reliable for AJAX Select2s
386
 * cy.getSelect2('#publisherSelect')
387
 *   .searchSelect2('#publisherSelect', "O'Reilly")
388
 *   .wait(500) // Allow time for AJAX results
389
 *   .select2({ selectIndex: 0 });
390
 */
391
Cypress.Commands.add("searchSelect2", (selector, searchText) => {
392
    return cy.getSelect2(selector).select2({ search: searchText });
393
});
394
395
/**
396
 * Helper to select an option in a Select2 dropdown by text
397
 * Combines search and select in one command, but can be less reliable for AJAX Select2s
398
 *
399
 * @param {string} selector - jQuery-like selector for the original select element
400
 * @param {string|Object} selectText - Text to select or object with matcher options
401
 * @param {string} [searchText=null] - Optional text to search for before selecting
402
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
403
 *
404
 * @example
405
 * // Basic usage
406
 * cy.selectFromSelect2('#authorSelect', 'J.R.R. Tolkien');
407
 *
408
 * @example
409
 * // With search text
410
 * cy.selectFromSelect2('#publisherSelect', 'O\'Reilly Media', 'O\'Reilly');
411
 *
412
 * @example
413
 * // Using advanced matching options
414
 * cy.selectFromSelect2('#bookSelect',
415
 *   { text: 'The Hobbit', matchType: 'exact' },
416
 *   'Hobbit'
417
 * );
418
 *
419
 * @example
420
 * // Chainable with other Cypress commands
421
 * cy.selectFromSelect2('#categorySelect', 'Fiction')
422
 *   .should('have.value', 'fiction')
423
 *   .and('be.visible');
424
 */
425
Cypress.Commands.add(
426
    "selectFromSelect2",
427
    (selector, selectText, searchText = null) => {
428
        return cy.getSelect2(selector).select2({
429
            search: searchText,
430
            select: selectText,
431
        });
432
    }
433
);
434
435
/**
436
 * Helper to select an option in a Select2 dropdown by index
437
 * Useful when the exact text is unknown or when needing to select a specific item by position
438
 *
439
 * @param {string} selector - jQuery-like selector for the original select element
440
 * @param {number} index - Index of the option to select (0-based)
441
 * @param {string} [searchText=null] - Optional text to search for before selecting
442
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
443
 *
444
 * @example
445
 * // Select the first item in dropdown
446
 * cy.selectFromSelect2ByIndex('#categorySelect', 0);
447
 *
448
 * @example
449
 * // Search first, then select by index
450
 * cy.selectFromSelect2ByIndex('#bookSelect', 2, 'Fiction');
451
 *
452
 * @example
453
 * // Chain with assertions
454
 * cy.selectFromSelect2ByIndex('#authorSelect', 0)
455
 *   .should('not.have.value', '')
456
 *   .and('be.visible');
457
 */
458
Cypress.Commands.add(
459
    "selectFromSelect2ByIndex",
460
    (selector, index, searchText = null) => {
461
        return cy.getSelect2(selector).select2({
462
            search: searchText,
463
            selectIndex: index,
464
        });
465
    }
466
);
467
468
/**
469
 * Helper to select an option in a Select2 dropdown using a custom matcher function
470
 * Most flexible option for complex Select2 structures with nested elements or specific attributes
471
 *
472
 * @param {string} selector - jQuery-like selector for the original select element
473
 * @param {Function} matcherFn - Custom function to match against option content
474
 * @param {string} [searchText=null] - Optional text to search for before selecting
475
 * @returns {Cypress.Chainable} - Returns a chainable Cypress object for further commands
476
 *
477
 * @example
478
 * // Select option with specific data attribute
479
 * cy.selectFromSelect2WithMatcher('#bookSelect',
480
 *   (option) => option.element.hasAttribute('data-special') &&
481
 *               option.text.includes('Special Edition'),
482
 *   'Lord of the Rings'
483
 * );
484
 *
485
 * @example
486
 * // Select option that contains both title and author
487
 * cy.selectFromSelect2WithMatcher('#bookSelect',
488
 *   (option) => option.text.includes('Tolkien') && option.text.includes('Hobbit'),
489
 *   'Tolkien'
490
 * );
491
 *
492
 * @example
493
 * // Chain with other commands
494
 * cy.selectFromSelect2WithMatcher('#publisherSelect',
495
 *   (option) => option.html.includes('<em>Premium</em>'),
496
 *   'Premium'
497
 * ).then(() => {
498
 *   cy.get('#premium-options').should('be.visible');
499
 * });
500
 */
501
Cypress.Commands.add(
502
    "selectFromSelect2WithMatcher",
503
    (selector, matcherFn, searchText = null) => {
504
        return cy.getSelect2(selector).select2({
505
            search: searchText,
506
            matcher: matcherFn,
507
        });
508
    }
509
);

Return to bug 39916