diff --git a/cypress/integration/04_document_spec.js b/cypress/integration/04_document_spec.js
index 22a5e3e7..28025e3a 100644
--- a/cypress/integration/04_document_spec.js
+++ b/cypress/integration/04_document_spec.js
@@ -1,140 +1,336 @@
 /// 
+import { FSApi } from '../../fusion-studio-extension/src/common/api';
 
 context('Document Operations', () => {
   describe('working with tree view', () => {
+    let fetchSpy;
+    const connection = {
+      server: Cypress.env('API_HOST'),
+      username: 'admin',
+      password: '',
+    };
     before(() => {
+      new Cypress.Promise(async resolve => {
+        await FSApi.remove(connection, '/db/test', true).catch(e => { });
+        await FSApi.newCollection(connection, '/db/test');
+        await FSApi.newCollection(connection, '/db/test/col1');
+        await FSApi.save(connection, '/db/test/col1/doc1', '');
+        await FSApi.save(connection, '/db/test/col1/doc2', '');
+        resolve();
+      })
       cy.connect()
-      cy.visit('/')
+      cy.visit('/', {
+        onBeforeLoad: win => fetchSpy = cy.spy(win, 'fetch'),
+      })
       cy.get(`[node-id=${CSS.escape('admin@' + Cypress.env('API_HOST'))}]`)
-        // TODO(DP): might have to improve by adding more before / after hooks to prevent dangling documents
-        // see #400
+      // TODO(DP): might have to improve by adding more before / after hooks to prevent dangling documents
+      // see #400
+    })
+    after(() => {
+      // delete the test colelction
+      new Cypress.Promise(resolve => FSApi.remove(connection, '/db/test', true).then(resolve).catch(resolve))
     })
+    afterEach(() => {
+      // make sure the tree has rendered all its items properly
+      cy.wait(10)
+    })
+    it('should display creation options', () => {
+      cy.get('.fusion-view', { timeout: 55000 })
+        .should('be.visible')
+      cy.get('.fusion-item')
+        .click()
+      // (DP): start workaround for #413
+      cy.get('[node-id$=db]')
+        .click()
+        .prev().should('not.have.class', 'fa-spin').wait(1)
+      // (DP): end workaround for #413
+      //  all we need is the final part of the node-id attribute
+      cy.get('[node-id$=test]')
+        .click()
+        .prev().should('not.have.class', 'fa-spin').wait(1)
+      cy.get('[node-id$=test]')
+        .rightclick()
+      cy.get('.p-Menu')
+        .should('be.visible')
+        .contains('New document...')
+        .trigger('mousemove')
+      cy.get('[data-command="fusion.new-document"]')
+        .contains('Empty document')
+        .click()
+      cy.focused()
+        .type('untitled_1{enter}')
+      cy.get('.fusion-view')
+        .contains('untitled_1')
 
-    describe('db context menu', () => {
-      it('should display creation options', () => {
-        cy.get('.ReactVirtualized__Grid', { timeout: 55000 })
-          .should('be.visible')
-        cy.get('.fusion-item')
-          .click()
-          //  all we need is the final part of the node-id attribute
-        cy.get('[node-id$=db]')
-          .rightclick()
-          .then(() => {
-            cy.get('.p-Menu')
-              .should('be.visible')
-              .contains('New document')
-              .trigger('mousemove')
-            cy.get('[data-command="fusion.new-document"] > .p-Menu-itemLabel')
-              .should('be.visible')
-              .click()
-          })
-          // (DP): start workaround for #413 
-        cy.get('[node-id$=db]')
-          .trigger('mousemove')
-          .type('{enter}')
-          // end workaround for #413
-        cy.get('.ReactVirtualized__Grid')
-          .contains('untitled-1')
+      // TODO(DP):
+      // - add test for #413 : change order, remove workaround, might need a call to focused()
+      // - check if tree view is deselected (it is but need not be), 
+      // - check if Explorer is updated properly (seems inconsistent need to double click)
+      // - check if editor window is opening the newly create doc in a new tab (it doesn't)
+      // - two doc create routes one with follow-up dialog (xquery lib) one without (txt, xml)
+    })
 
-        // TODO(DP):
-        // - add test for #413 : change order, remove workaround, might need a call to focused()
-        // - check if tree view is deselected (it is but need not be), 
-        // - check if Explorer is updated properly (seems inconsistent need to double click)
-        // - check if editor window is opening the newly create doc in a new tab (it doesn't)
-        // - two file create routes one with follow-up dialog (xquery lib) one without (txt, xml)
-      })
+    // see https://github.com/cypress-io/cypress/pull/15388/docs#
+    // see #414
 
-      // see https://github.com/cypress-io/cypress/pull/15388/files#
-      // see #414
+    it('should let users edit new document', () => {
+      cy.get('[node-id$=untitled_1]')
+        .dblclick()
+      if (Cypress.platform === 'darwin') {
+        cy.get('.view-line')
+          .type('asdf{meta+s}')
+      } else {
+        cy.get('.view-line')
+          .type('asdf{ctrl+s}')
+      }
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/untitled_1', {
+        method: 'PUT',
+        body: 'asdf',
+      });
+    })
+    // see #414 workaround is to run this after editing and saving the document, 
+    // we should be able to rename before entering content
+    it('should let users rename documents', () => {
+      cy.get('[node-id$=untitled_1]')
+        .rightclick()
+      cy.get('[data-command="fusion.rename"]')
+        .should('be.visible')
+        .contains('Rename')
+        .click()
+        .focused()
+        .type('test.txt{enter}')
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/test.txt', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/untitled_1' },
+      });
+    })
 
-      it('should let users edit new document', () => {
-          cy.get('[node-id$=untitled-1]')
-            .dblclick()
-          if( Cypress.platform === 'darwin') {
-            cy.get('.view-line')
-            .type('asdf{meta+s}')
-          } else {
-            cy.get('.view-line')
-            .type('asdf{ctrl+s}')
-          }          
-        })
-        // see #414 workaround is to run this after editing and saving the document, 
-        // we should be able to rename before entering content
-      it('should let users rename documents', () => {
-        cy.get('[node-id$=untitled-1]')
-          .rightclick()
-        cy.get('[data-command="fusion.rename"] > .p-Menu-itemLabel')
+    it('should display document properties', () => {
+      cy.get('[node-id$="test.txt"]')
+        .rightclick()
+      cy.get('.p-Menu')
+        .should('be.visible')
+        .find('[data-command="fusion.properties"]')
+        .contains('Properties...')
+        .click();
+      cy.get('.dialogTitle')
+        .should('contain.text', 'Properties')
+      // rename doc -> text.xml
+      cy.get('.value > .theia-input')
+        .clear()
+        .type('test.xml')
+      // check properties table 
+      cy.get('.dialogContent')
+        .find('.keys > tr')
+        .should('have.length', 11)
+        .contains('Media Type')
+      cy.get('.dialogContent')
+        .find('.keys > tr')
+        .contains('Owner')
+      // check permissions table  
+      cy.get('.dialogContent')
+        .find('.permissions-editor > tr')
+        .should('have.length', 3)
+        .contains('user')
+      cy.get('.main')
+        .click()
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/test.xml', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test.txt' },
+      });
+    })
+
+    it('should not create duplicate documents', () => {
+      cy.get('[node-id$=test]')
+        .rightclick()
+      cy.get('.p-Menu')
+        .should('be.visible')
+        .contains('New document')
+        .trigger('mousemove')
+      cy.get('[data-command="fusion.new-document-template:xml"] > .p-Menu-itemLabel')
+        .should('be.visible')
+        .click()
+      cy.get('.fs-inline-input > .theia-input')
+        .clear()
+        .type('test.xml{enter}')
+      cy.get('.error')
+        .should('exist')
+        .should('contain.text', 'Item already exists')
+      cy.get('.fs-inline-input > .theia-input')
+        .type('{esc}')
+    })
+
+    it('should upload a document using drag and drop', () => {
+      cy.extendedFiles().then(win => {
+        const file = new win.ExFile('/', [new Blob(['sample text content.'])], 'upload_test.txt', { type: 'text/plain' })
+
+        const originalDataTransfer = new win.DataTransfer();
+        originalDataTransfer.items.add(file);
+        const dataTransfer = {
+          ...originalDataTransfer,
+          items: [file],
+          files: [file],
+        };
+        dataTransfer.getData = (...args) => originalDataTransfer.getData(...args);
+
+        cy.get('[node-id$=test]')
+          .trigger('dragover', { dataTransfer })
+          .trigger('drop', { dataTransfer })
+        fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/upload_test.txt', { method: 'PUT' })
+        cy.get('[node-id$="test\\/upload_test.txt"]')
           .should('be.visible')
-          .click()
-          .focused()
-          .type('test.txt{enter}')
       })
+    })
 
