19153: Fix project cypress 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 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('shows collection owner', () => {
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((testCollection) => {
435                 cy.loginAs(activeUser);
436                 cy.goToPath(`/collections/${testCollection.uuid}`);
437                 cy.wait(5000);
438                 cy.get('[data-cy=collection-info-panel]').contains(`Collection User`);
439             });
440     });
441
442     it('tries to rename a file with illegal names', function () {
443         // Creates the collection using the admin token so we can set up
444         // a bogus manifest text without block signatures.
445         cy.createCollection(adminUser.token, {
446             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
447             owner_uuid: activeUser.user.uuid,
448             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
449         })
450             .as('testCollection').then(function () {
451                 cy.loginAs(activeUser);
452                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
453
454                 const illegalNamesFromUI = [
455                     ['.', "Name cannot be '.' or '..'"],
456                     ['..', "Name cannot be '.' or '..'"],
457                     ['', 'This field is required'],
458                     [' ', 'Leading/trailing whitespaces not allowed'],
459                     [' foo', 'Leading/trailing whitespaces not allowed'],
460                     ['foo ', 'Leading/trailing whitespaces not allowed'],
461                     ['//foo', 'Empty dir name not allowed']
462                 ]
463                 illegalNamesFromUI.forEach(([name, errMsg]) => {
464                     cy.get('[data-cy=collection-files-panel]')
465                         .contains('bar').rightclick();
466                     cy.get('[data-cy=context-menu]')
467                         .contains('Rename')
468                         .click();
469                     cy.get('[data-cy=form-dialog]')
470                         .should('contain', 'Rename')
471                         .within(() => {
472                             cy.get('input').type(`{selectall}{backspace}${name}`);
473                         });
474                     cy.get('[data-cy=form-dialog]')
475                         .should('contain', 'Rename')
476                         .within(() => {
477                             cy.contains(`${errMsg}`);
478                         });
479                     cy.get('[data-cy=form-cancel-btn]').click();
480                 })
481             });
482     });
483
484     it('can correctly display old versions', function () {
485         const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
486         let colUuid = '';
487         let oldVersionUuid = '';
488         // Make sure no other collections with this name exist
489         cy.doRequest('GET', '/arvados/v1/collections', null, {
490             filters: `[["name", "=", "${colName}"]]`,
491             include_old_versions: true
492         })
493             .its('body.items').as('collections')
494             .then(function () {
495                 expect(this.collections).to.be.empty;
496             });
497         // Creates the collection using the admin token so we can set up
498         // a bogus manifest text without block signatures.
499         cy.createCollection(adminUser.token, {
500             name: colName,
501             owner_uuid: activeUser.user.uuid,
502             preserve_version: true,
503             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
504         })
505             .as('originalVersion').then(function () {
506                 // Change the file name to create a new version.
507                 cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
508                     manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
509                 })
510                 colUuid = this.originalVersion.uuid;
511             });
512         // Confirm that there are 2 versions of the collection
513         cy.doRequest('GET', '/arvados/v1/collections', null, {
514             filters: `[["name", "=", "${colName}"]]`,
515             include_old_versions: true
516         })
517             .its('body.items').as('collections')
518             .then(function () {
519                 expect(this.collections).to.have.lengthOf(2);
520                 this.collections.map(function (aCollection) {
521                     expect(aCollection.current_version_uuid).to.equal(colUuid);
522                     if (aCollection.uuid !== aCollection.current_version_uuid) {
523                         oldVersionUuid = aCollection.uuid;
524                     }
525                 });
526                 // Check the old version displays as what it is.
527                 cy.loginAs(activeUser)
528                 cy.goToPath(`/collections/${oldVersionUuid}`);
529
530                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
531                 cy.get('[data-cy=read-only-icon]').should('exist');
532                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
533                 cy.get('[data-cy=collection-files-panel]').should('contain', 'bar');
534             });
535     });
536
537     it('views & edits storage classes data', function () {
538         const colName= `Test Collection ${Math.floor(Math.random() * 999999)}`;
539         cy.createCollection(adminUser.token, {
540             name: colName,
541             owner_uuid: activeUser.user.uuid,
542             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
543         }).as('collection').then(function () {
544             expect(this.collection.storage_classes_desired).to.deep.equal(['default'])
545
546             cy.loginAs(activeUser)
547             cy.goToPath(`/collections/${this.collection.uuid}`);
548
549             // Initial check: it should show the 'default' storage class
550             cy.get('[data-cy=collection-info-panel]')
551                 .should('contain', 'Storage classes')
552                 .and('contain', 'default')
553                 .and('not.contain', 'foo')
554                 .and('not.contain', 'bar');
555             // Edit collection: add storage class 'foo'
556             cy.get('[data-cy=collection-panel-options-btn]').click();
557             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
558             cy.get('[data-cy=form-dialog]')
559                 .should('contain', 'Edit Collection')
560                 .and('contain', 'Storage classes')
561                 .and('contain', 'default')
562                 .and('contain', 'foo')
563                 .and('contain', 'bar')
564                 .within(() => {
565                     cy.get('[data-cy=checkbox-foo]').click();
566                 });
567             cy.get('[data-cy=form-submit-btn]').click();
568             cy.get('[data-cy=collection-info-panel]')
569                 .should('contain', 'default')
570                 .and('contain', 'foo')
571                 .and('not.contain', 'bar');
572             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
573                 .its('body').as('updatedCollection')
574                 .then(function () {
575                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['default', 'foo']);
576                 });
577             // Edit collection: remove storage class 'default'
578             cy.get('[data-cy=collection-panel-options-btn]').click();
579             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
580             cy.get('[data-cy=form-dialog]')
581                 .should('contain', 'Edit Collection')
582                 .and('contain', 'Storage classes')
583                 .and('contain', 'default')
584                 .and('contain', 'foo')
585                 .and('contain', 'bar')
586                 .within(() => {
587                     cy.get('[data-cy=checkbox-default]').click();
588                 });
589             cy.get('[data-cy=form-submit-btn]').click();
590             cy.get('[data-cy=collection-info-panel]')
591                 .should('not.contain', 'default')
592                 .and('contain', 'foo')
593                 .and('not.contain', 'bar');
594             cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
595                 .its('body').as('updatedCollection')
596                 .then(function () {
597                     expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['foo']);
598                 });
599         })
600     });
601
602     it('moves a collection to a different project', function () {
603         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
604         const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
605         const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
606
607         cy.createCollection(adminUser.token, {
608             name: collName,
609             owner_uuid: activeUser.user.uuid,
610             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
611         }).as('testCollection');
612         cy.createGroup(adminUser.token, {
613             name: projName,
614             group_class: 'project',
615             owner_uuid: activeUser.user.uuid,
616         }).as('testProject');
617
618         cy.getAll('@testCollection', '@testProject')
619             .then(function ([testCollection, testProject]) {
620                 cy.loginAs(activeUser);
621                 cy.goToPath(`/collections/${testCollection.uuid}`);
622                 cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
623                 cy.get('[data-cy=collection-info-panel]')
624                     .should('not.contain', projName)
625                     .and('not.contain', testProject.uuid);
626                 cy.get('[data-cy=collection-panel-options-btn]').click();
627                 cy.get('[data-cy=context-menu]').contains('Move to').click();
628                 cy.get('[data-cy=form-dialog]')
629                     .should('contain', 'Move to')
630                     .within(() => {
631                         cy.get('[data-cy=projects-tree-home-tree-picker]')
632                             .find('i')
633                             .click();
634                         cy.get('[data-cy=projects-tree-home-tree-picker]')
635                             .contains(projName)
636                             .click();
637                     });
638                 cy.get('[data-cy=form-submit-btn]').click();
639                 cy.get('[data-cy=snackbar]')
640                     .contains('Collection has been moved')
641                 cy.get('[data-cy=collection-info-panel]')
642                     .contains(projName).and('contain', testProject.uuid);
643                 // Double check that the collection is in the project
644                 cy.goToPath(`/projects/${testProject.uuid}`);
645                 cy.get('[data-cy=project-panel]').should('contain', collName);
646             });
647     });
648
649     it('automatically updates the collection UI contents without using the Refresh button', function () {
650         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
651         const fileName = 'foobar'
652
653         cy.createCollection(adminUser.token, {
654             name: collName,
655             owner_uuid: activeUser.user.uuid,
656         }).as('testCollection');
657
658         cy.getAll('@testCollection').then(function ([testCollection]) {
659             cy.loginAs(activeUser);
660             cy.goToPath(`/collections/${testCollection.uuid}`);
661             cy.get('[data-cy=collection-files-panel]').should('contain', 'This collection is empty');
662             cy.get('[data-cy=collection-files-panel]').should('not.contain', fileName);
663             cy.get('[data-cy=collection-info-panel]').should('contain', collName);
664
665             cy.updateCollection(adminUser.token, testCollection.uuid, {
666                 name: `${collName + ' updated'}`,
667                 manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
668             }).as('updatedCollection');
669             cy.getAll('@updatedCollection').then(function ([updatedCollection]) {
670                 expect(updatedCollection.name).to.equal(`${collName + ' updated'}`);
671                 cy.get('[data-cy=collection-info-panel]').should('contain', updatedCollection.name);
672                 cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
673             });
674         });
675     })
676
677     it('makes a copy of an existing collection', function() {
678         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
679         const copyName = `Copy of: ${collName}`;
680
681         cy.createCollection(adminUser.token, {
682             name: collName,
683             owner_uuid: activeUser.user.uuid,
684             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
685         }).as('collection').then(function () {
686             cy.loginAs(activeUser)
687             cy.goToPath(`/collections/${this.collection.uuid}`);
688             cy.get('[data-cy=collection-files-panel]')
689                 .should('contain', 'some-file');
690             cy.get('[data-cy=collection-panel-options-btn]').click();
691             cy.get('[data-cy=context-menu]').contains('Make a copy').click();
692             cy.get('[data-cy=form-dialog]')
693                 .should('contain', 'Make a copy')
694                 .within(() => {
695                     cy.get('[data-cy=projects-tree-home-tree-picker]')
696                         .contains('Projects')
697                         .click();
698                     cy.get('[data-cy=form-submit-btn]').click();
699                 });
700             cy.get('[data-cy=snackbar]')
701                 .contains('Collection has been copied.')
702             cy.get('[data-cy=snackbar-goto-action]').click();
703             cy.get('[data-cy=project-panel]')
704                 .contains(copyName).click();
705             cy.get('[data-cy=collection-files-panel]')
706                 .should('contain', 'some-file');
707         });
708     });
709
710     it('uses the collection version browser to view a previous version', function () {
711         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
712
713         // Creates the collection using the admin token so we can set up
714         // a bogus manifest text without block signatures.
715         cy.createCollection(adminUser.token, {
716             name: colName,
717             owner_uuid: activeUser.user.uuid,
718             preserve_version: true,
719             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
720         })
721             .as('collection').then(function () {
722                 // Visit collection, check basic information
723                 cy.loginAs(activeUser)
724                 cy.goToPath(`/collections/${this.collection.uuid}`);
725
726                 cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
727                 cy.get('[data-cy=read-only-icon]').should('not.exist');
728                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
729                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
730                 cy.get('[data-cy=collection-files-panel]').should('contain', 'foo').and('contain', 'bar');
731
732                 // Modify collection, expect version number change
733                 cy.get('[data-cy=collection-files-panel]').contains('foo').rightclick();
734                 cy.get('[data-cy=context-menu]').contains('Remove').click();
735                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Removing file');
736                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
737                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
738                 cy.get('[data-cy=collection-files-panel]').should('not.contain', 'foo').and('contain', 'bar');
739
740                 // Click on version number, check version browser. Click on past version.
741                 cy.get('[data-cy=collection-version-browser]').should('not.exist');
742                 cy.get('[data-cy=collection-version-number]').contains('2').click();
743                 cy.get('[data-cy=collection-version-browser]')
744                     .should('contain', 'Nr').and('contain', 'Size').and('contain', 'Date')
745                     .within(() => {
746                         // Version 1: 6 bytes in size
747                         cy.get('[data-cy=collection-version-browser-select-1]')
748                             .should('contain', '1')
749                             .and('contain', '6 B')
750                             .and('contain', adminUser.user.uuid);
751                         // Version 2: 3 bytes in size (one file removed)
752                         cy.get('[data-cy=collection-version-browser-select-2]')
753                             .should('contain', '2')
754                             .and('contain', '3 B')
755                             .and('contain', activeUser.user.full_name);
756                         cy.get('[data-cy=collection-version-browser-select-3]')
757                             .should('not.exist');
758                         cy.get('[data-cy=collection-version-browser-select-1]')
759                             .click();
760                     });
761                 cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
762                 cy.get('[data-cy=read-only-icon]').should('exist');
763                 cy.get('[data-cy=collection-version-number]').should('contain', '1');
764                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
765                 cy.get('[data-cy=collection-files-panel]')
766                     .should('contain', 'foo').and('contain', 'bar');
767
768                 // Check that only old collection action are available on context menu
769                 cy.get('[data-cy=collection-panel-options-btn]').click();
770                 cy.get('[data-cy=context-menu]')
771                     .should('contain', 'Restore version')
772                     .and('not.contain', 'Add to favorites');
773                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
774
775                 // Click on "head version" link, confirm that it's the latest version.
776                 cy.get('[data-cy=collection-info-panel]').contains('head version').click();
777                 cy.get('[data-cy=collection-info-panel]')
778                     .should('not.contain', 'This is an old version');
779                 cy.get('[data-cy=read-only-icon]').should('not.exist');
780                 cy.get('[data-cy=collection-version-number]').should('contain', '2');
781                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
782                 cy.get('[data-cy=collection-files-panel]').
783                     should('not.contain', 'foo').and('contain', 'bar');
784
785                 // Check that old collection action isn't available on context menu
786                 cy.get('[data-cy=collection-panel-options-btn]').click()
787                 cy.get('[data-cy=context-menu]').should('not.contain', 'Restore version')
788                 cy.get('body').click(); // Collapse the menu avoiding details panel expansion
789
790                 // Make another change, confirm new version.
791                 cy.get('[data-cy=collection-panel-options-btn]').click();
792                 cy.get('[data-cy=context-menu]').contains('Edit collection').click();
793                 cy.get('[data-cy=form-dialog]')
794                     .should('contain', 'Edit Collection')
795                     .within(() => {
796                         // appends some text
797                         cy.get('input').first().type(' renamed');
798                     });
799                 cy.get('[data-cy=form-submit-btn]').click();
800                 cy.get('[data-cy=collection-info-panel]')
801                     .should('not.contain', 'This is an old version');
802                 cy.get('[data-cy=read-only-icon]').should('not.exist');
803                 cy.get('[data-cy=collection-version-number]').should('contain', '3');
804                 cy.get('[data-cy=collection-info-panel]').should('contain', colName + ' renamed');
805                 cy.get('[data-cy=collection-files-panel]')
806                     .should('not.contain', 'foo').and('contain', 'bar');
807                 cy.get('[data-cy=collection-version-browser-select-3]')
808                     .should('contain', '3').and('contain', '3 B');
809
810                 // Check context menus on version browser
811                 cy.get('[data-cy=collection-version-browser-select-3]').rightclick()
812                 cy.get('[data-cy=context-menu]')
813                     .should('contain', 'Add to favorites')
814                     .and('contain', 'Make a copy')
815                     .and('contain', 'Edit collection');
816                 cy.get('body').click();
817                 // (and now an old version...)
818                 cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
819                 cy.get('[data-cy=context-menu]')
820                     .should('not.contain', 'Add to favorites')
821                     .and('contain', 'Make a copy')
822                     .and('not.contain', 'Edit collection');
823                 cy.get('body').click();
824
825                 // Restore first version
826                 cy.get('[data-cy=collection-version-browser]').within(() => {
827                     cy.get('[data-cy=collection-version-browser-select-1]').click();
828                 });
829                 cy.get('[data-cy=collection-panel-options-btn]').click()
830                 cy.get('[data-cy=context-menu]').contains('Restore version').click();
831                 cy.get('[data-cy=confirmation-dialog]').should('contain', 'Restore version');
832                 cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
833                 cy.get('[data-cy=collection-info-panel]')
834                     .should('not.contain', 'This is an old version');
835                 cy.get('[data-cy=collection-version-number]').should('contain', '4');
836                 cy.get('[data-cy=collection-info-panel]').should('contain', colName);
837                 cy.get('[data-cy=collection-files-panel]')
838                     .should('contain', 'foo').and('contain', 'bar');
839             });
840     });
841
842     it('creates collection from selected files of another collection', () => {
843         cy.createCollection(adminUser.token, {
844             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
845             owner_uuid: activeUser.user.uuid,
846             preserve_version: true,
847             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
848         })
849             .as('collection').then(function () {
850                 // Visit collection, check basic information
851                 cy.loginAs(activeUser)
852                 cy.goToPath(`/collections/${this.collection.uuid}`);
853
854                 cy.get('[data-cy=collection-files-panel]').within(() => {
855                     cy.get('input[type=checkbox]').first().click();
856                 });
857
858                 cy.get('[data-cy=collection-files-panel-options-btn]').click();
859                 cy.get('[data-cy=context-menu]').contains('Create a new collection with selected').click();
860
861                 cy.get('[data-cy=form-dialog]').contains('Projects').click();
862
863                 cy.get('[data-cy=form-submit-btn]').click();
864
865                 cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click();
866
867                 cy.get('main').contains(`Files extracted from: ${this.collection.name}`).should('exist');
868             });
869     });
870
871     it('creates new collection with properties on home project', function () {
872         cy.loginAs(activeUser);
873         cy.goToPath(`/projects/${activeUser.user.uuid}`);
874         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
875         cy.get('[data-cy=breadcrumb-last]').should('not.exist');
876         // Create new collection
877         cy.get('[data-cy=side-panel-button]').click();
878         cy.get('[data-cy=side-panel-new-collection]').click();
879         // Name between brackets tests bugfix #17582
880         const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
881
882         // Select a storage class.
883         cy.get('[data-cy=form-dialog]')
884             .should('contain', 'New collection')
885             .and('contain', 'Storage classes')
886             .and('contain', 'default')
887             .and('contain', 'foo')
888             .and('contain', 'bar')
889             .within(() => {
890                 cy.get('[data-cy=parent-field]').within(() => {
891                     cy.get('input').should('have.value', 'Home project');
892                 });
893                 cy.get('[data-cy=name-field]').within(() => {
894                     cy.get('input').type(collName);
895                 });
896                 cy.get('[data-cy=checkbox-foo]').click();
897             })
898
899         // Add a property.
900         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
901         cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta');
902         cy.get('[data-cy=resource-properties-form]').within(() => {
903             cy.get('[data-cy=property-field-key]').within(() => {
904                 cy.get('input').type('Color');
905             });
906             cy.get('[data-cy=property-field-value]').within(() => {
907                 cy.get('input').type('Magenta');
908             });
909             cy.root().submit();
910         });
911         // Confirm proper vocabulary labels are displayed on the UI.
912         cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
913
914         cy.get('[data-cy=form-submit-btn]').click();
915         // Confirm that the user was taken to the newly created collection
916         cy.get('[data-cy=form-dialog]').should('not.exist');
917         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
918         cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
919         cy.get('[data-cy=collection-info-panel]')
920             .should('contain', 'default')
921             .and('contain', 'foo')
922             .and('contain', 'Color: Magenta')
923             .and('not.contain', 'bar');
924         // Confirm that the collection's properties has the real values.
925         cy.doRequest('GET', '/arvados/v1/collections', null, {
926             filters: `[["name", "=", "${collName}"]]`,
927         })
928         .its('body.items').as('collections')
929         .then(function() {
930             expect(this.collections).to.have.lengthOf(1);
931             expect(this.collections[0].properties).to.have.property(
932                 'IDTAGCOLORS', 'IDVALCOLORS3');
933         });
934     });
935
936     it('shows responsible person for collection if available', () => {
937         cy.createCollection(adminUser.token, {
938             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
939             owner_uuid: activeUser.user.uuid,
940             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
941         }).as('testCollection1');
942
943         cy.createCollection(adminUser.token, {
944             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
945             owner_uuid: adminUser.user.uuid,
946             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
947         }).as('testCollection2').then(function (testCollection2) {
948             cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
949         });
950
951         cy.getAll('@testCollection1', '@testCollection2')
952             .then(function ([testCollection1, testCollection2]) {
953                 cy.loginAs(activeUser);
954
955                 cy.goToPath(`/collections/${testCollection1.uuid}`);
956                 cy.get('[data-cy=responsible-person-wrapper]')
957                     .contains(activeUser.user.uuid);
958
959                 cy.goToPath(`/collections/${testCollection2.uuid}`);
960                 cy.get('[data-cy=responsible-person-wrapper]')
961                     .contains(adminUser.user.uuid);
962             });
963     });
964
965     describe('file upload', () => {
966         beforeEach(() => {
967             cy.createCollection(adminUser.token, {
968                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
969                 owner_uuid: activeUser.user.uuid,
970                 manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
971             }).as('testCollection1');
972         });
973
974         it('uploads a file and checks the collection UI to be fresh', () => {
975             cy.getAll('@testCollection1')
976                 .then(function([testCollection1]) {
977                     cy.loginAs(activeUser);
978                     cy.goToPath(`/collections/${testCollection1.uuid}`);
979                     cy.get('[data-cy=upload-button]').click();
980                     cy.get('[data-cy=collection-files-panel]')
981                         .contains('5mb_a.bin').should('not.exist');
982                     cy.get('[data-cy=collection-file-count]').should('contain', '2');
983                     cy.fixture('files/5mb.bin', 'base64').then(content => {
984                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
985                         cy.get('[data-cy=form-submit-btn]').click();
986                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
987                         cy.get('[data-cy=collection-files-panel]')
988                             .contains('5mb_a.bin').should('exist');
989                         cy.get('[data-cy=collection-file-count]').should('contain', '3');
990
991                         cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
992                         cy.get('[data-cy=upload-button]').click();
993                         cy.fixture('files/5mb.bin', 'base64').then(content => {
994                             cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
995                             cy.get('[data-cy=form-submit-btn]').click();
996                             cy.get('[data-cy=form-submit-btn]').should('not.exist');
997                             cy.get('[data-cy=collection-files-right-panel]')
998                                  .contains('5mb_b.bin').should('exist');
999                         });
1000                     });
1001                 });
1002         });
1003
1004         it('allows to cancel running upload', () => {
1005             cy.getAll('@testCollection1')
1006                 .then(function([testCollection1]) {
1007                     cy.loginAs(activeUser);
1008
1009                     cy.goToPath(`/collections/${testCollection1.uuid}`);
1010
1011                     cy.get('[data-cy=upload-button]').click();
1012
1013                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1014                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1015                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1016
1017                         cy.get('[data-cy=form-submit-btn]').click();
1018
1019                         cy.get('button').contains('Cancel').click();
1020
1021                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1022                     });
1023                 });
1024         });
1025
1026         it('allows to cancel single file from the running upload', () => {
1027             cy.getAll('@testCollection1')
1028                 .then(function([testCollection1]) {
1029                     cy.loginAs(activeUser);
1030
1031                     cy.goToPath(`/collections/${testCollection1.uuid}`);
1032
1033                     cy.get('[data-cy=upload-button]').click();
1034
1035                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1036                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1037                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1038
1039                         cy.get('[data-cy=form-submit-btn]').click();
1040
1041                         cy.get('button[aria-label=Remove]').eq(1).click();
1042
1043                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1044
1045                         cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
1046                     });
1047                 });
1048         });
1049
1050         it('allows to cancel all files from the running upload', () => {
1051             cy.getAll('@testCollection1')
1052                 .then(function([testCollection1]) {
1053                     cy.loginAs(activeUser);
1054
1055                     cy.goToPath(`/collections/${testCollection1.uuid}`);
1056
1057                     // Confirm initial collection state.
1058                     cy.get('[data-cy=collection-files-panel]')
1059                         .contains('bar').should('exist');
1060                     cy.get('[data-cy=collection-files-panel]')
1061                         .contains('5mb_a.bin').should('not.exist');
1062                     cy.get('[data-cy=collection-files-panel]')
1063                         .contains('5mb_b.bin').should('not.exist');
1064
1065                     cy.get('[data-cy=upload-button]').click();
1066
1067                     cy.fixture('files/5mb.bin', 'base64').then(content => {
1068                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
1069                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
1070
1071                         cy.get('[data-cy=form-submit-btn]').click();
1072
1073                         cy.get('button[aria-label=Remove]').should('exist');
1074                         cy.get('button[aria-label=Remove]')
1075                             .click({ multiple: true, force: true });
1076
1077                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
1078
1079                         // Confirm final collection state.
1080                         cy.get('[data-cy=collection-files-panel]')
1081                             .contains('bar').should('exist');
1082                         // The following fails, but doesn't seem to happen
1083                         // in the real world. Maybe there's a race between
1084                         // the PUT request finishing and the 'Remove' button
1085                         // dissapearing, because sometimes just one of the 2
1086                         // files gets uploaded.
1087                         // Maybe this will be needed to simulate a slow network:
1088                         // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
1089                         // cy.get('[data-cy=collection-files-panel]')
1090                         //     .contains('5mb_a.bin').should('not.exist');
1091                         // cy.get('[data-cy=collection-files-panel]')
1092                         //     .contains('5mb_b.bin').should('not.exist');
1093                     });
1094                 });
1095         });
1096     });
1097 })