From 8d813f7bf12503731f70c2acae23f20f287ebe80 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Sun, 18 Dec 2016 15:06:17 +0600 Subject: [PATCH] Fixes #12840, #12836, #13231, #13232 Additionally added tests for #10302 (#10284 (comment)), #10284 (#10217 (comment)), #11729 --- framework/CHANGELOG.md | 4 +- framework/assets/yii.gridView.js | 108 +++- tests/js/data/yii.gridView.html | 170 +++++++ tests/js/tests/yii.gridView.test.js | 754 ++++++++++++++++++++++++++++ 4 files changed, 1009 insertions(+), 27 deletions(-) create mode 100644 tests/js/data/yii.gridView.html create mode 100644 tests/js/tests/yii.gridView.test.js diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index a6561aab91..fae8fdb123 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -21,7 +21,7 @@ Yii Framework 2 Change Log - Bug #12822: Fixed `yii\i18n\Formatter::asTimestamp()` to process timestamp with miliseconds correctly (h311ion) - Bug #12824: Enabled usage of `yii\mutex\FileMutex` on Windows systems (davidsonalencar) - Bug #12828: Fixed handling of nested arrays, objects in `\yii\grid\GridView::guessColumns` (githubjeka) -- Bug #12836: Fixed `yii\widgets\GridView::filterUrl` to not ignore `#` part of filter URL (cebe) +- Bug #12836: Fixed `yii\widgets\GridView::filterUrl` to not ignore `#` part of filter URL (cebe, arogachev) - Bug #12856: Fixed `yii\web\XmlResponseFormatter` to use `true` and `false` to represent booleans (samdark) - Bug #12879: Console progress bar was not working properly in Windows terminals (samdark, kids-return) - Bug #12880: Fixed `yii\behaviors\AttributeTypecastBehavior` marks attributes with `null` value as 'dirty' (klimov-paul) @@ -36,6 +36,8 @@ Yii Framework 2 Change Log - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) - Bug #13200: Creating Urls for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe) +- Bug #13231: Fixed `destroy` method in `yii.gridView.js` which did not work as expected (arogachev) +- Bug #13232: Event handlers were not detached with changed selector in `yii.gridView.js` (arogachev) - Bug #13108: Fix execute command with negative integer parameter (pana1990, uaoleg) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) diff --git a/framework/assets/yii.gridView.js b/framework/assets/yii.gridView.js index 152125a7ba..a08c45f784 100644 --- a/framework/assets/yii.gridView.js +++ b/framework/assets/yii.gridView.js @@ -16,7 +16,7 @@ } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { - $.error('Method ' + method + ' does not exist on jQuery.yiiGridView'); + $.error('Method ' + method + ' does not exist in jQuery.yiiGridView'); return false; } }; @@ -50,6 +50,32 @@ afterFilter: 'afterFilter' }; + /** + * Used for storing active event handlers and removing them later. + * The structure of single event handler is: + * + * { + * gridViewId: { + * type: { + * event: '...', + * selector: '...' + * } + * } + * } + * + * Used types: + * + * - filter, used for filtering grid with elements found by filterSelector + * - checkRow, used for checking single row + * - checkAllRows, used for checking all rows with according "Check all" checkbox + * + * event is the name of event, for example: 'change.yiiGridView' + * selector is a jQuery selector for finding elements + * + * @type {{}} + */ + var gridEventHandlers = {}; + var methods = { init: function (options) { return this.each(function () { @@ -62,32 +88,32 @@ gridData[id] = $.extend(gridData[id], {settings: settings}); + var filterEvents = 'change.yiiGridView keydown.yiiGridView'; var enterPressed = false; - $(document).off('change.yiiGridView keydown.yiiGridView', settings.filterSelector) - .on('change.yiiGridView keydown.yiiGridView', settings.filterSelector, function (event) { - if (event.type === 'keydown') { - if (event.keyCode !== 13) { - return; // only react to enter key - } else { - enterPressed = true; - } + initEventHandler($e, 'filter', filterEvents, settings.filterSelector, function (event) { + if (event.type === 'keydown') { + if (event.keyCode !== 13) { + return; // only react to enter key } else { - // prevent processing for both keydown and change events - if (enterPressed) { - enterPressed = false; - return; - } + enterPressed = true; } + } else { + // prevent processing for both keydown and change events + if (enterPressed) { + enterPressed = false; + return; + } + } - methods.applyFilter.apply($e); + methods.applyFilter.apply($e); - return false; - }); + return false; + }); }); }, applyFilter: function () { - var $grid = $(this), event; + var $grid = $(this); var settings = gridData[$grid.attr('id')].settings; var data = {}; $.each($(settings.filterSelector).serializeArray(), function () { @@ -119,8 +145,8 @@ var pos = settings.filterUrl.indexOf('?'); var url = pos < 0 ? settings.filterUrl : settings.filterUrl.substring(0, pos); var hashPos = settings.filterUrl.indexOf('#'); - if (hashPos >= 0) { - url += settings.filterUrl.substring(pos); + if (pos >= 0 && hashPos >= 0) { + url += settings.filterUrl.substring(hashPos); } $grid.find('form.gridview-filter-form').remove(); @@ -137,7 +163,7 @@ }); }); - event = $.Event(gridEvents.beforeFilter); + var event = $.Event(gridEvents.beforeFilter); $grid.trigger(event); if (event.result === false) { return; @@ -161,10 +187,10 @@ var checkAll = "#" + id + " input[name='" + options.checkAll + "']"; var inputs = options['class'] ? "input." + options['class'] : "input[name='" + options.name + "']"; var inputsEnabled = "#" + id + " " + inputs + ":enabled"; - $(document).off('click.yiiGridView', checkAll).on('click.yiiGridView', checkAll, function () { + initEventHandler($grid, 'checkAllRows', 'click.yiiGridView', checkAll, function () { $grid.find(inputs + ":enabled").prop('checked', this.checked); }); - $(document).off('click.yiiGridView', inputsEnabled).on('click.yiiGridView', inputsEnabled, function () { + initEventHandler($grid, 'checkRow', 'click.yiiGridView', inputsEnabled, function () { var all = $grid.find(inputs).length == $grid.find(inputs + ":checked").length; $grid.find("input[name='" + options.checkAll + "']").prop('checked', all); }); @@ -183,10 +209,17 @@ }, destroy: function () { - return this.each(function () { - $(window).unbind('.yiiGridView'); - $(this).removeData('yiiGridView'); + var events = ['.yiiGridView', gridEvents.beforeFilter, gridEvents.afterFilter].join(' '); + this.off(events); + + var id = $(this).attr('id'); + $.each(gridEventHandlers[id], function (type, data) { + $(document).off(data.event, data.selector); }); + + delete gridData[id]; + + return this; }, data: function () { @@ -194,4 +227,27 @@ return gridData[id]; } }; + + /** + * Used for attaching event handler and prevent of duplicating them. With each call previously attached handler of + * the same type is removed even selector was changed. + * @param {jQuery} $gridView According jQuery grid view element + * @param {string} type Type of the event which acts like a key + * @param {string} event Event name, for example 'change.yiiGridView' + * @param {string} selector jQuery selector + * @param {function} callback The actual function to be executed with this event + */ + function initEventHandler($gridView, type, event, selector, callback) { + var id = $gridView.attr('id'); + var prevHandler = gridEventHandlers[id]; + if (prevHandler !== undefined && prevHandler[type] !== undefined) { + var data = prevHandler[type]; + $(document).off(data.event, data.selector); + } + if (prevHandler === undefined) { + gridEventHandlers[id] = {}; + } + $(document).on(event, selector, callback); + gridEventHandlers[id][type] = {event: event, selector: selector}; + } })(window.jQuery); diff --git a/tests/js/data/yii.gridView.html b/tests/js/data/yii.gridView.html new file mode 100644 index 0000000000..939fce3136 --- /dev/null +++ b/tests/js/data/yii.gridView.html @@ -0,0 +1,170 @@ + + +
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameCategoryTags
  + + + +
