Skip to content

Commit

Permalink
Feature/e2e (#164)
Browse files Browse the repository at this point in the history
* [KFI]refactor(dms-demo): use name instead of id for form fields

* [KFI]feat(dms): add cypress

* [KFI]ci: added e2e script to travis

* [KFI]ci(e2e test scripts):

* [KFI]ci(e2e): try chrome instead of google-chrome-stable

* [KFI]refactor(dms): cypress full typescript support

* [KFI]ci: add back lint and unit tests

* [KFI]refactor(dms-demo): logout + small cleanup

* [KFI]refactor(dms-demo): registerUser cypress command

* [KFI]ci: cypress dashboard service

* [KFI]test(dms-demo): logout wait for element to load

* [KFI]chore(dms-demo): add chance to dev dependency

* [KFI]chore(dms-demo): typings correction

* [KFI]test(dms-demo): create new document test with new user

* [KFI]chore(dms-demo): prevent cypress to generate example fixture

* [KFI]build: sendscreenshots script

* [KFI]test: failing login test

* [KFI]ci: not to record e2e

* [KFI]ci: first try sendscreenshots script

* [KFI]ci: add back cache + chmod for script

* [KFI]ci: sendscreenshots second

* [KFI]ci: run lint and unit tests

* [KFI]test(dms-demo): e2e fix failing tests

* [KFI]ci: test with coverage 🤓

* [KFI]refactor(dms-demo): increase timeout for logout test

* [KFI]test(dms-demo): new content creation tests

* [KFI]build(dms-demo): add cross-env

so enviroment variables can work on windows too

* [KFI]refactor(client-core): export all from upload

* [KFI]test(dms-demo): preview with admin

* [KFI]test(dms-demo): documents with admin helper

* [KFI]test(dms-demo): failing new content test

* [KFI]chore(dms-demo): cypress tsconfig fix

* [KFI]test(dms-demo): file upload improvement

* [KFI]refactor(client-core): add null check for inputref

* [KFI]test(dms-demo): rename test

* [KFI]fix(dms-demo): test fixes

remove predefined user
use object keys in object

* [KFI]refactor(dms-demo): wait for input focus

* [KFI]refactor(dms-demo): persist current user in local

* [KFI]test(dms-demo): copy to

* [KFI]test(dms-demo): move to

* [KFI]test(dms-demo): share content email check

* [KFI]ci: build stages

* [KFI]ci: try before script

* [KFI]ci: fix indentation

* [KFI]ci: try with e2e

* [KFI]ci: move scripts to jobs

* [KFI]ci: install latest yarn always

* [KFI]ci: cache all

* [KFI]ci: try deploy

* [KFI]ci: dist trusty only in e2e + run deploy on all branches

* [KFI]ci: missin dir flag

* [KFI]ci: run all stages

* [KFI]ci: allow e2e to fail

* [KFI]ci: dont overwrite before_install

* [KFI]ci: do not deploy from travis

* [KFI]refactor(dms-demo): increase timeout to 10 sec

* [KFI]test(dms-demo): edit properties

* [KFI]ci: send screenshots again

* [KFI]test(dms-demo): run once in ci

* [KFI]fix(dms-demo): open context menu

* [KFI]ci: deploy from travis

* [KFI]ci: allow failure from e2e

* [KFI]ci: fix if in deploy

* [KFI]ci: deploy and lint update

* [KFI]ci: deploy storybook

* [KFI]ci(try deploy 1):

* [KFI]ci: deploy try 2

* [KFI]ci: fix build path for snapp

* [KFI]ci: add test stages back

* [KFI]ci: fix sn app deploy dir

* [KFI]docs(contributing): running e2e tests locally

* [KFI]test(dms-demo): checkout & undo

* [KFI]chore(dms-demo): update cypress

* [KFI]test(dms-demo): check items fix

* [KFI]ci: no need to install chrome

* [KFI]ci: e2e test

* [KFI]ci: run all stages

* [KFI]test(dms-demo): use e2e admin

* Apply suggestions from code review

Co-Authored-By: Aniko Litvanyi <herflis33@gmail.com>

* [KFI]ci(e2e): run e2e tests on cron job

* [KFI]chore(dms): upgrade cypress to latest

* [KFI]refactor(dms-e2e): use resource strings where possible
  • Loading branch information
zoltanbedi committed May 27, 2019
1 parent 457224e commit 5bc97b1
Show file tree
Hide file tree
Showing 29 changed files with 1,151 additions and 68 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Expand Up @@ -11,6 +11,9 @@ cache:
yarn: true

jobs:
allow_failures:
- name: 'DMS e2e tests'

include:
- stage: 'Tests'
name: 'Static linting'
Expand All @@ -21,6 +24,12 @@ jobs:
script: yarn test:coverage
after_success: bash <(curl -s https://codecov.io/bash)

- name: 'DMS e2e tests'
if: type = cron
before_script: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p && yarn build
script: yarn test:dms:e2e
after_failure: bash ./scripts/sendscreenshots.sh

- stage: 'Deploy to netlify'
if: type = pull_request
name: 'DMS'
Expand Down
36 changes: 25 additions & 11 deletions CONTRIBUTING.md
Expand Up @@ -20,21 +20,35 @@ If you run into trouble here, make sure your node, npm, and **_yarn_** are on th
1. `git clone https://github.com/SenseNet/sn-client.git` _bonus_: use your own fork for this step
2. `cd sn-client`
3. `yarn`
4. `yarn build` this will run the build script in every package
5. `yarn test`
4. `yarn build` to compile all the typescript packages
5. `yarn test` to run unit tests

#### Bootstrapping everything
### Running package script

_This method is slow_
With yarn you can run any package scripts eg.: `test, build, lint`

1. `yarn`
2. Have a beer 🍺
3. `yarn build` (to verify everything worked)

#### Running package script
```shell
yarn workspace @sensenet/redux test
```

With yarn you can run the any package scripts eg.: `test, build, lint`
There are aliases for the example applications like dms, component-docs, sn-app.
You can run any command in these packages easily.

```shell
yarn workspace @sensenet/redux test
yarn dms build:webpack
yarn dms start
yarn snapp start
yarn storybook start
```

### Running e2e tests locally

Cypress is installed to dms-demo app right now. To run the tests you simply need to run `yarn test:dms:e2e`
To develop e2e tests:

- You need a running instance of dms (`yarn start:dms:e2e`) with NODE_ENV set to test
- Start cypress with `yarn cypress open -P examples/sn-dms-demo` in another command prompt
- Add test to `examples/sn-dms-demo/cypress/integration` with a spec.ts | spec.js file extension

Running the tests locally will create a currentUser.json with a new test user. The tests are going to use this user.
In order to use another user you can change the email and password of the current user or let the system create a new for you by deleting the json.
5 changes: 4 additions & 1 deletion examples/sn-dms-demo/.gitignore
Expand Up @@ -21,4 +21,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

.awcache
.awcache
cypress/videos/
cypress/screenshots/
cypress/fixtures/currentUser.json
6 changes: 6 additions & 0 deletions examples/sn-dms-demo/cypress.json
@@ -0,0 +1,6 @@
{
"baseUrl": "http://localhost:3000",
"testFiles": "**/*.spec.*",
"supportFile": "cypress/support/index.ts",
"fixturesFolder": "cypress/fixtures"
}
Binary file added examples/sn-dms-demo/cypress/fixtures/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 166 additions & 0 deletions examples/sn-dms-demo/cypress/integration/documents/documents.spec.ts
@@ -0,0 +1,166 @@
import Chance = require('chance')
import { resources } from '../../../src/assets/resources'
import {
contextMenuItems,
createNewFileName,
moveToFolderAndCheckIfFileExists,
newMenuItems,
openContextMenuItem,
openNew,
registerNewUser,
selectPathInListPicker,
uploadNewFileAndOpenContextMenuItem,
} from '../../support/documents'

context('The documents page', () => {
let currentUser = { email: '', password: '' }

const loginOrRegister = (user: { email: string; password: string }) => {
cy.login(user.email, user.password).then(isSuccesful => {
if (!isSuccesful) {
currentUser = registerNewUser()
cy.login(currentUser.email, currentUser.password)
}
})
}

before(async () => {
if (process.env.CI) {
currentUser = registerNewUser()
}
})

beforeEach(() => {
// Save current user locally to reduce user creation
if (!process.env.CI) {
cy.task('getCurrentUser', '../fixtures/currentUser.json')
.then(user => (currentUser = user))
.then(() => loginOrRegister(currentUser))
} else {
loginOrRegister(currentUser)
}
})

it('header contains the logged in users avatar', () => {
cy.visit('#/documents')

cy.url().should('include', '/documents')

cy.get(`div[aria-label="${currentUser.email}"]`).should('exist')
})

describe('left side menu', () => {
it('should contain new and upload buttons', () => {
cy.contains('span[role=button]', resources.UPLOAD_BUTTON_TITLE)
cy.contains('span[role=button]', resources.ADD_NEW)
})

it('should contain new document, sheet, slide, text, folder buttons', () => {
cy.contains('[data-cy=appbar]', 'Document library').should('exist')
cy.contains(resources.ADD_NEW).click()
newMenuItems.forEach(item => cy.contains('li[role=menuitem]', `${resources.ADD_NEW} ${item.name}`))
})

it(`creating a new item should show succes notification and can be found in the grid`, () => {
cy.contains('Document library').should('exist')
newMenuItems.forEach(item => {
openNew(item.name)
const displayName = Chance().word()
cy.get('#DisplayName').type(displayName + '{enter}')
cy.contains(displayName + item.ext + ' ' + resources.CREATE_CONTENT_SUCCESS_MESSAGE).should('exist')
cy.contains(displayName + item.ext).should('exist')
cy.get('[aria-label="Close"]').click()
})
})
})

it('rename document should work', () => {
const fileName = createNewFileName()
const newName = createNewFileName()
uploadNewFileAndOpenContextMenuItem(currentUser.email, fileName, contextMenuItems.rename)
// wait for input to be focused
cy.wait(1000)
cy.get('.rename')
.clear()
.type(`${newName}{enter}`)
cy.contains(newName)
cy.contains(resources.EDIT_PROPERTIES_SUCCESS_MESSAGE.replace('{contentName}', fileName))
})

it('copy to context menu item should work', () => {
const fileName = createNewFileName()
const copyToPath = 'Sample folder'
uploadNewFileAndOpenContextMenuItem(currentUser.email, fileName, contextMenuItems.copyTo)
// List picker component
selectPathInListPicker({ path: copyToPath, action: 'Copy' })
// Copy to confirm dialog
cy.contains('div[data-cy="copyTo"] h5', resources.COPY)
cy.contains('div[data-cy="copyTo"] button', resources.COPY).click()
cy.contains(`${fileName} ${resources.COPY_BATCH_SUCCESS_MESSAGE}`)
.should('be.visible')
.get('button[aria-label="Close"]')
.click()
// check successful copy
cy.contains(fileName).should('exist')
moveToFolderAndCheckIfFileExists(copyToPath, fileName)
})

it('move to context menu item should work', () => {
const fileName = createNewFileName()
const moveToPath = 'Sample folder'
uploadNewFileAndOpenContextMenuItem(currentUser.email, fileName, contextMenuItems.moveTo)
// List picker component
selectPathInListPicker({ path: moveToPath, action: 'Move' })
// Move to confirm dialog
cy.contains('div[data-cy="moveTo"] h5', resources.MOVE)
cy.contains('div[data-cy="moveTo"] button', resources.MOVE).click()
cy.contains(`${fileName} ${resources.MOVE_BATCH_SUCCESS_MESSAGE}`)
.should('be.visible')
.get('button[aria-label="Close"]')
.click()
// check successful copy
cy.contains(fileName).should('not.exist')
moveToFolderAndCheckIfFileExists(moveToPath, fileName)
})

it('edit properties should work', () => {
const fileName = createNewFileName()
uploadNewFileAndOpenContextMenuItem(currentUser.email, fileName, contextMenuItems.editProperties)
cy.contains('div[data-cy="editProperties"]', resources.EDIT_PROPERTIES).should('exist')
const properties = {
keywords: { value: 'keyword', selector: 'div[data-cy="editProperties"] .ql-editor' },
index: { value: '1', selector: '#Index' },
displayName: { value: Chance().word(), selector: '#DisplayName' },
watermark: { value: Chance().word(), selector: '#Watermark' },
}
Object.keys(properties).forEach(key =>
cy
.get(properties[key].selector)
.clear()
.type(properties[key].value.toString()),
)
cy.contains('div[data-cy="editProperties"] button', 'Submit').click()
cy.contains(resources.EDIT_PROPERTIES_SUCCESS_MESSAGE.replace('{contentName}', fileName)).should('exist')
openContextMenuItem(properties.displayName.value + '.png', contextMenuItems.editProperties)
Object.keys(properties).forEach(key => {
cy.get(properties[key].selector).should(key === 'keywords' ? 'have.text' : 'have.value', properties[key].value)
})
})

it('check out and undo should work', () => {
const fileName = createNewFileName()
uploadNewFileAndOpenContextMenuItem(currentUser.email, fileName, contextMenuItems.checkOut)
cy.contains(resources.CHECKOUT_SUCCESS_MESSAGE.replace('{contentName}', fileName)).should('exist')
cy.get('div[title="Checked out by: Me"]').should('exist')
openContextMenuItem(fileName, contextMenuItems.editProperties)
cy.get('#Watermark')
.clear()
.type('sometext')
cy.contains('div[data-cy="editProperties"] button', 'Submit').click()
openContextMenuItem(fileName, contextMenuItems.undoChanges)
cy.contains(resources.UNDOCHECKOUT_SUCCESS_MESSAGE.replace('{contentName}', fileName)).should('exist')
cy.wait(1000) // wait for undo
openContextMenuItem(fileName, contextMenuItems.editProperties)
cy.get('#Watermark').should('not.have.value', 'sometext')
})
})
@@ -0,0 +1,65 @@
import {
contextMenuItems,
createNewFileName,
openContextMenu,
uploadNewFileAndOpenContextMenuItem,
} from '../../support/documents'

context('The documents page with admin', () => {
const adminUser = {
email: 'e2e.admin@sensenet.com',
password: 'e2eadmin',
doclibPath: 'Root/Profiles/Public/e2e-admin/Document_Library',
}
beforeEach(() => {
cy.login(adminUser.email, adminUser.password)
})

it('should check actions availability', () => {
const fileName = createNewFileName()
cy.uploadWithApi({
parentPath: adminUser.doclibPath,
fileName,
})
cy.contains('div', fileName, { timeout: 10000 }).should('exist')
openContextMenu(fileName)
Object.keys(contextMenuItems).forEach(item => {
if (item === 'checkIn' || item === 'undoChanges') {
return
}
cy.get(`[title="${contextMenuItems[item]}"]`).should('exist')
})
})

it('should be able to open viewer from context menu and close with esc', () => {
const fileName = createNewFileName()
cy.uploadWithApi({
parentPath: adminUser.doclibPath,
fileName,
})
cy.contains('div', fileName, { timeout: 10000 }).should('exist')
openContextMenu(fileName)
cy.get(`[title="${contextMenuItems.preview}"]`).click()
cy.contains('Preview image generation is in progress').should('exist')
cy.get('.overlay').should('exist')
cy.get('body').type('{esc}')
cy.get('.overlay').should('not.exist')
})

describe('share context menu', () => {
it('should list valid emails', () => {
const fileName = createNewFileName()
uploadNewFileAndOpenContextMenuItem('e2e-admin', fileName, contextMenuItems.shareContent)
cy.get('form input[type="email"]').type('invalid{enter}')
cy.contains('invalid').should('not.exist')
cy.get('form input[type="email"]')
.clear()
.type('asd@asd.com{enter}')
cy.contains('asd@asd.com').should('exist')
cy.contains('button', 'Ok').click()
openContextMenu(fileName)
cy.get(`[title="${contextMenuItems.shareContent}"]`).click()
cy.contains('asd@asd.com').should('exist')
})
})
})
18 changes: 18 additions & 0 deletions examples/sn-dms-demo/cypress/integration/typings.d.ts
@@ -0,0 +1,18 @@
import { Content, Repository, UploadFileOptions, UploadResponse } from '@sensenet/client-core'
import { User } from '@sensenet/default-content-types'
import { Omit } from 'cypress/types/lodash'

declare global {
interface Window {
repository: Repository
}
namespace Cypress {
interface Chainable {
login: (email: string, password: string) => Cypress.Chainable<boolean>
registerUser: (email: string, password: string) => Cypress.Chainable<User>
uploadWithApi: (options: UploadOptions) => Cypress.Chainable<UploadResponse>
}
}
}

export type UploadOptions = Partial<Omit<UploadFileOptions<Content>, 'file'>> & { fileName: string }
45 changes: 45 additions & 0 deletions examples/sn-dms-demo/cypress/integration/user/logins.spec.ts
@@ -0,0 +1,45 @@
import { resources } from '../../../src/assets/resources'

context('The login page', () => {
beforeEach(() => {
cy.visit('/#/login')
})

it('requires email', () => {
cy.get('form')
.contains(resources.LOGIN_BUTTON_TEXT)
.click()
cy.contains(resources.EMAIL_IS_NOT_VALID_MESSAGE).should('exist')
})

it('requires password', () => {
cy.get('input[name=email]').type('businesscat@sensenet.com{enter}')
cy.contains(resources.PASSWORD_IS_NOT_VALID_MESSAGE).should('exist')
})

it('requires valid email and password', () => {
cy.get('input[name=email]').type('businesscat@sensenet.com')
cy.get('input[name=password]').type(`invalid{enter}`)
cy.contains(resources.WRONG_USERNAME_OR_PASSWORD).should('exist')
})

it('should navigate to registration page', () => {
cy.contains(resources.REGISTER_TAB_TEXT).click()
cy.url().should('include', '/registration')
cy.get('input').should('have.length', 3)
})

it('can authenticate properly', () => {
cy.get('input[name=email]').type('businesscat@sensenet.com')
cy.get('input[name=password]').type(`businesscat{enter}`)

cy.url({ timeout: 10000 }).should('include', '/documents')

cy.get('div[aria-label="Business Cat"]').should('exist')

cy.window()
.its('localStorage')
.invoke('getItem', 'sn-https://dmsservice.demo.sensenet.com-access')
.should('not.be.empty')
})
})
12 changes: 12 additions & 0 deletions examples/sn-dms-demo/cypress/integration/user/logout.spec.ts
@@ -0,0 +1,12 @@
context('Logout', () => {
beforeEach(() => {
cy.login('businesscat@sensenet.com', 'businesscat')
})

it('should navigate to login page', () => {
cy.contains('[data-cy=appbar]', 'Document library', { timeout: 10000 }).should('exist')
cy.get('[aria-owns="actionmenu"] > .material-icons').click()
cy.get('[title="Log out"]').click()
cy.url().should('include', '/login')
})
})

0 comments on commit 5bc97b1

Please sign in to comment.