// target.js
const urlPrefix = IS_TEST_ENV || _DEV_ ? 'https://xxx/cms/media/' : 'https://zzz/cms/media/';
const formatImageSizeUrl = (url, size) => {
if (!/\d+x\d+/.test(size)) {
return url;
}
if (url.startsWith(urlPrefix)) {
return `${url}?d=${size}`;
}
return url;
};
...
// jest.config.js
module.exports = {
globals: {
IS_TEST_ENV: true,
_DEV_: true
}
}
how to change IS_TEST_ENV and DEV into false temporarily.so can cover all branches
My opinion on this is that your code has some logic that needs mocking, but that logic is implicit and not currently mockable. I would refactor the code as follows:
// settings.js
// Check whether we are on a production environment or not
export const isProd = () => !(IS_TEST_ENV || _DEV_)
// target.js
// Reversed order of URLs as the boolean expression returns true if we are in production
const urlPrefix = isProd() ? 'https://zzz/cms/media/' : 'https://xxx/cms/media/';
// target.test.js
// Mock out isProd() to return true, as by default it will be false
jest.mock('../settings', () => {
isProd: () => true
})
// Run test to ensure we are getting the right URL
The upside to this approach is that you have a single canonical source of whether you're in production or not, which is useful if you wind up needing to make that check in multiple places. You also only need to mock out or update one method if you want to change this behaviour in the future.
Related
I am writing a custom webpack loader to remove unnecessary code that Terser can't pick up.
Here's the sample source output from webpack loader;
const SvgsMap = {
map1: () => {
return '1';
},
map2: () => {
return '2';
},
map3: () => {
return '3';
},
// ...redacted
map100: () => {
return '100';
},
}
Note that above comes into the loader as a string. And I have a whitelist of string[] as which of them that should be included in the build output;
const whitelistsArr = ["map1"]
I am currently writing a webpack loader to pre-process this before getting into bundled. Which currently uses Node VM that I assume could parse it to javascript object, in which then I can remove some of the unused properties in SvgsMap, then output it back again as a string.
My question is;
Am I doing it the right way with Loader to remove them? Or is it actually a webpack plugin job to do this? Any other alternatives?
I am hitting a rock doing this with VM, It seems like it's unable to mutate the existing code and output it back as a string. Am I wrong here?
Any suggestion is appreciated.
Here's my loader's code so far;
const path = require( 'path' );
const { loader } = require( 'webpack' );
const vm = require( 'vm' );
const whitelists = ['frame21Web'];
const loaderFn = function ( source ) {
/** #type {loader.LoaderContext} */
// eslint-disable-next-line babel/no-invalid-this
const self = this;
const filename = path.basename( self.resourcePath );
const templateWithoutLoaders = filename.replace( /^.+!/, '' ).replace( /\?.+$/, '' );
const vmContext = vm.createContext( { module: {} } );
let newSource = '';
try {
const vmScript = new vm.Script( source, { filename: templateWithoutLoaders } );
const cachedData = vmScript.createCachedData();
console.log(cachedData.toString()); // Doesn't seem to output as a string.
}
catch (err) {
console.error(err);
}
console.log( 'loader', filename, source );
process.exit( 0 );
return source;
};
module.exports = loaderFn;
There may be a couple answers to this question. Difficult to know without knowing the reasoning behind the removal.
If you have control of the file, you could use a combination of Webpack's Define plugin, and some if/else logic. For example
// in your WP config file
new webpack.DefinePlugin({
IS_CLIENT: JSON.stringify(true), // will only be true when compiled via WP
});
// in your module
if (process.env.IS_CLIENT) {
SvgsMap.map1 = () => '1';
}
The above pattern allows for adding/removing chunks of code for your Client bundle, while also allowing for use on the Server.
The other option would be to write a custom Babel plugin (not a WP plugin). I found this article helpful in the past, when I had to write some plugins. Babel gives you more control of how the parts of a JS file are processed, and you can use that plugin outside of WP (like while running Unit tests).
I need to use jest.mock together with async, await and import() so that I can use a function from another file inside the mocked module. Otherwise I must copy and paste a few hundreds of slocs or over 1000 slocs, or probably it is not even possible.
An example
This does work well:
jest.mock('./myLin.jsx', () => {
return {
abc: 967,
}
});
Everywhere I use abc later it has 967 as its value, which is different than the original one.
This does not work:
jest.mock('./myLin.jsx', async () => {
return {
abc: 967,
}
});
abc seems to not be available.
Actual issue
I need async to be able to do this:
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
// … here return an object which contains a call to
// blockTagDeserializer declared above; if I can't do this
// I cannot use blockTagDeserializer since it is outside of
// the scope of this function
}
Actual results
I get errors like:
TypeError: Cannot destructure property 'slate' of '((cov_1viq84mfum.s[13]++) , _config.settings)' as it is undefined.
where _config, I think, is the ~/config module object and slate is a property that should be available on _config.settings.
Expected results
No error, blockTagDeserializer works in the mocked module and the unit test is passed.
The unit test code
The code below is a newer not-working code based on this file on GitHub.
import React from 'react';
import renderer from 'react-test-renderer';
import WysiwygWidget from './WysiwygWidget';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-intl-redux';
const mockStore = configureStore();
global.__SERVER__ = true; // eslint-disable-line no-underscore-dangle
global.__CLIENT__ = false; // eslint-disable-line no-underscore-dangle
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
const createEmptyParagraph = () => {
return {
type: 'p',
children: [{ text: '' }],
};
};
return {
settings: {
supportedLanguages: [],
slate: {
elements: {
default: ({ attributes, children }) => (
<p {...attributes}>{children}</p>
),
strong: ({ children }) => {
return <strong>{children}</strong>;
},
},
leafs: {},
defaultBlockType: 'p',
textblockExtensions: [],
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: blockTagDeserializer('strong'),
};
return editor;
},
],
defaultValue: () => {
return [createEmptyParagraph()];
},
},
},
};
});
window.getSelection = () => ({});
test('renders a WysiwygWidget component', () => {
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});
const component = renderer.create(
<Provider store={store}>
<WysiwygWidget
id="qwertyu"
title="My Widget"
description="My little description."
required={true}
value={{ data: 'abc <strong>def</strong>' }}
onChange={(id, data) => {
// console.log('changed', data.data);
// setHtml(data.data);
}}
/>
</Provider>,
);
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
What I've tried
The code snippets above show partially what I have tried.
I searched the web for 'jest mock async await import' and did not found something relevant.
The question
If jest.mock is not made to work with async, what else can I do to make my unit test work?
Update 1
In the last snippet of code above, the line
STRONG: blockTagDeserializer('strong'),
uses blockTagDeserializer defined here which uses deserializeChildren, createEmptyParagraph (which is imported from another module), normalizeBlockNodes (which is imported from another module) and jsx (which is imported from another module) functions, which use deserialize which uses isWhitespace which is imported from another module and typeDeserialize which uses jsx and deserializeChildren.
Without using await import(...) syntax how can I fully mock the module so that my unit test works?
If you want to dig into our code, please note that the volto-slate/ prefix in the import statements is for the src/ folder in the repo.
Thank you.
I'd advise not doing any "heavy" stuff (whatever that means) in a callback of jest.mock, – it is designed only for mocking values.
Given your specific example, I'd just put whatever the output of blockTagDeserializer('strong') right inside the config:
jest.mock('~/config', () => {
// ...
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: ({ children }) => <strong>{children}</strong>, // or whatever this function actually returns for 'strong'
};
return editor;
},
],
// ...
});
This doesn't require anything asynchronous to be done.
If you need this setup to be present in a lot of files, extracting it in a setup file seems to be the next best thing.
I found a solution. I have a ref callback that sets the htmlTagsToSlate property of the editor in the actual code of the module, conditioned by global.__JEST__ which is defined as true in Jest command line usage:
import { htmlTagsToSlate } from 'volto-slate/editor/config';
[...]
testingEditorRef={(val) => {
ref.current = val;
if (val && global.__JEST__) {
val.htmlTagsToSlate = { ...htmlTagsToSlate };
}
}}
Now the jest.mock call for ~/config is simple, there is no need to do an import in it.
I also use this function:
const handleEditorRef = (editor, ref) => {
if (typeof ref === 'function') {
ref(editor);
} else if (typeof ref === 'object') {
ref.current = editor;
}
return editor;
};
const debugMode = true;
// if "isCritical" is true, display the message regardless of
// the value of debugMode
function logger(message, isCritical) {
if (isCritical) console.log(message);
else if (debugMode) console.log(message);
}
In the above function, if I issued the following command, would the UTIL. inspect function evaluate "myObj" and not pass the data? I'd preferably not want UTIL.inspect to invoke if "isCritical" is set to false.
logger(
"myObj =\n" +
UTIL.inspect(myObj, {
showHidden: false,
depth: null
}),
false
);
Is there a way to avoid the evaluation of the first parameter in the function when the second parameter is false?
Seperate your code for logging from the decision wether or not it should be executed.
And use a build pipeline that supports tree-shaking.
Live example on rollupjs.org
// config.js
export const DEBUG = false;
// logging.js
import { DEBUG } from "./config.js";
export const log = console.log; // or whatever
export const debug = DEBUG ? (...args) => logger("debug:", ...args) : () => void 0;
// main.js
import { DEBUG } from "./config.js";
import { log, debug } from "./logging.js";
log("this will always be logged");
if (DEBUG) {
log("This will be eliminated when DEBUG=false")
}
// or more concise:
DEBUG && log(`This can be eliminated ${window.location = "/side-effect"}`);
debug("This approach works for simple things: " + location);
debug(`But it has limits ${window.location = "/side-effect"} :(`);
We have a large test suite running on a CI server, and there appears to be no way of telling Cypress to exit if a test fails. It always runs the entire suite.
There is some discussion here, but no workable solution.
Is there a reliable way to have Cypress exit as soon as a test fails?
You can also use this Cypress plugin meanwhile Cypress does not support this feature natively: https://www.npmjs.com/package/cypress-fail-fast
Add the plugin to devDependencies:
npm i --save-dev cypress-fail-fast
Inside cypress/plugins/index.js:
module.exports = (on, config) => {
require("cypress-fail-fast/plugin")(on, config);
return config;
};
At the top of cypress/support/index.js:
import "cypress-fail-fast";
As you've mentioned, it's not officially supported yet (as of 3.6.0).
Here's my take at a hack (without the use of cookies and such for keeping state):
// cypress/plugins/index.js
let shouldSkip = false;
module.exports = ( on ) => {
on('task', {
resetShouldSkipFlag () {
shouldSkip = false;
return null;
},
shouldSkip ( value ) {
if ( value != null ) shouldSkip = value;
return shouldSkip;
}
});
}
// cypress/support/index.js
function abortEarly () {
if ( this.currentTest.state === 'failed' ) {
return cy.task('shouldSkip', true);
}
cy.task('shouldSkip').then( value => {
if ( value ) this.skip();
});
}
beforeEach(abortEarly);
afterEach(abortEarly);
before(() => {
if ( Cypress.browser.isHeaded ) {
// Reset the shouldSkip flag at the start of a run, so that it
// doesn't carry over into subsequent runs.
// Do this only for headed runs because in headless runs,
// the `before` hook is executed for each spec file.
cy.task('resetShouldSkipFlag');
}
});
Will skip all further tests once a failure is encountered. The output will look like:
Is it possible to use chance plugin with cypress.io?
https://chancejs.com
I installed plugin via npm to node_modules\chance and edited /plugins/index.js file, but still get error from cypress - Can't start, The plugins file is missing or invalid.
If using this plugin is impossible - what do you recommend to write tests basing on registration new users? I planned to use chance to generate "random: emails and passwords.
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = on => {
on("task", {
chance: require("chance"),
});
};
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
Just install chancejs with npm:
npm install chance
Then just use it in your test for example:
/// <reference types="cypress" />
import Chance from 'Chance';
const chance = new Chance();
describe('Testing chance', function (){
const company =chance.company();
it('type company in duckduckgo.com', function () {
cy.visit('https://duckduckgo.com/')
cy.get('#search_form_input_homepage')
.should('be.visible')
.type(company)
})
})
cypress/support/index.js:
Override the default task command so that you can supply multiple arguments as you'd normally.
Cypress.Commands.overwrite('task', (origFn, name, ...args) => {
return origFn(name, args);
});
// if you're gonna use `chance` plugin a lot, you can also add a custom command
Cypress.Commands.add('chance', (...args) => {
return cy.task('chance', ...args);
});
cypress/plugins/index.js:
Wrap the tasks in function that will spread the arguments, and also handle a case when the task doesn't return anything and return null instead, so that cypress doesn't complain.
const chance = require('chance').Chance();
// lodash should be installed alongside with cypress.
// If it doesn't resolve, you'll need to install it manually
const _ = require('lodash');
on('task', _.mapValues(
{
chance ( method, ...args ) {
return chance[method](...args);
}
},
func => args => Promise.resolve( func(...args) )
// ensure we return null instead of undefined to satisfy cy.task
.then( val => val === undefined ? null : val )
));
In your spec file:
describe('test', () => {
it('test', () => {
cy.document().then( doc => {
doc.body.innerHTML = `<input class="email">`;
});
cy.task('chance', 'email', {domain: 'example.com'}).then( email => {
cy.get('.email').type(email);
});
// or use the custom `chance` command we added
cy.chance('email', {domain: 'test.com'}).then( email => {
cy.get('.email').type(email);
});
});
});