dom-testing-library version: 3.16.5react-testing-library version: 5.4.4react version: 16.7.0node version: 8.4.0npm version: 5.3.0EditorContainer.ts - the component i'm testing:
import * as React from 'react';
import ReactHtmlParser from 'react-html-parser';
import { transform, preprocessNodes } from '../../utils/htmltransformer/htmltransformer';
import LoadingFullscreen from '../loading/loading';
import { handleMessage } from '../../actions/editorMsgHandle';
import { connect } from 'react-redux';
import Editor from './editor';
interface Props {
dispatch: any;
html?: string;
}
interface State {
html?: string;
}
/**
* The Editor - will have HTML injected by test page or add listener for window messages
* The HTML will then be parsed by the `htmltransformer`, to inject React components into the template.
*/
class EditorContainer extends React.PureComponent<Props, State> {
static b64DecodeUnicode(str: string): string {
return decodeURIComponent(Array.prototype.map.call(atob(str), c => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
constructor(props: Props) {
super(props);
this.state = {
html: props.html
};
}
onDataReceived = (event: any) => {
if (event.data.html !== undefined) {
const html = EditorContainer.b64DecodeUnicode(event.data.html);
this.props.dispatch(handleMessage(event, event.data.state));
this.setState({ html });
}
}
componentDidMount() {
// Add listener for when HTML isn't being passed as a prop via redux (this only happens when the test page uploads some html)
if (!this.state.html) {
window.addEventListener('message', this.onDataReceived);
console.log('Added listener, and waiting for window message...');
if (window.opener) {
window.opener.postMessage({ editorIsReady: true }, '*');
}
}
}
componentWillUnmount() {
window.removeEventListener('message', this.onDataReceived);
}
render() {
return (
this.state.html ? (
<LetterEditor template={ReactHtmlParser(this.state.html, { transform, preprocessNodes })}/>
) : <LoadingFullscreen/>
);
}
}
const mapStateToProps = (state: any): object => {
return {
html: state.testHTML
};
};
export default connect(mapStateToProps)(EditorContainer);
import * as React from 'react';
import { render, waitForElement } from 'react-testing-library';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import Editor from '../editor.container';
import rootReducer from '../../../reducers/';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import lightTheme from '../../../theme';
import * as fs from 'fs';
import * as path from 'path';
describe('editor.container', () => {
let testHTML = '';
beforeAll((done: Function) => {
fs.readFile(path.resolve(__dirname, '../../../../public/testHTML.html'), 'utf-8', (_, data: string) => {
testHTML = data;
done();
});
});
function b64EncodeUnicode(str: string) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1: string) => {
return String.fromCharCode(parseInt(p1, 16));
}));
}
const renderComponent = (initialState: object = {}) => render(
<Provider store={createStore(rootReducer, initialState)}>
<MuiThemeProvider muiTheme={lightTheme}>
<LetterEditor />
</MuiThemeProvider>
</Provider>
);
// Works
it('should display loading screen on initial render', () => {
const { getByText } = renderComponent();
expect(getByText('Loading')).toBeInTheDocument();
});
// Works
it('should display editor when html is injected via redux (test-portal does this)', async () => {
const { getByText } = renderComponent({ testHTML });
const title = await waitForElement(() => getByText('Some title that exists in the template'));
expect(title).toBeInTheDocument();
});
// Does not work!
it('should wait for input when booted and load in html on postMessage (system-integration does this)', async () => {
const { getByText } = renderComponent();
expect(getByText('Loading')).toBeInTheDocument();
const html = b64EncodeUnicode(testHTML);
window.postMessage({ html }, '*');
const title = await waitForElement(() => getByText('Some title that exists in the template'));
expect(title).toBeInTheDocument();
});
});
Test 3 fails, outputting that it could not find "Some title that exists in the template (anonymised here)" in:
<body>
<div />
<style>
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
</style>
</body>
In test 2 i'm able to simulate how the component acts when it gets the html from the redux store.
But when i run test 3 i get some kind of race condition, even though i use waitForElement. Doing console logs i can see that the message listener is invoked in the component, and that render is called again displaying the editor and not the loading screen. I'm also sometimes able to view the correct dom that is to be displayed using a mix of debug()s and queryByText.
I've also tried to use fireEvent on window, but could not get that to trigger the listener (don't know if it works for window?).
@kentcdodds any idea about this, or want me to provide an example sandbox? :smile:
or want me to provide an example sandbox? 馃槃
Always
I was not able to replicate this in a sandbox. Though i don't use redux in the sandbor nor material ui and other libraries. I think i need to investigate how my rendering is exactly occuring.
https://codesandbox.io/s/244870l2r0
The only difference i see between my two tests is that in the test that fails, the Editor is mounted because it haven't received any data yet (either via props or on event), and then receives data where it calls render. And in the other test where it passes, it is not mounted until rendering is done because it get's data passed in as a prop.
Did you ever figure this one out? You may be better off asking this on spectrum: https://spectrum.chat/react-testing-library
No, unfortunately not. The method worked in the sandbox, and investigating my application and render tree did not reveal any sideeffects that could have caused this issue.
@jakoandersen In your original post, it looks like you were using window.postMessage:
// Does not work!
it('should wait for input when booted and load in html on postMessage (system-integration does this)', async () => {
const { getByText } = renderComponent();
expect(getByText('Loading')).toBeInTheDocument();
const html = b64EncodeUnicode(testHTML);
window.postMessage({ html }, '*');
const title = await waitForElement(() => getByText('Some title that exists in the template'));
expect(title).toBeInTheDocument();
});
While in the sandbox, you were using fireEvent:
it("testing with fireEvent", async () => {
expect.extend({ toBeInTheDocument });
const { getByText } = render(<MyComponent />);
expect(getByText("Nothing yet")).toBeInTheDocument();
fireEvent(window, new Event("message", { myValue: "blabla" }));
const myValue = await waitForElement(() => getByText("Got value blabla"));
expect(myValue).toBeInTheDocument();
});
This could be the reason why the sandbox was working for you. I've had success firing a MessageEvent to simulate postMessage in a test. This works for me, and the data is coming through in the event like it should:
fireEvent(window, new MessageEvent('message', { data: { type: 'my-event' } }))
Thanks for the fireEvent tip, works!
If you additionally have logic in your message handler in you application that checks for the origin of the message, this snippet helps:
beforeEach(() => {
delete window.origin
window.origin = 'https://www.test.com/'
})
And when sending the event, make sure to set the origin as well:
fireEvent(window, new MessageEvent('message', { data: { type: 'my-event' }, origin: 'https://www.test.com/' }))
Most helpful comment
@jakoandersen In your original post, it looks like you were using
window.postMessage:While in the sandbox, you were using
fireEvent:This could be the reason why the sandbox was working for you. I've had success firing a
MessageEventto simulatepostMessagein a test. This works for me, and the data is coming through in the event like it should: