Skip to content

Commit

Permalink
Fix ajax search and maxOptions. Add initial Cypress tests (#2710)
Browse files Browse the repository at this point in the history
* fix ajax search. remove load method. support more argument

* swap source.load with source.data

* reset scrollTop when performing source.search

* Add source.pageSize option. Wait until user has started scrolling before loading next page. Pass correct searchTerm. Trigger fetched event. Change first page to 1.

* const -> var

* linting

* initial Cypress tests

* cypress workflow

* cypress config file

* fix maxOptions. add support for maxOptions when using source.data

* add tests for maxOptions
  • Loading branch information
caseyjhol authored Mar 17, 2022
1 parent 3b22002 commit 04cc1fd
Show file tree
Hide file tree
Showing 11 changed files with 1,767 additions and 4,830 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v2
with:
build: grunt build
1 change: 1 addition & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
137 changes: 137 additions & 0 deletions cypress/integration/basic.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const testArr = new Array(1000).fill(0).map((x, i) => {
return {
text: `Option ${i}`,
}
});

function buildPages(data) {
const pageSize = 40;
const pageCount = data.length / pageSize;
const pages = [];

for (let i = 0; i < pageCount; i++) {
const pageStart = i * pageSize;
pages.push(data.slice(pageStart, pageStart + pageSize));
}

return pages;
}

const selectpickerConfigs = [{
title: 'built via HTML',
config: {
html: `
<select class="selectpicker" data-live-search="true">
${testArr.map((obj) =>
`<option value="${obj.text}">${obj.text}</option>`
).join('')}
</select>
`
}
}, {
title: 'built via source with source.search',
config: {
options: {
liveSearch: true,
source: {
data: function (callback, page) {
const pages = buildPages(testArr);
callback(pages[page - 1], pages.length > page);
},
search: function (callback, page, searchTerm) {
if (searchTerm) {
const searchResults = testArr.filter((obj) => {
return (obj.text && obj.text.toLowerCase().includes(searchTerm.toLowerCase()))
});
const pages = buildPages(searchResults);

callback(pages[page - 1], pages.length > page);
}
}
}
}
}
}, {
title: 'built via source',
config: {
options: {
liveSearch: true,
source: {
data: function (callback) {
callback(testArr);
},
}
}
}
}, {
title: 'built via source (limit 30) with source.search',
config: {
options: {
liveSearch: true,
source: {
data: function (callback, page) {
const pages = buildPages(testArr.slice(0, 30));
callback(pages[page - 1], pages.length > page);
},
search: function (callback, page, searchTerm) {
if (searchTerm) {
const searchResults = testArr.slice(0, 30).filter((obj) => {
return (obj.text && obj.text.toLowerCase().includes(searchTerm.toLowerCase()))
});
const pages = buildPages(searchResults);

callback(pages[page - 1], pages.length > page);
}
}
}
}
}
}, {
title: 'built via source (limit 30)',
config: {
options: {
liveSearch: true,
source: {
data: function (callback) {
callback(testArr.slice(0, 30));
},
}
}
}
}];

describe('Single selects with search', () => {
beforeEach(() => {
cy.visit('/tests/index.html');
});

selectpickerConfigs.forEach(({config, title}) => {
it(title + ' can search for and select options', () => {
cy.selectpicker(config).then(($select) => {
const button = `[data-id="${$select[0].id}"]`;
cy.get(button).click();
$select.on('fetched.bs.select', cy.stub().as('fetched'));

cy.get('input').type('option 4');

if (config?.source?.search) {
cy.get('@fetched').its('callCount').should('equal', 8);
}
cy.get('.dropdown-menu').find('li').first().should(($el) => {
expect($el).to.have.class('active')
expect($el).to.contain('Option 4')
});

cy.get('.dropdown-menu').find('li').first().click();
cy.get(button).contains('Option 4').click();

cy.get('li').contains('Option 4').should('have.class', 'active');

cy.get('li').contains('Option 9').click();
cy.get(button).contains('Option 9').click();
cy.get('li').contains('Option 9').should('have.class', 'active');
cy.get('li').contains('Option 4').should('not.have.class', 'active');
});
});
});
});
82 changes: 82 additions & 0 deletions cypress/integration/max-options.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const optgroupTestArr = new Array(3).fill(0).map((x, i) => {
return {
text: `Optgroup ${i}`,
maxOptions: i + 1,
children: new Array(4).fill(0).map((x, j) => {
return {
text: `Option ${i}-${j}`,
}
})
}
});

describe('Multi-selects with maxOptions', () => {
beforeEach(() => {
cy.visit('/tests/index.html');
});

[{
title: 'built via HTML',
config: {
html: `
<select class="selectpicker" data-live-search="true" multiple data-max-options="4">
${optgroupTestArr.map((obj) =>
`<optgroup label="${obj.text}" data-max-options="${obj.maxOptions}">
${obj.children.map((option) => `<option value="${option.text}">${option.text}</option>`)}
</optgroup>`
).join('')}
</select>
`
}
}, {
title: 'built via source',
config: {
attrs: {
multiple: true,
'data-max-options': 4
},
options: {
liveSearch: true,
source: {
data: function (callback) {
callback(optgroupTestArr);
},
}
}
}
}].forEach(({config, title}) => {
it(title + ' selection is limited by maxOptions', () => {
cy.selectpicker(config).then(($select) => {
const button = `[data-id="${$select[0].id}"]`;
cy.get(button).click();
// $select.on('fetched.bs.select', cy.stub().as('fetched'));

// maxOptions 1
cy.get('li').contains('Option 0-0').click();
cy.get('li').contains('Option 0-1').click();
cy.get('li').contains('Option 0-0').should('not.have.class', 'selected');
cy.get('li').contains('Option 0-1').should('have.class', 'selected');

// maxOptions 2
cy.get('li').contains('Option 1-0').click();
cy.get('li').contains('Option 1-1').click();
cy.get('li').contains('Option 1-2').click();
cy.get('.notify').should('be.visible');
cy.get('li').contains('Option 1-0').should('have.class', 'selected');
cy.get('li').contains('Option 1-1').should('have.class', 'selected');
cy.get('li').contains('Option 1-2').should('not.have.class', 'selected');

// maxOptions 4 (on select)
cy.get('li').contains('Option 2-0').click();
cy.get('li').contains('Option 2-1').click();
cy.get('.notify').should('be.visible');
cy.get('li').contains('Option 2-0').should('have.class', 'selected');
cy.get('li').contains('Option 2-1').should('not.have.class', 'selected');

cy.get(`#${$select[0].id}`)
.invoke('val')
.should('deep.equal', ['Option 0-1', 'Option 1-0', 'Option 1-1', 'Option 2-0']);
});
});
});
});
22 changes: 22 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
31 changes: 31 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {v4 as uuidv4} from 'uuid';

Cypress.Commands.add('selectpicker', ({options, attrs, selector, html}) => {
let id;
if (!selector) {
id = uuidv4();
selector = `#${id}`;
}

cy.window().then(window => {
const $ = window.$;

if (html) {
const $select = $(html);

if (id && !$select[0].id) {
$select.attr('id', id);
}
$('body').append($select);
} else if (id) {
$('body').append(`<select id="${id}" />`);
if (attrs) {
$(`#${id}`).attr(attrs);
}
}

$(selector).selectpicker(options);
});

return cy.get(selector);
});
20 changes: 20 additions & 0 deletions cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')
Loading

0 comments on commit 04cc1fd

Please sign in to comment.