19007: Expands tests.
[arvados-workbench2.git] / cypress / integration / collection.spec.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 const path = require('path');
6
7 describe('Collection panel tests', function () {
8     let activeUser;
9     let adminUser;
10     let downloadsFolder;
11
12     before(function () {
13         // Only set up common users once. These aren't set up as aliases because
14         // aliases are cleaned up after every test. Also it doesn't make sense
15         // to set the same users on beforeEach() over and over again, so we
16         // separate a little from Cypress' 'Best Practices' here.
17         cy.getUser('admin', 'Admin', 'User', true, true)
18             .as('adminUser').then(function () {
19                 adminUser = this.adminUser;
20             }
21             );
22         cy.getUser('collectionuser1', 'Collection', 'User', false, true)
23             .as('activeUser').then(function () {
24                 activeUser = this.activeUser;
25             }
26             );
27         downloadsFolder = Cypress.config('downloadsFolder');
28     });
29
30     beforeEach(function () {
31         cy.clearCookies();
32         cy.clearLocalStorage();
33     });
34
35     it('allows to download mountain duck config for a collection', () => {
36         cy.createCollection(adminUser.token, {
37             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
38             owner_uuid: activeUser.user.uuid,
39             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
40         })
41         .as('testCollection').then(function (testCollection) {
42             cy.loginAs(activeUser);
43             cy.goToPath(`/collections/${testCollection.uuid}`);
44
45             cy.get('[data-cy=collection-panel-options-btn]').click();
46             cy.get('[data-cy=context-menu]').contains('Open with 3rd party client').click();
47             cy.get('[data-cy=download-button').click();
48
49             const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
50
51             cy.readFile(filename, { timeout: 15000 })
52                 .then((body) => {
53                     const childrenCollection = Array.prototype.slice.call(Cypress.$(body).find('dict')[0].children);
54                     const map = {};
55                     let i, j = 2;
56
57                     for (i=0; i < childrenCollection.length; i += j) {
58                       map[childrenCollection[i].outerText] = childrenCollection[i + 1].outerText;
59                     }
60
61                     cy.get('#simple-tabpanel-0').find('a')
62                         .then((a) => {
63                             const [host, port] = a.text().split('@')[1].split('/')[0].split(':');
64                             expect(map['Protocol']).to.equal('davs');
65                             expect(map['UUID']).to.equal(testCollection.uuid);
66                             expect(map['Username']).to.equal(activeUser.user.username);
67                             expect(map['Port']).to.equal(port);
68                             expect(map['Hostname']).to.equal(host);
69                             if (map['Path']) {
70                                 expect(map['Path']).to.equal(`/c=${testCollection.uuid}`);
71                             }
72                         });
73                 })
74                 .then(() => cy.task('clearDownload', { filename }));
75         });
76     });
77
78     it('uses the property editor (from edit dialog) with vocabulary terms', function () {
79         cy.createCollection(adminUser.token, {
80             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
81             owner_uuid: activeUser.user.uuid,
82             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
83         })
84             .as('testCollection').then(function () {
85                 cy.loginAs(activeUser);
86                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
87
88                 cy.get('[data-cy=collection-info-panel')
89                     .should('contain', this.testCollection.name)
90                     .and('not.contain', 'Color: Magenta');
91
92                 cy.get('[data-cy=collection-panel-options-btn]').click();
93                 cy.get('[data-cy=context-menu]').contains('Edit collection').click();
94                 cy.get('[data-cy=form-dialog]').should('contain', 'Properties');
95
96                 // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
97                 cy.get('[data-cy=resource-properties-form]').within(() => {
98                     cy.get('[data-cy=property-field-key]').within(() => {
99                         cy.get('input').type('Color');
100                     });
101                     cy.get('[data-cy=property-field-value]').within(() => {
102                         cy.get('input').type('Magenta');
103                     });
104                     cy.root().submit();
105                 });
106                 // Confirm proper vocabulary labels are displayed on the UI.
107                 cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
108                 cy.get('[data-cy=form-dialog]').contains('Save').click();
109                 cy.get('[data-cy=form-dialog]').should('not.exist');
110                 // Confirm proper vocabulary IDs were saved on the backend.
111                 cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
112                     .its('body').as('collection')
113                     .then(function () {
114                         expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
115                     });
116                 // Confirm the property is displayed on the UI.
117                 cy.get('[data-cy=collection-info-panel')
118                     .should('contain', this.testCollection.name)
119                     .and('contain', 'Color: Magenta');
120             });
121     });
122
123     it('uses the editor (from details panel) with vocabulary terms', function () {
124         cy.createCollection(adminUser.token, {
125             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
126             owner_uuid: activeUser.user.uuid,
127             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
128         })
129             .as('testCollection').then(function () {
130                 cy.loginAs(activeUser);
131                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
132
133                 cy.get('[data-cy=collection-info-panel')
134                     .should('contain', this.testCollection.name)
135                     .and('not.contain', 'Color: Magenta')
136                     .and('not.contain', 'Size: S');
137                 cy.get('[data-cy=additional-info-icon]').click();
138
139                 cy.get('[data-cy=details-panel]').within(() => {
140                     cy.get('[data-cy=details-panel-edit-btn]').click();
141                 });
142                 cy.get('[data-cy=form-dialog').contains('Edit Collection');
143
144                 // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
145                 cy.get('[data-cy=resource-properties-form]').within(() => {
146                     cy.get('[data-cy=property-field-key]').within(() => {
147                         cy.get('input').type('Color');
148                     });
149                     cy.get('[data-cy=property-field-value]').within(() => {
150                         cy.get('input').type('Magenta');
151                     });
152                     cy.root().submit();
153                 });
154                 // Confirm proper vocabulary labels are displayed on the UI.
155                 cy.get('[data-cy=form-dialog]')
156                     .should('contain', 'Color: Magenta');
157
158                 // Case-insensitive on-blur auto-selection test
159                 // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
160                 cy.get('[data-cy=resource-properties-form]').within(() => {
161                     cy.get('[data-cy=property-field-key]').within(() => {
162                         cy.get('input').type('sIzE');
163                     });
164                     cy.get('[data-cy=property-field-value]').within(() => {
165                         cy.get('input').type('sMaLL');
166                     });
167                     // Cannot "type()" TAB on Cypress so let's click another field
168                     // to trigger the onBlur event.
169                     cy.get('[data-cy=property-field-key]').click();
170                     cy.root().submit();
171                 });
172                 // Confirm proper vocabulary labels are displayed on the UI.
173                 cy.get('[data-cy=form-dialog]')
174                     .should('contain', 'Size: S');
175
176                 cy.get('[data-cy=form-dialog]').contains('Save').click();
177                 cy.get('[data-cy=form-dialog]').should('not.exist');
178
179                 // Confirm proper vocabulary IDs were saved on the backend.
180                 cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
181                     .its('body').as('collection')
182                     .then(function () {
183                         expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
184                         expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2');
185                     });
186
187                 // Confirm properties display on the UI.
188                 cy.get('[data-cy=collection-info-panel')
189                     .should('contain', this.testCollection.name)
190                     .and('contain', 'Color: Magenta')
191                     .and('contain', 'Size: S');
192             });
193     });
194
195     it('shows collection by URL', function () {
196         cy.loginAs(activeUser);
197         [true, false].map(function (isWritable) {
198             // Using different file names to avoid test flakyness: the second iteration
199             // on this loop may pass an assertion from the first iteration by looking
200             // for the same file name.
201             const fileName = isWritable ? 'bar' : 'foo';
202             const subDirName = 'subdir';
203             cy.createGroup(adminUser.token, {
204                 name: 'Shared project',
205                 group_class: 'project',
206             }).as('sharedGroup').then(function () {
207                 // Creates the collection using the admin token so we can set up
208                 // a bogus manifest text without block signatures.
209                 cy.doRequest('GET', '/arvados/v1/config', null, null)
210                     .its('body').should((clusterConfig) => {
211                       expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", false);
212                       expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
213                       expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
214                       const inlineUrl = clusterConfig.Services.WebDAV.ExternalURL !== ""
215                           ? clusterConfig.Services.WebDAV.ExternalURL
216                           : clusterConfig.Services.WebDAVDownload.ExternalURL;
217                       expect(inlineUrl).to.not.contain("*");
218                     })
219                     .createCollection(adminUser.token, {
220                       name: 'Test collection',
221                       owner_uuid: this.sharedGroup.uuid,
222                       properties: { someKey: 'someValue' },
223                       manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
224                     })
225                     .as('testCollection').then(function () {
226                         // Share the group with active user.
227                         cy.createLink(adminUser.token, {
228                             name: isWritable ? 'can_write' : 'can_read',
229                             link_class: 'permission',
230                             head_uuid: this.sharedGroup.uuid,
231                             tail_uuid: activeUser.user.uuid
232                         })
233                         cy.goToPath(`/collections/${this.testCollection.uuid}`);
234
235                         // Check that name & uuid are correct.
236                         cy.get('[data-cy=collection-info-panel]')
237                             .should('contain', this.testCollection.name)
238                             .and('contain', this.testCollection.uuid)
239                             .and('not.contain', 'This is an old version');
240                         // Check for the read-only icon
241                         cy.get('[data-cy=read-only-icon]').should(`${isWritable ? 'not.' : ''}exist`);
242                         // Check that both read and write operations are available on
243                         // the 'More options' menu.
244                         cy.get('[data-cy=collection-panel-options-btn]')
245                             .click()
246                         cy.get('[data-cy=context-menu]')
247                             .should('contain', 'Add to favorites')
248                             .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection');
249                         cy.get('body').click(); // Collapse the menu avoiding details panel expansion
250                         cy.get('[data-cy=collection-info-panel]')
251                             .should('contain', 'someKey: someValue')
252                             .and('not.contain', 'anotherKey: anotherValue');
253                         // Check that the file listing show both read & write operations
254                         cy.waitForDom().get('[data-cy=collection-files-panel]').within(() => {
255                             cy.get('[data-cy=collection-files-right-panel]', { timeout: 5000 })
256                                 .should('contain', fileName);
257                             if (isWritable) {
258                                 cy.get('[data-cy=upload-button]')
259                                     .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
260                             }
261                         });
262                         // Test context menus
263                         cy.get('[data-cy=collection-files-panel]')
264                             .contains(fileName).rightclick();
265                         cy.get('[data-cy=context-menu]')
266                             .should('contain', 'Download')
267                             .and('not.contain', 'Open in new tab')
268                             .and('contain', 'Copy to clipboard')
269                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
270                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
271                         cy.get('body').click(); // Collapse the menu
272                         cy.get('[data-cy=collection-files-panel]')
273                             .contains(subDirName).rightclick();
274                         cy.get('[data-cy=context-menu]')
275                             .should('not.contain', 'Download')
276                             .and('not.contain', 'Open in new tab')
277                             .and('contain', 'Copy to clipboard')
278                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
279                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
280                         cy.get('body').click(); // Collapse the menu
281                         // File/dir item 'more options' button
282                         cy.get('[data-cy=file-item-options-btn')
283                             .first()
284                             .click()
285                         cy.get('[data-cy=context-menu]')
286                             .should(`${isWritable ? '' : 'not.'}contain`, 'Remove');
287                         cy.get('body').click(); // Collapse the menu
288                         // Hamburger 'more options' menu button
289                         cy.get('[data-cy=collection-files-panel-options-btn]')
290                             .click()
291                         cy.get('[data-cy=context-menu]')
292                             .should('contain', 'Select all')
293                             .click()
294                         cy.get('[data-cy=collection-files-panel-options-btn]')
295                             .click()
296                         cy.get('[data-cy=context-menu]')
297                             .should(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
298                         cy.get('body').click(); // Collapse the menu
299                     })
300             })
301         })
302     })
303
304     it('renames a file using valid names', function () {
305         function eachPair(lst, func){
306             for(var i=0; i < lst.length - 1; i++){
307                 func(lst[i], lst[i + 1])
308             }
309         }
310         // Creates the collection using the admin token so we can set up
311         // a bogus manifest text without block signatures.
312         cy.createCollection(adminUser.token, {
313             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
314             owner_uuid: activeUser.user.uuid,
315             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
316         })
317             .as('testCollection').then(function () {
318                 cy.loginAs(activeUser);
319                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
320
321                 const names = [
322                     'bar', // initial name already set
323                     '&',
324                     'foo',
325                     '&amp;',
326                     'I ❤️ ⛵️',
327                     '...',
328                     '#..',
329                     'some name with whitespaces',
330                     'some name with #2',
331                     'is this name legal? I hope it is',
332                     'some_file.pdf#',
333                     'some_file.pdf?',
334                     '?some_file.pdf',
335                     'some%file.pdf',
336                     'some%2Ffile.pdf',
337                     'some%22file.pdf',
338                     'some%20file.pdf',
339                     "G%C3%BCnter's%20file.pdf",
340                     'table%&?*2',
341                     'bar' // make sure we can go back to the original name as a last step
342                 ];
343                 eachPair(names, (from, to) => {
344                     cy.waitForDom().get('[data-cy=collection-files-panel]')
345                         .contains(`${from}`).rightclick();
346                     cy.get('[data-cy=context-menu]')
347                         .contains('Rename')
348                         .click();
349                     cy.get('[data-cy=form-dialog]')
350                         .should('contain', 'Rename')
351                         .within(() => {
352                             cy.get('input')
353                                 .type('{selectall}{backspace}')
354                                 .type(to, { parseSpecialCharSequences: false });
355                         });
356                     cy.get('[data-cy=form-submit-btn]').click();
357                     cy.get('[data-cy=collection-files-panel]')
358                         .should('not.contain', `${from}`)
359                         .and('contain', `${to}`);
360                 })
361             });
362     });
363
364     it('renames a file to a different directory', function () {
365         // Creates the collection using the admin token so we can set up
366         // a bogus manifest text without block signatures.
367         cy.createCollection(adminUser.token, {
368             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
369             owner_uuid: activeUser.user.uuid,
370             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
371         })
372             .as('testCollection').then(function () {
373                 cy.loginAs(activeUser);
374                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
375
376                 ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
377                     cy.get('[data-cy=collection-files-panel]')
378                         .contains('bar').rightclick();
379                     cy.get('[data-cy=context-menu]')
380                         .contains('Rename')
381                         .click();
382                     cy.get('[data-cy=form-dialog]')
383                         .should('contain', 'Rename')
384                         .within(() => {
385                             cy.get('input').type(`{selectall}{backspace}${subdir}/foo`);
386                         });
387                     cy.get('[data-cy=form-submit-btn]').click();
388                     cy.get('[data-cy=collection-files-panel]')
389                         .should('not.contain', 'bar')
390                         .and('contain', subdir);
391                     cy.get('[data-cy=collection-files-panel]').contains(subdir).click();
392
393                     // Rename 'subdir/foo' to 'bar'
394                     cy.wait(1000);
395                     cy.get('[data-cy=collection-files-panel]')
396                         .contains('foo').rightclick();
397                     cy.get('[data-cy=context-menu]')
398                         .contains('Rename')
399                         .click();
400                     cy.get('[data-cy=form-dialog]')
401                         .should('contain', 'Rename')
402                         .within(() => {
403                             cy.get('input')
404                                 .should('have.value', `${subdir}/foo`)
405                                 .type(`{selectall}{backspace}bar`);
406                         });
407                     cy.get('[data-cy=form-submit-btn]').click();
408
409                     cy.get('[data-cy=collection-files-panel]')
410                         .contains('Home')
411                         .click();
412
413                     cy.wait(2000);
414                     cy.get('[data-cy=collection-files-panel]')
415                         .should('contain', subdir) // empty dir kept
416                         .and('contain', 'bar');
417
418                     cy.get('[data-cy=collection-files-panel]')
419                         .contains(subdir).rightclick();
420                     cy.get('[data-cy=context-menu]')
421                         .contains('Remove')
422                         .click();
423                     cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
424                 });
425             });
426     });
427
428     it('tries to rename a file with illegal names', function () {
429         // Creates the collection using the admin token so we can set up
430         // a bogus manifest text without block signatures.
431         cy.createCollection(adminUser.token, {
432             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
433             owner_uuid: activeUser.user.uuid,
434             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
435         })
436             .as('testCollection').then(function () {
437                 cy.loginAs(activeUser);
438                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
439
440                 const illegalNamesFromUI = [
441                     ['.', "Name cannot be '.' or '..'"],
442                     ['..', "Name cannot be '.' or '..'"],
443                     ['', 'This field is required'],
444                     [' ', 'Leading/trailing whitespaces not allowed'],
445                     [' foo', 'Leading/trailing whitespaces not allowed'],
446                     ['foo ', 'Leading/trailing whitespaces not allowed'],
447                     ['//foo', 'Empty dir name not allowed']
448                 ]
449                 illegalNamesFromUI.forEach(([name, errMsg]) => {
450                     cy.get('[data-cy=collection-files-panel]')
451                         .contains('bar').rightclick();
452                     cy.get('[data-cy=context-menu]')
453                         .contains('Rename')
454                         .click();
455                     cy.get('[data-cy=form-dialog]')
456                         .should('contain', 'Rename')
457                         .within(() => {
458                             cy.get('input').type(`{selectall}{backspace}${name}`);
459                         });
460                     cy.get('[data-cy=form-dialog]')
461                         .should('contain', 'Rename')
462                         .within(() => {
463                             cy.contains(`${errMsg}`);
464                         });
465                     cy.get('[data-cy=form-cancel-btn]').click();
466                 })
467             });
468     });
469
470     it('can correctly display old versions', function () {
471         const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
472         let colUuid = '';
473         let oldVersionUuid = '';
474         // Make sure no other collections with this name exist
475         cy.doRequest('GET', '/arvados/v1/collections', null, {
476             filters: `[["name", "=", "${colName}"]]`,
477             include_old_versions: true
478         })
479             .its('body.items').as('collections')
480             .then(function () {
481                 expect(this.collections).to.be.empty;
482             });
483         // Creates the collection using the admin token so we can set up
484         // a bogus manifest text without block signatures.
485         cy.createCollection(adminUser.token, {
486             name: colName,
487             owner_uuid: activeUser.user.uuid,
488             preserve_version: true,
489             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
490         })
491             .as('originalVersion').then(function () {
492                 // Change the file name to create a new version.
493                 cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
494                     manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
495                 })
496                 colUuid = this.originalVersion.uuid;
497             });
498         // Confirm that there are 2 versions of the collection
499         cy.doRequest('GET', '/arvados/v1/collections', null, {
500             filters: `[["name", "=", "${colName}"]]`,
501             include_old_versions: true
502         })
503             .its('body.items').as('collections')
504             .then(function () {
505                 expect(this.collections).to.have.lengthOf(2);
506                 this.collections.map(function (aCollection) {
507                     expect(aCollection.current_version_uuid).to.equal(colUuid);
508                     if (aCollection.uuid !== aCollection.current_version_uuid) {
509                         oldVersionUuid = aCollection.uuid;
510                     }
511                 });
512                 // Check the old version displays as what it is.
513                 cy.loginAs(activeUser)
514                 cy.goToPath(`/collections/${oldVersionUuid}`);
515
516                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
517                 cy.get('[data-cy=read-only-icon]').should('exist');
518                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
519                 cy.get('[data-cy=collection-files-panel]').should('contain', 'bar');
520             });
521     });
522
523     it('views & edits storage classes data', function () {
524         const colName= `Test Collection ${Math.floor(Math.random() * 999999)}`;
525         cy.createCollection(adminUser.token, {
526             name: colName,
527             owner_uuid: activeUser.user.uuid,
528             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
529         }).as('collection').then(function () {
530             expect(this.collection.storage_classes_desired).to.deep.equal(['default'])
531
532             cy.loginAs(activeUser)
533             cy.goToPath(`/collections/${this.collection.uuid}`);
534
535             // Initial check: it should show the 'default' storage class
536             cy.get('[data-cy=collection-info-panel]')
537                 .should('contain', 'Storage classes')
538                 .and('contain', 'default')
539                 .and('not.contain', 'foo')
540                 .and('not.contain', 'bar');
541             // Edit collection: add storage class 'foo'
542             cy.get('[data-cy=collection-panel-options-btn]').click();
543             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
544             cy.get('[data-cy=form-dialog]')
545                 .should('contain', 'Edit Collection')
546                 .and('contain', 'Storage classes')
547                 .and('contain', 'default')
548                 .and('contain', 'foo')
549                 .and('contain', 'bar')
550                 .within(() => {
551                     cy.get('[data-cy=checkbox-foo]').click();
552                 });
553             cy.get('[data-cy=form-submit-btn]').click();
554             cy.get('[data-cy=collection-info-panel]')
555                 .should('contain', 'default')
556                 .and('contain', 'foo')
557                 .and('not.contain', 'bar');
558             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
559                 .its('body').as('updatedCollection')
560                 .then(function () {
561                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['default', 'foo']);
562                 });
563             // Edit collection: remove storage class 'default'
564             cy.get('[data-cy=collection-panel-options-btn]').click();
565             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
566             cy.get('[data-cy=form-dialog]')
567                 .should('contain', 'Edit Collection')
568                 .and('contain', 'Storage classes')
569                 .and('contain', 'default')
570                 .and('contain', 'foo')
571                 .and('contain', 'bar')
572                 .within(() => {
573                     cy.get('[data-cy=checkbox-default]').click();
574                 });
575             cy.get('[data-cy=form-submit-btn]').click();
576             cy.get('[data-cy=collection-info-panel]')
577                 .should('not.contain', 'default')
578                 .and('contain', 'foo')
579                 .and('not.contain', 'bar');
580             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
581                 .its('body').as('updatedCollection')
582                 .then(function () {
583                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['foo']);
584                 });
585         })
586     });
587
588     it('moves a collection to a different project', function () {
589         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
590         const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
591         const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
592
593         cy.createCollection(adminUser.token, {
594             name: collName,
595             owner_uuid: activeUser.user.uuid,
596             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
597         }).as('testCollection');
598         cy.createGroup(adminUser.token, {
599             name: projName,
600             group_class: 'project',
601             owner_uuid: activeUser.user.uuid,
602         }).as('testProject');
603
604         cy.getAll('@testCollection', '@testProject')
605             .then(function ([testCollection, testProject]) {
606                 cy.loginAs(activeUser);
607                 cy.goToPath(`/collections/${testCollection.uuid}`);
608                 cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
609                 cy.get('[data-cy=collection-info-panel]')
610                     .should('not.contain', projName)
611                     .and('not.contain', testProject.uuid);
612                 cy.get('[data-cy=collection-panel-options-btn]').click();
613                 cy.get('[data-cy=context-menu]').contains('Move to').click();
614                 cy.get('[data-cy=form-dialog]')
615                     .should('contain', 'Move to')
616                     .within(() => {
617                         cy.get('[data-cy=projects-tree-home-tree-picker]')
618                             .find('i')
619                             .click();
620                         cy.get('[data-cy=projects-tree-home-tree-picker]')
621                             .contains(projName)
622                             .click();
623                     });
624                 cy.get('[data-cy=form-submit-btn]').click();
625                 cy.get('[data-cy=snackbar]')
626                     .contains('Collection has been moved')
627                 cy.get('[data-cy=collection-info-panel]')
628                     .contains(projName).and('contain', testProject.uuid);
629                 // Double check that the collection is in the project
630                 cy.goToPath(`/projects/${testProject.uuid}`);
631                 cy.get('[data-cy=project-panel]').should('contain', collName);
632             });
633     });
634
635     it('automatically updates the collection UI contents without using the Refresh button', function () {
636         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
637         const fileName = 'foobar'
638
639         cy.createCollection(adminUser.token, {
640             name: collName,
641             owner_uuid: activeUser.user.uuid,
642         }).as('testCollection');
643
644         cy.getAll('@testCollection').then(function ([testCollection]) {
645             cy.loginAs(activeUser);
646             cy.goToPath(`/collections/${testCollection.uuid}`);
647             cy.get('[data-cy=collection-files-panel]').should('contain', 'This collection is empty');
648             cy.get('[data-cy=collection-files-panel]').should('not.contain', fileName);
649             cy.get('[data-cy=collection-info-panel]').should('contain', collName);
650
651             cy.updateCollection(adminUser.token, testCollection.uuid, {
652                 name: `${collName + ' updated'}`,
653                 manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
654             }).as('updatedCollection');
655             cy.getAll('@updatedCollection').then(function ([updatedCollection]) {
656                 expect(updatedCollection.name).to.equal(`${collName + ' updated'}`);
657                 cy.get('[data-cy=collection-info-panel]').should('contain', updatedCollection.name);
658                 cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
659             });
660         });
661     })
662
663     it('makes a copy of an existing collection', function() {
664         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
665         const copyName = `Copy of: ${collName}`;
666
667         cy.createCollection(adminUser.token, {
668             name: collName,
669             owner_uuid: activeUser.user.uuid,
670             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
671         }).as('collection').then(function () {
672             cy.loginAs(activeUser)
673             cy.goToPath(`/collections/${this.collection.uuid}`);
674             cy.get('[data-cy=collection-files-panel]')
675                 .should('contain', 'some-file');
676             cy.get('[data-cy=collection-panel-options-btn]').click();
677             cy.get('[data-cy=context-menu]').contains('Make a copy').click();
678             cy.get('[data-cy=form-dialog]')
679                 .should('contain', 'Make a copy')
680                 .within(() => {
681                     cy.get('[data-cy=projects-tree-home-tree-picker]')
682                         .contains('Projects')
683                         .click();
684                     cy.get('[data-cy=form-submit-btn]').click();
685                 });
686             cy.get('[data-cy=snackbar]')
687                 .contains('Collection has been copied.')
688             cy.get('[data-cy=snackbar-goto-action]').click();
689             cy.get('[data-cy=project-panel]')
690                 .contains(copyName).click();
691             cy.get('[data-cy=collection-files-panel]')
692                 .should('contain', 'some-file');
693         });
694     });
695
696     it('uses the collection version browser to view a previous version', function () {
697         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
698
699         // Creates the collection using the admin token so we can set up
700         // a bogus manifest text without block signatures.
701         cy.createCollection(adminUser.token, {
702             name: colName,
703             owner_uuid: activeUser.user.uuid,
704             preserve_version: true,
705             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
706         })
707             .as('collection').then(function () {
708                 // Visit collection, check basic information
709                 cy.loginAs(activeUser)
710                 cy.goToPath(`/collections/${this.collection.uuid}`);
711
712                 cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
713                 cy.get('[data-cy=read-only-icon]').should('not.exist');
714                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
715                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
716                 cy.get('[data-cy=collection-files-panel]').should('contain', 'foo').and('contain', 'bar');
717
718                 // Modify collection, expect version number change
719                 cy.get('[data-cy=collection-files-panel]').contains('foo').rightclick();
720                 cy.get('[data-cy=context-menu]').contains('Remove').click();
721                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Removing file');
722                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
723                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
724                 cy.get('[data-cy=collection-files-panel]').should('not.contain', 'foo').and('contain', 'bar');
725
726                 // Click on version number, check version browser. Click on past version.
727                 cy.get('[data-cy=collection-version-browser]').should('not.exist');
728                 cy.get('[data-cy=collection-version-number]').contains('2').click();
729                 cy.get('[data-cy=collection-version-browser]')
730                     .should('contain', 'Nr').and('contain', 'Size').and('contain', 'Date')
731                     .within(() => {
732                         // Version 1: 6 bytes in size
733                         cy.get('[data-cy=collection-version-browser-select-1]')
734                             .should('contain', '1')
735                             .and('contain', '6 B')
736                             .and('contain', adminUser.user.uuid);
737                         // Version 2: 3 bytes in size (one file removed)
738                         cy.get('[data-cy=collection-version-browser-select-2]')
739                             .should('contain', '2')
740                             .and('contain', '3 B')
741                             .and('contain', activeUser.user.full_name);
742                         cy.get('[data-cy=collection-version-browser-select-3]')
743                             .should('not.exist');
744                         cy.get('[data-cy=collection-version-browser-select-1]')
745                             .click();
746                     });
747                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
748                 cy.get('[data-cy=read-only-icon]').should('exist');
749                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
750                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
751                 cy.get('[data-cy=collection-files-panel]')
752                     .should('contain', 'foo').and('contain', 'bar');
753
754                 // Check that only old collection action are available on context menu
755                 cy.get('[data-cy=collection-panel-options-btn]').click();
756                 cy.get('[data-cy=context-menu]')
757                     .should('contain', 'Restore version')
758                     .and('not.contain', 'Add to favorites');
759                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
760
761                 // Click on "head version" link, confirm that it's the latest version.
762                 cy.get('[data-cy=collection-info-panel]').contains('head version').click();
763                 cy.get('[data-cy=collection-info-panel]')
764                     .should('not.contain', 'This is an old version');
765                 cy.get('[data-cy=read-only-icon]').should('not.exist');
766                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
767                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
768                 cy.get('[data-cy=collection-files-panel]').
769                     should('not.contain', 'foo').and('contain', 'bar');
770
771                 // Check that old collection action isn't available on context menu
772                 cy.get('[data-cy=collection-panel-options-btn]').click()
773                 cy.get('[data-cy=context-menu]').should('not.contain', 'Restore version')
774                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
775
776                 // Make another change, confirm new version.
777                 cy.get('[data-cy=collection-panel-options-btn]').click();
778                 cy.get('[data-cy=context-menu]').contains('Edit collection').click();
779                 cy.get('[data-cy=form-dialog]')
780                     .should('contain', 'Edit Collection')
781                     .within(() => {
782                         // appends some text
783                         cy.get('input').first().type(' renamed');
784                     });
785                 cy.get('[data-cy=form-submit-btn]').click();
786                 cy.get('[data-cy=collection-info-panel]')
787                     .should('not.contain', 'This is an old version');
788                 cy.get('[data-cy=read-only-icon]').should('not.exist');
789                 cy.get('[data-cy=collection-version-number]').should('contain', '3');
790                 cy.get('[data-cy=collection-info-panel]').should('contain', colName + ' renamed');
791                 cy.get('[data-cy=collection-files-panel]')
792                     .should('not.contain', 'foo').and('contain', 'bar');
793                 cy.get('[data-cy=collection-version-browser-select-3]')
794                     .should('contain', '3').and('contain', '3 B');
795
796                 // Check context menus on version browser
797                 cy.get('[data-cy=collection-version-browser-select-3]').rightclick()
798                 cy.get('[data-cy=context-menu]')
799                     .should('contain', 'Add to favorites')
800                     .and('contain', 'Make a copy')
801                     .and('contain', 'Edit collection');
802                 cy.get('body').click();
803                 // (and now an old version...)
804                 cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
805                 cy.get('[data-cy=context-menu]')
806                     .should('not.contain', 'Add to favorites')
807                     .and('contain', 'Make a copy')
808                     .and('not.contain', 'Edit collection');
809                 cy.get('body').click();
810
811                 // Restore first version
812                 cy.get('[data-cy=collection-version-browser]').within(() => {
813                     cy.get('[data-cy=collection-version-browser-select-1]').click();
814                 });
815                 cy.get('[data-cy=collection-panel-options-btn]').click()
816                 cy.get('[data-cy=context-menu]').contains('Restore version').click();
817                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Restore version');
818                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
819                 cy.get('[data-cy=collection-info-panel]')
820                     .should('not.contain', 'This is an old version');
821                 cy.get('[data-cy=collection-version-number]').should('contain', '4');
822                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
823                 cy.get('[data-cy=collection-files-panel]')
824                     .should('contain', 'foo').and('contain', 'bar');
825             });
826     });
827
828     it('creates collection from selected files of another collection', () => {
829         cy.createCollection(adminUser.token, {
830             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
831             owner_uuid: activeUser.user.uuid,
832             preserve_version: true,
833             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
834         })
835             .as('collection').then(function () {
836                 // Visit collection, check basic information
837                 cy.loginAs(activeUser)
838                 cy.goToPath(`/collections/${this.collection.uuid}`);
839
840                 cy.get('[data-cy=collection-files-panel]').within(() => {
841                     cy.get('input[type=checkbox]').first().click();
842                 });
843
844                 cy.get('[data-cy=collection-files-panel-options-btn]').click();
845                 cy.get('[data-cy=context-menu]').contains('Create a new collection with selected').click();
846
847                 cy.get('[data-cy=form-dialog]').contains('Projects').click();
848
849                 cy.get('[data-cy=form-submit-btn]').click();
850
851                 cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click();
852
853                 cy.get('main').contains(`Files extracted from: ${this.collection.name}`).should('exist');
854             });
855     });
856
857     it('creates new collection with properties on home project', function () {
858         cy.loginAs(activeUser);
859         cy.goToPath(`/projects/${activeUser.user.uuid}`);
860         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
861         cy.get('[data-cy=breadcrumb-last]').should('not.exist');
862         // Create new collection
863         cy.get('[data-cy=side-panel-button]').click();
864         cy.get('[data-cy=side-panel-new-collection]').click();
865         // Name between brackets tests bugfix #17582
866         const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
867
868         // Select a storage class.
869         cy.get('[data-cy=form-dialog]')
870             .should('contain', 'New collection')
871             .and('contain', 'Storage classes')
872             .and('contain', 'default')
873             .and('contain', 'foo')
874             .and('contain', 'bar')
875             .within(() => {
876                 cy.get('[data-cy=parent-field]').within(() => {
877                     cy.get('input').should('have.value', 'Home project');
878                 });
879                 cy.get('[data-cy=name-field]').within(() => {
880                     cy.get('input').type(collName);
881                 });
882                 cy.get('[data-cy=checkbox-foo]').click();
883             })
884
885         // Add a property.
886         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
887         cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta');
888         cy.get('[data-cy=resource-properties-form]').within(() => {
889             cy.get('[data-cy=property-field-key]').within(() => {
890                 cy.get('input').type('Color');
891             });
892             cy.get('[data-cy=property-field-value]').within(() => {
893                 cy.get('input').type('Magenta');
894             });
895             cy.root().submit();
896         });
897         // Confirm proper vocabulary labels are displayed on the UI.
898         cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
899
900         cy.get('[data-cy=form-submit-btn]').click();
901         // Confirm that the user was taken to the newly created collection
902         cy.get('[data-cy=form-dialog]').should('not.exist');
903         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
904         cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
905         cy.get('[data-cy=collection-info-panel]')
906             .should('contain', 'default')
907             .and('contain', 'foo')
908             .and('contain', 'Color: Magenta')
909             .and('not.contain', 'bar');
910         // Confirm that the collection's properties has the real values.
911         cy.doRequest('GET', '/arvados/v1/collections', null, {
912             filters: `[["name", "=", "${collName}"]]`,
913         })
914         .its('body.items').as('collections')
915         .then(function() {
916             expect(this.collections).to.have.lengthOf(1);
917             expect(this.collections[0].properties).to.have.property(
918                 'IDTAGCOLORS', 'IDVALCOLORS3');
919         });
920     });
921
922     it('shows responsible person for collection if available', () => {
923         cy.createCollection(adminUser.token, {
924             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
925             owner_uuid: activeUser.user.uuid,
926             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
927         }).as('testCollection1');
928
929         cy.createCollection(adminUser.token, {
930             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
931             owner_uuid: adminUser.user.uuid,
932             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
933         }).as('testCollection2').then(function (testCollection2) {
934             cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
935         });
936
937         cy.getAll('@testCollection1', '@testCollection2')
938             .then(function ([testCollection1, testCollection2]) {
939                 cy.loginAs(activeUser);
940
941                 cy.goToPath(`/collections/${testCollection1.uuid}`);
942                 cy.get('[data-cy=responsible-person-wrapper]')
943                     .contains(activeUser.user.uuid);
944
945                 cy.goToPath(`/collections/${testCollection2.uuid}`);
946                 cy.get('[data-cy=responsible-person-wrapper]')
947                     .contains(adminUser.user.uuid);
948             });
949     });
950
951     describe('file upload', () => {
952         beforeEach(() => {
953             cy.createCollection(adminUser.token, {
954                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
955                 owner_uuid: activeUser.user.uuid,
956                 manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
957             }).as('testCollection1');
958         });
959
960         it('uploads a file and checks the collection UI to be fresh', () => {
961             cy.getAll('@testCollection1')
962                 .then(function([testCollection1]) {
963                     cy.loginAs(activeUser);
964                     cy.goToPath(`/collections/${testCollection1.uuid}`);
965                     cy.get('[data-cy=upload-button]').click();
966                     cy.get('[data-cy=collection-files-panel]')
967                         .contains('5mb_a.bin').should('not.exist');
968                     cy.get('[data-cy=collection-file-count]').should('contain', '2');
969                     cy.fixture('files/5mb.bin', 'base64').then(content => {
970                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
971                         cy.get('[data-cy=form-submit-btn]').click();
972                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
973                         cy.get('[data-cy=collection-files-panel]')
974                             .contains('5mb_a.bin').should('exist');
975                         cy.get('[data-cy=collection-file-count]').should('contain', '3');
976
977                         cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
978                         cy.get('[data-cy=upload-button]').click();
979                         cy.fixture('files/5mb.bin', 'base64').then(content => {
980                             cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
981                             cy.get('[data-cy=form-submit-btn]').click();
982                             cy.get('[data-cy=form-submit-btn]').should('not.exist');
983                             cy.get('[data-cy=collection-files-right-panel]')
984                                  .contains('5mb_b.bin').should('exist');
985                         });
986                     });
987                 });
988         });
989
990         it('allows to cancel running upload', () => {
991             cy.getAll('@testCollection1')
992                 .then(function([testCollection1]) {
993                     cy.loginAs(activeUser);
994
995                     cy.goToPath(`/collections/${testCollection1.uuid}`);
996
997                     cy.get('[data-cy=upload-button]').click();
998
999                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1000                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1001                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1002
1003                         cy.get('[data-cy=form-submit-btn]').click();
1004
1005                         cy.get('button').contains('Cancel').click();
1006
1007                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1008                     });
1009                 });
1010         });
1011
1012         it('allows to cancel single file from the running upload', () => {
1013             cy.getAll('@testCollection1')
1014                 .then(function([testCollection1]) {
1015                     cy.loginAs(activeUser);
1016
1017                     cy.goToPath(`/collections/${testCollection1.uuid}`);
1018
1019                     cy.get('[data-cy=upload-button]').click();
1020
1021                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1022                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1023                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1024
1025                         cy.get('[data-cy=form-submit-btn]').click();
1026
1027                         cy.get('button[aria-label=Remove]').eq(1).click();
1028
1029                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1030
1031                         cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
1032                     });
1033                 });
1034         });
1035
1036         it('allows to cancel all files from the running upload', () => {
1037             cy.getAll('@testCollection1')
1038                 .then(function([testCollection1]) {
1039                     cy.loginAs(activeUser);
1040
1041                     cy.goToPath(`/collections/${testCollection1.uuid}`);
1042
1043                     // Confirm initial collection state.
1044                     cy.get('[data-cy=collection-files-panel]')
1045                         .contains('bar').should('exist');
1046                     cy.get('[data-cy=collection-files-panel]')
1047                         .contains('5mb_a.bin').should('not.exist');
1048                     cy.get('[data-cy=collection-files-panel]')
1049                         .contains('5mb_b.bin').should('not.exist');
1050
1051                     cy.get('[data-cy=upload-button]').click();
1052
1053                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1054                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1055                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1056
1057                         cy.get('[data-cy=form-submit-btn]').click();
1058
1059                         cy.get('button[aria-label=Remove]').should('exist');
1060                         cy.get('button[aria-label=Remove]')
1061                             .click({ multiple: true, force: true });
1062
1063                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1064
1065                         // Confirm final collection state.
1066                         cy.get('[data-cy=collection-files-panel]')
1067                             .contains('bar').should('exist');
1068                         // The following fails, but doesn't seem to happen
1069                         // in the real world. Maybe there's a race between
1070                         // the PUT request finishing and the 'Remove' button
1071                         // dissapearing, because sometimes just one of the 2
1072                         // files gets uploaded.
1073                         // Maybe this will be needed to simulate a slow network:
1074                         // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
1075                         // cy.get('[data-cy=collection-files-panel]')
1076                         //     .contains('5mb_a.bin').should('not.exist');
1077                         // cy.get('[data-cy=collection-files-panel]')
1078                         //     .contains('5mb_b.bin').should('not.exist');
1079                     });
1080                 });
1081         });
1082     });
1083 })