I have a custom Flutter widget, RadioSelect, which accepts an array of options and a callback function when one of those options is pressed. The callback is called with the selected option passed as it's only parameter. I'm trying to write a test which verifies that the callback was called and checks that the returned parameter is correct but I'm not sure how to structure it. What's a sensible way to check that a standalone callback function was called?
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: RadioSelect(
["option1","option2", "option3"],
// callback function passed here
),
);
},
),
);
expect(find.text('option1'), findsOneWidget);
await tester.press(find.text('option2'));
await tester.pump();
// test for callback here
You can also use a Completer
testWidgets('callback', (WidgetTester tester) async {
final completer = Completer<void>();
await tester.pumpWidget(
MaterialApp(
home: FlatButton(
child: Text('press me'),
onPressed: completer.complete,
),
),
);
await tester.tap(find.byType(FlatButton));
expect(completer.isCompleted, isTrue);
});
source: https://luksza.org/2020/testing-flutter-callbacks/
In the body of a Callback function, you can print the received arguments, and then expect if it prints correctly.
Here is another sample doing a similar test:
CHILD OF A TESTING WIDGET:
...
...
Checkbox(
key: Key('StatusCheckBox'),
value: isCompleted,
onChanged: (_) => toggleCompletionStatus(),
),
...
...
I'm passing print('Call') as a body of toggleCompletionStatus()
Which can be tested this way:
expectLater(
() => tester.tap(find.byKey(Key('StatusCheckBox'))), prints('Call\n'));
Related
I have two ducks (ui and backend) with epics in them.
I need to trigger two actions after finishing backend operations
One of these actions reside in the backend duck, the other in the ui duck.
I started with the background action and things worked as expected.
Adding the second action leads me to issues, as I can reach the action (console logs correctly), but not the reducer (no log)
The challenge I'm trying to solve is:
Kicking off two actions in one epic
dispatching an action in another reducer
My code looks similar to this:
the backendDuck's epic:
fetchFooEpic : (action$, store) =>
action$.pipe(
operators.filter(action => action.type === types.LOAD),
// start added section for second call
operators.switchMap(action => {
const response = operators.from(fetchSomeUrl(action))
.pipe(
operators.of(uiDuck.actions.fetchUserFulfilled(response.props)),
),
operators.catchError(err => {
console.error('Error happened!', err.message)
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
return response
}),
// start added section for second call
// original first call
operators.map(a => ({ type: types.ENDACTION, payload: a.payload })),
operators.catchError(err => {
console.error('Error happened!', err.message)
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
)
the uiDuck:
export actions={
...
fetchUserFulfilled: (value) => {
console.log('hello from action')
return ({ type: types.FETCHUSERFULFILLED, payload: value })
},
...
}
...
export default function reducer(state = initialState, action) {
switch (action.type) {
case types.FETCHUSERFULFILLED:
console.log('hello from reducer')
return {
...state,
user: action.payload,
}
...
Turns out I was combining the two calls in the wrong way.
For being able to pipe along, the piped observable needs to return an observable again.
When mapping to another redux-action, it seems to me that it doesn't return an observable (?) thus, the call needs to happen for all desired redux-actions at the same location (eg with concat)
For the sake of completeness I strive to explain all parts of the code in comments
import * as operators from 'rxjs'
fetchFooEpic : (action$, store) =>
action$.pipe(
operators.filter(action => action.type === types.LOAD), // Filter
operators.switchMap(action => // restart inner actions on each call
operators.from(fetchSomeUrl(action)) // creating observable from result
.pipe( // starting new flow on observable (self)
//operators.tap(a => console.log('Now running fetchfooepic 2', a)), // dedicated location for sideeffects
operators.switchMap( // restart inner actions on each call
(response) => operators.concat( // Kicking off several actions sequentially (merge() would do that in parallel)
operators.of(uiDuck.actions.fetchUserFulfilled(response)), // addressing the redux action in other reducer
operators.of(({ // addressing the redux action via the type in this duck (ENDACTION is controlled by epics only, no action exists for it)
type: types.ENDACTION,
payload: response
}})),
)),
operators.catchError(err =>{
console.error('Shit happens!', err.message) // errorhandling
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
)
)
),
Generally the functions are documented with some (more or less understandable) examples in
https://rxjs.dev/api/index/function/
I've seen a lot of examples of async action creators, but they all do some sort of fetching and pushing data to redux store and return nothing. I need another logic that looks something like:
const createUserAction = (user) => {
firestore().collection('users').add(user)
.then(result => {
dispatch({type: 'SET_USER', payload: {...user, id: result.id}})
})
}
I need to return result.id from createUserAction to navigate to page that displays user by his id. In my imagine it should work like
createUserAction({name: John}).then(id => navigation.navigate('UserDetailPage', {userId: id}))
I don't know how to implement that and I'll be glad if somebody can help
Returning values from action creators is a No-Go. The solution for this scenario that I've used and think is better is to do the redirect in the async action itself:
// afterCreation = callback function with one argument, the created user
const createUserAction = async (user, afterCreation) => {
const createdUser = await firestore().collection('users').add(user);
dispatch({type: 'SET_USER', payload: {...user, id: createdUser.id}});
afterCreation(createdUser);
};
createUserAction(
{name: John},
// Pass callback to action creator
(user) => navigation.navigate('UserDetailPage', {userId: user.id})
);
Am trying to provide test authors with a fluent PageModel api in TestCafe, like:
await MyApp // a Page Model class instance
.navigateTo(xyz) // clicks a button to navigate to a specific part in my app
.edit() // clicks the edit button
.setField(abc, 12.34)
.save()
.changeStatus('complete');
I had all the individual methods working as async methods that can be awaited individually, but that makes the code quite unreadable and as a result error prone.
However, whatever way I attempt to make the api fluent, it results in the following error:
Selector cannot implicitly resolve the test run in context of which it
should be executed. If you need to call Selector from the Node.js API
callback, pass the test controller manually via Selector's .with({ boundTestRun: t }) method first. Note that you cannot execute
Selector outside the test code.
The trick into making a fluent async api is imho switching from async functions to regular functions as methods and have those methods return a thenable 'this' value. And in order to prevent the await oscillating, the 'then' function needs to be removed once called (and then reinstalled when
A very basic example that reproduces the issue can be seen below:
import { Selector } from 'testcafe'
class MyPage {
queue: [];
async asyncTest() {
return await Selector(':focus').exists;
}
queuedTest() {
this.then = (resolve, reject) => {
delete this.then; // remove 'then' once thenable gets called to prevent endless loop
// calling hardcoded method, in a fluent api would processes whatever is on the queue and then resolve with something
resolve(this.asyncTest());
};
// In a real fluent api impl. there would be code here to put something into the queue
// to execute once the 'then' method gets called
// ...
return this;
}
}
fixture `Demo`
.page `https://google.com`;
test('demo', async () => {
const myPage = new MyPage();
console.log('BEFORE')
await myPage.asyncTest();
console.log('BETWEEN')
await myPage.queuedTest(); // Here it bombs out
console.log('AFTER')
});
Note that the sample above isn't showcasing a fluent api, it just demonstrates that calling methods that use Selectors through the 'then' function (which imho is key to creating a fluent api) results in the aforementioned error.
Note: I know what the error means and that the suggestion is to add .with({boundTestRun: t}) to the selector, but that would result in required boilerplate code and make things less maintainable.
Any thoughts appreciated
P.
In your example, a selector cannot be evaluated because it does not have access to the test controller (t). You can try to avoid directly evaluating selectors without assertion.
Here is my example of the chained Page Model (based on this article: Async Method Chaining in Node):
Page Model:
import { Selector, t } from 'testcafe';
export class MyPage {
constructor () {
this.queue = Promise.resolve();
this.developerName = Selector('#developer-name');
this.submitButton = Selector('#submit-button');
this.articleHeader = Selector('#article-header');
}
_chain (callback) {
this.queue = this.queue.then(callback);
return this;
}
then (callback) {
return callback(this.queue);
}
navigateTo (url) {
return this._chain(async () => await t.navigateTo(url));
}
typeName (name) {
return this._chain(async () => await t.typeText(this.developerName, name));
}
submit () {
return this._chain(async () => await t.click(this.submitButton));
}
checkName (name) {
return this._chain(async () => await t.expect(this.articleHeader.textContent).contains(name));
}
getHeader () {
this._chain(async () => console.log(await this.articleHeader.textContent));
return this;
}
}
Test:
import { MyPage } from "./page-model";
fixture`Page Model Tests`;
const page = new MyPage();
test('Test 1', async () => {
await page
.navigateTo('http://devexpress.github.io/testcafe/example/')
.typeName('John')
.submit()
.checkName('John')
.getHeader();
});
I got 3 pages
homepage, productList and productDetails
When going from homepage to productList I pass a route param,
navigation.navigate('productList', { showCategory: 'productListA'} )
InitialProcess when component mounted
Inside the productList page when the component is mounted. I am declaring use state like this.
const {showCateory} = route.params;
const [activeTab, setActiveTab] = useState(showCateory);
and calling api using that activeTab
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
User Interaction
But I also add the button in the productList so that user can change the current active tab
<TouchableOpacity onPress={() => changeTab()}></TouchableOpacity>
const changeTab = async () => {
await setActiveTab('productListB'),
await dispatch(fetchProductList(activeTab)
}
Take note that right now active tab and data coming from api is different from when the component is start mounted.
Navigation Change again
When use goes from productList to productDetails. All thing is fine.
But inside the product details I am going back to productList with this.
navigation.goBack().
When I am back in productList page The activeTab is change back to productListA and the data is change back to when component is mounted
Can I pass or change the route params when calling navigation.goBack()?
add activeTab in useEffect depedineces.
as docs say
The array of dependencies is not passed as arguments to the effect function. Conceptually, though, that’s what they represent: every value referenced inside the effect function should also appear in the dependencies array. In the future, a sufficiently advanced compiler could create this array automatically.
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
//this value will always updated when activeTab change
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, [activeTab]); //<<<<< here
also you need to know setState() does not always immediately update the component. see here
so change this
const changeTab = async () => {
//await setActiveTab('productListB'),
//await dispatch(fetchProductList(activeTab)
setActiveTab('productListB')
dispatch(fetchProductList('productListB'))
}
This might be happening because route.params is still set to { showCategory: 'productListA'} when you are coming back to the screen.
If this is the case, you can fix it by Changing params object in changeTab() like
navigation.setParams({
showCategory: 'productListB',
});
I hope this will fix your problem.
This happens because the callback function inside the focus listener uses the initial value of the state when the function was defined (at initial page render) . Throughout the lifespan of listener the callback function uses this stale state value.You can read more about this behaviour in this answer
Although the answer by Ahmed Gaber works in this case as the listener is cleared and redefined after each state change.Another common work-around is to use an useRef instead of useEffect.A ref is basically a recipe that provides a mutable object that can be passed by reference.
In your case you can initialise activeTab with navigation param value using useRef hook as :
const activeTab = useRef(showCateory);
and the focus listener callback function should be changed to use the Reference current value as
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab.current, //<<<<<<---------here
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
and the changeTab function can directly update reference current value
const changeTab = async () => {
setActiveTab.current = 'productListB';
dispatch(fetchProductList('productListB'))
}
I am trying to run unit test (enzyme) throws error on Formik 'resetForm'.
TypeError: Cannot read property 'resetForm' of undefined
FormikForm.js
_handleSubmitPress = (values, { resetForm }) => {
const { onSubmit } = this.props;
if (onSubmit) {
onSubmit({ ...values, resetForm });
}
};
UnitTest.js:
it('Should fire formik form submit', () => {
const UpdateButtonPressMock = jest.fn();
const component = Component({
onSubmit: UpdateButtonPressMock,
});
expect(component.find(Formik)).toHaveLength(1);
component.find(Formik)
.first()
.simulate('Submit');
expect(UpdateButtonPressMock).toHaveBeenCalled();
});
I couldn't find any solution for this error.
Could someone help me on the above? I would really appreciate any help.
According to official docs for simulate, the function signature accepts an optional mock event.
The code you are testing uses properties that are not included in the default SyntheticEvent object that ReactWrapper passes to your event handler by default, for instance event.resetForm.
One way to do this is by triggering Formik's onSubmit directly like so:
// UnitTest.js
.simulate("submit", { resetForm: whateverYourMockResetFormValueShouldBe })
component.find(Formik)
.first()
.prop('onSubmit')(valuesMock, { resetForm: UpdateButtonPressMock });
expect(UpdateButtonPressMock).toHaveBeenCalled();
I haven't tested this, but you should be able to pass the event along with simulate as well.
// UnitTest.js
component.find(Formik)
.first()
.simulate("submit", { resetForm: UpdateButtonPressMock })
expect(UpdateButtonPressMock).toHaveBeenCalled();