added e2e tests (#298)

- added e2e tests for
  - banner
  - history
  - intro
  - language
  - link
- added e2e workflow
- added cypress badge to README
This commit is contained in:
Philip Molares 2020-07-16 11:22:53 +02:00 committed by GitHub
parent 1a5d4f6db8
commit f0fe7f5ac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1332 additions and 77 deletions

View file

@ -2,9 +2,9 @@ name: lint and build
on: on:
push: push:
branches: [master, dev] branches: [master]
pull_request: pull_request:
branches: [master, dev] branches: [master]
jobs: jobs:
build: build:

34
.github/workflows/e2e.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: e2e
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
end2end:
runs-on: ubuntu-latest
strategy:
matrix:
browser: ['e2e:chrome', 'e2e:firefox']
name: ${{ matrix.browser }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Cache node_modules
uses: actions/cache@v1.1.0
with:
path: node_modules
key: node_modules
- name: Cache ~/.cache
uses: actions/cache@v1.1.0
with:
path: ~/.cache
key: cache
- name: install cypress dependencies
run: sudo apt-get install libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- name: Install dependencies
run: yarn install
- name: Run e2e in chrome
run: yarn ${{ matrix.browser }}

View file

@ -1,5 +1,7 @@
# CodiMD - React Client # CodiMD - React Client
![e2e](https://github.com/codimd/react-client/workflows/e2e/badge.svg)
This is the new, improved and better looking frontend for CodiMD 2.0. This is the new, improved and better looking frontend for CodiMD 2.0.
Our goal is to recreate the current frontend in react and to improve it. Our goal is to recreate the current frontend in react and to improve it.
@ -18,6 +20,17 @@ This should run the app in the development mode and open [http://localhost:3000]
The page will reload if you make edits. The page will reload if you make edits.
You will also see any lint errors in the console. You will also see any lint errors in the console.
### Tests
#### End2End
We use [cypress](https://cypress.io) for e2e tests.
1. Run the frontend with `yarn start`
2. RUn `yarn cy:open` to open the cypress test loader
3. Choose your browser and test
4. Let the tests run
## Production mode ## Production mode
1. Clone this repo (e.g. `git clone https://github.com/codimd/react-client.git codimd-react-client`) 1. Clone this repo (e.g. `git clone https://github.com/codimd/react-client.git codimd-react-client`)

8
cypress.json Normal file
View file

@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:3000/",
"experimentalFetchPolyfill": true,
"firefoxGcInterval": {
"runMode": null,
"openMode": null
}
}

23
cypress/.eslintrc.json Normal file
View file

@ -0,0 +1,23 @@
{
"parserOptions": {
"tsconfigRootDir": "",
"project": [
"./cypress/tsconfig.json"
]
},
"plugins": [
"cypress",
"chai-friendly"
],
"extends": [
"plugin:cypress/recommended"
],
"rules": {
"@typescript-eslint/no-unused-expressions": 0,
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": 2
},
"env": {
"cypress/globals": true
}
}

View file

@ -0,0 +1,30 @@
export const languages: string[] = [
'English',
'简体中文',
'繁體中文',
'Français',
'Deutsch',
'日本語',
'Español',
'Català',
'Ελληνικά',
'Português',
'Italiano',
'Türkçe',
'Русский',
'Nederlands',
'Hrvatski',
'Polski',
'Українська',
'हिन्दी',
'Svenska',
'Esperanto',
'Dansk',
'한국어',
'Bahasa Indonesia',
'Cрпски',
'Tiếng Việt',
'العربية',
'Česky',
'Slovensky'
]

View file

@ -0,0 +1,26 @@
import { banner } from '../support/config'
describe('Banner', () => {
beforeEach(() => {
cy.visit('/')
expect(localStorage.getItem('bannerTimeStamp')).to.be.null
})
it('shows the correct alert banner text', () => {
cy.get('.alert-primary.show')
.contains(banner.text)
})
it('can be dismissed', () => {
cy.get('.alert-primary.show')
.contains(banner.text)
cy.get('.alert-primary.show')
.find('.fa-times')
.click()
.then(() => {
expect(localStorage.getItem('bannerTimeStamp')).to.equal(banner.timestamp)
})
cy.get('.alert-primary.show')
.should('not.exist')
})
})

View file

@ -0,0 +1,35 @@
describe('History', () => {
beforeEach(() => {
cy.visit('/history')
})
describe('History Mode', () => {
it('Cards', () => {
cy.get('div.card')
})
it('Table', () => {
cy.get('i.fa-table')
.click()
cy.get('table.history-table')
})
})
describe('Pinning', () => {
it('Cards', () => {
cy.get('.fa-thumb-tack')
.first()
.click()
cy.get('.modal-dialog')
.should('be.visible')
})
it('Table', () => {
cy.get('.fa-thumb-tack')
.first()
.click()
cy.get('.modal-dialog')
.should('be.visible')
})
})
})

View file

@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
describe('Intro', () => {
beforeEach(() => {
cy.visit('/')
})
describe('Cover Button are hidden when logged in', () => {
it('Sign in Cover Button', () => {
cy.get('.cover-button.btn-success')
.should('not.exist')
})
it('Features Cover Button', () => {
cy.get('.cover-button.btn-primary')
.should('not.exist')
})
})
describe('Cover Button are shown when logged out', () => {
beforeEach(() => {
cy.logout()
})
it('Sign in Cover Button', () => {
cy.get('.cover-button.btn-success')
.should('exist')
})
it('Features Cover Button', () => {
cy.get('.cover-button.btn-primary')
.should('exist')
})
})
describe('Version', () => {
it('can be opened', () => {
cy.get('#versionModal')
.should('not.be.visible')
cy.get('#version')
.click()
cy.get('#versionModal')
.should('be.visible')
})
it('can be closed', () => {
cy.get('#versionModal')
.should('not.be.visible')
cy.get('#version')
.click()
cy.get('#versionModal')
.should('be.visible')
cy.get('body')
.click()
cy.get('#versionModal')
.should('not.be.visible')
})
})
})

View file

@ -0,0 +1,30 @@
import { languages } from '../fixtures/languages'
describe('Languages', () => {
beforeEach(() => {
cy.visit('/')
})
it('all languages are available', () => {
cy.get('option')
.as('languages')
cy.get('@languages')
.should('have.length', 28)
languages.forEach(language => {
cy.get('@languages').contains(language)
})
})
it('language changes affect the UI', () => {
cy.get('select')
.select('English')
cy.get('.d-inline-flex.btn-primary')
.find('span')
.contains('New note')
cy.get('select')
.select('Deutsch')
cy.get('.d-inline-flex.btn-primary')
.find('span')
.contains('Neue Notiz')
})
})

View file

@ -0,0 +1,165 @@
import '../support/index'
describe('Links Intro', () => {
beforeEach(() => {
cy.visit('/')
})
describe('Cover Buttons', () => {
beforeEach(() => {
cy.logout()
})
it('Sign in Cover Button', () => {
cy.get('.cover-button.btn-success')
.click()
cy.url()
.should('include', '/login')
})
it('Features Cover Button', () => {
cy.get('.cover-button.btn-primary')
.click()
cy.url()
.should('include', '/features')
})
})
it('History', () => {
cy.get('#navLinkHistory')
.click()
cy.url()
.should('include', '/history')
cy.get('#navLinkIntro')
.click()
cy.url()
.should('include', '/intro')
})
describe('Menu Buttons logged out', () => {
beforeEach(() => {
cy.logout()
})
it('New guest note', () => {
cy.get('.d-inline-flex.btn-primary')
.click()
cy.url()
.should('include', '/new')
})
it('Sign In', () => {
cy.get('.btn-success.btn-sm')
.click()
cy.url()
.should('include', '/login')
})
})
describe('Menu Buttons logged in', () => {
it('New note', () => {
cy.get('.d-inline-flex.btn-primary').click()
cy.url()
.should('include', '/new')
})
describe('User Menu', () => {
beforeEach(() => {
cy.get('#dropdown-user').click()
})
it('Features', () => {
cy.get('a.dropdown-item > i.fa-bolt')
.click()
cy.url()
.should('include', '/features')
})
it('Features', () => {
cy.get('a.dropdown-item > i.fa-user')
.click()
cy.url()
.should('include', '/profile')
})
})
})
describe('Feature Links', () => {
it('Share-Notes', () => {
cy.get('i.fa-bolt.fa-3x')
.click()
cy.url()
.should('include', '/features#Share-Notes')
})
it('MathJax', () => {
cy.get('i.fa-bar-chart.fa-3x')
.click()
cy.url()
.should('include', '/features#MathJax')
})
it('Slide-Mode', () => {
cy.get('i.fa-television.fa-3x')
.click()
cy.url()
.should('include', '/features#Slide-Mode')
})
})
describe('Powered By Links', () => {
it('CodiMD', () => {
cy.get('a[href="https://codimd.org"]')
.checkExternalLink('https://codimd.org')
})
it('Releases', () => {
cy.get('a[href*="/n/release-notes"]')
.click()
cy.url()
.should('include', '/n/release-notes')
})
it('Privacy', () => {
cy.get('a[href="https://example.com/privacy"]')
.checkExternalLink('https://example.com/privacy')
})
it('TermsOfUse', () => {
cy.get('a[href="https://example.com/termsOfUse"]')
.checkExternalLink('https://example.com/termsOfUse')
})
it('Imprint', () => {
cy.get('a[href="https://example.com/imprint"]')
.checkExternalLink('https://example.com/imprint')
})
})
describe('Follow us Links', () => {
it('Github', () => {
cy.get('a[href="https://github.com/codimd/server"]')
.checkExternalLink('https://github.com/codimd/server')
})
it('Discourse', () => {
cy.get('a[href="https://community.codimd.org"]')
.checkExternalLink('https://community.codimd.org')
})
it('Matrix', () => {
cy.get('a[href="https://riot.im/app/#/room/#codimd:matrix.org"]')
.checkExternalLink('https://riot.im/app/#/room/#codimd:matrix.org')
})
it('Mastodon', () => {
cy.get('a[href="https://social.codimd.org/mastodon"]')
.checkExternalLink('https://social.codimd.org/mastodon')
})
it('POEditor', () => {
cy.get('a[href="https://translate.codimd.org"]')
.checkExternalLink('https://translate.codimd.org')
})
})
})

View file

@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
interface Chainable {
/**
* Custom command to check an external Link.
* @example cy.get(a#extern).checkExternalLink('http://example.com')
*/
checkExternalLink (url: string): Chainable<Element>
}
}
Cypress.Commands.add('checkExternalLink', { prevSubject: 'element' }, ($element: JQuery, url: string) => {
cy.wrap($element).should('have.attr', 'href', url)
.should('have.attr', 'target', '_blank')
})

47
cypress/support/config.ts Normal file
View file

@ -0,0 +1,47 @@
export const banner = {
text: 'This is the mock banner call',
timestamp: '2020-05-22T20:46:08.962Z'
}
beforeEach(() => {
cy.server()
cy.route({
url: '/api/v2/config',
response: {
allowAnonymous: true,
authProviders: {
facebook: true,
github: true,
twitter: true,
gitlab: true,
dropbox: true,
ldap: true,
google: true,
saml: true,
oauth2: true,
email: true,
openid: true
},
branding: {
name: 'ACME Corp',
logo: 'http://localhost:3000/acme.png'
},
banner: banner,
customAuthNames: {
ldap: 'FooBar',
oauth2: 'Olaf2',
saml: 'aufSAMLn.de'
},
specialLinks: {
privacy: 'https://example.com/privacy',
termsOfUse: 'https://example.com/termsOfUse',
imprint: 'https://example.com/imprint'
},
version: {
version: 'mock',
sourceCodeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
}
}
})
})

18
cypress/support/index.ts Normal file
View file

@ -0,0 +1,18 @@
// ***********************************************************
// This example support/index.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './checkLinks'
import './config'
import './login'

15
cypress/support/login.ts Normal file
View file

@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
interface Chainable {
/**
* Custom command to log the user out.
* @example cy.logout()
*/
logout (): Chainable<Window>
}
}
Cypress.Commands.add('logout', () => {
cy.get('#dropdown-user').click()
cy.get('.fa-sign-out').click()
})

12
cypress/tsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": [
"**/*.ts"
]
}

