Develop, maintain, and publish your React Components.
@s-ui/studio helps you develop and document isolated UI components for your projects. It provides,
- Isolated development of components
- A unified development platform
- Productivity improvement, focusing on component-development experience
- Live demos with auto-generated playgrounds
- Catalog of components generated from live demos and markdown docs.
npm install @s-ui/studio -D
In case you want to create a new studio, check out sui-studio-create
Once you're in a new project, execute sui-studio start
to start the development browser and work on your components.
$ npx sui-studio generate house window
Use -C
or --context
parameter to generate the component with a default context
$ npx sui-studio generate house window -C
Use -C path
or --context path
parameter to generate the component with a given default context
$ npx sui-studio generate house window -C ./myCustomContext.js
### Use new compiler
We're migrating to swc
so you could generate the new component with the expected prepare
field by using the flag --swc
or -W
.
$ npx sui-studio dev house/window
First of all, stage you changes for commit with git add
or whatever you use.
DO NOT use git commit
directly. Instead, use:
$ npm run co
Add the script to your package.json
{
"scripts": {
"co": "sui-mono commit"
}
}
It will prompt a question form. The way you answer to this question form affects the way the commit's comment is built. Comments will be used later, after merging to master in order to decide what kind of change (release) is going to be applied (minor or major).
Then just push your changes using git push
and merge them into master after review.
Select master branch. First, check that the release will be properly built by executing,
$ sui-mono check-release
If the output is the expected one, then run:
$ sui-mono release
Launch a development environment where you can see all your components at once. If there are too many components, use the dev
command.
Build a static version of a web app aimed to be deployed, where you will be able to interact with all the components. The interface will be the same you use for the start command, only this one is optimized for production.
-O, --only-changes
: only build changed components or demos-B, --before-build <command>
: command to be executed before the build (not working with multiple commands likenpx sui-mono phoenix && npx sui-lint js
, try to group them into just one:npm run before_build
, for example)
# run the build for all components
npx sui-studio build
# run the build just for changed components on a PR and also execute whatever you want before the build (phoenix, lint...)
GITHUB_PULL_REQUEST=123 npx sui-studio build --only-changes --before-build="npx sui-mono phoenix"
Launch a development environment where you can work in total isolation on your component.
Launch all project tests in a Karma browser.
This command allows you to copy files from a source to a destination using glob patterns. It's useful to copy files from the source to the build folder.
# copy all files with scss extension from src to lib
$ cpx './src/**/*.scss' ./lib
Test the studio's components both in the demo as in the development environment. Currently in experimental mode
Here's an example of what could go inside test/[category]/[component]/index.js
:
/* eslint react/jsx-no-undef:0 */
/* global AtomButton */
import chai, {expect} from 'chai'
import chaiDOM from 'chai-dom'
import {render} from '@testing-library/react'
chai.use(chaiDOM)
describe('AtomButton', () => {
it('Render', () => {
const {getByRole} = render(<AtomButton>HOLA</AtomButton>)
expect(getByRole('button')).to.have.text('HOLA')
})
})
The component will be a global object when running tests, so it is PARAMOUNT NOT to import it. In order to avoid problems with the linter, add relevant comments, as in the example above.
If there is a demo/context.js
file where you define several SUI contexts for your components. You have to apply a patch to Mocha to allow setup describe by context. This allows you to have a "contextify" version of your component, for the context selected.
First, you have to import the patcher to create the context
object, inside the describe
object
import '@s-ui/studio/src/patcher-mocha'
After that, you can use the describe.context
object to have a key for every context definition in your demo/context.js
file.
For example, if your context.js
file looks like:
export default () => {
return Promise.resolve({
default: {
user: {id: 12},
language: 'es'
},
other: {
user: {id: 34},
language: 'ca'
}
})
}
The test file should be like:
import '@s-ui/studio/src/patcher-mocha'
chai.use(chaiDOM)
describe.context.default('atom/button', AtomButton => {
it('Render', () => {
const {getByText} = render(<AtomButton>HOLA</AtomButton>)
expect(getByText('HOLA')).to.have.text('HOLA 12')
})
})
describe.context.other('atom/button', AtomButton => {
it('Render', () => {
const {getByText} = render(<AtomButton>HOLA</AtomButton>)
expect(getByText('HOLA')).to.have.text('HOLA 34')
})
})
TLDR: Apply the context exported from the /demo/context.js
file for React components other than just the ones in /test/index.test.js
.
Since you can use the PATTERN=appraisal/report npm run test:studio
(see the section on cli testing) command to test a single studio component and you can have more than one *.test.js
file inside the /test
folder, you can use the following to test more than one component and apply to it the React Context exported from the /demo/context.js
file.
Naming convention: components.{componentName}.test.js
.
We use this naming convention so that we can have test files other than index.test.js
than can test regular functions that are not React Components.
For example, a test for a sum(a, b)
function that checks if the addition is working fine.
With this approach we have a way to define subcomponent tests that don't conflict with other test files that don't test React components.
test
├── components.AgencyEvaluationForm.test.js
├── components.AgencyEvaluationInfo.test.js
└── index.test.js
Inside your /demo/context.js
you export the React context that you want to apply to your component inside the Demo or test (sui domain, i18n, sui pde, etc).
import {domain, i18nFactory} from 'utils'
export default () => {
return i18nFactory().then(i18n => ({
default: {
domain: domain.build(),
i18n: {
culture: i18n.culture,
locale: i18n.locale,
t: i18n.t.bind(i18n),
c: i18n.c.bind(i18n),
n: i18n.n.bind(i18n),
f: i18n.f.bind(i18n)
}
}
}))
}
Then, you will need to use the '@s-ui/studio/src/patcher-mocha'
package that can use a componentKey
to load a specific studio component.
If we provide a component key that matches the naming system, we can set the context for subcomponents.
// /test/components.AgencyEvaluationInfo.test.js
/* global setupEnvironment */
import '@s-ui/studio/src/patcher-mocha'
import chai, {expect} from 'chai'
import chaiDOM from 'chai-dom'
import {ProvidersWrapper} from 'utils'
chai.use(chaiDOM)
const COMPONENT_KEY = 'appraisal/report/src/AgencyEvaluationInfo'
describe.context.default(
'AgencyEvaluationInfo',
describeCallback,
COMPONENT_KEY
)
function describeCallback(AgencyEvaluationInfo) {
const WrappedComponent = props => (
<ProvidersWrapper>
<AgencyEvaluationInfo {...props} />
</ProvidersWrapper>
)
const setup = setupEnvironment(WrappedComponent)
const baseProps = {}
it('should render the pdf link', async () => {
// Given
const props = {...baseProps}
const linkText = 'Informe de ejemplo'
// When
const {findByRole} = setup(props)
const pdfLink = await findByRole('link', {name: linkText})
// Then
expect(pdfLink).to.have.text('Ver informe de ejemplo')
})
}
Each test has a setupEnvironment
function in global scope. This function works equal to React Testing Library (RTL) render
method but has two wrapped features:
- It uses a container to isolate the tested component.
- It has a
contexts
render option in order to pass an array of contexts providers to be wrapped.
An example could be:
const ComponentWithContext = () => {
const value = useContext(ThemeContext)
return <p>{value}</p>
}
const setup = setupEnvironment(ComponentWithContext, {
contexts: [
{
provider: ThemeContext.Provider,
value: 'dark'
}
],
wrapper: ({children}) => (
<div>
<p>Hello</p>
{children}
</div>
)
})
You can see RTL render options documentation here
If a component is exported wrapped memoized: export default React.memo(Component)
, it loses the displayName and sui-test dispatch an Error because it couldn't find the component.
If you need to make a test using a memoized component, just wrap it like:
const Component = React.memo(() => <></>)
Component.displayName = 'Component'
export default Component
or
const Component = () => <></>
Component.displayName = 'Component'
const MemoComponent = React.memo(Component)
MemoComponent.displayName = 'MemoComponent'
export default MemoComponent
@s-ui/studio provides tools for running your entire component tests of your project on a karma browser
Add this scripts on your own components project
// package.json
{
scripts: {
"test": "sui-studio test",
"test:watch": "sui-studio test --watch"
}
}
Usage: sui-studio-test [options]
Options:
-H, --headless Run components tests in CLI, headless mode
-W, --watch Watch mode
-T, --timeout <timeout> Timeout
--coverage Create coverage (default: false)
-h, --help display help for command
Examples:
$ sui-studio test --headless
$ sui-studio test --headless --watch
$ sui-studio test --help
You could execute some specific tests with the sui-studio-test
command using PATTERN
ENV variables.
PATTERN
should be a glob pattern and:
-
It will be checked against the test file component path, for example
'./ad/card/test/index.test.js'
. -
It match any part of the test component path.
-
It uses a micromatch as a library for pattern matching (same as fast-glob).
Some examples:
-
For a specific component:
PATTERN='./ad/card/test/index.test.js' sui-studio test
orPATTERN='ad/card/test' sui-studio test
-
For categories:
PATTERN='{ad,shipping}/*/test' sui-studio test
Also, there is a way to only execute component categories but this is deprecated:
- If you want to execute the tests for some specific categories only use
CATEGORIES
environment variable. It takes a comma separated set of category names (e.g.CATEGORIES="user,shipping" sui-studio test
)
@s-ui/studio profusely uses the concept of "convention over configuration" for file structure.
├── components
│ ├── README.md
│ └── atom <- Component's category
│ ├── button <- Component's name
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── index.js
│ │ │ └── index.scss
│ │ ├── demo
│ │ │ ├── context.js
│ │ │ ├── playground <- Basic code that will be shown in the component's demo
│ │ │ └── themes <- SASS files stored in this folder will be themes shown on the interface
│ │ │ └── myStudioTheme.scss
│ │ └── test
│ │ └── index.js <- File containing all component's tests
│ └── header
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── index.js
│ │ └── index.scss
│ ├── demo <- Create a `demo` folder to put an demo app of your component (playground will be ignored)
│ │ ├── index.js
│ │ ├── index.scss
│ │ └── package.json
│ └── test
│ └── index.js
└── package.json
lowerCamelCase is the choice for directories and files.
components/house/mainWindow/...