test: add e2e automated tests

This commit is contained in:
Divlo 2022-08-26 23:47:48 +02:00
parent 50d724eb6a
commit 0819304e1e
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
12 changed files with 3177 additions and 6 deletions

View File

@ -23,3 +23,26 @@ jobs:
- name: 'Test' - name: 'Test'
run: 'npm run test' run: 'npm run test'
test-e2e:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0'
with:
node-version: 'lts/*'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Build Package'
run: 'npm run build'
- name: 'Build Example'
run: 'cd example && npm install && npm run build'
- name: 'End To End (e2e) Test Example'
run: 'npm run test:e2e'

3
example/.gitignore vendored
View File

@ -7,6 +7,9 @@
# testing # testing
/coverage /coverage
cypress/screenshots
cypress/videos
cypress/downloads
# next.js # next.js
/.next/ /.next/

View File

@ -18,7 +18,7 @@ export const About: React.FC = () => {
</Link> </Link>
</h2> </h2>
<p className='max-w-lg mt-6 text-base'> <p className='max-w-lg mt-6 text-base' data-cy='main-description'>
<Translation <Translation
i18nKey='common:about' i18nKey='common:about'
components={[<TextSpecial key='special' />]} components={[<TextSpecial key='special' />]}

View File

@ -22,7 +22,7 @@ export const FormExample: React.FC = () => {
formData, formData,
formElement formElement
) => { ) => {
await simulateServerRequest(4000) await simulateServerRequest(2000)
console.log('onSubmit:', formData) console.log('onSubmit:', formData)
formElement.reset() formElement.reset()
return { return {
@ -54,7 +54,7 @@ export const FormExample: React.FC = () => {
error={getFirstErrorTranslation(errors.email)} error={getFirstErrorTranslation(errors.email)}
/> />
<Button className='mt-6 w-full' type='submit'> <Button className='mt-6 w-full' type='submit' data-cy='submit'>
Submit Submit
</Button> </Button>
</Form> </Form>

View File

@ -24,8 +24,13 @@ export const Input: React.FC<InputProps> = (props) => {
{...rest} {...rest}
id={name} id={name}
name={name} name={name}
data-cy={`input-${name}`}
/>
<FormState
id={`error-${name ?? 'input'}`}
state={error == null ? 'idle' : 'error'}
message={error}
/> />
<FormState state={error == null ? 'idle' : 'error'} message={error} />
</div> </div>
</div> </div>
) )

13
example/cypress.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'cypress'
export default defineConfig({
fixturesFolder: false,
video: false,
downloadsFolder: undefined,
screenshotOnRunFailure: false,
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: false
}
})

View File

@ -0,0 +1,65 @@
describe('Form', () => {
beforeEach(() => {
cy.visit('/')
})
it('suceeds, reset input values and display the global success message', () => {
cy.get('[data-cy=input-name]').type('John')
cy.get('[data-cy=input-email]').type('john@john.com')
cy.get('#error-name').should('not.exist')
cy.get('#error-email').should('not.exist')
cy.get('[data-cy=submit]').click()
cy.get('[data-cy=input-name]').should('have.value', '')
cy.get('[data-cy=input-email]').should('have.value', '')
cy.get('#message').should(
'have.text',
'Success: The form has been submitted.'
)
})
it('fails with all inputs as required with error messages and update error messages when updating language (translation)', () => {
const requiredErrorMessage = {
en: 'Error: Oops, this field is required 🙈.',
fr: 'Erreur: Oups, ce champ est obligatoire 🙈.'
}
cy.get('#error-name').should('not.exist')
cy.get('#error-email').should('not.exist')
cy.get('[data-cy=submit]').click()
cy.get('#error-name').should('have.text', requiredErrorMessage.en)
cy.get('#error-email').should('have.text', requiredErrorMessage.en)
cy.get('[data-cy=language-click]').click()
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
cy.get('#error-name').should('have.text', requiredErrorMessage.fr)
cy.get('#error-email').should('have.text', requiredErrorMessage.fr)
})
it('fails with invalid name (less than 3 characters)', () => {
cy.get('[data-cy=input-name]').type('a')
cy.get('[data-cy=submit]').click()
cy.get('#error-name').should(
'have.text',
'Error: The field must contain at least 3 characters.'
)
})
it('fails with invalid name (more than 10 characters)', () => {
cy.get('[data-cy=input-name]').type('12345678910aaaa')
cy.get('[data-cy=submit]').click()
cy.get('#error-name').should(
'have.text',
'Error: The field must contain at most 10 characters.'
)
})
it('fails with wrong email format', () => {
cy.get('#error-email').should('not.exist')
cy.get('[data-cy=input-email]').type('test')
cy.get('[data-cy=submit]').click()
cy.get('#error-email').should(
'have.text',
'Error: Mmm… It seems that this email is not valid 🤔.'
)
})
})
export {}

View File

@ -0,0 +1,49 @@
describe('Header', () => {
beforeEach(() => cy.visit('/'))
describe('Switch theme color (dark/light)', () => {
it('should switch theme from `dark` (default) to `light`', () => {
cy.get('[data-cy=switch-theme-dark]').should('be.visible')
cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
cy.get('body').should(
'not.have.css',
'background-color',
'rgb(255, 255, 255)'
)
cy.get('[data-cy=switch-theme-click]').click()
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
cy.get('[data-cy=switch-theme-light]').should('be.visible')
cy.get('body').should(
'have.css',
'background-color',
'rgb(255, 255, 255)'
)
})
})
describe('Switch Language', () => {
it('should switch language from EN (default) to FR', () => {
cy.get('[data-cy=main-description]').contains('This is an example')
cy.get('[data-cy=language-flag-text]').contains('EN')
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-click]').click()
cy.get('[data-cy=languages-list]').should('be.visible')
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-flag-text]').contains('FR')
cy.get('[data-cy=main-description]').contains('Ceci est un exemple')
})
it('should close the language list menu when clicking outside', () => {
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-click]').click()
cy.get('[data-cy=languages-list]').should('be.visible')
cy.get('[data-cy=main-description]').click()
cy.get('[data-cy=languages-list]').should('not.be.visible')
})
})
})
export {}

3007
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,12 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\""
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.24.28",
"clsx": "1.2.1", "clsx": "1.2.1",
"next": "12.2.5", "next": "12.2.5",
"next-themes": "0.2.0", "next-themes": "0.2.0",
@ -22,9 +25,11 @@
"@types/react": "18.0.17", "@types/react": "18.0.17",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",
"autoprefixer": "10.4.8", "autoprefixer": "10.4.8",
"cypress": "10.6.0",
"eslint": "8.22.0", "eslint": "8.22.0",
"eslint-config-next": "12.2.5", "eslint-config-next": "12.2.5",
"postcss": "8.4.16", "postcss": "8.4.16",
"start-server-and-test": "1.14.0",
"tailwindcss": "3.1.8", "tailwindcss": "3.1.8",
"typescript": "4.8.2" "typescript": "4.8.2"
} }

View File

@ -5,7 +5,7 @@ import '../styles/globals.css'
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
return ( return (
<ThemeProvider attribute='class' defaultTheme='light'> <ThemeProvider attribute='class' defaultTheme='dark'>
<Component {...pageProps} /> <Component {...pageProps} />
</ThemeProvider> </ThemeProvider>
) )

View File

@ -6,6 +6,7 @@
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"types": ["cypress"],
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,