Jest test for a copy to clipboard method using react with typescript - reactjs

I am trying to ensure that the right value is copied to the users clipboard when they click a button. This is my copy method. I am using a ref on the input to access the right value.
protected copyToClipboard() {
console.log("clicked!");
const text = this.controls.copyData;
if (!_.isNil(text)) {
text.current.focus();
text.current.select();
document.execCommand("copy");
this.setState({copied: true});
}
}
For my test:
test("Ensure right value is copied to clipboard", () => {
const wrapper = mount(<MyComponent />);
const copyButton = wrapper.find(".copyBtn");
copyButton.simulate("click");
const copyToClipboardSpy = jest.spyOn(document as any, "execCommand");
wrapper.update();
expect(copyToClipboardSpy).toHaveBeenCalledWith("copy");
});
The error I receive when I run the test is TypeError: document.execCommand is not a function which makes sense, but I am unsure how to approach this.
I am relatively new to testing, just to put that out there. I also have read that I may not be able to access the document.execCommand but have struggled to find a good alternative to hijack the test and access the value being copied. I appreciate any advice that can be given on the matter!

Posting this in case anyone else was in a similar boat. It's doesn't necessarily check the value yet, but one piece I managed was with the document.execCommand method.
I set up a mock function above the wrapper:
document.execCommand = jest.fn();
With this, the test stopped throwing the TypeError. Then my expectations included checking for the spy to have been called, expect my copy state to have changed to true, and:
expect(document.execCommand).toHaveBeenCalledWith("copy");
Test passes! A possible solution for the value is to see if I can "paste" the value and then check it. Will edit this response if/when I can manage that

When you use navigator.clipBoard.writeText instead of using document.exec("copy"), you can refer to this thread for an elegant solution that lets you assert on the content as well.

Being execCommand no longer an option as it is deprecated (see MDN), you should be using navigator.clipboard.writeText('your copied data');.
To mock navigator.clipboard, you could do the following:
// It's important to keep a copy, so your tests don't bleed
const originalClipboard = navigator.clipboard;
const mockedWriteText = jest.fn();
navigator.clipboard = {
writeText: mockedWriteText,
};
const copyComponent = await screen.findByTestId('copy-component');
await userEvent.click(copyComponent);
expect(mockedWriteText).toHaveBeenCalledTimes(1);
expect(mockedWriteText).toHaveBeenCalledWith('your copied data');
// Remember to restore the original clipboard
navigator.clipboard = originalClipboard;
jest.resetAllMocks();
You can also do Object.assignProperty instead of directly modifying the navigator object.
This snippet assumes you are using React Testing Library with User Event.

Related

Jest doesn't recognize FileSystemFileHandle createWritable() method

In my code, I am using the relatively new interface of FileSystemFileHandle (documentation here: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createWritable)
In the documentation, it shows you can just call the createWritable() method, and React knows what to do with it. This would be correct, and my code works. However, when trying to create a test for my new method, Jest thinks createWritable() is not a function. This makes sense, since I didn't have to import any library to use this method.
How can I test my code using createWritable(), or is there any workaround so that the test at least passes?
//code example:
this.state = {
handleFile: []
};
async fileWriter({ data }) {
//request writable stream
const writer = await this.state.handleFile.createWritable();
await writer.write(new Blob([data])); // write the Blob directly
await writer.close(); // end writing
}

Handling OAuth with React 18 useEffect hook running twice

Background
I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.
I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.
The issue
Essentially the issue I have run into is I am implementing OAuth integration with a third-party product. The flow:
-> User clicks create integration -> Redirect to product login -> Gets redirected back to our app with authorisation code -> We hit our API to finalise the integration (HTTP POST request)
The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.
This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.
Considered solutions
Refactoring to use a button
I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).
The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.
Ignore the duplicate API call
This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further
Code setup
I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.
const IntegrationRedirect: React.FC = () => {
const navigate = useNavigate();
const organisationIntegrationsService = useOrganisationIntegrationsService();
// Make call on the mount of this component
useEffect(() => {
// Call the method
handleCreateIntegration();
}, []);
const handleCreateIntegration = async (): Promise<void> => {
// Setup request
const request: ICreateIntegration = {
authorisationCode: ''
};
try {
// Make service call
const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);
// Handle error
if (setupIntegrationResponse.data.errors) {
throw 'Failed to setup integrations';
}
// Navigate away on success
routes.organisation.integrations.navigate(navigate);
}
catch (error) {
// Handle error
}
};
return ();
};
What I am after
I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.
You can use the useRef() together with useEffect() for a workaround
const effectRan = useRef(false)
useEffect(() => {
if (effectRan.current === false) {
// do the async data fetch here
handleCreateIntegration();
}
//cleanup function
return () => {
effectRan.current = true // this will be set to true on the initial unmount
}
}, []);
This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

session.subscribe throws error when called in onMount

