diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/cypress/e2e/back-to-top.cy.ts | 14 | ||||
| -rw-r--r-- | tests/cypress/e2e/nav.cy.ts | 74 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/blog.cy.ts | 44 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/contact.cy.ts | 41 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/cv.cy.ts | 6 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/homepage.cy.ts | 9 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/legal-notice.cy.ts | 6 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/projects.cy.ts | 6 | ||||
| -rw-r--r-- | tests/cypress/e2e/search.cy.ts | 41 | ||||
| -rw-r--r-- | tests/cypress/e2e/settings.cy.ts | 177 | ||||
| -rw-r--r-- | tests/cypress/fixtures/example.json | 5 | ||||
| -rw-r--r-- | tests/cypress/plugins/index.ts | 22 | ||||
| -rw-r--r-- | tests/cypress/support/commands.ts | 6 | ||||
| -rw-r--r-- | tests/cypress/support/e2e.ts | 20 | ||||
| -rw-r--r-- | tests/cypress/tsconfig.json | 10 | ||||
| -rw-r--r-- | tests/jest/__mocks__/matchMedia.mock.js | 15 | ||||
| -rw-r--r-- | tests/utils/index.tsx | 43 |
17 files changed, 539 insertions, 0 deletions
diff --git a/tests/cypress/e2e/back-to-top.cy.ts b/tests/cypress/e2e/back-to-top.cy.ts new file mode 100644 index 0000000..06ac3e6 --- /dev/null +++ b/tests/cypress/e2e/back-to-top.cy.ts @@ -0,0 +1,14 @@ +describe('Back to top', () => { + it('show a back to top button when scrolling', async () => { + cy.visit('/'); + cy.findByRole('link', { name: /Retour en haut de page/i }).should( + 'not.be.visible' + ); + + // See @components/templates/layout/layout.tsx for scroll position. + cy.scrollTo(0, 300); + cy.findByRole('link', { name: /Retour en haut de page/i }).should( + 'be.visible' + ); + }); +}); diff --git a/tests/cypress/e2e/nav.cy.ts b/tests/cypress/e2e/nav.cy.ts new file mode 100644 index 0000000..5851058 --- /dev/null +++ b/tests/cypress/e2e/nav.cy.ts @@ -0,0 +1,74 @@ +describe( + 'Main navigation', + { viewportWidth: 1280, viewportHeight: 720 }, + () => { + beforeEach(() => { + cy.visit('/'); + }); + + it( + 'should show hamburger button on small devices', + { viewportWidth: 810, viewportHeight: 1080 }, + () => { + cy.findByLabelText(/Ouvrir le menu/i).should('exist'); + } + ); + + it( + 'should open and close main nav menu by clicking on hamburger button', + { viewportWidth: 810, viewportHeight: 1080 }, + () => { + cy.findByLabelText(/Fermer le menu/i).should('not.exist'); + cy.findByRole('link', { name: /Blog/i }).should('not.exist'); + cy.findByLabelText(/Ouvrir le menu/i).click(); + cy.findByLabelText(/Ouvrir le menu/i).should('not.exist'); + cy.findByLabelText(/Fermer le menu/i).should('exist'); + cy.findByRole('link', { name: /Blog/i }).should('exist'); + cy.findByLabelText(/Fermer le menu/i).click(); + cy.findByLabelText(/Fermer le menu/i).should('not.exist'); + cy.findByRole('link', { name: /Blog/i }).should('not.exist'); + cy.findByLabelText(/Ouvrir le menu/i).should('exist'); + } + ); + + it('should hide hamburger button on large devices', () => { + cy.findByLabelText(/Ouvrir le menu/i).should('be.hidden'); + }); + + it('should navigate to the blog page', async () => { + cy.findByRole('link', { name: /Blog/i }).click(); + cy.url().should('include', '/blog'); + cy.findByRole('heading', { level: 1 }).contains('Blog'); + }); + + it('should navigate to the CV page', async () => { + cy.findByRole('link', { name: /CV/i }).click(); + cy.url().should('include', '/cv'); + cy.findByRole('heading', { level: 1 }).contains('CV'); + }); + + it('should navigate to the projects page', async () => { + cy.findByRole('link', { name: /Projects/i }).click(); + cy.url().should('include', '/projets'); + cy.findByRole('heading', { level: 1 }).contains('Projets'); + }); + + it('should navigate to the contact page', async () => { + cy.findByRole('link', { name: /Contact/i }).click(); + cy.url().should('include', '/contact'); + cy.findByRole('heading', { level: 1 }).contains('Contact'); + }); + } +); + +describe('Footer navigation', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('should navigate to the legal notice page', async () => { + cy.findByRole('link', { name: /Mentions légales/i }).click(); + cy.url().should('include', '/mentions-legales'); + cy.findByRole('heading', { level: 1 }).contains('Mentions légales'); + }); +}); diff --git a/tests/cypress/e2e/pages/blog.cy.ts b/tests/cypress/e2e/pages/blog.cy.ts new file mode 100644 index 0000000..dedc0e4 --- /dev/null +++ b/tests/cypress/e2e/pages/blog.cy.ts @@ -0,0 +1,44 @@ +import { settings } from '@utils/config'; + +describe('Blog Page', () => { + beforeEach(() => { + cy.visit('/blog'); + }); + + it('loads the correct number of pages', () => { + cy.findByText(/(\d+) articles chargés sur un total de (\d+)/i) + .then(($div) => { + type ArticlesGroup = { + first: string; + total: string; + }; + + const firstLastNumbers = /(?<first>\d+).*[\D](?<total>\d+)/; + const result = $div.text().match(firstLastNumbers); + expect(result).to.not.be.null; + + const { first, total } = result!.groups as ArticlesGroup; + const firstArticles = parseInt(first, 10); + const totalArticles = parseInt(total, 10); + expect(firstArticles).to.be.within(1, settings.postsPerPage); + expect(totalArticles).to.be.at.least(1); + + const totalPages = Math.ceil(totalArticles / settings.postsPerPage); + const remainingPages = totalPages - 1; + return Array.from({ length: remainingPages }, (_, i) => i + 1); + }) + .then((remainingPages) => { + if (remainingPages.length >= 1) { + cy.wrap(remainingPages).each(() => { + cy.findByRole('button', { + name: /Charger plus d’articles/i, + }).click(); + }); + } + + cy.findByRole('button', { name: /Charger plus d’articles/i }).should( + 'not.exist' + ); + }); + }); +}); diff --git a/tests/cypress/e2e/pages/contact.cy.ts b/tests/cypress/e2e/pages/contact.cy.ts new file mode 100644 index 0000000..6e1cdb6 --- /dev/null +++ b/tests/cypress/e2e/pages/contact.cy.ts @@ -0,0 +1,41 @@ +const userName = 'Cypress Test'; +const userEmail = 'cypress@testing.com'; +const object = '[Cypress] quos aperiam culpa'; +const message = + 'Asperiores ea nihil. Nam ipsam est sunt porro. Ratione in facilis cum. Voluptatem pariatur rerum.'; + +describe('Contact Page', () => { + beforeEach(() => { + cy.visit('/contact'); + }); + + it('shows a heading and a contact form', () => { + cy.findByRole('heading', { level: 1 }).contains(/Contact/i); + cy.findByRole('form', { name: /Formulaire de contact/i }); + }); + + it('submits the form', async () => { + cy.findByRole('textbox', { name: /Nom/i }) + .type(userName) + .should('have.value', userName); + cy.findByRole('textbox', { name: /E-mail/i }) + .type(userEmail) + .should('have.value', userEmail); + cy.findByRole('textbox', { name: /Sujet/i }) + .type(object) + .should('have.value', object); + cy.findByRole('textbox', { name: /Message/i }) + .type(message) + .should('have.value', message); + cy.findByRole('button', { name: /Envoyer/i }).click(); + cy.findByText(/E-mail en cours d'envoi/i).should('be.visible'); + }); + + it('prevents the form to submit if some fields are missing', async () => { + cy.findByRole('textbox', { name: /E-mail/i }) + .type(userEmail) + .should('have.value', userEmail); + cy.findByRole('button', { name: /Envoyer/i }).click(); + cy.findByText(/E-mail en cours d'envoi/i).should('not.be.visible'); + }); +}); diff --git a/tests/cypress/e2e/pages/cv.cy.ts b/tests/cypress/e2e/pages/cv.cy.ts new file mode 100644 index 0000000..419a098 --- /dev/null +++ b/tests/cypress/e2e/pages/cv.cy.ts @@ -0,0 +1,6 @@ +describe('CV Page', () => { + it('successfully loads', () => { + cy.visit('/cv'); + cy.findByRole('heading', { level: 1 }).contains('CV'); + }); +}); diff --git a/tests/cypress/e2e/pages/homepage.cy.ts b/tests/cypress/e2e/pages/homepage.cy.ts new file mode 100644 index 0000000..52bfbc7 --- /dev/null +++ b/tests/cypress/e2e/pages/homepage.cy.ts @@ -0,0 +1,9 @@ +import { settings } from '@utils/config'; + +describe('HomePage', () => { + it('successfully loads', () => { + cy.visit('/'); + cy.findByRole('heading', { level: 1 }).contains(settings.name); + cy.findByText(settings.baseline.fr).should('exist'); + }); +}); diff --git a/tests/cypress/e2e/pages/legal-notice.cy.ts b/tests/cypress/e2e/pages/legal-notice.cy.ts new file mode 100644 index 0000000..f338a7a --- /dev/null +++ b/tests/cypress/e2e/pages/legal-notice.cy.ts @@ -0,0 +1,6 @@ +describe('Legal Notice Page', () => { + it('successfully loads', () => { + cy.visit('/mentions-legales'); + cy.findByRole('heading', { level: 1 }).contains('Mentions légales'); + }); +}); diff --git a/tests/cypress/e2e/pages/projects.cy.ts b/tests/cypress/e2e/pages/projects.cy.ts new file mode 100644 index 0000000..b477400 --- /dev/null +++ b/tests/cypress/e2e/pages/projects.cy.ts @@ -0,0 +1,6 @@ +describe('Projects Page', () => { + it('successfully loads', () => { + cy.visit('/projets'); + cy.findByRole('heading', { level: 1 }).contains('Projets'); + }); +}); diff --git a/tests/cypress/e2e/search.cy.ts b/tests/cypress/e2e/search.cy.ts new file mode 100644 index 0000000..f105a5c --- /dev/null +++ b/tests/cypress/e2e/search.cy.ts @@ -0,0 +1,41 @@ +const queryWithArticles = 'Coldark'; +const queryWithoutArticles = 'etEtRerum'; + +describe('Search', () => { + it('should open and close search form by clicking on search button', () => { + cy.visit('/'); + cy.findByLabelText(/Fermer la recherche/i).should('not.exist'); + cy.findByRole('searchbox', { name: /Rechercher/i }).should('not.exist'); + cy.findByLabelText(/Ouvrir la recherche/i).click(); + cy.findByLabelText(/Ouvrir la recherche/i).should('not.exist'); + cy.findByLabelText(/Fermer la recherche/i).should('exist'); + cy.findByRole('searchbox', { name: /Rechercher/i }).should('exist'); + cy.findByLabelText(/Fermer la recherche/i).click(); + cy.findByLabelText(/Fermer la recherche/i).should('not.exist'); + cy.findByRole('searchbox', { name: /Rechercher/i }).should('not.exist'); + cy.findByLabelText(/Ouvrir la recherche/i).should('exist'); + }); + + it('should navigate the search page', async () => { + cy.visit('/'); + cy.findByLabelText(/Ouvrir la recherche/i).click(); + cy.findByRole('searchbox', { name: /Rechercher/i }).type( + `${queryWithArticles}{enter}` + ); + cy.url().should('include', '/blog'); + cy.findByRole('heading', { level: 1 }).contains( + /Résultats de la recherche pour/i + ); + }); + + it('should display the total of articles if successful', async () => { + cy.visit(`/recherche?s=${encodeURIComponent(queryWithArticles)}`); + const dtSiblings = cy.findByRole('term', { name: /Total/i }).siblings(); + dtSiblings.findByRole('definition').contains(/article/i); + }); + + it('should display a search form if unsuccessful', () => { + cy.visit(`/recherche?s=${encodeURIComponent(queryWithoutArticles)}`); + cy.findByRole('searchbox', { name: /Rechercher/i }).should('exist'); + }); +}); diff --git a/tests/cypress/e2e/settings.cy.ts b/tests/cypress/e2e/settings.cy.ts new file mode 100644 index 0000000..abdbcdf --- /dev/null +++ b/tests/cypress/e2e/settings.cy.ts @@ -0,0 +1,177 @@ +describe('Settings', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('should open and close a settings menu by clicking on a button', () => { + cy.findByLabelText(/Fermer les réglages/i).should('not.exist'); + cy.findByLabelText(/Ouvrir les réglages/i).click(); + cy.findByLabelText(/Ouvrir les réglages/i).should('not.exist'); + cy.findByLabelText(/Fermer les réglages/i).click(); + cy.findByLabelText(/Fermer les réglages/i).should('not.exist'); + cy.findByLabelText(/Ouvrir les réglages/i).should('exist'); + }); + + it('should open and close a tooltip by clicking on a button', () => { + cy.findByLabelText(/Ouvrir les réglages/i).click(); + cy.findByText(/Ackee/).should('not.be.visible'); + cy.findByRole('button', { name: /Aide/i }).click(); + cy.findByText(/Ackee/).should('be.visible'); + cy.findByRole('button', { name: /Aide/i }).click(); + cy.findByText(/Ackee/).should('not.be.visible'); + }); + + it('should change the current theme', () => { + cy.findByLabelText(/Ouvrir les réglages/i).click(); + cy.findByRole('document') + .parent() + .then(($html) => { + const initialTheme = $html.attr('data-theme'); + + if (initialTheme === 'light') { + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème clair/i }) + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème sombre/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème clair/i }) + .should('not.be.checked'); + cy.findByRole('document') + .parent() + .should('have.attr', 'data-theme', 'dark') + .then(() => { + expect(localStorage.getItem('theme')).to.eq('dark'); + }); + } else { + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème sombre/i }) + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème clair/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Thème/i }) + .findByRole('radio', { name: /Thème sombre/i }) + .should('not.be.checked'); + cy.findByRole('document') + .parent() + .should('have.attr', 'data-theme', 'light') + .then(() => { + expect(localStorage.getItem('theme')).to.eq('light'); + }); + } + }); + }); + + it('should change the Prism theme', () => { + cy.findByLabelText(/Ouvrir les réglages/i).click(); + // We assume that the default theme is light theme. + cy.findByRole('radiogroup', { name: /Blocs de code/i }) + .findByRole('radio', { name: /Thème sombre/i }) + .check({ force: true }) // because of label + .should('be.checked') + .then(() => { + expect(localStorage.getItem('prismjs-color-scheme')).to.eq('"dark"'); + }); + cy.findByRole('radiogroup', { name: /Blocs de code/i }) + .findByRole('radio', { name: /Thème clair/i }) + .check({ force: true }) // because of label + .should('be.checked') + .then(() => { + expect(localStorage.getItem('prismjs-color-scheme')).to.eq('"light"'); + }); + }); + + it('should change the motion setting', () => { + cy.findByLabelText(/Ouvrir les réglages/i).click(); + cy.findByRole('document') + .parent() + .then(($html) => { + const initialValue = $html.attr('data-reduced-motion'); + + if (initialValue === 'false') { + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Marche/i }) + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Arrêt/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Marche/i }) + .should('not.be.checked'); + cy.findByRole('document') + .parent() + .should('have.attr', 'data-reduced-motion', 'true') + .then(() => { + expect(localStorage.getItem('reduced-motion')).to.eq('true'); + }); + } else { + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Arrêt/i }) + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Marche/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radiogroup', { name: /Animations/i }) + .findByRole('radio', { name: /Arrêt/i }) + .should('not.be.checked'); + cy.findByRole('document') + .parent() + .should('have.attr', 'data-reduced-motion', 'true') + .then(() => { + expect(localStorage.getItem('reduced-motion')).to.eq('true'); + }); + } + }); + }); + + it('should change the Ackee setting', () => { + cy.findByLabelText(/Ouvrir les réglages/i) + .click() + .then(() => { + const storedValue = localStorage.getItem('ackee-tracking'); + const parsedStoredValue = storedValue ? JSON.parse(storedValue) : ''; + + if (parsedStoredValue === 'full') { + cy.findByRole('radio', { name: /Complet/i }).should('be.checked'); + cy.findByRole('radio', { name: /Partiel/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radio', { name: /Complet/i }) + .should('not.be.checked') + .then(() => { + const newStoredValue = localStorage.getItem('ackee-tracking'); + const parsedNewStoredValue = newStoredValue + ? JSON.parse(newStoredValue) + : ''; + expect(parsedNewStoredValue).to.eq('partial'); + }); + } else { + cy.findByRole('radio', { name: /Partiel/i }).should('be.checked'); + cy.findByRole('radio', { name: /Complet/i }) + .should('not.be.checked') + .check({ force: true }) // because of label + .should('be.checked'); + cy.findByRole('radio', { name: /Partiel/i }) + .should('not.be.checked') + .then(() => { + const newStoredValue = localStorage.getItem('ackee-tracking'); + const parsedNewStoredValue = newStoredValue + ? JSON.parse(newStoredValue) + : ''; + expect(parsedNewStoredValue).to.eq('full'); + }); + } + }); + }); +}); diff --git a/tests/cypress/fixtures/example.json b/tests/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/tests/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/tests/cypress/plugins/index.ts b/tests/cypress/plugins/index.ts new file mode 100644 index 0000000..8229063 --- /dev/null +++ b/tests/cypress/plugins/index.ts @@ -0,0 +1,22 @@ +/// <reference types="cypress" /> +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/tests/cypress/support/commands.ts b/tests/cypress/support/commands.ts new file mode 100644 index 0000000..a88e131 --- /dev/null +++ b/tests/cypress/support/commands.ts @@ -0,0 +1,6 @@ +// *********************************************** +// For examples of custom commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +import '@testing-library/cypress/add-commands'; diff --git a/tests/cypress/support/e2e.ts b/tests/cypress/support/e2e.ts new file mode 100644 index 0000000..37a498f --- /dev/null +++ b/tests/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js 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 commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json new file mode 100644 index 0000000..66b725c --- /dev/null +++ b/tests/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "isolatedModules": false, + "noEmit": true, + "types": ["cypress", "@testing-library/cypress"] + }, + "include": ["../../node_modules/cypress", "**/*.ts", "**/*.tsx"], + "exclude": ["**/*.test.ts", "**/*.test.tsx"] +} diff --git a/tests/jest/__mocks__/matchMedia.mock.js b/tests/jest/__mocks__/matchMedia.mock.js new file mode 100644 index 0000000..a983ad3 --- /dev/null +++ b/tests/jest/__mocks__/matchMedia.mock.js @@ -0,0 +1,15 @@ +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +export {}; diff --git a/tests/utils/index.tsx b/tests/utils/index.tsx new file mode 100644 index 0000000..1bcea8e --- /dev/null +++ b/tests/utils/index.tsx @@ -0,0 +1,43 @@ +import { render, RenderOptions } from '@testing-library/react'; +import { ThemeProvider } from 'next-themes'; +import { FC, ReactElement, ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; + +type ProvidersConfig = { + children: ReactNode; + locale?: 'en' | 'fr'; +}; + +type CustomRenderOptions = { + providers?: ProvidersConfig; + testingLibrary?: Omit<RenderOptions, 'wrapper'>; +}; + +/** + * Return a component wrapped with Intl and Theme Provider. + * + * @returns A component wrapped Intl and Theme providers. + */ +const AllTheProviders: FC<ProvidersConfig> = ({ children, locale = 'en' }) => { + return ( + <IntlProvider locale={locale}> + <ThemeProvider>{children}</ThemeProvider> + </IntlProvider> + ); +}; + +/** + * Render a component with all the providers. + * + * @param {ReactElement} ui - A React component. + * @param {CustomRenderOptions} [options] - An object of render options and providers options. + * @returns A React component wrapped with all the providers. + */ +const customRender = (ui: ReactElement, options?: CustomRenderOptions) => + render(ui, { + wrapper: (props) => <AllTheProviders {...props} {...options?.providers} />, + ...options?.testingLibrary, + }); + +export * from '@testing-library/react'; +export { customRender as render }; |
