aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-06-03 11:58:21 +0200
committerGitHub <noreply@github.com>2022-06-03 11:58:21 +0200
commit6d0a9504406524957b351aee748d9f5c8a84c299 (patch)
tree1b859015ef5e52b2c7c4e7521f428fb2215df21d /tests
parenta8af53c118478e6ed68975c32cc1202b7c7b798e (diff)
parentfc7a6e98268d34f313d79c817e38c09ad6cde960 (diff)
test: add end to end tests (#19)
In addition to Jest tests, I configure Cypress to test some pages and features. I also fix some Jest errors due to images import.
Diffstat (limited to 'tests')
-rw-r--r--tests/cypress/e2e/back-to-top.cy.ts14
-rw-r--r--tests/cypress/e2e/nav.cy.ts74
-rw-r--r--tests/cypress/e2e/pages/blog.cy.ts44
-rw-r--r--tests/cypress/e2e/pages/contact.cy.ts41
-rw-r--r--tests/cypress/e2e/pages/cv.cy.ts6
-rw-r--r--tests/cypress/e2e/pages/homepage.cy.ts9
-rw-r--r--tests/cypress/e2e/pages/legal-notice.cy.ts6
-rw-r--r--tests/cypress/e2e/pages/projects.cy.ts6
-rw-r--r--tests/cypress/e2e/search.cy.ts41
-rw-r--r--tests/cypress/e2e/settings.cy.ts177
-rw-r--r--tests/cypress/fixtures/example.json5
-rw-r--r--tests/cypress/plugins/index.ts22
-rw-r--r--tests/cypress/support/commands.ts6
-rw-r--r--tests/cypress/support/e2e.ts20
-rw-r--r--tests/cypress/tsconfig.json10
-rw-r--r--tests/jest/__mocks__/matchMedia.mock.js15
-rw-r--r--tests/utils/index.tsx43
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 };