16118: Adds test checking writable/readonly collection UI changes.
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Mon, 18 May 2020 13:12:23 +0000 (10:12 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Mon, 18 May 2020 13:12:23 +0000 (10:12 -0300)
WIP: There's deactivated code that shows a snackbar whenever a service request
returns an error, no matter if the error is handled somewhere up in the stack.
I think that isn't a good approach, also it prevents the 'readonly' case to
work because the snackbar appears in fron of a menu button and cannot be
clicked.

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

cypress/integration/collection-panel.spec.js
src/components/collection-panel-files/collection-panel-files.tsx
src/components/context-menu/context-menu.tsx
src/components/file-tree/file-tree-item.tsx
src/index.tsx
src/views-components/resource-properties-form/property-key-field.tsx
src/views-components/resource-properties-form/property-value-field.tsx
src/views-components/resource-properties-form/resource-properties-form.tsx
src/views/collection-panel/collection-panel.tsx

index 287bfe10db2c45f00dd2e5b5e8ac077ca6e97656..0ae728093e5bd28d86abaca4cc96169ccd8d260f 100644 (file)
@@ -28,21 +28,90 @@ describe('Collection panel tests', function() {
         cy.clearLocalStorage()
     })
 
-    it('shows collection by URL', function() {
+    it('shows collection by URL', function() {
         cy.loginAs(activeUser);
-        cy.createCollection(adminUser.token, {
-            name: 'Test collection',
-            owner_uuid: activeUser.user.uuid,
-            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
-        .as('testCollection').then(function() {
-            cy.visit(`/collections/${this.testCollection.uuid}`);
-            cy.get('[data-cy=collection-info-panel]')
-                .should('contain', this.testCollection.name)
-                .and('contain', this.testCollection.uuid);
-            cy.get('[data-cy=collection-files-panel]')
-                .should('contain', 'bar');
+        [true, false].map(function(isWritable) {
+            // Creates the collection using the admin token so we can set up
+            // a bogus manifest text without block signatures.
+            cy.createCollection(adminUser.token, {
+                name: 'Test collection',
+                owner_uuid: isWritable
+                    ? activeUser.user.uuid
+                    : adminUser.user.uuid,
+                properties: {someKey: 'someValue'},
+                manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+            .as('testCollection').then(function() {
+                if (isWritable === false) {
+                    // Share collection as read-only
+                    cy.do_request('POST', '/arvados/v1/links', {
+                        link: JSON.stringify({
+                            name: 'can_read',
+                            link_class: 'permission',
+                            head_uuid: this.testCollection.uuid,
+                            tail_uuid: activeUser.user.uuid
+                        })
+                    }, null, adminUser.token, null);
+                }
+                cy.visit(`/collections/${this.testCollection.uuid}`);
+                // Check that name & uuid are correct.
+                cy.get('[data-cy=collection-info-panel]')
+                    .should('contain', this.testCollection.name)
+                    .and(`${isWritable ? 'not.': ''}contain`, 'Read-only')
+                    .and('contain', this.testCollection.uuid);
+                // Check that both read and write operations are available on
+                // the 'More options' menu.
+                cy.get('[data-cy=collection-panel-options-btn]')
+                    .click()
+                cy.get('[data-cy=context-menu]')
+                    .should('contain', 'Add to favorites')
+                    .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection')
+                    .type('{esc}'); // Collapse the options menu
+                cy.get('[data-cy=collection-properties-panel]')
+                    .should('contain', 'someKey')
+                    .and('contain', 'someValue')
+                    .and('not.contain', 'anotherKey')
+                    .and('not.contain', 'anotherValue')
+                // Check that properties can be added.
+                if (isWritable === true) {
+                    cy.get('[data-cy=collection-properties-form]').within(() => {
+                        cy.get('[data-cy=property-field-key]').within(() => {
+                            cy.get('input').type('anotherKey');
+                        });
+                        cy.get('[data-cy=property-field-value]').within(() => {
+                            cy.get('input').type('anotherValue');
+                        });
+                        cy.root().submit();
+                    })
+                    cy.get('[data-cy=collection-properties-panel]')
+                        .should('contain', 'anotherKey')
+                        .and('contain', 'anotherValue')
+                }
+                // Check that the file listing show both read & write operations
+                cy.get('[data-cy=collection-files-panel]').within(() => {
+                    cy.root().should('contain', 'bar');
+                    cy.get('[data-cy=upload-button]')
+                        .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
+                });
+                // Hamburger 'more options' menu button
+                cy.get('[data-cy=collection-files-panel-options-btn]')
+                    .click()
+                cy.get('[data-cy=context-menu]')
+                    .should('contain', 'Select all')
+                    .click()
+                cy.get('[data-cy=collection-files-panel-options-btn]')
+                    .click()
+                cy.get('[data-cy=context-menu]')
+                    .should('contain', 'Download selected')
+                    .and(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
+                    .type('{esc}'); // Collapse the options menu
+                // File item 'more options' button
+                cy.get('[data-cy=file-item-options-btn')
+                    .click()
+                cy.get('[data-cy=context-menu]')
+                    .should('contain', 'Download')
+                    .and(`${isWritable ? '' : 'not.'}contain`, 'Remove')
+                    .type('{esc}'); // Collapse
+            })
         })
     })
-
-    // it('')
 })
\ No newline at end of file
index 3de4068f207241ac905cb1de115f83511b607eeb..48b36be16ada976777bad3b161bd803ba57d87f0 100644 (file)
@@ -50,12 +50,15 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
 export const CollectionPanelFiles =
     withStyles(styles)(
         ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes, isWritable, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
-            <Card className={classes.root}>
+            <Card data-cy='collection-files-panel' className={classes.root}>
                 <CardHeader
                     title="Files"
                     classes={{ action: classes.button }}
                     action={
-                        isWritable && <Button onClick={onUploadDataClick}
+                        isWritable &&
+                        <Button
+                            data-cy='upload-button'
+                            onClick={onUploadDataClick}
                             variant='contained'
                             color='primary'
                             size='small'>
@@ -67,7 +70,9 @@ export const CollectionPanelFiles =
                     className={classes.cardSubheader}
                     action={
                         <Tooltip title="More options" disableFocusListener>
-                            <IconButton onClick={(ev) => onOptionsMenuOpen(ev, isWritable)}>
+                            <IconButton
+                                data-cy='collection-files-panel-options-btn'
+                                onClick={(ev) => onOptionsMenuOpen(ev, isWritable)}>
                                 <CustomizeTableIcon />
                             </IconButton>
                         </Tooltip>
index 98456dad51369c8efd240cb4249cbd734ba85de5..ecade812e01a68de37d16aae2543a997fe29a6b8 100644 (file)
@@ -32,7 +32,7 @@ export class ContextMenu extends React.PureComponent<ContextMenuProps> {
             transformOrigin={DefaultTransformOrigin}
             anchorOrigin={DefaultTransformOrigin}
             onContextMenu={this.handleContextMenu}>
-            <List dense>
+            <List data-cy='context-menu' dense>
                 {items.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
                         {group.map((item, actionIndex) =>
index dc8f09b96fb480e94e35877ec86f5d25d5279a47..23273daceb3e037538c9707ee676a830a69de833 100644 (file)
@@ -53,6 +53,7 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
                         variant="caption">{formatFileSize(item.data.size)}</Typography>
                     <Tooltip title="More options" disableFocusListener>
                         <IconButton
+                            data-cy='file-item-options-btn'
                             className={classes.button}
                             onClick={this.handleClick}>
                             <MoreOptionsIcon className={classes.moreOptions} />
index 16759b7f1b6485fbc1e3b8bab18ff3907eb585c3..eec60dc113c12602f832e9f6ae750f7dd039c0ab 100644 (file)
@@ -104,18 +104,14 @@ fetchConfig()
             },
             errorFn: (id, error) => {
                 console.error("Backend error:", error);
-                if (error.errors) {
+                if (false) { // WIP: Should we mix backend with UI code?
                     store.dispatch(snackbarActions.OPEN_SNACKBAR({
-                        message: `${error.errors[0]}`,
+                        message: `${error.errors
+                            ? error.errors[0]
+                            : error.message}`,
                         kind: SnackbarKind.ERROR,
-                        hideDuration: 8000
-                    }));
-                } else {
-                    store.dispatch(snackbarActions.OPEN_SNACKBAR({
-                        message: `${error.message}`,
-                        kind: SnackbarKind.ERROR,
-                        hideDuration: 8000
-                    }));
+                        hideDuration: 8000})
+                    );
                 }
             }
         });
index 1f92118885690992b19cb6c81e5a88eaea47c959..d17f50d46530484b07a6682314eb8527b2e10d02 100644 (file)
@@ -16,11 +16,13 @@ export const PROPERTY_KEY_FIELD_ID = 'keyID';
 
 export const PropertyKeyField = connectVocabulary(
     ({ vocabulary, skipValidation }: VocabularyProp & ValidationProp) =>
+        <span data-cy='property-field-key'>
         <Field
             name={PROPERTY_KEY_FIELD_NAME}
             component={PropertyKeyInput}
             vocabulary={vocabulary}
             validate={skipValidation ? undefined : getValidation(vocabulary)} />
+        </span>
 );
 
 const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
index 99745199feebe96b7dad0ca80cb140da4c6853e2..c5a5071fb8aa44a8a8d6a08f79d9ae97f48ecb86 100644 (file)
@@ -28,11 +28,13 @@ const connectVocabularyAndPropertyKey = compose(
 
 export const PropertyValueField = connectVocabularyAndPropertyKey(
     ({ skipValidation, ...props }: PropertyValueFieldProps) =>
+        <span data-cy='property-field-value'>
         <Field
             name={PROPERTY_VALUE_FIELD_NAME}
             component={PropertyValueInput}
             validate={skipValidation ? undefined : getValidation(props)}
             {...props} />
+        </span>
 );
 
 const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
index db40e4a7e8718e609e7d13814bf9e6ac638a6946..0632b97cd5fbb4889b599b61f0f82eeaec538d92 100644 (file)
@@ -20,7 +20,7 @@ export interface ResourcePropertiesFormData {
 export type ResourcePropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<GridClassKey>;
 
 export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) =>
-    <form onSubmit={handleSubmit}>
+    <form data-cy='collection-properties-form' onSubmit={handleSubmit}>
         <Grid container spacing={16} classes={classes}>
             <Grid item xs>
                 <PropertyKeyField />
index be2afc721bf7238135522dee01f23c708fd947ac..defe73b9e6cdb95235d46870b59fbcab42523518 100644 (file)
@@ -29,7 +29,7 @@ import { GroupResource } from '~/models/group';
 import { UserResource } from '~/models/user';
 import { getUserUuid } from '~/common/getuser';
 
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link' | 'centeredLabel';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link' | 'centeredLabel' | 'readOnlyChip';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -60,6 +60,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         '&:hover': {
             cursor: 'pointer'
         }
+    },
+    readOnlyChip: {
+        marginLeft: theme.spacing.unit
     }
 });
 
@@ -101,6 +104,7 @@ export const CollectionPanel = withStyles(styles)(
                                 action={
                                     <Tooltip title="More options" disableFocusListener>
                                         <IconButton
+                                            data-cy='collection-panel-options-btn'
                                             aria-label="More options"
                                             onClick={this.handleContextMenu}>
                                             <MoreOptionsIcon />
@@ -111,7 +115,7 @@ export const CollectionPanel = withStyles(styles)(
                                     <span>
                                         <IllegalNamingWarning name={item.name}/>
                                         {item.name}
-                                        {isWritable || <Chip variant="outlined" icon={<ReadOnlyIcon />} label="Read-only"/>}
+                                        {isWritable || <Chip variant="outlined" icon={<ReadOnlyIcon />} label="Read-only" className={classes.readOnlyChip} />}
                                     </span>
                                 }
                                 titleTypographyProps={this.titleProps}
@@ -142,7 +146,7 @@ export const CollectionPanel = withStyles(styles)(
                             </CardContent>
                         </Card>
 
-                        <Card className={classes.card}>
+                        <Card data-cy='collection-properties-panel' className={classes.card}>
                             <CardHeader title="Properties" />
                             <CardContent>
                                 <Grid container direction="column">
@@ -173,7 +177,7 @@ export const CollectionPanel = withStyles(styles)(
                                 </Grid>
                             </CardContent>
                         </Card>
-                        <div className={classes.card} data-cy="collection-files-panel">
+                        <div className={classes.card}>
                             <CollectionPanelFiles isWritable={isWritable} />
                         </div>
                     </>