-      it('should display document properties', () => {
-        cy.get('[node-id$=test\\.txt]')
-          .rightclick()
-          .type('{alt+enter}', { force: true })
-        cy.get('.dialogTitle')
-          .should('contain.text', 'Properties')
-          // rename file -> text.xml
-        cy.get('.value > .theia-input')
-          .clear()
-          .type('test.xml')
-          // check properties table 
-        cy.get('.dialogContent')
-          .find('.keys > tr')
-          .should('have.length', 11)
-          .contains('Media Type')
-        cy.get('.dialogContent')
-          .find('.keys > tr')
-          .contains('Owner')
-          // check permissions table  
-        cy.get('.dialogContent')
-          .find('.permissions-editor > tr')
-          .should('have.length', 3)
-          .contains('user')
-        cy.get('.main')
-          .click()
+    it('should upload a document using the upload dialog', () => {
+      cy.writeFile(require('path').resolve(Cypress.env('homedir'), 'upload_file_test.txt'), 'sample text content');
+      cy.get('[node-id$=test]')
+        .rightclick()
+      cy.get('[data-command="fusion.upload-document"]')
+        .should('be.visible')
+        .contains('Upload document(s)')
+        .click()
+      cy.get('.dialogBlock .theia-Tree.theia-FileTree')
+      const timer = Date.now();
+      new Cypress.Promise((resolve, reject) => {
+        function tick() {
+          const file = cy.$$('.theia-TreeNode:contains(upload_file_test.txt)');
+          if (file.length) {
+            cy.wrap(file[0])
+              .click({ force: true })
+            cy.get('.main').click();
+            fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxqs/fusiondb/document?uri=/db/test/upload_file_test.txt', { method: 'PUT' })
+            cy.get('[node-id$="test\\/upload_file_test.txt"]')
+              .should('be.visible')
+            resolve();
+          } else {
+            if (Date.now() < timer + 5000) {
+              cy.get('.theia-FileTree.theia-FileDialog.ps .ps__rail-y').click('bottom', { force: true })
+              cy.wait(100).then(tick);
+            }
+          }
+        }
+        tick()
       })
+    })
 
-      it('should not create duplicate documents', () => {
-        cy.get('[node-id$=db]')
-          .rightclick()
-          .then(() => {
-            cy.get('.p-Menu')
-              .should('be.visible')
-              .contains('New document')
-              .trigger('mousemove')
-            cy.get('[data-command="fusion.new-document-template:xml"] > .p-Menu-itemLabel')
-              .should('be.visible')
-              .click()
-            cy.get('.fs-inline-input > .theia-input')
-              .clear()
-              .type('test.xml{enter}')
-            cy.get('.error')
-              .should('exist')
-              .should('contain.text', 'Item already exists')
-          })
+    it('should move a document', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/test.xml"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=col1]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer })
+        .prev().should('not.have.class', 'fa-spin').wait(1)
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/col1/test.xml', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test.xml' },
       })
+      cy.get('[node-id$="test\\/test.xml"]')
+        .should('not.exist')
+      cy.get('[node-id$="col1\\/test.xml"]')
+        .should('be.visible')
+    })
 
