I am using react-native-testing-library - https://callstack.github.io/react-native-testing-library/docs/getting-started
I have a <SegmentedControlIOS> - https://facebook.github.io/react-native/docs/segmentedcontrolios
I want to pres the first segment. I am doing this:
const testID = "SegmentedControl";
const stub = jest.fn();
const values = [{ label: "foo" }];
const { getByTestId } = render(
<SegmentedControlIOS values={['foo', 'bar']} onChange={stub} testID={testID} />
);
expect(() => {
getByTestId(testID);
}).not.toThrow();
fireEvent(getByTestId(testID), "change ", {
nativeEvent: {
value: values[0],
selectedSegmentIndex: 0,
},
});
However I get the error:
No handler function found for event: "change "
Screenshot below. Anyone know how to press different segments in <SegmentedControlIOS>?
fireEvent(element: ReactTestInstance, eventName: string, ...data:
Array): void
The change function is located in the fireEvent object. Here's how to use it:
Version 5 or later:
fireEvent.change(getByTestId(testID), { target: { value: values[0],selectedSegmentIndex: 0 } });
Version 5 or before:
const input = getByTestId(testID);
input.value = values[0];
input.selectedSegmentIndex = 0;
fireEvent.change(input);
If you want to check the onChange function of SegmentedControlIOS,
using fireEvent with native events that aren't already aliased by the fireEvent api.
// you can omit the `on` prefix
fireEvent(getByTestId(testID), 'onChange');
A solution was posted here, I didn't try it yet, but it looks more right I think - https://github.com/callstack/react-native-testing-library/issues/220#issuecomment-541067962
import React from "react";
import { SegmentedControlIOS } from "react-native";
import { fireEvent, render } from "react-native-testing-library";
const testID = "SegmentedControl";
const stub = jest.fn();
const values = [{ label: "foo" }];
const { getByTestId } = render(
<SegmentedControlIOS
values={["foo", "bar"]}
onChange={stub}
testID={testID}
/>,
);
it("sends events", () => {
fireEvent(getByTestId(testID), "onChange", {
nativeEvent: {
value: values[0],
selectedSegmentIndex: 0,
},
});
});
Related
I want to integrate flutterwave in my react native application. I downloaded their npm package called flutterwave-react-native and followed their tutorial but still can't do it. I'm using their sample snippet on Github and I'm getting an error that says:
this.usePaymentLink is not a function
I searched everywhere but couldn't find where this.usePaymentLink was defined. You can check out my snippet and tell me what I missed and how this.usePaymentLink can look like.
import React from 'react';
import {View, TouchableOpacity} from 'react-native';
import {FlutterwaveInit} from 'flutterwave-react-native';
class MyCart extends React.Component {
abortController = null;
componentWillUnmout() {
if (this.abortController) {
this.abortController.abort();
}
}
handlePaymentInitialization = () => {
this.setState({
isPending: true,
}, () => {
// set abort controller
this.abortController = new AbortController;
try {
// initialize payment
const paymentLink = await FlutterwaveInit(
{
tx_ref: generateTransactionRef(),
authorization: '[merchant public key]',
amount: 100,
currency: 'USD',
customer: {
email: 'customer-email#example.com',
},
payment_options: 'card',
},
this.abortController
);
// use payment link
return this.usePaymentLink(paymentLink);
} catch (error) {
// do nothing if our payment initialization was aborted
if (error.code === 'ABORTERROR') {
return;
}
// handle other errors
this.displayErrorMessage(error.message);
}
});
}
render() {
const {isPending} = this.state;
return (
<View>
...
<TouchableOpacity
style={[
styles.paymentbutton,
isPending ? styles.paymentButtonBusy : {}
]}
disabled={isPending}
onPress={this.handlePaymentInitialization}
>
Pay $100
</TouchableOpacity>
</View>
)
}
}
so i have been trying to apply it on expo but finally got a breakthrough.
// so i made some little corrections before i could get it running
// this is the code directly from their npm or github
import {PayWithFlutterwave} from 'flutterwave-react-native';
<PayWithFlutterwave
...
onRedirect={handleOnRedirect}
options={{
tx_ref: transactionReference,
authorization: '[merchant public key]',
customer: {
email: 'customer-email#example.com'
},
amount: 2000,
currency: 'NGN',
payment_options: 'card'
}}
/>
// my correction
first of all handleOnRedirect must be a defined function
secondly i removed the three dots (...) before the handleOnRedirect function
then created a function to generate a randomized refrenced no
then i pasted my public flutterwave account key for "merchant public key"
i also pasted my flutterwave account email in place of this 'customer-email#example.com'
import {PayWithFlutterwave} from 'flutterwave-react-native';
const handleOnRedirect = () => {
console.log('sadi')
}
const generateRef = (length) => {
var a = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".split("");
var b = [];
for (var i=0; i<length; i++) {
var j = (Math.random() * (a.length-1)).toFixed(0);
b[i] = a[j];
}
return b.join("");
}
<PayWithFlutterwave
onRedirect={handleOnRedirect}
options={{
tx_ref: generateRef(11),
authorization: 'MY_PUBLIC_KEY',
customer: {
email: 'user#gmail.com'
},
amount: 2000,
currency: 'NGN',
payment_options: 'card'
}}
/>
``
I have a custom plugin in ckeditor5.
When user click on toolbar icon my plugin convert selected test to custom element with a custom attribute name comment-id.
this work properly.
Now I want to watch on click element and get comment-id on click and I don't know how can I do that.
this is the code of my custom plugin
import uploadIcon from './message.svg'
import ButtonView from '#ckeditor/ckeditor5-ui/src/button/buttonview'
import Plugin from '#ckeditor/ckeditor5-core/src/plugin'
import Command from '#ckeditor/ckeditor5-core/src/command'
import './comment.css'
export default class CustomFileExporerPlugin extends Plugin {
init() {
const editor = this.editor
const config = editor.config.get('comment')
console.log(editor.editing.view.document.isFocused)
editor.editing.view.document.on('click', a => {
console.log(a)
})
editor.model.schema.extend('$text', { allowAttributes: 'comment' })
editor.conversion.attributeToElement({
model: 'comment',
view: (commentId, writer) => {
debugger
if (writer) {
return writer.writer.createAttributeElement(
'comment',
{
'comment-id': commentId,
class: `ck-comment-marker`
},
{ priority: 5 }
)
}
}
})
editor.commands.add('comment', new CommentCommand(editor))
editor.ui.componentFactory.add('comment', locale => {
const view = new ButtonView(locale)
view.set({
label: 'add comment',
icon: uploadIcon,
tooltip: true
})
view.on('execute', async () => {
editor.editing.view.focus()
let id = await config.callback()
editor.execute('comment', { value: 'comment', id })
})
return view
})
}
}
class CommentCommand extends Command {
refresh() {
const model = this.editor.model
const doc = model.document
this.value = doc.selection.getAttribute('comment')
this.isEnabled = model.schema.checkAttributeInSelection(
doc.selection,
'comment'
)
}
execute(options = {}) {
const model = this.editor.model
const document = model.document
const selection = document.selection
const highlighter = options.value
model.change(writer => {
const ranges = model.schema.getValidRanges(
selection.getRanges(),
'comment'
)
if (selection.isCollapsed) {
const position = selection.getFirstPosition()
if (selection.hasAttribute('comment')) {
const isSameHighlight = value => {
return (
value.item.hasAttribute('comment') &&
value.item.getAttribute('comment') === this.value
)
}
const highlightStart = position.getLastMatchingPosition(
isSameHighlight,
{ direction: 'backward' }
)
const highlightEnd = position.getLastMatchingPosition(isSameHighlight)
const highlightRange = writer.createRange(
highlightStart,
highlightEnd
)
writer.removeAttribute('comment', highlightRange)
writer.removeSelectionAttribute('comment')
} else if (highlighter) {
writer.setSelectionAttribute('comment', highlighter)
}
} else {
for (const range of ranges) {
writer.setAttribute('comment', options, range)
}
}
})
}
}
When rendering editor, you can catch custom plugin click event using below code.
const command = editor.commands.get('comment')
command.on('execute', () => { catch click event in custom plugin})
I'm using AsyncStorage to store and retrieve an array of objects. The structure of the array is like this:
const tracks = [
{
title: title1,
exercises: [
{
object1Info...
},
{
object2Info...
}
]
},
{
title: title2,
exercises: [
{
object1Info...
}
]
},
{
title: title3,
exercises: [
{
object1Info...
}
]
}
]
As you can see, the objects in the array do themselves contain arrays, which again contain objects.
I'm storing the array like this:
const storeData = async (array) => {
try {
const stringifiedArray = JSON.stringify(array)
await AsyncStorage.setItem('#tracks_array', stringifiedArray)
} catch (e) {
console.log("Error saving data")
}
}
This seems to work fine. I then retrieve the data like this:
const retrieveData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#tracks_array');
console.log('Parsed value: ' + JSON.parse(jsonValue)); //This prints 'Parsed value: [object Object],[object Object],[object Object],[object Object]'
return jsonValue !== null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log("Error retrieving data")
}
}
This seems to work fine as well.
I have the array stored also as state. So what I want to do is add an object to the array in state, store that new array in the AsyncStorage, and then retrieve the array and set this new array back to state. Storing the object seems to have no problems.
When I retrieve the new array, and console.log(JSON.parse(jsonValue)) inside retrieveData, it prints [object Object],[object Object],[object Object],[object Object]. However after I call const newData = retrieveData(), console.log(newData) prints just [object Object]. This is my first time using AsyncStorage so I must be misunderstanding something. Why does it only return one object, instead of the whole array?
EDIT: Sharing the whole component code:
import {
StyleSheet,
ScrollView,
View,
Text
} from 'react-native';
import Modal from 'react-native-modal';
import AsyncStorage from '#react-native-community/async-storage'
import Track from './Track.js';
import New from './New.js';
class Main extends Component {
constructor(props) {
super(props);
this.state = {
tracksData: tracks,
newTrack: false,
newExercise: false
}
storeData(this.state.tracksData);
}
renderTracks(data) {
console.log('Main data = ' + data)
return data.map((item, i) => {
console.log('Item = ' + item)
return (
<Track key={i} data={item} />
)
});
}
render() {
return (
<ScrollView horizontal={true} style={styles.Main}>
{this.renderTracks(this.state.tracksData)}
<Track data={{title: 'NewTrack', exercises: 'NewTrack'}} newTrackBox={this.toggleTrackBox} />
<Modal isVisible={this.state.newTrack} coverScreen={true}>
<New type={'track'} visible={this.toggleTrackBox} add={(name) => this.addTrack(name)}/>
</Modal>
</ScrollView>
);
}
toggleTrackBox = () => {
this.setState({
newTrack: !this.state.newTrack
})
}
addTrack = (name) => {
this.setState({
newTrack: false
});
var newTracks = this.state.tracksData;
newTracks.push({title: name, exercises: []})
console.log('newTracks = ' + newTracks)
storeData(newTracks);
this.updateData();
}
updateData() {
var newData = retrieveData();
console.log('newData = ' + newData)
setTimeout(() => {
console.log('Retrieved data = ' + newData);
if (newData) {
this.setState({
tracksData: newData
});
console.log("Data updated");
return true;
} else {
console.log("Data couldn't be retrieved");
return false;
}
}, 5000)
}
}
const storeData = async (value) => {
try {
const stringifiedArray = JSON.stringify(value)
console.log('Value to store: ' + value)
console.log('Stringified value to store: ' + stringifiedArray)
await AsyncStorage.setItem('#tracks_array', stringifiedArray)
//alert("Success saving data!")
} catch (e) {
console.log("Error saving data")
alert("Error saving data");
}
}
const retrieveData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#tracks_array');
console.log('Stringified value retrieved: ' + jsonValue)
console.log('Parsed value: ' + JSON.parse(jsonValue))
return jsonValue !== null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log("Error retrieving data")
alert("Error retrieving data");
}
}
const tracks = [ //each member of this array is sent to a Track
{
title: 'Pull-up', // used in Track
exercises: [ // each member of this array is sent to an Exercise by Track
{
name: 'Pull-up', // used in Exercise
setStart: 2, // this and below used to calculate no of tiles and their contents, which are then sent to Tile
setEnd: 3,
repStart: 5,
repEnd: 8,
isInSeconds: false,
inProgress: null,
completed: true
},
{
name: 'Weighted Pull-up',
setStart: 3,
setEnd: 3,
repStart: 5,
repEnd: 8,
isInSeconds: false,
inProgress: [3, 5],
completed: false
}
]
},
{
title: 'Dip',
exercises: [
{
name: 'Dip',
setStart: 2,
setEnd: 3,
repStart: 5,
repEnd: 8,
isInSeconds: false,
inProgress: null,
completed: true
}
]
},
{
title: 'Squat',
exercises: [
{
name: 'Pistol squat',
setStart: 2,
setEnd: 3,
repStart: 5,
repEnd: 8,
isInSeconds: false,
inProgress: [2, 8],
completed: false
}
]
}
]
const styles = StyleSheet.create({
Main: {
flex: 1,
flexDirection: 'row',
backgroundColor: '#022763'
}
})
export default Main;
Also, I should have mentioned, the actual error I'm getting is:
TypeError: undefined is not a function (near '...data.map...')
"retrieveData" is async function and hence returns a Promise.
What happened was it didn't finish retrieving the data and hence newData got 1 object out of all the array.
Try changing updateData like this:
updateData() {
var newData = retrieveData().then(data => {
console.log('newData = ' + newData)
setTimeout(() => {
console.log('Retrieved data = ' + newData);
if (newData) {
this.setState({
tracksData: newData
});
console.log("Data updated");
return true;
} else {
console.log("Data couldn't be retrieved");
return false;
}
}, 5000)
};
}
I've figured out the issue. I was retrieving data with AsyncStorage, then setting that data to the state something like this:
var newData = asyncRetrieveDataFunction();
this.setState({state1: newData})
However, because I declared the retrieveData() function as async, it was setting the state before the data had finished retrieving. The solution was to use the then keyword and change it to something like this:
asyncRetrieveDataFunction().then(data => this.setState({state1: data}));
This ensures that the data has been returned BEFORE assigning it to a state.
I'm building an app with react-navigation-4.2.1. The app has multiple stack navigators. So there are a lots of navigation.push('Routename') calls.
Trouble is when the control surface (i.e. TouchableOpacity) is tapped rapidly multiple times (first one, and the rest during screen transition) I end up pushing multiple screens into the stack. Is there a way to restrict the surface to the first tap/call of push()?
The component below is what i use to make things touchable. it handle multiple touches in small period of time.
Use component below instead of TouchableOpacity. wrap any thing you want with this component and it will be touchable.
<SafeTouch
onPress={...}
>
<Text> hey! im a touchable text now</Text>
</SafeTouch>
The component below is written used TypeScirpt.
every touch within 300ms after first touch will be ignored(thats where help you with your problem).
import * as React from 'react'
import { TouchableOpacity } from 'react-native'
interface ISafeTouchProps {
onPress: () => void
onLongPress?: () => void
onPressIn?: () => void
onPressOut?: () => void,
activeOpacity?: number,
disabled?: boolean,
style: any
}
export class SafeTouch extends React.PureComponent<ISafeTouchProps> {
public static defaultProps: ISafeTouchProps = {
onPress: () => { },
onLongPress: () => { },
onPressIn: () => { },
onPressOut: () => { },
disabled: false,
style: null
}
private isTouchValid: boolean = true
private touchTimeout: any = null
public constructor(props: ISafeTouchProps) {
super(props)
{// Binding methods
this.onPressEvent = this.onPressEvent.bind(this)
}
}
public render(): JSX.Element {
return (
<TouchableOpacity
onPress={this.onPressEvent}
onLongPress={this.props.onLongPress}
onPressIn={this.props.onPressIn}
onPressOut={this.props.onPressOut}
activeOpacity={this.props.activeOpacity}
disabled={this.props.disabled}
style={[{minWidth: 24, minHeight: 24}, this.props.style]}
>
{
this.props.children
}
</TouchableOpacity>
)
}
public componentWillUnmount() {
this.clearTimeoutIfExists()
}
private onPressEvent(): void {
requestAnimationFrame(() => {
if (this.isTouchValid === false) {
return
}
this.isTouchValid = false
this.clearTimeoutIfExists()
this.touchTimeout = setTimeout(() => {
this.isTouchValid = true
}, 300)
if (typeof this.props.onPress === 'function') {
this.props.onPress()
}
})
}
private clearTimeoutIfExists(): void {
if (this.touchTimeout != null) {
clearTimeout(this.touchTimeout)
this.touchTimeout = null
}
}
}
This is the proper behavior for Push and it is not a bug if you want
to avoid the duplicate screen on double tab you can just use navigation.navigate.
To avoid pushing the screen more than once when clicking in the same button in a short span of time, I created a generic hook to avoid running a function more than once (accepting an interval to allow run again):
export const useCallOnce = <T extends unknown[], K>(
fn: (...args: T) => K,
allowAfter?: number,
) => {
const ref = React.useRef<number | undefined>();
const resultFn = (...args: T) => {
const now = new Date().getTime();
if (!ref.current || (allowAfter && ref.current + allowAfter < now)) {
ref.current = now;
return fn(...args);
}
};
return resultFn;
};
Then, you can just call it as in the following example:
const navigation = useNavigation<NativeStackNavigationProp<{ ExampleScreen: undefined }>>();
const push = useCallOnce(() => navigation.push('ExampleScreen'), 500);
// just call on the button click event as: onSomeEvent={() => push()}
You can create a generic button component that accept the push parameters with the hook above, similar to the example, and use this button whenever you want a button to navigate between pages.
just trying come silly stuff and playing around with Cycle.js. and running into problem. Basically I just have a button. When you click it it's suppose to navigate the location to a random hash and display it. Almost like a stupid router w/o predefined routes. Ie. routes are dynamic. Again this isn't anything practical I am just messing with some stuff and trying to learn Cycle.js. But the code below crashes after I click "Add" button. However the location is updated. If I actually just navigate to "#/asdf" it displays the correct content with "Hash: #/asdf". Not sure why the flow is crashing with error:
render-dom.js:242 TypeError: Cannot read property 'subscribe' of undefined(…)
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import ranomdstring from 'randomstring';
const history = createHashHistory({ queryKey: false });
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => {
return ranomdstring.generate(10);
}).startWith(null);
const vtree$ = create$.map(rs => rs ?
history.push(`/${rs}`) :
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$ };
}
function main(sources) {
const hash = location.hash;
const DOM = sources.DOM;
const vtree$ = hash ?
Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
) :
CreateButton({ DOM }).DOM;
return {
DOM: vtree$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container')
});
Thank you for the help
I would further suggest using #cycle/history to do your route changing
(Only showing relevant parts)
import {makeHistoryDriver} from '#cycle/history'
import {createHashHistory} from 'history'
function main(sources) {
...
return {history: Rx.Observable.just('/some/route') } // a stream of urls
}
const history = createHashHistory({ queryKey: false })
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
history: makeHistoryDriver(history),
})
On your function CreateButton you are mapping your clicks to history.push() instead of mapping it to a vtree which causes the error:
function CreateButton({ DOM }) {
...
const vtree$ = create$.map(rs => rs
? history.push(`/${rs}`) // <-- not a vtree
: button('.create-button .btn .btn-default', 'Add')
);
...
}
Instead you could use the do operator to perform the hashchange:
function CreateButton({ DOM }) {
const create$ =
...
.do(history.push(`/${rs}`)); // <-- here
const vtree$ = Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
...
}
However in functional programming you should not perform side effects on you app logic, every function must remain pure. Instead, all side effects should be handled by drivers. To learn more take a look at the drivers section on Cycle's documentation
To see a working driver jump at the end of the message.
Moreover on your main function you were not using streams to render your vtree. It would have not been reactive to locationHash changes because vtree$ = hash ? ... : ... is only evaluated once on app bootstrapping (when the main function is evaluated and "wires" every streams together).
An improvement will be to declare your main's vtree$ as following while keeping the same logic:
const vtree$ = hash$.map((hash) => hash ? ... : ...)
Here is a complete solution with a small locationHash driver:
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import randomstring from 'randomstring';
function makeLocationHashDriver (params) {
const history = createHashHistory(params);
return (routeChange$) => {
routeChange$
.filter(hash => {
const currentHash = location.hash.replace(/^#?\//g, '')
return hash && hash !== currentHash
})
.subscribe(hash => history.push(`/${hash}`));
return Rx.Observable.fromEvent(window, 'hashchange')
.startWith({})
.map(_ => location.hash);
}
}
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => randomstring.generate(10))
.startWith(null);
const vtree$ = Rx.Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$, routeChange$: create$ };
}
function main({ DOM, hash }) {
const button = CreateButton({ DOM })
const vtree$ = hash.map(hash => hash
? Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
)
: button.DOM
)
return {
DOM: vtree$,
hash: button.routeChange$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
hash: makeLocationHashDriver({ queryKey: false })
});
PS: there is a typo in your randomstring function name, I fixed it in my example.