@uuv/cypress 1.6.1 → 1.7.0

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.
@@ -3,51 +3,67 @@ NE PAS MODIFIER, FICHIER GENERE
3
3
  *******************************/
4
4
 
5
5
  /**
6
- * Software Name : UUV
7
- *
8
- * SPDX-FileCopyrightText: Copyright (c) 2022-2023 Orange
9
- * SPDX-License-Identifier: MIT
10
- *
11
- * This software is distributed under the MIT License,
12
- * the text of which is available at https://spdx.org/licenses/MIT.html
13
- * or see the "LICENSE" file for more details.
14
- *
15
- * Authors: NJAKO MOLOM Louis Fredice & SERVICAL Stanley
16
- * Software description: Make test writing fast, understandable by any human
17
- * understanding English or French.
18
- */
19
-
20
- import { DataTable, Given, Then, When, } from "@badeball/cypress-cucumber-preprocessor";
6
+ * Software Name : UUV
7
+ *
8
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2023 Orange
9
+ * SPDX-License-Identifier: MIT
10
+ *
11
+ * This software is distributed under the MIT License,
12
+ * the text of which is available at https://spdx.org/licenses/MIT.html
13
+ * or see the "LICENSE" file for more details.
14
+ *
15
+ * Authors: NJAKO MOLOM Louis Fredice & SERVICAL Stanley
16
+ * Software description: Make test writing fast, understandable by any human
17
+ * understanding English or French.
18
+ */
19
+
20
+ import { DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
21
21
  import { Context } from "../_context";
22
22
  import "../../../../cypress/commands";
23
23
  import { Method } from "cypress/types/net-stubbing";
24
-
24
+ import { key, KEY_PRESS } from "@uuv/runner-commons";
25
25
  import {
26
- assertTextContent,
27
- findWithRoleAndName,
28
- findWithRoleAndNameAndContent, findWithRoleAndNameAndContentDisable, findWithRoleAndNameAndContentEnable,
29
- notFoundWithRoleAndName,
30
- withinRoleAndName
26
+ assertTextContent,
27
+ findWithRoleAndName,
28
+ findWithRoleAndNameAndContent, findWithRoleAndNameAndContentDisable, findWithRoleAndNameAndContentEnable,
29
+ notFoundWithRoleAndName,
30
+ withinRoleAndName
31
31
  } from "../core-engine";
32
32
 
33
33
  When(`je visite l'Url {string}`, function(siteUrl: string) {
34
- if (siteUrl.match("^http:\\/\\/|https:\\/\\/")) {
35
- return cy.visit(`${siteUrl}`);
36
- }
37
- return cy.visit(`${Cypress.config().baseUrl}${siteUrl}`);
34
+ if (siteUrl.match("^http:\\/\\/|https:\\/\\/")) {
35
+ return cy.visit(`${siteUrl}`);
36
+ }
37
+ return cy.visit(`${Cypress.config().baseUrl}${siteUrl}`);
38
38
  });
39
39
 
40
40
  When(`je clique`, function() {
41
- cy.uuvCheckContextFocusedElement().then(context => {
42
- context.focusedElement!.click();
43
- });
41
+ cy.uuvCheckContextFocusedElement().then(context => {
42
+ context.focusedElement!.click();
43
+ });
44
44
  });
45
45
 
46
46
  When(`je saisie le(s) mot(s) {string}`, function(textToType: string) {
47
+ cy.uuvCheckContextFocusedElement().then((context) => {
48
+ context.focusedElement!.focus();
49
+ context.focusedElement!.type(textToType);
50
+ });
51
+ });
52
+
53
+ When(`j'appuie {int} fois sur {string}`, function(nbTimes: number, key: string) {
54
+ for (let i = 1; i <= nbTimes; i++) {
47
55
  cy.uuvCheckContextFocusedElement().then((context) => {
48
- context.focusedElement!.focus();
49
- context.focusedElement!.type(textToType);
56
+ context.focusedElement!.focus();
57
+ pressKey(context.focusedElement!, key);
50
58
  });
59
+ }
60
+ });
61
+
62
+ When(`j'appuie sur {string}`, function(key: string) {
63
+ cy.uuvCheckContextFocusedElement().then((context) => {
64
+ context.focusedElement!.focus();
65
+ pressKey(context.focusedElement!, key);
66
+ });
51
67
  });
52
68
 
53
69
  ////////////////////////////////////////////
@@ -55,126 +71,126 @@ When(`je saisie le(s) mot(s) {string}`, function(textToType: string) {
55
71
  ////////////////////////////////////////////
56
72
 
57
73
  Given(`je redimensionne la fenêtre à la vue {string}`, function(viewportPreset: string) {
58
- return cy.viewport(viewportPreset as Cypress.ViewportPreset);
74
+ return cy.viewport(viewportPreset as Cypress.ViewportPreset);
59
75
  });
60
76
 
61
77
  Given(
62
- `je redimensionne la fenêtre avec une largeur de {int} px et une longueur de {int} px`,
63
- function(width: number, height: number) {
64
- return cy.viewport(width, height);
65
- }
78
+ `je redimensionne la fenêtre avec une largeur de {int} px et une longueur de {int} px`,
79
+ function(width: number, height: number) {
80
+ return cy.viewport(width, height);
81
+ }
66
82
  );
67
83
 
68
84
  When(`je positionne le timeout à {int} secondes`, function(newTimeout: number) {
69
- return cy.uuvPatchContext({
70
- timeout: newTimeout,
71
- });
85
+ return cy.uuvPatchContext({
86
+ timeout: newTimeout
87
+ });
72
88
  });
73
89
 
74
90
  When(`je vais à l'intérieur de l'élément ayant pour rôle {string} et pour nom {string}`, function(role: string, name: string) {
75
- return withinRoleAndName(role, name);
91
+ return withinRoleAndName(role, name);
76
92
  });
77
93
 
78
94
  When(`je vais à l'intérieur de l'élément ayant pour aria-label {string}`, function(expectedAriaLabel: string) {
79
- const foundedElement = cy.uuvFindByLabelText(expectedAriaLabel, {})
80
- .uuvFoundedElement()
81
- .should("exist");
95
+ const foundedElement = cy.uuvFindByLabelText(expectedAriaLabel, {})
96
+ .uuvFoundedElement()
97
+ .should("exist");
82
98
 
83
- return cy.uuvPatchContext({
84
- focusedElement: foundedElement
85
- });
99
+ return cy.uuvPatchContext({
100
+ focusedElement: foundedElement
101
+ });
86
102
  });
87
103
 
88
104
  When(`je vais à l'intérieur de l'élément ayant pour testId {string}`, function(testId: string) {
89
- const foundedElement = cy.uuvFindByTestId(testId)
90
- .uuvFoundedElement()
91
- .should("exist");
105
+ const foundedElement = cy.uuvFindByTestId(testId)
106
+ .uuvFoundedElement()
107
+ .should("exist");
92
108
 
93
- return cy.uuvPatchContext({
94
- focusedElement: foundedElement
95
- });
109
+ return cy.uuvPatchContext({
110
+ focusedElement: foundedElement
111
+ });
96
112
  });
97
113
 
98
114
  When(`je vais à l'intérieur de l'élément ayant pour sélecteur {string}`, function(selector: string) {
99
- const foundedElement = cy.uuvGetContext().then(context => {
100
- const parentElement = context.focusedElement;
101
- if (parentElement) {
102
- console.log("parentElement: ", parentElement);
103
- return parentElement.should("exist").within(() => {
104
- cy.get(selector).as("foundedChildElement");
105
- });
106
- }
107
- cy.wrap(null).as("foundedChildElement");
108
- return cy.get(selector);
109
- }).uuvFoundedElement()
110
- .should("exist");
111
-
112
- return cy.uuvPatchContext({
113
- focusedElement: foundedElement
114
- });
115
+ const foundedElement = cy.uuvGetContext().then(context => {
116
+ const parentElement = context.focusedElement;
117
+ if (parentElement) {
118
+ console.log("parentElement: ", parentElement);
119
+ return parentElement.should("exist").within(() => {
120
+ cy.get(selector).as("foundedChildElement");
121
+ });
122
+ }
123
+ cy.wrap(null).as("foundedChildElement");
124
+ return cy.get(selector);
125
+ }).uuvFoundedElement()
126
+ .should("exist");
127
+
128
+ return cy.uuvPatchContext({
129
+ focusedElement: foundedElement
130
+ });
115
131
  });
116
132
 
117
133
  When(`je reinitialise le contexte`, function() {
118
- return cy.wrap(new Context()).as("context");
134
+ return cy.wrap(new Context()).as("context");
119
135
  });
120
136
 
121
137
  When(
122
- `je simule une requête {} sur l'url {string} nommée {string} avec le contenu suivant {}`,
123
- function(verb: Method, uri: string, name: string, body: any) {
124
- return cy
125
- .intercept(verb, uri, {
126
- body: body,
127
- })
128
- .as(name);
129
- }
138
+ `je simule une requête {} sur l'url {string} nommée {string} avec le contenu suivant {}`,
139
+ function(verb: Method, uri: string, name: string, body: any) {
140
+ return cy
141
+ .intercept(verb, uri, {
142
+ body: body
143
+ })
144
+ .as(name);
145
+ }
130
146
  );
131
147
 
132
148
  When(
133
- `je simule une requête {} sur l'url {string} nommée {string} avec le code http {int}`,
134
- function(verb: Method, uri: string, name: string, statusCode: number) {
135
- return cy
136
- .intercept(verb, uri, {
137
- statusCode: statusCode,
138
- })
139
- .as(name);
140
- }
149
+ `je simule une requête {} sur l'url {string} nommée {string} avec le code http {int}`,
150
+ function(verb: Method, uri: string, name: string, statusCode: number) {
151
+ return cy
152
+ .intercept(verb, uri, {
153
+ statusCode: statusCode
154
+ })
155
+ .as(name);
156
+ }
141
157
  );
142
158
 
143
159
  When(
144
- `je simule une requête {} sur l'url {string} nommée {string} avec le fichier suivant {}`,
145
- function(verb: Method, uri: string, name: string, fixture: any) {
146
- return cy
147
- .intercept(verb, uri, {
148
- fixture: fixture,
149
- })
150
- .as(name);
151
- }
160
+ `je simule une requête {} sur l'url {string} nommée {string} avec le fichier suivant {}`,
161
+ function(verb: Method, uri: string, name: string, fixture: any) {
162
+ return cy
163
+ .intercept(verb, uri, {
164
+ fixture: fixture
165
+ })
166
+ .as(name);
167
+ }
152
168
  );
153
169
 
154
170
  ////////////////////////////////////////////
155
171
  // INTERCEPTION
156
172
  ////////////////////////////////////////////
157
173
  When(
158
- `je saisie le(s) header(s) pour l'Uri {string} et la methode {string}`,
159
- function(url: string, method: string, headersToSet: DataTable) {
160
- cy.intercept(method as Method, url, (req) => {
161
- req.headers = {
162
- ...req.headers,
163
- ...headersToSet.rowsHash(),
164
- };
165
- req.continue();
166
- });
167
- }
174
+ `je saisie le(s) header(s) pour l'Uri {string} et la methode {string}`,
175
+ function(url: string, method: string, headersToSet: DataTable) {
176
+ cy.intercept(method as Method, url, (req) => {
177
+ req.headers = {
178
+ ...req.headers,
179
+ ...headersToSet.rowsHash()
180
+ };
181
+ req.continue();
182
+ });
183
+ }
168
184
  );
169
185
 
170
186
  When(`je saisie le(s) header(s) pour l'Uri {string}`, function(url: string, headersToSet: DataTable) {
171
- cy.intercept(url, (req) => {
172
- req.headers = {
173
- ...req.headers,
174
- ...headersToSet.rowsHash(),
175
- };
176
- req.continue();
177
- });
187
+ cy.intercept(url, (req) => {
188
+ req.headers = {
189
+ ...req.headers,
190
+ ...headersToSet.rowsHash()
191
+ };
192
+ req.continue();
193
+ });
178
194
  });
179
195
 
180
196
  ////////////////////////////////////////////
@@ -185,138 +201,217 @@ When(`je saisie le(s) header(s) pour l'Uri {string}`, function(url: string, head
185
201
  * Look for an element based on its content
186
202
  */
187
203
  Then(`je dois voir un élément qui contient {string}`, async function(textContent: string) {
188
- cy.uuvFindByText(textContent, {})
189
- .uuvFoundedElement()
190
- .should("exist");
204
+ cy.uuvFindByText(textContent, {})
205
+ .uuvFoundedElement()
206
+ .should("exist");
191
207
  });
192
208
 
193
209
  Then(`je ne dois pas voir un élément qui contient {string}`, async function(textContent: string) {
194
- cy.uuvFindByText(textContent, {})
195
- .should("not.exist");
210
+ cy.uuvFindByText(textContent, {})
211
+ .should("not.exist");
196
212
  });
197
213
 
198
214
  Then(`je dois voir un élément ayant pour testId {string}`, async function(testId: string) {
199
- cy.uuvFindByTestId(testId)
200
- .uuvFoundedElement()
201
- .should("exist");
215
+ cy.uuvFindByTestId(testId)
216
+ .uuvFoundedElement()
217
+ .should("exist");
202
218
  });
203
219
 
204
220
  Then(`je ne dois pas voir un élément ayant pour testId {string}`, async function(testId: string) {
205
- cy.uuvFindByTestId(testId)
206
- .should("not.exist");
221
+ cy.uuvFindByTestId(testId)
222
+ .should("not.exist");
207
223
  });
208
224
 
209
225
  Then(`je dois voir un élément avec le rôle {string} et le nom {string}`, async function(role: string, name: string) {
210
- findWithRoleAndName(role, name);
226
+ findWithRoleAndName(role, name);
211
227
  });
212
228
 
213
229
  Then(
214
- `je ne dois pas voir un élément avec le rôle {string} et le nom {string}`,
215
- async function(role: string, name: string) {
216
- notFoundWithRoleAndName(role, name);
217
- }
230
+ `je ne dois pas voir un élément avec le rôle {string} et le nom {string}`,
231
+ async function(role: string, name: string) {
232
+ notFoundWithRoleAndName(role, name);
233
+ }
218
234
  );
219
235
 
220
236
  Then(
221
- `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string}`,
222
- async function(expectedRole: string, name: string, expectedTextContent: string) {
223
- findWithRoleAndNameAndContent(expectedRole, name, expectedTextContent);
224
- }
237
+ `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string}`,
238
+ async function(expectedRole: string, name: string, expectedTextContent: string) {
239
+ findWithRoleAndNameAndContent(expectedRole, name, expectedTextContent);
240
+ }
225
241
  );
226
242
 
227
243
  Then(
228
- `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string} inactif`,
229
- async function(expectedRole: string, name: string, expectedTextContent: string) {
230
- findWithRoleAndNameAndContentDisable(expectedRole, name, expectedTextContent);
231
- }
244
+ `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string} inactif`,
245
+ async function(expectedRole: string, name: string, expectedTextContent: string) {
246
+ findWithRoleAndNameAndContentDisable(expectedRole, name, expectedTextContent);
247
+ }
232
248
  );
233
249
 
234
250
  Then(
235
- `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string} actif`,
236
- async function(expectedRole: string, name: string, expectedTextContent: string) {
237
- findWithRoleAndNameAndContentEnable(expectedRole, name, expectedTextContent);
238
- }
251
+ `je dois voir un élément avec le rôle {string} et le nom {string} et pour contenu {string} actif`,
252
+ async function(expectedRole: string, name: string, expectedTextContent: string) {
253
+ findWithRoleAndNameAndContentEnable(expectedRole, name, expectedTextContent);
254
+ }
239
255
  );
240
256
 
241
257
  Then(`je dois voir un élément ayant pour aria-label {string}`, async function(expectedAriaLabel: string) {
242
- cy.uuvFindByLabelText(expectedAriaLabel, {})
243
- .uuvFoundedElement()
244
- .should("exist");
258
+ cy.uuvFindByLabelText(expectedAriaLabel, {})
259
+ .uuvFoundedElement()
260
+ .should("exist");
245
261
  });
246
262
 
247
263
  Then(`je ne dois pas voir un élément ayant pour aria-label {string}`, async function(expectedAriaLabel: string) {
248
- cy.uuvFindByLabelText(expectedAriaLabel, {})
249
- .should("not.exist");
264
+ cy.uuvFindByLabelText(expectedAriaLabel, {})
265
+ .should("not.exist");
250
266
  });
251
267
 
252
268
  Then(
253
- `je dois voir un élément ayant pour aria-label {string} et pour contenu {string}`,
254
- async function(expectedAriaLabel: string, expectedTextContent: string) {
255
- cy.uuvFindByLabelText(expectedAriaLabel, {})
256
- .uuvFoundedElement()
257
- .should("exist")
258
- .then((response) => {
259
- assert.equal(response.length, 1);
260
- assertTextContent(response, expectedTextContent);
261
- });
262
- }
269
+ `je dois voir un élément ayant pour aria-label {string} et pour contenu {string}`,
270
+ async function(expectedAriaLabel: string, expectedTextContent: string) {
271
+ cy.uuvFindByLabelText(expectedAriaLabel, {})
272
+ .uuvFoundedElement()
273
+ .should("exist")
274
+ .then((response) => {
275
+ assert.equal(response.length, 1);
276
+ assertTextContent(response, expectedTextContent);
277
+ });
278
+ }
263
279
  );
264
280
 
265
281
  Then(`je dois consommer le bouchon nommé {string}`, async function(name: string) {
266
- cy.wait([`@${name}`]);
282
+ cy.wait([`@${name}`]);
267
283
  });
268
284
 
269
285
  Then(`j'attends {int} ms`, async function(ms: number) {
270
- cy.wait(ms);
286
+ cy.wait(ms);
271
287
  });
272
288
 
273
289
  Then(
274
- `je dois voir des elements de la liste ayant pour nom {string}`,
275
- async function(expectedListName: string, expectedElementsOfList: DataTable) {
276
- cy.uuvFindByRole("list", { name: expectedListName })
277
- .uuvFoundedElement()
278
- .should("exist")
279
- .within(() => {
280
- return cy.uuvFindAllByRole("listitem", {}).then((listitem) => {
281
- const foundedElement: any[] = [];
282
- for (let i = 0; i < listitem.length; i++) {
283
- foundedElement.push([listitem[i].textContent]);
284
- }
285
- assert.equal(listitem.length, expectedElementsOfList.raw().length);
286
- assert.deepEqual(
287
- foundedElement,
288
- expectedElementsOfList.raw(),
289
- `expected [${expectedElementsOfList.raw()}] to be [${foundedElement}]`
290
- );
291
- });
292
- });
293
- }
290
+ `je dois voir des elements de la liste ayant pour nom {string}`,
291
+ async function(expectedListName: string, expectedElementsOfList: DataTable) {
292
+ cy.uuvFindByRole("list", { name: expectedListName })
293
+ .uuvFoundedElement()
294
+ .should("exist")
295
+ .within(() => {
296
+ return cy.uuvFindAllByRole("listitem", {}).then((listitem) => {
297
+ const foundedElement: any[] = [];
298
+ for (let i = 0; i < listitem.length; i++) {
299
+ foundedElement.push([listitem[i].textContent]);
300
+ }
301
+ assert.equal(listitem.length, expectedElementsOfList.raw().length);
302
+ assert.deepEqual(
303
+ foundedElement,
304
+ expectedElementsOfList.raw(),
305
+ `expected [${expectedElementsOfList.raw()}] to be [${foundedElement}]`
306
+ );
307
+ });
308
+ });
309
+ }
294
310
  );
295
311
 
296
312
  Then(
297
- `je dois voir les attributs avec valeurs suivantes`,
298
- async function(expectedAttributeList: DataTable) {
299
- cy.uuvCheckContextFocusedElement().then((context) => {
300
- const elementToSelect = context.focusedElement!;
301
- for (const currentIndex in expectedAttributeList.raw()) {
302
- const attributeName = expectedAttributeList.raw()[currentIndex][0];
303
- const attributeValue = expectedAttributeList.raw()[currentIndex][1];
304
- elementToSelect.then((response) => {
305
- assert.equal(response[0].getAttribute(attributeName), attributeValue);
306
- });
307
- }
308
- });
309
- }
313
+ `je dois voir les attributs avec valeurs suivantes`,
314
+ async function(expectedAttributeList: DataTable) {
315
+ cy.uuvCheckContextFocusedElement().then((context) => {
316
+ const elementToSelect = context.focusedElement!;
317
+ for (const currentIndex in expectedAttributeList.raw()) {
318
+ const attributeName = expectedAttributeList.raw()[currentIndex][0];
319
+ const attributeValue = expectedAttributeList.raw()[currentIndex][1];
320
+ elementToSelect.then((response) => {
321
+ assert.equal(response[0].getAttribute(attributeName), attributeValue);
322
+ });
323
+ }
324
+ });
325
+ }
310
326
  );
311
327
 
312
328
  Then(`je dois voir un élément ayant pour sélecteur {string}`, async function(selector: string) {
313
- cy.get(selector).should("exist");
329
+ cy.get(selector).should("exist");
314
330
  });
315
331
 
316
332
 
317
333
  Then(
318
- `je ne dois pas avoir de problèmes d'accessibilité`,
319
- async function() {
320
- cy.injectAxe();
321
- cy.checkA11y();
322
- });
334
+ `je ne dois pas avoir de problèmes d'accessibilité`,
335
+ async function() {
336
+ cy.injectAxe();
337
+ cy.checkA11y();
338
+ });
339
+
340
+ Then(
341
+ `je ne dois pas avoir de problèmes d'accessibilité de niveau critique`,
342
+ async function() {
343
+ cy.injectAxe();
344
+ cy.checkA11y(undefined, {
345
+ includedImpacts: ["critical"]
346
+ });
347
+ });
348
+
349
+ Then(
350
+ `je ne dois pas avoir de problèmes d'accessibilité avec l(es) impact(s) {}`,
351
+ async function(impacts: any) {
352
+ cy.injectAxe();
353
+ cy.checkA11y(undefined, {
354
+ includedImpacts: [impacts]
355
+ });
356
+ });
357
+
358
+ Then(
359
+ `je ne dois pas avoir de problèmes d'accessibilité avec le(s) standard(s) {}`,
360
+ async function(tags: any) {
361
+ cy.injectAxe();
362
+ cy.checkA11y(undefined, {
363
+ runOnly: {
364
+ type: "tag",
365
+ values: [tags]
366
+ }
367
+ });
368
+ });
369
+
370
+ Then(
371
+ `je ne dois pas avoir de problèmes d'accessibilité sur le fichier json suivant de contexte {} et avec le fichier json suivant d'option {}`,
372
+ async function(context: any, option: any) {
373
+ cy.injectAxe();
374
+ cy.fixture(context).then(context => {
375
+ cy.fixture(option).then(option => {
376
+ cy.checkA11y(context, option);
377
+ });
378
+ });
379
+ });
380
+
381
+ Then(
382
+ `je ne dois pas avoir de problèmes d'accessibilité avec le fichier json suivant d'option {}`,
383
+ async function(option: any) {
384
+ cy.injectAxe();
385
+ cy.fixture(option).then(data => {
386
+ cy.checkA11y(undefined, data);
387
+ });
388
+ });
389
+
390
+ function pressKey(context: Cypress.Chainable<JQuery<HTMLElement>>, key: string) {
391
+ switch (key) {
392
+ case KEY_PRESS.TAB:
393
+ context.realPress("Tab");
394
+ break;
395
+ case KEY_PRESS.REVERSE_TAB:
396
+ context.realPress(["ShiftLeft", "Tab"]);
397
+ break;
398
+ case KEY_PRESS.UP:
399
+ context.realPress("ArrowUp");
400
+ break;
401
+ case KEY_PRESS.DOWN:
402
+ context.realPress("ArrowDown");
403
+ break;
404
+ case KEY_PRESS.LEFT:
405
+ context.realPress("ArrowLeft");
406
+ break;
407
+ case KEY_PRESS.RIGHT:
408
+ context.realPress("ArrowRight");
409
+ break;
410
+ default:
411
+ console.error("the command" + key + " is unrecognized.");
412
+ break;
413
+ }
414
+ cy.uuvPatchContext({
415
+ focusedElement: cy.focused()
416
+ });
417
+ }