-      it('should let users delete documents', () => {
-        cy.get('[node-id$=test\\.xml]')
-          .rightclick()
-        cy.get('[data-command="fusion.delete"] > .p-Menu-itemLabel')
-          .should('be.visible')
-          .click()
-        cy.get('.main')
-          .click()
-          // make sure all test files are gone see #400
-        cy.get('[node-id$=untitled-1]')
-          .should('not.exist')
-        cy.get('[node-id$=test\\.txt]')
-          .should('not.exist')
-        cy.get('[node-id$=test\\.xml]')
-          .should('not.exist')
-        cy.get('[node-id$=untitled-2]')
-          .should('not.exist')
+    it('should copy a document', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="col1\\/test.xml"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer, ctrlKey: true })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/test.xml', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/col1/test.xml' },
+      })
+      cy.get('[node-id$="col1\\/test.xml"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/test.xml"]')
+        .should('be.visible')
+    })
+
+    it('should move more than one document', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="col1\\/doc1"]')
+        .should('be.visible')
+        .click()
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$="col1\\/doc2"]')
+        .should('be.visible')
+        .click({ ctrlKey: true })
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/doc1', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/col1/doc1' },
+      })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/doc2', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/col1/doc2' },
       })
+      cy.get('[node-id$="col1\\/doc1"]')
+        .should('not.exist')
+      cy.get('[node-id$="col1\\/doc2"]')
+        .should('not.exist')
+      cy.get('[node-id$="test\\/doc1"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/doc2"]')
+        .should('be.visible')
+    })
+
+    it('should copy more than one document', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/doc1"]')
+        .should('be.visible')
+        .click()
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$="test\\/doc2"]')
+        .should('be.visible')
+        .click({ ctrlKey: true })
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=col1]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer, ctrlKey: true })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/col1/doc1', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/doc1' },
+      })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/col1/doc2', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/doc2' },
+      })
+      cy.get('[node-id$="test\\/doc1"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/doc2"]')
+        .should('be.visible')
+      cy.get('[node-id$="col1\\/doc1"]')
+        .should('be.visible')
+      cy.get('[node-id$="col1\\/doc2"]')
+        .should('be.visible')
+    })
+
+    it('should let users delete documents', () => {
+      cy.get('[node-id$="test\\/test.xml"]')
+        .rightclick()
+      cy.get('[data-command="fusion.delete"] > .p-Menu-itemLabel')
+        .should('be.visible')
+        .click()
+      cy.get('.main')
+        .click()
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/test.xml', { method: 'DELETE' });
+      // make sure all test docs are gone see #400
+      cy.get('[node-id$=test\\/untitled_1]')
+        .should('not.exist')
+      cy.get('[node-id$="test\\/test.txt"]')
+        .should('not.exist')
+      cy.get('[node-id$="test\\/test.xml"]')
+        .should('not.exist')
+      cy.get('[node-id$=test\\/untitled-2]')
+        .should('not.exist')
     })
   })
 })
\ No newline at end of file
diff --git a/cypress/integration/05_collection_spec.js b/cypress/integration/05_collection_spec.js
index 3a561b75..931b7d19 100644
--- a/cypress/integration/05_collection_spec.js
+++ b/cypress/integration/05_collection_spec.js
@@ -1,164 +1,364 @@
 /// 
