test: add e2e automated tests
This commit is contained in:
parent
50d724eb6a
commit
0819304e1e
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@ -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
3
example/.gitignore
vendored
@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
cypress/downloads
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
@ -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' />]}
|
||||||
|
@ -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>
|
||||||
|
@ -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
13
example/cypress.config.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
65
example/cypress/e2e/Form.cy.ts
Normal file
65
example/cypress/e2e/Form.cy.ts
Normal 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 {}
|
49
example/cypress/e2e/Header.cy.ts
Normal file
49
example/cypress/e2e/Header.cy.ts
Normal 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
3007
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user