How to query by text string which contains html tags using React Testing Library? - reactjs

Current Working Solution
Using this html:
<p data-testid="foo">Name: <strong>Bob</strong> <em>(special guest)</em></p>
I can use the React Testing Library getByTestId method to find the textContent:
expect(getByTestId('foo').textContent).toEqual('Name: Bob (special guest)')
Is there a better way?
I would like to simply use this html:
<p>Name: <strong>Bob</strong> <em>(special guest)</em></p>
And use React Testing Library's getByText method like this:
expect(getByText('Name: Bob (special guest)')).toBeTruthy()
But this does not work.
So, the question…
Is there a simpler way to use React Testing Library to find strings of text content with the tags striped out?

Update 2
Having used this many times, I've created a helper. Below is an example test using this helper.
Test helper:
// withMarkup.ts
import { MatcherFunction } from '#testing-library/react'
type Query = (f: MatcherFunction) => HTMLElement
const withMarkup = (query: Query) => (text: string): HTMLElement =>
query((content: string, node: HTMLElement) => {
const hasText = (node: HTMLElement) => node.textContent === text
const childrenDontHaveText = Array.from(node.children).every(
child => !hasText(child as HTMLElement)
)
return hasText(node) && childrenDontHaveText
})
export default withMarkup
Test:
// app.test.tsx
import { render } from '#testing-library/react'
import App from './App'
import withMarkup from '../test/helpers/withMarkup'
it('tests foo and bar', () => {
const { getByText } = render(<App />)
const getByTextWithMarkup = withMarkup(getByText)
getByTextWithMarkup('Name: Bob (special guest)')
})
Update 1
Here is an example where a new matcher getByTextWithMarkup is created. Note that this function extends getByText in a test, thus it must be defined there. (Sure the function could be updated to accept getByText as a parameter.)
import { render } from "#testing-library/react";
import "jest-dom/extend-expect";
test("pass functions to matchers", () => {
const Hello = () => (
<div>
Hello <span>world</span>
</div>
);
const { getByText } = render(<Hello />);
const getByTextWithMarkup = (text: string) => {
getByText((content, node) => {
const hasText = (node: HTMLElement) => node.textContent === text
const childrenDontHaveText = Array.from(node.children).every(
child => !hasText(child as HTMLElement)
)
return hasText(node) && childrenDontHaveText
})
}
getByTextWithMarkup('Hello world')
Here is a solid answer from the 4th of Five Things You (Probably) Didn't Know About Testing Library from Giorgio Polvara's Blog:
Queries accept functions too
You have probably seen an error like this one:
Unable to find an element with the text: Hello world.
This could be because the text is broken up by multiple elements.
In this case, you can provide a function for your text
matcher to make your matcher more flexible.
Usually, it happens because your HTML looks like this:
<div>Hello <span>world</span></div>
The solution is contained inside the error message: "[...] you can provide a function for your text matcher [...]".
What's that all about? It turns out matchers accept strings, regular expressions or functions.
The function gets called for each node you're rendering. It receives two arguments: the node's content and the node itself. All you have to do is to return true or false depending on if the node is the one you want.
An example will clarify it:
import { render } from "#testing-library/react";
import "jest-dom/extend-expect";
test("pass functions to matchers", () => {
const Hello = () => (
<div>
Hello <span>world</span>
</div>
);
const { getByText } = render(<Hello />);
// These won't match
// getByText("Hello world");
// getByText(/Hello world/);
getByText((content, node) => {
const hasText = node => node.textContent === "Hello world";
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every(
child => !hasText(child)
);
return nodeHasText && childrenDontHaveText;
});
});
We're ignoring the content argument because in this case, it will either be "Hello", "world" or an empty string.
What we are checking instead is that the current node has the right textContent. hasText is a little helper function to do that. I declared it to keep things clean.
That's not all though. Our div is not the only node with the text we're looking for. For example, body in this case has the same text. To avoid returning more nodes than needed we are making sure that none of the children has the same text as its parent. In this way we're making sure that the node we're returning is the smallest—in other words the one closes to the bottom of our DOM tree.
Read the rest of Five Things You (Probably) Didn't Know About Testing Library

If you are using testing-library/jest-dom in your project. You can also use toHaveTextContent.
expect(getByTestId('foo')).toHaveTextContent('Name: Bob (special guest)')
if you need a partial match, you can also use regex search patterns
expect(getByTestId('foo')).toHaveTextContent(/Name: Bob/)
Here's a link to the package

For substring matching, you can pass { exact: false }:
https://testing-library.com/docs/dom-testing-library/api-queries#textmatch
const el = getByText('Name:', { exact: false })
expect(el.textContent).toEqual('Name: Bob (special guest)');

The existing answers are outdated. The new *ByRole query supports this:
getByRole('button', {name: 'Bob (special guest)'})

Update
The solution below works but for some cases, it might return more than one result. This is the correct implementation:
getByText((_, node) => {
const hasText = node => node.textContent === "Name: Bob (special guest)";
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every(
child => !hasText(child)
);
return nodeHasText && childrenDontHaveText;
});
You can pass a method to getbyText:
getByText((_, node) => node.textContent === 'Name: Bob (special guest)')
You could put the code into a helper function so you don't have to type it all the time:
const { getByText } = render(<App />)
const getByTextWithMarkup = (text) =>
getByText((_, node) => node.textContent === text)

To avoid matching multiple elements, for some use cases simply only returning elements that actually have text content themselves, filters out unwanted parents just fine:
expect(
// - content: text content of current element, without text of its children
// - element.textContent: content of current element plus its children
screen.getByText((content, element) => {
return content !== '' && element.textContent === 'Name: Bob (special guest)';
})
).toBeInTheDocument();
The above requires some content for the element one is testing, so works for:
<div>
<p>Name: <strong>Bob</strong> <em>(special guest)</em></p>
</div>
...but not if <p> has no text content of its own:
<div>
<p><em>Name: </em><strong>Bob</strong><em> (special guest)</em></p>
</div>
So, for a generic solution the other answers are surely better.

The other answers ended up in type errors or non-functional code at all. This worked for me.
Note: I'm using screen.* here
import React from 'react';
import { screen } from '#testing-library/react';
/**
* Preparation: generic function for markup
* matching which allows a customized
* /query/ function.
**/
namespace Helper {
type Query = (f: MatcherFunction) => HTMLElement
export const byTextWithMarkup = (query: Query, textWithMarkup: string) => {
return query((_: string, node: Element | null) => {
const hasText = (node: Element | null) => !!(node?.textContent === textWithMarkup);
const childrenDontHaveText = node ? Array.from(node.children).every(
child => !hasText(child as Element)
) : false;
return hasText(node) && childrenDontHaveText
})}
}
/**
* Functions you use in your test code.
**/
export class Jest {
static getByTextWithMarkup = (textWithMarkup: string) => Helper.byTextWithMarkup(screen.getByText, textWithMarkup);
static queryByTextWith = (textWithMarkup: string) => Helper.byTextWithMarkup(screen.queryByText, textWithMarkup);
}
Usage:
Jest.getByTextWithMarkup("hello world");
Jest.queryByTextWithMarkup("hello world");

Now you can use the 'toHaveTextContent' method for matching text with substrings or markup
for example
const { container } = render(
<Card name="Perro Loko" age="22" />,
);
expect(container).toHaveTextContent('Name: Perro Loko Age: 22');

getByText('Hello World'); // full string match
getByText('llo Worl', { exact: false }); // substring match
getByText('hello world', { exact: false }); // ignore case-sensitivity
source: https://testing-library.com/docs/react-testing-library/cheatsheet/#queries

Related

React createProtal called outsite a JSX component not updating the DOM

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?
React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

React wrap a word inside a React component

I have a text such as
"This is my text Albert is my special word"
also I have a react component called SpecialWord I want to search through the text above and wrap all the special words in this case (Albert) with my SpecialWord Component
so the text above will output to something like this
This is my text <SpecialWord>Albert</SpecialWord> is my special word
after that I want to render the final result as a react component so something like this if possible
<div dangerouslySetInnerHTML={{__html: finalResult}}></div>
I already tried to get the start and end index of the special word and wrapped it with native html elements, when rendered it works fine since they are native html elements but I can't use the same approach with the SpecialWord component since it's not a native html elements
I would avoid using dangerouslySetInnerHTML.
You can do something like:
function SpecialWord({ children }) {
return <strong>{children}</strong>;
}
function Component() {
const segmenter = new Intl.Segmenter([], {
granularity: 'word'
});
const parts = Array.from(segmenter.segment("This is my text Albert is my special word")).map(part => part.segment);
return parts.map((part, index) => {
if (part === "Albert") {
return <SpecialWord key={index}>Albert</SpecialWord>;
}
return part;
})
}
ReactDOM.render(
<Component />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
A couple of notes:
You need a way to split the sentence in an array, so that you can use map over the words of the sentence and output a React component for the special word. I used Intl.Segmenter which is fairly new and might not be supported on older browsers. Also, you might want to match things like "special word". Intl.Segmenter splits in words so you won't be able to match multiple words.
I used key={index}, using an index as key is often a mistake. In this case it works because we don't have a unique ID for each word.
Instead of passing the special word as child, it's better to use a property for this: <SpecialWord word={part} /> (React children are kinda hard to work with)
I hope this helps!
I solved the problem by some splitting and recombining if anyone were interested in the solution here's a sandbox
https://codesandbox.io/s/amazing-surf-uor8sd?file=/src/pages/index.js
import { useEffect } from "react";
import { useState } from "react";
const text = "hello albert is my special word";
export default function Ex() {
const [result, setResult] = useState([]);
const [specialWords, setSpecialWords] = useState([
{
word: "albert",
data: {
explination: "albert is a special word",
},
},
]);
useEffect(() => {
const arrayOfWords = text.split(" ");
let result = arrayOfWords.map((word) => {
const specialWord = specialWords.find(
(specialWord) => specialWord.word === word
);
if (specialWord) {
return <SpecialWord data={specialWord} key={specialWord.word} />;
}
return word;
});
// the results array look someting like this:
// ['word', 'word', <SpecialWord />, 'word', 'word']
// we need to join as much string as possible to make the result look like this:
// ['word word', <SpecialWord />, 'word word']
// this will improve the performance of the rendering
let joinedResult = [];
let currentString = "";
for (let i = 0; i < result.length; i++) {
const word = result[i];
if (typeof word === "string") {
currentString += word + " ";
} else {
joinedResult.push(currentString);
joinedResult.push(word);
currentString = "";
}
}
joinedResult.push(" " + currentString);
setResult(joinedResult);
}, []);
return (
<div>
{result.map((word) => {
return word;
})}
</div>
);
}
function SpecialWord({ data }) {
return <span style={{ color: "red" }}>{data.word}</span>;
}
You can put your text in an array and use map to return the words wrapped with the correct wrapper, for example:
const message = 'Hello world'
const special = "world"
const result = message.split(" ").map(w =>{
if(special.includes(w)) return <SpecialWord>Albert</SpecialWord>
else return w
})

React ref that depends by an element's reference does not get passed to the child components

The following code creates an object ref that's called editor, but as you see it depends by the contentDiv element that's a ref to a HTMLElement. After the editor object is created it needs to be passed to the TabularRibbon. The problem is that the editor is always null in tabular component. Even if I add a conditional contentDiv?.current, in front of this, it still remains null...
Anyone has any idea?
export const Editor = () => {
let contentDiv = useRef<HTMLDivElement>(null);
let editor = useRef<Editor>();
useEffect(() => {
let options: EditorOptions = { };
editor.current = new Editor(contentDiv.current, options);
return () => {
editor.current.dispose();
}
}, [contentDiv?.current])
return (
<div >
<TabularRibbon
editor={editor.current}
/>
<div ref={contentDiv} />
..........

How to call react function from external JavaScript file

I have read this post [ https://brettdewoody.com/accessing-component-methods-and-state-from-outside-react/ ]
But I don't understand.
that is not working on my source code.
it's my tsx file
declare global {
interface Window {
fn_test(): void;
childComponent: HTMLDivElement; <-- what the... ref type????
}
}
export default function Contact(): React.ReactElement {
....
function file_input_html( i:number ): React.ReactElement {
return (
<form id={`frm_write_file_${i}`} .... </form>
)
}
....
return (
<div ref={(childComponent) => {window.childComponent = childComponent}}>
....
)
it's my external javascript file
function fn_test(){
window.childComponent.file_input_html(3)
var element = document.getElementById("ifrm_write_file");
// element.value = "mystyle";
}
How can i call file_input_html function?
plase help me ...
You have some logic here that doesn't completely make sense.
In your class, you define file_input_html, which returns a component.
Then, in fn_test, you call attempt to call that function (which doesn't work -- I'll address that in a minute), but you don't do anything with the output.
The article that you linked to tells you how to get a ref to a component (eg the div in this case) -- not the actual Contact, which doesn't have a property named file_input_html anyway -- that's just a function you declared inside its scope.
What I'm assuming you want to happen (based on the code you shared) is for your external javascript file to be able to tell your component to render a form with a certain ID and then be able to get a reference to it. Here's an example of how to do this (it's a little convoluted, but it's a funny situation):
const { useState } = React
const App = (props) => {
const [formId, setFormId] = useState(2)
useEffect(() => {
window.alterFormId = setFormId
},[])
return (<div id={"form" + formId} ref={(ourComponent) => {window.ourComponent = ourComponent}}>
Text {formId}
</div>);
}
setTimeout(() => {
window.alterFormId(8);
setTimeout(() => {
console.log(window.ourComponent)
window.ourComponent.innerText = "Test"
}, 20)
}, 1000)
ReactDOM.render(<App />,
document.getElementById("root"))
What's happening here:
In useEffect, I set alterFormId on window so that it can be used outside of the React files
Using the technique you linked to, I get a ref to the div that's created. Note that I'm setting the ID here as well, based on the state of formId
The setTimeout function at the end tests all this:
a) It waits until the first render (the first setTimeout) and then calls alterFormId
b) Then, it waits again (just 20ms) so that the next run loop has happened and the React component has re-rendered, with the new formId and reference
c) From there, it calls a method on the div just to prove that the reference works.
I'm not exactly sure of your use case for all this and there are probably easier ways to architect things to avoid these issues, but this should get you started.
안녕하세요. 자바스크립트로 흐름만 알려드리겠습니다
아래 코드들을 참고해보세요.
iframe간 통신은
window.postMessage API와
window.addEventListener('message', handler) 메시지 수신 이벤트 리스너 로 구현할 수있습니다. 보안관련해서도 방어로직이 몇줄 필요합니다(origin 체크 등)
in parent
import React from 'react';
export function Parent () {
const childRef = React.useRef(null);
const handleMessage = (ev) => {
// 방어로직들
if (check ev.origin, check ev.source, ....) {
return false;
}
console.log('handleMessage(parent)', ev)
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<iframe ref="childRef" src="child_src" id="iframe"></iframe>
</div>
)
}
in child
import React from 'react';
export function Iframe () {
const handleMessage = (ev) => {
console.log('handleMessage(child)', ev)
}
const messagePush = () => {
window.parent.postMessage({ foo: 'bar' }, 'host:port')
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<button onClick={messagePush}>Push message</button>
</div>
)
}

Can i search about some files stored in google bucket inside my react app?

I have a reaction app that stores some files in the google cloud " Bucket " so I wonder if I can search for some files stored in a 'Bucket' inside my React app which i don't know what is the exact name of it, Can I do that?
If yes, in what way?
if you have any tutorial, i will be appreciate.
What i mean by search is this list and filter:
thanks in advance.
What do you mean "search"? If you already know the name you want to find, you can try to open the file. If it fails, it either doesn't exist or you don't have permission to open it.
If you want to see if it exists before opening, this should point you in the right direction:
from google.cloud import storage
client = storage.Client()
blobs = client.list_blobs('your_default_bucket')
filenames = []
for blob in blobs:
filenames.append(blob.name)
print(filenames)
file_exists = 'my_file.csv' in filenames
print(f"file_exists: {file_exists}")
For this kind of cases it's better to use 3rd part libraries. One that could suit your need is react-autosuggest.
basic usage:
import Autosuggest from 'react-autosuggest';
// Imagine you have a list of languages that you'd like to autosuggest.
const files = [
{
name: 'file1'
},
{
name: 'file2'
},
...
];
// Teach Autosuggest how to calculate suggestions for any given input value.
const getSuggestions = value => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
return inputLength === 0 ? [] : languages.filter(lang =>
lang.name.toLowerCase().slice(0, inputLength) === inputValue
);
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
const getSuggestionValue = suggestion => suggestion.name;
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}
</div>
);
class Example extends React.Component {
constructor() {
super();
// Autosuggest is a controlled component.
// This means that you need to provide an input value
// and an onChange handler that updates this value (see below).
// Suggestions also need to be provided to the Autosuggest,
// and they are initially empty because the Autosuggest is closed.
this.state = {
value: '',
suggestions: []
};
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: 'Type a programming language',
value,
onChange: this.onChange
};
// Finally, render it!
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
);
}
}
check a demo here also

Resources