diff --git a/.circleci/config.yml b/.circleci/config.yml index b2c56530..378d3c7d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,7 @@ jobs: curl -v --connect-timeout 4 --max-time 8 http://localhost:3000 - run: working_directory: . - command: npx cypress run --config defaultCommandTimeout=20000 + command: npx cypress run --config defaultCommandTimeout=25000 # command: npx cypress run -b chrome --config defaultCommandTimeout=58000 no_output_timeout: 2m workflows: diff --git a/README.md b/README.md index 7033c270..75528f5a 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ If you don't know what Theia is, then you likely want the full [Fusion Studio ID * [Fusion Studio API](https://github.com/evolvedbinary/fusion-studio-api) installed in a compatible [FusionDB Server](https://www.fusiondb.com) or [eXist-db](https://www.exist-db.org) database. #### For building -* [Node 12](https://nodejs.org/dist/v12.18.3/). `>= 12.18.3` (it should most likely be installed with [nvm](https://github.com/nvm-sh/nvm)) - * Node 10 may work, and Node 14 should work... however we are focused on Node 12 compatibility. +* [Node LTS](https://nodejs.org/en/). `>= 12.18.3` (use [nvm](https://github.com/nvm-sh/nvm)) + * Node `12` should work should work..., however we are focused on Node `LTS` compatibility. * [Yarn](https://yarnpkg.com). `> 1.15.x` (it can easily be installed globally via npm (Node Package Manager), but you should be aware this has a small [security implication](https://classic.yarnpkg.com/en/docs/install/#install-via-npm). npm is installed when you install Node). * [Python](https://www.python.org/) `>= 3.7.7.` (if your system does not provide it, consider using [pyenv](https://github.com/pyenv/pyenv)). If you are having trouble building and have multiple versions of Python installed via `pyenv` or any other mechanism, see the [Debugging Python Build Issues](#debugging-python-build-issues) section). @@ -45,7 +45,7 @@ then the following information may be useful. The following commands will install the required packages and setup Python 3 via pyenv on Ubuntu 20.04. ``` -sudo curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +sudo curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt-get install nodejs yarnpkg libx11-dev libxkbfile-dev alias yarn=yarnpkg @@ -116,19 +116,16 @@ $ yarn start ``` #### Integration Testing -To run the integrations tests you need a running database with the fusion-studio-api installed. It should be reachable at `localhost:8080` and have an empty admin password. You can then run the integration test GUI locally by using: -```bash -yarn run cypress open -``` -or in cases where the above fails to load the [cypress test runner](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview), use: +To run the integrations tests you need a running database with the [fusion-studio-api](https://github.com/evolvedbinary/fusion-studio-api) installed. It should be reachable at `localhost:8080` and have an empty admin password. ```bash npx cypress open ``` -Integration tests are also run on travis. To see a similar command line style output use: +Integration tests are also run on circleCI. To use the electron headless browser via command line use: ```bash -yarn run cypress run +npx cypress run ``` + ### Developing * To compile css: ```bash diff --git a/cypress/integration/04_document_spec.js b/cypress/integration/04_document_spec.js index 10d87114..34f9f736 100644 --- a/cypress/integration/04_document_spec.js +++ b/cypress/integration/04_document_spec.js @@ -1,23 +1,38 @@ /// -context.skip('Document Operations', () => { +context('Document Operations', () => { describe('working with tree view', () => { before(() => { cy.connect() cy.visit('/') 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 + .should('be.visible') + }) + + after(() => { + // make sure all test files are gone see #400 + cy.get('[node-id$=db]') + .should('be.visible') + 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') }) describe('db context menu', () => { - it('should display creation options', () => { - cy.get('.ReactVirtualized__Grid', { timeout: 55000 }) + it('should create documents', () => { + cy.get('.ReactVirtualized__Grid') .should('be.visible') cy.get('.fusion-item') - .click() - // all we need is the final part of the node-id attribute + .should('be.visible') + .click({ animationDistanceThreshold: 2 }) + // all we need is the final part of the node-id attribute cy.get('[node-id$=db]') + .should('be.visible') .rightclick() .then(() => { cy.get('.p-Menu') @@ -28,18 +43,28 @@ context.skip('Document Operations', () => { .should('be.visible') .click() }) - // (DP): start workaround for #413 + // (DP) untitled-1 document has been created + // (DP): start workaround for #413 cy.get('.fusion-item') - .click() + .should('be.visible') + .click({ animationDistanceThreshold: 2 }) cy.get('[node-id$=db]') - .trigger('mousemove') + .should('be.visible') + .focused() .type('{enter}') - // end workaround for #413 + // end workaround for #413 cy.get('.ReactVirtualized__Grid') + .should('be.visible') .contains('untitled-1') + // (DP) cleanup welcome tab so we don't have to deal with it in later tests + cy.get('#shell-tab-fusion-welcome > .p-TabBar-tabCloseIcon') + .click() + cy.get('#shell-tab-fusion-welcome') + .should('not.exist') + // TODO(DP): - // - add test for #413 : change order, remove workaround, might need a call to focused() + // - add test for #413 : remove workaround // - 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) @@ -49,92 +74,214 @@ context.skip('Document Operations', () => { // 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') + it('should edit document contents', () => { + cy.get('.fusion-item') + .should('be.visible') + // (DP) edit and save untitled-1 + cy.get('[node-id$=untitled-1]') + .dblclick() + if (Cypress.platform === 'darwin') { + cy.get('.view-line') .type('asdf{meta+s}') - } else { - cy.get('.view-line') + } 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', () => { + } + // (DP): see #525 close edited editor pane + cy.get('#theia-main-content-panel > .p-TabBar > .p-TabBar-content-container > .p-TabBar-content') + .within(() => { + cy.get('.p-TabBar-tabCloseIcon') + .click({ multiple: true }) + }) + cy.get('.main') + .click() + cy.get('.view-lines') + .should('not.exist') cy.get('[node-id$=untitled-1]') - .rightclick() - cy.get('[data-command="fusion.rename"] > .p-Menu-itemLabel') + .should('exist') + cy.get('[node-id$=untitled-2]') + .should('not.exist') + }) + + // (DP): Fix #527 then finish this + it.skip('should load previously stored document', () => { + cy.get('.fusion-item') .should('be.visible') + cy.get('[node-id$=untitled-1]') + .dblclick() + cy.get('.view-lines') + .should('exist') + .contains('asdf') + cy.get('#theia-main-content-panel > .p-TabBar > .p-TabBar-content-container > .p-TabBar-content') + .within(() => { + cy.get('.p-TabBar-tabCloseIcon') + .click({ multiple: true }) + }) + cy.get('.main') .click() - .focused() - .type('test.txt{enter}') + cy.get('[node-id$=untitled-1]') + .should('exist') + cy.get('[node-id$=untitled-2]') + .should('not.exist') }) it('should display document properties', () => { - cy.get('[node-id$=test\\.txt]') - .rightclick() + cy.get('[node-id$=untitled-1]') + .should('be.visible') .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 + // check properties table + // TODO (DP) # 519 flaky test, properties table changes based on filetype + // hence only check visibility. cy.get('.dialogContent') .find('.keys > tr') - .should('have.length', 11) - .contains('Media Type') + // .should('have.length', 11) + // .should('contain.text', 'Media Type') + .should('be.visible') cy.get('.dialogContent') .find('.keys > tr') - .contains('Owner') - // check permissions table + .should('contain.text', 'Owner') + // check permissions table cy.get('.dialogContent') .find('.permissions-editor > tr') .should('have.length', 3) - .contains('user') - cy.get('.main') + .should('contain.text', 'user') + cy.get('.secondary') .click() + cy.get('[node-id$=untitled-1]') + .should('exist') }) it('should not create duplicate documents', () => { cy.get('[node-id$=db]') + .should('be.visible') .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') + cy.get('[data-command="fusion.new-document"] > .p-Menu-itemLabel') .should('be.visible') .click() cy.get('.fs-inline-input > .theia-input') .clear() - .type('test.xml{enter}') + .type('untitled-1{enter}') cy.get('.error') .should('exist') .should('contain.text', 'Item already exists') + .type('{esc}') + cy.get('[node-id$=untitled-1]') + .should('exist') }) }) - it('should let users delete documents', () => { - cy.get('[node-id$=test\\.xml]') + it('should delete documents', () => { + cy.get('[node-id$=untitled-1]') + .should('be.visible') .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$=db]') + .should('be.visible') cy.get('[node-id$=untitled-1]') .should('not.exist') + }) + + // see #414 workaround is to run this after editing and saving the document, + // we should be able to rename before editing content + it.skip('should let users rename documents', () => { + cy.get('.fusion-item') + .should('exist') + // (DP) rename untitled-1 -> test.xml + cy.get('[node-id$=untitled-1]') + .should('be.visible') + // (DP) press F2 not working see #526 + // .trigger('keydown', { keyCode: 112, which: 112 }) + // .trigger('keyup', { keyCode: 112, which: 112 }) + // (DP) see #522 + .rightclick({ force: true }) + cy.get('[data-command="fusion.rename"] > .p-Menu-itemLabel') + .should('be.visible') + .click() + .focused() + .clear() + .type('test.txt{enter}', { force: true }) + // (DP): see #414 here the funkyness starts instead of having renamed the first file upon hitting {enter} + // - we now have 2 files in the db tree untitled-1 and test.txt, there should only be 1 => test.txt + // - test.txt isn't really renamed as its contents are empty, it should have the contents previously stored as untitled-1 + // - uncomment the following to test any potential fixes + // cy.get('[node-id$=untitled-1]') + // .should('not.exist') + + // (DP): failed workaround which needs to include a subworkaround but ultimately fails due to #527 + // cy.get('.ReactVirtualized__Grid') + // .then(() => { + // cy.get('[node-id$=test\\.txt]') + // .dblclick() + // }) + // if (Cypress.platform === 'darwin') { + // cy.get('.view-line') + // .type('fdsa{meta+s}') + // } else { + // cy.get('.view-line') + // .type('fdsa{ctrl+s}') + // } + // // (DP): subworkaround #525 close edited editor pane + // cy.get('#theia-main-content-panel > .p-TabBar > .p-TabBar-content-container > .p-TabBar-content') + // .within(() => { + // cy.get('.p-TabBar-tabCloseIcon') + // .click({ multiple: true }) + // }) + // cy.get('.main') + // .click() + // (DP): end workarounds #414 #525 #527 + + // (DP): if #414 is fixed the below needs to be activated + // cy.get('[node-id$=test\\.txt]') + // .should('be.visible') + }) + + it.skip('should not create duplicate documents', () => { + cy.get('[node-id$=db]') + .should('be.visible') + .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() + cy.get('.fs-inline-input > .theia-input') + .clear() + .type('test.txt{enter}') + cy.get('.error') + .should('exist') + .should('contain.text', 'Item already exists') + // DP see #414 activate to confirm fix + // cy.get('[node-id$=untitled-1]') + // .should('not.exist') + }) + }) + + it.skip('should delete documents', () => { + cy.get('[node-id$=test\\.txt]') + .should('be.visible') + .rightclick() + cy.get('[data-command="fusion.delete"] > .p-Menu-itemLabel') + .should('be.visible') + .click() + cy.get('.main') + .click() + cy.get('[node-id$=db]') + .should('be.visible') 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') }) }) diff --git a/cypress/integration/05_collection_spec.js b/cypress/integration/05_collection_spec.js index 798c300c..a6793836 100644 --- a/cypress/integration/05_collection_spec.js +++ b/cypress/integration/05_collection_spec.js @@ -1,75 +1,87 @@ /// -context.skip('Collection Operations', () => { - let fetchSpy; +context('Collection Operations', () => { + // let fetchSpy describe('working with tree view', () => { before(() => { cy.connect() - cy.visit('/'); + cy.visit('/') + cy.get(`[node-id=${CSS.escape('admin@' + Cypress.env('API_HOST'))}]`) + .should('be.visible') }) - beforeEach(() => { - cy.window().then(win => fetchSpy = cy.spy(win, 'fetch').as('fetch')); + // beforeEach(() => { + // cy.window().then(win => fetchSpy = cy.spy(win, 'fetch').as('fetch')); + // }) + after(() => { + // make sure all test collections are gone see #400 + 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') }) describe('db context menu', () => { - it('should display creation options', () => { + it('should create collections', () => { cy.get('.fusion-view') .should('be.visible') cy.get('.fusion-item') + .should('be.visible') .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') - .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]') + // .click() + // cy.get('.fa-spinner') + // .should('not.exist') + // cy.get('@fetch').should('be.calledWith', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/explorer?uri=/db'); + // cy.get('[node-id$=db]') .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' }, - }); + .then(() => { + cy.get('.p-Menu') + .should('be.visible') + .contains('New collection') + cy.get('[data-command="fusion.new-collection"]') + .should('be.visible') + .click() + // (DP) untitled-1 collection has been created… + // … the tree however has collapsed and is hidding the currently active input prompt from the user + // (DP): start workaround for #413 + cy.get('.fusion-item') + .should('be.visible') + .click() + cy.get('[node-id$=db]') + .should('be.visible') + .focused() + // end workaround for #413 + .type('{enter}') + }) cy.get('.fusion-view') - .contains('test_col') + .should('contain.text', 'untitled-1') cy.get('[node-id$=untitled-1]') + .should('be.visible') + cy.get('[node-id$=untitled-2]') .should('not.exist') + + // cy.get('@fetch').should('be.calledWithMatch', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/untitled-1', { method: 'PUT' }); }) it('should display collection properties', () => { - cy.get('[node-id$=test_col]') + cy.get('[node-id$=untitled-1]') .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') + .should('have.value', 'untitled-1') // check properties table cy.get('.dialogContent') .find('.keys > tr') @@ -84,12 +96,10 @@ context.skip('Collection Operations', () => { .should('contain', 'user') .should('contain', 'group') .should('contain', 'other') - cy.get('.main') + cy.get('.secondary') .click() cy.get('.dialogBlock') .should('not.exist'); - cy.get('[node-id$=test_col2]') - .should('exist') cy.get('[node-id$=test_col]') .should('not.exist') }) @@ -105,38 +115,59 @@ context.skip('Collection Operations', () => { cy.get('[data-command="fusion.new-collection"]') .should('be.visible') .click() - cy.focused() + .focused() .clear() - .type('test_col2{enter}') + .type('untitled-1{enter}') cy.get('.error') .should('exist') .should('contain.text', 'Item already exists') + .type('{esc}') }) }) it('should create nested collection', () => { - cy.get('[node-id$=test_col2]') - .click() + cy.get('[node-id$=untitled-1]') + .trigger('mousemove') .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 + .then(() => { + cy.get('.p-Menu') + .should('be.visible') + .contains('New collection') + cy.get('[data-command="fusion.new-collection"]') + .should('be.visible') + .click() + .focused() + .clear() + .type('test_colA{enter}') + }) cy.get('.fusion-view') .contains('test_colA') }) + it('should rename collection', () => { + cy.get('[node-id$=test_colA]') + .rightclick() + .then(() => { + cy.get('[data-command="fusion.rename"]') + .should('be.visible') + .contains('Rename') + .click() + .focused() + .clear() + .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$=test_colA]') + .should('not.exist') + }) - it('should let users delete collection', () => { - cy.get('[node-id$=test_col2]') + it('should delete collection', () => { + cy.get('[node-id$=untitled-1]') .rightclick() cy.get('[data-command="fusion.delete"]') .should('be.visible') @@ -144,20 +175,12 @@ context.skip('Collection Operations', () => { .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('@fetch').should('be.calledWithMatch', Cypress.env('API_HOST') + '/exist/restxq/fusiondb/collection?uri=/db/test_col2', { method: 'DELETE' }); + // make sure collections 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') }) }) }) diff --git a/cypress/integration/06_permission_spec.js b/cypress/integration/06_permission_spec.js index 0b4e85c4..517bde50 100644 --- a/cypress/integration/06_permission_spec.js +++ b/cypress/integration/06_permission_spec.js @@ -5,6 +5,7 @@ context('Permission Manager', () => { cy.connect() cy.visit('/') cy.get(`[node-id=${CSS.escape('admin@' + Cypress.env('API_HOST'))}]`) + .should('be.visible') .click() }) diff --git a/cypress/support/index.js b/cypress/support/index.js index d68db96d..771d623a 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -16,5 +16,21 @@ // Import commands.js using ES2015 syntax: import './commands' +// (DP) workaround for #517 these need to go. +// https://docs.cypress.io/api/events/catalog-of-events#Uncaught-Exceptions + +Cypress.on('uncaught:exception', (err, runnable, promise) => { + if (promise) { + return false + } +}) + +Cypress.on('uncaught:exception', (err, runnable) => { + if (err.message.includes('filesystem provider')) { + return false + } + }) + + // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/package.json b/package.json index 16c4dfb0..9c421a72 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "chai": "^4.3.4", - "cypress": "^8.4.0", + "cypress": "^8.4.1", "lerna": "4.0.0", "mocha": "^9.1.1", "node-sass": "6.0.1", diff --git a/yarn.lock b/yarn.lock index 931a95b5..ef497be2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4444,10 +4444,10 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.4.0.tgz#09ec06a73f1cb10121c103cba15076e659e24876" - integrity sha512-RtVgGFR06ikyMaq/VqapeqOjGaIA42PpK7F0qe1MCiFArfUuJECsLmeYaOA+1TlmNUgJNMSF5fWKkZIJr5Uc7w== +cypress@^8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.4.1.tgz#8b5898bf49359cadc28f02ba05d51f63b8e3a717" + integrity sha512-itJXq0Vx3sXCUrDyBi2IUrkxVu/gTTp1VhjB5tzGgkeCR8Ae+/T8WV63rsZ7fS8Tpq7LPPXiyoM/sEdOX7cR6A== dependencies: "@cypress/request" "^2.88.6" "@cypress/xvfb" "^1.2.4"