').appendTo(this.element);
+ assert.ok($testElement.parent().hasClass('dx-button'), 'test element is in button');
+ this.instance.option('text', 'new test button text');
+ assert.ok($testElement.parent().hasClass('dx-button'), 'test element is still in button');
+ });
+});
+
+QUnit.module('regressions', {
+ beforeEach: function() {
+ this.element = $('#button').Button();
+ this.instance = this.element.Button('instance');
+ }
+}, () => {
+ QUnit.test('B230602', function(assert) {
+ this.instance.option('icon', '1.png');
+ assert.equal(this.element.find('img').length, 1);
+
+ this.instance.option('icon', '2.png');
+ assert.equal(this.element.find('img').length, 1);
+ });
+
+ QUnit.test('Q513961', function(assert) {
+ this.instance.option({ text: '123', 'icon': 'home' });
+ assert.equal(this.element.find('.dx-icon-home').index(), 0);
+ });
+
+ QUnit.test('B238735: Button holds the shape of an arrow after you change it\'s type from back to any other', function(assert) {
+ this.instance.option('type', 'back');
+ assert.equal(this.element.hasClass(BUTTON_BACK_CLASS), true, 'back button css class removed');
+
+ this.instance.option('type', 'normal');
+ assert.equal(this.element.hasClass(BUTTON_BACK_CLASS), false, 'back button css class removed');
+ });
+});
+
+QUnit.module('contentReady', {}, () => {
+ QUnit.test('T355000 - the \'onContentReady\' action should be fired after widget is rendered entirely', function(assert) {
+ const done = assert.async();
+ const buttonConfig = {
+ text: 'Test button',
+ icon: 'trash'
+ };
+
+ const areElementsEqual = (first, second) => {
+ if(first.length !== second.length) {
+ return false;
+ }
+
+ if(first.length === 0) {
+ return true;
+ }
+
+ if(first.text() !== second.text()) {
+ return false;
+ }
+
+ if(first.attr('class') !== second.attr('class')) {
+ return false;
+ }
+
+ const firstChildren = first.children();
+ const secondChildren = second.children();
+
+ for(let i = 0, n = first.length; i < n; i++) {
+ if(!areElementsEqual(firstChildren.eq(i), secondChildren.eq(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+
+ const $firstButton = $('#widget').Button(buttonConfig);
+
+ $('#button').Button($.extend({}, buttonConfig, {
+ onContentReady(e) {
+ assert.ok(areElementsEqual($firstButton, $(e.element)), 'rendered widget and widget with fired action are equals');
+ done();
+ }
+ }));
+ });
+});
+
+QUnit.module('inkRipple', {}, () => {
+ QUnit.test('inkRipple should be removed when widget is removed', function(assert) {
+ const $element = $('#inkButton');
+
+ $element.Button({
+ useInkRipple: true,
+ });
+ $element.Button('instance').option('onClick', (e) => {
+ const $element = $(e.component.$element());
+ $element.triggerHandler({ type: 'dxremove' });
+ $element.trigger('dxinactive');
+ assert.ok(true, 'no exceptions');
+ });
+
+ $element.trigger('dxclick');
+ });
+
+ QUnit.test('widget should works correctly when the useInkRipple option is changed at runtime', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const $inkButton = $('#inkButton').Button({
+ text: 'test',
+ useInkRipple: true
+ });
+ const inkButton = $inkButton.Button('instance');
+ const pointer = pointerMock($inkButton);
+
+ pointer.start('touch').down();
+ clock.tick();
+ pointer.start('touch').up();
+ assert.strictEqual($inkButton.find(`.${INK_RIPPLE_CLASS}`).length, 1, 'inkRipple element was rendered');
+
+ inkButton.option('useInkRipple', false);
+ assert.strictEqual($inkButton.find(`.${INK_RIPPLE_CLASS}`).length, 0, 'inkRipple element was removed');
+
+ pointer.start('touch').down();
+ clock.tick();
+ pointer.start('touch').up();
+ assert.strictEqual($inkButton.find(`.${INK_RIPPLE_CLASS}`).length, 0, 'inkRipple element was removed is still removed after click');
+
+ inkButton.option('useInkRipple', true);
+ pointer.start('touch').down();
+ clock.tick();
+ pointer.start('touch').up();
+ assert.strictEqual($inkButton.find(`.${INK_RIPPLE_CLASS}`).length, 1, 'inkRipple element was rendered');
+
+ clock.restore();
+ });
+});
+
+QUnit.module('widget sizing render', {}, () => {
+ QUnit.test('default', function(assert) {
+ const $element = $('#widget').Button({ text: 'ahoy!' });
+
+ assert.ok($element.outerWidth() > 0, 'outer width of the element must be more than zero');
+ });
+
+ QUnit.test('constructor', function(assert) {
+ const $element = $('#widget').Button({ text: 'ahoy!', width: 400 });
+ const instance = $element.Button('instance');
+
+ assert.strictEqual(instance.option('width'), 400);
+ assert.strictEqual($element.outerWidth(), 400, 'outer width of the element must be equal to custom width');
+ });
+
+ QUnit.test('root with custom width', function(assert) {
+ const $element = $('#widthRootStyle').Button({ text: 'ahoy!' });
+ const instance = $element.Button('instance');
+
+ assert.strictEqual(instance.option('width'), undefined);
+ assert.strictEqual($element.outerWidth(), 300, 'outer width of the element must be equal to custom width');
+ });
+
+ QUnit.test('change width', function(assert) {
+ const $element = $('#widget').Button({ text: 'ahoy!' });
+ const instance = $element.Button('instance');
+ const customWidth = 400;
+
+ instance.option('width', customWidth);
+
+ assert.strictEqual($element.outerWidth(), customWidth, 'outer width of the element must be equal to custom width');
+ });
+});
+
+QUnit.module('keyboard navigation', {}, () => {
+ QUnit.test('click fires on enter', function(assert) {
+ assert.expect(2);
+
+ let clickFired = 0;
+
+ const $element = $('#button').Button();
+
+ // NOTE: initialize onClick in constructor doesn't trigger events correctly (dxclick, focusin, etc)
+ $element.Button('instance').option('onClick', () => clickFired++);
+
+ const keyboard = keyboardMock($element);
+
+ $element.trigger('focusin');
+ keyboard.keyDown('enter');
+ assert.equal(clickFired, 1, 'press enter on button call click action');
+
+ keyboard.keyDown('space');
+ assert.equal(clickFired, 2, 'press space on button call click action');
+ });
+
+ QUnit.test('arguments on key press', function(assert) {
+ const clickHandler = sinon.spy();
+
+ const $element = $('#button').Button();
+
+ // NOTE: initialize onClick in constructor doesn't trigger events correctly (dxclick, focusin, etc)
+ $element.Button('instance').option('onClick', clickHandler);
+
+ const keyboard = keyboardMock($element);
+
+ $element.trigger('focusin');
+ keyboard.keyDown('enter');
+
+ assert.ok(clickHandler.calledOnce, 'Handler should be called');
+
+ const params = clickHandler.getCall(0).args[0];
+ assert.ok(params, 'Event params should be passed');
+ assert.ok(params.event, 'Event should be passed');
+ assert.ok(params.validationGroup, 'validationGroup should be passed');
+ });
+});
+
+QUnit.module('submit behavior', {
+ beforeEach: function() {
+ this.clock = sinon.useFakeTimers();
+ this.$element = $('#button').Button({ useSubmitBehavior: true });
+ this.$form = $('#form');
+ this.clickButton = function() {
+ this.$element.trigger('dxclick');
+ this.clock.tick();
+ };
+ },
+ afterEach: function() {
+ this.clock.restore();
+ }
+}, () => {
+ QUnit.test('render input with submit type', function(assert) {
+ assert.strictEqual(this.$element.find('input[type=submit]').length, 1);
+ });
+
+ QUnit.test('submit input has .dx-button-submit-input CSS class', function(assert) {
+ assert.strictEqual(this.$element.find(`.${BUTTON_SUBMIT_INPUT_CLASS}`).length, 1);
+ });
+
+ QUnit.test('button click call click() on submit input', function(assert) {
+ const clickHandlerSpy = sinon.spy();
+ // NOTE: workaround to synchronize test
+ const $element = this.$element.Button({ validationGroup: '' });
+
+ $element
+ .find('.' + BUTTON_SUBMIT_INPUT_CLASS)
+ .on('click', clickHandlerSpy);
+
+ this.clickButton();
+
+ assert.ok(clickHandlerSpy.calledOnce);
+ });
+
+ QUnit.test('widget should work correctly if useSubmitBehavior was changed runtime', function(assert) {
+ const instance = this.$element.Button('instance');
+
+ instance.option('useSubmitBehavior', false);
+ assert.strictEqual(this.$element.find('input[type=submit]').length, 0, 'no submit input if useSubmitBehavior is false');
+ assert.strictEqual(this.$element.find(`.${BUTTON_SUBMIT_INPUT_CLASS}`).length, 0, 'no submit class if useSubmitBehavior is false');
+
+ instance.option('useSubmitBehavior', true);
+ assert.strictEqual(this.$element.find('input[type=submit]').length, 1, 'has submit input if useSubmitBehavior is false');
+ assert.strictEqual(this.$element.find(`.${BUTTON_SUBMIT_INPUT_CLASS}`).length, 1, 'has submit class if useSubmitBehavior is false');
+ });
+
+ QUnit.test('preventDefault is called to dismiss submit of form if validation failed', function(assert) {
+ assert.expect(2);
+ try {
+ const validatorStub = sinon.createStubInstance(Validator);
+
+ const clickHandlerSpy = sinon.spy(e => {
+ assert.ok(e.isDefaultPrevented(), 'default is prevented');
+ });
+
+ const $element = this.$element.Button({ validationGroup: 'testGroup' });
+
+ validatorStub.validate = () => {
+ return { isValid: false };
+ };
+
+ ValidationEngine.registerValidatorInGroup('testGroup', validatorStub);
+
+ $element
+ .find('.' + BUTTON_SUBMIT_INPUT_CLASS)
+ .on('click', clickHandlerSpy);
+
+ this.clickButton();
+
+ assert.ok(clickHandlerSpy.called);
+ } finally {
+ ValidationEngine.initGroups();
+ }
+ });
+
+ QUnit.test('button onClick event handler should raise once (T443747)', function(assert) {
+ const clickHandlerSpy = sinon.spy();
+ this.$element.Button({ onClick: clickHandlerSpy });
+ this.clickButton();
+ assert.ok(clickHandlerSpy.calledOnce);
+ });
+
+ QUnit.test('Submit button should not be enabled on pending', function(assert) {
+ try {
+ const validator = new Validator(document.createElement('div'), {
+ adapter: sinon.createStubInstance(DefaultAdapter),
+ validationRules: [{
+ type: 'async',
+ validationCallback: function() {
+ const d = new Deferred();
+ return d.promise();
+ }
+ }]
+ });
+ const clickHandlerSpy = sinon.spy(e => {
+ assert.ok(e.isDefaultPrevented(), 'default is prevented');
+ });
+ const $element = this.$element.Button({ validationGroup: 'testGroup' });
+ const buttonInstance = this.$element.Button('instance');
+
+
+ ValidationEngine.registerValidatorInGroup('testGroup', validator);
+
+ $element
+ .find('.' + BUTTON_SUBMIT_INPUT_CLASS)
+ .on('click', clickHandlerSpy);
+
+ this.clickButton();
+
+ assert.ok(clickHandlerSpy.called);
+ assert.ok(buttonInstance.option('disabled'), 'button is disabled after the click');
+ } finally {
+ ValidationEngine.initGroups();
+ }
+ });
+});
+
+QUnit.module('templates', () => {
+ checkStyleHelper.testInChromeOnDesktopActiveWindow('parent styles when button is not focused', function(assert) {
+ const $template = $('
').text('test1');
+ $('#button').Button({
+ template: function() { return $template; }
+ });
+ $('#input1').focus();
+
+ assert.equal(checkStyleHelper.getColor($template[0]), 'rgb(51, 51, 51)', 'color');
+ assert.equal(checkStyleHelper.getBackgroundColor($template[0]), 'rgb(255, 255, 255)', 'backgroundColor');
+ assert.equal(checkStyleHelper.getOverflowX($template[0].parentNode), 'visible', 'overflowX');
+ assert.equal(checkStyleHelper.getTextOverflow($template[0].parentNode), 'clip', 'textOverflow');
+ assert.equal(checkStyleHelper.getWhiteSpace($template[0].parentNode), 'normal', 'whiteSpace');
+ });
+});
+
+QUnit.module('events subscriptions', () => {
+ QUnit.test('click', function(assert) {
+ const done = assert.async();
+ const clickHandler = sinon.spy();
+ const $button = $('#button').Button({
+ text: 'test'
+ });
+ const button = $button.Button('instance');
+
+ button.on('click', clickHandler);
+
+ setTimeout(() => {
+ $button.trigger('dxclick');
+
+ setTimeout(() => {
+ assert.ok(clickHandler.calledOnce, 'Handler should be called');
+ const params = clickHandler.getCall(0).args[0];
+ assert.ok(params, 'Event params should be passed');
+ assert.ok(params.event, 'Event should be passed');
+ assert.ok(params.validationGroup, 'validationGroup should be passed');
+
+ done();
+ }, 100);
+ }, 100);
+ });
+
+ QUnit.test('contentReady', function(assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const button = $('#button').Button({
+ text: 'test'
+ }).Button('instance');
+
+ // NOTE: now we shouldn't call repaint, because we call onContentReady async
+ button.on('contentReady', (e) => {
+ assert.ok(e.component, 'Component info should be passed');
+ assert.ok(e.element, 'Element info should be passed');
+ assert.strictEqual($(e.element).text(), 'test', 'Text is rendered to the element');
+ done();
+ });
+ });
+});
diff --git a/testing/tests/Renovation/widget.tests.js b/testing/tests/Renovation/widget.tests.js
new file mode 100644
index 000000000000..51809b81d163
--- /dev/null
+++ b/testing/tests/Renovation/widget.tests.js
@@ -0,0 +1,127 @@
+import $ from 'jquery';
+import 'renovation/widget.j';
+
+QUnit.testStart(function() {
+ $('#qunit-fixture').html(`
+
+
+ `);
+});
+
+const config = {
+ beforeEach: function(module) {
+ // it needs for Preact timers https://github.com/preactjs/preact/blob/master/hooks/src/index.js#L273
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach: function() {
+ this.clock.tick(100);
+ this.clock.restore();
+ }
+};
+
+QUnit.module('Props: width/height', config);
+
+QUnit.test('should overwrite predefined dimensions', function(assert) {
+ const $element = $('#component');
+ const style = $element.get(0).style;
+
+ $element.css({ width: '20px', height: '30px' });
+ assert.strictEqual(style.width, '20px');
+ assert.strictEqual(style.height, '30px');
+
+ $element.Widget({ width: void 0, height: void 0 });
+ // assert.strictEqual(style.width, '20px');
+ // assert.strictEqual(style.height, '30px');
+
+ $element.css({ width: '20px', height: '30px' });
+ assert.strictEqual(style.width, '20px');
+ assert.strictEqual(style.height, '30px');
+
+ $element.Widget({ width: null, height: null });
+ // assert.strictEqual(style.width, '');
+ // assert.strictEqual(style.height, '');
+
+ $element.css({ width: '20px', height: '30px' });
+ assert.strictEqual(style.width, '20px');
+ assert.strictEqual(style.height, '30px');
+
+ $element.Widget({ width: '', height: '' });
+ assert.strictEqual(style.width, '');
+ assert.strictEqual(style.height, '');
+});
+
+QUnit.module('Props: accessKey');
+
+QUnit.test('should change "accesskey" attribute', function(assert) {
+ const $widget = $('#component').Widget({
+ focusStateEnabled: true,
+ accessKey: 'y'
+ });
+ const instance = $widget.Widget('instance');
+
+ instance.option('accessKey', 'g');
+ assert.strictEqual($widget.attr('accesskey'), 'g');
+});
+
+QUnit.module('Container', config);
+
+QUnit.test('should not remove attributes from container after render', function(assert) {
+ const $container = $('#component').attr({
+ 'custom-attr': 'v1',
+ 'class': 'my-widget-class'
+ });
+ const widget = $container.Widget({}).Widget('instance');
+
+ assert.strictEqual(widget.$element().attr('id'), 'component');
+ assert.strictEqual(widget.$element().attr('custom-attr'), 'v1');
+ assert.ok($container.hasClass('my-widget-class'));
+ assert.deepEqual(widget.option.elementAttr, undefined);
+});
+
+QUnit.test('should rewrite container attributes after render', function(assert) {
+ const $container = $('#component').attr({ 'custom-attr': 'v1' });
+ const widget = $container.Widget({
+ elementAttr: { 'custom-attr': 'v2' }
+ }).Widget('instance');
+
+ assert.strictEqual(widget.$element().attr('custom-attr'), 'v2');
+ assert.deepEqual(widget.option().elementAttr, { 'custom-attr': 'v2' });
+});
+
+QUnit.test('should save attributes after rerender', function(assert) {
+ const widget = $('#component').Widget({
+ elementAttr: { 'custom-attr': 'v2' }
+ }).Widget('instance');
+
+ // NOTE: force rerender
+ widget.option('elementAttr', { 'a': 'v' });
+
+ assert.strictEqual(widget.$element().attr('id'), 'component');
+});
+
+QUnit.test('should not recreate container element', function(assert) {
+ const $container = $('#component');
+ const container = $container.get(0);
+ const widget = $container.Widget({}).Widget('instance');
+
+ assert.strictEqual(widget.$element().get(0), container);
+});
+
+QUnit.test('should not recreate container element after rerender', function(assert) {
+ const $container = $('#component');
+ const container = $container.get(0);
+ const widget = $container.Widget({}).Widget('instance');
+
+ // NOTE: force rerender
+ widget.option('elementAttr', { 'a': 'v' });
+
+ assert.strictEqual(widget.$element().get(0), container);
+});
+
+QUnit.module('Preact Wrapper', config);
+
+QUnit.test('should create in separate element', function(assert) {
+ $('
').Widget({});
+
+ assert.ok(true, 'no exceptions');
+});
diff --git a/themebuilder-scss/tests/data/scss/widgets/generic/_colors.scss b/themebuilder-scss/tests/data/scss/widgets/generic/_colors.scss
index abdbc0f2b370..c3e4e15861c7 100644
--- a/themebuilder-scss/tests/data/scss/widgets/generic/_colors.scss
+++ b/themebuilder-scss/tests/data/scss/widgets/generic/_colors.scss
@@ -1,7 +1,6 @@
@forward "tb/widgets/generic/colors";
@use "tb/widgets/generic/colors" as *;
@use "sass:color";
-
$color: null !default;
$base-font-family: null !default;
diff --git a/themebuilder-scss/tests/metadata/generator.test.ts b/themebuilder-scss/tests/metadata/generator.test.ts
index b57812db2daf..4fcda67ae354 100644
--- a/themebuilder-scss/tests/metadata/generator.test.ts
+++ b/themebuilder-scss/tests/metadata/generator.test.ts
@@ -61,8 +61,8 @@ $slideout-background: $base-color;`,
}, {
'spaces after comments':
`/**
-* $name Slide out background
-* $type color
+* $name Slide out background
+* $type color
*/
$slideout-background: #000;`,
}];
@@ -103,8 +103,8 @@ $base-color: rgb(0,170,0);
$slideout-background2: $base-color;
/**
-* $name Slide out background
-* $type color
+* $name Slide out background
+* $type color
*/
$slideout-background3: #000;`;
diff --git a/themebuilder/.eslintrc b/themebuilder/.eslintrc
new file mode 100644
index 000000000000..1c7ccb3d6aab
--- /dev/null
+++ b/themebuilder/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "env": {
+ "node": false
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000000..803b20f05fb7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,47 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "es6",
+ "target": "es5",
+ "jsx": "preserve",
+ "declaration": false,
+ "strict": true,
+ "alwaysStrict": true,
+ "noImplicitAny": false,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "strictNullChecks": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "noUnusedLocals": true,
+ "moduleResolution": "node",
+ "noLib": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "downlevelIteration": true,
+ "sourceMap": false,
+ "esModuleInterop": true,
+ "lib": [
+ "es6",
+ "es7",
+ "es2017.object",
+ "dom"
+ ],
+ "baseUrl": "./",
+ "stripInternal": true,
+ "paths": {
+ "*": [
+ "node_modules"
+ ]
+ },
+ "outDir": "dist"
+ },
+ "include": [
+ "./js/renovation",
+ ],
+ "exclude": [
+ "artifacts",
+ "node_modules",
+ "**/*.tests.ts*",
+ "**/dist"
+ ]
+}
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index 9a848b4958ac..cf30267f0714 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -7,11 +7,23 @@ module.exports = Object.assign({
module: {
rules: [{
test: /\.js$/,
- exclude: /(node_modules|bower_components)/,
+ exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
+ {
+ test: /\.jsx$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ 'plugins': [
+ ['transform-react-jsx', { 'pragma': 'Preact.h' }]
+ ]
+ }
+ }
+ },
{
test: /version\.js$/,
loader: 'string-replace-loader',
@@ -20,5 +32,8 @@ module.exports = Object.assign({
replace: require('./package.json').version,
}
}]
+ },
+ resolve: {
+ extensions: ['.js', '.jsx'],
}
}, baseConfig);