-
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[test] Port e2e infra back to Base UI
- Loading branch information
1 parent
f5f2f88
commit 88c2ff0
Showing
15 changed files
with
604 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = { | ||
extension: ['js', 'ts', 'tsx'], | ||
recursive: true, | ||
slow: 500, | ||
timeout: (process.env.CIRCLECI === 'true' ? 4 : 2) * 1000, // Circle CI has low-performance CPUs. | ||
reporter: 'dot', | ||
require: ['@mui/internal-test-utils/setupBabelPlaywright'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# end-to-end testing | ||
|
||
End-to-end tests (short <abbr title="end-to-end">e2e</abbr>) are split into two parts: | ||
|
||
1. The rendered UI (short: fixture) | ||
2. Instrumentation of that UI | ||
|
||
## Rendered UI | ||
|
||
The composition of all tests happens in `./index.js`. | ||
The rendered UI is located inside a separate file in `./fixtures` and written as a React component. | ||
If you're adding a new test prefer a new component instead of editing existing files since that might unknowingly alter existing tests. | ||
|
||
## Instrumentation | ||
|
||
We're using [`playwright`](https://playwright.dev) to replay user actions. | ||
Each test tests only a single fixture. | ||
A fixture can be loaded with `await renderFixture(fixturePath)`, for example `renderFixture('FocusTrap/OpenFocusTrap')`. | ||
|
||
## Commands | ||
|
||
For development `pnpm test:e2e:dev` and `pnpm test:e2e:run --watch` in separate terminals is recommended. | ||
|
||
| command | description | | ||
| :--------------------- | :-------------------------------------------------------------------------------------------- | | ||
| `pnpm test:e2e` | Full run | | ||
| `pnpm test:e2e:dev` | Prepares the fixtures to be able to test in watchmode | | ||
| `pnpm test:e2e:run` | Runs the tests (requires `pnpm test:e2e:dev` or `pnpm test:e2e:build`+`pnpm test:e2e:server`) | | ||
| `pnpm test:e2e:build` | Builds the Webpack bundle for viewing the fixtures | | ||
| `pnpm test:e2e:server` | Serves the fixture bundle. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import * as React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
function TestViewer(props) { | ||
const { children } = props; | ||
|
||
// We're simulating `act(() => ReactDOM.render(children))` | ||
// In the end children passive effects should've been flushed. | ||
// React doesn't have any such guarantee outside of `act()` so we're approximating it. | ||
const [ready, setReady] = React.useState(false); | ||
React.useEffect(() => { | ||
setReady(true); | ||
}, []); | ||
|
||
return ( | ||
<React.Suspense fallback={<div aria-busy />}> | ||
<div aria-busy={!ready} data-testid="testcase"> | ||
{children} | ||
</div> | ||
</React.Suspense> | ||
); | ||
} | ||
|
||
TestViewer.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
}; | ||
|
||
export default TestViewer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import * as React from 'react'; | ||
import { FocusTrap } from '@base_ui/react/FocusTrap'; | ||
|
||
export default function ClosedFocusTrap() { | ||
return ( | ||
<React.Fragment> | ||
<button type="button" autoFocus> | ||
initial focus | ||
</button> | ||
<FocusTrap open={false}> | ||
<div data-testid="root"> | ||
<button type="button">inside focusable</button> | ||
</div> | ||
</FocusTrap> | ||
<button type="button">final tab target</button> | ||
</React.Fragment> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import * as React from 'react'; | ||
import { FocusTrap } from '@base_ui/react/FocusTrap'; | ||
|
||
export default function BaseFocusTrap() { | ||
const [open, close] = React.useReducer(() => false, true); | ||
|
||
return ( | ||
<React.Fragment> | ||
<button type="button" autoFocus data-testid="initial-focus"> | ||
initial focus | ||
</button> | ||
<FocusTrap isEnabled={() => true} open={open} disableAutoFocus> | ||
<div data-testid="root"> | ||
<div>Title</div> | ||
<button type="button" onClick={close}> | ||
close | ||
</button> | ||
<button type="button">noop</button> | ||
</div> | ||
</FocusTrap> | ||
</React.Fragment> | ||
); | ||
} |
19 changes: 19 additions & 0 deletions
19
test/e2e/fixtures/FocusTrap/DisableEnforceFocusFocusTrap.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import * as React from 'react'; | ||
import { FocusTrap } from '@base_ui/react/FocusTrap'; | ||
|
||
export default function disableEnforceFocusFocusTrap() { | ||
return ( | ||
<React.Fragment> | ||
<button data-testid="initial-focus" type="button" autoFocus> | ||
initial focus | ||
</button> | ||
<FocusTrap open disableEnforceFocus disableAutoFocus> | ||
<div data-testid="root"> | ||
<button data-testid="inside-trap-focus" type="button"> | ||
inside focusable | ||
</button> | ||
</div> | ||
</FocusTrap> | ||
</React.Fragment> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import * as React from 'react'; | ||
import { FocusTrap } from '@base_ui/react/FocusTrap'; | ||
|
||
export default function BaseFocusTrap() { | ||
return ( | ||
<React.Fragment> | ||
<button type="button" autoFocus data-testid="initial-focus"> | ||
initial focus | ||
</button> | ||
<FocusTrap isEnabled={() => true} open> | ||
<div tabIndex={-1} data-testid="root"> | ||
<div>Title</div> | ||
<button type="button">x</button> | ||
<button type="button">cancel</button> | ||
<button type="button">ok</button> | ||
</div> | ||
</FocusTrap> | ||
</React.Fragment> | ||
); | ||
} |
29 changes: 29 additions & 0 deletions
29
test/e2e/fixtures/TextareaAutosize/TextareaAutosizeSuspense.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as React from 'react'; | ||
import { TextareaAutosize } from '@base_ui/react/TextareaAutosize'; | ||
|
||
function LazyRoute() { | ||
const [isDone, setIsDone] = React.useState(false); | ||
|
||
if (!isDone) { | ||
// Force React to show fallback suspense | ||
throw new Promise((resolve) => { | ||
setTimeout(resolve, 1); | ||
setIsDone(true); | ||
}); | ||
} | ||
|
||
return <div />; | ||
} | ||
|
||
export default function TextareaAutosizeSuspense() { | ||
const [showRoute, setShowRoute] = React.useState(false); | ||
|
||
return ( | ||
<React.Fragment> | ||
<button type="button" onClick={() => setShowRoute((r) => !r)}>Toggle view</button> | ||
<React.Suspense fallback={null}> | ||
{showRoute ? <LazyRoute /> : <TextareaAutosize />} | ||
</React.Suspense> | ||
</React.Fragment> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import * as React from 'react'; | ||
import * as ReactDOM from 'react-dom'; | ||
import * as ReactDOMClient from 'react-dom/client'; | ||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; | ||
import * as DomTestingLibrary from '@testing-library/dom'; | ||
import TestViewer from './TestViewer'; | ||
|
||
const fixtures = []; | ||
|
||
const importFixtures = require.context('./fixtures', true, /\.(js|ts|tsx)$/, 'lazy'); | ||
importFixtures.keys().forEach((path) => { | ||
// require.context contains paths for module alias imports and relative imports | ||
if (!path.startsWith('.')) { | ||
return; | ||
} | ||
const [suite, name] = path | ||
.replace('./', '') | ||
.replace(/\.\w+$/, '') | ||
.split('/'); | ||
fixtures.push({ | ||
path, | ||
suite: `e2e/${suite}`, | ||
name, | ||
Component: React.lazy(() => importFixtures(path)), | ||
}); | ||
}); | ||
|
||
function App() { | ||
function computeIsDev() { | ||
if (window.location.hash === '#dev') { | ||
return true; | ||
} | ||
if (window.location.hash === '#no-dev') { | ||
return false; | ||
} | ||
return process.env.NODE_ENV === 'development'; | ||
} | ||
const [isDev, setDev] = React.useState(computeIsDev); | ||
React.useEffect(() => { | ||
function handleHashChange() { | ||
setDev(computeIsDev()); | ||
} | ||
window.addEventListener('hashchange', handleHashChange); | ||
|
||
return () => { | ||
window.removeEventListener('hashchange', handleHashChange); | ||
}; | ||
}, []); | ||
|
||
function computePath(fixture) { | ||
return `/${fixture.suite}/${fixture.name}`; | ||
} | ||
|
||
return ( | ||
<Router> | ||
<Routes> | ||
{fixtures.map((fixture) => { | ||
const path = computePath(fixture); | ||
const FixtureComponent = fixture.Component; | ||
if (FixtureComponent === undefined) { | ||
console.warn('Missing `Component` ', fixture); | ||
return null; | ||
} | ||
|
||
return ( | ||
<Route | ||
key={path} | ||
exact | ||
path={path} | ||
element={ | ||
<TestViewer> | ||
<FixtureComponent /> | ||
</TestViewer> | ||
} | ||
/> | ||
); | ||
})} | ||
</Routes> | ||
<div hidden={!isDev}> | ||
<p> | ||
Devtools can be enabled by appending <code>#dev</code> in the addressbar or disabled by | ||
appending <code>#no-dev</code>. | ||
</p> | ||
<a href="#no-dev">Hide devtools</a> | ||
<details> | ||
<summary id="my-test-summary">nav for all tests</summary> | ||
<nav id="tests"> | ||
<ol> | ||
{fixtures.map((test) => { | ||
const path = computePath(test); | ||
return ( | ||
<li key={path}> | ||
<Link to={path}>{path}</Link> | ||
</li> | ||
); | ||
})} | ||
</ol> | ||
</nav> | ||
</details> | ||
</div> | ||
</Router> | ||
); | ||
} | ||
|
||
const container = document.getElementById('react-root'); | ||
const children = <App />; | ||
if (typeof ReactDOM.unstable_createRoot === 'function') { | ||
const root = ReactDOM.unstable_createRoot(container); | ||
root.render(children); | ||
} else { | ||
const root = ReactDOMClient.createRoot(container); | ||
root.render(children); | ||
} | ||
|
||
window.DomTestingLibrary = DomTestingLibrary; | ||
window.elementToString = function elementToString(element) { | ||
if ( | ||
element != null && | ||
(element.nodeType === element.ELEMENT_NODE || element.nodeType === element.DOCUMENT_NODE) | ||
) { | ||
return window.DomTestingLibrary.prettyDOM(element, undefined, { | ||
highlight: true, | ||
maxDepth: 1, | ||
}); | ||
} | ||
return String(element); | ||
}; |
Oops, something went wrong.