@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,1905 @@
|
|
|
1
|
+
import { jqLite, dealoc } from "../../../src/jqLite";
|
|
2
|
+
import { publishExternalAPI } from "../../../src/public";
|
|
3
|
+
import { createInjector } from "../../../src/injector";
|
|
4
|
+
import { NgModelController } from "../../../src/directive/ngModel";
|
|
5
|
+
import { isDefined, valueFn, isObject } from "../../../src/core/utils";
|
|
6
|
+
import { browserTrigger } from "../../test-utils";
|
|
7
|
+
|
|
8
|
+
import { Angular } from "../../../src/loader";
|
|
9
|
+
|
|
10
|
+
describe("ngModel", () => {
|
|
11
|
+
let ctrl;
|
|
12
|
+
let scope;
|
|
13
|
+
let element;
|
|
14
|
+
let parentFormCtrl;
|
|
15
|
+
let $compile;
|
|
16
|
+
let injector;
|
|
17
|
+
let $rootScope;
|
|
18
|
+
let $q;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
publishExternalAPI().decorator("$exceptionHandler", function () {
|
|
22
|
+
return (exception, cause) => {
|
|
23
|
+
throw new Error(exception.message);
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
injector = createInjector(["ng"]);
|
|
27
|
+
$compile = injector.get("$compile");
|
|
28
|
+
|
|
29
|
+
const attrs = { name: "testAlias", ngModel: "value" };
|
|
30
|
+
|
|
31
|
+
parentFormCtrl = {
|
|
32
|
+
$$setPending: jasmine.createSpy("$$setPending"),
|
|
33
|
+
$setValidity: jasmine.createSpy("$setValidity"),
|
|
34
|
+
$setDirty: jasmine.createSpy("$setDirty"),
|
|
35
|
+
$$clearControlValidity: () => {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
element = jqLite("<form><input></form>");
|
|
39
|
+
let $controller = injector.get("$controller");
|
|
40
|
+
scope = injector.get("$rootScope");
|
|
41
|
+
$q = injector.get("$q");
|
|
42
|
+
$rootScope = scope;
|
|
43
|
+
ctrl = $controller(NgModelController, {
|
|
44
|
+
$scope: scope,
|
|
45
|
+
$element: element.find("input"),
|
|
46
|
+
$attrs: attrs,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Assign the mocked parentFormCtrl to the model controller
|
|
50
|
+
ctrl.$$parentForm = parentFormCtrl;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
dealoc(element);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("NgModelController", () => {
|
|
58
|
+
it("should init the properties", () => {
|
|
59
|
+
expect(ctrl.$untouched).toBe(true);
|
|
60
|
+
expect(ctrl.$touched).toBe(false);
|
|
61
|
+
expect(ctrl.$dirty).toBe(false);
|
|
62
|
+
expect(ctrl.$pristine).toBe(true);
|
|
63
|
+
expect(ctrl.$valid).toBe(true);
|
|
64
|
+
expect(ctrl.$invalid).toBe(false);
|
|
65
|
+
|
|
66
|
+
expect(ctrl.$viewValue).toBeDefined();
|
|
67
|
+
expect(ctrl.$modelValue).toBeDefined();
|
|
68
|
+
|
|
69
|
+
expect(ctrl.$formatters).toEqual([]);
|
|
70
|
+
expect(ctrl.$parsers).toEqual([]);
|
|
71
|
+
|
|
72
|
+
expect(ctrl.$name).toBe("testAlias");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("setValidity", () => {
|
|
76
|
+
function expectOneError() {
|
|
77
|
+
expect(ctrl.$error).toEqual({ someError: true });
|
|
78
|
+
expect(ctrl.$$success).toEqual({});
|
|
79
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function expectOneSuccess() {
|
|
83
|
+
expect(ctrl.$error).toEqual({});
|
|
84
|
+
expect(ctrl.$$success).toEqual({ someError: true });
|
|
85
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function expectOnePending() {
|
|
89
|
+
expect(ctrl.$error).toEqual({});
|
|
90
|
+
expect(ctrl.$$success).toEqual({});
|
|
91
|
+
expect(ctrl.$pending).toEqual({ someError: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function expectCleared() {
|
|
95
|
+
expect(ctrl.$error).toEqual({});
|
|
96
|
+
expect(ctrl.$$success).toEqual({});
|
|
97
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it("should propagate validity to the parent form", () => {
|
|
101
|
+
expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled();
|
|
102
|
+
ctrl.$setValidity("ERROR", false);
|
|
103
|
+
expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith(
|
|
104
|
+
"ERROR",
|
|
105
|
+
false,
|
|
106
|
+
ctrl,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should transition from states correctly", () => {
|
|
111
|
+
expectCleared();
|
|
112
|
+
|
|
113
|
+
ctrl.$setValidity("someError", false);
|
|
114
|
+
expectOneError();
|
|
115
|
+
|
|
116
|
+
ctrl.$setValidity("someError", undefined);
|
|
117
|
+
expectOnePending();
|
|
118
|
+
|
|
119
|
+
ctrl.$setValidity("someError", true);
|
|
120
|
+
expectOneSuccess();
|
|
121
|
+
|
|
122
|
+
ctrl.$setValidity("someError", null);
|
|
123
|
+
expectCleared();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should set valid/invalid with multiple errors", () => {
|
|
127
|
+
ctrl.$setValidity("first", false);
|
|
128
|
+
expect(ctrl.$valid).toBe(false);
|
|
129
|
+
expect(ctrl.$invalid).toBe(true);
|
|
130
|
+
|
|
131
|
+
ctrl.$setValidity("second", false);
|
|
132
|
+
expect(ctrl.$valid).toBe(false);
|
|
133
|
+
expect(ctrl.$invalid).toBe(true);
|
|
134
|
+
|
|
135
|
+
ctrl.$setValidity("third", undefined);
|
|
136
|
+
expect(ctrl.$valid).toBeUndefined();
|
|
137
|
+
expect(ctrl.$invalid).toBeUndefined();
|
|
138
|
+
|
|
139
|
+
ctrl.$setValidity("third", null);
|
|
140
|
+
expect(ctrl.$valid).toBe(false);
|
|
141
|
+
expect(ctrl.$invalid).toBe(true);
|
|
142
|
+
|
|
143
|
+
ctrl.$setValidity("second", true);
|
|
144
|
+
expect(ctrl.$valid).toBe(false);
|
|
145
|
+
expect(ctrl.$invalid).toBe(true);
|
|
146
|
+
|
|
147
|
+
ctrl.$setValidity("first", true);
|
|
148
|
+
expect(ctrl.$valid).toBe(true);
|
|
149
|
+
expect(ctrl.$invalid).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("setPristine", () => {
|
|
154
|
+
it("should set control to its pristine state", () => {
|
|
155
|
+
ctrl.$setViewValue("edit");
|
|
156
|
+
expect(ctrl.$dirty).toBe(true);
|
|
157
|
+
expect(ctrl.$pristine).toBe(false);
|
|
158
|
+
|
|
159
|
+
ctrl.$setPristine();
|
|
160
|
+
expect(ctrl.$dirty).toBe(false);
|
|
161
|
+
expect(ctrl.$pristine).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("setDirty", () => {
|
|
166
|
+
it("should set control to its dirty state", () => {
|
|
167
|
+
expect(ctrl.$pristine).toBe(true);
|
|
168
|
+
expect(ctrl.$dirty).toBe(false);
|
|
169
|
+
|
|
170
|
+
ctrl.$setDirty();
|
|
171
|
+
expect(ctrl.$pristine).toBe(false);
|
|
172
|
+
expect(ctrl.$dirty).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should set parent form to its dirty state", () => {
|
|
176
|
+
ctrl.$setDirty();
|
|
177
|
+
expect(parentFormCtrl.$setDirty).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("setUntouched", () => {
|
|
182
|
+
it("should set control to its untouched state", () => {
|
|
183
|
+
ctrl.$setTouched();
|
|
184
|
+
|
|
185
|
+
ctrl.$setUntouched();
|
|
186
|
+
expect(ctrl.$touched).toBe(false);
|
|
187
|
+
expect(ctrl.$untouched).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("setTouched", () => {
|
|
192
|
+
it("should set control to its touched state", () => {
|
|
193
|
+
ctrl.$setUntouched();
|
|
194
|
+
|
|
195
|
+
ctrl.$setTouched();
|
|
196
|
+
expect(ctrl.$touched).toBe(true);
|
|
197
|
+
expect(ctrl.$untouched).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("view -> model", () => {
|
|
202
|
+
it("should set the value to $viewValue", () => {
|
|
203
|
+
ctrl.$setViewValue("some-val");
|
|
204
|
+
expect(ctrl.$viewValue).toBe("some-val");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should pipeline all registered parsers and set result to $modelValue", () => {
|
|
208
|
+
const log = [];
|
|
209
|
+
|
|
210
|
+
ctrl.$parsers.push((value) => {
|
|
211
|
+
log.push(value);
|
|
212
|
+
return `${value}-a`;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
ctrl.$parsers.push((value) => {
|
|
216
|
+
log.push(value);
|
|
217
|
+
return `${value}-b`;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
ctrl.$setViewValue("init");
|
|
221
|
+
expect(log).toEqual(["init", "init-a"]);
|
|
222
|
+
expect(ctrl.$modelValue).toBe("init-a-b");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should fire viewChangeListeners when the value changes in the view (even if invalid)", () => {
|
|
226
|
+
const spy = jasmine.createSpy("viewChangeListener");
|
|
227
|
+
ctrl.$viewChangeListeners.push(spy);
|
|
228
|
+
ctrl.$setViewValue("val");
|
|
229
|
+
expect(spy).toHaveBeenCalled();
|
|
230
|
+
spy.calls.reset();
|
|
231
|
+
|
|
232
|
+
// invalid
|
|
233
|
+
ctrl.$parsers.push(() => undefined);
|
|
234
|
+
ctrl.$setViewValue("val2");
|
|
235
|
+
expect(spy).toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should reset the model when the view is invalid", () => {
|
|
239
|
+
ctrl.$setViewValue("aaaa");
|
|
240
|
+
expect(ctrl.$modelValue).toBe("aaaa");
|
|
241
|
+
|
|
242
|
+
// add a validator that will make any input invalid
|
|
243
|
+
ctrl.$parsers.push(() => undefined);
|
|
244
|
+
expect(ctrl.$modelValue).toBe("aaaa");
|
|
245
|
+
ctrl.$setViewValue("bbbb");
|
|
246
|
+
expect(ctrl.$modelValue).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should not reset the model when the view is invalid due to an external validator", () => {
|
|
250
|
+
ctrl.$setViewValue("aaaa");
|
|
251
|
+
expect(ctrl.$modelValue).toBe("aaaa");
|
|
252
|
+
|
|
253
|
+
ctrl.$setValidity("someExternalError", false);
|
|
254
|
+
ctrl.$setViewValue("bbbb");
|
|
255
|
+
expect(ctrl.$modelValue).toBe("bbbb");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should not reset the view when the view is invalid", () => {
|
|
259
|
+
// this test fails when the view changes the model and
|
|
260
|
+
// then the model listener in ngModel picks up the change and
|
|
261
|
+
// tries to update the view again.
|
|
262
|
+
|
|
263
|
+
// add a validator that will make any input invalid
|
|
264
|
+
ctrl.$parsers.push(() => undefined);
|
|
265
|
+
spyOn(ctrl, "$render");
|
|
266
|
+
|
|
267
|
+
// first digest
|
|
268
|
+
ctrl.$setViewValue("bbbb");
|
|
269
|
+
expect(ctrl.$modelValue).toBeUndefined();
|
|
270
|
+
expect(ctrl.$viewValue).toBe("bbbb");
|
|
271
|
+
expect(ctrl.$render).not.toHaveBeenCalled();
|
|
272
|
+
expect(scope.value).toBeUndefined();
|
|
273
|
+
|
|
274
|
+
// further digests
|
|
275
|
+
scope.$apply('value = "aaa"');
|
|
276
|
+
expect(ctrl.$viewValue).toBe("aaa");
|
|
277
|
+
ctrl.$render.calls.reset();
|
|
278
|
+
|
|
279
|
+
ctrl.$setViewValue("cccc");
|
|
280
|
+
expect(ctrl.$modelValue).toBeUndefined();
|
|
281
|
+
expect(ctrl.$viewValue).toBe("cccc");
|
|
282
|
+
expect(ctrl.$render).not.toHaveBeenCalled();
|
|
283
|
+
expect(scope.value).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should call parentForm.$setDirty only when pristine", () => {
|
|
287
|
+
ctrl.$setViewValue("");
|
|
288
|
+
expect(ctrl.$pristine).toBe(false);
|
|
289
|
+
expect(ctrl.$dirty).toBe(true);
|
|
290
|
+
expect(parentFormCtrl.$setDirty).toHaveBeenCalled();
|
|
291
|
+
|
|
292
|
+
parentFormCtrl.$setDirty.calls.reset();
|
|
293
|
+
ctrl.$setViewValue("");
|
|
294
|
+
expect(ctrl.$pristine).toBe(false);
|
|
295
|
+
expect(ctrl.$dirty).toBe(true);
|
|
296
|
+
expect(parentFormCtrl.$setDirty).not.toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should remove all other errors when any parser returns undefined", () => {
|
|
300
|
+
let a;
|
|
301
|
+
let b;
|
|
302
|
+
const val = function (val, x) {
|
|
303
|
+
return x ? val : x;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
ctrl.$parsers.push((v) => val(v, a));
|
|
307
|
+
ctrl.$parsers.push((v) => val(v, b));
|
|
308
|
+
|
|
309
|
+
ctrl.$validators.high = function (value) {
|
|
310
|
+
return !isDefined(value) || value > 5;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
ctrl.$validators.even = function (value) {
|
|
314
|
+
return !isDefined(value) || value % 2 === 0;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
a = b = true;
|
|
318
|
+
|
|
319
|
+
ctrl.$setViewValue("3");
|
|
320
|
+
expect(ctrl.$error).toEqual({ high: true, even: true });
|
|
321
|
+
|
|
322
|
+
ctrl.$setViewValue("10");
|
|
323
|
+
expect(ctrl.$error).toEqual({});
|
|
324
|
+
|
|
325
|
+
a = undefined;
|
|
326
|
+
|
|
327
|
+
ctrl.$setViewValue("12");
|
|
328
|
+
expect(ctrl.$error).toEqual({ parse: true });
|
|
329
|
+
|
|
330
|
+
a = true;
|
|
331
|
+
b = undefined;
|
|
332
|
+
|
|
333
|
+
ctrl.$setViewValue("14");
|
|
334
|
+
expect(ctrl.$error).toEqual({ parse: true });
|
|
335
|
+
|
|
336
|
+
a = undefined;
|
|
337
|
+
b = undefined;
|
|
338
|
+
|
|
339
|
+
ctrl.$setViewValue("16");
|
|
340
|
+
expect(ctrl.$error).toEqual({ parse: true });
|
|
341
|
+
|
|
342
|
+
a = b = false; // not undefined
|
|
343
|
+
|
|
344
|
+
ctrl.$setViewValue("2");
|
|
345
|
+
expect(ctrl.$error).toEqual({ high: true });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should not remove external validators when a parser failed", () => {
|
|
349
|
+
ctrl.$parsers.push((v) => undefined);
|
|
350
|
+
ctrl.$setValidity("externalError", false);
|
|
351
|
+
ctrl.$setViewValue("someValue");
|
|
352
|
+
expect(ctrl.$error).toEqual({ externalError: true, parse: true });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should remove all non-parse-related CSS classes from the form when a parser fails", () => {
|
|
356
|
+
const element = $compile(
|
|
357
|
+
'<form name="myForm">' +
|
|
358
|
+
'<input name="myControl" ng-model="value" >' +
|
|
359
|
+
"</form>",
|
|
360
|
+
)($rootScope);
|
|
361
|
+
const inputElm = element.find("input");
|
|
362
|
+
const ctrl = $rootScope.myForm.myControl;
|
|
363
|
+
|
|
364
|
+
let parserIsFailing = false;
|
|
365
|
+
ctrl.$parsers.push((value) => (parserIsFailing ? undefined : value));
|
|
366
|
+
|
|
367
|
+
ctrl.$validators.alwaysFail = function () {
|
|
368
|
+
return false;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
ctrl.$setViewValue("123");
|
|
372
|
+
scope.$digest();
|
|
373
|
+
|
|
374
|
+
expect(element[0].classList.contains("ng-valid-parse")).toBeTrue();
|
|
375
|
+
expect(element[0].classList.contains("ng-invalid-parse")).toBeFalse();
|
|
376
|
+
expect(
|
|
377
|
+
element[0].classList.contains("ng-invalid-always-fail"),
|
|
378
|
+
).toBeTrue();
|
|
379
|
+
|
|
380
|
+
parserIsFailing = true;
|
|
381
|
+
ctrl.$setViewValue("12345");
|
|
382
|
+
scope.$digest();
|
|
383
|
+
|
|
384
|
+
expect(element[0].classList.contains("ng-valid-parse")).toBeFalse();
|
|
385
|
+
expect(element[0].classList.contains("ng-invalid-parse")).toBeTrue();
|
|
386
|
+
expect(
|
|
387
|
+
element[0].classList.contains("ng-invalid-always-fail"),
|
|
388
|
+
).toBeFalse();
|
|
389
|
+
dealoc(element);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should set the ng-invalid-parse and ng-valid-parse CSS class when parsers fail and pass", () => {
|
|
393
|
+
let pass = true;
|
|
394
|
+
ctrl.$parsers.push((v) => (pass ? v : undefined));
|
|
395
|
+
|
|
396
|
+
const input = element.find("input");
|
|
397
|
+
|
|
398
|
+
ctrl.$setViewValue("1");
|
|
399
|
+
expect(input[0].classList.contains("ng-valid-parse")).toBeTrue();
|
|
400
|
+
expect(input[0].classList.contains("ng-invalid-parse")).toBeFalse();
|
|
401
|
+
|
|
402
|
+
pass = undefined;
|
|
403
|
+
|
|
404
|
+
ctrl.$setViewValue("2");
|
|
405
|
+
expect(input[0].classList.contains("ng-valid-parse")).toBeFalse();
|
|
406
|
+
expect(input[0].classList.contains("ng-invalid-parse")).toBeTrue();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should update the model after all async validators resolve", () => {
|
|
410
|
+
let defer;
|
|
411
|
+
ctrl.$asyncValidators.promiseValidator = function (value) {
|
|
412
|
+
defer = $q.defer();
|
|
413
|
+
return defer.promise;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// set view value on first digest
|
|
417
|
+
ctrl.$setViewValue("b");
|
|
418
|
+
|
|
419
|
+
expect(ctrl.$modelValue).toBeUndefined();
|
|
420
|
+
expect(scope.value).toBeUndefined();
|
|
421
|
+
|
|
422
|
+
defer.resolve();
|
|
423
|
+
scope.$digest();
|
|
424
|
+
|
|
425
|
+
expect(ctrl.$modelValue).toBe("b");
|
|
426
|
+
expect(scope.value).toBe("b");
|
|
427
|
+
|
|
428
|
+
// set view value on further digests
|
|
429
|
+
ctrl.$setViewValue("c");
|
|
430
|
+
|
|
431
|
+
expect(ctrl.$modelValue).toBe("b");
|
|
432
|
+
expect(scope.value).toBe("b");
|
|
433
|
+
|
|
434
|
+
defer.resolve();
|
|
435
|
+
scope.$digest();
|
|
436
|
+
|
|
437
|
+
expect(ctrl.$modelValue).toBe("c");
|
|
438
|
+
expect(scope.value).toBe("c");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("should not throw an error if the scope has been destroyed", () => {
|
|
442
|
+
scope.$destroy();
|
|
443
|
+
ctrl.$setViewValue("some-val");
|
|
444
|
+
expect(ctrl.$viewValue).toBe("some-val");
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("model -> view", () => {
|
|
449
|
+
it("should set the value to $modelValue", () => {
|
|
450
|
+
scope.$apply("value = 10");
|
|
451
|
+
expect(ctrl.$modelValue).toBe(10);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should pipeline all registered formatters in reversed order and set result to $viewValue", () => {
|
|
455
|
+
const log = [];
|
|
456
|
+
|
|
457
|
+
ctrl.$formatters.unshift((value) => {
|
|
458
|
+
log.push(value);
|
|
459
|
+
return value + 2;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
ctrl.$formatters.unshift((value) => {
|
|
463
|
+
log.push(value);
|
|
464
|
+
return `${value}`;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
scope.$apply("value = 3");
|
|
468
|
+
expect(log).toEqual([3, 5]);
|
|
469
|
+
expect(ctrl.$viewValue).toBe("5");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should $render only if value changed", () => {
|
|
473
|
+
spyOn(ctrl, "$render");
|
|
474
|
+
|
|
475
|
+
scope.$apply("value = 3");
|
|
476
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
477
|
+
ctrl.$render.calls.reset();
|
|
478
|
+
|
|
479
|
+
ctrl.$formatters.push(() => 3);
|
|
480
|
+
scope.$apply("value = 5");
|
|
481
|
+
expect(ctrl.$render).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("should clear the view even if invalid", () => {
|
|
485
|
+
spyOn(ctrl, "$render");
|
|
486
|
+
|
|
487
|
+
ctrl.$formatters.push(() => undefined);
|
|
488
|
+
scope.$apply("value = 5");
|
|
489
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should render immediately even if there are async validators", () => {
|
|
493
|
+
spyOn(ctrl, "$render");
|
|
494
|
+
ctrl.$asyncValidators.someValidator = function () {
|
|
495
|
+
return $q.defer().promise;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
scope.$apply("value = 5");
|
|
499
|
+
expect(ctrl.$viewValue).toBe(5);
|
|
500
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("should not rerender nor validate in case view value is not changed", () => {
|
|
504
|
+
ctrl.$formatters.push((value) => "nochange");
|
|
505
|
+
|
|
506
|
+
spyOn(ctrl, "$render");
|
|
507
|
+
ctrl.$validators.spyValidator = jasmine.createSpy("spyValidator");
|
|
508
|
+
scope.$apply('value = "first"');
|
|
509
|
+
scope.$apply('value = "second"');
|
|
510
|
+
expect(ctrl.$validators.spyValidator).toHaveBeenCalled();
|
|
511
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should always format the viewValue as a string for a blank input type when the value is present", () => {
|
|
515
|
+
const form = $compile(
|
|
516
|
+
'<form name="form"><input name="field" ng-model="val" /></form>',
|
|
517
|
+
)($rootScope);
|
|
518
|
+
|
|
519
|
+
$rootScope.val = 123;
|
|
520
|
+
$rootScope.$digest();
|
|
521
|
+
expect($rootScope.form.field.$viewValue).toBe("123");
|
|
522
|
+
|
|
523
|
+
$rootScope.val = null;
|
|
524
|
+
$rootScope.$digest();
|
|
525
|
+
expect($rootScope.form.field.$viewValue).toBe(null);
|
|
526
|
+
|
|
527
|
+
dealoc(form);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should always format the viewValue as a string for a `text` input type when the value is present", () => {
|
|
531
|
+
const form = $compile(
|
|
532
|
+
'<form name="form"><input type="text" name="field" ng-model="val" /></form>',
|
|
533
|
+
)($rootScope);
|
|
534
|
+
$rootScope.val = 123;
|
|
535
|
+
$rootScope.$digest();
|
|
536
|
+
expect($rootScope.form.field.$viewValue).toBe("123");
|
|
537
|
+
|
|
538
|
+
$rootScope.val = null;
|
|
539
|
+
$rootScope.$digest();
|
|
540
|
+
expect($rootScope.form.field.$viewValue).toBe(null);
|
|
541
|
+
|
|
542
|
+
dealoc(form);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("should always format the viewValue as a string for an `email` input type when the value is present", () => {
|
|
546
|
+
const form = $compile(
|
|
547
|
+
'<form name="form"><input type="email" name="field" ng-model="val" /></form>',
|
|
548
|
+
)($rootScope);
|
|
549
|
+
$rootScope.val = 123;
|
|
550
|
+
$rootScope.$digest();
|
|
551
|
+
expect($rootScope.form.field.$viewValue).toBe("123");
|
|
552
|
+
|
|
553
|
+
$rootScope.val = null;
|
|
554
|
+
$rootScope.$digest();
|
|
555
|
+
expect($rootScope.form.field.$viewValue).toBe(null);
|
|
556
|
+
|
|
557
|
+
dealoc(form);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("should always format the viewValue as a string for a `url` input type when the value is present", () => {
|
|
561
|
+
const form = $compile(
|
|
562
|
+
'<form name="form"><input type="url" name="field" ng-model="val" /></form>',
|
|
563
|
+
)($rootScope);
|
|
564
|
+
$rootScope.val = 123;
|
|
565
|
+
$rootScope.$digest();
|
|
566
|
+
expect($rootScope.form.field.$viewValue).toBe("123");
|
|
567
|
+
|
|
568
|
+
$rootScope.val = null;
|
|
569
|
+
$rootScope.$digest();
|
|
570
|
+
expect($rootScope.form.field.$viewValue).toBe(null);
|
|
571
|
+
|
|
572
|
+
dealoc(form);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("should set NaN as the $modelValue when an asyncValidator is present", () => {
|
|
576
|
+
ctrl.$asyncValidators.test = function () {
|
|
577
|
+
return $q((resolve, reject) => {
|
|
578
|
+
resolve();
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
scope.$apply("value = 10");
|
|
583
|
+
expect(ctrl.$modelValue).toBe(10);
|
|
584
|
+
|
|
585
|
+
expect(() => {
|
|
586
|
+
scope.$apply(() => {
|
|
587
|
+
scope.value = NaN;
|
|
588
|
+
});
|
|
589
|
+
}).not.toThrow();
|
|
590
|
+
|
|
591
|
+
expect(ctrl.$modelValue).toBeNaN();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("$processModelValue", () => {
|
|
595
|
+
// Emulate setting the model on the scope
|
|
596
|
+
function setModelValue(ctrl, value) {
|
|
597
|
+
ctrl.$modelValue = ctrl.$$rawModelValue = value;
|
|
598
|
+
ctrl.$$parserValid = undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
it("should run the model -> view pipeline", () => {
|
|
602
|
+
const log = [];
|
|
603
|
+
const input = ctrl.$$element;
|
|
604
|
+
|
|
605
|
+
ctrl.$formatters.unshift((value) => {
|
|
606
|
+
log.push(value);
|
|
607
|
+
return value + 2;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
ctrl.$formatters.unshift((value) => {
|
|
611
|
+
log.push(value);
|
|
612
|
+
return `${value}`;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
spyOn(ctrl, "$render");
|
|
616
|
+
|
|
617
|
+
setModelValue(ctrl, 3);
|
|
618
|
+
|
|
619
|
+
expect(ctrl.$modelValue).toBe(3);
|
|
620
|
+
|
|
621
|
+
ctrl.$processModelValue();
|
|
622
|
+
|
|
623
|
+
expect(ctrl.$modelValue).toBe(3);
|
|
624
|
+
expect(log).toEqual([3, 5]);
|
|
625
|
+
expect(ctrl.$viewValue).toBe("5");
|
|
626
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// TODO
|
|
630
|
+
// it("should add the validation and empty-state classes", () => {
|
|
631
|
+
// const input = $compile(
|
|
632
|
+
// '<input name="myControl" maxlength="1" ng-model="value" >',
|
|
633
|
+
// )($rootScope);
|
|
634
|
+
// $rootScope.$digest();
|
|
635
|
+
|
|
636
|
+
// spyOn($animate, "addClass");
|
|
637
|
+
// spyOn($animate, "removeClass");
|
|
638
|
+
|
|
639
|
+
// const ctrl = input.controller("ngModel");
|
|
640
|
+
|
|
641
|
+
// expect(input[0].classList.contains("ng-empty")).toBeTrue();
|
|
642
|
+
// expect(input[0].classList.contains("ng-valid")).toBeTrue();
|
|
643
|
+
|
|
644
|
+
// setModelValue(ctrl, 3);
|
|
645
|
+
// ctrl.$processModelValue();
|
|
646
|
+
|
|
647
|
+
// // $animate adds / removes classes in the $$postDigest, which
|
|
648
|
+
// // we cannot trigger with $digest, because that would set the model from the scope,
|
|
649
|
+
// // so we simply check if the functions have been called
|
|
650
|
+
// expect($animate.removeClass.calls.mostRecent().args[0][0]).toBe(
|
|
651
|
+
// input[0],
|
|
652
|
+
// );
|
|
653
|
+
// expect($animate.removeClass.calls.mostRecent().args[1]).toBe(
|
|
654
|
+
// "ng-empty",
|
|
655
|
+
// );
|
|
656
|
+
|
|
657
|
+
// expect($animate.addClass.calls.mostRecent().args[0][0]).toBe(
|
|
658
|
+
// input[0],
|
|
659
|
+
// );
|
|
660
|
+
// expect($animate.addClass.calls.mostRecent().args[1]).toBe(
|
|
661
|
+
// "ng-not-empty",
|
|
662
|
+
// );
|
|
663
|
+
|
|
664
|
+
// $animate.removeClass.calls.reset();
|
|
665
|
+
// $animate.addClass.calls.reset();
|
|
666
|
+
|
|
667
|
+
// setModelValue(ctrl, 35);
|
|
668
|
+
// ctrl.$processModelValue();
|
|
669
|
+
|
|
670
|
+
// expect($animate.addClass.calls.argsFor(1)[0][0]).toBe(input[0]);
|
|
671
|
+
// expect($animate.addClass.calls.argsFor(1)[1]).toBe("ng-invalid");
|
|
672
|
+
|
|
673
|
+
// expect($animate.addClass.calls.argsFor(2)[0][0]).toBe(input[0]);
|
|
674
|
+
// expect($animate.addClass.calls.argsFor(2)[1]).toBe(
|
|
675
|
+
// "ng-invalid-maxlength",
|
|
676
|
+
// );
|
|
677
|
+
// });
|
|
678
|
+
|
|
679
|
+
// this is analogue to $setViewValue
|
|
680
|
+
it("should run the model -> view pipeline even if the value has not changed", () => {
|
|
681
|
+
const log = [];
|
|
682
|
+
|
|
683
|
+
ctrl.$formatters.unshift((value) => {
|
|
684
|
+
log.push(value);
|
|
685
|
+
return value + 2;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
ctrl.$formatters.unshift((value) => {
|
|
689
|
+
log.push(value);
|
|
690
|
+
return `${value}`;
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
spyOn(ctrl, "$render");
|
|
694
|
+
|
|
695
|
+
setModelValue(ctrl, 3);
|
|
696
|
+
ctrl.$processModelValue();
|
|
697
|
+
|
|
698
|
+
expect(ctrl.$modelValue).toBe(3);
|
|
699
|
+
expect(ctrl.$viewValue).toBe("5");
|
|
700
|
+
expect(log).toEqual([3, 5]);
|
|
701
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
702
|
+
|
|
703
|
+
ctrl.$processModelValue();
|
|
704
|
+
expect(ctrl.$modelValue).toBe(3);
|
|
705
|
+
expect(ctrl.$viewValue).toBe("5");
|
|
706
|
+
expect(log).toEqual([3, 5, 3, 5]);
|
|
707
|
+
// $render() is not called if the viewValue didn't change
|
|
708
|
+
expect(ctrl.$render).toHaveBeenCalled();
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe("validation", () => {
|
|
714
|
+
describe("$validate", () => {
|
|
715
|
+
it("should perform validations when $validate() is called", () => {
|
|
716
|
+
scope.$apply('value = ""');
|
|
717
|
+
|
|
718
|
+
let validatorResult = false;
|
|
719
|
+
ctrl.$validators.someValidator = function (value) {
|
|
720
|
+
return validatorResult;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
ctrl.$validate();
|
|
724
|
+
|
|
725
|
+
expect(ctrl.$valid).toBe(false);
|
|
726
|
+
|
|
727
|
+
validatorResult = true;
|
|
728
|
+
ctrl.$validate();
|
|
729
|
+
|
|
730
|
+
expect(ctrl.$valid).toBe(true);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("should pass the last parsed modelValue to the validators", () => {
|
|
734
|
+
ctrl.$parsers.push((modelValue) => `${modelValue}def`);
|
|
735
|
+
|
|
736
|
+
ctrl.$setViewValue("abc");
|
|
737
|
+
|
|
738
|
+
ctrl.$validators.test = function (modelValue, viewValue) {
|
|
739
|
+
return true;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
spyOn(ctrl.$validators, "test");
|
|
743
|
+
|
|
744
|
+
ctrl.$validate();
|
|
745
|
+
|
|
746
|
+
expect(ctrl.$validators.test).toHaveBeenCalledWith("abcdef", "abc");
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("should set the model to undefined when it becomes invalid", () => {
|
|
750
|
+
let valid = true;
|
|
751
|
+
ctrl.$validators.test = function (modelValue, viewValue) {
|
|
752
|
+
return valid;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
scope.$apply('value = "abc"');
|
|
756
|
+
expect(scope.value).toBe("abc");
|
|
757
|
+
|
|
758
|
+
valid = false;
|
|
759
|
+
ctrl.$validate();
|
|
760
|
+
|
|
761
|
+
expect(scope.value).toBeUndefined();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("should update the model when it becomes valid", () => {
|
|
765
|
+
let valid = true;
|
|
766
|
+
ctrl.$validators.test = function (modelValue, viewValue) {
|
|
767
|
+
return valid;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
scope.$apply('value = "abc"');
|
|
771
|
+
expect(scope.value).toBe("abc");
|
|
772
|
+
|
|
773
|
+
valid = false;
|
|
774
|
+
ctrl.$validate();
|
|
775
|
+
expect(scope.value).toBeUndefined();
|
|
776
|
+
|
|
777
|
+
valid = true;
|
|
778
|
+
ctrl.$validate();
|
|
779
|
+
expect(scope.value).toBe("abc");
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("should not update the model when it is valid, but there is a parse error", () => {
|
|
783
|
+
ctrl.$parsers.push((modelValue) => undefined);
|
|
784
|
+
|
|
785
|
+
ctrl.$setViewValue("abc");
|
|
786
|
+
expect(ctrl.$error.parse).toBe(true);
|
|
787
|
+
expect(scope.value).toBeUndefined();
|
|
788
|
+
|
|
789
|
+
ctrl.$validators.test = function (modelValue, viewValue) {
|
|
790
|
+
return true;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
ctrl.$validate();
|
|
794
|
+
expect(ctrl.$error).toEqual({ parse: true });
|
|
795
|
+
expect(scope.value).toBeUndefined();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("should not set an invalid model to undefined when validity is the same", () => {
|
|
799
|
+
ctrl.$validators.test = function () {
|
|
800
|
+
return false;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
scope.$apply('value = "invalid"');
|
|
804
|
+
expect(ctrl.$valid).toBe(false);
|
|
805
|
+
expect(scope.value).toBe("invalid");
|
|
806
|
+
|
|
807
|
+
ctrl.$validate();
|
|
808
|
+
expect(ctrl.$valid).toBe(false);
|
|
809
|
+
expect(scope.value).toBe("invalid");
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("should not change a model that has a formatter", () => {
|
|
813
|
+
ctrl.$validators.test = function () {
|
|
814
|
+
return true;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
ctrl.$formatters.push((modelValue) => "xyz");
|
|
818
|
+
|
|
819
|
+
scope.$apply('value = "abc"');
|
|
820
|
+
expect(ctrl.$viewValue).toBe("xyz");
|
|
821
|
+
|
|
822
|
+
ctrl.$validate();
|
|
823
|
+
expect(scope.value).toBe("abc");
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("should not change a model that has a parser", () => {
|
|
827
|
+
ctrl.$validators.test = function () {
|
|
828
|
+
return true;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
ctrl.$parsers.push((modelValue) => "xyz");
|
|
832
|
+
|
|
833
|
+
scope.$apply('value = "abc"');
|
|
834
|
+
|
|
835
|
+
ctrl.$validate();
|
|
836
|
+
expect(scope.value).toBe("abc");
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
describe("view -> model update", () => {
|
|
841
|
+
it("should always perform validations using the parsed model value", () => {
|
|
842
|
+
let captures;
|
|
843
|
+
ctrl.$validators.raw = function () {
|
|
844
|
+
captures = Array.prototype.slice.call(arguments);
|
|
845
|
+
return captures[0];
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
ctrl.$parsers.push((value) => value.toUpperCase());
|
|
849
|
+
|
|
850
|
+
ctrl.$setViewValue("my-value");
|
|
851
|
+
|
|
852
|
+
expect(captures).toEqual(["MY-VALUE", "my-value"]);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("should always perform validations using the formatted view value", () => {
|
|
856
|
+
let captures;
|
|
857
|
+
ctrl.$validators.raw = function () {
|
|
858
|
+
captures = Array.prototype.slice.call(arguments);
|
|
859
|
+
return captures[0];
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
ctrl.$formatters.push((value) => `${value}...`);
|
|
863
|
+
|
|
864
|
+
scope.$apply('value = "matias"');
|
|
865
|
+
|
|
866
|
+
expect(captures).toEqual(["matias", "matias..."]);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("should only perform validations if the view value is different", () => {
|
|
870
|
+
let count = 0;
|
|
871
|
+
ctrl.$validators.countMe = function () {
|
|
872
|
+
count++;
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
ctrl.$setViewValue("my-value");
|
|
876
|
+
expect(count).toBe(1);
|
|
877
|
+
|
|
878
|
+
ctrl.$setViewValue("my-value");
|
|
879
|
+
expect(count).toBe(1);
|
|
880
|
+
|
|
881
|
+
ctrl.$setViewValue("your-value");
|
|
882
|
+
expect(count).toBe(2);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("should perform validations twice each time the model value changes within a digest", () => {
|
|
887
|
+
let count = 0;
|
|
888
|
+
ctrl.$validators.number = function (value) {
|
|
889
|
+
count++;
|
|
890
|
+
return /^\d+$/.test(value);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
scope.$apply('value = ""');
|
|
894
|
+
expect(count).toBe(1);
|
|
895
|
+
|
|
896
|
+
scope.$apply("value = 1");
|
|
897
|
+
expect(count).toBe(2);
|
|
898
|
+
|
|
899
|
+
scope.$apply("value = 1");
|
|
900
|
+
expect(count).toBe(2);
|
|
901
|
+
|
|
902
|
+
scope.$apply('value = ""');
|
|
903
|
+
expect(count).toBe(3);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("should only validate to true if all validations are true", () => {
|
|
907
|
+
ctrl.$modelValue = undefined;
|
|
908
|
+
ctrl.$validators.a = valueFn(true);
|
|
909
|
+
ctrl.$validators.b = valueFn(true);
|
|
910
|
+
ctrl.$validators.c = valueFn(false);
|
|
911
|
+
|
|
912
|
+
ctrl.$validate();
|
|
913
|
+
expect(ctrl.$valid).toBe(false);
|
|
914
|
+
|
|
915
|
+
ctrl.$validators.c = valueFn(true);
|
|
916
|
+
|
|
917
|
+
ctrl.$validate();
|
|
918
|
+
expect(ctrl.$valid).toBe(true);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("should treat all responses as boolean for synchronous validators", () => {
|
|
922
|
+
const expectValid = function (value, expected) {
|
|
923
|
+
ctrl.$modelValue = undefined;
|
|
924
|
+
ctrl.$validators.a = valueFn(value);
|
|
925
|
+
|
|
926
|
+
ctrl.$validate();
|
|
927
|
+
expect(ctrl.$valid).toBe(expected);
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// False tests
|
|
931
|
+
expectValid(false, false);
|
|
932
|
+
expectValid(undefined, false);
|
|
933
|
+
expectValid(null, false);
|
|
934
|
+
expectValid(0, false);
|
|
935
|
+
expectValid(NaN, false);
|
|
936
|
+
expectValid("", false);
|
|
937
|
+
|
|
938
|
+
// True tests
|
|
939
|
+
expectValid(true, true);
|
|
940
|
+
expectValid(1, true);
|
|
941
|
+
expectValid("0", true);
|
|
942
|
+
expectValid("false", true);
|
|
943
|
+
expectValid([], true);
|
|
944
|
+
expectValid({}, true);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("should register invalid validations on the $error object", () => {
|
|
948
|
+
ctrl.$modelValue = undefined;
|
|
949
|
+
ctrl.$validators.unique = valueFn(false);
|
|
950
|
+
ctrl.$validators.tooLong = valueFn(false);
|
|
951
|
+
ctrl.$validators.notNumeric = valueFn(true);
|
|
952
|
+
|
|
953
|
+
ctrl.$validate();
|
|
954
|
+
|
|
955
|
+
expect(ctrl.$error.unique).toBe(true);
|
|
956
|
+
expect(ctrl.$error.tooLong).toBe(true);
|
|
957
|
+
expect(ctrl.$error.notNumeric).not.toBe(true);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("should render a validator asynchronously when a promise is returned", () => {
|
|
961
|
+
let defer;
|
|
962
|
+
ctrl.$asyncValidators.promiseValidator = function (value) {
|
|
963
|
+
defer = $q.defer();
|
|
964
|
+
return defer.promise;
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
scope.$apply('value = ""');
|
|
968
|
+
|
|
969
|
+
expect(ctrl.$valid).toBeUndefined();
|
|
970
|
+
expect(ctrl.$invalid).toBeUndefined();
|
|
971
|
+
expect(ctrl.$pending.promiseValidator).toBe(true);
|
|
972
|
+
|
|
973
|
+
defer.resolve();
|
|
974
|
+
scope.$digest();
|
|
975
|
+
|
|
976
|
+
expect(ctrl.$valid).toBe(true);
|
|
977
|
+
expect(ctrl.$invalid).toBe(false);
|
|
978
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
979
|
+
|
|
980
|
+
scope.$apply('value = "123"');
|
|
981
|
+
|
|
982
|
+
defer.reject();
|
|
983
|
+
scope.$digest();
|
|
984
|
+
|
|
985
|
+
expect(ctrl.$valid).toBe(false);
|
|
986
|
+
expect(ctrl.$invalid).toBe(true);
|
|
987
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it("should throw an error when a promise is not returned for an asynchronous validator", () => {
|
|
991
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
992
|
+
return true;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
expect(() => {
|
|
996
|
+
scope.$apply('value = "123"');
|
|
997
|
+
}).toThrowError(/nopromise/);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("should only run the async validators once all the sync validators have passed", () => {
|
|
1001
|
+
const stages = {};
|
|
1002
|
+
|
|
1003
|
+
stages.sync = { status1: false, status2: false, count: 0 };
|
|
1004
|
+
ctrl.$validators.syncValidator1 = function (modelValue, viewValue) {
|
|
1005
|
+
stages.sync.count++;
|
|
1006
|
+
return stages.sync.status1;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
ctrl.$validators.syncValidator2 = function (modelValue, viewValue) {
|
|
1010
|
+
stages.sync.count++;
|
|
1011
|
+
return stages.sync.status2;
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
stages.async = { defer: null, count: 0 };
|
|
1015
|
+
ctrl.$asyncValidators.asyncValidator = function (
|
|
1016
|
+
modelValue,
|
|
1017
|
+
viewValue,
|
|
1018
|
+
) {
|
|
1019
|
+
stages.async.defer = $q.defer();
|
|
1020
|
+
stages.async.count++;
|
|
1021
|
+
return stages.async.defer.promise;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
scope.$apply('value = "123"');
|
|
1025
|
+
|
|
1026
|
+
expect(ctrl.$valid).toBe(false);
|
|
1027
|
+
expect(ctrl.$invalid).toBe(true);
|
|
1028
|
+
|
|
1029
|
+
expect(stages.sync.count).toBe(2);
|
|
1030
|
+
expect(stages.async.count).toBe(0);
|
|
1031
|
+
|
|
1032
|
+
stages.sync.status1 = true;
|
|
1033
|
+
|
|
1034
|
+
scope.$apply('value = "456"');
|
|
1035
|
+
|
|
1036
|
+
expect(stages.sync.count).toBe(4);
|
|
1037
|
+
expect(stages.async.count).toBe(0);
|
|
1038
|
+
|
|
1039
|
+
stages.sync.status2 = true;
|
|
1040
|
+
|
|
1041
|
+
scope.$apply('value = "789"');
|
|
1042
|
+
|
|
1043
|
+
expect(stages.sync.count).toBe(6);
|
|
1044
|
+
expect(stages.async.count).toBe(1);
|
|
1045
|
+
|
|
1046
|
+
stages.async.defer.resolve();
|
|
1047
|
+
scope.$apply();
|
|
1048
|
+
|
|
1049
|
+
expect(ctrl.$valid).toBe(true);
|
|
1050
|
+
expect(ctrl.$invalid).toBe(false);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("should ignore expired async validation promises once delivered", () => {
|
|
1054
|
+
let defer;
|
|
1055
|
+
let oldDefer;
|
|
1056
|
+
let newDefer;
|
|
1057
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
1058
|
+
defer = $q.defer();
|
|
1059
|
+
return defer.promise;
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
scope.$apply('value = ""');
|
|
1063
|
+
oldDefer = defer;
|
|
1064
|
+
scope.$apply('value = "123"');
|
|
1065
|
+
newDefer = defer;
|
|
1066
|
+
|
|
1067
|
+
newDefer.reject();
|
|
1068
|
+
scope.$digest();
|
|
1069
|
+
oldDefer.resolve();
|
|
1070
|
+
scope.$digest();
|
|
1071
|
+
|
|
1072
|
+
expect(ctrl.$valid).toBe(false);
|
|
1073
|
+
expect(ctrl.$invalid).toBe(true);
|
|
1074
|
+
expect(ctrl.$pending).toBeUndefined();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("should clear and ignore all pending promises when the model value changes", () => {
|
|
1078
|
+
ctrl.$validators.sync = function (value) {
|
|
1079
|
+
return true;
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
const defers = [];
|
|
1083
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
1084
|
+
const defer = $q.defer();
|
|
1085
|
+
defers.push(defer);
|
|
1086
|
+
return defer.promise;
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
scope.$apply('value = "123"');
|
|
1090
|
+
expect(ctrl.$pending).toEqual({ async: true });
|
|
1091
|
+
expect(ctrl.$valid).toBeUndefined();
|
|
1092
|
+
expect(ctrl.$invalid).toBeUndefined();
|
|
1093
|
+
expect(defers.length).toBe(1);
|
|
1094
|
+
expect(isObject(ctrl.$pending)).toBe(true);
|
|
1095
|
+
|
|
1096
|
+
scope.$apply('value = "456"');
|
|
1097
|
+
expect(ctrl.$pending).toEqual({ async: true });
|
|
1098
|
+
expect(ctrl.$valid).toBeUndefined();
|
|
1099
|
+
expect(ctrl.$invalid).toBeUndefined();
|
|
1100
|
+
expect(defers.length).toBe(2);
|
|
1101
|
+
expect(isObject(ctrl.$pending)).toBe(true);
|
|
1102
|
+
|
|
1103
|
+
defers[1].resolve();
|
|
1104
|
+
scope.$digest();
|
|
1105
|
+
expect(ctrl.$valid).toBe(true);
|
|
1106
|
+
expect(ctrl.$invalid).toBe(false);
|
|
1107
|
+
expect(isObject(ctrl.$pending)).toBe(false);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it("should clear and ignore all pending promises when a parser fails", () => {
|
|
1111
|
+
let failParser = false;
|
|
1112
|
+
ctrl.$parsers.push((value) => (failParser ? undefined : value));
|
|
1113
|
+
|
|
1114
|
+
let defer;
|
|
1115
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
1116
|
+
defer = $q.defer();
|
|
1117
|
+
return defer.promise;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
ctrl.$setViewValue("x..y..z");
|
|
1121
|
+
expect(ctrl.$valid).toBeUndefined();
|
|
1122
|
+
expect(ctrl.$invalid).toBeUndefined();
|
|
1123
|
+
|
|
1124
|
+
failParser = true;
|
|
1125
|
+
|
|
1126
|
+
ctrl.$setViewValue("1..2..3");
|
|
1127
|
+
expect(ctrl.$valid).toBe(false);
|
|
1128
|
+
expect(ctrl.$invalid).toBe(true);
|
|
1129
|
+
expect(isObject(ctrl.$pending)).toBe(false);
|
|
1130
|
+
|
|
1131
|
+
defer.resolve();
|
|
1132
|
+
scope.$digest();
|
|
1133
|
+
|
|
1134
|
+
expect(ctrl.$valid).toBe(false);
|
|
1135
|
+
expect(ctrl.$invalid).toBe(true);
|
|
1136
|
+
expect(isObject(ctrl.$pending)).toBe(false);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it("should clear all errors from async validators if a parser fails", () => {
|
|
1140
|
+
let failParser = false;
|
|
1141
|
+
ctrl.$parsers.push((value) => (failParser ? undefined : value));
|
|
1142
|
+
|
|
1143
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
1144
|
+
return $q.reject();
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
ctrl.$setViewValue("x..y..z");
|
|
1148
|
+
expect(ctrl.$error).toEqual({ async: true });
|
|
1149
|
+
|
|
1150
|
+
failParser = true;
|
|
1151
|
+
|
|
1152
|
+
ctrl.$setViewValue("1..2..3");
|
|
1153
|
+
expect(ctrl.$error).toEqual({ parse: true });
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("should clear all errors from async validators if a sync validator fails", () => {
|
|
1157
|
+
let failValidator = false;
|
|
1158
|
+
ctrl.$validators.sync = function (value) {
|
|
1159
|
+
return !failValidator;
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
ctrl.$asyncValidators.async = function (value) {
|
|
1163
|
+
return $q.reject();
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
ctrl.$setViewValue("x..y..z");
|
|
1167
|
+
expect(ctrl.$error).toEqual({ async: true });
|
|
1168
|
+
|
|
1169
|
+
failValidator = true;
|
|
1170
|
+
|
|
1171
|
+
ctrl.$setViewValue("1..2..3");
|
|
1172
|
+
expect(ctrl.$error).toEqual({ sync: true });
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it("should be possible to extend Object prototype and still be able to do form validation", () => {
|
|
1176
|
+
// eslint-disable-next-line no-extend-native
|
|
1177
|
+
Object.prototype.someThing = function () {};
|
|
1178
|
+
const element = $compile(
|
|
1179
|
+
'<form name="myForm">' +
|
|
1180
|
+
'<input type="text" name="username" ng-model="username" minlength="10" required />' +
|
|
1181
|
+
"</form>",
|
|
1182
|
+
)($rootScope);
|
|
1183
|
+
const inputElm = element.find("input");
|
|
1184
|
+
|
|
1185
|
+
const formCtrl = $rootScope.myForm;
|
|
1186
|
+
const usernameCtrl = formCtrl.username;
|
|
1187
|
+
|
|
1188
|
+
$rootScope.$digest();
|
|
1189
|
+
expect(usernameCtrl.$invalid).toBe(true);
|
|
1190
|
+
expect(formCtrl.$invalid).toBe(true);
|
|
1191
|
+
|
|
1192
|
+
usernameCtrl.$setViewValue("valid-username");
|
|
1193
|
+
$rootScope.$digest();
|
|
1194
|
+
|
|
1195
|
+
expect(usernameCtrl.$invalid).toBe(false);
|
|
1196
|
+
expect(formCtrl.$invalid).toBe(false);
|
|
1197
|
+
delete Object.prototype.someThing;
|
|
1198
|
+
|
|
1199
|
+
dealoc(element);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it("should re-evaluate the form validity state once the asynchronous promise has been delivered", () => {
|
|
1203
|
+
const element = $compile(
|
|
1204
|
+
'<form name="myForm">' +
|
|
1205
|
+
'<input type="text" name="username" ng-model="username" minlength="10" required />' +
|
|
1206
|
+
'<input type="number" name="age" ng-model="age" min="10" required />' +
|
|
1207
|
+
"</form>",
|
|
1208
|
+
)($rootScope);
|
|
1209
|
+
const inputElm = element.find("input");
|
|
1210
|
+
|
|
1211
|
+
const formCtrl = $rootScope.myForm;
|
|
1212
|
+
const usernameCtrl = formCtrl.username;
|
|
1213
|
+
const ageCtrl = formCtrl.age;
|
|
1214
|
+
|
|
1215
|
+
let usernameDefer;
|
|
1216
|
+
usernameCtrl.$asyncValidators.usernameAvailability = function () {
|
|
1217
|
+
usernameDefer = $q.defer();
|
|
1218
|
+
return usernameDefer.promise;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
$rootScope.$digest();
|
|
1222
|
+
expect(usernameCtrl.$invalid).toBe(true);
|
|
1223
|
+
expect(formCtrl.$invalid).toBe(true);
|
|
1224
|
+
|
|
1225
|
+
usernameCtrl.$setViewValue("valid-username");
|
|
1226
|
+
$rootScope.$digest();
|
|
1227
|
+
|
|
1228
|
+
expect(formCtrl.$pending.usernameAvailability).toBeTruthy();
|
|
1229
|
+
expect(usernameCtrl.$invalid).toBeUndefined();
|
|
1230
|
+
expect(formCtrl.$invalid).toBeUndefined();
|
|
1231
|
+
|
|
1232
|
+
usernameDefer.resolve();
|
|
1233
|
+
$rootScope.$digest();
|
|
1234
|
+
expect(usernameCtrl.$invalid).toBe(false);
|
|
1235
|
+
expect(formCtrl.$invalid).toBe(true);
|
|
1236
|
+
|
|
1237
|
+
ageCtrl.$setViewValue(22);
|
|
1238
|
+
$rootScope.$digest();
|
|
1239
|
+
|
|
1240
|
+
expect(usernameCtrl.$invalid).toBe(false);
|
|
1241
|
+
expect(ageCtrl.$invalid).toBe(false);
|
|
1242
|
+
expect(formCtrl.$invalid).toBe(false);
|
|
1243
|
+
|
|
1244
|
+
usernameCtrl.$setViewValue("valid");
|
|
1245
|
+
$rootScope.$digest();
|
|
1246
|
+
|
|
1247
|
+
expect(usernameCtrl.$invalid).toBe(true);
|
|
1248
|
+
expect(ageCtrl.$invalid).toBe(false);
|
|
1249
|
+
expect(formCtrl.$invalid).toBe(true);
|
|
1250
|
+
|
|
1251
|
+
usernameCtrl.$setViewValue("another-valid-username");
|
|
1252
|
+
$rootScope.$digest();
|
|
1253
|
+
|
|
1254
|
+
usernameDefer.resolve();
|
|
1255
|
+
$rootScope.$digest();
|
|
1256
|
+
|
|
1257
|
+
expect(usernameCtrl.$invalid).toBe(false);
|
|
1258
|
+
expect(formCtrl.$invalid).toBe(false);
|
|
1259
|
+
expect(formCtrl.$pending).toBeFalsy();
|
|
1260
|
+
expect(ageCtrl.$invalid).toBe(false);
|
|
1261
|
+
|
|
1262
|
+
dealoc(element);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it("should always use the most recent $viewValue for validation", () => {
|
|
1266
|
+
ctrl.$parsers.push((value) => {
|
|
1267
|
+
if (value && value.substr(-1) === "b") {
|
|
1268
|
+
value = "a";
|
|
1269
|
+
ctrl.$setViewValue(value);
|
|
1270
|
+
ctrl.$render();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return value;
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
ctrl.$validators.mock = function (modelValue) {
|
|
1277
|
+
return true;
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
spyOn(ctrl.$validators, "mock").and.callThrough();
|
|
1281
|
+
|
|
1282
|
+
ctrl.$setViewValue("ab");
|
|
1283
|
+
|
|
1284
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledWith("a", "a");
|
|
1285
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledTimes(2);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it("should validate even if the modelValue did not change", () => {
|
|
1289
|
+
ctrl.$parsers.push((value) => {
|
|
1290
|
+
if (value && value.substr(-1) === "b") {
|
|
1291
|
+
value = "a";
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return value;
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
ctrl.$validators.mock = function (modelValue) {
|
|
1298
|
+
return true;
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
spyOn(ctrl.$validators, "mock").and.callThrough();
|
|
1302
|
+
|
|
1303
|
+
ctrl.$setViewValue("a");
|
|
1304
|
+
|
|
1305
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledWith("a", "a");
|
|
1306
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledTimes(1);
|
|
1307
|
+
|
|
1308
|
+
ctrl.$setViewValue("ab");
|
|
1309
|
+
|
|
1310
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledWith("a", "ab");
|
|
1311
|
+
expect(ctrl.$validators.mock).toHaveBeenCalledTimes(2);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it("should validate correctly when $parser name equals $validator key", () => {
|
|
1315
|
+
ctrl.$validators.parserOrValidator = function (value) {
|
|
1316
|
+
switch (value) {
|
|
1317
|
+
case "allInvalid":
|
|
1318
|
+
case "parseValid-validatorsInvalid":
|
|
1319
|
+
case "stillParseValid-validatorsInvalid":
|
|
1320
|
+
return false;
|
|
1321
|
+
default:
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
ctrl.$validators.validator = function (value) {
|
|
1327
|
+
switch (value) {
|
|
1328
|
+
case "allInvalid":
|
|
1329
|
+
case "parseValid-validatorsInvalid":
|
|
1330
|
+
case "stillParseValid-validatorsInvalid":
|
|
1331
|
+
return false;
|
|
1332
|
+
default:
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
ctrl.$parsers.push((value) => {
|
|
1338
|
+
switch (value) {
|
|
1339
|
+
case "allInvalid":
|
|
1340
|
+
case "stillAllInvalid":
|
|
1341
|
+
case "parseInvalid-validatorsValid":
|
|
1342
|
+
case "stillParseInvalid-validatorsValid":
|
|
1343
|
+
ctrl.$$parserName = "parserOrValidator";
|
|
1344
|
+
return undefined;
|
|
1345
|
+
default:
|
|
1346
|
+
return value;
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// Parser and validators are invalid
|
|
1351
|
+
scope.$apply('value = "allInvalid"');
|
|
1352
|
+
expect(scope.value).toBe("allInvalid");
|
|
1353
|
+
expect(ctrl.$error).toEqual({
|
|
1354
|
+
parserOrValidator: true,
|
|
1355
|
+
validator: true,
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
ctrl.$validate();
|
|
1359
|
+
expect(scope.value).toEqual("allInvalid");
|
|
1360
|
+
expect(ctrl.$error).toEqual({
|
|
1361
|
+
parserOrValidator: true,
|
|
1362
|
+
validator: true,
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
ctrl.$setViewValue("stillAllInvalid");
|
|
1366
|
+
expect(scope.value).toBeUndefined();
|
|
1367
|
+
expect(ctrl.$error).toEqual({ parserOrValidator: true });
|
|
1368
|
+
|
|
1369
|
+
ctrl.$validate();
|
|
1370
|
+
expect(scope.value).toBeUndefined();
|
|
1371
|
+
expect(ctrl.$error).toEqual({ parserOrValidator: true });
|
|
1372
|
+
|
|
1373
|
+
// Parser is valid, validators are invalid
|
|
1374
|
+
scope.$apply('value = "parseValid-validatorsInvalid"');
|
|
1375
|
+
expect(scope.value).toBe("parseValid-validatorsInvalid");
|
|
1376
|
+
expect(ctrl.$error).toEqual({
|
|
1377
|
+
parserOrValidator: true,
|
|
1378
|
+
validator: true,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
ctrl.$validate();
|
|
1382
|
+
expect(scope.value).toBe("parseValid-validatorsInvalid");
|
|
1383
|
+
expect(ctrl.$error).toEqual({
|
|
1384
|
+
parserOrValidator: true,
|
|
1385
|
+
validator: true,
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
ctrl.$setViewValue("stillParseValid-validatorsInvalid");
|
|
1389
|
+
expect(scope.value).toBeUndefined();
|
|
1390
|
+
expect(ctrl.$error).toEqual({
|
|
1391
|
+
parserOrValidator: true,
|
|
1392
|
+
validator: true,
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
ctrl.$validate();
|
|
1396
|
+
expect(scope.value).toBeUndefined();
|
|
1397
|
+
expect(ctrl.$error).toEqual({
|
|
1398
|
+
parserOrValidator: true,
|
|
1399
|
+
validator: true,
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// Parser is invalid, validators are valid
|
|
1403
|
+
scope.$apply('value = "parseInvalid-validatorsValid"');
|
|
1404
|
+
expect(scope.value).toBe("parseInvalid-validatorsValid");
|
|
1405
|
+
expect(ctrl.$error).toEqual({});
|
|
1406
|
+
|
|
1407
|
+
ctrl.$validate();
|
|
1408
|
+
expect(scope.value).toBe("parseInvalid-validatorsValid");
|
|
1409
|
+
expect(ctrl.$error).toEqual({});
|
|
1410
|
+
|
|
1411
|
+
ctrl.$setViewValue("stillParseInvalid-validatorsValid");
|
|
1412
|
+
expect(scope.value).toBeUndefined();
|
|
1413
|
+
expect(ctrl.$error).toEqual({ parserOrValidator: true });
|
|
1414
|
+
|
|
1415
|
+
ctrl.$validate();
|
|
1416
|
+
expect(scope.value).toBeUndefined();
|
|
1417
|
+
expect(ctrl.$error).toEqual({ parserOrValidator: true });
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
describe("override ModelOptions", () => {
|
|
1422
|
+
it("should replace the previous model options", () => {
|
|
1423
|
+
const { $options } = ctrl;
|
|
1424
|
+
ctrl.$overrideModelOptions({});
|
|
1425
|
+
expect(ctrl.$options).not.toBe($options);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it("should set the given options", () => {
|
|
1429
|
+
const { $options } = ctrl;
|
|
1430
|
+
ctrl.$overrideModelOptions({ debounce: 1000, updateOn: "blur" });
|
|
1431
|
+
expect(ctrl.$options.getOption("debounce")).toEqual(1000);
|
|
1432
|
+
expect(ctrl.$options.getOption("updateOn")).toEqual("blur");
|
|
1433
|
+
expect(ctrl.$options.getOption("updateOnDefault")).toBe(false);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it("should inherit from a parent model options if specified", () => {
|
|
1437
|
+
const element = $compile(
|
|
1438
|
+
'<form name="form" ng-model-options="{debounce: 1000, updateOn: \'blur\'}">' +
|
|
1439
|
+
' <input ng-model="value" name="input">' +
|
|
1440
|
+
"</form>",
|
|
1441
|
+
)($rootScope);
|
|
1442
|
+
const ctrl = $rootScope.form.input;
|
|
1443
|
+
ctrl.$overrideModelOptions({ debounce: 2000, "*": "$inherit" });
|
|
1444
|
+
expect(ctrl.$options.getOption("debounce")).toEqual(2000);
|
|
1445
|
+
expect(ctrl.$options.getOption("updateOn")).toEqual("blur");
|
|
1446
|
+
expect(ctrl.$options.getOption("updateOnDefault")).toBe(false);
|
|
1447
|
+
dealoc(element);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it("should not inherit from a parent model options if not specified", () => {
|
|
1451
|
+
const element = $compile(
|
|
1452
|
+
'<form name="form" ng-model-options="{debounce: 1000, updateOn: \'blur\'}">' +
|
|
1453
|
+
' <input ng-model="value" name="input">' +
|
|
1454
|
+
"</form>",
|
|
1455
|
+
)($rootScope);
|
|
1456
|
+
const ctrl = $rootScope.form.input;
|
|
1457
|
+
ctrl.$overrideModelOptions({ debounce: 2000 });
|
|
1458
|
+
expect(ctrl.$options.getOption("debounce")).toEqual(2000);
|
|
1459
|
+
expect(ctrl.$options.getOption("updateOn")).toEqual("");
|
|
1460
|
+
expect(ctrl.$options.getOption("updateOnDefault")).toBe(true);
|
|
1461
|
+
dealoc(element);
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
describe("CSS classes", () => {
|
|
1467
|
+
const EMAIL_REGEXP =
|
|
1468
|
+
/^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
1469
|
+
|
|
1470
|
+
it("should set ng-empty or ng-not-empty when the view value changes", () => {
|
|
1471
|
+
const element = $compile('<input ng-model="value" />')($rootScope);
|
|
1472
|
+
|
|
1473
|
+
$rootScope.$digest();
|
|
1474
|
+
expect(element.val()).toBe("");
|
|
1475
|
+
|
|
1476
|
+
$rootScope.value = "XXX";
|
|
1477
|
+
$rootScope.$digest();
|
|
1478
|
+
expect(element.val()).toBe("XXX");
|
|
1479
|
+
|
|
1480
|
+
element.val("");
|
|
1481
|
+
browserTrigger(element, "change");
|
|
1482
|
+
expect(element.val()).toBe("");
|
|
1483
|
+
|
|
1484
|
+
element.val("YYY");
|
|
1485
|
+
browserTrigger(element, "change");
|
|
1486
|
+
expect(element.val()).toBe("YYY");
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it("should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)", () => {
|
|
1490
|
+
const element = $compile('<input type="email" ng-model="value" />')(
|
|
1491
|
+
$rootScope,
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
$rootScope.$digest();
|
|
1495
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
1496
|
+
expect(element[0].classList.contains("ng-pristine")).toBeTrue();
|
|
1497
|
+
expect(element[0].classList.contains("ng-touched")).toBeFalse();
|
|
1498
|
+
expect(element[0].classList.contains("ng-valid-email")).toBe(true);
|
|
1499
|
+
expect(element[0].classList.contains("ng-invalid-email")).toBe(false);
|
|
1500
|
+
|
|
1501
|
+
$rootScope.$apply("value = 'invalid-email'");
|
|
1502
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
1503
|
+
expect(element[0].classList.contains("ng-pristine")).toBeTrue();
|
|
1504
|
+
expect(element[0].classList.contains("ng-valid-email")).toBe(false);
|
|
1505
|
+
expect(element[0].classList.contains("ng-invalid-email")).toBe(true);
|
|
1506
|
+
|
|
1507
|
+
element.val("invalid-again");
|
|
1508
|
+
browserTrigger(element, "change");
|
|
1509
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
1510
|
+
expect(element[0].classList.contains("ng-dirty")).toBeTrue();
|
|
1511
|
+
expect(element[0].classList.contains("ng-valid-email")).toBe(false);
|
|
1512
|
+
expect(element[0].classList.contains("ng-invalid-email")).toBe(true);
|
|
1513
|
+
|
|
1514
|
+
element.val("vojta@google.com");
|
|
1515
|
+
browserTrigger(element, "change");
|
|
1516
|
+
expect(element[0].classList.contains("ng-valid")).toBeTrue();
|
|
1517
|
+
expect(element[0].classList.contains("ng-dirty")).toBeTrue();
|
|
1518
|
+
expect(element[0].classList.contains("ng-valid-email")).toBe(true);
|
|
1519
|
+
expect(element[0].classList.contains("ng-invalid-email")).toBe(false);
|
|
1520
|
+
|
|
1521
|
+
browserTrigger(element, "blur");
|
|
1522
|
+
|
|
1523
|
+
expect(element[0].classList.contains("ng-touched")).toBeTrue();
|
|
1524
|
+
|
|
1525
|
+
dealoc(element);
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
it("should set invalid classes on init", () => {
|
|
1529
|
+
const element = $compile(
|
|
1530
|
+
'<input type="email" ng-model="value" required />',
|
|
1531
|
+
)($rootScope);
|
|
1532
|
+
$rootScope.$digest();
|
|
1533
|
+
|
|
1534
|
+
expect(element[0].classList.contains("ng-invalid")).toBeTrue();
|
|
1535
|
+
expect(element[0].classList.contains("ng-invalid-required")).toBeTrue();
|
|
1536
|
+
|
|
1537
|
+
dealoc(element);
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
describe("custom formatter and parser that are added by a directive in post linking", () => {
|
|
1542
|
+
let inputElm;
|
|
1543
|
+
let scope;
|
|
1544
|
+
let module;
|
|
1545
|
+
|
|
1546
|
+
beforeEach(() => {
|
|
1547
|
+
angular = new Angular();
|
|
1548
|
+
publishExternalAPI();
|
|
1549
|
+
module = window.angular
|
|
1550
|
+
.module("myModule", [])
|
|
1551
|
+
.directive("customFormat", () => ({
|
|
1552
|
+
require: "ngModel",
|
|
1553
|
+
link(scope, element, attrs, ngModelCtrl) {
|
|
1554
|
+
ngModelCtrl.$formatters.push((value) => value.part);
|
|
1555
|
+
ngModelCtrl.$parsers.push((value) => ({ part: value }));
|
|
1556
|
+
},
|
|
1557
|
+
}));
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
afterEach(() => {
|
|
1561
|
+
dealoc(inputElm);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
function createInput(type) {
|
|
1565
|
+
inputElm = jqLite(`<input type="${type}" ng-model="val" custom-format/>`);
|
|
1566
|
+
const injector = angular.bootstrap(inputElm, ["myModule"]);
|
|
1567
|
+
scope = injector.get("$rootScope");
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
it("should use them after the builtin ones for text inputs", () => {
|
|
1571
|
+
createInput("text");
|
|
1572
|
+
scope.$apply('val = {part: "a"}');
|
|
1573
|
+
expect(inputElm.val()).toBe("a");
|
|
1574
|
+
|
|
1575
|
+
inputElm.val("b");
|
|
1576
|
+
browserTrigger(inputElm, "change");
|
|
1577
|
+
expect(scope.val).toEqual({ part: "b" });
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it("should use them after the builtin ones for number inputs", () => {
|
|
1581
|
+
createInput("number");
|
|
1582
|
+
scope.$apply("val = {part: 1}");
|
|
1583
|
+
expect(inputElm.val()).toBe("1");
|
|
1584
|
+
|
|
1585
|
+
inputElm.val("2");
|
|
1586
|
+
browserTrigger(inputElm, "change");
|
|
1587
|
+
expect(scope.val).toEqual({ part: 2 });
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it("should use them after the builtin ones for date inputs", () => {
|
|
1591
|
+
createInput("date");
|
|
1592
|
+
scope.$apply(() => {
|
|
1593
|
+
scope.val = { part: "2000-11-08" };
|
|
1594
|
+
});
|
|
1595
|
+
expect(inputElm.val()).toBe("2000-11-08");
|
|
1596
|
+
|
|
1597
|
+
inputElm.val("2001-12-09");
|
|
1598
|
+
browserTrigger(inputElm, "change");
|
|
1599
|
+
expect(scope.val).toEqual({ part: "2001-12-09" });
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
describe("$touched", () => {
|
|
1604
|
+
it('should set the control touched state on "blur" event', () => {
|
|
1605
|
+
const element = $compile(
|
|
1606
|
+
'<form name="myForm">' +
|
|
1607
|
+
'<input name="myControl" ng-model="value" >' +
|
|
1608
|
+
"</form>",
|
|
1609
|
+
)($rootScope);
|
|
1610
|
+
const inputElm = element.find("input");
|
|
1611
|
+
const control = $rootScope.myForm.myControl;
|
|
1612
|
+
|
|
1613
|
+
expect(control.$touched).toBe(false);
|
|
1614
|
+
expect(control.$untouched).toBe(true);
|
|
1615
|
+
|
|
1616
|
+
browserTrigger(inputElm, "blur");
|
|
1617
|
+
expect(control.$touched).toBe(true);
|
|
1618
|
+
expect(control.$untouched).toBe(false);
|
|
1619
|
+
|
|
1620
|
+
dealoc(element);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
it('should not cause a digest on "blur" event if control is already touched', () => {
|
|
1624
|
+
const element = $compile(
|
|
1625
|
+
'<form name="myForm">' +
|
|
1626
|
+
'<input name="myControl" ng-model="value" >' +
|
|
1627
|
+
"</form>",
|
|
1628
|
+
)($rootScope);
|
|
1629
|
+
const inputElm = element.find("input");
|
|
1630
|
+
const control = $rootScope.myForm.myControl;
|
|
1631
|
+
|
|
1632
|
+
control.$setTouched();
|
|
1633
|
+
spyOn($rootScope, "$apply");
|
|
1634
|
+
browserTrigger(inputElm, "blur");
|
|
1635
|
+
|
|
1636
|
+
expect($rootScope.$apply).not.toHaveBeenCalled();
|
|
1637
|
+
|
|
1638
|
+
dealoc(element);
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
it('should digest asynchronously on "blur" event if a apply is already in progress', () => {
|
|
1642
|
+
const element = $compile(
|
|
1643
|
+
'<form name="myForm">' +
|
|
1644
|
+
'<input name="myControl" ng-model="value" >' +
|
|
1645
|
+
"</form>",
|
|
1646
|
+
)($rootScope);
|
|
1647
|
+
const inputElm = element.find("input");
|
|
1648
|
+
const control = $rootScope.myForm.myControl;
|
|
1649
|
+
|
|
1650
|
+
$rootScope.$apply(() => {
|
|
1651
|
+
expect(control.$touched).toBe(false);
|
|
1652
|
+
expect(control.$untouched).toBe(true);
|
|
1653
|
+
|
|
1654
|
+
browserTrigger(inputElm, "blur");
|
|
1655
|
+
|
|
1656
|
+
expect(control.$touched).toBe(false);
|
|
1657
|
+
expect(control.$untouched).toBe(true);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
expect(control.$touched).toBe(true);
|
|
1661
|
+
expect(control.$untouched).toBe(false);
|
|
1662
|
+
|
|
1663
|
+
dealoc(element);
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
describe("nested in a form", () => {
|
|
1668
|
+
it("should register/deregister a nested ngModel with parent form when entering or leaving DOM", () => {
|
|
1669
|
+
const element = $compile(
|
|
1670
|
+
'<form name="myForm">' +
|
|
1671
|
+
'<input ng-if="inputPresent" name="myControl" ng-model="value" required >' +
|
|
1672
|
+
"</form>",
|
|
1673
|
+
)($rootScope);
|
|
1674
|
+
let isFormValid;
|
|
1675
|
+
|
|
1676
|
+
$rootScope.inputPresent = false;
|
|
1677
|
+
$rootScope.$watch("myForm.$valid", (value) => {
|
|
1678
|
+
isFormValid = value;
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
$rootScope.$apply();
|
|
1682
|
+
|
|
1683
|
+
expect($rootScope.myForm.$valid).toBe(true);
|
|
1684
|
+
expect(isFormValid).toBe(true);
|
|
1685
|
+
expect($rootScope.myForm.myControl).toBeUndefined();
|
|
1686
|
+
|
|
1687
|
+
$rootScope.inputPresent = true;
|
|
1688
|
+
$rootScope.$apply();
|
|
1689
|
+
|
|
1690
|
+
expect($rootScope.myForm.$valid).toBe(false);
|
|
1691
|
+
expect(isFormValid).toBe(false);
|
|
1692
|
+
expect($rootScope.myForm.myControl).toBeDefined();
|
|
1693
|
+
|
|
1694
|
+
$rootScope.inputPresent = false;
|
|
1695
|
+
$rootScope.$apply();
|
|
1696
|
+
|
|
1697
|
+
expect($rootScope.myForm.$valid).toBe(true);
|
|
1698
|
+
expect(isFormValid).toBe(true);
|
|
1699
|
+
expect($rootScope.myForm.myControl).toBeUndefined();
|
|
1700
|
+
|
|
1701
|
+
dealoc(element);
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
it("should register/deregister a nested ngModel with parent form when entering or leaving DOM with animations", () => {
|
|
1705
|
+
// ngAnimate performs the dom manipulation after digest, and since the form validity can be affected by a form
|
|
1706
|
+
// control going away we must ensure that the deregistration happens during the digest while we are still doing
|
|
1707
|
+
// dirty checking.
|
|
1708
|
+
// module("ngAnimate");
|
|
1709
|
+
|
|
1710
|
+
const element = $compile(
|
|
1711
|
+
'<form name="myForm">' +
|
|
1712
|
+
'<input ng-if="inputPresent" name="myControl" ng-model="value" required >' +
|
|
1713
|
+
"</form>",
|
|
1714
|
+
)($rootScope);
|
|
1715
|
+
let isFormValid;
|
|
1716
|
+
|
|
1717
|
+
$rootScope.inputPresent = false;
|
|
1718
|
+
// this watch ensure that the form validity gets updated during digest (so that we can observe it)
|
|
1719
|
+
$rootScope.$watch("myForm.$valid", (value) => {
|
|
1720
|
+
isFormValid = value;
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
$rootScope.$apply();
|
|
1724
|
+
|
|
1725
|
+
expect($rootScope.myForm.$valid).toBe(true);
|
|
1726
|
+
expect(isFormValid).toBe(true);
|
|
1727
|
+
expect($rootScope.myForm.myControl).toBeUndefined();
|
|
1728
|
+
|
|
1729
|
+
$rootScope.inputPresent = true;
|
|
1730
|
+
$rootScope.$apply();
|
|
1731
|
+
|
|
1732
|
+
expect($rootScope.myForm.$valid).toBe(false);
|
|
1733
|
+
expect(isFormValid).toBe(false);
|
|
1734
|
+
expect($rootScope.myForm.myControl).toBeDefined();
|
|
1735
|
+
|
|
1736
|
+
$rootScope.inputPresent = false;
|
|
1737
|
+
$rootScope.$apply();
|
|
1738
|
+
|
|
1739
|
+
expect($rootScope.myForm.$valid).toBe(true);
|
|
1740
|
+
expect(isFormValid).toBe(true);
|
|
1741
|
+
expect($rootScope.myForm.myControl).toBeUndefined();
|
|
1742
|
+
|
|
1743
|
+
dealoc(element);
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
it("should keep previously defined watches consistent when changes in validity are made", () => {
|
|
1747
|
+
let isFormValid;
|
|
1748
|
+
$rootScope.$watch("myForm.$valid", (value) => {
|
|
1749
|
+
isFormValid = value;
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
const element = $compile(
|
|
1753
|
+
'<form name="myForm">' +
|
|
1754
|
+
'<input name="myControl" ng-model="value" required >' +
|
|
1755
|
+
"</form>",
|
|
1756
|
+
)($rootScope);
|
|
1757
|
+
|
|
1758
|
+
$rootScope.$apply();
|
|
1759
|
+
expect(isFormValid).toBe(false);
|
|
1760
|
+
expect($rootScope.myForm.$valid).toBe(false);
|
|
1761
|
+
|
|
1762
|
+
$rootScope.value = "value";
|
|
1763
|
+
$rootScope.$apply();
|
|
1764
|
+
expect(isFormValid).toBe(true);
|
|
1765
|
+
expect($rootScope.myForm.$valid).toBe(true);
|
|
1766
|
+
|
|
1767
|
+
dealoc(element);
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
// TODO:
|
|
1772
|
+
// describe("animations", () => {
|
|
1773
|
+
// function findElementAnimations(element, queue) {
|
|
1774
|
+
// const node = element[0];
|
|
1775
|
+
// const animations = [];
|
|
1776
|
+
// for (let i = 0; i < queue.length; i++) {
|
|
1777
|
+
// const animation = queue[i];
|
|
1778
|
+
// if (animation.element[0] === node) {
|
|
1779
|
+
// animations.push(animation);
|
|
1780
|
+
// }
|
|
1781
|
+
// }
|
|
1782
|
+
// return animations;
|
|
1783
|
+
// }
|
|
1784
|
+
|
|
1785
|
+
// function assertValidAnimation(animation, event, classNameA, classNameB) {
|
|
1786
|
+
// expect(animation.event).toBe(event);
|
|
1787
|
+
// expect(animation.args[1]).toBe(classNameA);
|
|
1788
|
+
// if (classNameB) expect(animation.args[2]).toBe(classNameB);
|
|
1789
|
+
// }
|
|
1790
|
+
|
|
1791
|
+
// let doc;
|
|
1792
|
+
// let input;
|
|
1793
|
+
// let scope;
|
|
1794
|
+
// let model;
|
|
1795
|
+
|
|
1796
|
+
// //beforeEach(module("ngAnimateMock"));
|
|
1797
|
+
|
|
1798
|
+
// beforeEach(() => {
|
|
1799
|
+
// scope = $rootScope.$new();
|
|
1800
|
+
// doc = jqLite(
|
|
1801
|
+
// '<form name="myForm">' +
|
|
1802
|
+
// ' <input type="text" ng-model="input" name="myInput" />' +
|
|
1803
|
+
// "</form>",
|
|
1804
|
+
// );
|
|
1805
|
+
// $rootElement.append(doc);
|
|
1806
|
+
// $compile(doc)(scope);
|
|
1807
|
+
// $animate.queue = [];
|
|
1808
|
+
|
|
1809
|
+
// input = doc.find("input");
|
|
1810
|
+
// model = scope.myForm.myInput;
|
|
1811
|
+
// });
|
|
1812
|
+
|
|
1813
|
+
// afterEach(() => {
|
|
1814
|
+
// dealoc(input);
|
|
1815
|
+
// });
|
|
1816
|
+
|
|
1817
|
+
// it("should trigger an animation when invalid", () => {
|
|
1818
|
+
// model.$setValidity("required", false);
|
|
1819
|
+
|
|
1820
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1821
|
+
// assertValidAnimation(animations[0], "removeClass", "ng-valid");
|
|
1822
|
+
// assertValidAnimation(animations[1], "addClass", "ng-invalid");
|
|
1823
|
+
// assertValidAnimation(animations[2], "addClass", "ng-invalid-required");
|
|
1824
|
+
// });
|
|
1825
|
+
|
|
1826
|
+
// it("should trigger an animation when valid", () => {
|
|
1827
|
+
// model.$setValidity("required", false);
|
|
1828
|
+
|
|
1829
|
+
// $animate.queue = [];
|
|
1830
|
+
|
|
1831
|
+
// model.$setValidity("required", true);
|
|
1832
|
+
|
|
1833
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1834
|
+
// assertValidAnimation(animations[0], "addClass", "ng-valid");
|
|
1835
|
+
// assertValidAnimation(animations[1], "removeClass", "ng-invalid");
|
|
1836
|
+
// assertValidAnimation(animations[2], "addClass", "ng-valid-required");
|
|
1837
|
+
// assertValidAnimation(animations[3], "removeClass", "ng-invalid-required");
|
|
1838
|
+
// });
|
|
1839
|
+
|
|
1840
|
+
// it("should trigger an animation when dirty", () => {
|
|
1841
|
+
// model.$setViewValue("some dirty value");
|
|
1842
|
+
|
|
1843
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1844
|
+
// assertValidAnimation(animations[0], "removeClass", "ng-empty");
|
|
1845
|
+
// assertValidAnimation(animations[1], "addClass", "ng-not-empty");
|
|
1846
|
+
// assertValidAnimation(animations[2], "removeClass", "ng-pristine");
|
|
1847
|
+
// assertValidAnimation(animations[3], "addClass", "ng-dirty");
|
|
1848
|
+
// });
|
|
1849
|
+
|
|
1850
|
+
// it("should trigger an animation when pristine", () => {
|
|
1851
|
+
// model.$setPristine();
|
|
1852
|
+
|
|
1853
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1854
|
+
// assertValidAnimation(animations[0], "removeClass", "ng-dirty");
|
|
1855
|
+
// assertValidAnimation(animations[1], "addClass", "ng-pristine");
|
|
1856
|
+
// });
|
|
1857
|
+
|
|
1858
|
+
// it("should trigger an animation when untouched", () => {
|
|
1859
|
+
// model.$setUntouched();
|
|
1860
|
+
|
|
1861
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1862
|
+
// assertValidAnimation(animations[0], "setClass", "ng-untouched");
|
|
1863
|
+
// expect(animations[0].args[2]).toBe("ng-touched");
|
|
1864
|
+
// });
|
|
1865
|
+
|
|
1866
|
+
// it("should trigger an animation when touched", () => {
|
|
1867
|
+
// model.$setTouched();
|
|
1868
|
+
|
|
1869
|
+
// const animations = findElementAnimations(input, $animate.queue);
|
|
1870
|
+
// assertValidAnimation(
|
|
1871
|
+
// animations[0],
|
|
1872
|
+
// "setClass",
|
|
1873
|
+
// "ng-touched",
|
|
1874
|
+
// "ng-untouched",
|
|
1875
|
+
// );
|
|
1876
|
+
// expect(animations[0].args[2]).toBe("ng-untouched");
|
|
1877
|
+
// });
|
|
1878
|
+
|
|
1879
|
+
// it("should trigger custom errors as addClass/removeClass when invalid/valid", () => {
|
|
1880
|
+
// model.$setValidity("custom-error", false);
|
|
1881
|
+
|
|
1882
|
+
// let animations = findElementAnimations(input, $animate.queue);
|
|
1883
|
+
// assertValidAnimation(animations[0], "removeClass", "ng-valid");
|
|
1884
|
+
// assertValidAnimation(animations[1], "addClass", "ng-invalid");
|
|
1885
|
+
// assertValidAnimation(
|
|
1886
|
+
// animations[2],
|
|
1887
|
+
// "addClass",
|
|
1888
|
+
// "ng-invalid-custom-error",
|
|
1889
|
+
// );
|
|
1890
|
+
|
|
1891
|
+
// $animate.queue = [];
|
|
1892
|
+
// model.$setValidity("custom-error", true);
|
|
1893
|
+
|
|
1894
|
+
// animations = findElementAnimations(input, $animate.queue);
|
|
1895
|
+
// assertValidAnimation(animations[0], "addClass", "ng-valid");
|
|
1896
|
+
// assertValidAnimation(animations[1], "removeClass", "ng-invalid");
|
|
1897
|
+
// assertValidAnimation(animations[2], "addClass", "ng-valid-custom-error");
|
|
1898
|
+
// assertValidAnimation(
|
|
1899
|
+
// animations[3],
|
|
1900
|
+
// "removeClass",
|
|
1901
|
+
// "ng-invalid-custom-error",
|
|
1902
|
+
// );
|
|
1903
|
+
// });
|
|
1904
|
+
// });
|
|
1905
|
+
});
|