@angular-wave/angular.ts 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +29 -0
- package/.github/workflows/playwright.yml +27 -0
- package/CHANGELOG.md +17974 -0
- package/CODE_OF_CONDUCT.md +3 -0
- package/CONTRIBUTING.md +246 -0
- package/DEVELOPERS.md +488 -0
- package/LICENSE +22 -0
- package/Makefile +31 -0
- package/README.md +115 -0
- package/RELEASE.md +98 -0
- package/SECURITY.md +16 -0
- package/TRIAGING.md +135 -0
- package/css/angular.css +22 -0
- package/dist/angular-ts.cjs.js +36843 -0
- package/dist/angular-ts.esm.js +36841 -0
- package/dist/angular-ts.umd.js +36848 -0
- package/dist/build/angular-animate.js +4272 -0
- package/dist/build/angular-aria.js +426 -0
- package/dist/build/angular-message-format.js +1072 -0
- package/dist/build/angular-messages.js +829 -0
- package/dist/build/angular-mocks.js +3757 -0
- package/dist/build/angular-parse-ext.js +1275 -0
- package/dist/build/angular-resource.js +911 -0
- package/dist/build/angular-route.js +1266 -0
- package/dist/build/angular-sanitize.js +891 -0
- package/dist/build/angular-touch.js +368 -0
- package/dist/build/angular.js +36600 -0
- package/e2e/unit.spec.ts +15 -0
- package/images/android-chrome-192x192.png +0 -0
- package/images/android-chrome-512x512.png +0 -0
- package/images/apple-touch-icon.png +0 -0
- package/images/favicon-16x16.png +0 -0
- package/images/favicon-32x32.png +0 -0
- package/images/favicon.ico +0 -0
- package/images/site.webmanifest +1 -0
- package/index.html +104 -0
- package/package.json +47 -0
- package/playwright.config.ts +78 -0
- package/public/circle.html +1 -0
- package/public/my_child_directive.html +1 -0
- package/public/my_directive.html +1 -0
- package/public/my_other_directive.html +1 -0
- package/public/test.html +1 -0
- package/rollup.config.js +31 -0
- package/src/animations/animateCache.js +55 -0
- package/src/animations/animateChildrenDirective.js +105 -0
- package/src/animations/animateCss.js +1139 -0
- package/src/animations/animateCssDriver.js +291 -0
- package/src/animations/animateJs.js +367 -0
- package/src/animations/animateJsDriver.js +67 -0
- package/src/animations/animateQueue.js +851 -0
- package/src/animations/animation.js +506 -0
- package/src/animations/module.js +779 -0
- package/src/animations/ngAnimateSwap.js +119 -0
- package/src/animations/rafScheduler.js +50 -0
- package/src/animations/shared.js +378 -0
- package/src/constants.js +20 -0
- package/src/core/animate.js +845 -0
- package/src/core/animateCss.js +73 -0
- package/src/core/animateRunner.js +195 -0
- package/src/core/attributes.js +199 -0
- package/src/core/cache.js +45 -0
- package/src/core/compile.js +4727 -0
- package/src/core/controller.js +225 -0
- package/src/core/exceptionHandler.js +63 -0
- package/src/core/filter.js +146 -0
- package/src/core/interpolate.js +442 -0
- package/src/core/interval.js +188 -0
- package/src/core/intervalFactory.js +57 -0
- package/src/core/location.js +1086 -0
- package/src/core/parser/parse.js +2562 -0
- package/src/core/parser/parse.md +13 -0
- package/src/core/q.js +746 -0
- package/src/core/rootScope.js +1596 -0
- package/src/core/sanitizeUri.js +85 -0
- package/src/core/sce.js +1161 -0
- package/src/core/taskTrackerFactory.js +125 -0
- package/src/core/timeout.js +121 -0
- package/src/core/urlUtils.js +187 -0
- package/src/core/utils.js +1349 -0
- package/src/directive/a.js +37 -0
- package/src/directive/attrs.js +283 -0
- package/src/directive/bind.js +51 -0
- package/src/directive/bind.md +142 -0
- package/src/directive/change.js +12 -0
- package/src/directive/change.md +25 -0
- package/src/directive/cloak.js +12 -0
- package/src/directive/cloak.md +24 -0
- package/src/directive/events.js +75 -0
- package/src/directive/events.md +166 -0
- package/src/directive/form.js +725 -0
- package/src/directive/init.js +15 -0
- package/src/directive/init.md +41 -0
- package/src/directive/input.js +1783 -0
- package/src/directive/list.js +46 -0
- package/src/directive/list.md +22 -0
- package/src/directive/ngClass.js +249 -0
- package/src/directive/ngController.js +64 -0
- package/src/directive/ngCsp.js +82 -0
- package/src/directive/ngIf.js +134 -0
- package/src/directive/ngInclude.js +217 -0
- package/src/directive/ngModel.js +1356 -0
- package/src/directive/ngModelOptions.js +509 -0
- package/src/directive/ngOptions.js +670 -0
- package/src/directive/ngRef.js +90 -0
- package/src/directive/ngRepeat.js +650 -0
- package/src/directive/ngShowHide.js +255 -0
- package/src/directive/ngSwitch.js +178 -0
- package/src/directive/ngTransclude.js +98 -0
- package/src/directive/non-bindable.js +11 -0
- package/src/directive/non-bindable.md +17 -0
- package/src/directive/script.js +30 -0
- package/src/directive/select.js +624 -0
- package/src/directive/style.js +25 -0
- package/src/directive/style.md +23 -0
- package/src/directive/validators.js +329 -0
- package/src/exts/aria.js +544 -0
- package/src/exts/messages.js +852 -0
- package/src/filters/filter.js +207 -0
- package/src/filters/filter.md +69 -0
- package/src/filters/filters.js +239 -0
- package/src/filters/json.md +16 -0
- package/src/filters/limit-to.js +43 -0
- package/src/filters/limit-to.md +19 -0
- package/src/filters/order-by.js +183 -0
- package/src/filters/order-by.md +83 -0
- package/src/index.js +13 -0
- package/src/injector.js +1034 -0
- package/src/jqLite.js +1117 -0
- package/src/loader.js +1320 -0
- package/src/public.js +215 -0
- package/src/routeToRegExp.js +41 -0
- package/src/services/anchorScroll.js +135 -0
- package/src/services/browser.js +321 -0
- package/src/services/cacheFactory.js +398 -0
- package/src/services/cookieReader.js +72 -0
- package/src/services/document.js +64 -0
- package/src/services/http.js +1537 -0
- package/src/services/httpBackend.js +206 -0
- package/src/services/log.js +160 -0
- package/src/services/templateRequest.js +139 -0
- package/test/angular.spec.js +2153 -0
- package/test/aria/aria.spec.js +1245 -0
- package/test/binding.spec.js +504 -0
- package/test/build-test.html +14 -0
- package/test/injector.spec.js +2327 -0
- package/test/jasmine/jasmine-5.1.2/boot0.js +65 -0
- package/test/jasmine/jasmine-5.1.2/boot1.js +133 -0
- package/test/jasmine/jasmine-5.1.2/jasmine-html.js +963 -0
- package/test/jasmine/jasmine-5.1.2/jasmine.css +320 -0
- package/test/jasmine/jasmine-5.1.2/jasmine.js +10824 -0
- package/test/jasmine/jasmine-5.1.2/jasmine_favicon.png +0 -0
- package/test/jasmine/jasmine-browser.json +17 -0
- package/test/jasmine/jasmine.json +9 -0
- package/test/jqlite.spec.js +2133 -0
- package/test/loader.spec.js +219 -0
- package/test/messages/messages.spec.js +1146 -0
- package/test/min-err.spec.js +174 -0
- package/test/mock-test.html +13 -0
- package/test/module-test.html +15 -0
- package/test/ng/anomate.spec.js +606 -0
- package/test/ng/cache-factor.spec.js +334 -0
- package/test/ng/compile.spec.js +17956 -0
- package/test/ng/controller-provider.spec.js +227 -0
- package/test/ng/cookie-reader.spec.js +98 -0
- package/test/ng/directive/a.spec.js +192 -0
- package/test/ng/directive/bind.spec.js +334 -0
- package/test/ng/directive/boolean.spec.js +136 -0
- package/test/ng/directive/change.spec.js +71 -0
- package/test/ng/directive/class.spec.js +858 -0
- package/test/ng/directive/click.spec.js +38 -0
- package/test/ng/directive/cloak.spec.js +44 -0
- package/test/ng/directive/constoller.spec.js +194 -0
- package/test/ng/directive/element-style.spec.js +92 -0
- package/test/ng/directive/event.spec.js +282 -0
- package/test/ng/directive/form.spec.js +1518 -0
- package/test/ng/directive/href.spec.js +143 -0
- package/test/ng/directive/if.spec.js +402 -0
- package/test/ng/directive/include.spec.js +828 -0
- package/test/ng/directive/init.spec.js +68 -0
- package/test/ng/directive/input.spec.js +3810 -0
- package/test/ng/directive/list.spec.js +170 -0
- package/test/ng/directive/model-options.spec.js +1008 -0
- package/test/ng/directive/model.spec.js +1905 -0
- package/test/ng/directive/non-bindable.spec.js +55 -0
- package/test/ng/directive/options.spec.js +3583 -0
- package/test/ng/directive/ref.spec.js +575 -0
- package/test/ng/directive/repeat.spec.js +1675 -0
- package/test/ng/directive/script.spec.js +52 -0
- package/test/ng/directive/scrset.spec.js +67 -0
- package/test/ng/directive/select.spec.js +2541 -0
- package/test/ng/directive/show-hide.spec.js +253 -0
- package/test/ng/directive/src.spec.js +157 -0
- package/test/ng/directive/style.spec.js +178 -0
- package/test/ng/directive/switch.spec.js +647 -0
- package/test/ng/directive/validators.spec.js +717 -0
- package/test/ng/document.spec.js +52 -0
- package/test/ng/filter/filter.spec.js +714 -0
- package/test/ng/filter/filters.spec.js +35 -0
- package/test/ng/filter/limit-to.spec.js +251 -0
- package/test/ng/filter/order-by.spec.js +891 -0
- package/test/ng/filter.spec.js +149 -0
- package/test/ng/http-backend.spec.js +398 -0
- package/test/ng/http.spec.js +4071 -0
- package/test/ng/interpolate.spec.js +642 -0
- package/test/ng/interval.spec.js +343 -0
- package/test/ng/location.spec.js +3488 -0
- package/test/ng/on.spec.js +229 -0
- package/test/ng/parse.spec.js +4655 -0
- package/test/ng/prop.spec.js +805 -0
- package/test/ng/q.spec.js +2904 -0
- package/test/ng/root-element.spec.js +16 -0
- package/test/ng/sanitize-uri.spec.js +249 -0
- package/test/ng/sce.spec.js +660 -0
- package/test/ng/scope.spec.js +3442 -0
- package/test/ng/template-request.spec.js +236 -0
- package/test/ng/timeout.spec.js +351 -0
- package/test/ng/url-utils.spec.js +156 -0
- package/test/ng/utils.spec.js +144 -0
- package/test/original-test.html +21 -0
- package/test/public.spec.js +34 -0
- package/test/sanitize/bing-html.spec.js +36 -0
- package/test/server/express.js +158 -0
- package/test/test-utils.js +11 -0
- package/tsconfig.json +17 -0
- package/types/angular.d.ts +138 -0
- package/types/global.d.ts +9 -0
- package/types/index.d.ts +2357 -0
- package/types/jqlite.d.ts +558 -0
- package/vite.config.js +14 -0
|
@@ -0,0 +1,2541 @@
|
|
|
1
|
+
import { publishExternalAPI } from "../../../src/public";
|
|
2
|
+
import { createInjector } from "../../../src/injector";
|
|
3
|
+
import { dealoc, jqLite } from "../../../src/jqLite";
|
|
4
|
+
import { forEach, hashKey, equals, isNumberNaN } from "../../../src/core/utils";
|
|
5
|
+
import { browserTrigger } from "../../test-utils";
|
|
6
|
+
|
|
7
|
+
describe("select", () => {
|
|
8
|
+
let scope;
|
|
9
|
+
let formElement;
|
|
10
|
+
let element;
|
|
11
|
+
let $compile;
|
|
12
|
+
let ngModelCtrl;
|
|
13
|
+
let selectCtrl;
|
|
14
|
+
let renderSpy;
|
|
15
|
+
let $rootScope;
|
|
16
|
+
const optionAttributesList = [];
|
|
17
|
+
|
|
18
|
+
function compile(html) {
|
|
19
|
+
formElement = jqLite(`<form name="form">${html}</form>`);
|
|
20
|
+
element = formElement.find("select");
|
|
21
|
+
$compile(formElement)(scope);
|
|
22
|
+
ngModelCtrl = element.controller("ngModel");
|
|
23
|
+
scope.$digest();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setSelectValue(selectElement, optionIndex) {
|
|
27
|
+
const option = selectElement.find("option").eq(optionIndex);
|
|
28
|
+
selectElement.val(option.val());
|
|
29
|
+
browserTrigger(element, "change");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compileRepeatedOptions() {
|
|
33
|
+
compile(
|
|
34
|
+
'<select ng-model="robot">' +
|
|
35
|
+
'<option value="{{item.value}}" ng-repeat="item in robots">{{item.label}}</option>' +
|
|
36
|
+
"</select>",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compileGroupedOptions() {
|
|
41
|
+
compile(
|
|
42
|
+
'<select ng-model="mySelect">' +
|
|
43
|
+
'<option ng-repeat="item in values">{{item.name}}</option>' +
|
|
44
|
+
'<optgroup ng-repeat="group in groups" label="{{group.name}}">' +
|
|
45
|
+
'<option ng-repeat="item in group.values">{{item.name}}</option>' +
|
|
46
|
+
"</optgroup>" +
|
|
47
|
+
"</select>",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function unknownValue(value) {
|
|
52
|
+
return `? ${hashKey(value)} ?`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
publishExternalAPI().decorator("$exceptionHandler", function () {
|
|
57
|
+
return (exception, cause) => {
|
|
58
|
+
throw new Error(exception);
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
createInjector([
|
|
62
|
+
"ng",
|
|
63
|
+
($compileProvider) => {
|
|
64
|
+
$compileProvider.directive("spyOnWriteValue", () => ({
|
|
65
|
+
require: "select",
|
|
66
|
+
link: {
|
|
67
|
+
pre(scope, element, attrs, ctrl) {
|
|
68
|
+
selectCtrl = ctrl;
|
|
69
|
+
renderSpy = jasmine.createSpy("renderSpy");
|
|
70
|
+
selectCtrl.ngModelCtrl.$render = renderSpy.and.callFake(
|
|
71
|
+
selectCtrl.ngModelCtrl.$render,
|
|
72
|
+
);
|
|
73
|
+
spyOn(selectCtrl, "writeValue").and.callThrough();
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
$compileProvider.directive("myOptions", () => ({
|
|
79
|
+
scope: { myOptions: "=" },
|
|
80
|
+
replace: true,
|
|
81
|
+
template:
|
|
82
|
+
'<option value="{{ option.value }}" ng-repeat="option in myOptions">' +
|
|
83
|
+
"{{ options.label }}" +
|
|
84
|
+
"</option>",
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
$compileProvider.directive("exposeAttributes", () => ({
|
|
88
|
+
require: "^^select",
|
|
89
|
+
link: {
|
|
90
|
+
pre(scope, element, attrs, ctrl) {
|
|
91
|
+
optionAttributesList.push(attrs);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
},
|
|
96
|
+
]).invoke((_$rootScope_, _$compile_) => {
|
|
97
|
+
scope = _$rootScope_.$new(); // create a child scope because the root scope can't be $destroy-ed
|
|
98
|
+
$rootScope = _$rootScope_;
|
|
99
|
+
$compile = _$compile_;
|
|
100
|
+
formElement = element = null;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
scope.$destroy(); // disables unknown option work during destruction
|
|
106
|
+
dealoc(formElement);
|
|
107
|
+
ngModelCtrl = null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
jasmine.addMatchers({
|
|
112
|
+
toEqualSelectWithOptions() {
|
|
113
|
+
return {
|
|
114
|
+
compare(actual, expected) {
|
|
115
|
+
const actualValues = {};
|
|
116
|
+
let optionGroup;
|
|
117
|
+
let optionValue;
|
|
118
|
+
|
|
119
|
+
forEach(actual.find("option"), (option) => {
|
|
120
|
+
optionGroup = option.parentNode.label || "";
|
|
121
|
+
actualValues[optionGroup] = actualValues[optionGroup] || [];
|
|
122
|
+
// IE9 doesn't populate the label property from the text property like other browsers
|
|
123
|
+
optionValue = option.label || option.text;
|
|
124
|
+
actualValues[optionGroup].push(
|
|
125
|
+
option.selected ? [optionValue] : optionValue,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const message = function () {
|
|
130
|
+
return `Expected ${toJson(actualValues)} to equal ${toJson(expected)}.`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
pass: equals(expected, actualValues),
|
|
135
|
+
message,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should not add options to the select if ngModel is not present", () => {
|
|
144
|
+
const scope = $rootScope;
|
|
145
|
+
scope.d = "d";
|
|
146
|
+
scope.e = "e";
|
|
147
|
+
scope.f = "f";
|
|
148
|
+
|
|
149
|
+
compile(
|
|
150
|
+
"<select>" +
|
|
151
|
+
"<option ng-value=\"'a'\">alabel</option>" +
|
|
152
|
+
'<option value="b">blabel</option>' +
|
|
153
|
+
"<option >c</option>" +
|
|
154
|
+
'<option ng-value="d">dlabel</option>' +
|
|
155
|
+
'<option value="{{e}}">elabel</option>' +
|
|
156
|
+
"<option>{{f}}</option>" +
|
|
157
|
+
"</select>",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const selectCtrl = element.controller("select");
|
|
161
|
+
|
|
162
|
+
expect(selectCtrl.hasOption("a")).toBe(false);
|
|
163
|
+
expect(selectCtrl.hasOption("b")).toBe(false);
|
|
164
|
+
expect(selectCtrl.hasOption("c")).toBe(false);
|
|
165
|
+
expect(selectCtrl.hasOption("d")).toBe(false);
|
|
166
|
+
expect(selectCtrl.hasOption("e")).toBe(false);
|
|
167
|
+
expect(selectCtrl.hasOption("f")).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("select-one", () => {
|
|
171
|
+
it("should compile children of a select without a ngModel, but not create a model for it", () => {
|
|
172
|
+
compile(
|
|
173
|
+
"<select>" +
|
|
174
|
+
'<option selected="true">{{a}}</option>' +
|
|
175
|
+
'<option value="">{{b}}</option>' +
|
|
176
|
+
"<option>C</option>" +
|
|
177
|
+
"</select>",
|
|
178
|
+
);
|
|
179
|
+
scope.$apply(() => {
|
|
180
|
+
scope.a = "foo";
|
|
181
|
+
scope.b = "bar";
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(element.text()).toBe("foobarC");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should not interfere with selection via selected attr if ngModel directive is not present", () => {
|
|
188
|
+
compile(
|
|
189
|
+
"<select>" +
|
|
190
|
+
"<option>not me</option>" +
|
|
191
|
+
"<option selected>me!</option>" +
|
|
192
|
+
"<option>nah</option>" +
|
|
193
|
+
"</select>",
|
|
194
|
+
);
|
|
195
|
+
expect(element[0].value).toBe("me!");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("required state", () => {
|
|
199
|
+
it("should set the error if the empty option is selected", () => {
|
|
200
|
+
compile(
|
|
201
|
+
'<select name="select" ng-model="selection" required>' +
|
|
202
|
+
'<option value=""></option>' +
|
|
203
|
+
'<option value="a">A</option>' +
|
|
204
|
+
'<option value="b">B</option>' +
|
|
205
|
+
"</select>",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
scope.$apply(() => {
|
|
209
|
+
scope.selection = "a";
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
213
|
+
expect(ngModelCtrl.$error.required).toBeFalsy();
|
|
214
|
+
|
|
215
|
+
let options = element.find("option");
|
|
216
|
+
|
|
217
|
+
// view -> model
|
|
218
|
+
setSelectValue(element, 0);
|
|
219
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
220
|
+
expect(ngModelCtrl.$error.required).toBeTruthy();
|
|
221
|
+
|
|
222
|
+
setSelectValue(element, 1);
|
|
223
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
224
|
+
expect(ngModelCtrl.$error.required).toBeFalsy();
|
|
225
|
+
|
|
226
|
+
// // model -> view
|
|
227
|
+
scope.$apply("selection = null");
|
|
228
|
+
options = element.find("option");
|
|
229
|
+
expect(options[0].selected).toBe(true);
|
|
230
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
231
|
+
expect(ngModelCtrl.$error.required).toBeTruthy();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should validate with empty option and bound ngRequired", () => {
|
|
235
|
+
compile(
|
|
236
|
+
'<select name="select" ng-model="selection" ng-required="required">' +
|
|
237
|
+
'<option value=""></option>' +
|
|
238
|
+
'<option value="a">A</option>' +
|
|
239
|
+
'<option value="b">B</option>' +
|
|
240
|
+
"</select>",
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
scope.$apply(() => {
|
|
244
|
+
scope.required = false;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const options = element.find("option");
|
|
248
|
+
|
|
249
|
+
setSelectValue(element, 0);
|
|
250
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
251
|
+
|
|
252
|
+
scope.$apply("required = true");
|
|
253
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
254
|
+
|
|
255
|
+
scope.$apply('selection = "a"');
|
|
256
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
257
|
+
expect(element[0].value).toBe("a");
|
|
258
|
+
|
|
259
|
+
setSelectValue(element, 0);
|
|
260
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
261
|
+
|
|
262
|
+
scope.$apply("required = false");
|
|
263
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should not be invalid if no required attribute is present", () => {
|
|
267
|
+
compile(
|
|
268
|
+
'<select name="select" ng-model="selection">' +
|
|
269
|
+
'<option value=""></option>' +
|
|
270
|
+
'<option value="c">C</option>' +
|
|
271
|
+
"</select>",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
275
|
+
expect(element[0].classList.contains("ng-pristine")).toBeTrue();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should NOT set the error if the unknown option is selected", () => {
|
|
279
|
+
compile(
|
|
280
|
+
'<select name="select" ng-model="selection" required>' +
|
|
281
|
+
'<option value="a">A</option>' +
|
|
282
|
+
'<option value="b">B</option>' +
|
|
283
|
+
"</select>",
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
scope.$apply(() => {
|
|
287
|
+
scope.selection = "a";
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
291
|
+
expect(ngModelCtrl.$error.required).toBeFalsy();
|
|
292
|
+
|
|
293
|
+
scope.$apply('selection = "c"');
|
|
294
|
+
expect(element[0].value).toBe(unknownValue("c"));
|
|
295
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
296
|
+
expect(ngModelCtrl.$error.required).toBeFalsy();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should work with repeated value options", () => {
|
|
301
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
302
|
+
scope.robot = "r2d2";
|
|
303
|
+
compile(
|
|
304
|
+
'<select ng-model="robot">' +
|
|
305
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
306
|
+
"</select>",
|
|
307
|
+
);
|
|
308
|
+
expect(element[0].value).toBe("r2d2");
|
|
309
|
+
|
|
310
|
+
setSelectValue(element, 0);
|
|
311
|
+
expect(element[0].value).toBe("c3p0");
|
|
312
|
+
expect(scope.robot).toBe("c3p0");
|
|
313
|
+
|
|
314
|
+
scope.$apply(() => {
|
|
315
|
+
scope.robots.unshift("wallee");
|
|
316
|
+
});
|
|
317
|
+
expect(element[0].value).toBe("c3p0");
|
|
318
|
+
expect(scope.robot).toBe("c3p0");
|
|
319
|
+
|
|
320
|
+
scope.$apply(() => {
|
|
321
|
+
scope.robots = ["c3p0+", "r2d2+"];
|
|
322
|
+
scope.robot = "r2d2+";
|
|
323
|
+
});
|
|
324
|
+
expect(element[0].value).toBe("r2d2+");
|
|
325
|
+
expect(scope.robot).toBe("r2d2+");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should interpolate select names", () => {
|
|
329
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
330
|
+
scope.name = "r2d2";
|
|
331
|
+
scope.nameID = 47;
|
|
332
|
+
compile(
|
|
333
|
+
'<form name="form"><select ng-model="name" name="name{{nameID}}">' +
|
|
334
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
335
|
+
"</select></form>",
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(scope.form.name47.$pristine).toBeTruthy();
|
|
339
|
+
setSelectValue(element, 0);
|
|
340
|
+
expect(scope.form.name47.$dirty).toBeTruthy();
|
|
341
|
+
expect(scope.name).toBe("c3p0");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should rename select controls in form when interpolated name changes", () => {
|
|
345
|
+
scope.nameID = "A";
|
|
346
|
+
compile('<select ng-model="name" name="name{{nameID}}"></select>');
|
|
347
|
+
expect(scope.form.nameA.$name).toBe("nameA");
|
|
348
|
+
const oldModel = scope.form.nameA;
|
|
349
|
+
scope.nameID = "B";
|
|
350
|
+
scope.$digest();
|
|
351
|
+
expect(scope.form.nameA).toBeUndefined();
|
|
352
|
+
expect(scope.form.nameB).toBe(oldModel);
|
|
353
|
+
expect(scope.form.nameB.$name).toBe("nameB");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should select options in a group when there is a linebreak before an option", () => {
|
|
357
|
+
scope.mySelect = "B";
|
|
358
|
+
scope.$apply();
|
|
359
|
+
|
|
360
|
+
const select = jqLite(
|
|
361
|
+
'<select ng-model="mySelect">' +
|
|
362
|
+
'<optgroup label="first">' +
|
|
363
|
+
'<option value="A">A</option>' +
|
|
364
|
+
"</optgroup>" +
|
|
365
|
+
'<optgroup label="second">\n' +
|
|
366
|
+
'<option value="B">B</option>' +
|
|
367
|
+
"</optgroup> " +
|
|
368
|
+
"</select>",
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
$compile(select)(scope);
|
|
372
|
+
scope.$apply();
|
|
373
|
+
expect(select).toEqualSelectWithOptions({
|
|
374
|
+
first: ["A"],
|
|
375
|
+
second: [["B"]],
|
|
376
|
+
});
|
|
377
|
+
dealoc(select);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should only call selectCtrl.writeValue after a digest has occurred", () => {
|
|
381
|
+
scope.mySelect = "B";
|
|
382
|
+
scope.$apply();
|
|
383
|
+
|
|
384
|
+
const select = jqLite(
|
|
385
|
+
'<select spy-on-write-value ng-model="mySelect">' +
|
|
386
|
+
'<optgroup label="first">' +
|
|
387
|
+
'<option value="A">A</option>' +
|
|
388
|
+
"</optgroup>" +
|
|
389
|
+
'<optgroup label="second">\n' +
|
|
390
|
+
'<option value="B">B</option>' +
|
|
391
|
+
"</optgroup> " +
|
|
392
|
+
"</select>",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
$compile(select)(scope);
|
|
396
|
+
expect(selectCtrl.writeValue).not.toHaveBeenCalled();
|
|
397
|
+
|
|
398
|
+
scope.$digest();
|
|
399
|
+
expect(selectCtrl.writeValue).toHaveBeenCalled();
|
|
400
|
+
dealoc(select);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should remove the "selected" attribute from the previous option when the model changes', () => {
|
|
404
|
+
compile(
|
|
405
|
+
'<select name="select" ng-model="selected">' +
|
|
406
|
+
'<option value="">--empty--</option>' +
|
|
407
|
+
'<option value="a">A</option>' +
|
|
408
|
+
'<option value="b">B</option>' +
|
|
409
|
+
"</select>",
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
scope.$digest();
|
|
413
|
+
|
|
414
|
+
let options = element.find("option");
|
|
415
|
+
expect(options[0].selected).toBeTrue();
|
|
416
|
+
expect(options[1].selected).toBeFalse();
|
|
417
|
+
expect(options[2].selected).toBeFalse();
|
|
418
|
+
|
|
419
|
+
scope.selected = "a";
|
|
420
|
+
scope.$digest();
|
|
421
|
+
|
|
422
|
+
options = element.find("option");
|
|
423
|
+
expect(options.length).toBe(3);
|
|
424
|
+
expect(options[0].selected).toBeFalse();
|
|
425
|
+
expect(options[1].selected).toBeTrue();
|
|
426
|
+
expect(options[2].selected).toBeFalse();
|
|
427
|
+
|
|
428
|
+
scope.selected = "b";
|
|
429
|
+
scope.$digest();
|
|
430
|
+
|
|
431
|
+
options = element.find("option");
|
|
432
|
+
expect(options[0].selected).toBeFalse();
|
|
433
|
+
expect(options[1].selected).toBeFalse();
|
|
434
|
+
expect(options[2].selected).toBeTrue();
|
|
435
|
+
|
|
436
|
+
// This will select the empty option
|
|
437
|
+
scope.selected = null;
|
|
438
|
+
scope.$digest();
|
|
439
|
+
|
|
440
|
+
expect(options[0].selected).toBeTrue();
|
|
441
|
+
expect(options[1].selected).toBeFalse();
|
|
442
|
+
expect(options[2].selected).toBeFalse();
|
|
443
|
+
|
|
444
|
+
// This will add and select the unknown option
|
|
445
|
+
scope.selected = "unmatched value";
|
|
446
|
+
scope.$digest();
|
|
447
|
+
options = element.find("option");
|
|
448
|
+
|
|
449
|
+
expect(options[0].selected).toBeTrue();
|
|
450
|
+
expect(options[1].selected).toBeFalse();
|
|
451
|
+
expect(options[2].selected).toBeFalse();
|
|
452
|
+
expect(options[3].selected).toBeFalse();
|
|
453
|
+
|
|
454
|
+
// Back to matched value
|
|
455
|
+
scope.selected = "b";
|
|
456
|
+
scope.$digest();
|
|
457
|
+
options = element.find("option");
|
|
458
|
+
|
|
459
|
+
expect(options[0].selected).toBeFalse();
|
|
460
|
+
expect(options[1].selected).toBeFalse();
|
|
461
|
+
expect(options[2].selected).toBeTrue();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe("empty option", () => {
|
|
465
|
+
it("should allow empty option to be added and removed dynamically", () => {
|
|
466
|
+
scope.dynamicOptions = [];
|
|
467
|
+
scope.robot = "";
|
|
468
|
+
compile(
|
|
469
|
+
'<select ng-model="robot">' +
|
|
470
|
+
'<option ng-repeat="opt in dynamicOptions" value="{{opt.val}}">{{opt.display}}</option>' +
|
|
471
|
+
"</select>",
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(element[0].value).toBe("? string: ?");
|
|
475
|
+
|
|
476
|
+
scope.dynamicOptions = [
|
|
477
|
+
{ val: "", display: "--empty--" },
|
|
478
|
+
{ val: "x", display: "robot x" },
|
|
479
|
+
{ val: "y", display: "robot y" },
|
|
480
|
+
];
|
|
481
|
+
scope.$digest();
|
|
482
|
+
|
|
483
|
+
expect(element[0].value).toBe("");
|
|
484
|
+
|
|
485
|
+
scope.robot = "x";
|
|
486
|
+
scope.$digest();
|
|
487
|
+
expect(element[0].value).toBe("x");
|
|
488
|
+
scope.dynamicOptions.shift();
|
|
489
|
+
|
|
490
|
+
scope.$digest();
|
|
491
|
+
expect(element[0].value).toBe("x");
|
|
492
|
+
|
|
493
|
+
scope.robot = undefined;
|
|
494
|
+
scope.$digest();
|
|
495
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("should cope use a dynamic empty option that is added to a static empty option", () => {
|
|
499
|
+
// We do not make any special provisions for multiple empty options, so this behavior is
|
|
500
|
+
// largely untested
|
|
501
|
+
scope.dynamicOptions = [];
|
|
502
|
+
scope.robot = "x";
|
|
503
|
+
compile(
|
|
504
|
+
'<select ng-model="robot">' +
|
|
505
|
+
'<option value="">--static-select--</option>' +
|
|
506
|
+
'<option ng-repeat="opt in dynamicOptions" value="{{opt.val}}">{{opt.display}}</option>' +
|
|
507
|
+
"</select>",
|
|
508
|
+
);
|
|
509
|
+
scope.$digest();
|
|
510
|
+
expect(element[0].value).toBe(unknownValue("x"));
|
|
511
|
+
|
|
512
|
+
scope.robot = undefined;
|
|
513
|
+
scope.$digest();
|
|
514
|
+
expect(element.find("option").eq(0).prop("selected")).toBe(true);
|
|
515
|
+
expect(element.find("option").eq(0).text()).toBe("--static-select--");
|
|
516
|
+
|
|
517
|
+
scope.dynamicOptions = [
|
|
518
|
+
{ val: "", display: "--dynamic-select--" },
|
|
519
|
+
{ val: "x", display: "robot x" },
|
|
520
|
+
{ val: "y", display: "robot y" },
|
|
521
|
+
];
|
|
522
|
+
scope.$digest();
|
|
523
|
+
expect(element[0].value).toBe("");
|
|
524
|
+
|
|
525
|
+
scope.dynamicOptions = [];
|
|
526
|
+
scope.$digest();
|
|
527
|
+
expect(element[0].value).toBe("");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should select the empty option when model is undefined", () => {
|
|
531
|
+
compile(
|
|
532
|
+
'<select ng-model="robot">' +
|
|
533
|
+
'<option value="">--select--</option>' +
|
|
534
|
+
'<option value="x">robot x</option>' +
|
|
535
|
+
'<option value="y">robot y</option>' +
|
|
536
|
+
"</select>",
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(element[0].value).toBe("");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("should support defining an empty option anywhere in the option list", () => {
|
|
543
|
+
compile(
|
|
544
|
+
'<select ng-model="robot">' +
|
|
545
|
+
'<option value="x">robot x</option>' +
|
|
546
|
+
'<option value="">--select--</option>' +
|
|
547
|
+
'<option value="y">robot y</option>' +
|
|
548
|
+
"</select>",
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(element[0].value).toBe("");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("should set the model to empty string when empty option is selected", () => {
|
|
555
|
+
scope.robot = "x";
|
|
556
|
+
compile(
|
|
557
|
+
'<select ng-model="robot">' +
|
|
558
|
+
'<option value="">--select--</option>' +
|
|
559
|
+
'<option value="x">robot x</option>' +
|
|
560
|
+
'<option value="y">robot y</option>' +
|
|
561
|
+
"</select>",
|
|
562
|
+
);
|
|
563
|
+
expect(element[0].value).toBe("x");
|
|
564
|
+
setSelectValue(element, 0);
|
|
565
|
+
|
|
566
|
+
expect(element[0].value).toBe("");
|
|
567
|
+
expect(scope.robot).toBe("");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should remove unknown option when model is undefined", () => {
|
|
571
|
+
scope.robot = "other";
|
|
572
|
+
compile(
|
|
573
|
+
'<select ng-model="robot">' +
|
|
574
|
+
'<option value="">--select--</option>' +
|
|
575
|
+
'<option value="x">robot x</option>' +
|
|
576
|
+
'<option value="y">robot y</option>' +
|
|
577
|
+
"</select>",
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
expect(element[0].value).toBe(unknownValue("other"));
|
|
581
|
+
|
|
582
|
+
scope.robot = undefined;
|
|
583
|
+
scope.$digest();
|
|
584
|
+
|
|
585
|
+
expect(element[0].value).toBe("");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should support option without a value attribute", () => {
|
|
589
|
+
compile(
|
|
590
|
+
'<select ng-model="robot">' +
|
|
591
|
+
"<option>--select--</option>" +
|
|
592
|
+
'<option value="x">robot x</option>' +
|
|
593
|
+
'<option value="y">robot y</option>' +
|
|
594
|
+
"</select>",
|
|
595
|
+
);
|
|
596
|
+
expect(element[0].value).toBe("? undefined:undefined ?");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("should support option without a value with other HTML attributes", () => {
|
|
600
|
+
compile(
|
|
601
|
+
'<select ng-model="robot">' +
|
|
602
|
+
'<option data-foo="bar">--select--</option>' +
|
|
603
|
+
'<option value="x">robot x</option>' +
|
|
604
|
+
'<option value="y">robot y</option>' +
|
|
605
|
+
"</select>",
|
|
606
|
+
);
|
|
607
|
+
expect(element[0].value).toBe("? undefined:undefined ?");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe("interactions with repeated options", () => {
|
|
611
|
+
it("should select empty option when model is undefined", () => {
|
|
612
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
613
|
+
compile(
|
|
614
|
+
'<select ng-model="robot">' +
|
|
615
|
+
'<option value="">--select--</option>' +
|
|
616
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
617
|
+
"</select>",
|
|
618
|
+
);
|
|
619
|
+
expect(element[0].value).toBe("");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("should set model to empty string when selected", () => {
|
|
623
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
624
|
+
compile(
|
|
625
|
+
'<select ng-model="robot">' +
|
|
626
|
+
'<option value="">--select--</option>' +
|
|
627
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
628
|
+
"</select>",
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
setSelectValue(element, 1);
|
|
632
|
+
expect(element[0].value).toBe("c3p0");
|
|
633
|
+
|
|
634
|
+
expect(scope.robot).toBe("c3p0");
|
|
635
|
+
|
|
636
|
+
setSelectValue(element, 0);
|
|
637
|
+
expect(element[0].value).toBe("");
|
|
638
|
+
expect(scope.robot).toBe("");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("should not break if both the select and repeater models change at once", () => {
|
|
642
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
643
|
+
scope.robot = "c3p0";
|
|
644
|
+
compile(
|
|
645
|
+
'<select ng-model="robot">' +
|
|
646
|
+
'<option value="">--select--</option>' +
|
|
647
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
648
|
+
"</select>",
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(element[0].value).toBe("c3p0");
|
|
652
|
+
|
|
653
|
+
scope.$apply(() => {
|
|
654
|
+
scope.robots = ["wallee"];
|
|
655
|
+
scope.robot = "";
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
expect(element[0].value).toBe("");
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should add/remove the "selected" attribute when the empty option is selected/unselected', () => {
|
|
663
|
+
compile(
|
|
664
|
+
'<select name="select" ng-model="selected">' +
|
|
665
|
+
'<option value="">--select--</option>' +
|
|
666
|
+
'<option value="a">A</option>' +
|
|
667
|
+
'<option value="b">B</option>' +
|
|
668
|
+
"</select>",
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
scope.$digest();
|
|
672
|
+
|
|
673
|
+
let options = element.find("option");
|
|
674
|
+
expect(options.length).toBe(3);
|
|
675
|
+
expect(options[0].selected).toBeTrue();
|
|
676
|
+
expect(options[1].selected).toBeFalse();
|
|
677
|
+
expect(options[2].selected).toBeFalse();
|
|
678
|
+
|
|
679
|
+
scope.selected = "a";
|
|
680
|
+
scope.$digest();
|
|
681
|
+
|
|
682
|
+
options = element.find("option");
|
|
683
|
+
expect(options.length).toBe(3);
|
|
684
|
+
expect(options[0].selected).toBeFalse();
|
|
685
|
+
expect(options[1].selected).toBeTrue();
|
|
686
|
+
expect(options[2].selected).toBeFalse();
|
|
687
|
+
|
|
688
|
+
scope.selected = "no match";
|
|
689
|
+
scope.$digest();
|
|
690
|
+
|
|
691
|
+
options = element.find("option");
|
|
692
|
+
expect(options[0].selected).toBeTrue();
|
|
693
|
+
expect(options[1].selected).toBeFalse();
|
|
694
|
+
expect(options[2].selected).toBeFalse();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe("unknown option", () => {
|
|
699
|
+
it("should insert&select temporary unknown option when no options-model match", () => {
|
|
700
|
+
compile(
|
|
701
|
+
'<select ng-model="robot">' +
|
|
702
|
+
"<option>c3p0</option>" +
|
|
703
|
+
"<option>r2d2</option>" +
|
|
704
|
+
"</select>",
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
708
|
+
|
|
709
|
+
scope.$apply(() => {
|
|
710
|
+
scope.robot = "r2d2";
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
expect(element[0].value).toBe("r2d2");
|
|
714
|
+
|
|
715
|
+
scope.$apply(() => {
|
|
716
|
+
scope.robot = "wallee";
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
expect(element[0].value).toBe(unknownValue("wallee"));
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("should NOT insert temporary unknown option when model is undefined and empty options is present", () => {
|
|
723
|
+
compile(
|
|
724
|
+
'<select ng-model="robot">' +
|
|
725
|
+
'<option value="">--select--</option>' +
|
|
726
|
+
"<option>c3p0</option>" +
|
|
727
|
+
"<option>r2d2</option>" +
|
|
728
|
+
"</select>",
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
expect(element[0].value).toBe("");
|
|
732
|
+
expect(scope.robot).toBeUndefined();
|
|
733
|
+
|
|
734
|
+
scope.$apply(() => {
|
|
735
|
+
scope.robot = null;
|
|
736
|
+
});
|
|
737
|
+
expect(element[0].value).toBe("");
|
|
738
|
+
|
|
739
|
+
scope.$apply(() => {
|
|
740
|
+
scope.robot = "r2d2";
|
|
741
|
+
});
|
|
742
|
+
expect(element[0].value).toBe("r2d2");
|
|
743
|
+
|
|
744
|
+
scope.$apply(() => {
|
|
745
|
+
delete scope.robot;
|
|
746
|
+
});
|
|
747
|
+
expect(element[0].value).toBe("");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it(
|
|
751
|
+
"should insert&select temporary unknown option when no options-model match, empty " +
|
|
752
|
+
"option is present and model is defined",
|
|
753
|
+
() => {
|
|
754
|
+
scope.robot = "wallee";
|
|
755
|
+
compile(
|
|
756
|
+
'<select ng-model="robot">' +
|
|
757
|
+
'<option value="">--select--</option>' +
|
|
758
|
+
"<option>c3p0</option>" +
|
|
759
|
+
"<option>r2d2</option>" +
|
|
760
|
+
"</select>",
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
expect(element[0].value).toBe(unknownValue("wallee"));
|
|
764
|
+
|
|
765
|
+
scope.$apply(() => {
|
|
766
|
+
scope.robot = "r2d2";
|
|
767
|
+
});
|
|
768
|
+
expect(element[0].value).toBe("r2d2");
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
describe("interactions with repeated options", () => {
|
|
773
|
+
it("should work with repeated options", () => {
|
|
774
|
+
compile(
|
|
775
|
+
'<select ng-model="robot">' +
|
|
776
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
777
|
+
"</select>",
|
|
778
|
+
);
|
|
779
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
780
|
+
expect(scope.robot).toBeUndefined();
|
|
781
|
+
|
|
782
|
+
scope.$apply(() => {
|
|
783
|
+
scope.robot = "r2d2";
|
|
784
|
+
});
|
|
785
|
+
expect(element[0].value).toBe(unknownValue("r2d2"));
|
|
786
|
+
expect(scope.robot).toBe("r2d2");
|
|
787
|
+
|
|
788
|
+
scope.$apply(() => {
|
|
789
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
790
|
+
});
|
|
791
|
+
expect(element[0].value).toBe("r2d2");
|
|
792
|
+
expect(scope.robot).toBe("r2d2");
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it("should work with empty option and repeated options", () => {
|
|
796
|
+
compile(
|
|
797
|
+
'<select ng-model="robot">' +
|
|
798
|
+
'<option value="">--select--</option>' +
|
|
799
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
800
|
+
"</select>",
|
|
801
|
+
);
|
|
802
|
+
expect(element[0].value).toBe("");
|
|
803
|
+
expect(scope.robot).toBeUndefined();
|
|
804
|
+
|
|
805
|
+
scope.$apply(() => {
|
|
806
|
+
scope.robot = "r2d2";
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(element[0].value).toBe(unknownValue("r2d2"));
|
|
810
|
+
expect(scope.robot).toBe("r2d2");
|
|
811
|
+
|
|
812
|
+
scope.$apply(() => {
|
|
813
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
814
|
+
});
|
|
815
|
+
expect(element[0].value).toBe("r2d2");
|
|
816
|
+
expect(scope.robot).toBe("r2d2");
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("should insert unknown element when repeater shrinks and selected option is unavailable", () => {
|
|
820
|
+
scope.robots = ["c3p0", "r2d2"];
|
|
821
|
+
scope.robot = "r2d2";
|
|
822
|
+
compile(
|
|
823
|
+
'<select ng-model="robot">' +
|
|
824
|
+
'<option ng-repeat="r in robots">{{r}}</option>' +
|
|
825
|
+
"</select>",
|
|
826
|
+
);
|
|
827
|
+
expect(element[0].value).toBe("r2d2");
|
|
828
|
+
expect(scope.robot).toBe("r2d2");
|
|
829
|
+
|
|
830
|
+
scope.$apply(() => {
|
|
831
|
+
scope.robots.pop();
|
|
832
|
+
});
|
|
833
|
+
expect(element[0].value).toBe(unknownValue(null));
|
|
834
|
+
expect(scope.robot).toBe(null);
|
|
835
|
+
|
|
836
|
+
scope.$apply(() => {
|
|
837
|
+
scope.robots.unshift("r2d2");
|
|
838
|
+
});
|
|
839
|
+
expect(element[0].value).toBe(unknownValue(null));
|
|
840
|
+
expect(scope.robot).toBe(null);
|
|
841
|
+
|
|
842
|
+
scope.$apply(() => {
|
|
843
|
+
scope.robot = "r2d2";
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
expect(element[0].value).toBe("r2d2");
|
|
847
|
+
|
|
848
|
+
scope.$apply(() => {
|
|
849
|
+
delete scope.robots;
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
expect(element[0].value).toBe(unknownValue(null));
|
|
853
|
+
expect(scope.robot).toBe(null);
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it(
|
|
859
|
+
"should not break when adding options via a directive with `replace: true` " +
|
|
860
|
+
"and a structural directive in its template",
|
|
861
|
+
() => {
|
|
862
|
+
scope.options = [
|
|
863
|
+
{ value: "1", label: "Option 1" },
|
|
864
|
+
{ value: "2", label: "Option 2" },
|
|
865
|
+
{ value: "3", label: "Option 3" },
|
|
866
|
+
];
|
|
867
|
+
compile(
|
|
868
|
+
'<select ng-model="mySelect"><option my-options="options"></option></select>',
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
expect(element[0].value).toBe(unknownValue());
|
|
872
|
+
},
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
it("should not throw when removing the element and all its children", () => {
|
|
876
|
+
const template =
|
|
877
|
+
'<select ng-model="mySelect" ng-if="visible">' +
|
|
878
|
+
'<option value="">--- Select ---</option>' +
|
|
879
|
+
"</select>";
|
|
880
|
+
scope.visible = true;
|
|
881
|
+
|
|
882
|
+
compile(template);
|
|
883
|
+
|
|
884
|
+
// It should not throw when removing the element
|
|
885
|
+
scope.$apply("visible = false");
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe("selectController", () => {
|
|
890
|
+
it(
|
|
891
|
+
"should expose .$hasEmptyOption(), .$isEmptyOptionSelected(), " +
|
|
892
|
+
"and .$isUnknownOptionSelected()",
|
|
893
|
+
() => {
|
|
894
|
+
compile('<select ng-model="mySelect"></select>');
|
|
895
|
+
|
|
896
|
+
const selectCtrl = element.controller("select");
|
|
897
|
+
|
|
898
|
+
expect(selectCtrl.$hasEmptyOption).toEqual(jasmine.any(Function));
|
|
899
|
+
expect(selectCtrl.$isEmptyOptionSelected).toEqual(
|
|
900
|
+
jasmine.any(Function),
|
|
901
|
+
);
|
|
902
|
+
expect(selectCtrl.$isUnknownOptionSelected).toEqual(
|
|
903
|
+
jasmine.any(Function),
|
|
904
|
+
);
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
it("should reflect the status of empty and unknown option", () => {
|
|
909
|
+
scope.dynamicOptions = [];
|
|
910
|
+
scope.selected = "";
|
|
911
|
+
compile(
|
|
912
|
+
'<select ng-model="selected">' +
|
|
913
|
+
'<option ng-if="empty" value="">--no selection--</option>' +
|
|
914
|
+
'<option ng-repeat="opt in dynamicOptions" value="{{opt.val}}">{{opt.display}}</option>' +
|
|
915
|
+
"</select>",
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
const selectCtrl = element.controller("select");
|
|
919
|
+
|
|
920
|
+
expect(element[0].value).toBe("? string: ?");
|
|
921
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
|
922
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
923
|
+
|
|
924
|
+
scope.dynamicOptions = [
|
|
925
|
+
{ val: "x", display: "robot x" },
|
|
926
|
+
{ val: "y", display: "robot y" },
|
|
927
|
+
];
|
|
928
|
+
scope.empty = true;
|
|
929
|
+
|
|
930
|
+
scope.$digest();
|
|
931
|
+
expect(element[0].value).toBe("");
|
|
932
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
933
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
|
934
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
935
|
+
|
|
936
|
+
// empty -> selection
|
|
937
|
+
scope.$apply('selected = "x"');
|
|
938
|
+
expect(element[0].value).toBe("x");
|
|
939
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
940
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
941
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
942
|
+
|
|
943
|
+
// remove empty
|
|
944
|
+
scope.$apply("empty = false");
|
|
945
|
+
expect(element[0].value).toBe("x");
|
|
946
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
|
947
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
948
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
949
|
+
|
|
950
|
+
// selection -> unknown
|
|
951
|
+
scope.$apply('selected = "unmatched"');
|
|
952
|
+
expect(element[0].value).toBe(unknownValue("unmatched"));
|
|
953
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(false);
|
|
954
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
955
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
|
956
|
+
|
|
957
|
+
// add empty
|
|
958
|
+
scope.$apply("empty = true");
|
|
959
|
+
expect(element[0].value).toBe(unknownValue("unmatched"));
|
|
960
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
961
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
962
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
|
963
|
+
|
|
964
|
+
// unknown -> empty
|
|
965
|
+
scope.$apply("selected = null");
|
|
966
|
+
|
|
967
|
+
expect(element[0].value).toBe("");
|
|
968
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
969
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
|
970
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
971
|
+
|
|
972
|
+
// empty -> unknown
|
|
973
|
+
scope.$apply('selected = "unmatched"');
|
|
974
|
+
|
|
975
|
+
expect(element[0].value).toBe(unknownValue("unmatched"));
|
|
976
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
977
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
978
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
|
|
979
|
+
|
|
980
|
+
// unknown -> selection
|
|
981
|
+
scope.$apply('selected = "y"');
|
|
982
|
+
|
|
983
|
+
expect(element[0].value).toBe("y");
|
|
984
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
985
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
|
|
986
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
987
|
+
|
|
988
|
+
// selection -> empty
|
|
989
|
+
scope.$apply("selected = null");
|
|
990
|
+
|
|
991
|
+
expect(element[0].value).toBe("");
|
|
992
|
+
expect(selectCtrl.$hasEmptyOption()).toBe(true);
|
|
993
|
+
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
|
|
994
|
+
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
describe("selectController.hasOption", () => {
|
|
999
|
+
describe("flat options", () => {
|
|
1000
|
+
it("should return false for options shifted via ngRepeat", () => {
|
|
1001
|
+
scope.robots = [
|
|
1002
|
+
{ value: 1, label: "c3p0" },
|
|
1003
|
+
{ value: 2, label: "r2d2" },
|
|
1004
|
+
];
|
|
1005
|
+
|
|
1006
|
+
compileRepeatedOptions();
|
|
1007
|
+
|
|
1008
|
+
const selectCtrl = element.controller("select");
|
|
1009
|
+
|
|
1010
|
+
scope.$apply(() => {
|
|
1011
|
+
scope.robots.shift();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
expect(selectCtrl.hasOption("1")).toBe(false);
|
|
1015
|
+
expect(selectCtrl.hasOption("2")).toBe(true);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it("should return false for options popped via ngRepeat", () => {
|
|
1019
|
+
scope.robots = [
|
|
1020
|
+
{ value: 1, label: "c3p0" },
|
|
1021
|
+
{ value: 2, label: "r2d2" },
|
|
1022
|
+
];
|
|
1023
|
+
|
|
1024
|
+
compileRepeatedOptions();
|
|
1025
|
+
|
|
1026
|
+
const selectCtrl = element.controller("select");
|
|
1027
|
+
|
|
1028
|
+
scope.$apply(() => {
|
|
1029
|
+
scope.robots.pop();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
expect(selectCtrl.hasOption("1")).toBe(true);
|
|
1033
|
+
expect(selectCtrl.hasOption("2")).toBe(false);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it("should return true for options added via ngRepeat", () => {
|
|
1037
|
+
scope.robots = [{ value: 2, label: "r2d2" }];
|
|
1038
|
+
|
|
1039
|
+
compileRepeatedOptions();
|
|
1040
|
+
|
|
1041
|
+
const selectCtrl = element.controller("select");
|
|
1042
|
+
|
|
1043
|
+
scope.$apply(() => {
|
|
1044
|
+
scope.robots.unshift({ value: 1, label: "c3p0" });
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
expect(selectCtrl.hasOption("1")).toBe(true);
|
|
1048
|
+
expect(selectCtrl.hasOption("2")).toBe(true);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it("should keep all the options when changing the model", () => {
|
|
1052
|
+
compile(
|
|
1053
|
+
"<select ng-model=\"mySelect\"><option ng-repeat=\"o in ['A','B','C']\">{{o}}</option></select>",
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
const selectCtrl = element.controller("select");
|
|
1057
|
+
|
|
1058
|
+
scope.$apply(() => {
|
|
1059
|
+
scope.mySelect = "C";
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
expect(selectCtrl.hasOption("A")).toBe(true);
|
|
1063
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1064
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1065
|
+
expect(element).toEqualSelectWithOptions({ "": ["A", "B", ["C"]] });
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
describe("grouped options", () => {
|
|
1070
|
+
it("should be able to detect when elements move from a previous group", () => {
|
|
1071
|
+
scope.values = [{ name: "A" }];
|
|
1072
|
+
scope.groups = [
|
|
1073
|
+
{
|
|
1074
|
+
name: "first",
|
|
1075
|
+
values: [{ name: "B" }, { name: "C" }, { name: "D" }],
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: "second",
|
|
1079
|
+
values: [{ name: "E" }],
|
|
1080
|
+
},
|
|
1081
|
+
];
|
|
1082
|
+
|
|
1083
|
+
compileGroupedOptions();
|
|
1084
|
+
|
|
1085
|
+
const selectCtrl = element.controller("select");
|
|
1086
|
+
|
|
1087
|
+
scope.$apply(() => {
|
|
1088
|
+
const itemD = scope.groups[0].values.pop();
|
|
1089
|
+
scope.groups[1].values.unshift(itemD);
|
|
1090
|
+
scope.values.shift();
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
expect(selectCtrl.hasOption("A")).toBe(false);
|
|
1094
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1095
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1096
|
+
expect(selectCtrl.hasOption("D")).toBe(true);
|
|
1097
|
+
expect(selectCtrl.hasOption("E")).toBe(true);
|
|
1098
|
+
expect(element).toEqualSelectWithOptions({
|
|
1099
|
+
"": [[""]],
|
|
1100
|
+
first: ["B", "C"],
|
|
1101
|
+
second: ["D", "E"],
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("should be able to detect when elements move from a following group", () => {
|
|
1106
|
+
scope.values = [{ name: "A" }];
|
|
1107
|
+
scope.groups = [
|
|
1108
|
+
{
|
|
1109
|
+
name: "first",
|
|
1110
|
+
values: [{ name: "B" }, { name: "C" }],
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: "second",
|
|
1114
|
+
values: [{ name: "D" }, { name: "E" }],
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
|
|
1118
|
+
compileGroupedOptions();
|
|
1119
|
+
|
|
1120
|
+
const selectCtrl = element.controller("select");
|
|
1121
|
+
|
|
1122
|
+
scope.$apply(() => {
|
|
1123
|
+
const itemD = scope.groups[1].values.shift();
|
|
1124
|
+
scope.groups[0].values.push(itemD);
|
|
1125
|
+
scope.values.shift();
|
|
1126
|
+
});
|
|
1127
|
+
expect(selectCtrl.hasOption("A")).toBe(false);
|
|
1128
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1129
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1130
|
+
expect(selectCtrl.hasOption("D")).toBe(true);
|
|
1131
|
+
expect(selectCtrl.hasOption("E")).toBe(true);
|
|
1132
|
+
expect(element).toEqualSelectWithOptions({
|
|
1133
|
+
"": [[""]],
|
|
1134
|
+
first: ["B", "C", "D"],
|
|
1135
|
+
second: ["E"],
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it("should be able to detect when an element is replaced with an element from a previous group", () => {
|
|
1140
|
+
scope.values = [{ name: "A" }];
|
|
1141
|
+
scope.groups = [
|
|
1142
|
+
{
|
|
1143
|
+
name: "first",
|
|
1144
|
+
values: [{ name: "B" }, { name: "C" }, { name: "D" }],
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "second",
|
|
1148
|
+
values: [{ name: "E" }, { name: "F" }],
|
|
1149
|
+
},
|
|
1150
|
+
];
|
|
1151
|
+
|
|
1152
|
+
compileGroupedOptions();
|
|
1153
|
+
|
|
1154
|
+
const selectCtrl = element.controller("select");
|
|
1155
|
+
|
|
1156
|
+
scope.$apply(() => {
|
|
1157
|
+
const itemD = scope.groups[0].values.pop();
|
|
1158
|
+
scope.groups[1].values.unshift(itemD);
|
|
1159
|
+
scope.groups[1].values.pop();
|
|
1160
|
+
});
|
|
1161
|
+
expect(selectCtrl.hasOption("A")).toBe(true);
|
|
1162
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1163
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1164
|
+
expect(selectCtrl.hasOption("D")).toBe(true);
|
|
1165
|
+
expect(selectCtrl.hasOption("E")).toBe(true);
|
|
1166
|
+
expect(selectCtrl.hasOption("F")).toBe(false);
|
|
1167
|
+
expect(element).toEqualSelectWithOptions({
|
|
1168
|
+
"": [[""], "A"],
|
|
1169
|
+
first: ["B", "C"],
|
|
1170
|
+
second: ["D", "E"],
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it("should be able to detect when element is replaced with an element from a following group", () => {
|
|
1175
|
+
scope.values = [{ name: "A" }];
|
|
1176
|
+
scope.groups = [
|
|
1177
|
+
{
|
|
1178
|
+
name: "first",
|
|
1179
|
+
values: [{ name: "B" }, { name: "C" }],
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
name: "second",
|
|
1183
|
+
values: [{ name: "D" }, { name: "E" }],
|
|
1184
|
+
},
|
|
1185
|
+
];
|
|
1186
|
+
|
|
1187
|
+
compileGroupedOptions();
|
|
1188
|
+
|
|
1189
|
+
const selectCtrl = element.controller("select");
|
|
1190
|
+
|
|
1191
|
+
scope.$apply(() => {
|
|
1192
|
+
scope.groups[0].values.pop();
|
|
1193
|
+
const itemD = scope.groups[1].values.shift();
|
|
1194
|
+
scope.groups[0].values.push(itemD);
|
|
1195
|
+
});
|
|
1196
|
+
expect(selectCtrl.hasOption("A")).toBe(true);
|
|
1197
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1198
|
+
expect(selectCtrl.hasOption("C")).toBe(false);
|
|
1199
|
+
expect(selectCtrl.hasOption("D")).toBe(true);
|
|
1200
|
+
expect(selectCtrl.hasOption("E")).toBe(true);
|
|
1201
|
+
expect(element).toEqualSelectWithOptions({
|
|
1202
|
+
"": [[""], "A"],
|
|
1203
|
+
first: ["B", "D"],
|
|
1204
|
+
second: ["E"],
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it("should be able to detect when an element is removed", () => {
|
|
1209
|
+
scope.values = [{ name: "A" }];
|
|
1210
|
+
scope.groups = [
|
|
1211
|
+
{
|
|
1212
|
+
name: "first",
|
|
1213
|
+
values: [{ name: "B" }, { name: "C" }],
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
name: "second",
|
|
1217
|
+
values: [{ name: "D" }, { name: "E" }],
|
|
1218
|
+
},
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
compileGroupedOptions();
|
|
1222
|
+
|
|
1223
|
+
const selectCtrl = element.controller("select");
|
|
1224
|
+
|
|
1225
|
+
scope.$apply(() => {
|
|
1226
|
+
scope.groups[1].values.shift();
|
|
1227
|
+
});
|
|
1228
|
+
expect(selectCtrl.hasOption("A")).toBe(true);
|
|
1229
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1230
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1231
|
+
expect(selectCtrl.hasOption("D")).toBe(false);
|
|
1232
|
+
expect(selectCtrl.hasOption("E")).toBe(true);
|
|
1233
|
+
expect(element).toEqualSelectWithOptions({
|
|
1234
|
+
"": [[""], "A"],
|
|
1235
|
+
first: ["B", "C"],
|
|
1236
|
+
second: ["E"],
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
it("should be able to detect when a group is removed", () => {
|
|
1241
|
+
scope.values = [{ name: "A" }];
|
|
1242
|
+
scope.groups = [
|
|
1243
|
+
{
|
|
1244
|
+
name: "first",
|
|
1245
|
+
values: [{ name: "B" }, { name: "C" }],
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "second",
|
|
1249
|
+
values: [{ name: "D" }, { name: "E" }],
|
|
1250
|
+
},
|
|
1251
|
+
];
|
|
1252
|
+
|
|
1253
|
+
compileGroupedOptions();
|
|
1254
|
+
|
|
1255
|
+
const selectCtrl = element.controller("select");
|
|
1256
|
+
|
|
1257
|
+
scope.$apply(() => {
|
|
1258
|
+
scope.groups.pop();
|
|
1259
|
+
});
|
|
1260
|
+
expect(selectCtrl.hasOption("A")).toBe(true);
|
|
1261
|
+
expect(selectCtrl.hasOption("B")).toBe(true);
|
|
1262
|
+
expect(selectCtrl.hasOption("C")).toBe(true);
|
|
1263
|
+
expect(selectCtrl.hasOption("D")).toBe(false);
|
|
1264
|
+
expect(selectCtrl.hasOption("E")).toBe(false);
|
|
1265
|
+
expect(element).toEqualSelectWithOptions({
|
|
1266
|
+
"": [[""], "A"],
|
|
1267
|
+
first: ["B", "C"],
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
describe("select-multiple", () => {
|
|
1274
|
+
it('should support type="select-multiple"', () => {
|
|
1275
|
+
compile(
|
|
1276
|
+
'<select ng-model="selection" multiple>' +
|
|
1277
|
+
"<option>A</option>" +
|
|
1278
|
+
"<option>B</option>" +
|
|
1279
|
+
"</select>",
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
scope.$apply(() => {
|
|
1283
|
+
scope.selection = ["A"];
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
let optionElements = element.find("option");
|
|
1287
|
+
|
|
1288
|
+
expect(element[0].value).toBe("A");
|
|
1289
|
+
expect(optionElements[0].selected).toBeTrue();
|
|
1290
|
+
expect(optionElements[1].selected).toBeFalse();
|
|
1291
|
+
|
|
1292
|
+
scope.$apply(() => {
|
|
1293
|
+
scope.selection.push("B");
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
optionElements = element.find("option");
|
|
1297
|
+
|
|
1298
|
+
expect(element[0].value).toBe("A");
|
|
1299
|
+
expect(optionElements[0].selected).toBeTrue();
|
|
1300
|
+
expect(optionElements[1].selected).toBeTrue();
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it("should work with optgroups", () => {
|
|
1304
|
+
compile(
|
|
1305
|
+
'<select ng-model="selection" multiple>' +
|
|
1306
|
+
'<optgroup label="group1">' +
|
|
1307
|
+
"<option>A</option>" +
|
|
1308
|
+
"<option>B</option>" +
|
|
1309
|
+
"</optgroup>" +
|
|
1310
|
+
"</select>",
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
expect(element[0].value).toBe("");
|
|
1314
|
+
expect(scope.selection).toBeUndefined();
|
|
1315
|
+
|
|
1316
|
+
scope.$apply(() => {
|
|
1317
|
+
scope.selection = ["A"];
|
|
1318
|
+
});
|
|
1319
|
+
expect(element[0].value).toBe("A");
|
|
1320
|
+
|
|
1321
|
+
scope.$apply(() => {
|
|
1322
|
+
scope.selection.push("B");
|
|
1323
|
+
});
|
|
1324
|
+
expect(element[0].value).toBe("A");
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
it("should require", () => {
|
|
1328
|
+
compile(
|
|
1329
|
+
'<select name="select" ng-model="selection" multiple required>' +
|
|
1330
|
+
"<option>A</option>" +
|
|
1331
|
+
"<option>B</option>" +
|
|
1332
|
+
"</select>",
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
scope.$apply(() => {
|
|
1336
|
+
scope.selection = [];
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
expect(scope.form.select.$error.required).toBeTruthy();
|
|
1340
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
1341
|
+
expect(element[0].classList.contains("ng-pristine")).toBeTrue();
|
|
1342
|
+
|
|
1343
|
+
scope.$apply(() => {
|
|
1344
|
+
scope.selection = ["A"];
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
1348
|
+
expect(element[0].classList.contains("ng-pristine")).toBeTrue();
|
|
1349
|
+
|
|
1350
|
+
element[0].value = "B";
|
|
1351
|
+
browserTrigger(element, "change");
|
|
1352
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
1353
|
+
expect(element[0].classList.contains("ng-dirty")).toBeTrue();
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
describe("calls to $render", () => {
|
|
1357
|
+
let ngModelCtrl;
|
|
1358
|
+
|
|
1359
|
+
beforeEach(() => {
|
|
1360
|
+
compile(
|
|
1361
|
+
'<select name="select" ng-model="selection" multiple>' +
|
|
1362
|
+
"<option>A</option>" +
|
|
1363
|
+
"<option>B</option>" +
|
|
1364
|
+
"</select>",
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
ngModelCtrl = element.controller("ngModel");
|
|
1368
|
+
spyOn(ngModelCtrl, "$render").and.callThrough();
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
it("should call $render once when the reference to the viewValue changes", () => {
|
|
1372
|
+
scope.$apply(() => {
|
|
1373
|
+
scope.selection = ["A"];
|
|
1374
|
+
});
|
|
1375
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(1);
|
|
1376
|
+
|
|
1377
|
+
scope.$apply(() => {
|
|
1378
|
+
scope.selection = ["A", "B"];
|
|
1379
|
+
});
|
|
1380
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(2);
|
|
1381
|
+
|
|
1382
|
+
scope.$apply(() => {
|
|
1383
|
+
scope.selection = [];
|
|
1384
|
+
});
|
|
1385
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(3);
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it("should call $render once when the viewValue deep-changes", () => {
|
|
1389
|
+
scope.$apply(() => {
|
|
1390
|
+
scope.selection = ["A"];
|
|
1391
|
+
});
|
|
1392
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(1);
|
|
1393
|
+
|
|
1394
|
+
scope.$apply(() => {
|
|
1395
|
+
scope.selection.push("B");
|
|
1396
|
+
});
|
|
1397
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(2);
|
|
1398
|
+
|
|
1399
|
+
scope.$apply(() => {
|
|
1400
|
+
scope.selection.length = 0;
|
|
1401
|
+
});
|
|
1402
|
+
expect(ngModelCtrl.$render).toHaveBeenCalledTimes(3);
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
describe("option", () => {
|
|
1408
|
+
it("should populate a missing value attribute with the option text", () => {
|
|
1409
|
+
compile('<select ng-model="x"><option selected>abc</option></select>');
|
|
1410
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("should ignore the option text if the value attribute exists", () => {
|
|
1414
|
+
compile('<select ng-model="x"><option value="abc">xyz</option></select>');
|
|
1415
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
it("should set value even if self closing HTML", () => {
|
|
1419
|
+
scope.x = "hello";
|
|
1420
|
+
compile('<select ng-model="x"><option>hello</select>');
|
|
1421
|
+
expect(element[0].value).toBe("hello");
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
it("should add options with interpolated value attributes", () => {
|
|
1425
|
+
scope.option1 = "option1";
|
|
1426
|
+
scope.option2 = "option2";
|
|
1427
|
+
|
|
1428
|
+
compile(
|
|
1429
|
+
'<select ng-model="selected">' +
|
|
1430
|
+
'<option value="{{option1}}">Option 1</option>' +
|
|
1431
|
+
'<option value="{{option2}}">Option 2</option>' +
|
|
1432
|
+
"</select>",
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
scope.$digest();
|
|
1436
|
+
expect(scope.selected).toBeUndefined();
|
|
1437
|
+
|
|
1438
|
+
setSelectValue(element, 0);
|
|
1439
|
+
expect(scope.selected).toBe("option1");
|
|
1440
|
+
|
|
1441
|
+
scope.selected = "option2";
|
|
1442
|
+
scope.$digest();
|
|
1443
|
+
expect(element.find("option").eq(1).prop("selected")).toBe(true);
|
|
1444
|
+
expect(element.find("option").eq(1).text()).toBe("Option 2");
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
it("should update the option when the interpolated value attribute changes", () => {
|
|
1448
|
+
scope.option1 = "option1";
|
|
1449
|
+
scope.option2 = "";
|
|
1450
|
+
|
|
1451
|
+
compile(
|
|
1452
|
+
'<select ng-model="selected">' +
|
|
1453
|
+
'<option value="{{option1}}">Option 1</option>' +
|
|
1454
|
+
'<option value="{{option2}}">Option 2</option>' +
|
|
1455
|
+
"</select>",
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
const selectCtrl = element.controller("select");
|
|
1459
|
+
spyOn(selectCtrl, "removeOption").and.callThrough();
|
|
1460
|
+
|
|
1461
|
+
scope.$digest();
|
|
1462
|
+
expect(scope.selected).toBeUndefined();
|
|
1463
|
+
expect(selectCtrl.removeOption).not.toHaveBeenCalled();
|
|
1464
|
+
|
|
1465
|
+
// Change value of option2
|
|
1466
|
+
scope.option2 = "option2Changed";
|
|
1467
|
+
scope.selected = "option2Changed";
|
|
1468
|
+
scope.$digest();
|
|
1469
|
+
|
|
1470
|
+
expect(selectCtrl.removeOption).toHaveBeenCalledWith("");
|
|
1471
|
+
expect(element.find("option").eq(1).prop("selected")).toBe(true);
|
|
1472
|
+
expect(element.find("option").eq(1).text()).toBe("Option 2");
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it("should add options with interpolated text", () => {
|
|
1476
|
+
scope.option1 = "Option 1";
|
|
1477
|
+
scope.option2 = "Option 2";
|
|
1478
|
+
|
|
1479
|
+
compile(
|
|
1480
|
+
'<select ng-model="selected">' +
|
|
1481
|
+
"<option>{{option1}}</option>" +
|
|
1482
|
+
"<option>{{option2}}</option>" +
|
|
1483
|
+
"</select>",
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
scope.$digest();
|
|
1487
|
+
expect(scope.selected).toBeUndefined();
|
|
1488
|
+
|
|
1489
|
+
setSelectValue(element, 0);
|
|
1490
|
+
expect(scope.selected).toBe("Option 1");
|
|
1491
|
+
|
|
1492
|
+
scope.selected = "Option 2";
|
|
1493
|
+
scope.$digest();
|
|
1494
|
+
expect(element.find("option").eq(1).prop("selected")).toBe(true);
|
|
1495
|
+
expect(element.find("option").eq(1).text()).toBe("Option 2");
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
it("should update options when their interpolated text changes", () => {
|
|
1499
|
+
scope.option1 = "Option 1";
|
|
1500
|
+
scope.option2 = "";
|
|
1501
|
+
|
|
1502
|
+
compile(
|
|
1503
|
+
'<select ng-model="selected">' +
|
|
1504
|
+
"<option>{{option1}}</option>" +
|
|
1505
|
+
"<option>{{option2}}</option>" +
|
|
1506
|
+
"</select>",
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
const selectCtrl = element.controller("select");
|
|
1510
|
+
spyOn(selectCtrl, "removeOption").and.callThrough();
|
|
1511
|
+
|
|
1512
|
+
scope.$digest();
|
|
1513
|
+
expect(scope.selected).toBeUndefined();
|
|
1514
|
+
expect(selectCtrl.removeOption).not.toHaveBeenCalled();
|
|
1515
|
+
|
|
1516
|
+
// Change value of option2
|
|
1517
|
+
scope.option2 = "Option 2 Changed";
|
|
1518
|
+
scope.selected = "Option 2 Changed";
|
|
1519
|
+
scope.$digest();
|
|
1520
|
+
|
|
1521
|
+
expect(selectCtrl.removeOption).toHaveBeenCalledWith("");
|
|
1522
|
+
expect(element.find("option").eq(1).prop("selected")).toBe(true);
|
|
1523
|
+
expect(element.find("option").eq(1).text()).toBe("Option 2 Changed");
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("should not blow up when option directive is found inside of a datalist", () => {
|
|
1527
|
+
const element = $compile(
|
|
1528
|
+
"<div>" +
|
|
1529
|
+
"<datalist><option>some val</option></datalist>" +
|
|
1530
|
+
"<span>{{foo}}</span>" +
|
|
1531
|
+
"</div>",
|
|
1532
|
+
)($rootScope);
|
|
1533
|
+
|
|
1534
|
+
$rootScope.foo = "success";
|
|
1535
|
+
$rootScope.$digest();
|
|
1536
|
+
expect(element.find("span").text()).toBe("success");
|
|
1537
|
+
dealoc(element);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it('should throw an exception if an option value interpolates to "hasOwnProperty"', () => {
|
|
1541
|
+
scope.hasOwnPropertyOption = "hasOwnProperty";
|
|
1542
|
+
expect(() => {
|
|
1543
|
+
compile(
|
|
1544
|
+
'<select ng-model="x">' +
|
|
1545
|
+
"<option>{{hasOwnPropertyOption}}</option>" +
|
|
1546
|
+
"</select>",
|
|
1547
|
+
);
|
|
1548
|
+
}).toThrowError(/badname/);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
describe("with ngValue (and non-primitive values)", () => {
|
|
1552
|
+
[
|
|
1553
|
+
"string",
|
|
1554
|
+
undefined,
|
|
1555
|
+
1,
|
|
1556
|
+
true,
|
|
1557
|
+
null,
|
|
1558
|
+
{ prop: "value" },
|
|
1559
|
+
["a"],
|
|
1560
|
+
NaN,
|
|
1561
|
+
].forEach((prop) => {
|
|
1562
|
+
it("should set the option attribute and select it for value $prop", () => {
|
|
1563
|
+
scope.option1 = prop;
|
|
1564
|
+
scope.option2 = "red";
|
|
1565
|
+
scope.selected = "NOMATCH";
|
|
1566
|
+
|
|
1567
|
+
compile(
|
|
1568
|
+
'<select ng-model="selected">' +
|
|
1569
|
+
'<option ng-value="option1">{{option1}}</option>' +
|
|
1570
|
+
'<option ng-value="option2">{{option2}}</option>' +
|
|
1571
|
+
"</select>",
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
scope.$digest();
|
|
1575
|
+
expect(element.find("option").eq(0).val()).toBe("? string:NOMATCH ?");
|
|
1576
|
+
|
|
1577
|
+
scope.selected = prop;
|
|
1578
|
+
scope.$digest();
|
|
1579
|
+
|
|
1580
|
+
expect(element.find("option").eq(0).val()).toBe(hashKey(prop));
|
|
1581
|
+
|
|
1582
|
+
// Reset
|
|
1583
|
+
scope.selected = false;
|
|
1584
|
+
scope.$digest();
|
|
1585
|
+
|
|
1586
|
+
expect(element.find("option").eq(0).val()).toBe("? boolean:false ?");
|
|
1587
|
+
|
|
1588
|
+
setSelectValue(element, 0);
|
|
1589
|
+
if (isNumberNaN(prop)) {
|
|
1590
|
+
expect(scope.selected).toBeNaN();
|
|
1591
|
+
} else {
|
|
1592
|
+
expect(scope.selected).toBe(prop);
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
[
|
|
1598
|
+
"string",
|
|
1599
|
+
undefined,
|
|
1600
|
+
1,
|
|
1601
|
+
true,
|
|
1602
|
+
null,
|
|
1603
|
+
{ prop: "value" },
|
|
1604
|
+
["a"],
|
|
1605
|
+
NaN,
|
|
1606
|
+
].forEach((prop) => {
|
|
1607
|
+
it("should update the option attribute and select it for value $prop", () => {
|
|
1608
|
+
scope.option = prop;
|
|
1609
|
+
scope.option2 = "red";
|
|
1610
|
+
scope.selected = "NOMATCH";
|
|
1611
|
+
|
|
1612
|
+
compile(
|
|
1613
|
+
'<select ng-model="selected">' +
|
|
1614
|
+
'<option ng-value="option">{{option}}</option>' +
|
|
1615
|
+
'<option ng-value="option2">{{option2}}</option>' +
|
|
1616
|
+
"</select>",
|
|
1617
|
+
);
|
|
1618
|
+
|
|
1619
|
+
const selectController = element.controller("select");
|
|
1620
|
+
spyOn(selectController, "removeOption").and.callThrough();
|
|
1621
|
+
|
|
1622
|
+
scope.$digest();
|
|
1623
|
+
expect(selectController.removeOption).not.toHaveBeenCalled();
|
|
1624
|
+
expect(element.find("option").eq(0).val()).toBe("? string:NOMATCH ?");
|
|
1625
|
+
|
|
1626
|
+
scope.selected = prop;
|
|
1627
|
+
scope.$digest();
|
|
1628
|
+
|
|
1629
|
+
expect(element.find("option").eq(0).val()).toBe(hashKey(prop));
|
|
1630
|
+
expect(element[0].selectedIndex).toBe(0);
|
|
1631
|
+
|
|
1632
|
+
scope.option = "UPDATEDVALUE";
|
|
1633
|
+
scope.$digest();
|
|
1634
|
+
|
|
1635
|
+
expect(selectController.removeOption.calls.count()).toBe(1);
|
|
1636
|
+
|
|
1637
|
+
// Updating the option value currently does not update the select model
|
|
1638
|
+
if (isNumberNaN(prop)) {
|
|
1639
|
+
expect(selectController.removeOption.calls.argsFor(0)[0]).toBeNaN();
|
|
1640
|
+
} else {
|
|
1641
|
+
expect(selectController.removeOption.calls.argsFor(0)[0]).toBe(
|
|
1642
|
+
prop,
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
expect(scope.selected).toBe(null);
|
|
1647
|
+
expect(element[0].selectedIndex).toBe(0);
|
|
1648
|
+
expect(element.find("option").length).toBe(3);
|
|
1649
|
+
expect(element.find("option").eq(0).prop("selected")).toBe(true);
|
|
1650
|
+
expect(element.find("option").eq(0).val()).toBe(unknownValue(prop));
|
|
1651
|
+
expect(element.find("option").eq(1).prop("selected")).toBe(false);
|
|
1652
|
+
expect(element.find("option").eq(1).val()).toBe(
|
|
1653
|
+
"string:UPDATEDVALUE",
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
scope.selected = "UPDATEDVALUE";
|
|
1657
|
+
scope.$digest();
|
|
1658
|
+
|
|
1659
|
+
expect(element[0].selectedIndex).toBe(0);
|
|
1660
|
+
expect(element.find("option").eq(0).val()).toBe(
|
|
1661
|
+
"string:UPDATEDVALUE",
|
|
1662
|
+
);
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
it("should interact with custom attribute $observe and $set calls", () => {
|
|
1666
|
+
const log = [];
|
|
1667
|
+
let optionAttr;
|
|
1668
|
+
|
|
1669
|
+
compile(
|
|
1670
|
+
'<select ng-model="selected">' +
|
|
1671
|
+
'<option expose-attributes ng-value="option">{{option}}</option>' +
|
|
1672
|
+
"</select>",
|
|
1673
|
+
);
|
|
1674
|
+
|
|
1675
|
+
optionAttr = optionAttributesList[0];
|
|
1676
|
+
optionAttr.$observe("value", (newVal) => {
|
|
1677
|
+
log.push(newVal);
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
scope.option = "init";
|
|
1681
|
+
scope.$digest();
|
|
1682
|
+
|
|
1683
|
+
expect(log[0]).toBe("init");
|
|
1684
|
+
expect(element.find("option").eq(1).val()).toBe("string:init");
|
|
1685
|
+
|
|
1686
|
+
optionAttr.$set("value", "update");
|
|
1687
|
+
expect(log[1]).toBe("update");
|
|
1688
|
+
expect(element.find("option").eq(1).val()).toBe("string:update");
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it("should ignore the option text / value attribute if the ngValue attribute exists", () => {
|
|
1692
|
+
scope.ngvalue = "abc";
|
|
1693
|
+
scope.value = "def";
|
|
1694
|
+
scope.textvalue = "ghi";
|
|
1695
|
+
|
|
1696
|
+
compile(
|
|
1697
|
+
'<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>',
|
|
1698
|
+
);
|
|
1699
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
it("should ignore option text with multiple interpolations if the ngValue attribute exists", () => {
|
|
1703
|
+
scope.ngvalue = "abc";
|
|
1704
|
+
scope.textvalue = "def";
|
|
1705
|
+
scope.textvalue2 = "ghi";
|
|
1706
|
+
|
|
1707
|
+
compile(
|
|
1708
|
+
'<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>',
|
|
1709
|
+
);
|
|
1710
|
+
expect(element[0].value).toBe(unknownValue(undefined));
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
it("should select the first option if it is `undefined`", () => {
|
|
1714
|
+
scope.selected = undefined;
|
|
1715
|
+
|
|
1716
|
+
scope.option1 = undefined;
|
|
1717
|
+
scope.option2 = "red";
|
|
1718
|
+
|
|
1719
|
+
compile(
|
|
1720
|
+
'<select ng-model="selected">' +
|
|
1721
|
+
'<option ng-value="option1">{{option1}}</option>' +
|
|
1722
|
+
'<option ng-value="option2">{{option2}}</option>' +
|
|
1723
|
+
"</select>",
|
|
1724
|
+
);
|
|
1725
|
+
|
|
1726
|
+
expect(element[0].value).toBe("undefined:undefined");
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
describe("and select[multiple]", () => {
|
|
1730
|
+
it("should allow multiple selection", () => {
|
|
1731
|
+
scope.options = {
|
|
1732
|
+
a: "string",
|
|
1733
|
+
b: undefined,
|
|
1734
|
+
c: 1,
|
|
1735
|
+
d: true,
|
|
1736
|
+
e: null,
|
|
1737
|
+
f: NaN,
|
|
1738
|
+
};
|
|
1739
|
+
scope.selected = [];
|
|
1740
|
+
|
|
1741
|
+
compile(
|
|
1742
|
+
'<select multiple ng-model="selected">' +
|
|
1743
|
+
'<option ng-value="options.a">{{options.a}}</option>' +
|
|
1744
|
+
'<option ng-value="options.b">{{options.b}}</option>' +
|
|
1745
|
+
'<option ng-value="options.c">{{options.c}}</option>' +
|
|
1746
|
+
'<option ng-value="options.d">{{options.d}}</option>' +
|
|
1747
|
+
'<option ng-value="options.e">{{options.e}}</option>' +
|
|
1748
|
+
'<option ng-value="options.f">{{options.f}}</option>' +
|
|
1749
|
+
"</select>",
|
|
1750
|
+
);
|
|
1751
|
+
|
|
1752
|
+
scope.$digest();
|
|
1753
|
+
|
|
1754
|
+
expect(
|
|
1755
|
+
Object.values(element[0].childNodes)
|
|
1756
|
+
.map((x) => x.value)
|
|
1757
|
+
.join(""),
|
|
1758
|
+
).toBe(
|
|
1759
|
+
[
|
|
1760
|
+
"string:string",
|
|
1761
|
+
"undefined:undefined",
|
|
1762
|
+
"number:1",
|
|
1763
|
+
"boolean:true",
|
|
1764
|
+
"object:null",
|
|
1765
|
+
"number:NaN",
|
|
1766
|
+
].join(""),
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
scope.selected = ["string", 1];
|
|
1770
|
+
scope.$digest();
|
|
1771
|
+
|
|
1772
|
+
expect(element.find("option").eq(0).prop("selected")).toBe(true);
|
|
1773
|
+
expect(element.find("option").eq(2).prop("selected")).toBe(true);
|
|
1774
|
+
|
|
1775
|
+
setSelectValue(element, 1);
|
|
1776
|
+
expect(scope.selected).toEqual([undefined]);
|
|
1777
|
+
|
|
1778
|
+
// reset
|
|
1779
|
+
scope.selected = [];
|
|
1780
|
+
scope.$digest();
|
|
1781
|
+
|
|
1782
|
+
forEach(element.find("option"), (option) => {
|
|
1783
|
+
// browserTrigger can't produce click + ctrl, so set selection manually
|
|
1784
|
+
jqLite(option).prop("selected", true);
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
browserTrigger(element, "change");
|
|
1788
|
+
|
|
1789
|
+
const arrayVal = ["a"];
|
|
1790
|
+
arrayVal.$$hashKey = "object:4";
|
|
1791
|
+
|
|
1792
|
+
expect(scope.selected).toEqual([
|
|
1793
|
+
"string",
|
|
1794
|
+
undefined,
|
|
1795
|
+
1,
|
|
1796
|
+
true,
|
|
1797
|
+
null,
|
|
1798
|
+
NaN,
|
|
1799
|
+
]);
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
describe("updating the model and selection when option elements are manipulated", () => {
|
|
1805
|
+
["ngValue", "interpolatedValue", "interpolatedText"].forEach((prop) => {
|
|
1806
|
+
it("should set the model to null when the currently selected option with $prop is removed", () => {
|
|
1807
|
+
const A = { name: "A" };
|
|
1808
|
+
const B = { name: "B" };
|
|
1809
|
+
const C = { name: "C" };
|
|
1810
|
+
|
|
1811
|
+
scope.options = [A, B, C];
|
|
1812
|
+
scope.obj = {};
|
|
1813
|
+
|
|
1814
|
+
let optionString = "";
|
|
1815
|
+
|
|
1816
|
+
switch (prop) {
|
|
1817
|
+
case "ngValue":
|
|
1818
|
+
optionString =
|
|
1819
|
+
'<option ng-repeat="option in options" ng-value="option">{{$index}}</option>';
|
|
1820
|
+
break;
|
|
1821
|
+
case "interpolatedValue":
|
|
1822
|
+
optionString =
|
|
1823
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
1824
|
+
break;
|
|
1825
|
+
case "interpolatedText":
|
|
1826
|
+
optionString =
|
|
1827
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
1832
|
+
|
|
1833
|
+
let optionElements = element.find("option");
|
|
1834
|
+
expect(optionElements.length).toEqual(4);
|
|
1835
|
+
setSelectValue(optionElements, 0);
|
|
1836
|
+
|
|
1837
|
+
optionElements = element.find("option");
|
|
1838
|
+
expect(optionElements.length).toEqual(3);
|
|
1839
|
+
expect(scope.obj.value).toBe(prop === "ngValue" ? A : "A");
|
|
1840
|
+
|
|
1841
|
+
scope.options.shift();
|
|
1842
|
+
scope.$digest();
|
|
1843
|
+
|
|
1844
|
+
optionElements = element.find("option");
|
|
1845
|
+
expect(optionElements.length).toEqual(3);
|
|
1846
|
+
expect(scope.obj.value).toBe(null);
|
|
1847
|
+
expect(element.val()).toBe("? object:null ?");
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
it("should set the model to null when the currently selected option with $prop changes its value", () => {
|
|
1851
|
+
const A = { name: "A" };
|
|
1852
|
+
const B = { name: "B" };
|
|
1853
|
+
const C = { name: "C" };
|
|
1854
|
+
|
|
1855
|
+
scope.options = [A, B, C];
|
|
1856
|
+
scope.obj = {};
|
|
1857
|
+
|
|
1858
|
+
let optionString = "";
|
|
1859
|
+
|
|
1860
|
+
switch (prop) {
|
|
1861
|
+
case "ngValue":
|
|
1862
|
+
optionString =
|
|
1863
|
+
'<option ng-repeat="option in options" ng-value="option.name">{{$index}}</option>';
|
|
1864
|
+
break;
|
|
1865
|
+
case "interpolatedValue":
|
|
1866
|
+
optionString =
|
|
1867
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
1868
|
+
break;
|
|
1869
|
+
case "interpolatedText":
|
|
1870
|
+
optionString =
|
|
1871
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
1872
|
+
break;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
1876
|
+
|
|
1877
|
+
let optionElements = element.find("option");
|
|
1878
|
+
expect(optionElements.length).toEqual(4);
|
|
1879
|
+
setSelectValue(optionElements, 0);
|
|
1880
|
+
|
|
1881
|
+
optionElements = element.find("option");
|
|
1882
|
+
expect(optionElements.length).toEqual(3);
|
|
1883
|
+
expect(scope.obj.value).toBe("A");
|
|
1884
|
+
|
|
1885
|
+
A.name = "X";
|
|
1886
|
+
scope.$digest();
|
|
1887
|
+
|
|
1888
|
+
optionElements = element.find("option");
|
|
1889
|
+
expect(optionElements.length).toEqual(4);
|
|
1890
|
+
expect(scope.obj.value).toBe(null);
|
|
1891
|
+
expect(element.val()).toBe("? string:A ?");
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
it("should set the model to null when the currently selected option with $prop is disabled", () => {
|
|
1895
|
+
const A = { name: "A" };
|
|
1896
|
+
const B = { name: "B" };
|
|
1897
|
+
const C = { name: "C" };
|
|
1898
|
+
|
|
1899
|
+
scope.options = [A, B, C];
|
|
1900
|
+
scope.obj = {};
|
|
1901
|
+
|
|
1902
|
+
let optionString = "";
|
|
1903
|
+
|
|
1904
|
+
switch (prop) {
|
|
1905
|
+
case "ngValue":
|
|
1906
|
+
optionString =
|
|
1907
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" ng-value="option.name">{{$index}}</option>';
|
|
1908
|
+
break;
|
|
1909
|
+
case "interpolatedValue":
|
|
1910
|
+
optionString =
|
|
1911
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" value="{{option.name}}">{{$index}}</option>';
|
|
1912
|
+
break;
|
|
1913
|
+
case "interpolatedText":
|
|
1914
|
+
optionString =
|
|
1915
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled">{{option.name}}</option>';
|
|
1916
|
+
break;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
1920
|
+
|
|
1921
|
+
let optionElements = element.find("option");
|
|
1922
|
+
expect(optionElements.length).toEqual(4);
|
|
1923
|
+
setSelectValue(optionElements, 0);
|
|
1924
|
+
|
|
1925
|
+
optionElements = element.find("option");
|
|
1926
|
+
expect(optionElements.length).toEqual(3);
|
|
1927
|
+
expect(scope.obj.value).toBe("A");
|
|
1928
|
+
|
|
1929
|
+
A.disabled = true;
|
|
1930
|
+
scope.$digest();
|
|
1931
|
+
|
|
1932
|
+
optionElements = element.find("option");
|
|
1933
|
+
expect(optionElements.length).toEqual(4);
|
|
1934
|
+
expect(scope.obj.value).toBe(null);
|
|
1935
|
+
expect(element.val()).toBe("? object:null ?");
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
it("should select a disabled option with $prop when the model is set to the matching value", () => {
|
|
1939
|
+
const A = { name: "A" };
|
|
1940
|
+
const B = { name: "B" };
|
|
1941
|
+
const C = { name: "C" };
|
|
1942
|
+
|
|
1943
|
+
scope.options = [A, B, C];
|
|
1944
|
+
scope.obj = {};
|
|
1945
|
+
|
|
1946
|
+
let optionString = "";
|
|
1947
|
+
|
|
1948
|
+
switch (prop) {
|
|
1949
|
+
case "ngValue":
|
|
1950
|
+
optionString =
|
|
1951
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" ng-value="option.name">{{$index}}</option>';
|
|
1952
|
+
break;
|
|
1953
|
+
case "interpolatedValue":
|
|
1954
|
+
optionString =
|
|
1955
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" value="{{option.name}}">{{$index}}</option>';
|
|
1956
|
+
break;
|
|
1957
|
+
case "interpolatedText":
|
|
1958
|
+
optionString =
|
|
1959
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled">{{option.name}}</option>';
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
1964
|
+
|
|
1965
|
+
let optionElements = element.find("option");
|
|
1966
|
+
expect(optionElements.length).toEqual(4);
|
|
1967
|
+
expect(optionElements[0].value).toEqual(unknownValue(undefined));
|
|
1968
|
+
|
|
1969
|
+
B.disabled = true;
|
|
1970
|
+
scope.$digest();
|
|
1971
|
+
|
|
1972
|
+
optionElements = element.find("option");
|
|
1973
|
+
expect(optionElements.length).toEqual(4);
|
|
1974
|
+
expect(optionElements[0].value).toEqual(unknownValue(undefined));
|
|
1975
|
+
|
|
1976
|
+
scope.obj.value = "B";
|
|
1977
|
+
scope.$digest();
|
|
1978
|
+
|
|
1979
|
+
optionElements = element.find("option");
|
|
1980
|
+
expect(optionElements.length).toEqual(3);
|
|
1981
|
+
expect(scope.obj.value).toBe("B");
|
|
1982
|
+
// jQuery returns null for val() when the option is disabled, see
|
|
1983
|
+
// https://bugs.jquery.com/ticket/13097
|
|
1984
|
+
expect(element[0].value).toBe(prop === "ngValue" ? "string:B" : "B");
|
|
1985
|
+
expect(optionElements.eq(1).prop("selected")).toBe(true);
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
it("should ignore an option with $prop that becomes enabled and does not match the model", () => {
|
|
1989
|
+
const A = { name: "A" };
|
|
1990
|
+
const B = { name: "B" };
|
|
1991
|
+
const C = { name: "C" };
|
|
1992
|
+
|
|
1993
|
+
scope.options = [A, B, C];
|
|
1994
|
+
scope.obj = {};
|
|
1995
|
+
|
|
1996
|
+
let optionString = "";
|
|
1997
|
+
|
|
1998
|
+
switch (prop) {
|
|
1999
|
+
case "ngValue":
|
|
2000
|
+
optionString =
|
|
2001
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" ng-value="option.name">{{$index}}</option>';
|
|
2002
|
+
break;
|
|
2003
|
+
case "interpolatedValue":
|
|
2004
|
+
optionString =
|
|
2005
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" value="{{option.name}}">{{$index}}</option>';
|
|
2006
|
+
break;
|
|
2007
|
+
case "interpolatedText":
|
|
2008
|
+
optionString =
|
|
2009
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled">{{option.name}}</option>';
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
2014
|
+
|
|
2015
|
+
let optionElements = element.find("option");
|
|
2016
|
+
expect(optionElements.length).toEqual(4);
|
|
2017
|
+
setSelectValue(optionElements, 0);
|
|
2018
|
+
|
|
2019
|
+
optionElements = element.find("option");
|
|
2020
|
+
expect(optionElements.length).toEqual(3);
|
|
2021
|
+
expect(scope.obj.value).toBe("A");
|
|
2022
|
+
|
|
2023
|
+
A.disabled = true;
|
|
2024
|
+
scope.$digest();
|
|
2025
|
+
|
|
2026
|
+
optionElements = element.find("option");
|
|
2027
|
+
expect(optionElements.length).toEqual(4);
|
|
2028
|
+
expect(scope.obj.value).toBe(null);
|
|
2029
|
+
expect(element.val()).toBe("? object:null ?");
|
|
2030
|
+
|
|
2031
|
+
A.disabled = false;
|
|
2032
|
+
scope.$digest();
|
|
2033
|
+
|
|
2034
|
+
optionElements = element.find("option");
|
|
2035
|
+
expect(optionElements.length).toEqual(4);
|
|
2036
|
+
expect(scope.obj.value).toBe(null);
|
|
2037
|
+
expect(element.val()).toBe("? object:null ?");
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
it("should select a newly added option with $prop when it matches the current model", () => {
|
|
2041
|
+
const A = { name: "A" };
|
|
2042
|
+
const B = { name: "B" };
|
|
2043
|
+
const C = { name: "C" };
|
|
2044
|
+
|
|
2045
|
+
scope.options = [A, B];
|
|
2046
|
+
scope.obj = {
|
|
2047
|
+
value: prop === "ngValue" ? C : "C",
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
let optionString = "";
|
|
2051
|
+
|
|
2052
|
+
switch (prop) {
|
|
2053
|
+
case "ngValue":
|
|
2054
|
+
optionString =
|
|
2055
|
+
'<option ng-repeat="option in options" ng-value="option">{{$index}}</option>';
|
|
2056
|
+
break;
|
|
2057
|
+
case "interpolatedValue":
|
|
2058
|
+
optionString =
|
|
2059
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
2060
|
+
break;
|
|
2061
|
+
case "interpolatedText":
|
|
2062
|
+
optionString =
|
|
2063
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
2068
|
+
|
|
2069
|
+
let optionElements = element.find("option");
|
|
2070
|
+
expect(optionElements.length).toEqual(3);
|
|
2071
|
+
|
|
2072
|
+
scope.options.push(C);
|
|
2073
|
+
scope.$digest();
|
|
2074
|
+
|
|
2075
|
+
optionElements = element.find("option");
|
|
2076
|
+
expect(optionElements.length).toEqual(3);
|
|
2077
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
it("should keep selection and model when repeated options with track by are replaced with equal options", () => {
|
|
2081
|
+
const A = { name: "A" };
|
|
2082
|
+
const B = { name: "B" };
|
|
2083
|
+
const C = { name: "C" };
|
|
2084
|
+
|
|
2085
|
+
scope.options = [A, B, C];
|
|
2086
|
+
scope.obj = {
|
|
2087
|
+
value: "C",
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
let optionString = "";
|
|
2091
|
+
|
|
2092
|
+
switch (prop) {
|
|
2093
|
+
case "ngValue":
|
|
2094
|
+
optionString =
|
|
2095
|
+
'<option ng-repeat="option in options track by option.name" ng-value="option.name">{{$index}}</option>';
|
|
2096
|
+
break;
|
|
2097
|
+
case "interpolatedValue":
|
|
2098
|
+
optionString =
|
|
2099
|
+
'<option ng-repeat="option in options track by option.name" value="{{option.name}}">{{$index}}</option>';
|
|
2100
|
+
break;
|
|
2101
|
+
case "interpolatedText":
|
|
2102
|
+
optionString =
|
|
2103
|
+
'<option ng-repeat="option in options track by option.name">{{option.name}}</option>';
|
|
2104
|
+
break;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
compile(`<select ng-model="obj.value">${optionString}</select>`);
|
|
2108
|
+
|
|
2109
|
+
let optionElements = element.find("option");
|
|
2110
|
+
expect(optionElements.length).toEqual(3);
|
|
2111
|
+
|
|
2112
|
+
scope.obj.value = "C";
|
|
2113
|
+
scope.$digest();
|
|
2114
|
+
|
|
2115
|
+
optionElements = element.find("option");
|
|
2116
|
+
expect(element.val()).toBe(prop === "ngValue" ? "string:C" : "C");
|
|
2117
|
+
expect(optionElements.length).toEqual(3);
|
|
2118
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2119
|
+
expect(scope.obj.value).toBe("C");
|
|
2120
|
+
|
|
2121
|
+
scope.options = [{ name: "A" }, { name: "B" }, { name: "C" }];
|
|
2122
|
+
scope.$digest();
|
|
2123
|
+
|
|
2124
|
+
optionElements = element.find("option");
|
|
2125
|
+
expect(element.val()).toBe(prop === "ngValue" ? "string:C" : "C");
|
|
2126
|
+
expect(optionElements.length).toEqual(3);
|
|
2127
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2128
|
+
expect(scope.obj.value).toBe("C");
|
|
2129
|
+
});
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
describe("when multiple", () => {
|
|
2133
|
+
["ngValue", "interpolatedValue", "interpolatedText"].forEach((prop) => {
|
|
2134
|
+
it("should set the model to null when the currently selected option with $prop is removed", () => {
|
|
2135
|
+
const A = { name: "A" };
|
|
2136
|
+
const B = { name: "B" };
|
|
2137
|
+
const C = { name: "C" };
|
|
2138
|
+
|
|
2139
|
+
scope.options = [A, B, C];
|
|
2140
|
+
scope.obj = {};
|
|
2141
|
+
|
|
2142
|
+
let optionString = "";
|
|
2143
|
+
|
|
2144
|
+
switch (prop) {
|
|
2145
|
+
case "ngValue":
|
|
2146
|
+
optionString =
|
|
2147
|
+
'<option ng-repeat="option in options" ng-value="option">{{$index}}</option>';
|
|
2148
|
+
break;
|
|
2149
|
+
case "interpolatedValue":
|
|
2150
|
+
optionString =
|
|
2151
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
2152
|
+
break;
|
|
2153
|
+
case "interpolatedText":
|
|
2154
|
+
optionString =
|
|
2155
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
compile(
|
|
2160
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2161
|
+
);
|
|
2162
|
+
|
|
2163
|
+
const ngModelCtrl = element.controller("ngModel");
|
|
2164
|
+
const ngModelCtrlSpy = spyOn(
|
|
2165
|
+
ngModelCtrl,
|
|
2166
|
+
"$setViewValue",
|
|
2167
|
+
).and.callThrough();
|
|
2168
|
+
|
|
2169
|
+
let optionElements = element.find("option");
|
|
2170
|
+
expect(optionElements.length).toEqual(3);
|
|
2171
|
+
|
|
2172
|
+
optionElements.eq(0).prop("selected", true);
|
|
2173
|
+
optionElements.eq(2).prop("selected", true);
|
|
2174
|
+
browserTrigger(element);
|
|
2175
|
+
|
|
2176
|
+
optionElements = element.find("option");
|
|
2177
|
+
expect(optionElements.length).toEqual(3);
|
|
2178
|
+
|
|
2179
|
+
ngModelCtrlSpy.calls.reset();
|
|
2180
|
+
scope.options.shift();
|
|
2181
|
+
scope.options.pop();
|
|
2182
|
+
scope.$digest();
|
|
2183
|
+
|
|
2184
|
+
optionElements = element.find("option");
|
|
2185
|
+
expect(optionElements.length).toEqual(1);
|
|
2186
|
+
expect(scope.obj.value).toEqual([]);
|
|
2187
|
+
|
|
2188
|
+
// Cover both jQuery 3.x ([]) and 2.x (null) behavior.
|
|
2189
|
+
let val = element.val();
|
|
2190
|
+
if (val === null) {
|
|
2191
|
+
val = [];
|
|
2192
|
+
}
|
|
2193
|
+
expect(val).toEqual([]);
|
|
2194
|
+
|
|
2195
|
+
expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1);
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
it("should set the model to null when the currently selected option with $prop changes its value", () => {
|
|
2199
|
+
const A = { name: "A" };
|
|
2200
|
+
const B = { name: "B" };
|
|
2201
|
+
const C = { name: "C" };
|
|
2202
|
+
|
|
2203
|
+
scope.options = [A, B, C];
|
|
2204
|
+
scope.obj = {};
|
|
2205
|
+
|
|
2206
|
+
let optionString = "";
|
|
2207
|
+
|
|
2208
|
+
switch (prop) {
|
|
2209
|
+
case "ngValue":
|
|
2210
|
+
optionString =
|
|
2211
|
+
'<option ng-repeat="option in options" ng-value="option.name">{{$index}}</option>';
|
|
2212
|
+
break;
|
|
2213
|
+
case "interpolatedValue":
|
|
2214
|
+
optionString =
|
|
2215
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
2216
|
+
break;
|
|
2217
|
+
case "interpolatedText":
|
|
2218
|
+
optionString =
|
|
2219
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
compile(
|
|
2224
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
const ngModelCtrl = element.controller("ngModel");
|
|
2228
|
+
const ngModelCtrlSpy = spyOn(
|
|
2229
|
+
ngModelCtrl,
|
|
2230
|
+
"$setViewValue",
|
|
2231
|
+
).and.callThrough();
|
|
2232
|
+
|
|
2233
|
+
let optionElements = element.find("option");
|
|
2234
|
+
expect(optionElements.length).toEqual(3);
|
|
2235
|
+
|
|
2236
|
+
optionElements.eq(0).prop("selected", true);
|
|
2237
|
+
optionElements.eq(2).prop("selected", true);
|
|
2238
|
+
browserTrigger(element, "change");
|
|
2239
|
+
|
|
2240
|
+
optionElements = element.find("option");
|
|
2241
|
+
expect(optionElements.length).toEqual(3);
|
|
2242
|
+
expect(scope.obj.value).toEqual(["A", "C"]);
|
|
2243
|
+
|
|
2244
|
+
ngModelCtrlSpy.calls.reset();
|
|
2245
|
+
A.name = "X";
|
|
2246
|
+
C.name = "Z";
|
|
2247
|
+
scope.$digest();
|
|
2248
|
+
|
|
2249
|
+
optionElements = element.find("option");
|
|
2250
|
+
expect(optionElements.length).toEqual(3);
|
|
2251
|
+
expect(scope.obj.value).toEqual([]);
|
|
2252
|
+
|
|
2253
|
+
// Cover both jQuery 3.x ([]) and 2.x (null) behavior.
|
|
2254
|
+
let val = element.val();
|
|
2255
|
+
if (val === null) {
|
|
2256
|
+
val = [];
|
|
2257
|
+
}
|
|
2258
|
+
expect(val).toEqual([]);
|
|
2259
|
+
|
|
2260
|
+
expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1);
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
it("should set the model to null when the currently selected option with $prop becomes disabled", () => {
|
|
2264
|
+
const A = { name: "A" };
|
|
2265
|
+
const B = { name: "B" };
|
|
2266
|
+
const C = { name: "C" };
|
|
2267
|
+
const D = { name: "D" };
|
|
2268
|
+
|
|
2269
|
+
scope.options = [A, B, C, D];
|
|
2270
|
+
scope.obj = {};
|
|
2271
|
+
|
|
2272
|
+
let optionString = "";
|
|
2273
|
+
|
|
2274
|
+
switch (prop) {
|
|
2275
|
+
case "ngValue":
|
|
2276
|
+
optionString =
|
|
2277
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" ng-value="option.name">{{$index}}</option>';
|
|
2278
|
+
break;
|
|
2279
|
+
case "interpolatedValue":
|
|
2280
|
+
optionString =
|
|
2281
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" value="{{option.name}}">{{$index}}</option>';
|
|
2282
|
+
break;
|
|
2283
|
+
case "interpolatedText":
|
|
2284
|
+
optionString =
|
|
2285
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled">{{option.name}}</option>';
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
compile(
|
|
2290
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
const ngModelCtrl = element.controller("ngModel");
|
|
2294
|
+
const ngModelCtrlSpy = spyOn(
|
|
2295
|
+
ngModelCtrl,
|
|
2296
|
+
"$setViewValue",
|
|
2297
|
+
).and.callThrough();
|
|
2298
|
+
|
|
2299
|
+
let optionElements = element.find("option");
|
|
2300
|
+
expect(optionElements.length).toEqual(4);
|
|
2301
|
+
|
|
2302
|
+
optionElements.eq(0).prop("selected", true);
|
|
2303
|
+
optionElements.eq(2).prop("selected", true);
|
|
2304
|
+
optionElements.eq(3).prop("selected", true);
|
|
2305
|
+
browserTrigger(element, "change");
|
|
2306
|
+
|
|
2307
|
+
optionElements = element.find("option");
|
|
2308
|
+
expect(optionElements.length).toEqual(4);
|
|
2309
|
+
expect(scope.obj.value).toEqual(["A", "C", "D"]);
|
|
2310
|
+
|
|
2311
|
+
ngModelCtrlSpy.calls.reset();
|
|
2312
|
+
A.disabled = true;
|
|
2313
|
+
C.disabled = true;
|
|
2314
|
+
scope.$digest();
|
|
2315
|
+
|
|
2316
|
+
optionElements = element.find("option");
|
|
2317
|
+
expect(optionElements.length).toEqual(4);
|
|
2318
|
+
expect(scope.obj.value).toEqual(["D"]);
|
|
2319
|
+
expect(element.val()).toEqual(
|
|
2320
|
+
prop === "ngValue" ? ["string:D"] : ["D"],
|
|
2321
|
+
);
|
|
2322
|
+
expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1);
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
it("should select disabled options with $prop when the model is set to matching values", () => {
|
|
2326
|
+
const A = { name: "A" };
|
|
2327
|
+
const B = { name: "B" };
|
|
2328
|
+
const C = { name: "C" };
|
|
2329
|
+
const D = { name: "D" };
|
|
2330
|
+
|
|
2331
|
+
scope.options = [A, B, C, D];
|
|
2332
|
+
scope.obj = {};
|
|
2333
|
+
|
|
2334
|
+
let optionString = "";
|
|
2335
|
+
|
|
2336
|
+
switch (prop) {
|
|
2337
|
+
case "ngValue":
|
|
2338
|
+
optionString =
|
|
2339
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" ng-value="option">{{$index}}</option>';
|
|
2340
|
+
break;
|
|
2341
|
+
case "interpolatedValue":
|
|
2342
|
+
optionString =
|
|
2343
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled" value="{{option.name}}">{{$index}}</option>';
|
|
2344
|
+
break;
|
|
2345
|
+
case "interpolatedText":
|
|
2346
|
+
optionString =
|
|
2347
|
+
'<option ng-repeat="option in options" ng-disabled="option.disabled">{{option.name}}</option>';
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
compile(
|
|
2352
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
let optionElements = element.find("option");
|
|
2356
|
+
expect(optionElements.length).toEqual(4);
|
|
2357
|
+
expect(element[0].value).toBe("");
|
|
2358
|
+
|
|
2359
|
+
A.disabled = true;
|
|
2360
|
+
D.disabled = true;
|
|
2361
|
+
scope.$digest();
|
|
2362
|
+
|
|
2363
|
+
optionElements = element.find("option");
|
|
2364
|
+
expect(optionElements.length).toEqual(4);
|
|
2365
|
+
expect(element[0].value).toBe("");
|
|
2366
|
+
|
|
2367
|
+
scope.obj.value = prop === "ngValue" ? [A, C, D] : ["A", "C", "D"];
|
|
2368
|
+
scope.$digest();
|
|
2369
|
+
|
|
2370
|
+
optionElements = element.find("option");
|
|
2371
|
+
expect(optionElements.length).toEqual(4);
|
|
2372
|
+
expect(optionElements.eq(0).prop("selected")).toBe(true);
|
|
2373
|
+
expect(optionElements.eq(2).prop("selected")).toBe(true);
|
|
2374
|
+
expect(optionElements.eq(3).prop("selected")).toBe(true);
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
it("should select a newly added option with $prop when it matches the current model", () => {
|
|
2378
|
+
const A = { name: "A" };
|
|
2379
|
+
const B = { name: "B" };
|
|
2380
|
+
const C = { name: "C" };
|
|
2381
|
+
|
|
2382
|
+
scope.options = [A, B];
|
|
2383
|
+
scope.obj = {
|
|
2384
|
+
value: prop === "ngValue" ? [B, C] : ["B", "C"],
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
let optionString = "";
|
|
2388
|
+
|
|
2389
|
+
switch (prop) {
|
|
2390
|
+
case "ngValue":
|
|
2391
|
+
optionString =
|
|
2392
|
+
'<option ng-repeat="option in options" ng-value="option">{{$index}}</option>';
|
|
2393
|
+
break;
|
|
2394
|
+
case "interpolatedValue":
|
|
2395
|
+
optionString =
|
|
2396
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{$index}}</option>';
|
|
2397
|
+
break;
|
|
2398
|
+
case "interpolatedText":
|
|
2399
|
+
optionString =
|
|
2400
|
+
'<option ng-repeat="option in options">{{option.name}}</option>';
|
|
2401
|
+
break;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
compile(
|
|
2405
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2406
|
+
);
|
|
2407
|
+
|
|
2408
|
+
let optionElements = element.find("option");
|
|
2409
|
+
expect(optionElements.length).toEqual(2);
|
|
2410
|
+
expect(optionElements.eq(1).prop("selected")).toBe(true);
|
|
2411
|
+
|
|
2412
|
+
scope.options.push(C);
|
|
2413
|
+
scope.$digest();
|
|
2414
|
+
|
|
2415
|
+
optionElements = element.find("option");
|
|
2416
|
+
|
|
2417
|
+
expect(optionElements.length).toEqual(3);
|
|
2418
|
+
expect(optionElements[1].selected).toBe(true);
|
|
2419
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2420
|
+
});
|
|
2421
|
+
|
|
2422
|
+
it("should keep selection and model when a repeated options with track by are replaced with equal options", () => {
|
|
2423
|
+
const A = { name: "A" };
|
|
2424
|
+
const B = { name: "B" };
|
|
2425
|
+
const C = { name: "C" };
|
|
2426
|
+
|
|
2427
|
+
scope.options = [A, B, C];
|
|
2428
|
+
scope.obj = {
|
|
2429
|
+
value: "C",
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
let optionString = "";
|
|
2433
|
+
|
|
2434
|
+
switch (prop) {
|
|
2435
|
+
case "ngValue":
|
|
2436
|
+
optionString =
|
|
2437
|
+
'<option ng-repeat="option in options track by option.name" ng-value="option.name">{{$index}}</option>';
|
|
2438
|
+
break;
|
|
2439
|
+
case "interpolatedValue":
|
|
2440
|
+
optionString =
|
|
2441
|
+
'<option ng-repeat="option in options track by option.name" value="{{option.name}}">{{$index}}</option>';
|
|
2442
|
+
break;
|
|
2443
|
+
case "interpolatedText":
|
|
2444
|
+
optionString =
|
|
2445
|
+
'<option ng-repeat="option in options track by option.name">{{option.name}}</option>';
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
compile(
|
|
2450
|
+
`<select ng-model="obj.value" multiple>${optionString}</select>`,
|
|
2451
|
+
);
|
|
2452
|
+
|
|
2453
|
+
let optionElements = element.find("option");
|
|
2454
|
+
expect(optionElements.length).toEqual(3);
|
|
2455
|
+
|
|
2456
|
+
scope.obj.value = ["B", "C"];
|
|
2457
|
+
scope.$digest();
|
|
2458
|
+
|
|
2459
|
+
optionElements = element.find("option");
|
|
2460
|
+
|
|
2461
|
+
expect(optionElements.length).toEqual(3);
|
|
2462
|
+
expect(optionElements[1].selected).toBe(true);
|
|
2463
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2464
|
+
expect(scope.obj.value).toEqual(["B", "C"]);
|
|
2465
|
+
|
|
2466
|
+
scope.options = [{ name: "A" }, { name: "B" }, { name: "C" }];
|
|
2467
|
+
scope.$digest();
|
|
2468
|
+
|
|
2469
|
+
optionElements = element.find("option");
|
|
2470
|
+
|
|
2471
|
+
expect(optionElements.length).toEqual(3);
|
|
2472
|
+
expect(optionElements[1].selected).toBe(true);
|
|
2473
|
+
expect(optionElements[2].selected).toBe(true);
|
|
2474
|
+
expect(scope.obj.value).toEqual(["B", "C"]);
|
|
2475
|
+
});
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
it("should keep the ngModel value when the selected option is recreated by ngRepeat", () => {
|
|
2480
|
+
scope.options = [{ name: "A" }, { name: "B" }, { name: "C" }];
|
|
2481
|
+
scope.obj = {
|
|
2482
|
+
value: "B",
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2485
|
+
compile(
|
|
2486
|
+
'<select ng-model="obj.value">' +
|
|
2487
|
+
'<option ng-repeat="option in options" value="{{option.name}}">{{option.name}}</option>' +
|
|
2488
|
+
"</select>",
|
|
2489
|
+
);
|
|
2490
|
+
|
|
2491
|
+
let optionElements = element.find("option");
|
|
2492
|
+
expect(optionElements.length).toEqual(3);
|
|
2493
|
+
expect(optionElements[0].value).toBe("A");
|
|
2494
|
+
expect(optionElements[1].selected).toBeTrue();
|
|
2495
|
+
expect(scope.obj.value).toBe("B");
|
|
2496
|
+
|
|
2497
|
+
scope.$apply(() => {
|
|
2498
|
+
// Only when new objects are used, ngRepeat re-creates the element from scratch
|
|
2499
|
+
scope.options = [{ name: "B" }, { name: "C" }, { name: "D" }];
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
const previouslySelectedOptionElement = optionElements[1];
|
|
2503
|
+
optionElements = element.find("option");
|
|
2504
|
+
|
|
2505
|
+
expect(optionElements.length).toEqual(3);
|
|
2506
|
+
expect(optionElements[0].value).toBe("B");
|
|
2507
|
+
expect(optionElements[0].selected).toBeTrue();
|
|
2508
|
+
expect(scope.obj.value).toBe("B");
|
|
2509
|
+
// Ensure the assumption that the element is re-created is true
|
|
2510
|
+
expect(previouslySelectedOptionElement).not.toBe(optionElements[0]);
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
it("should validate when the options change", () => {
|
|
2514
|
+
scope.values = ["A", "B"];
|
|
2515
|
+
scope.selection = "A";
|
|
2516
|
+
|
|
2517
|
+
compile(
|
|
2518
|
+
'<select ng-model="selection" required>' +
|
|
2519
|
+
'<option value="">--select--</option>' +
|
|
2520
|
+
'<option ng-repeat="option in values" value="{{option}}">{{option}}</option>' +
|
|
2521
|
+
"</select>",
|
|
2522
|
+
);
|
|
2523
|
+
|
|
2524
|
+
expect(element[0].value).toBe("A");
|
|
2525
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
2526
|
+
expect(ngModelCtrl.$error.required).toBeFalsy();
|
|
2527
|
+
|
|
2528
|
+
scope.$apply(() => {
|
|
2529
|
+
// Only when new objects are used, ngRepeat re-creates the element from scratch
|
|
2530
|
+
scope.values = ["B", "C"];
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
expect(element[0].value).toBe("");
|
|
2534
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
2535
|
+
expect(ngModelCtrl.$error.required).toBeTruthy();
|
|
2536
|
+
// ngModel sets undefined for invalid values
|
|
2537
|
+
expect(scope.selection).toBeUndefined();
|
|
2538
|
+
});
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2541
|
+
});
|