18219: Fixes collection edit dialog. Adds test.
[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 property 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=property-editor-btn]').click();
141                 });
142                 cy.get('[data-cy=resource-properties-dialog').contains('Edit properties');
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=resource-properties-dialog]')
156                     .should('contain', 'Color: Magenta');
157                 // Confirm proper vocabulary IDs were saved on the backend.
158                 cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
159                     .its('body').as('collection')
160                     .then(function () {
161                         expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
162                     });
163
164                 // Case-insensitive on-blur auto-selection test
165                 // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
166                 cy.get('[data-cy=resource-properties-form]').within(() => {
167                     cy.get('[data-cy=property-field-key]').within(() => {
168                         cy.get('input').type('sIzE');
169                     });
170                     cy.get('[data-cy=property-field-value]').within(() => {
171                         cy.get('input').type('sMaLL');
172                     });
173                     // Cannot "type()" TAB on Cypress so let's click another field
174                     // to trigger the onBlur event.
175                     cy.get('[data-cy=property-field-key]').click();
176                     cy.root().submit();
177                 });
178                 // Confirm proper vocabulary labels are displayed on the UI.
179                 cy.get('[data-cy=resource-properties-dialog]')
180                     .should('contain', 'Size: S');
181                 // Confirm proper vocabulary IDs were saved on the backend.
182                 cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
183                     .its('body').as('collection')
184                     .then(function () {
185                         expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2');
186                     });
187
188                 // Close property editor & confirm properties display on the UI.
189                 cy.get('[data-cy=resource-properties-dialog]').within(() => {
190                     cy.get('[data-cy=close-dialog-btn]').click();
191                 });
192                 cy.get('[data-cy=collection-info-panel')
193                     .should('contain', this.testCollection.name)
194                     .and('contain', 'Color: Magenta')
195                     .and('contain', 'Size: S');
196             });
197     });
198
199     it('shows collection by URL', function () {
200         cy.loginAs(activeUser);
201         [true, false].map(function (isWritable) {
202             // Using different file names to avoid test flakyness: the second iteration
203             // on this loop may pass an assertion from the first iteration by looking
204             // for the same file name.
205             const fileName = isWritable ? 'bar' : 'foo';
206             const subDirName = 'subdir';
207             cy.createGroup(adminUser.token, {
208                 name: 'Shared project',
209                 group_class: 'project',
210             }).as('sharedGroup').then(function () {
211                 // Creates the collection using the admin token so we can set up
212                 // a bogus manifest text without block signatures.
213                 cy.doRequest('GET', '/arvados/v1/config', null, null)
214                     .its('body').should((clusterConfig) => {
215                       expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", false);
216                       expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
217                       expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
218                       const inlineUrl = clusterConfig.Services.WebDAV.ExternalURL !== ""
219                           ? clusterConfig.Services.WebDAV.ExternalURL
220                           : clusterConfig.Services.WebDAVDownload.ExternalURL;
221                       expect(inlineUrl).to.not.contain("*");
222                     })
223                     .createCollection(adminUser.token, {
224                       name: 'Test collection',
225                       owner_uuid: this.sharedGroup.uuid,
226                       properties: { someKey: 'someValue' },
227                       manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
228                     })
229                     .as('testCollection').then(function () {
230                         // Share the group with active user.
231                         cy.createLink(adminUser.token, {
232                             name: isWritable ? 'can_write' : 'can_read',
233                             link_class: 'permission',
234                             head_uuid: this.sharedGroup.uuid,
235                             tail_uuid: activeUser.user.uuid
236                         })
237                         cy.goToPath(`/collections/${this.testCollection.uuid}`);
238
239                         // Check that name & uuid are correct.
240                         cy.get('[data-cy=collection-info-panel]')
241                             .should('contain', this.testCollection.name)
242                             .and('contain', this.testCollection.uuid)
243                             .and('not.contain', 'This is an old version');
244                         // Check for the read-only icon
245                         cy.get('[data-cy=read-only-icon]').should(`${isWritable ? 'not.' : ''}exist`);
246                         // Check that both read and write operations are available on
247                         // the 'More options' menu.
248                         cy.get('[data-cy=collection-panel-options-btn]')
249                             .click()
250                         cy.get('[data-cy=context-menu]')
251                             .should('contain', 'Add to favorites')
252                             .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection');
253                         cy.get('body').click(); // Collapse the menu avoiding details panel expansion
254                         cy.get('[data-cy=collection-info-panel]')
255                             .should('contain', 'someKey: someValue')
256                             .and('not.contain', 'anotherKey: anotherValue');
257                         // Check that the file listing show both read & write operations
258                         cy.get('[data-cy=collection-files-panel]').within(() => {
259                             cy.wait(1000);
260                             cy.root().should('contain', fileName);
261                             if (isWritable) {
262                                 cy.get('[data-cy=upload-button]')
263                                     .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
264                             }
265                         });
266                         // Test context menus
267                         cy.get('[data-cy=collection-files-panel]')
268                             .contains(fileName).rightclick({ force: true });
269                         cy.get('[data-cy=context-menu]')
270                             .should('contain', 'Download')
271                             .and('not.contain', 'Open in new tab')
272                             .and('contain', 'Copy to clipboard')
273                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
274                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
275                         cy.get('body').click(); // Collapse the menu
276                         cy.get('[data-cy=collection-files-panel]')
277                             .contains(subDirName).rightclick({ force: true });
278                         cy.get('[data-cy=context-menu]')
279                             .should('not.contain', 'Download')
280                             .and('not.contain', 'Open in new tab')
281                             .and('contain', 'Copy to clipboard')
282                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
283                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
284                         cy.get('body').click(); // Collapse the menu
285                         // Hamburger 'more options' menu button
286                         cy.get('[data-cy=collection-files-panel-options-btn]')
287                             .click()
288                         cy.get('[data-cy=context-menu]')
289                             .should('contain', 'Select all')
290                             .click()
291                         cy.get('[data-cy=collection-files-panel-options-btn]')
292                             .click()
293                         cy.get('[data-cy=context-menu]')
294                             .should(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
295                         cy.get('body').click(); // Collapse the menu
296                     })
297             })
298         })
299     })
300
301     it('renames a file using valid names', function () {
302         function eachPair(lst, func){
303             for(var i=0; i < lst.length - 1; i++){
304                 func(lst[i], lst[i + 1])
305             }
306         }
307         // Creates the collection using the admin token so we can set up
308         // a bogus manifest text without block signatures.
309         cy.createCollection(adminUser.token, {
310             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
311             owner_uuid: activeUser.user.uuid,
312             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
313         })
314             .as('testCollection').then(function () {
315                 cy.loginAs(activeUser);
316                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
317
318                 const names = [
319                     'bar', // initial name already set
320                     '&',
321                     'foo',
322                     '&amp;',
323                     'I ❤️ ⛵️',
324                     '...',
325                     '#..',
326                     'some name with whitespaces',
327                     'some name with #2',
328                     'is this name legal? I hope it is',
329                     'some_file.pdf#',
330                     'some_file.pdf?',
331                     '?some_file.pdf',
332                     'some%file.pdf',
333                     'some%2Ffile.pdf',
334                     'some%22file.pdf',
335                     'some%20file.pdf',
336                     "G%C3%BCnter's%20file.pdf",
337                     'table%&?*2',
338                     'bar' // make sure we can go back to the original name as a last step
339                 ];
340                 eachPair(names, (from, to) => {
341                     cy.get('[data-cy=collection-files-panel]')
342                         .contains(`${from}`).rightclick();
343                     cy.get('[data-cy=context-menu]')
344                         .contains('Rename')
345                         .click();
346                     cy.get('[data-cy=form-dialog]')
347                         .should('contain', 'Rename')
348                         .within(() => {
349                             cy.get('input')
350                                 .type('{selectall}{backspace}')
351                                 .type(to, { parseSpecialCharSequences: false });
352                         });
353                     cy.get('[data-cy=form-submit-btn]').click();
354                     cy.get('[data-cy=collection-files-panel]')
355                         .should('not.contain', `${from}`)
356                         .and('contain', `${to}`);
357                 })
358             });
359     });
360
361     it('renames a file to a different directory', function () {
362         // Creates the collection using the admin token so we can set up
363         // a bogus manifest text without block signatures.
364         cy.createCollection(adminUser.token, {
365             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
366             owner_uuid: activeUser.user.uuid,
367             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
368         })
369             .as('testCollection').then(function () {
370                 cy.loginAs(activeUser);
371                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
372
373                 ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
374                     cy.get('[data-cy=collection-files-panel]')
375                         .contains('bar').rightclick({force: true});
376                     cy.get('[data-cy=context-menu]')
377                         .contains('Rename')
378                         .click();
379                     cy.get('[data-cy=form-dialog]')
380                         .should('contain', 'Rename')
381                         .within(() => {
382                             cy.get('input').type(`{selectall}{backspace}${subdir}/foo`);
383                         });
384                     cy.get('[data-cy=form-submit-btn]').click();
385                     cy.get('[data-cy=collection-files-panel]')
386                         .should('not.contain', 'bar')
387                         .and('contain', subdir);
388                     cy.wait(1000);
389                     cy.get('[data-cy=collection-files-panel]').contains(subdir).click();
390                     // Rename 'subdir/foo' to 'foo'
391                     cy.wait(1000);
392                     cy.get('[data-cy=collection-files-panel]')
393                         .contains('foo').rightclick();
394                     cy.get('[data-cy=context-menu]')
395                         .contains('Rename')
396                         .click();
397                     cy.get('[data-cy=form-dialog]')
398                         .should('contain', 'Rename')
399                         .within(() => {
400                             cy.get('input')
401                                 .should('have.value', `${subdir}/foo`)
402                                 .type(`{selectall}{backspace}bar`);
403                         });
404                     cy.get('[data-cy=form-submit-btn]').click();
405
406                     cy.wait(1000);
407                     cy.get('[data-cy=collection-files-panel]')
408                         .contains('Home')
409                         .click();
410
411                     cy.wait(2000);
412                     cy.get('[data-cy=collection-files-panel]')
413                         .should('contain', subdir) // empty dir kept
414                         .and('contain', 'bar');
415
416                     cy.get('[data-cy=collection-files-panel]')
417                         .contains(subdir).rightclick();
418                     cy.get('[data-cy=context-menu]')
419                         .contains('Remove')
420                         .click();
421                     cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
422                 });
423             });
424     });
425
426     it('tries to rename a file with illegal names', function () {
427         // Creates the collection using the admin token so we can set up
428         // a bogus manifest text without block signatures.
429         cy.createCollection(adminUser.token, {
430             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
431             owner_uuid: activeUser.user.uuid,
432             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
433         })
434             .as('testCollection').then(function () {
435                 cy.loginAs(activeUser);
436                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
437
438                 const illegalNamesFromUI = [
439                     ['.', "Name cannot be '.' or '..'"],
440                     ['..', "Name cannot be '.' or '..'"],
441                     ['', 'This field is required'],
442                     [' ', 'Leading/trailing whitespaces not allowed'],
443                     [' foo', 'Leading/trailing whitespaces not allowed'],
444                     ['foo ', 'Leading/trailing whitespaces not allowed'],
445                     ['//foo', 'Empty dir name not allowed']
446                 ]
447                 illegalNamesFromUI.forEach(([name, errMsg]) => {
448                     cy.get('[data-cy=collection-files-panel]')
449                         .contains('bar').rightclick();
450                     cy.get('[data-cy=context-menu]')
451                         .contains('Rename')
452                         .click();
453                     cy.get('[data-cy=form-dialog]')
454                         .should('contain', 'Rename')
455                         .within(() => {
456                             cy.get('input').type(`{selectall}{backspace}${name}`);
457                         });
458                     cy.get('[data-cy=form-dialog]')
459                         .should('contain', 'Rename')
460                         .within(() => {
461                             cy.contains(`${errMsg}`);
462                         });
463                     cy.get('[data-cy=form-cancel-btn]').click();
464                 })
465             });
466     });
467
468     it('can correctly display old versions', function () {
469         const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
470         let colUuid = '';
471         let oldVersionUuid = '';
472         // Make sure no other collections with this name exist
473         cy.doRequest('GET', '/arvados/v1/collections', null, {
474             filters: `[["name", "=", "${colName}"]]`,
475             include_old_versions: true
476         })
477             .its('body.items').as('collections')
478             .then(function () {
479                 expect(this.collections).to.be.empty;
480             });
481         // Creates the collection using the admin token so we can set up
482         // a bogus manifest text without block signatures.
483         cy.createCollection(adminUser.token, {
484             name: colName,
485             owner_uuid: activeUser.user.uuid,
486             preserve_version: true,
487             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
488         })
489             .as('originalVersion').then(function () {
490                 // Change the file name to create a new version.
491                 cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
492                     manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
493                 })
494                 colUuid = this.originalVersion.uuid;
495             });
496         // Confirm that there are 2 versions of the collection
497         cy.doRequest('GET', '/arvados/v1/collections', null, {
498             filters: `[["name", "=", "${colName}"]]`,
499             include_old_versions: true
500         })
501             .its('body.items').as('collections')
502             .then(function () {
503                 expect(this.collections).to.have.lengthOf(2);
504                 this.collections.map(function (aCollection) {
505                     expect(aCollection.current_version_uuid).to.equal(colUuid);
506                     if (aCollection.uuid !== aCollection.current_version_uuid) {
507                         oldVersionUuid = aCollection.uuid;
508                     }
509                 });
510                 // Check the old version displays as what it is.
511                 cy.loginAs(activeUser)
512                 cy.goToPath(`/collections/${oldVersionUuid}`);
513
514                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
515                 cy.get('[data-cy=read-only-icon]').should('exist');
516                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
517                 cy.get('[data-cy=collection-files-panel]').should('contain', 'bar');
518             });
519     });
520
521     it('views & edits storage classes data', function () {
522         const colName= `Test Collection ${Math.floor(Math.random() * 999999)}`;
523         cy.createCollection(adminUser.token, {
524             name: colName,
525             owner_uuid: activeUser.user.uuid,
526             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
527         }).as('collection').then(function () {
528             expect(this.collection.storage_classes_desired).to.deep.equal(['default'])
529
530             cy.loginAs(activeUser)
531             cy.goToPath(`/collections/${this.collection.uuid}`);
532
533             // Initial check: it should show the 'default' storage class
534             cy.get('[data-cy=collection-info-panel]')
535                 .should('contain', 'Storage classes')
536                 .and('contain', 'default')
537                 .and('not.contain', 'foo')
538                 .and('not.contain', 'bar');
539             // Edit collection: add storage class 'foo'
540             cy.get('[data-cy=collection-panel-options-btn]').click();
541             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
542             cy.get('[data-cy=form-dialog]')
543                 .should('contain', 'Edit Collection')
544                 .and('contain', 'Storage classes')
545                 .and('contain', 'default')
546                 .and('contain', 'foo')
547                 .and('contain', 'bar')
548                 .within(() => {
549                     cy.get('[data-cy=checkbox-foo]').click();
550                 });
551             cy.get('[data-cy=form-submit-btn]').click();
552             cy.get('[data-cy=collection-info-panel]')
553                 .should('contain', 'default')
554                 .and('contain', 'foo')
555                 .and('not.contain', 'bar');
556             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
557                 .its('body').as('updatedCollection')
558                 .then(function () {
559                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['default', 'foo']);
560                 });
561             // Edit collection: remove storage class 'default'
562             cy.get('[data-cy=collection-panel-options-btn]').click();
563             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
564             cy.get('[data-cy=form-dialog]')
565                 .should('contain', 'Edit Collection')
566                 .and('contain', 'Storage classes')
567                 .and('contain', 'default')
568                 .and('contain', 'foo')
569                 .and('contain', 'bar')
570                 .within(() => {
571                     cy.get('[data-cy=checkbox-default]').click();
572                 });
573             cy.get('[data-cy=form-submit-btn]').click();
574             cy.get('[data-cy=collection-info-panel]')
575                 .should('not.contain', 'default')
576                 .and('contain', 'foo')
577                 .and('not.contain', 'bar');
578             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
579                 .its('body').as('updatedCollection')
580                 .then(function () {
581                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['foo']);
582                 });
583         })
584     });
585
586     it('moves a collection to a different project', function () {
587         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
588         const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
589         const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
590
591         cy.createCollection(adminUser.token, {
592             name: collName,
593             owner_uuid: activeUser.user.uuid,
594             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
595         }).as('testCollection');
596         cy.createGroup(adminUser.token, {
597             name: projName,
598             group_class: 'project',
599             owner_uuid: activeUser.user.uuid,
600         }).as('testProject');
601
602         cy.getAll('@testCollection', '@testProject')
603             .then(function ([testCollection, testProject]) {
604                 cy.loginAs(activeUser);
605                 cy.goToPath(`/collections/${testCollection.uuid}`);
606                 cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
607                 cy.get('[data-cy=collection-info-panel]')
608                     .should('not.contain', projName)
609                     .and('not.contain', testProject.uuid);
610                 cy.get('[data-cy=collection-panel-options-btn]').click();
611                 cy.get('[data-cy=context-menu]').contains('Move to').click();
612                 cy.get('[data-cy=form-dialog]')
613                     .should('contain', 'Move to')
614                     .within(() => {
615                         cy.get('[data-cy=projects-tree-home-tree-picker]')
616                             .find('i')
617                             .click();
618                         cy.get('[data-cy=projects-tree-home-tree-picker]')
619                             .contains(projName)
620                             .click();
621                     });
622                 cy.get('[data-cy=form-submit-btn]').click();
623                 cy.get('[data-cy=snackbar]')
624                     .contains('Collection has been moved')
625                 cy.get('[data-cy=collection-info-panel]')
626                     .contains(projName).and('contain', testProject.uuid);
627                 // Double check that the collection is in the project
628                 cy.goToPath(`/projects/${testProject.uuid}`);
629                 cy.get('[data-cy=project-panel]').should('contain', collName);
630             });
631     });
632
633     it('makes a copy of an existing collection', function() {
634         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
635         const copyName = `Copy of: ${collName}`;
636
637         cy.createCollection(adminUser.token, {
638             name: collName,
639             owner_uuid: activeUser.user.uuid,
640             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
641         }).as('collection').then(function () {
642             cy.loginAs(activeUser)
643             cy.goToPath(`/collections/${this.collection.uuid}`);
644             cy.get('[data-cy=collection-files-panel]')
645                 .should('contain', 'some-file');
646             cy.get('[data-cy=collection-panel-options-btn]').click();
647             cy.get('[data-cy=context-menu]').contains('Make a copy').click();
648             cy.get('[data-cy=form-dialog]')
649                 .should('contain', 'Make a copy')
650                 .within(() => {
651                     cy.get('[data-cy=projects-tree-home-tree-picker]')
652                         .contains('Projects')
653                         .click();
654                     cy.get('[data-cy=form-submit-btn]').click();
655                 });
656             cy.get('[data-cy=snackbar]')
657                 .contains('Collection has been copied.')
658             cy.get('[data-cy=snackbar-goto-action]').click();
659             cy.get('[data-cy=project-panel]')
660                 .contains(copyName).click();
661             cy.get('[data-cy=collection-files-panel]')
662                 .should('contain', 'some-file');
663         });
664     });
665
666     it('uses the collection version browser to view a previous version', function () {
667         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
668
669         // Creates the collection using the admin token so we can set up
670         // a bogus manifest text without block signatures.
671         cy.createCollection(adminUser.token, {
672             name: colName,
673             owner_uuid: activeUser.user.uuid,
674             preserve_version: true,
675             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
676         })
677             .as('collection').then(function () {
678                 // Visit collection, check basic information
679                 cy.loginAs(activeUser)
680                 cy.goToPath(`/collections/${this.collection.uuid}`);
681
682                 cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
683                 cy.get('[data-cy=read-only-icon]').should('not.exist');
684                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
685                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
686                 cy.get('[data-cy=collection-files-panel]').should('contain', 'foo').and('contain', 'bar');
687
688                 // Modify collection, expect version number change
689                 cy.get('[data-cy=collection-files-panel]').contains('foo').rightclick();
690                 cy.get('[data-cy=context-menu]').contains('Remove').click();
691                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Removing file');
692                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
693                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
694                 cy.get('[data-cy=collection-files-panel]').should('not.contain', 'foo').and('contain', 'bar');
695
696                 // Click on version number, check version browser. Click on past version.
697                 cy.get('[data-cy=collection-version-browser]').should('not.exist');
698                 cy.get('[data-cy=collection-version-number]').contains('2').click();
699                 cy.get('[data-cy=collection-version-browser]')
700                     .should('contain', 'Nr').and('contain', 'Size').and('contain', 'Date')
701                     .within(() => {
702                         // Version 1: 6 bytes in size
703                         cy.get('[data-cy=collection-version-browser-select-1]')
704                             .should('contain', '1')
705                             .and('contain', '6 B')
706                             .and('contain', adminUser.user.uuid);
707                         // Version 2: 3 bytes in size (one file removed)
708                         cy.get('[data-cy=collection-version-browser-select-2]')
709                             .should('contain', '2')
710                             .and('contain', '3 B')
711                             .and('contain', activeUser.user.full_name);
712                         cy.get('[data-cy=collection-version-browser-select-3]')
713                             .should('not.exist');
714                         cy.get('[data-cy=collection-version-browser-select-1]')
715                             .click();
716                     });
717                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
718                 cy.get('[data-cy=read-only-icon]').should('exist');
719                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
720                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
721                 cy.get('[data-cy=collection-files-panel]')
722                     .should('contain', 'foo').and('contain', 'bar');
723
724                 // Check that only old collection action are available on context menu
725                 cy.get('[data-cy=collection-panel-options-btn]').click();
726                 cy.get('[data-cy=context-menu]')
727                     .should('contain', 'Restore version')
728                     .and('not.contain', 'Add to favorites');
729                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
730
731                 // Click on "head version" link, confirm that it's the latest version.
732                 cy.get('[data-cy=collection-info-panel]').contains('head version').click();
733                 cy.get('[data-cy=collection-info-panel]')
734                     .should('not.contain', 'This is an old version');
735                 cy.get('[data-cy=read-only-icon]').should('not.exist');
736                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
737                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
738                 cy.get('[data-cy=collection-files-panel]').
739                     should('not.contain', 'foo').and('contain', 'bar');
740
741                 // Check that old collection action isn't available on context menu
742                 cy.get('[data-cy=collection-panel-options-btn]').click()
743                 cy.get('[data-cy=context-menu]').should('not.contain', 'Restore version')
744                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
745
746                 // Make another change, confirm new version.
747                 cy.get('[data-cy=collection-panel-options-btn]').click();
748                 cy.get('[data-cy=context-menu]').contains('Edit collection').click();
749                 cy.get('[data-cy=form-dialog]')
750                     .should('contain', 'Edit Collection')
751                     .within(() => {
752                         // appends some text
753                         cy.get('input').first().type(' renamed');
754                     });
755                 cy.get('[data-cy=form-submit-btn]').click();
756                 cy.get('[data-cy=collection-info-panel]')
757                     .should('not.contain', 'This is an old version');
758                 cy.get('[data-cy=read-only-icon]').should('not.exist');
759                 cy.get('[data-cy=collection-version-number]').should('contain', '3');
760                 cy.get('[data-cy=collection-info-panel]').should('contain', colName + ' renamed');
761                 cy.get('[data-cy=collection-files-panel]')
762                     .should('not.contain', 'foo').and('contain', 'bar');
763                 cy.get('[data-cy=collection-version-browser-select-3]')
764                     .should('contain', '3').and('contain', '3 B');
765
766                 // Check context menus on version browser
767                 cy.get('[data-cy=collection-version-browser-select-3]').rightclick()
768                 cy.get('[data-cy=context-menu]')
769                     .should('contain', 'Add to favorites')
770                     .and('contain', 'Make a copy')
771                     .and('contain', 'Edit collection');
772                 cy.get('body').click();
773                 // (and now an old version...)
774                 cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
775                 cy.get('[data-cy=context-menu]')
776                     .should('not.contain', 'Add to favorites')
777                     .and('contain', 'Make a copy')
778                     .and('not.contain', 'Edit collection');
779                 cy.get('body').click();
780
781                 // Restore first version
782                 cy.get('[data-cy=collection-version-browser]').within(() => {
783                     cy.get('[data-cy=collection-version-browser-select-1]').click();
784                 });
785                 cy.get('[data-cy=collection-panel-options-btn]').click()
786                 cy.get('[data-cy=context-menu]').contains('Restore version').click();
787                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Restore version');
788                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
789                 cy.get('[data-cy=collection-info-panel]')
790                     .should('not.contain', 'This is an old version');
791                 cy.get('[data-cy=collection-version-number]').should('contain', '4');
792                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
793                 cy.get('[data-cy=collection-files-panel]')
794                     .should('contain', 'foo').and('contain', 'bar');
795             });
796     });
797
798     it('creates new collection with properties on home project', function () {
799         cy.loginAs(activeUser);
800         cy.goToPath(`/projects/${activeUser.user.uuid}`);
801         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
802         cy.get('[data-cy=breadcrumb-last]').should('not.exist');
803         // Create new collection
804         cy.get('[data-cy=side-panel-button]').click();
805         cy.get('[data-cy=side-panel-new-collection]').click();
806         // Name between brackets tests bugfix #17582
807         const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
808
809         // Select a storage class.
810         cy.get('[data-cy=form-dialog]')
811             .should('contain', 'New collection')
812             .and('contain', 'Storage classes')
813             .and('contain', 'default')
814             .and('contain', 'foo')
815             .and('contain', 'bar')
816             .within(() => {
817                 cy.get('[data-cy=parent-field]').within(() => {
818                     cy.get('input').should('have.value', 'Home project');
819                 });
820                 cy.get('[data-cy=name-field]').within(() => {
821                     cy.get('input').type(collName);
822                 });
823                 cy.get('[data-cy=checkbox-foo]').click();
824             })
825
826         // Add a property.
827         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
828         cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta');
829         cy.get('[data-cy=resource-properties-form]').within(() => {
830             cy.get('[data-cy=property-field-key]').within(() => {
831                 cy.get('input').type('Color');
832             });
833             cy.get('[data-cy=property-field-value]').within(() => {
834                 cy.get('input').type('Magenta');
835             });
836             cy.root().submit();
837         });
838         // Confirm proper vocabulary labels are displayed on the UI.
839         cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
840
841         cy.get('[data-cy=form-submit-btn]').click();
842         // Confirm that the user was taken to the newly created collection
843         cy.get('[data-cy=form-dialog]').should('not.exist');
844         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
845         cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
846         cy.get('[data-cy=collection-info-panel]')
847             .should('contain', 'default')
848             .and('contain', 'foo')
849             .and('contain', 'Color: Magenta')
850             .and('not.contain', 'bar');
851         // Confirm that the collection's properties has the real values.
852         cy.doRequest('GET', '/arvados/v1/collections', null, {
853             filters: `[["name", "=", "${collName}"]]`,
854         })
855         .its('body.items').as('collections')
856         .then(function() {
857             expect(this.collections).to.have.lengthOf(1);
858             expect(this.collections[0].properties).to.have.property(
859                 'IDTAGCOLORS', 'IDVALCOLORS3');
860         });
861     });
862
863     it('shows responsible person for collection if available', () => {
864         cy.createCollection(adminUser.token, {
865             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
866             owner_uuid: activeUser.user.uuid,
867             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
868         })
869             .as('testCollection1');
870
871         cy.createCollection(adminUser.token, {
872             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
873             owner_uuid: adminUser.user.uuid,
874             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
875         })
876             .as('testCollection2').then(function (testCollection2) {
877                 cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
878             });
879
880         cy.getAll('@testCollection1', '@testCollection2')
881             .then(function ([testCollection1, testCollection2]) {
882                 cy.loginAs(activeUser);
883
884                 cy.goToPath(`/collections/${testCollection1.uuid}`);
885                 cy.get('[data-cy=responsible-person-wrapper]')
886                     .contains(activeUser.user.uuid);
887
888                 cy.goToPath(`/collections/${testCollection2.uuid}`);
889                 cy.get('[data-cy=responsible-person-wrapper]')
890                     .contains(adminUser.user.uuid);
891             });
892     });
893
894     describe('file upload', () => {
895         beforeEach(() => {
896             cy.createCollection(adminUser.token, {
897                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
898                 owner_uuid: activeUser.user.uuid,
899                 manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
900             })
901                 .as('testCollection1');
902         });
903
904         it('allows to cancel running upload', () => {
905             cy.getAll('@testCollection1')
906                 .then(function([testCollection1]) {
907                     cy.loginAs(activeUser);
908
909                     cy.goToPath(`/collections/${testCollection1.uuid}`);
910
911                     cy.get('[data-cy=upload-button]').click();
912
913                     cy.fixture('files/5mb.bin', 'base64').then(content => {
914                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
915                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
916
917                         cy.get('[data-cy=form-submit-btn]').click();
918
919                         cy.get('button').contains('Cancel').click();
920
921                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
922                     });
923                 });
924         });
925
926         it('allows to cancel single file from the running upload', () => {
927             cy.getAll('@testCollection1')
928                 .then(function([testCollection1]) {
929                     cy.loginAs(activeUser);
930
931                     cy.goToPath(`/collections/${testCollection1.uuid}`);
932
933                     cy.get('[data-cy=upload-button]').click();
934
935                     cy.fixture('files/5mb.bin', 'base64').then(content => {
936                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
937                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
938
939                         cy.get('[data-cy=form-submit-btn]').click();
940
941                         cy.get('button[aria-label=Remove]').eq(1).click();
942
943                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
944
945                         cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
946                     });
947                 });
948         });
949
950         it('allows to cancel all files from the running upload', () => {
951             cy.getAll('@testCollection1')
952                 .then(function([testCollection1]) {
953                     cy.loginAs(activeUser);
954
955                     cy.goToPath(`/collections/${testCollection1.uuid}`);
956
957                     cy.get('[data-cy=upload-button]').click();
958
959                     cy.fixture('files/5mb.bin', 'base64').then(content => {
960                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
961                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
962
963                         cy.get('[data-cy=form-submit-btn]').click();
964
965                         cy.get('button[aria-label=Remove]').should('exist');
966                         cy.get('button[aria-label=Remove]').click({ multiple: true, force: true });
967
968                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
969                     });
970                 });
971         });
972     });
973 })