From aecfaf807a407c8b7bb4ae084b627df4b328831d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Jan 2025 18:23:30 +0100 Subject: [PATCH] fix: add tests and structure --- e2e/App.tsx | 84 +++++++++++++++++++- e2e/README.md | 43 ++++++++++ e2e/mocks/mockErrors.ts | 11 +++ e2e/package.json | 12 ++- e2e/playwright-report/index.html | 71 +++++++++++++++++ e2e/playwright.config.ts | 2 +- e2e/test-results/.last-run.json | 4 + e2e/tests/componentsWithinModal/render.tsx | 58 ++++++++++++++ e2e/tests/componentsWithinModal/test.spec.ts | 40 ++++++++++ e2e/tests/default.spec.ts | 9 --- e2e/tests/ui/checkbox.spec.ts | 22 ----- e2e/tests/ui/list.spec.ts | 26 ------ e2e/tsconfig.json | 18 ++++- e2e/vite.config.ts | 11 +++ pnpm-lock.yaml | 66 +++++++++++++++ 15 files changed, 411 insertions(+), 66 deletions(-) create mode 100644 e2e/README.md create mode 100644 e2e/mocks/mockErrors.ts create mode 100644 e2e/playwright-report/index.html create mode 100644 e2e/test-results/.last-run.json create mode 100644 e2e/tests/componentsWithinModal/render.tsx create mode 100644 e2e/tests/componentsWithinModal/test.spec.ts delete mode 100644 e2e/tests/default.spec.ts delete mode 100644 e2e/tests/ui/checkbox.spec.ts delete mode 100644 e2e/tests/ui/list.spec.ts 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: + + + +) + +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}
+ +
{}} methods={methods}> + +
{methods.watch().lastName}
+ +
+ +
+ +
+ ) +} +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