diff --git a/e2e/App.tsx b/e2e/App.tsx
index f7e62838cf..45f574e6d8 100644
--- a/e2e/App.tsx
+++ b/e2e/App.tsx
@@ -1,3 +1,85 @@
-const App = () => <>Welcome !>
+import { ThemeProvider } from '@emotion/react'
+import '@ultraviolet/fonts/fonts.css'
+import { consoleLightTheme } from '@ultraviolet/themes'
+import { Text } from '@ultraviolet/ui'
+import { lazy } from 'react'
+import {
+ Link as ReactRouterLink,
+ Route,
+ BrowserRouter as Router,
+ Routes,
+} from 'react-router-dom'
+
+/**
+ * We get all the render.tsx in tests folder and generate individual pages / routes to render the content.
+ */
+const modules = import.meta.glob('./tests/**/*.tsx')
+const pagesToRender = Object.keys(modules)
+ .map(path =>
+ path.includes('render.tsx')
+ ? {
+ name: path.replace('.tsx', ''),
+ Component: lazy(
+ () => import(`./tests/${path?.split('/')[2]}/render.tsx`),
+ ),
+ }
+ : null,
+ )
+ .filter(Boolean)
+/**
+ * ----------------------------------------
+ */
+
+const WelcomePage = () => (
+ <>
+
+ Welcome to the Ultraviolet E2E testing suite!
+
+
+ This page is a placeholder for the available pages to test. Please read
+ the README.md file for more information.
+
+
+ Available pages to test:
+
+
+ {pagesToRender.map(path => (
+ -
+
+ {path?.name?.split('/')[2]?.toLowerCase()}
+
+
+ ))}
+
+ >
+)
+
+const App = () => (
+
+
+
+ } />
+ } />
+ {pagesToRender.map(path => {
+ const Element = path?.Component
+
+ if (Element) {
+ return (
+ }
+ />
+ )
+ }
+
+ return null
+ })}
+
+
+
+)
export default App
diff --git a/e2e/README.md b/e2e/README.md
new file mode 100644
index 0000000000..9130fe1a76
--- /dev/null
+++ b/e2e/README.md
@@ -0,0 +1,43 @@
+# E2E Testing
+
+This project uses playwright to run end-to-end tests. As we are testing a library we need to setup a test environment
+for rendering the component and the test file for each test case.
+
+> [!WARNING]
+> Not all components needs a e2e test. We only want to test specific interaction that are not covered by the unit tests.
+> E2E tests can be used for testing a mix of component and see their interaction. Example: select input within a modal and vice versa.
+
+## Structure
+```
+e2e
+├── tests
+│ ├── useCase1
+│ │ ├── render.tsx # This file will render the component. File name is important!
+│ │ └── test.spec.ts # This file will contain the test cases
+│ ├── useCase2
+│ │ ├── render.tsx
+│ │ └── test.spec.ts
+└── playwright.config.ts
+```
+
+What you need to understand is that for each test case you need to create a file `render.tsx` that will render the component.
+And a file `test.spec.ts` that will contain the test cases run by playwright.
+
+## Running the tests
+
+To run the tests you can use the following command:
+
+```bash
+pnpm start # This will start the server with all the components
+
+# In another terminal
+pnpm test:e2e # This will run the tests
+```
+
+> [!NOTE]
+> Test are run on 3 different browsers: `chromium`, `firefox` and `webkit`.
+
+## Writing the tests
+
+To write the tests you can copy and paste the `template` folder in `tests/template`. You can then rename the folder and change the content
+of the files to match the test case you want to test.
diff --git a/e2e/mocks/mockErrors.ts b/e2e/mocks/mockErrors.ts
new file mode 100644
index 0000000000..21f4494c4c
--- /dev/null
+++ b/e2e/mocks/mockErrors.ts
@@ -0,0 +1,11 @@
+export const mockErrors = {
+ maxDate: () => `Date must be lower than ...`,
+ maxLength: () => `This field should have a length lower than ...`,
+ minDate: () => `Date must be greater than ...`,
+ minLength: () => `This field should have a length greater than ...`,
+ pattern: () => `This field should match the regex`,
+ required: () => 'This field is required',
+ max: () => `This field is too high (maximum is : ...)`,
+ min: () => `This field is too low (minimum is: ...)`,
+ isInteger: () => `Incorrect field`,
+}
diff --git a/e2e/package.json b/e2e/package.json
index b7d195b47d..a643821f54 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -5,6 +5,8 @@
"type": "module",
"scripts": {
"start": "vite",
+ "e2e": "playwright test",
+ "e2e:debug": "playwright test --ui",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
@@ -12,14 +14,19 @@
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
+ "@ultraviolet/fonts": "workspace:*",
+ "@ultraviolet/form": "workspace:*",
"@ultraviolet/icons": "workspace:*",
"@ultraviolet/themes": "workspace:*",
"@ultraviolet/ui": "workspace:*",
"react": "19.0.0",
- "react-dom": "19.0.0"
+ "react-dom": "19.0.0",
+ "react-router-dom": "7.1.3"
},
"devDependencies": {
+ "@emotion/babel-plugin": "11.13.5",
"@eslint/js": "9.17.0",
+ "@playwright/test": "1.49.1",
"@types/react": "19.0.5",
"@types/react-dom": "19.0.3",
"@vitejs/plugin-react": "4.3.4",
@@ -29,7 +36,6 @@
"globals": "15.14.0",
"typescript": "5.7.3",
"typescript-eslint": "^8.19.1",
- "vite": "6.0.7",
- "@emotion/babel-plugin": "11.13.5"
+ "vite": "6.0.7"
}
}
diff --git a/e2e/playwright-report/index.html b/e2e/playwright-report/index.html
new file mode 100644
index 0000000000..1e6dfcdb20
--- /dev/null
+++ b/e2e/playwright-report/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index ce481616fc..91ba2f63e9 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -4,7 +4,7 @@ import { defineConfig, devices } from '@playwright/test'
const isCI = process.env['CI']
-const baseURL = process.env['BASE_URL'] ?? 'http://localhost:6006'
+const baseURL = process.env['BASE_URL'] ?? 'http://localhost:5173'
const times = {
'1min': 60 * 1000,
diff --git a/e2e/test-results/.last-run.json b/e2e/test-results/.last-run.json
new file mode 100644
index 0000000000..f740f7c700
--- /dev/null
+++ b/e2e/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
diff --git a/e2e/tests/componentsWithinModal/render.tsx b/e2e/tests/componentsWithinModal/render.tsx
new file mode 100644
index 0000000000..b72743163a
--- /dev/null
+++ b/e2e/tests/componentsWithinModal/render.tsx
@@ -0,0 +1,58 @@
+import {
+ Form,
+ SelectInputFieldV2,
+ TextInputFieldV2,
+ useForm,
+} from '@ultraviolet/form'
+import { Button, Modal, TextInputV2 } from '@ultraviolet/ui'
+import { useState } from 'react'
+import { mockErrors } from '../../mocks/mockErrors'
+
+const Render = () => {
+ const methods = useForm<{ lastName: ''; color: '' }>()
+ const [firstName, setFirstName] = useState()
+
+ return (
+ Open Modal}>
+
+ {firstName}
+
+
+
+ )
+}
+export default Render
diff --git a/e2e/tests/componentsWithinModal/test.spec.ts b/e2e/tests/componentsWithinModal/test.spec.ts
new file mode 100644
index 0000000000..fb091b2906
--- /dev/null
+++ b/e2e/tests/componentsWithinModal/test.spec.ts
@@ -0,0 +1,40 @@
+import { expect, test } from '@playwright/test'
+
+test('open modal, fill text inputs, close modal', async ({ page, baseURL }) => {
+ await page.goto(`${baseURL}/componentsWithinModal`)
+ await page.getByRole('button', { name: 'Open Modal' }).click()
+ await page.getByLabel('First name').click()
+ await page.getByLabel('First name').fill('Test First Name')
+
+ await expect(page.locator('div[data-testid="input-value"]')).toHaveText(
+ 'Test First Name',
+ )
+
+ await page.getByLabel('Last name').click()
+ await page.getByLabel('Last name').fill('Test Last Name')
+
+ await expect(page.locator('div[data-testid="form-content"]')).toHaveText(
+ 'Test Last Name',
+ )
+})
+
+test('open modal, click and fill on select input, close modal', async ({
+ page,
+ baseURL,
+}) => {
+ await page.goto(`${baseURL}/componentsWithinModal`)
+ await page.getByRole('button', { name: 'Open Modal' }).click()
+ await page.getByLabel('First name').click()
+ await page.getByLabel('First name').fill('Test First Name')
+
+ await expect(page.locator('div[data-testid="input-value"]')).toHaveText(
+ 'Test First Name',
+ )
+
+ await page.getByLabel('Last name').click()
+ await page.getByLabel('Last name').fill('Test Last Name')
+
+ await expect(page.locator('div[data-testid="form-content"]')).toHaveText(
+ 'Test Last Name',
+ )
+})
diff --git a/e2e/tests/default.spec.ts b/e2e/tests/default.spec.ts
deleted file mode 100644
index 8526507500..0000000000
--- a/e2e/tests/default.spec.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { expect, test } from '@playwright/test'
-
-test.describe('Default', () => {
- test('title', async ({ page, baseURL }) => {
- await page.goto(`${baseURL}`)
-
- await expect(page).toHaveTitle('Get started - Docs ⋅ Storybook')
- })
-})
diff --git a/e2e/tests/ui/checkbox.spec.ts b/e2e/tests/ui/checkbox.spec.ts
deleted file mode 100644
index c4ccaeeefe..0000000000
--- a/e2e/tests/ui/checkbox.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect, test } from '@playwright/test'
-
-const defaultLocator = 'iframe[title="storybook-preview-iframe"]'
-
-const defaultURL = `/?path=/story/components-data-entry-checkbox--playground`
-
-test.describe('List', () => {
- test('Checkbox Row', async ({ page, baseURL }) => {
- const url = `${baseURL}${defaultURL}`
-
- await page.goto(url)
-
- const rootLocator = page.locator(defaultLocator).contentFrame()
- await rootLocator.getByRole('checkbox').click()
-
- const checkbox = rootLocator.locator(`input[type='checkbox']`)
-
- await expect(checkbox).toBeChecked({
- checked: true,
- })
- })
-})
diff --git a/e2e/tests/ui/list.spec.ts b/e2e/tests/ui/list.spec.ts
deleted file mode 100644
index ec8e6d23b7..0000000000
--- a/e2e/tests/ui/list.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { expect, test } from '@playwright/test'
-
-const defaultLocator = 'iframe[title="storybook-preview-iframe"]'
-const defaultURL = `/?path=/story/components-data-display-list--selectable`
-
-test.describe('List', () => {
- test('Checkbox Row', async ({ page, baseURL }) => {
- const url = `${baseURL}${defaultURL}`
-
- await page.goto(url)
-
- const rootLocator = page.locator(defaultLocator).contentFrame()
- await rootLocator
- .getByRole('row', { name: 'select Venus 0.718AU 0.728AU' })
- .getByLabel('select')
- .check()
-
- const checkbox = rootLocator.locator(
- `input[type='checkbox'][name='list-select-checkbox'][value="venus"]`,
- )
-
- await expect(checkbox).toBeChecked({
- checked: true,
- })
- })
-})
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
index 933f1fee32..33e8472d87 100644
--- a/e2e/tsconfig.json
+++ b/e2e/tsconfig.json
@@ -1,9 +1,19 @@
{
- "extends": "../tsconfig.json",
+ "$schema": "http://json.schemastore.org/tsconfig",
+ "extends": "@scaleway/tsconfig",
"compilerOptions": {
- "baseUrl": ".",
- "allowImportingTsExtensions": true
+ "target": "esnext",
+ "module": "esnext",
+ "jsx": "preserve",
+ "jsxImportSource": "@emotion/react",
+ "allowImportingTsExtensions": true,
+ "forceConsistentCasingInFileNames": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "types": ["vite/client"]
},
- "include": ["src", "../../global.d.ts", "emotion.d.ts"],
+ "include": ["global.d.ts", "emotion.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "coverage", "dist"]
}
diff --git a/e2e/vite.config.ts b/e2e/vite.config.ts
index cd7a8375bc..238885fd4c 100644
--- a/e2e/vite.config.ts
+++ b/e2e/vite.config.ts
@@ -10,4 +10,15 @@ export default defineConfig({
},
}),
],
+ build: {
+ rollupOptions: {
+ external: ['fsevents'],
+ },
+ },
+ server: {
+ // Sends all requests to index.html if file not found
+ fs: {
+ allow: ['.'], // or paths as needed
+ },
+ },
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b41713fbb..c1a6073357 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -330,6 +330,12 @@ importers:
'@emotion/styled':
specifier: 11.14.0
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.0.5)(react@19.0.0))(@types/react@19.0.5)(react@19.0.0)
+ '@ultraviolet/fonts':
+ specifier: workspace:*
+ version: link:../packages/fonts
+ '@ultraviolet/form':
+ specifier: workspace:*
+ version: link:../packages/form
'@ultraviolet/icons':
specifier: workspace:*
version: link:../packages/icons
@@ -345,6 +351,9 @@ importers:
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
+ react-router-dom:
+ specifier: 7.1.3
+ version: 7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
devDependencies:
'@emotion/babel-plugin':
specifier: 11.13.5
@@ -352,6 +361,9 @@ importers:
'@eslint/js':
specifier: 9.17.0
version: 9.17.0
+ '@playwright/test':
+ specifier: 1.49.1
+ version: 1.49.1
'@types/react':
specifier: 19.0.5
version: 19.0.5
@@ -3179,6 +3191,9 @@ packages:
'@types/conventional-commits-parser@5.0.0':
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
@@ -4210,6 +4225,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
@@ -7035,6 +7054,23 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-router-dom@7.1.3:
+ resolution: {integrity: sha512-qQGTE+77hleBzv9SIUIkGRvuFBQGagW+TQKy53UTZAO/3+YFNBYvRsNIZ1GT17yHbc63FylMOdS+m3oUriF1GA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.1.3:
+ resolution: {integrity: sha512-EezYymLY6Guk/zLQ2vRA8WvdUhWFEj5fcE3RfWihhxXBW7+cd1LsIiA3lmx+KCmneAGQuyBv820o44L2+TtkSA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-schemaorg@2.0.0:
resolution: {integrity: sha512-UqciFKA203ewNjn0zC09uYKuJSvMD8L75L1s/cW4rc7cS64w8l7vaI3SYkuoI/nwCBkJRmOkSJedWDUZBlYZwg==}
engines: {node: '>=12.0.0'}
@@ -7356,6 +7392,9 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -7887,6 +7926,9 @@ packages:
cpu: [arm64]
os: [linux]
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
turbo-windows-64@2.3.3:
resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==}
cpu: [x64]
@@ -11446,6 +11488,8 @@ snapshots:
dependencies:
'@types/node': 22.10.7
+ '@types/cookie@0.6.0': {}
+
'@types/d3-color@3.1.3': {}
'@types/d3-delaunay@6.0.4': {}
@@ -12750,6 +12794,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
@@ -16065,6 +16111,22 @@ snapshots:
react-refresh@0.14.2: {}
+ react-router-dom@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-router: 7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+
+ react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 19.0.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.0.0(react@19.0.0)
+
react-schemaorg@2.0.0(react@19.0.0)(schema-dts@1.1.2(typescript@5.7.3))(typescript@5.7.3):
dependencies:
react: 19.0.0
@@ -16490,6 +16552,8 @@ snapshots:
dependencies:
randombytes: 2.1.0
+ set-cookie-parser@2.7.1: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -17071,6 +17135,8 @@ snapshots:
turbo-linux-arm64@2.3.3:
optional: true
+ turbo-stream@2.4.0: {}
+
turbo-windows-64@2.3.3:
optional: true