Name 1Programminghtml, css
Name 2Programmingjs
Name 3Programmingphp
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDName
 
1Name 1
2Name 2
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTags
+ + +
Name 1html, css
Name 2js
Name 3php
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + IDName
 
+ + + 1Name 1
+ + + 2Name 2
+
diff --git a/tests/js/tests/yii.gridView.test.js b/tests/js/tests/yii.gridView.test.js new file mode 100644 index 0000000000..be91ea7cf6 --- /dev/null +++ b/tests/js/tests/yii.gridView.test.js @@ -0,0 +1,754 @@ +var assert = require('chai').assert; +var sinon; +var withData = require('leche').withData; +var jsdom = require('mocha-jsdom'); + +var fs = require('fs'); +var vm = require('vm'); + +describe('yii.gridView', function () { + var yiiGridViewPath = 'framework/assets/yii.gridView.js'; + var yiiPath = 'framework/assets/yii.js'; + var jQueryPath = 'vendor/bower/jquery/dist/jquery.js'; + var $; + var $gridView; + var settings = { + filterUrl: '/posts/index', + filterSelector: '#w0-filters input, #w0-filters select' + }; + var commonSettings = { + filterUrl: '/posts/index', + filterSelector: '#w-common-filters input, #w-common-filters select' + }; + var $textInput; + var $select; + var $multipleSelect; + var $listBox; + var $checkAllCheckbox; + var $checkRowCheckboxes; + + function registerYii() { + var code = fs.readFileSync(yiiPath); + var script = new vm.Script(code); + var sandbox = {window: window, jQuery: $}; + var context = new vm.createContext(sandbox); + script.runInContext(context); + return sandbox.window.yii; + } + + function registerTestableCode() { + var yii = registerYii(); + var code = fs.readFileSync(yiiGridViewPath); + var script = new vm.Script(code); + var context = new vm.createContext({window: window, document: window.document, yii: yii}); + script.runInContext(context); + } + + var gridViewHtml = fs.readFileSync('tests/js/data/yii.gridView.html', 'utf-8'); + var html = '' + gridViewHtml + ''; + + jsdom({ + html: html, + src: fs.readFileSync(jQueryPath, 'utf-8') + }); + + before(function () { + $ = window.$; + registerTestableCode(); + sinon = require('sinon'); + }); + + beforeEach(function () { + $textInput = $('#w0-name'); + $select = $('#w0-category'); + $multipleSelect = $('#w0-tags'); + $listBox = $('#w2-tags'); + $checkAllCheckbox = $('#w0-check-all'); + $checkRowCheckboxes = $('.w0-check-row'); + }); + + afterEach(function () { + if ($gridView.length) { + $gridView.yiiGridView('destroy'); + } + $textInput.val(''); + $select.val(''); + $multipleSelect.find('option:selected').prop('selected', false); + $listBox.find('option:selected').prop('selected', false); + $checkAllCheckbox.prop('checked', false); + $checkRowCheckboxes.prop('checked', false); + }); + + /** + * Simulate pressing "Enter" button while focused on some element + * @param $el + */ + function pressEnter($el) { + var e = $.Event('keydown', {keyCode: 13}); + $el.trigger(e); + } + + /** + * Simulate pressing keyboard button while focused on the text input. For simplicity, intended to use with letter + * buttons, such as "a", "b", etc. Case insensitive. + * @param $el + * @param buttonName + */ + function pressButton($el, buttonName) { + $el.val(buttonName); + var keyCode = buttonName.charCodeAt(0); + var e = $.Event('keydown', {keyCode: keyCode}); + $el.trigger(e); + } + + /** + * Simulate changing value in the select + * @param $el + * @param value + */ + function changeValue($el, value) { + $el.val(value); + var e = $.Event('change'); + $el.trigger(e); + } + + /** + * Simulate losing focus of the element after the value was changed + * @param $el + */ + function loseFocus($el) { + var e = $.Event('change'); + $el.trigger(e); + } + + /** + * Simulate click in the checkbox + * @param $el + */ + function click($el) { + var e = $.Event('click'); + $el.trigger(e); + } + + /** + * Simulate hovering on the new value and pressing "Enter" button in the select + * @param $el + */ + function hoverAndPressEnter($el) { + pressEnter($el); + // After pressing enter while hovering the value will be immediately changed as well like with losing focus + loseFocus($el); + } + + describe('init', function () { + var customSettings = { + filterUrl: '/posts/filter', + filterSelector: '#w-common-filters input' + }; + + withData({ + 'no method specified': [function () { + $gridView = $('.grid-view').yiiGridView(commonSettings); + }, commonSettings], + 'no method specified, custom settings': [function () { + $gridView = $('.grid-view').yiiGridView(customSettings); + }, customSettings], + 'manual method call': [function () { + $gridView = $('.grid-view').yiiGridView('init', commonSettings); + }, commonSettings] + }, function (initFunction, expectedSettings) { + it('should save settings for all elements', function () { + initFunction(); + assert.deepEqual($('#w0').yiiGridView('data'), {settings: expectedSettings}); + assert.deepEqual($('#w1').yiiGridView('data'), {settings: expectedSettings}); + }); + }); + + describe('with repeated call', function () { + var jQuerySubmitStub; + + before(function () { + jQuerySubmitStub = sinon.stub($.fn, 'submit'); + }); + + after(function () { + jQuerySubmitStub.restore(); + }); + + it('should remove "filter" event handler', function () { + $gridView = $('#w0').yiiGridView(settings); + $gridView.yiiGridView(settings); + // Change selector to make sure event handlers are removed regardless of the selector + $gridView.yiiGridView({ + filterUrl: '/posts/index', + filterSelector: '#w0-filters select' + }); + + pressEnter($textInput); + assert.isFalse(jQuerySubmitStub.called); + + changeValue($select, 1); + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + }); + }); + + describe('applyFilter', function () { + var jQuerySubmit = function () { + }; + var jQuerySubmitStub; + + beforeEach(function () { + jQuerySubmitStub = sinon.stub($.fn, 'submit', jQuerySubmit); + }); + + afterEach(function () { + jQuerySubmitStub.restore(); + }); + + describe('with beforeFilter returning not false', function () { + var calledMethods = []; // For testing the order of called methods + var beforeFilterSpy; + var afterFilterSpy; + + before(function () { + jQuerySubmit = function () { + calledMethods.push('submit'); + + return this; + }; + beforeFilterSpy = sinon.spy(function () { + calledMethods.push('beforeFilter'); + }); + afterFilterSpy = sinon.spy(function () { + calledMethods.push('afterFilter'); + }); + }); + + after(function () { + jQuerySubmit = function () { + }; + beforeFilterSpy.reset(); + afterFilterSpy.reset(); + calledMethods = []; + }); + + var message = 'should send the request to correct url with correct parameters and apply events in ' + + 'correct order'; + it(message, function () { + $gridView = $('#w0').yiiGridView(settings) + .on('beforeFilter', beforeFilterSpy) + .on('afterFilter', afterFilterSpy); + + $textInput.val('a'); + $select.val(1); + $multipleSelect.find('option[value="1"]').prop('selected', true); + $multipleSelect.find('option[value="2"]').prop('selected', true); + + $gridView.yiiGridView('applyFilter'); + + var expectedHtml = '
' + + '' + + '' + + '' + + '' + + '
'; + var $form = $('.grid-view .gridview-filter-form'); + assert.equal($form.get(0).outerHTML, expectedHtml); + + assert.isTrue(beforeFilterSpy.calledOnce); + assert.instanceOf(beforeFilterSpy.getCall(0).args[0], $.Event); + assert.equal($(beforeFilterSpy.getCall(0).args[0].target).attr('id'), $gridView.attr('id')); + + assert.isTrue(jQuerySubmitStub.calledOnce); + assert.equal(jQuerySubmitStub.returnValues[0].attr('class'), 'gridview-filter-form'); + + assert.isTrue(afterFilterSpy.calledOnce); + assert.instanceOf(afterFilterSpy.getCall(0).args[0], $.Event); + assert.equal($(afterFilterSpy.getCall(0).args[0].target).attr('id'), $gridView.attr('id')); + + assert.deepEqual(calledMethods, ['beforeFilter', 'submit', 'afterFilter']); + }); + }); + + describe('with beforeFilter returning false', function () { + var beforeFilterSpy; + var afterFilterSpy; + + before(function () { + beforeFilterSpy = sinon.spy(function () { + return false; + }); + afterFilterSpy = sinon.spy(); + }); + + after(function () { + beforeFilterSpy.reset(); + afterFilterSpy.reset(); + }); + + it('should prevent from sending request and triggering "afterFilter" event', function () { + $gridView = $('#w0').yiiGridView(settings) + .on('beforeFilter', beforeFilterSpy) + .on('afterFilter', afterFilterSpy); + $gridView.yiiGridView('applyFilter'); + + assert.isTrue(beforeFilterSpy.calledOnce); + assert.isFalse(jQuerySubmitStub.called); + assert.isFalse(afterFilterSpy.called); + }); + }); + + describe('with different urls', function () { + describe('with no filter data sent', function () { + withData({ + 'query parameters': [ + '/posts/index?foo=1&bar=2', + '/posts/index', + 'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2' + ], + // https://github.com/yiisoft/yii2/pull/10302 + 'query parameter with multiple values (not array)': [ + '/posts/index?foo=1&foo=2', + '/posts/index', + 'PostSearch[name]=&PostSearch[category_id]=&foo=1&foo=2' + ], + 'query parameter with multiple values (array)': [ + '/posts/index?foo[]=1&foo[]=2', + '/posts/index', + 'PostSearch[name]=&PostSearch[category_id]=&foo[]=1&foo[]=2' + ], + // https://github.com/yiisoft/yii2/issues/12836 + 'anchor': [ + '/posts/index#post', + '/posts/index#post', + 'PostSearch[name]=&PostSearch[category_id]=' + ], + 'query parameters, anchor': [ + '/posts/index?foo=1&bar=2#post', + '/posts/index#post', + 'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2' + ], + 'relative url, query parameters': [ + '?foo=1&bar=2', + '', + 'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2' + ], + 'relative url, anchor': [ + '#post', + '#post', + 'PostSearch[name]=&PostSearch[category_id]=' + ], + 'relative url, query parameters, anchor': [ + '?foo=1&bar=2#post', + '#post', + 'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2' + ] + }, function (filterUrl, expectedUrl, expectedQueryString) { + it('should send the request to correct url with correct parameters', function () { + var customSettings = $.extend({}, settings, {filterUrl: filterUrl}); + $gridView = $('#w0').yiiGridView(customSettings); + $gridView.yiiGridView('applyFilter'); + + var $form = $gridView.find('.gridview-filter-form'); + assert.isTrue(jQuerySubmitStub.calledOnce); + assert.equal($form.attr('action'), expectedUrl); + assert.equal(decodeURIComponent($form.serialize()), expectedQueryString); + }); + }); + }); + + // https://github.com/yiisoft/yii2/pull/10302 + + describe('with filter data sent', function () { + it('should send the request to correct url with new parameter values', function () { + var filterUrl = '/posts/index?CategorySearch[id]=5&CategorySearch[name]=c' + + '&PostSearch[name]=a&PostSearch[category_id]=1&PostSearch[tags][]=1&PostSearch[tags][]=2' + + '&foo[]=1&foo[]=2&bar=1#post'; + var customSettings = $.extend({}, settings, {filterUrl: filterUrl}); + $gridView = $('#w0').yiiGridView(customSettings); + + $textInput.val('b'); + $select.val('1'); // Leave value as is (simulate setting "selected" in HTML) + $multipleSelect.find('option[value="2"]').prop('selected', true); + $multipleSelect.find('option[value="3"]').prop('selected', true); + + $gridView.yiiGridView('applyFilter'); + + var $form = $gridView.find('.gridview-filter-form'); + assert.isTrue(jQuerySubmitStub.calledOnce); + assert.equal($form.attr('action'), '/posts/index#post'); + // Parameters not related with current filter are appended to the end + var expectedQueryString = 'PostSearch[name]=b&PostSearch[category_id]=1' + + '&PostSearch[tags][]=2&PostSearch[tags][]=3' + + '&CategorySearch[id]=5&CategorySearch[name]=c' + + '&foo[]=1&foo[]=2&bar=1'; + assert.equal(decodeURIComponent($form.serialize()), expectedQueryString); + }); + }); + }); + + // https://github.com/yiisoft/yii2/pull/10284 + + describe('with list box', function () { + var queryString = 'PostSearch[name]=&PostSearch[tags]=-1&PostSearch[tags][]=1&PostSearch[tags][]=2'; + + beforeEach(function () { + $listBox.find('option[value="1"]').prop('selected', true); + $listBox.find('option[value="2"]').prop('selected', true); + }); + + describe('with values selected', function () { + it('should send the request to correct url with correct parameters', function () { + $gridView = $('#w2').yiiGridView({ + filterUrl: '/posts/index', + filterSelector: '#w2-filters input, #w2-filters select' + }); + $gridView.yiiGridView('applyFilter'); + + var $form = $gridView.find('.gridview-filter-form'); + assert.equal($form.attr('action'), '/posts/index'); + assert.equal(decodeURIComponent($form.serialize()), queryString); + }); + }); + + describe('with unselected values after applied filter', function () { + it('should send the request to correct url with correct parameters', function () { + $gridView = $('#w2').yiiGridView({ + filterUrl: '/posts/index/?' + queryString, + filterSelector: '#w2-filters input, #w2-filters select' + }); + $listBox.find('option:selected').prop('selected', false); + $gridView.yiiGridView('applyFilter'); + + var $form = $gridView.find('.gridview-filter-form'); + assert.equal($form.attr('action'), '/posts/index/'); + assert.equal(decodeURIComponent($form.serialize()), 'PostSearch[name]=&PostSearch[tags]=-1'); + }); + }); + }); + + describe('with repeated method call', function () { + it('should delete the hidden form', function () { + $gridView = $('#w0').yiiGridView(settings); + $gridView.yiiGridView('applyFilter'); + $gridView.yiiGridView('applyFilter'); + + var $form = $gridView.find('.gridview-filter-form'); + assert.lengthOf($form, 1); + }); + }); + + describe('with filter event handlers', function () { + beforeEach(function () { + $gridView = $('#w0').yiiGridView(settings); + }); + + describe('with text entered in the text input', function () { + it('should not submit form', function () { + pressButton($textInput, 'a'); + assert.isFalse(jQuerySubmitStub.called); + }); + }); + + describe('with "Enter" pressed in the text input', function () { + it('should submit form once', function () { + pressEnter($textInput); + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + }); + + describe('with text entered in the text input and lost focus', function () { + it('should submit form once', function () { + pressButton($textInput, 'a'); + loseFocus($textInput); + + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + }); + + describe('with value changed in the select', function () { + it('should submit form once', function () { + changeValue($select, 1); + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + }); + + describe('with hover on different value and "Enter" pressed in select', function () { + it('should submit form once', function () { + // Simulate hovering on new value and pressing "Enter" + $select.val(1); + hoverAndPressEnter($select); + + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + }); + }); + }); + + describe('setSelectionColumn method', function () { + describe('with name option and', function () { + withData({ + 'nothing else': [{}], + 'checkAll option': [{checkAll: 'selection_all'}], + 'multiple option set to true': [{multiple: true}], + 'multiple and checkAll options, multiple set to false': [{multiple: false, checkAll: 'selection_all'}] + }, function (customOptions) { + it('should update data and do not activate "check all" functionality', function () { + $gridView = $('#w0').yiiGridView(settings); + + var defaultOptions = {name: 'selection[]'}; + var options = $.extend({}, defaultOptions, customOptions); + $gridView.yiiGridView('setSelectionColumn', options); + + assert.equal($gridView.yiiGridView('data').selectionColumn, 'selection[]'); + + click($checkAllCheckbox); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0); + + click($checkAllCheckbox); // Back to initial condition + click($checkRowCheckboxes); + assert.isFalse($checkAllCheckbox.prop('checked')); + }); + }); + }); + + describe('with name, multiple and checkAll options, multiple set to true and', function () { + withData({ + 'nothing else': [{}], + // https://github.com/yiisoft/yii2/pull/11729 + 'class option': [{'class': 'w0-check-row'}] + }, function (customOptions) { + it('should update data and "check all" functionality should work', function () { + $gridView = $('#w0').yiiGridView(settings); + + var defaultOptions = {name: 'selection[]', multiple: true, checkAll: 'selection_all'}; + var options = $.extend({}, defaultOptions, customOptions); + $gridView.yiiGridView('setSelectionColumn', options); + + assert.equal($gridView.yiiGridView('data').selectionColumn, 'selection[]'); + + var $checkFirstRowCheckbox = $checkRowCheckboxes.filter('[value="1"]'); + + // Check all + click($checkAllCheckbox); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3); + assert.isTrue($checkAllCheckbox.prop('checked')); + + // Uncheck all + click($checkAllCheckbox); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0); + assert.isFalse($checkAllCheckbox.prop('checked')); + + // Check all manually + click($checkRowCheckboxes); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3); + assert.isTrue($checkAllCheckbox.prop('checked')); + + // Uncheck all manually + click($checkRowCheckboxes); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0); + assert.isFalse($checkAllCheckbox.prop('checked')); + + // Check first row + click($checkFirstRowCheckbox); + assert.isTrue($checkFirstRowCheckbox.prop('checked')); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 1); + assert.isFalse($checkAllCheckbox.prop('checked')); + + // Then check all + click($checkAllCheckbox); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3); + assert.isTrue($checkAllCheckbox.prop('checked')); + + // Uncheck first row + click($checkFirstRowCheckbox); + assert.isFalse($checkFirstRowCheckbox.prop('checked')); + assert.lengthOf($checkRowCheckboxes.filter(':checked'), 2); + assert.isFalse($checkAllCheckbox.prop('checked')); + }); + }); + }); + + describe('with repeated calls', function () { + var jQueryPropStub; + + before(function () { + jQueryPropStub = sinon.stub($, 'prop'); + }); + + after(function () { + jQueryPropStub.restore(); + }); + + it('should not duplicate event handler calls', function () { + $gridView = $('#w3').yiiGridView({ + filterUrl: '/posts/index', + filterSelector: '#w3-filters input, #w3-filters select' + }); + + $gridView.yiiGridView('setSelectionColumn', { + name: 'selection[]', + multiple: true, + checkAll: 'selection_all' + }); + // Change selectors to make sure event handlers are removed regardless of the selector + $gridView.yiiGridView('setSelectionColumn', { + name: 'selection2[]', + multiple: true, + checkAll: 'selection_all2' + }); + $gridView.yiiGridView('setSelectionColumn', { + name: 'selection[]', + multiple: true, + checkAll: 'selection_all' + }); + $gridView.yiiGridView('setSelectionColumn', { + 'class': 'w3-check-row', + multiple: true, + checkAll: 'selection_all' + }); + + // Check first row ("prop" should be called once) + click($gridView.find('input[name="selection[]"][value="1"]')); + // Check all rows ("prop" should be called 2 times, 1 time for each row) + click($gridView.find('input[name="selection_all"]')); + + assert.equal(jQueryPropStub.callCount, 3); + }); + }); + }); + + describe('getSelectedRows method', function () { + withData({ + 'selectionColumn not set, no rows selected': [undefined, [], false, []], + 'selectionColumn not set, 1st and 2nd rows selected': [undefined, [1, 2], false, []], + 'selectionColumn set, no rows selected': ['selection[]', [], false, []], + 'selectionColumn set, 1st row selected': ['selection[]', [1], false, [1]], + 'selectionColumn set, 1st and 2nd rows selected': ['selection[]', [1, 2], false, [1, 2]], + 'selectionColumn set, all rows selected, "Check all" checkbox checked': [ + 'selection[]', [1, 2, 3], true, [1, 2, 3] + ] + }, function (selectionColumn, selectedRows, checkAll, expectedSelectedRows) { + it('should return array with ids of selected rows', function () { + $gridView = $('#w0').yiiGridView(settings); + $gridView.yiiGridView('setSelectionColumn', {name: selectionColumn}); + for (var i = 0; i < selectedRows.length; i++) { + $checkRowCheckboxes.filter('[value="' + selectedRows[i] + '"]').prop('checked', true); + } + if (checkAll) { + $checkAllCheckbox.prop('checked', true); + } + assert.deepEqual($gridView.yiiGridView('getSelectedRows'), expectedSelectedRows); + }); + }); + }); + + describe('destroy method', function () { + var jQuerySubmitStub; + var jQueryPropStub; + var beforeFilterSpy; + var afterFilterSpy; + + beforeEach(function () { + jQuerySubmitStub = sinon.stub($.fn, 'submit'); + jQueryPropStub = sinon.stub($, 'prop'); + beforeFilterSpy = sinon.spy(); + afterFilterSpy = sinon.spy(); + }); + + afterEach(function () { + jQuerySubmitStub.restore(); + jQueryPropStub.restore(); + beforeFilterSpy.reset(); + afterFilterSpy.reset(); + }); + + it('should remove saved settings for destroyed element only and return initial jQuery object', function () { + $gridView = $('.grid-view').yiiGridView(commonSettings); + var $gridView1 = $('#w0'); + var $gridView2 = $('#w1'); + var destroyResult = $gridView1.yiiGridView('destroy'); + + assert.strictEqual(destroyResult, $gridView1); + assert.isUndefined($gridView1.yiiGridView('data')); + assert.deepEqual($gridView2.yiiGridView('data'), {settings: commonSettings}); + }); + + it('should remove "beforeFilter" and "afterFilter" event handlers for destroyed element only', function () { + $gridView = $('.grid-view').yiiGridView(commonSettings) + .on('beforeFilter', beforeFilterSpy) + .on('afterFilter', afterFilterSpy); + var $gridView1 = $('#w0'); + var $gridView2 = $('#w1'); + $gridView1.yiiGridView('destroy'); + + assert.throws(function () { + $gridView1.yiiGridView('applyFilter'); + }, "Cannot read property 'settings' of undefined"); + $gridView1.yiiGridView(settings); // Reinitialize without "beforeFilter" and "afterFilter" event handlers + + $gridView1.yiiGridView('applyFilter'); + assert.isTrue(jQuerySubmitStub.calledOnce); + assert.isFalse(beforeFilterSpy.called); + assert.isFalse(afterFilterSpy.called); + + $gridView2.yiiGridView('applyFilter'); + assert.isTrue(jQuerySubmitStub.calledTwice); + assert.isTrue(beforeFilterSpy.calledOnce); + assert.isTrue(afterFilterSpy.calledOnce); + }); + + it('should remove "filter" event handler for destroyed element only', function () { + var $gridView1 = $('#w0'); + var $gridView2 = $('#w1'); + $gridView1.yiiGridView(settings); + $gridView2.yiiGridView({ + filterUrl: '/posts/index', + filterSelector: '#w1-filters input, #w1-filters select' + }); + $gridView2.yiiGridView('destroy'); + + pressEnter($gridView2.find('input[name="PostSearch[id]"]')); + assert.isFalse(jQuerySubmitStub.called); + + pressEnter($textInput); + assert.isTrue(jQuerySubmitStub.calledOnce); + }); + + it('should remove "checkRow" and "checkAllRows" filter event handlers for destroyed element only', function () { + $gridView = $('.grid-view').yiiGridView(commonSettings); + var options = {name: 'selection[]', multiple: true, checkAll: 'selection_all'}; + var $gridView1 = $('#w0'); + var $gridView2 = $('#w1'); + $gridView1.yiiGridView('setSelectionColumn', options); + $gridView2.yiiGridView('setSelectionColumn', options); + $gridView2.yiiGridView('destroy'); + + click($gridView2.find('input[name="selection_all"]')); + click($gridView2.find('input[name="selection[]"][value="1"]')); + assert.equal(jQueryPropStub.callCount, 0); + + click($checkRowCheckboxes.filter('[value="1"]')); // Check first row ("prop" should be called once) + click($checkAllCheckbox); // Check all rows ("prop" should be called 3 times, 1 time for each row) + assert.equal(jQueryPropStub.callCount, 4); + }); + }); + + describe('data method', function () { + it('should return saved settings', function () { + $gridView = $('#w0').yiiGridView(settings); + assert.deepEqual($gridView.yiiGridView('data'), {settings: settings}); + }); + }); + + describe('call of not existing method', function () { + it('should throw according error', function () { + $gridView = $('#w0').yiiGridView(settings); + assert.throws(function () { + $gridView.yiiGridView('foobar'); + }, 'Method foobar does not exist in jQuery.yiiGridView'); + }); + }); +});