Upgrade Angular to 1.6.5 (#13543)

* [angular/$http] remove use of .success() and .error() callbacks

* [angular/$route] remove `!` hash prefix

* [angular] upgrade

* [angular/$timeout] prevent unhandled exception "canceled" logging

* [ui/fancy_form] refactor FormController for compatibility

* [ngModelController] ensure method calls keep context

* [ui/queryBar/tests] attach $elem to DOM so "click" triggers "submit"

* [confirmModalPromise] fix test for rejected promise

* [watchMulti] specifically check watchers array for length

* [typeahead] check for property rather than own keys

* [ui/compat] add initAfterBindingsWorkaround

* [ui/fancyForms] fix _getInvalidModels()

* [fancyForm] add tests that check nested form error counting

* [ui/fancyForms] ensure that submit is blocked properly

* [ui/fancyForms] escalate soft errors on failed submit

* [ui/fancyForms] bind handlers to this in constructor

* [uiBootstrap/tooltip] describe the new error handling

* [ui/confirmModalPromise] use more sinon assertions

* [$http] resp => data before old .success() and .error() handlers

* [indices/createWizard] apply callAfterBindings workarounds

(cherry picked from commit 2eae80cd0c)
This commit is contained in:
Spencer 2017-08-25 14:50:11 -07:00 committed by spalger
parent ba05ad56b0
commit 74da002ee5
26 changed files with 442 additions and 148 deletions

View file

@ -78,7 +78,7 @@
"@elastic/webpack-directory-name-as-main": "2.0.2",
"JSONStream": "1.1.1",
"accept-language-parser": "1.2.0",
"angular": "1.4.7",
"angular": "1.6.5",
"angular-bootstrap-colorpicker": "3.0.19",
"angular-elastic": "2.5.0",
"angular-route": "1.4.7",

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import { callAfterBindingsWorkaround } from 'ui/compat';
import { uiModules } from 'ui/modules';
import contextAppTemplate from './app.html';
import './components/loading_button';
@ -28,7 +29,7 @@ const module = uiModules.get('apps/context', [
module.directive('contextApp', function ContextApp() {
return {
bindToController: true,
controller: ContextAppController,
controller: callAfterBindingsWorkaround(ContextAppController),
controllerAs: 'contextApp',
restrict: 'E',
scope: {

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import { uiModules } from 'ui/modules';
import { callAfterBindingsWorkaround } from 'ui/compat';
import contextSizePickerTemplate from './size_picker.html';
import './size_picker.less';
@ -10,7 +11,7 @@ const module = uiModules.get('apps/context', [
module.directive('contextSizePicker', function ContextSizePicker() {
return {
bindToController: true,
controller: ContextSizePickerController,
controller: callAfterBindingsWorkaround(ContextSizePickerController),
controllerAs: 'contextSizePicker',
link: linkContextSizePicker,
replace: true,

View file

@ -23,9 +23,13 @@ const MetricsRequestHandlerProvider = function (Private, Notifier, config, timef
try {
const maxBuckets = config.get('metrics:max_buckets');
validateInterval(timefilter, panel, maxBuckets);
return $http.post('../api/metrics/vis/data', params)
.success(resolve)
.error(resp => {
const httpResult = $http.post('../api/metrics/vis/data', params)
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
return httpResult
.then(resolve)
.catch(resp => {
resolve({});
const err = new Error(resp.message);
err.stack = resp.stack;

View file

@ -6,13 +6,17 @@ const FetchFieldsProvider = (Notifier, $http) => {
const fields = {};
Promise.all(indexPatterns.map(pattern => {
return $http.get(`../api/metrics/fields?index=${pattern}`)
.success(resp => {
const httpResult = $http.get(`../api/metrics/fields?index=${pattern}`)
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
return httpResult
.then(resp => {
if (resp.length && pattern) {
fields[pattern] = resp;
}
})
.error(resp => {
.catch(resp => {
const err = new Error(resp.message);
err.stack = resp.stack;
notify.error(err);
@ -26,4 +30,3 @@ const FetchFieldsProvider = (Notifier, $http) => {
};
export { FetchFieldsProvider };

View file

@ -215,15 +215,18 @@ app.controller('timelion', function (
$scope.state.save();
$scope.running = true;
$http.post('../api/timelion/run', {
const httpResult = $http.post('../api/timelion/run', {
sheet: $scope.state.sheet,
time: _.extend(timefilter.time, {
interval: $scope.state.interval,
timezone: timezone
}),
})
// data, status, headers, config
.success(function (resp) {
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
httpResult
.then(function (resp) {
dismissNotifications();
$scope.stats = resp.stats;
$scope.sheet = resp.sheet;
@ -234,7 +237,7 @@ app.controller('timelion', function (
});
$scope.running = false;
})
.error(function (resp) {
.catch(function (resp) {
$scope.sheet = [];
$scope.running = false;

View file

@ -20,7 +20,7 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
const expression = vis.params.expression;
if (!expression) return;
$http.post('../api/timelion/run', {
const httpResult = $http.post('../api/timelion/run', {
sheet: [expression],
extended: {
es: {
@ -32,10 +32,14 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
timezone: timezone
}),
})
.success(function (resp) {
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
httpResult
.then(function (resp) {
resolve(resp);
})
.error(function (resp) {
.catch(function (resp) {
const err = new Error(resp.message);
err.stack = resp.stack;
notify.error(err);

View file

@ -155,7 +155,19 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
// This happens if show is triggered multiple times before any hide is triggered.
if (!popupTimeout) {
popupTimeout = $timeout( show, ttScope.popupDelay, false );
popupTimeout.then(function(reposition){reposition();});
popupTimeout
.then(reposition => reposition())
.catch((error) => {
// if the timeout is canceled then the string `canceled` is thrown. To prevent
// this from triggering an 'unhandled promise rejection' in angular 1.5+ the
// $timeout service explicitely tells $q that the promise it generated is "handled"
// but that does not include down chain promises like the one created by calling
// `popupTimeout.then()`. Because of this we need to ignore the "canceled" string
// and only propagate real errors
if (error !== 'canceled') {
throw error
}
});
}
} else {
show()();

View file

@ -172,7 +172,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
//we need to propagate user's query so we can higlight matches
scope.query = undefined;
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
var timeoutPromise;
var scheduleSearchWithTimeout = function(inputValue) {
@ -379,8 +379,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
},
link:function (scope, element, attrs) {
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
$http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
element.replaceWith($compile(tplContent.trim())(scope));
$http.get(tplUrl, {cache: $templateCache}).then(function(resp){
element.replaceWith($compile(resp.data.trim())(scope));
});
}
};

View file

@ -33,11 +33,13 @@ export function initAngularApi(chrome, internals) {
return a.href;
}()))
.config(chrome.$setupXsrfRequestInterceptor)
.config(['$compileProvider', function ($compileProvider) {
.config(function ($compileProvider, $locationProvider) {
if (!internals.devMode) {
$compileProvider.debugInfoEnabled(false);
}
}])
$locationProvider.hashPrefix('');
})
.run(($location, $rootScope, Private, config) => {
chrome.getFirstPathSegment = () => {
return $location.path().split('/')[1];

View file

@ -0,0 +1,4 @@
export {
InitAfterBindingsWorkaround,
callAfterBindingsWorkaround
} from './init_after_bindings_workaround';

View file

@ -0,0 +1,63 @@
/**
* WHAT NEEDS THIS WORKAROUND?
* ===========================
* Any directive that meets all of the following criteria:
* - uses isolate scope bindings
* - sets `bindToController: true`
* - synchronously accesses the bound values in the controller constructor
*
*
*
* HOW DO I GET RID OF IT?
* =======================
* The quick band-aid solution:
* Wrap your constructor logic so it doesn't access bound values
* synchronously. This can have subtle bugs which is why I didn't
* just wrap all of the offenders in $timeout() and made this
* workaround instead.
*
* The more complete solution:
* Use the new component lifecycle methods, like `$onInit()`, to access
* bindings immediately after the constructor is called, which shouldn't
* have any observable effect outside of the constructor.
*
* NOTE: `$onInit()` is not dependency injected, if you need controller specific
* dependencies like `$scope` then you're probably using watchers and should
* take a look at the new one-way data flow facitilies available to
* directives/components:
*
* https://docs.angularjs.org/guide/component#component-based-application-architecture
*
*/
export class InitAfterBindingsWorkaround {
static $inject = ['$injector', '$attrs', '$element', '$scope', '$transclude']
constructor($injector, $attrs, $element, $scope, $transclude) {
if (!this.initAfterBindings) {
throw new Error('When using inheritance you must move the logic in the constructor to the `initAfterBindings` method');
}
this.$onInit = () => {
$injector.invoke(this.initAfterBindings, this, {
$attrs,
$element,
$scope,
$transclude
});
};
}
}
export function callAfterBindingsWorkaround(constructor) {
return function InitAfterBindingsWrapper($injector, $attrs, $element, $scope, $transclude) {
this.$onInit = () => {
$injector.invoke(constructor, this, {
$attrs,
$element,
$scope,
$transclude
});
};
};
}

View file

@ -6,7 +6,7 @@ import { ConfigDelayedUpdaterProvider } from 'ui/config/_delayed_updater';
const module = uiModules.get('kibana/config');
// service for delivering config variables to everywhere else
module.service(`config`, function (Private, $rootScope, $http, chrome, uiSettings) {
module.service(`config`, function (Private, $rootScope, chrome, uiSettings) {
const config = this;
const notify = new Notifier({ location: `Config` });
const { defaults, user: initialUserSettings } = uiSettings;

View file

@ -0,0 +1,178 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import $ from 'jquery';
const template = `
<form name="person" ng-submit="onSubmit()">
<input data-test-subj="name" ng-model="name" required/>
<ul>
<li ng-repeat="task in tasks">
<ng-form data-test-subj="{{'task-' + $index}}">
<input data-test-subj="taskName" ng-model="task.name" required />
<input data-test-subj="taskDesc" ng-model="task.description" required />
</ng-form>
</li>
</ul>
<button data-test-subj="submit" type="submit">Submit</button>
</form>
`;
describe('fancy forms', function () {
let setup;
const trash = [];
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(($injector) => {
const $rootScope = $injector.get('$rootScope');
const $compile = $injector.get('$compile');
setup = function (options = {}) {
const {
name = 'person1',
tasks = [],
onSubmit = () => {},
} = options;
const $el = $(template).appendTo('body');
trash.push(() => $el.remove());
const $scope = $rootScope.$new();
$scope.name = name;
$scope.tasks = tasks;
$scope.onSubmit = onSubmit;
$compile($el)($scope);
$scope.$apply();
return {
$el,
$scope,
};
};
}));
afterEach(() => trash.splice(0).forEach(fn => fn()));
describe('nested forms', function () {
it('treats new fields as "soft" errors', function () {
const { $scope } = setup({ name: '' });
expect($scope.person.errorCount()).to.be(1);
expect($scope.person.softErrorCount()).to.be(0);
});
it('upgrades fields to regular errors on attempted submit', function () {
const { $scope, $el } = setup({ name: '' });
expect($scope.person.errorCount()).to.be(1);
expect($scope.person.softErrorCount()).to.be(0);
$el.findTestSubject('submit').click();
expect($scope.person.errorCount()).to.be(1);
expect($scope.person.softErrorCount()).to.be(1);
});
it('prevents submit when there are errors', function () {
const onSubmit = sinon.stub();
const { $scope, $el } = setup({ name: '', onSubmit });
expect($scope.person.errorCount()).to.be(1);
sinon.assert.notCalled(onSubmit);
$el.findTestSubject('submit').click();
expect($scope.person.errorCount()).to.be(1);
sinon.assert.notCalled(onSubmit);
$scope.$apply(() => {
$scope.name = 'foo';
});
expect($scope.person.errorCount()).to.be(0);
sinon.assert.notCalled(onSubmit);
$el.findTestSubject('submit').click();
expect($scope.person.errorCount()).to.be(0);
sinon.assert.calledOnce(onSubmit);
});
it('new fields are no longer soft after blur', function () {
const { $scope, $el } = setup({ name: '' });
expect($scope.person.softErrorCount()).to.be(0);
$el.findTestSubject('name').blur();
expect($scope.person.softErrorCount()).to.be(1);
});
it('counts errors/softErrors in sub forms', function () {
const { $scope, $el } = setup();
expect($scope.person.errorCount()).to.be(0);
$scope.$apply(() => {
$scope.tasks = [
{
name: 'foo',
description: ''
},
{
name: 'foo',
description: ''
}
];
});
expect($scope.person.errorCount()).to.be(2);
expect($scope.person.softErrorCount()).to.be(0);
$el.findTestSubject('taskDesc').first().blur();
expect($scope.person.errorCount()).to.be(2);
expect($scope.person.softErrorCount()).to.be(1);
});
it('only counts down', function () {
const { $scope, $el } = setup({
tasks: [
{
name: 'foo',
description: ''
},
{
name: 'bar',
description: ''
},
{
name: 'baz',
description: ''
}
]
});
// top level form sees 3 errors
expect($scope.person.errorCount()).to.be(3);
expect($scope.person.softErrorCount()).to.be(0);
$el.find('ng-form').toArray().forEach((el, i) => {
const $task = $(el);
const $taskScope = $task.scope();
const form = $task.controller('form');
// sub forms only see one error
expect(form.errorCount()).to.be(1);
expect(form.softErrorCount()).to.be(0);
// blurs only count locally
$task.findTestSubject('taskDesc').blur();
expect(form.softErrorCount()).to.be(1);
// but parent form see them
expect($scope.person.softErrorCount()).to.be(1);
$taskScope.$apply(() => {
$taskScope.task.description = 'valid';
});
expect(form.errorCount()).to.be(0);
expect(form.softErrorCount()).to.be(0);
expect($scope.person.errorCount()).to.be(2 - i);
expect($scope.person.softErrorCount()).to.be(0);
});
});
});
});

View file

@ -1,41 +1,12 @@
import _ from 'lodash';
import { KbnFormController } from 'ui/fancy_forms/kbn_form_controller';
import { uiModules } from 'ui/modules';
import { decorateFormController } from './kbn_form_controller';
import { decorateModelController } from './kbn_model_controller';
uiModules
.get('kibana')
.config(function ($provide) {
function decorateDirectiveController(DecorativeController) {
return function ($delegate, $injector) {
// directive providers are arrays
$delegate.forEach(function (directive) {
// get metadata about all init fns
const chain = [directive.controller, DecorativeController].map(function (fn) {
const deps = $injector.annotate(fn);
return { deps: deps, fn: _.isArray(fn) ? _.last(fn) : fn };
});
// replace the controller with one that will setup the actual controller
directive.controller = function stub() {
const allDeps = _.toArray(arguments);
return chain.reduce(function (controller, link) {
const deps = allDeps.splice(0, link.deps.length);
return link.fn.apply(controller, deps) || controller;
}, this);
};
// set the deps of our new controller to be the merged deps of every fn
directive.controller.$inject = chain.reduce(function (deps, link) {
return deps.concat(link.deps);
}, []);
});
return $delegate;
};
}
$provide.decorator('formDirective', decorateDirectiveController(KbnFormController));
$provide.decorator('ngFormDirective', decorateDirectiveController(KbnFormController));
$provide.decorator('formDirective', decorateFormController);
$provide.decorator('ngFormDirective', decorateFormController);
$provide.decorator('ngModelDirective', decorateModelController);
});

View file

@ -1,74 +1,78 @@
import _ from 'lodash';
export function decorateFormController($delegate, $injector) {
const [directive] = $delegate;
const FormController = directive.controller;
/**
* Extension of Angular's FormController class
* that provides helpers for error handling/validation.
*
* @param {$scope} $scope
*/
export function KbnFormController($scope, $element) {
const self = this;
class KbnFormController extends FormController {
// prevent inheriting FormController's static $inject property
// which is angular's cache of the DI arguments for a function
static $inject = ['$scope', '$element'];
self.errorCount = function () {
return self.$$invalidModels().length;
};
constructor($scope, $element, ...superArgs) {
super(...superArgs);
// same as error count, but filters out untouched and pristine models
self.softErrorCount = function () {
return self.$$invalidModels(function (model) {
return model.$touched || model.$dirty;
}).length;
};
const onSubmit = () => {
this._markInvalidTouched();
};
self.describeErrors = function () {
const count = self.softErrorCount();
return count + ' Error' + (count === 1 ? '' : 's');
};
self.$$invalidModels = function (predicate) {
predicate = _.callback(predicate);
const invalid = [];
_.forOwn(self.$error, function collect(models) {
if (!models) return;
models.forEach(function (model) {
if (model.$$invalidModels) {
// recurse into child form
_.forOwn(model.$error, collect);
} else {
if (predicate(model)) {
// prevent dups
let len = invalid.length;
while (len--) if (invalid[len] === model) return;
invalid.push(model);
}
}
$element.on('submit', onSubmit);
$scope.$on('$destroy', () => {
$element.off('submit', onSubmit);
});
});
}
return invalid;
};
errorCount() {
return this._getInvalidModels().length;
}
self.$setTouched = function () {
self.$$invalidModels().forEach(function (model) {
// only kbnModels have $setTouched
if (model.$setTouched) model.$setTouched();
});
};
// same as error count, but filters out untouched and pristine models
softErrorCount() {
return this._getInvalidModels()
.filter(model => model.$touched || model.$dirty)
.length;
}
function filterSubmits(event) {
if (self.errorCount()) {
event.preventDefault();
event.stopImmediatePropagation();
self.$setTouched();
describeErrors() {
const count = this.softErrorCount();
return `${count} Error${count === 1 ? '' : 's'}`;
}
$setTouched() {
this._getInvalidModels()
.forEach(model => model.$setTouched());
}
_markInvalidTouched(event) {
if (this.errorCount()) {
event.preventDefault();
event.stopImmediatePropagation();
this.$setTouched();
}
}
_getInvalidModels() {
return this.$$controls.reduce((acc, control) => {
// recurse into sub-form
if (typeof control._getInvalidModels === 'function') {
return [...acc, ...control._getInvalidModels()];
}
if (control.$invalid) {
return [...acc, control];
}
return acc;
}, []);
}
}
$element.on('submit', filterSubmits);
$scope.$on('$destroy', function () {
$element.off('submit', filterSubmits);
});
// replace controller with our wrapper
directive.controller = [
...$injector.annotate(KbnFormController),
...$injector.annotate(FormController),
(...args) => (
new KbnFormController(...args)
)
];
return $delegate;
}

View file

@ -0,0 +1,37 @@
export function decorateModelController($delegate, $injector) {
const [directive] = $delegate;
const ModelController = directive.controller;
class KbnModelController extends ModelController {
// prevent inheriting ModelController's static $inject property
// which is angular's cache of the DI arguments for a function
static $inject = ['$scope', '$element'];
constructor($scope, $element, ...superArgs) {
super(...superArgs);
const onInvalid = () => {
this.$setTouched();
};
// the browser emits an "invalid" event when browser supplied
// validation fails, which implies that the user has indirectly
// interacted with the control and it should be treated as "touched"
$element.on('invalid', onInvalid);
$scope.$on('$destroy', () => {
$element.off('invalid', onInvalid);
});
}
}
// replace controller with our wrapper
directive.controller = [
...$injector.annotate(KbnModelController),
...$injector.annotate(ModelController),
(...args) => (
new KbnModelController(...args)
)
];
return $delegate;
}

View file

@ -19,7 +19,7 @@ uiModules
attrs.$observe('id', () => $scope.id = attrs.id);
// bind our local model with the outside ngModel
$scope.$watch('model', ngModelCntrl.$setViewValue);
$scope.$watch('model', v => ngModelCntrl.$setViewValue(v));
ngModelCntrl.$render = function () {
$scope.model = ngModelCntrl.$viewValue;
};

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import { uiModules } from 'ui/modules';
import { callAfterBindingsWorkaround } from 'ui/compat';
import { FILTER_OPERATOR_TYPES } from './lib/filter_operators';
import template from './filter_editor.html';
import { documentationLinks } from '../documentation_links/documentation_links';
@ -33,7 +34,7 @@ module.directive('filterEditor', function ($timeout, indexPatterns) {
},
controllerAs: 'filterEditor',
bindToController: true,
controller: function ($scope, $element) {
controller: callAfterBindingsWorkaround(function ($scope, $element) {
this.init = () => {
const { filter } = this;
this.docLinks = documentationLinks;
@ -124,6 +125,6 @@ module.directive('filterEditor', function ($timeout, indexPatterns) {
$timeout(() => this.onCancel());
}
});
}
})
};
});

View file

@ -26,7 +26,7 @@ export function getRoutes() {
};
}
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise, kbnUrl) {
export function IndexPatternProvider(Private, config, Promise, confirmModalPromise, kbnUrl) {
const fieldformats = Private(RegistryFieldFormatsProvider);
const getConfig = (...args) => config.get(...args);
const getIds = Private(IndexPatternsGetProvider)('id');

View file

@ -1,7 +1,7 @@
import angular from 'angular';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import $ from 'jquery';
describe('ui/modals/confirm_modal_promise', function () {
@ -23,11 +23,8 @@ describe('ui/modals/confirm_modal_promise', function () {
});
afterEach(function () {
const confirmButton = angular.element(document.body).find('[data-test-subj=confirmModalConfirmButton]');
if (confirmButton) {
$rootScope.$digest();
angular.element(confirmButton).click();
}
$rootScope.$digest();
$.findTestSubject('confirmModalConfirmButton').click();
});
describe('before timeout completes', function () {
@ -42,7 +39,7 @@ describe('ui/modals/confirm_modal_promise', function () {
describe('after timeout completes', function () {
it('confirmation dialogue is loaded to dom with message', function () {
$rootScope.$digest();
const confirmModalElement = angular.element(document.body).find('[data-test-subj=confirmModal]');
const confirmModalElement = $.findTestSubject('confirmModal');
expect(confirmModalElement).to.not.be(undefined);
const htmlString = confirmModalElement[0].innerHTML;
@ -57,7 +54,7 @@ describe('ui/modals/confirm_modal_promise', function () {
promise.then(confirmCallback, cancelCallback);
$rootScope.$digest();
const confirmButton = angular.element(document.body).find('[data-test-subj=confirmModalConfirmButton]');
const confirmButton = $.findTestSubject('confirmModalConfirmButton');
confirmButton.click();
expect(confirmCallback.called).to.be(true);
@ -72,7 +69,7 @@ describe('ui/modals/confirm_modal_promise', function () {
promise.then(confirmCallback, cancelCallback);
$rootScope.$digest();
const noButton = angular.element(document.body).find('[data-test-subj=confirmModalCancelButton]');
const noButton = $.findTestSubject('confirmModalCancelButton');
noButton.click();
expect(cancelCallback.called).to.be(true);
@ -82,12 +79,17 @@ describe('ui/modals/confirm_modal_promise', function () {
describe('error is thrown', function () {
it('when no confirm button text is used', function () {
try {
confirmModalPromise(message);
expect(false).to.be(true);
} catch (error) {
expect(error).to.not.be(undefined);
}
const confirmCallback = sinon.spy();
const cancelCallback = sinon.spy();
confirmModalPromise(message).then(confirmCallback, cancelCallback);
$rootScope.$digest();
sinon.assert.notCalled(confirmCallback);
sinon.assert.calledOnce(cancelCallback);
sinon.assert.calledWithExactly(
cancelCallback,
sinon.match.has('message', sinon.match(/confirmation button text/))
);
});
});
});

View file

@ -1,4 +1,5 @@
import { uiModules } from 'ui/modules';
import { callAfterBindingsWorkaround } from 'ui/compat';
import template from './pattern_checker.html';
import './pattern_checker.less';
import chrome from 'ui/chrome';
@ -14,7 +15,7 @@ module.directive('patternChecker', function () {
scope: {
pattern: '='
},
controller: function (Notifier, $scope, $timeout, $http) {
controller: callAfterBindingsWorkaround(function (Notifier, $scope, $timeout, $http) {
let validationTimeout;
const notify = new Notifier({
@ -46,7 +47,6 @@ module.directive('patternChecker', function () {
});
this.validateInstall();
}
})
};
});

View file

@ -1,4 +1,5 @@
import { uiModules } from 'ui/modules';
import { callAfterBindingsWorkaround } from 'ui/compat';
import template from './query_bar.html';
import { queryLanguages } from '../lib/queryLanguages';
import { documentationLinks } from '../../documentation_links/documentation_links.js';
@ -17,7 +18,7 @@ module.directive('queryBar', function () {
},
controllerAs: 'queryBar',
bindToController: true,
controller: function ($scope, config) {
controller: callAfterBindingsWorkaround(function ($scope, config) {
this.queryDocLinks = documentationLinks.query;
this.appName = this.appName || 'global';
this.availableQueryLanguages = queryLanguages;
@ -36,7 +37,7 @@ module.directive('queryBar', function () {
$scope.$watch('queryBar.query', (newQuery) => {
this.localQuery = Object.assign({}, newQuery);
}, true);
}
})
};
});

View file

@ -112,7 +112,10 @@ describe('typeahead directive', function () {
describe('controller scope', function () {
it('should contain the input model', function () {
expect($typeaheadScope.inputModel).to.be.an('object');
expect($typeaheadScope.inputModel).to.have.keys(['$viewValue', '$modelValue', '$setViewValue']);
expect($typeaheadScope.inputModel)
.to.have.property('$viewValue')
.and.have.property('$modelValue')
.and.have.property('$setViewValue').a('function');
});
it('should save data to the scope', function () {

View file

@ -12,7 +12,7 @@ import visOptionsTemplate from './vis_options.html';
uiModules
.get('app/visualize')
.directive('visEditorVisOptions', function (Private, $timeout, $compile) {
.directive('visEditorVisOptions', function (Private, $compile) {
return {
restrict: 'E',
template: visOptionsTemplate,

View file

@ -40,9 +40,9 @@ describe('$scope.$watchMulti', function () {
expect(triggers).to.be(2);
// remove watchers
expect($scope.$$watchers).to.not.eql([]);
expect($scope.$$watchers).to.not.have.length(0);
unwatch();
expect($scope.$$watchers).to.eql([]);
expect($scope.$$watchers).to.have.length(0);
// prove that it doesn't trigger anymore
$scope.a++;