+import { FSApi } from '../../fusion-studio-extension/src/common/api';
 
 context('Collection Operations', () => {
-  let fetchSpy;
   describe('working with tree view', () => {
+    let fetchSpy;
+    const connection = {
+      server: Cypress.env('API_HOST'),
+      username: 'admin',
+      password: '',
+    };
     before(() => {
+      // prepare collections and documents used in the test
+      new Cypress.Promise(async resolve => {
+        await FSApi.remove(connection, '/db/test', true).catch(e => { });
+        await FSApi.newCollection(connection, '/db/test');
+        await FSApi.newCollection(connection, '/db/test/col1');
+        await FSApi.save(connection, '/db/test/col1/test.txt', 'test text file');
+        resolve();
+      })
       cy.connect()
-      cy.visit('/');
+      cy.visit('/', {
+        onBeforeLoad(win) {
+          fetchSpy = cy.spy(win, 'fetch')
+        }
+      });
+    })
+    after(() => {
+      // delete the test collection
+      new Cypress.Promise(resolve => FSApi.remove(connection, '/db/test', true).then(resolve).catch(resolve))
+    })
+    afterEach(() => {
+      // make sure the tree has rendered all its items properly
+      cy.wait(10)
+    })
+
+    it('should display creation options', () => {
+      cy.get('.fusion-view')
+        .should('be.visible')
+      cy.get('.fusion-item')
+        .click()
+      //  all we need is the final part of the node-id attribute
+      // (DP): start workaround for #413
+      cy.get('[node-id$=db]')
+        .click()
+        .prev().should('not.have.class', 'fa-spin')
+      // (DP): end workaround for #413
+      cy.get('[node-id$=test]')
+        .click()
+        .prev().should('not.have.class', 'fa-spin')
+      fetchSpy.calledWith(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/explorer?uri=/db');
+      fetchSpy.calledWith(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/explorer?uri=/db/test');
+      cy.get('[node-id$=test]')
+        .rightclick();
+      cy.get('.p-Menu')
+        .should('be.visible')
+        .find('[data-command="fusion.new-collection"]')
+        .should('be.visible')
+        .contains('New collection')
+        .click()
+      cy.focused()
+        .type('{enter}')
+      cy.get('.fusion-view')
+        .contains('untitled-1')
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/untitled-1', { method: 'PUT' });
+    })
+
+    it('should let users rename collection', () => {
+      cy.get('[node-id$=untitled-1]')
+        .rightclick()
+      cy.get('[data-command="fusion.rename"]')
+        .should('be.visible')
+        .contains('Rename')
+        .click()
+      cy.focused()
+        .type('test_col{enter}')
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/test_col', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/untitled-1' },
+      });
+      cy.get('.fusion-view')
+        .contains('test_col')
+      cy.get('[node-id$=untitled-1]')
+        .should('not.exist')
+    })
+
+    it('should display collection properties', () => {
+      cy.get('[node-id$=test_col]')
+        .click()
+        .type('{alt+enter}', { force: true })
+      cy.get('.dialogTitle')
+        .should('contain.text', 'Properties')
+      // rename file -> text.xml
+      cy.get('.value > .theia-input')
+        .should('have.value', 'test_col')
+        .clear()
+        .type('test_col2')
+      // check properties table 
+      cy.get('.dialogContent')
+        .find('.keys > tr')
+        .should('have.length', 7)
+        .should('contain', 'Created')
+        .should('contain', 'Owner')
+        .should('contain', 'Group')
+      // check permissions table  
+      cy.get('.dialogContent')
+        .find('.permissions-editor > tr')
+        .should('have.length', 3)
+        .should('contain', 'user')
+        .should('contain', 'group')
+        .should('contain', 'other')
+      cy.get('.main')
+        .click()
+      cy.get('.dialogBlock')
+        .should('not.exist');
+      cy.get('[node-id$=test_col2]')
+        .should('exist')
+      cy.get('[node-id$=test_col]')
+        .should('not.exist')
+    })
+
+    it('should not create duplicate collection', () => {
+      cy.get('[node-id$=test]')
+        .rightclick()
+        .then(() => {
+          cy.get('.p-Menu')
+            .should('be.visible')
+            .contains('New collection')
+            .trigger('mousemove')
+          cy.get('[data-command="fusion.new-collection"]')
+            .should('be.visible')
+            .click()
+          cy.focused()
+            .clear()
+            .type('test_col2{enter}')
+          cy.get('.error')
+            .should('exist')
+            .should('contain.text', 'Item already exists')
+        })
     })
-    beforeEach(() => {
-      cy.window().then(win => fetchSpy = cy.spy(win, 'fetch').as('fetch'));
+
+    it('should create nested collection', () => {
+      cy.get('[node-id$=test_col2]')
+        .click()
+        .rightclick()
+      cy.get('.p-Menu')
+        .should('be.visible')
+        .contains('New collection')
+      cy.get('[data-command="fusion.new-collection"]')
+        .should('be.visible')
+        .click()
+      cy.focused()
+        .clear()
+        .type('test_colA{enter}')
+      // TODO(DP): we migh want to check the proper nesting more explicitely,
+      // but that is already covered by checking for this collection after deleting
+      // its parent collection 
+      cy.get('.fusion-view')
+        .contains('test_colA')
+        .should('exist')
     })
 
-    describe('db context menu', () => {
-      it('should display creation options', () => {
-        cy.get('.fusion-view')
+    it('should upload a collection', () => {
+      cy.extendedFiles().then(win => {
+        const file = new win.ExFile('/uploaded_col/', [new Blob(['sample text content.'])], 'uploaded_test.txt', { type: 'text/plain' })
+        const dir = new win.ExDir('/', [file], 'uploaded_col')
+
+        const originalDataTransfer = new win.DataTransfer();
+        originalDataTransfer.items.add(file);
+        const dataTransfer = {
+          ...originalDataTransfer,
+          items: [dir],
+          files: [dir],
+        };
+        dataTransfer.getData = (...args) => originalDataTransfer.getData(...args);
+
+        cy.get('[node-id$=test]')
+          .trigger('dragover', { dataTransfer })
+          .trigger('drop', { dataTransfer })
+        fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/document?uri=/db/test/uploaded_col', { method: 'PUT' })
+        cy.get('[node-id$="test\\/uploaded_col"]')
           .should('be.visible')
-        cy.get('.fusion-item')
-          .click()
-        //  all we need is the final part of the node-id attribute
-        // (DP): start workaround for #413
-        cy.get('[node-id$=db]')
           .click()
-        cy.get('.fa-spinner')
-          .should('not.exist')
-        // (DP): end workaround for #413
-        cy.get('@fetch').should('be.calledWith', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/explorer?uri=/db');
-        cy.get('[node-id$=db]')
-          .rightclick();
-        cy.get('.p-Menu')
+        cy.get('[node-id$="uploaded_col\\/uploaded_test.txt"]')
           .should('be.visible')
-          .find('[data-command="fusion.new-collection"]')
-          .should('be.visible')
-          .contains('New collection')
-          .click()
-        cy.focused()
-          .type('{enter}')
-        cy.get('.fusion-view')
-          .contains('untitled-1')
-        cy.get('@fetch').should('be.calledWithMatch', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/untitled-1', { method: 'PUT' });
       })
+    })
 
-      it('should let users rename collection', () => {
-        cy.get('[node-id$=untitled-1]')
-          .rightclick()
-        cy.get('[data-command="fusion.rename"]')
-          .should('be.visible')
-          .contains('Rename')
-          .click()
-        cy.focused()
-          .type('test_col{enter}')
-        cy.get('@fetch').should('be.calledWithMatch', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test_col', {
-          method: 'PUT',
-          headers: { 'x-fs-move-source': '/db/untitled-1' },
-        });
-        cy.get('.fusion-view')
-          .contains('test_col')
-        cy.get('[node-id$=untitled-1]')
-          .should('not.exist')
+    it('should move a collection', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test_col2]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/test_col2/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/col1' },
       })
+      cy.get('[node-id$="test_col\\/col1"]')
+        .should('not.exist')
+      cy.get('[node-id$="test_col2\\/col1"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="test_col2\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
 
-      it('should display collection properties', () => {
-        cy.get('[node-id$=test_col]')
-          .click()
-          .type('{alt+enter}', { force: true })
-        cy.get('.dialogTitle')
-          .should('contain.text', 'Properties')
-        // rename file -> text.xml
-        cy.get('.value > .theia-input')
-          .should('have.value', 'test_col')
-          .clear()
-          .type('test_col2')
-        // check properties table 
-        cy.get('.dialogContent')
-          .find('.keys > tr')
-          .should('have.length', 7)
-          .should('contain', 'Created')
-          .should('contain', 'Owner')
-          .should('contain', 'Group')
-        // check permissions table  
-        cy.get('.dialogContent')
-          .find('.permissions-editor > tr')
-          .should('have.length', 3)
-          .should('contain', 'user')
-          .should('contain', 'group')
-          .should('contain', 'other')
-        cy.get('.main')
-          .click()
-        cy.get('.dialogBlock')
-          .should('not.exist');
-        cy.get('[node-id$=test_col2]')
-          .should('exist')
-        cy.get('[node-id$=test_col]')
-          .should('not.exist')
+    it('should copy a collection', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/test_col2\\/col1"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer, ctrlKey: true })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/test_col2/col1' },
       })
+      cy.get('[node-id$="test\\/test_col2\\/col1"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/test_col2\\/col1\\/test.txt"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="test\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
 
-      it('should not create duplicate collection', () => {
-        cy.get('[node-id$=db]')
-          .rightclick()
-          .then(() => {
-            cy.get('.p-Menu')
-              .should('be.visible')
-              .contains('New collection')
-              .trigger('mousemove')
-            cy.get('[data-command="fusion.new-collection"]')
-              .should('be.visible')
-              .click()
-            cy.focused()
-              .clear()
-              .type('test_col2{enter}')
-            cy.get('.error')
-              .should('exist')
-              .should('contain.text', 'Item already exists')
-          })
+    it('should move more than one collection', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test_col2\\/col1"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="test_col2\\/test_colA"]')
+        .should('be.visible')
+        .click({ ctrlKey: true })
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test\\/col1]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/col1/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test_col2/col1' },
+      })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/col1/test_colA', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test_col2/test_colA' },
       })
+      cy.get('[node-id$="test_col2\\/col1"]')
+        .should('not.exist')
+      cy.get('[node-id$="test_col2\\/test_colA"]')
+        .should('not.exist')
+      cy.get('[node-id$="col1\\/test_colA"]')
+        .should('be.visible')
+      cy.get('[node-id$="col1\\/col1"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="col1\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
 
-      it('should create nested collection', () => {
-        cy.get('[node-id$=test_col2]')
-          .click()
-          .rightclick()
-        cy.get('.p-Menu')
-          .should('be.visible')
-          .contains('New collection')
-        cy.get('[data-command="fusion.new-collection"]')
-          .should('be.visible')
-          .click()
-        cy.focused()
-          .clear()
-          .type('test_colA{enter}')
-        // TODO(DP): we migh want to check the proper nesting more explicitely,
-        // but that is already covered by checking for this collection after deleting
-        // its parent collection 
-        cy.get('.fusion-view')
-          .contains('test_colA')
+    it('should copy more than one collection', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="col1\\/test_colA"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="col1\\/col1"]')
+        .should('be.visible')
+        .click({ ctrlKey: true })
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test\\/test_col2]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer, ctrlKey: true })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/test_col2/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/col1/col1' },
       })
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/test_col2/test_colA', {
+        method: 'PUT',
+        headers: { 'x-fs-copy-source': '/db/test/col1/test_colA' },
+      })
+      cy.get('[node-id$="col1\\/col1"]')
+        .should('be.visible')
+      cy.get('[node-id$="col1\\/test_colA"]')
+        .should('be.visible')
+      cy.get('[node-id$="col1\\/col1\\/test.txt"]')
+        .should('be.visible')
+      cy.get('[node-id$="test_col2\\/test_colA"]')
+        .should('be.visible')
+      cy.get('[node-id$="test_col2\\/col1"]')
+        .should('be.visible')
+        .click()
+      cy.get('[node-id$="test_col2\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
 
+    it('should not move a collection to one of its sub-collections', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test\\/col1\\/col1]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer })
+      fetchSpy.neverCalledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/col1/col1/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test/col1' },
+      })
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/col1\\/col1\\/col1"]')
+        .should('not.exist')
+      cy.get('[node-id$="col1\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
 
-      it('should let users delete collection', () => {
-        cy.get('[node-id$=test_col2]')
-          .rightclick()
-        cy.get('[data-command="fusion.delete"]')
-          .should('be.visible')
-          .contains('Delete')
-          .click()
-        cy.get('.main')
-          .click()
-        cy.get('@fetch').should('be.calledWithMatch', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test_col2', { method: 'DELETE' });
-        // make sure all test files are gone see #400, including those produced by failed create commands
-        cy.get('[node-id$=untitled-1]')
-          .should('not.exist')
-        cy.get('[node-id$=untitled-2]')
-          .should('not.exist')
-        cy.get('[node-id$=test_col]')
-          .should('not.exist')
-        cy.get('[node-id$=test_col1]')
-          .should('not.exist')
-        cy.get('[node-id$=test_col2]')
-          .should('not.exist')
-        cy.get('[node-id$=test_colA]')
-          .should('not.exist')
+    it('should not copy a collection to one of its sub-collections', () => {
+      const dataTransfer = new DataTransfer();
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+        .trigger('dragstart', { dataTransfer })
+      cy.get('[node-id$=test\\/col1\\/col1]')
+        .trigger('dragover', { dataTransfer })
+        .trigger('drop', { dataTransfer, ctrlKey: true })
+      fetchSpy.neverCalledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/col1/col1/col1', {
+        method: 'PUT',
+        headers: { 'x-fs-move-source': '/db/test/test/col1' },
       })
+      cy.get('[node-id$="test\\/col1"]')
+        .should('be.visible')
+      cy.get('[node-id$="test\\/col1\\/col1\\/col1"]')
+        .should('not.exist')
+      cy.get('[node-id$="col1\\/col1\\/test.txt"]')
+        .should('be.visible')
+    })
+
+    it('should let users delete collection', () => {
+      cy.get('[node-id$=test_col2]')
+        .rightclick()
+      cy.get('[data-command="fusion.delete"]')
+        .should('be.visible')
+        .contains('Delete')
+        .click()
+      cy.get('.main')
+        .click()
+      fetchSpy.calledWithMatch(Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test/test_col2', { method: 'DELETE' });
+      // make sure all test files are gone see #400, including those produced by failed create commands
+      cy.get('[node-id$=untitled-1]')
+        .should('not.exist')
+      cy.get('[node-id$=untitled-2]')
+        .should('not.exist')
+      cy.get('[node-id$=test_col]')
+        .should('not.exist')
+      cy.get('[node-id$=test_col1]')
+        .should('not.exist')
+      cy.get('[node-id$=test_col2]')
+        .should('not.exist')
     })
   })
 })
\ No newline at end of file
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index be8c3120..8b51d857 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -58,5 +58,6 @@ module.exports = (on, config) => {
     report: process.report,
     traceDeprecation: process.traceDeprecation,
   };
+  config.env.homedir = require('os').homedir();
   return config;
 }
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 4f47715b..783f7e99 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -17,26 +17,62 @@
 // assumes default 'admin' user and '' password
 // actual server URL is retrieved via ENV
 Cypress.Commands.add("connect", () => {
-    // conn_val mimics actual app behavior, its value is inconsequential for establishing a connection
-    let conn_val = 'admin@' + Cypress.env('API_HOST')
-    let nested = { "name": "localhost", "server": Cypress.env('API_HOST'), "username": "admin", "password": "", "users": [], "groups": [] }
-    let obj = {}
-    obj[conn_val] = nested
+  // conn_val mimics actual app behavior, its value is inconsequential for establishing a connection
+  let conn_val = 'admin@' + Cypress.env('API_HOST')
+  let nested = { "name": "localhost", "server": Cypress.env('API_HOST'), "username": "admin", "password": "", "users": [], "groups": [] }
+  let obj = {}
+  obj[conn_val] = nested
 
-    localStorage.setItem('connections', JSON.stringify(obj))
+  localStorage.setItem('connections', JSON.stringify(obj))
 
 })
 Cypress.Commands.overwrite('visit', (orig, url, options) => {
-    // this is a fix to include the process variable when using the Electron browser
-    return orig(url, Cypress.isBrowser('electron') ? {
-        ...options,
-        onBeforeLoad(win) {
-            win.process = Cypress.config('process');
-            if (options?.onBeforeLoad) {
-                options.onBeforeLoad(win);
-            }
-        }
-    } : options);
+  // this is a fix to include the process variable when using the Electron browser
+  return orig(url, Cypress.isBrowser('electron') ? {
+    ...options,
+    onBeforeLoad(win) {
+      win.process = Cypress.config('process');
+      if (options?.onBeforeLoad) {
+        options.onBeforeLoad(win);
+      }
+    }
+  } : options);
+
+})
+Cypress.Commands.add("extendedFiles", () => {
+  return cy.window().then(win => {
+    win.ExFile = class extends win.File {
+      constructor(root, data, fileName, options) {
+        super(data, fileName, options);
+        this.root = root;
+      }
+      webkitGetAsEntry() {
+        const me = this;
+        return {
+          isDirectory: false,
+          isFile: true,
+          fullPath: this.root + this.name,
+          file: callback => callback(this),
+        };
+      }
+    }
+    win.ExDir = class extends win.ExFile {
+      constructor(root, entries, fileName, options) {
+        super(root, [], fileName, options);
+        this.entries = entries.map(entry => entry.webkitGetAsEntry());
+      }
+      webkitGetAsEntry() {
+        const me = this;
+        return {
+          isDirectory: true,
+          isFile: false,
+          fullPath: this.root + this.name,
+          createReader: () => ({ readEntries: callback => callback(this.entries) }),
+        };
+      }
+    }
+    return win;
+  });
 })
 //
 //
diff --git a/fusion-studio-extension/src/browser/core.ts b/fusion-studio-extension/src/browser/core.ts
index ea78f65b..140cb228 100644
--- a/fusion-studio-extension/src/browser/core.ts
+++ b/fusion-studio-extension/src/browser/core.ts
@@ -1,6 +1,6 @@
 import { injectable, inject } from "inversify";
 import { v4 } from "uuid";
-import { FSNode, FSDocumentNode, FSCollectionNode, FSToolbarNode, FSConnectionNode, FSItemNode, FSSecurityNode, FSUsersNode, FSGroupsNode, FSUserNode, FSGroupNode, FSContainerNode, FSIndexesNode, FSIndexNode, FSRestNode, FSRestURINode, FSRestMethodNode } from "../classes/node";
+import { FSNode, FSDocumentNode, FSCollectionNode, FSToolbarNode, FSConnectionNode, FSItemNode, FSSecurityNode, FSUsersNode, FSGroupsNode, FSUserNode, FSGroupNode, FSContainerNode, FSIndexesNode, FSIndexNode, FSRestNode, FSRestURINode, FSRestMethodNode, FSLoadEvent } from "../classes/node";
 import { open, TreeNode, CompositeTreeNode, ConfirmDialog, SingleTextInputDialog, OpenerService, StatusBar, StatusBarAlignment, WidgetManager } from "@theia/core/lib/browser";
 import { WorkspaceService } from "@theia/workspace/lib/browser";
 import { OpenFileDialogProps, FileDialogService } from "@theia/filesystem/lib/browser";
@@ -65,6 +65,7 @@ export class FSCore {
   updating = false;
   renaming = '';
   dict: Record = {};
+  loadEvents: Record = {};
 
   setLabelProvider(labelProvider: FSLabelProviderContribution) {
     this._labelProvider = labelProvider;
@@ -192,6 +193,9 @@ export class FSCore {
   }
 
   protected removeNode(child: FSNode) {
+    if (this.loadEvents[child.id]) {
+      delete(this.loadEvents[child.id]);
+    }
     const removeNodeId = (node: FSNode) => {
       delete(this.dict[node.nodeId]);
       if (CompositeTreeNode.is(node)) {
@@ -254,8 +258,24 @@ export class FSCore {
     }
   }
 
-  public expand(node: CompositeTreeNode) {
-    this.model && this.model.expandNode(node as any);
+  public async expand(node: CompositeTreeNode) {
+    this.model && await this.model.expandNode(node as any);
+  }
+
+  public expandAndWait(node: FSContainerNode) {
+    return new Promise(resolve => {
+      this.addLoadEvent(node, node => {
+        resolve(null);
+        return true;
+      });
+      this.expand(node);
+    })
+  }
+
+  public async ensureExpanded(node: FSContainerNode) {
+    if (!node.expanded && !node.loaded) {
+      await this.expandAndWait(node);
+    }
   }
 
   public getNode(id: string): FSNode | undefined {
@@ -360,10 +380,54 @@ export class FSCore {
     return false;
   }
 
-  protected endLoading(node: TreeNode): void {
+  protected async endLoading(node: TreeNode) {
     if (FSNode.is(node)) {
       node.loading = false;
-      this.refresh();
+      await this.refresh();
+      if (this.loadEvents[node.id]) {
+        // const a =
+        this.loadEvents[node.id] = await (await Promise.all(this.loadEvents[node.id].map(async event => {
+          const result = event(node);
+          if (!result) {
+            return event;
+          }
+          if (result === true) {
+            return null;
+          }
+          return await result ? null : event
+        }))).filter(event => event != null) as FSLoadEvent[];
+        if (this.loadEvents[node.id].length < 1) {
+          delete(this.loadEvents[node.id]);
+        }
+      }
+    }
+    (window as any).loadEvents = this.loadEvents;
+  }
+
+  protected addLoadEvent(node: TreeNode, event: FSLoadEvent): void {
+    if (FSNode.is(node)) {
+      if (!this.loadEvents[node.id]) {
+        this.loadEvents[node.id] = [event];
+      } else {
+        if (!this.loadEvents[node.id].find(value => value === event)) {
+          this.loadEvents[node.id].push(event);
+        }
+      }
+    }
+  }
+
+  protected removeLoadEvent(node: TreeNode, event?: FSLoadEvent): void {
+    if (FSNode.is(node)) {
+      if (this.loadEvents[node.id]) {
+        if (event) {
+          this.loadEvents[node.id] = this.loadEvents[node.id].filter(value => value != event);
+          if (this.loadEvents[node.id].length < 1) {
+            delete(this.loadEvents[node.id]);
+          }
+        } else {
+          delete(this.loadEvents[node.id]);
+        }
+      }
     }
   }
   
@@ -460,7 +524,7 @@ export class FSCore {
       this.startLoading(node);
       const docs = await FSApi.saveDocuments(node.connectionNode.connection, node.collection, documents);
       this.endLoading(node);
-      this.load(node, node.uri);
+      this.asyncLoad(node);
       return docs;
     } catch (error) {
       this.endLoading(node);
@@ -1320,7 +1384,9 @@ export class FSCore {
       node.uri = name;
     }
     node.nodeName = this.getName(name);;
+    delete(this.dict[node.nodeId]);
     node.nodeId = this.ID(node);
+    this.dict[node.nodeId] = node;
     return node;
   }
 
@@ -1448,6 +1514,7 @@ export class FSCore {
       return false;
     }
     const collection = this.node as FSCollectionNode;
+    await this.ensureExpanded(collection);
     const validator = (input: string) => input !== '' && !this.fileExists(input);
     let initialName = this.newName(validator);
     if (extension) {
@@ -1543,6 +1610,7 @@ export class FSCore {
       }
     }
     if (FSNode.isCollection(collection)) {
+      await this.ensureExpanded(collection);
       const validator = (input: string) => input !== '' && !this.fileExists(input);
       const dialog = new SingleTextInputDialog({
         initialValue: this.newName(validator),
diff --git a/fusion-studio-extension/src/classes/node.ts b/fusion-studio-extension/src/classes/node.ts
index 1e4760b9..ed94ad47 100644
--- a/fusion-studio-extension/src/classes/node.ts
+++ b/fusion-studio-extension/src/classes/node.ts
@@ -5,6 +5,8 @@ import { FSRestURI, FSRestMethod } from "./rest";
 
 
 export type FSNodeType = 'connection' | 'toolbar' | 'item' | 'users' | 'groups' | 'user' | 'group' | 'security' | 'indexes' | 'index' | 'rest' | 'rest-uri' | 'rest-method';
+export type FSLoadEvent = (mode: FSNode) => void | true | Promise;
+
 export interface FSNode extends TreeNode {
   type: FSNodeType;
   connectionNode: FSConnectionNode;