22
cypress/webpack.config.js Normal file
View file

@ -0,0 +1,22 @@
const path = require('path')
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}

View file

@ -85,7 +85,12 @@
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"cy:open": "cypress open",
"cy:run:chrome": "cypress run --browser chrome",
"cy:run:firefox": "cypress run --browser firefox",
"e2e:chrome": "start-server-and-test start http-get://localhost:3000 cy:run:chrome",
"e2e:firefox": "start-server-and-test start http-get://localhost:3000 cy:run:firefox"
}, },
"eslintConfig": { "eslintConfig": {
"parserOptions": { "parserOptions": {
@ -121,9 +126,17 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@cypress/webpack-preprocessor": "^5.4.1",
"@testing-library/cypress": "^6.0.0",
"@types/redux-devtools": "3.0.47", "@types/redux-devtools": "3.0.47",
"@types/redux-devtools-extension": "2.13.2", "@types/redux-devtools-extension": "2.13.2",
"@types/testing-library__cypress": "^5.0.6",
"cypress": "^4.9.0",
"eslint-plugin-chai-friendly": "^0.6.0",
"eslint-plugin-cypress": "^2.11.1",
"redux-devtools": "3.5.0", "redux-devtools": "3.5.0",
"redux-devtools-extension": "2.13.8" "redux-devtools-extension": "2.13.8",
"start-server-and-test": "^1.11.0",
"ts-loader": "^7.0.5"
} }
} }

