Modify args in a play function #17140
Replies: 6 comments 1 reply
-
Hi @shaunpersad! I'm not 100% sure this would solve the issue you are experiencing but have you tried using the import { useArgs } from '@storybook/client-api'
import { expect, jest } from '@storybook/jest'
const spy = jest.fn()
const Template = (args) => {
const [{ checked }, updateArgs] = useArgs()
const handleOnChange = () => {
spy()
updateArgs({ checked: !checked })
}
return <Checkbox {...args} onChange={handleOnChange} />
} And instead of |
Beta Was this translation helpful? Give feedback.
-
It would be great if this would be possible, it would make the component tests that can be written in the play-function a lot more powerful, currently you can't write any test that want to ensure that the component reacts correctly to it's parameters changing, even though this is one of the most common sources of bugs. I've tried this under Storybook 6.5.14 and 7.0.5. Changing the Sadly, this solution only works in the browser. It does not work for the test-runner, which is essential, nobody wants to have to click through every single story manually. The moment the test-runner encounters the changing of the args, it seems to stop evaluating the play-function and simply reports a success. No expectations after the args-change are executed. Does anyone know of a way to fix this? Note: I'm using angular, maybe that's of relevance. |
Beta Was this translation helpful? Give feedback.
-
Bumping this, not being able to update args makes this feature all but useless for me. |
Beta Was this translation helpful? Give feedback.
-
I want to test how my component behaves when an arg changes after the component has fully initialized. Is this at all possible to do with storybook in its current state (and without any hacky workarounds)? |
Beta Was this translation helpful? Give feedback.
-
Since I don't use args on my played stories, I bypass the Storybook args completely. But you can quite easily iterate on my code to make it work with Storybook args (with a useEffect in The idea works a bit like a basic Redux. You have a separate store with vanilla JS listeners/triggers so that you can abstract this part from the internal export interface ArgsStoreItem<T extends Args> {
args: T | undefined
listeners: Listener<T>[]
}
export type Args = Record<string, any>
export type Listener<T extends Args> = (args: T) => void
export class ArgsStore {
#store: Map<ArgStoreKey, ArgsStoreItem<Args>>
constructor() {
this.#store = new Map()
}
getArgs<T extends Args>(key: ArgStoreKey): T | undefined {
return this.#store.get(key)?.args as T
}
initArgs<T extends Args>(key: ArgStoreKey, newArgs: T): T {
const storeEntry = this.#store.get(key)
const newListeners = storeEntry?.listeners ?? []
this.#store.set(key, {
args: newArgs,
listeners: newListeners,
})
return newArgs
}
updateArgs<T extends Args>(key: ArgStoreKey, nextlArgsPatch: Partial<T>): void {
const storeEntry = this.#store.get(key)
if (!storeEntry) {
return
}
const nextArgs = { ...storeEntry.args, ...nextlArgsPatch }
this.#store.set(key, {
args: nextArgs,
listeners: storeEntry.listeners,
})
this.#triggerChange(key, nextArgs)
}
addListener<T extends Args>(key: ArgStoreKey, callback: Listener<T>): void {
const storeEntry = this.#store.get(key)
const args = storeEntry?.args
const listeners = storeEntry?.listeners ?? []
const nextListeners = [...listeners, callback]
this.#store.set(key, {
args,
listeners: nextListeners as Listener<Args>[],
})
}
removeListener<T extends Args>(key: ArgStoreKey, callbackToRemove: Listener<T>): void {
const storeEntry = this.#store.get(key)
if (!storeEntry) {
return
}
const updatedListeners = storeEntry.listeners.filter(callback => callback !== callbackToRemove)
this.#store.set(key, {
...storeEntry,
listeners: updatedListeners,
})
}
#triggerChange<T extends Args>(key: ArgStoreKey, updatedArgs: T): void {
const storeEntry = this.#store.get(key)
if (!storeEntry) {
return
}
storeEntry.listeners.forEach(callback => callback(updatedArgs))
}
}
export const argsStore = new ArgsStore()
export function useArgsStoreArgs<T extends Args>(
key: ArgStoreKey,
initialArgs: T,
): [T, (nextArgsPatch: Partial<T>) => void] {
const [args, setArgs] = useState<T>(argsStore.initArgs<T>(key, initialArgs))
const updateArgs = useCallback(
(nextArgsPatch: Partial<T>) => {
argsStore.updateArgs<T>(key, nextArgsPatch)
},
[key],
)
useEffect(() => {
const handleArgsChange = (updatedArgs: T) => {
setArgs(updatedArgs)
}
argsStore.addListener<T>(key, handleArgsChange)
return () => {
argsStore.removeListener<T>(key, handleArgsChange)
}
}, [key])
return [args, updateArgs]
} |
Beta Was this translation helpful? Give feedback.
-
Hi! First of all, I love Storybook as a development tool, and I'm very excited to start using it more and more as a testing tool. I recently tried out play functions and they have a lot of promise! I hope one day they can fully replace writing tests in other suites.
One thing I think would be useful in achieving that goal would be to somehow modify the args and rerun the story within a play function. Consider this example:
I could not run this as-is today on a controlled checkbox, because
checkbox.checked
would never becomefalse
after a click, since theonChange
arg is just a mock not tied to any state. The best I can do today is to just test ifonChange
was called withfalse
after the first click.You could of course make the argument that the implementation of that state falls outside the component, which would be true, but this is such a common use-case that I feel there should be some kind of mechanism we can rely on to implement what I would consider a very typical use of a play function.
I believe a solution could either be found in some kind of pattern for state handling, or by just being able to modify args to re-render the story.
Beta Was this translation helpful? Give feedback.
All reactions