Can a custom command overload be done the same way as a function overload?
There is no answer to this in the documentation.
For example:
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apisPath: string[], timeout?: number) => {
let options = {};
if (timeout !== undefined) {
options = { timeout: TIMEOUT };
}
apisPath.forEach((api)=> {
cy.intercept(`/api/${api}`).as(api);
})
cy.visit(`/${relativePath}`);
cy.wait(apisPath.map(apiPath => `#${apiPath}`), options);
});
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apiPath: string, timeout?: number) => {
cy.navigateAndWaitForApi(relativePath, [apiPath], timeout);
});
It does not appear so. The command name navigateAndWaitForApi is the total signature.
Add this after the command definitions
console.log(Cypress.Commands._commands)
shows commands are stored in an object, keyed by the command name.
Adding the same command twice, the second overwrites the first.
It's possible to type-check the params at runtime.
Cypress.Commands.add('navigateAndWaitForApi',
(relativePath: string, apiPaths: string|string[], timeout?: number) => {
if (typeof apiPaths === 'string') {
apiPaths = [apiPaths]
}
let options = {};
if (timeout !== undefined) {
options = { timeout: TIMEOUT };
}
apiPaths.forEach((api)=> {
cy.intercept(`/api/${api}`).as(api);
})
cy.visit(`/${relativePath}`);
// cy.wait(apiPaths.map(apiPath => `#${apiPath}`), options); // look dubious
apiPaths.forEach(apiPath => {
cy.wait(`#${apiPath}`), options)
})
});
Related
I try to use cypress-wait-until for a simple case. https://github.com/NoriSte/cypress-wait-until
Visit a page
Check if an element is here
If not, reload the page until this element is found.
Once found, assert the element exists
Working code (cypress-wait-until not used)
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const selector = 'button[data-test=startRegistration-individual-button]';
cy.get(selector).should('exist');
});
});
Not working, timed out retrying
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const options = { timeout: 8000, interval: 4000 };
const selector = 'button[data-test=startRegistration-individual-button]';
cy.waitUntil(() => cy.reload().then(() => Cypress.$(selector).length), options);
cy.get(selector).should('exist');
});
});
Not working, see error below
before(() => {
cy.visit('http://localhost:8080/en/registration');
});
describe('Foo', () => {
it('should check that registration button is displayed', () => {
const options = { timeout: 8000, interval: 4000 };
const selector = 'button[data-test=startRegistration-individual-button]';
cy.waitUntil(() => {
cy.reload();
return Cypress.$(selector).length;
}, options);
cy.get(selector).should('exist');
});
For the two versions not working as soon as I remove cy.reload(), it starts to work.
Question
What can I do to make it work with a reload?
EDIT
This command I wrote works correctly.
Cypress.Commands.add('refreshUntil', (selector: string, opts?: { retries: number; waitAfterRefresh: number }) => {
const defaultOptions = {
retries: 10,
waitAfterRefresh: 2500,
};
const options = { ...defaultOptions, ...opts };
function check(selector: string): any {
if (Cypress.$(selector).length) { // Element is there
return true;
}
if (options.retries === 0) {
throw Error(`${selector} not found`);
}
options.retries -= 1;
cy.log(`Element ${selector} not found. Remaining attempts: ${options.retries}`);
cy.reload();
// Wait a some time for the server to respond
return cy.wait(options.waitAfterRefresh).then(() => check(selector));
}
check(parsedSelector);
});
I could see two potential difference with waitUntil from cypress-wait-until
Cypress.$(selector).length would be new on each try
There is a wait time after the reload before checking again if the element is there
EDIT 2
Here is the working solution using cypress-wait-until
cy.waitUntil(() => cy.reload().wait(2500).then(() => Cypress.$(selector).length), options);
Cypress rules apply inside cy.waitUntil() as well as outside so .wait(2500) would be bad practice.
It would be better to change your non-retying Cypress.$(selector).length into a proper Cypress retrying command. That way you get 4 seconds (default) retry but only wait as long as needed.
Particulary since cy.waitUntil() repeats n times, you are hard-waiting (wasting) a lot of seconds.
cy.waitUntil(() => { cy.reload(); cy.get(selector) }, options)
// NB retry period for `cy.get()` is > 2.5 seconds
Here is the working solution using cypress-wait-until
cy.waitUntil(() => cy.reload().wait(2500).then(() => Cypress.$(selector).length), options);
I ended up writing my own method (inspired from cypress-wait-until ) without the need to have a hard wait time
/**
* Run a check, and then refresh wait until an element is displayed.
* Retries for a specified amount of time.
*
* #function
* #param {function} firstCheckFunction - The function to run before checking if the element is displayed.
* #param {string|{ selector: string, type: string }} selector - The selector to search for. Can be a string or an object with selector and type properties.
* #param {WaitUntilOptions} [opts={timeout: 5000, interval: 500}] - The options object, with timeout and interval properties.
* #throws {Error} if the firstWaitFunction parameter is not a function.
* #throws {Error} if the specified element is not found after all retries.
* #example
* cy.refreshUntilDisplayed('#element-id', () => {...});
* cy.refreshUntilDisplayed({ selector: 'element-id', type: 'div' }, () => {...});
*/
Cypress.Commands.add('waitFirstRefreshUntilDisplayed', (firstCheckFunction, selector: string | { selector: string, type: string }, opts = {}) => {
if (!(firstCheckFunction instanceof Function)) {
throw new Error(`\`firstCheckFunction\` parameter should be a function. Found: ${firstCheckFunction}`);
}
let parsedSelector = '';
// Define the default options for the underlying `cy.wait` command
const defaultOptions = {
timeout: 5000,
interval: 500,
};
const options = { ...defaultOptions, ...opts };
// Calculate the number of retries to wait for the element to be displayed
let retries = Math.floor(options.timeout / options.interval);
const totalRetries = retries;
if (typeof selector === 'string') {
parsedSelector = selector;
}
if (typeof selector !== 'string' && selector.selector && selector.type) {
parsedSelector = `${selector.type}[data-test=${selector.selector}]`;
}
// Define the check function that will be called recursively until the element is displayed
function check(selector: string): boolean {
if (Cypress.$(selector).length) { // Element exists
return true;
}
if (retries < 1) {
throw Error(`${selector} not found`);
}
if (totalRetries !== retries) { // we don't reload first time
cy.log(`Element ${parsedSelector} not found. ${retries} left`);
cy.reload();
}
// Waits for the firstCheckFunction to return true,
// then pause for the time define in options.interval
// and call recursively the check function
cy.waitUntil(firstCheckFunction, options).then(() => { // We first for firstCheckFunction to be true
cy.wait(options.interval).then(() => { // Then we loop until the selector is displayed
retries -= 1;
return check(selector);
});
});
return false;
}
check(parsedSelector);
});
I have this app that I'm working on that is using RTK and in the documentation for selecting values from results, in queries using RTK Query, they have an example with a createSelector and React.useMemo. Here's that code and the page
import { createSelector } from '#reduxjs/toolkit'
import { selectUserById } from '../users/usersSlice'
import { useGetPostsQuery } from '../api/apiSlice'
export const UserPage = ({ match }) => {
const { userId } = match.params
const user = useSelector(state => selectUserById(state, userId))
const selectPostsForUser = useMemo(() => {
const emptyArray = []
// Return a unique selector instance for this page so that
// the filtered results are correctly memoized
return createSelector(
res => res.data,
(res, userId) => userId,
(data, userId) => data?.filter(post => post.user === userId) ?? emptyArray
)
}, [])
// Use the same posts query, but extract only part of its data
const { postsForUser } = useGetPostsQuery(undefined, {
selectFromResult: result => ({
// We can optionally include the other metadata fields from the result here
...result,
// Include a field called `postsForUser` in the hook result object,
// which will be a filtered list of posts
postsForUser: selectPostsForUser(result, userId)
})
})
// omit rendering logic
}
So I did the same in my app, but I thought that if it's using the createSelector then it can be in a separate slice file. So I have this code in a slice file:
export const selectFoo = createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
(result: { data?: TypeOne[] }, status: TypeTwo) => status,
],
(data: TypeOne[] | undefined, status) => {
return data?.filter((d) => d.status === status) ?? [];
}
);
Then I created a hook that uses this selector so that I can just pass in a status value and get the filtered results. This is in another file as well.
function useGetFooByStatus(status: WebBookmkarkStatus) {
const selectFooMemoized = useMemo(() => {
return selectFoo;
}, []);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result, status),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
Then lastly I'm using this hook in several places in the app.
The problem then is when I'm causing a re-render in another part of the app causes the query hook to run again (I think), but the selector function runs again, not returning the memoized value, even though nothing has changed. I haven't really figured it out what causes the re-render in another part of the app, but when I do the following step, it stops re-rendering.
If I replace the selector function in the useGetFooByStatus with the same one in the slice file. With this, the value is memoized correctly.
(Just to remove any doubt, the hook would look like this)
function useGetFooByStatus(status: TypeTwo) {
const selectFooMemoized = useMemo(() => {
return createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
(result: { data?: TypeOne[] }, status: TypeTwo) =>
status,
],
(data: TypeOne[] | undefined, status) =>
data?.filter((d) => d.status === status) ?? []
);
}, []);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result, status),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
Sorry for the long question, just want to try and explain everything :)
Solution 1 has one selector used for your whole app. That selector has a cache size of 1, so if you call it always with the same argument it will not recalculate, but if you call it with 1 and then with 2 and then with 1 and then with 2 it will always recalculate in-between and always return a different (new object) as a result.
Solution 2 creates one such selector per component instance.
Now imagine two different components calling these selectors - with two different queries with two different results.
Solution 1 will flip-flop and always create a new result - solution 2 will stay stable "per-component" and not cause rerenders.
Does the following work:
const EMPTY = [];
const createSelectFoo = (status: TypeTwo) => createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
],
(data: TypeOne[] | undefined) => {
return data?.filter((d) => d.status === status) ? EMPTY;
}
);
function useGetFooByStatus(status: TypeTwo) {
//only create selector if status changes, this will
// memoize the result when multiple components
// call this hook with different status in one render
// cycle
const selectFooMemoized = useMemo(() => {
return createSelectFoo(status);
}, [status]);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
You may want to make your component a pure component with React.memo, some more information with examples of selectors can be found here
Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future
I have a working subscription that uses withFilter:
User_Presence_Subscription: {
subscribe: withFilter(
() => pubsub.asyncIterator(USER_PRESENCE_UPDATED_CHANNEL),
(payload, args, context) => {
if (typeof (payload) === 'undefined') {
return false;
}
const localUserId = (typeof(context) == 'undefined' || typeof(context.userId) == 'undefined') ? null : context.userId;
const ids_to_watch = args.ids_to_watch;
const usersWithUpdatedPresence = payload.User_Presence_Subscription;
let result = false;
console.log("User_Presence_Subscription - args == ", args, result);
return result;
}
)
}
I'd like to modify the payload before sending it to the client. I tried adding a resolve function as shown in the docs:
User_Presence_Subscription: {
resolve: (payload, args, context) => {
debugger; <== NEVER ACTIVATES
return {
User_Presence_Subscription: payload,
};
},
subscribe: withFilter(
() => pubsub.asyncIterator(USER_PRESENCE_UPDATED_CHANNEL),
(payload, args, context) => {
if (typeof (payload) === 'undefined') {
return false;
}
const localUserId = (typeof(context) == 'undefined' || typeof(context.userId) == 'undefined') ? null : context.userId;
const ids_to_watch = args.ids_to_watch;
const usersWithUpdatedPresence = payload.User_Presence_Subscription;
let result = false;
console.log("User_Presence_Subscription - args == ", args, result);
return result;
}
)
}
...but the debugger line in the resolve function never gets hit.
What's the correct syntax to use here?
Solved. The only reason the resolver wasn't being hit was that in my test code I was returning false from the withFilter function. When it returns true the resolver is hit as expected.
Currently, I have a function that sometimes return an object with some functions inside. When using expect(...).toEqual({...}) it doesn't seem to match those complex objects. Objects having functions or the File class (from input type file), it just can't. How to overcome this?
Try the Underscore _.isEqual() function:
expect(_.isEqual(obj1, obj2)).toEqual(true);
If that works, you could create a custom matcher:
this.addMatchers({
toDeepEqual: function(expected) {
return _.isEqual(this.actual, expected);
};
});
You can then write specs like the following:
expect(some_obj).toDeepEqual(expected_obj);
As Vlad Magdalin pointed out in the comments, making the object to a JSON string, it can be as deep as it is, and functions and File/FileList class. Of course, instead of toString() on the function, it could just be called 'Function'
function replacer(k, v) {
if (typeof v === 'function') {
v = v.toString();
} else if (window['File'] && v instanceof File) {
v = '[File]';
} else if (window['FileList'] && v instanceof FileList) {
v = '[FileList]';
}
return v;
}
beforeEach(function(){
this.addMatchers({
toBeJsonEqual: function(expected){
var one = JSON.stringify(this.actual, replacer).replace(/(\\t|\\n)/g,''),
two = JSON.stringify(expected, replacer).replace(/(\\t|\\n)/g,'');
return one === two;
}
});
});
expect(obj).toBeJsonEqual(obj2);
If anyone is using node.js like myself, the following method is what I use in my Jasmine tests when I am only concerned with comparing the simple properties while ignoring all functions. This method requires json-stable-stringify which is used to sort the object properties prior to serializing.
Usage:
var stringify = require('json-stable-stringify');
var obj1 = {
func: function() {
},
str1: 'str1 value',
str2: 'str2 value',
nest1: {
nest2: {
val1:'value 1',
val2:'value 2',
someOtherFunc: function() {
}
}
}
};
var obj2 = {
str2: 'str2 value',
str1: 'str1 value',
func: function() {
},
nest1: {
nest2: {
otherFunc: function() {
},
val2:'value 2',
val1:'value 1'
}
}
};
it('should compare object properties', function () {
expect(stringify(obj1)).toEqual(stringify(obj2));
});
Extending #Vlad Magdalin's answer, this worked in Jasmine 2:
http://jasmine.github.io/2.0/custom_matcher.html
beforeEach(function() {
jasmine.addMatchers({
toDeepEqual: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var result = {};
result.pass = _.isEqual(actual, expected);
return result;
}
}
}
});
});
If you're using Karma, put that in the startup callback:
callback: function() {
// Add custom Jasmine matchers.
beforeEach(function() {
jasmine.addMatchers({
toDeepEqual: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var result = {};
result.pass = _.isEqual(actual, expected);
return result;
}
}
}
});
});
window.__karma__.start();
});
here's how I did it using the Jasmine 2 syntax.
I created a customMatchers module in ../support/customMatchers.js (I like making modules).
"use strict";
/**
* Custom Jasmine matchers to make unit testing easier.
*/
module.exports = {
// compare two functions.
toBeTheSameFunctionAs: function(util, customEqualityTesters) {
let preProcess = function(func) {
return JSON.stringify(func.toString()).replace(/(\\t|\\n)/g,'');
};
return {
compare: function(actual, expected) {
return {
pass: (preProcess(actual) === preProcess(expected)),
message: 'The functions were not the same'
};
}
};
}
}
Which is then used in my test as follows:
"use strict";
let someExternalFunction = require('../../lib/someExternalFunction');
let thingBeingTested = require('../../lib/thingBeingTested');
let customMatchers = require('../support/customMatchers');
describe('myTests', function() {
beforeEach(function() {
jasmine.addMatchers(customMatchers);
let app = {
use: function() {}
};
spyOn(app, 'use');
thingBeingTested(app);
});
it('calls app.use with the correct function', function() {
expect(app.use.calls.count()).toBe(1);
expect(app.use.calls.argsFor(0)).toBeTheSameFunctionAs(someExternalFunction);
});
});
If you want to compare two objects but ignore their functions, you can use the methods _.isEqualWith together with _.isFunction from lodash as follows.
function ignoreFunctions(objValue, otherValue) {
if (_.isFunction(objValue) && _.isFunction(otherValue)) {
return true;
}
}
it('check object equality but ignore their functions', () => {
...
expect(_.isEqualWith(actualObject, expectedObject, ignoreFunctions)).toBeTrue();
});