<script>
import {onMount} from 'svelte';
import {session} from "$app/stores"
import {writable} from 'svelte/store';
const store = writable('some value');
let value = null
onMount(() => {
// this works
// return store.subscribe( (storeValue) => {value = storeValue}); // this works
// this throws an error:
// return session.subscribe( (sessionValue) => {value = sessionValue});
// Uncaught (in promise) Error: Function called outside component initialization
});
</script>
can someone please explain to me the problem with session.subscribe and why it keeps throwing?
if I move session.subscribe outside onMount it runs fine.
Note: this code is part of a SvelteKit Project, inside a Svelte component, not a SvelteKit page/route.
What goes wrong
It seems that you are actually experiencing intended behaviour. Under the documentation for $app/stores you will find this:
Stores are contextual — they are added to the context of your root component. This means that session and page are unique to each request on the server, rather than shared between multiple requests handled by the same server simultaneously, which is what makes it safe to include user-specific data in session.
Because of that, you must subscribe to the stores during component initialization (which happens automatically if you reference the store value, e.g. as $page, in a component) before you can use them.
When you were attempting this, you probably got a callstack that looks something like this:
Error: Function called outside component initialization
at get_current_component (index.mjs:953:15)
at getContext (index.mjs:989:12) <----------Here is the problem
at getStores (stores.js:19:17)
at Object.subscribe (stores.js:70:17)
at index.svelte:10:13
at run (index.mjs:18:12)
at Array.map (<anonymous>)
at index.mjs:1816:45
at flush (index.mjs:1075:17)
at init (index.mjs:1908:9)
We can see that Svelte attempts to call getContext when you subscribe to the session. Calling getContext outside of the component root is not allowed, which causes the subscription to fail.
I agree that this is quite unintuitive and I am not really sure why they implemented it this way.
Workaround
By the way, are you really sure you only want to subscribe to session on mount? What are you trying to do?
If you really only want to subscribe to session after component mount, you could use this workaround: Create your own store that updates whenever the session changes, then listen to that.
<script>
import { onMount } from "svelte";
import { session } from "$app/stores";
import { writable } from "svelte/store";
let mySession = writable($session);
$: $mySession = $session;
onMount(()=>{
mySession.subscribe(...whatever...);
})
</script>

How do I correct official React.js testing recipe to work for React/Jest/Fetch asynchronous testing?

I am trying to learn the simplest way to mock fetch with jest. I am trying to start with the official React docs recipe for fetch but it doesnt actually work without some modification.
My aim is to use Jest and (only) native React to wait for component rendering to complete with useEffect[].fetch() call before running assertions and testing initialisation worked.
I have imported the Data fetching recipe from official docs:
https://reactjs.org/docs/testing-recipes.html#data-fetching
into codesandbox here
https://codesandbox.io/s/jest-data-fetching-ov8og
Result: test fail
expect(received).toContain(expected) // indexOf
Expected substring: "123, Charming Avenue"
Received string: "loading..."
Possible cause?: I suspect its failing because global.fetch is not being used in the component which appears to be using real fetch hence is stuck "loading".
UPDATE
I have managed to make the test work through trial and error. Changed the call to fetch in the actual component to global.fetch() to match the test. This is not desirable for working code to refer to global prefix everywhere fetch is used and is also not in the example code.
e.g.
export default function User(props) {
const [user, setUser] = useState(null);
async function fetchUserData(id) {
// this doesnt point to the correct mock function
// const response = await fetch("/" + id);
// this fixes the test by pointing to the correct mock function
const response = await global.fetch("/" + id);
const json = await response.json();
setUser(json);
}
...
any help or advice much appreciated.

Determine which hooks are being called

If I have some state setup with useState, such as:
const [s, setS] = React.useState();
and I want to put some sort of logging on the middleware, for instance, to log when each call to setS is made, I can do something a bit hacky, like this:
const [s, setS_] = React.useState();
const setS = x => {
console.log('setS called with: ');
console.log(x);
setS(x);
}
But this gets a little unwieldy with lots of items of state. Is it possible to do this in a way that's avoids repetition, for instance, establishing some hook on useState?
If you're wondering why I want to do this in the first place, I have some dispatchAction's that are taking a very long time, and I'd like to try and begin debugging these; it's a little difficult as I have no way to know which actions are taking so long.
You can create a useEffect which triggers when s changes. Not sure if this satisfies all of your requirements as it won't be triggered if setS is pass the current state of s, (ie. s = 3 and setS(3) is called)
useEffect(() => {
console.log('s changed to: ', s)
}, [s])
You can wrap the original React.useState function to enable logging, but the main challenge is to figure out who's calling and for what piece of state, because there's no info passed to useState() except for an optional initial value.
How to identify the component / piece of state
One idea is to use Function.caller to identify who's calling, but it doesn't work in strict mode (and depending on how you have your build process set up, it might be enabled automatically).
An alternative is to throw, and capture, an error and look at its stacktrace:
let oldUseState = React.useState;
React.useState = function useState() {
let ctx;
try { throw new Error(); } catch(err) { ctx = err; }
let [val,updater] = oldUseState(...arguments);
return [val, function() {
console.log(arguments, ctx);
return updater(...arguments);
}];
}
Whenever the updater function for a piece of state is called, you'll get a stacktrace in the console, with the added bonus that the list of callers is cross-referenced with your code, so you can click to see what invoked the function.

Resources