View file

@ -27,9 +27,9 @@
"saml": "aufSAMLn.de" "saml": "aufSAMLn.de"
}, },
"specialLinks": { "specialLinks": {
"privacy": "test", "privacy": "https://example.com/privacy",
"termsOfUse": "test", "termsOfUse": "https://example.com/termsOfUse",
"imprint": "test" "imprint": "https://example.com/imprint"
}, },
"version": { "version": {
"version": "mock", "version": "mock",

View file

@ -3,11 +3,12 @@ import { ForkAwesomeIcon, IconName } from '../fork-awesome/fork-awesome-icon'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import { LinkWithTextProps } from './types' import { LinkWithTextProps } from './types'
export const ExternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, className = 'text-light' }) => { export const ExternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light' }) => {
return ( return (
<a href={href} <a href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
id={id}
className={className} className={className}
dir='auto' dir='auto'
> >

View file

@ -4,10 +4,12 @@ import { ForkAwesomeIcon, IconName } from '../fork-awesome/fork-awesome-icon'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import { LinkWithTextProps } from './types' import { LinkWithTextProps } from './types'
export const InternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, className = 'text-light' }) => { export const InternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light' }) => {
return ( return (
<Link to={href} <Link to={href}
className={className}> className={className}
id={id}
>
<ShowIf condition={!!icon}> <ShowIf condition={!!icon}>
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true}/>&nbsp; <ForkAwesomeIcon icon={icon as IconName} fixedWidth={true}/>&nbsp;
</ShowIf> </ShowIf>

View file

@ -2,16 +2,17 @@ import { StringMap, TOptionsBase } from 'i18next'
import { IconName } from '../fork-awesome/fork-awesome-icon' import { IconName } from '../fork-awesome/fork-awesome-icon'
export interface GeneralLinkProp { export interface GeneralLinkProp {
href: string; href: string
icon?: IconName; icon?: IconName
id?: string
className?: string className?: string
} }
export interface LinkWithTextProps extends GeneralLinkProp { export interface LinkWithTextProps extends GeneralLinkProp {
text: string; text: string
} }
export interface TranslatedLinkProps extends GeneralLinkProp{ export interface TranslatedLinkProps extends GeneralLinkProp{
i18nKey: string; i18nKey: string
i18nOption?: (TOptionsBase & StringMap) | string i18nOption?: (TOptionsBase & StringMap) | string
} }

View file

@ -17,10 +17,10 @@ const HeaderBar: React.FC = () => {
return ( return (
<Navbar className="justify-content-between"> <Navbar className="justify-content-between">
<div className="nav header-nav"> <div className="nav header-nav">
<HeaderNavLink to="/intro"> <HeaderNavLink to="/intro" id='navLinkIntro'>
<Trans i18nKey="landing.navigation.intro"/> <Trans i18nKey="landing.navigation.intro"/>
</HeaderNavLink> </HeaderNavLink>
<HeaderNavLink to="/history"> <HeaderNavLink to="/history" id='navLinkHistory'>
<Trans i18nKey="landing.navigation.history"/> <Trans i18nKey="landing.navigation.history"/>
</HeaderNavLink> </HeaderNavLink>
</div> </div>

View file

@ -1,16 +1,17 @@
import React from 'react'
import { Nav } from 'react-bootstrap' import { Nav } from 'react-bootstrap'
import { LinkContainer } from 'react-router-bootstrap' import { LinkContainer } from 'react-router-bootstrap'
import React from 'react'
export interface HeaderNavLinkProps { export interface HeaderNavLinkProps {
to: string to: string
id: string
} }
export const HeaderNavLink: React.FC<HeaderNavLinkProps> = (props) => { export const HeaderNavLink: React.FC<HeaderNavLinkProps> = ({ to, id, children }) => {
return ( return (
<Nav.Item> <Nav.Item>
<LinkContainer to={props.to}> <LinkContainer to={to}>
<Nav.Link className="text-light" href={props.to}>{props.children}</Nav.Link> <Nav.Link id={id} className="text-light" href={to}>{children}</Nav.Link>
</LinkContainer> </LinkContainer>
</Nav.Item> </Nav.Item>
) )

View file

@ -34,8 +34,8 @@ export const VersionInfo: React.FC = () => {
return ( return (
<Fragment> <Fragment>
<Link to={'#'} className={'text-light'} onClick={handleShow}><Trans i18nKey={'landing.versionInfo.versionInfo'}/></Link> <Link id='version' to={'#'} className={'text-light'} onClick={handleShow}><Trans i18nKey={'landing.versionInfo.versionInfo'}/></Link>
<Modal show={show} onHide={handleClose} animation={true}> <Modal id='versionModal' show={show} onHide={handleClose} animation={true}>
<Modal.Body className="text-dark"> <Modal.Body className="text-dark">
<h3><Trans i18nKey={'landing.versionInfo.title'}/></h3> <h3><Trans i18nKey={'landing.versionInfo.title'}/></h3>
<Row> <Row>

796
yarn.lock

File diff suppressed because it is too large Load diff