Nextjs config file - how to export more content - node.js

I am trying edit nextjs config file. To use ant design and i18next.
For ant design i need this.
/* eslint-disable */
const withCss = require('#zeit/next-css')
module.exports =
withCss({
webpack: (config, {
isServer
}) => {
if (isServer) {
const antStyles = /antd\/.*?\/style\/css.*?/
const origExternals = [...config.externals]
config.externals = [
(context, request, callback) => {
if (request.match(antStyles)) return callback()
if (typeof origExternals[0] === 'function') {
origExternals[0](context, request, callback)
} else {
callback()
}
},
...(typeof origExternals[0] === 'function' ? [] : origExternals),
]
config.module.rules.unshift({
test: antStyles,
use: 'null-loader',
})
}
return config
},
})
And for i18next i need
module.exports = {
publicRuntimeConfig: {
localeSubpaths: typeof process.env.LOCALE_SUBPATHS === 'string'
? process.env.LOCALE_SUBPATHS
: 'none',
},
}
So i combined it into:
/* eslint-disable */
const withCss = require('#zeit/next-css')
module.exports = ({
publicRuntimeConfig: {
localeSubpaths: typeof process.env.LOCALE_SUBPATHS === 'string' ?
process.env.LOCALE_SUBPATHS : 'none',
}
}, withCss({
webpack: (config, {
isServer
}) => {
if (isServer) {
const antStyles = /antd\/.*?\/style\/css.*?/
const origExternals = [...config.externals]
config.externals = [
(context, request, callback) => {
if (request.match(antStyles)) return callback()
if (typeof origExternals[0] === 'function') {
origExternals[0](context, request, callback)
} else {
callback()
}
},
...(typeof origExternals[0] === 'function' ? [] : origExternals),
]
config.module.rules.unshift({
test: antStyles,
use: 'null-loader',
})
}
return config
},
}))
But I am not sure if is it the correct way how to do it because I am still getting an error (ant design works alright and I am trying to import i18next)
D:\xxx\xxx\nextcms\i18n.js:4
} = require('next/config').default().publicRuntimeConfig
^
TypeError: Cannot read property 'publicRuntimeConfig' of undefined
It can be caused by some other problem but I just need to know if I am correctly exporting those ant design with i18next.
Thank for your time.

You need to wrap your entire export using withCss().
Try this instead:
/* eslint-disable */
const withCss = require('#zeit/next-css')
module.exports = withCss({
publicRuntimeConfig: {
// ...
},
webpack: (config, { isServer }) => {
// ...
return config
},
})

Related

How to combine the modules of the webpack through the merge function?

Through the merge function, in case the version of the web park is development, I try to add a server file to it only in this case. Writes an error. He writes that the types are not correct, but I indicated the same type in both interfaces
An argument of type "Devserver" cannot be assigned to a parameter of type "IgetConfig". The "entry" property is missing in the "I dev Server" type and is mandatory in the "I get Config" type.
return merge(mainConfig, serverTuning);
file dev.config.ts
module.exports = {
devServer: {
port: 3000,
},
};
file webpack config
import { merge } from 'webpack-merge';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { PlatformPath } from 'path';
const path: PlatformPath = require('path');
interface IdevServer {
devServer: {
port: number;
};
}
const serverTuning: IdevServer = require('./dev.config');
const test = 'main';
const ROOT_DIRECTORY = path.resolve(__dirname, '..');
interface IgetConfig {
entry: {
main: string[];
};
devServer?: {
port: number;
};
}
const getConfig = (mode): IgetConfig => {
const isProduction = mode === 'production';
const isDevelopment = mode === 'development';
return {
entry: {
[test]: ['./src/index.js'],
},
};
};
module.exports = (env, { mode }) => {
const mainConfig = getConfig(mode);
if (mode === 'development') {
return merge(mainConfig, serverTuning);
}
return mainConfig;
};
module.exports = (env, { mode }) => {
const mainConfig = getConfig(mode);
if (mode === 'development') {
return merge(mainConfig, serverTuning);
}
return mainConfig;
};

Why is my _middleware not able to route well in production (NEXTJS)?

I have the following on my _middleware.ts file with the objective of mainly routing requests to mysubdomain. Its working well on my local server but when I deploy to vercel its throwing a 404. Any help will be much appreciated.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { request } from "http";
export default function middleware(req: NextRequest) {
// Clone the request url
const newReq = req.nextUrl.clone();
const regex = new RegExp(`\.subdomaingivenbyvercel-[\w\w\w\w\w\w\w\w\w]-username\.vercel\.app`);
const pathname = req.nextUrl.pathname.toString();
// Get hostname of request (e.g. demo.vercel.pub)
const hostname = req.headers.get("host");
if (!hostname)
return new Response(null, {
status: 400,
statusText: "No hostname found in request headers"
});
const currentHost =
process.env.VERCEL_ENV === `production` && process.env.VERCEL === `1`
? // You have to replace ".vercel.pub" with your own domain if you deploy this
example under your domain.
// You can use wildcard subdomains on .vercel.app links that are associated
with your Vercel team slug
// in this case, our team slug is "platformize", thus
*.platformize.vercel.app works
hostname
.replace(`.myrootdomain.com`, "")
.replace(`.branchname.vercel.app`, "")
.replace(`.*.vercel.app`, "")
.replace(regex, "")
: hostname.replace(`.localhost:3000`, "");
if (pathname.startsWith(`/_sites`))
return new Response(null, {
status: 404
});
if (
!pathname.includes(".") &&
!pathname.startsWith("/api")
) {
if (currentHost === "mysubdomain") {
if (
pathname === "/login" &&
(req.cookies["next-auth.session-token"] ||
req.cookies["__Secure-next-auth.session-token"])
) {
newReq.pathname = "/";
return NextResponse.redirect(newReq.toString());
}
newReq.pathname = `/mysubdomain${pathname}`;
return NextResponse.rewrite(newReq.toString());
}
newReq.pathname = `${pathname}`;
return NextResponse.rewrite(newReq.toString());
}
}
I try all of the routes and none of them can be found. I have tried everything around the way the middleware parses the URL without success. Here you can see my next.config.js just in case Im doing a redirect without knowing it:
/**
* #type {import('next').NextConfig}
*/
const plugins = require("next-compose-plugins");
const withBundleAnalyzer = require("#next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true"
});
const withOffline = require("next-offline");
function esbuildLoader(config, options) {
const jsLoader = config.module.rules.find(
(rule) => rule.test && rule.test.test(".js")
);
if (jsLoader && jsLoader.use) {
if (jsLoader.use.length > 0) {
jsLoader.use.forEach((e) => {
e.loader = "esbuild-loader";
e.options = options;
});
} else {
jsLoader.use.loader = "esbuild-loader";
jsLoader.use.options = options;
}
}
}
// the config break if we use next export
const nextConfig =
process.env.EXPORT !== "true"
? {
webpack(config, { webpack, dev, isServer }) {
config.plugins.push(
new webpack.ProvidePlugin({
React: "react"
})
);
// audio support
config.module.rules.push({
test: /\.(ogg|mp3|wav|mpe?g)$/i,
exclude: config.exclude,
use: [
{
loader: require.resolve("url-loader"),
options: {
limit: config.inlineImageLimit,
fallback: require.resolve("file-loader"),
publicPath: `${config.assetPrefix}/_next/static/images/`,
outputPath: `${isServer ? "../" : ""}static/images/`,
name: "[name]-[hash].[ext]",
esModule: config.esModule || false
}
}
]
});
config.module.rules.push({
test: /\.(glsl|vs|fs|vert|frag)$/,
exclude: /node_modules/,
use: ["raw-loader", "glslify-loader", "#svgr/webpack"]
});
config.module.rules.push({
test: /\.svg$/,
use: [`#svgr/webpack`]
});
return config;
},
images: {
domains: [
"lomplay.com/ipfs",
"nft.storage",
"avatars.githubusercontent.com"
]
},
reactStrictMode: true,
swcMinify: false // Required to fix: https://nextjs.org/docs/messages/failed-
loading-swc
}
: {};
module.exports = plugins(
[
[
withOffline,
{
workboxOpts: {
swDest: process.env.NEXT_EXPORT
? "service-worker.js"
: "static/service-worker.js",
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: "NetworkFirst",
options: {
cacheName: "offlineCache",
expiration: {
maxEntries: 200
}
}
}
]
},
async rewrites() {
return {
beforeFiles: [
{
source: "/_subdomain/api/newsletter",
destination: "https://rootdomain.com/api/newsletter"
}
],
afterFiles: [
{
source: "/service-worker.js",
destination: "/_next/static/service-worker.js"
}
]
};
}
}
],
withBundleAnalyzer
],
nextConfig
);

Auth0 with Electron working in development but not when packaged

Im using Auth0 in an electron app to manage a log-in system. I referenced this tutorial here:
https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/
to get started with using it.
Auth0 has been working great when I've been working in development but for some reason fails after I call "yarn package" to build the app. My electron app used electron-react-boilerplate (https://github.com/electron-react-boilerplate/electron-react-boilerplate).
Here are the important files:
// imports
...
import {
getAuthenticationURL,
refreshTokens,
loadTokens,
logout,
getLogOutUrl,
getProfile,
getResponse,
} from './services/authservice';
export default class AppUpdater {
constructor() {
...
}
}
let mainWindow: BrowserWindow | null = null;
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
require('electron-debug')();
}
const installExtensions = async () => {
...
};
const createWindow = async () => {
console.log('now starting the main process');
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
await installExtensions();
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../assets');
const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
mainWindow = new BrowserWindow({
show: true,
width: 1024,
height: 728,
titleBarStyle: 'hidden', // add this line
frame: false,
//icon: getAssetPath('icon.png'),
webPreferences: {
nodeIntegration: true,
},
});
mainWindow.loadURL(`file://${__dirname}/index.html`);
const devtools = new BrowserWindow();
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
mainWindow.webContents.openDevTools({ mode: 'detach' });
};
/**
* Add event listeners...
*/
let win = null;
function createAuthWindow() {
destroyAuthWin();
win = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false,
},
});
console.log(getAuthenticationURL());
win.loadURL(getAuthenticationURL());
const {
session: { webRequest },
} = win.webContents;
const filter = {
urls: [
'file:///callback*',
],
};
webRequest.onBeforeRequest(filter, async ({ url }) => {
console.log(url);
await loadTokens(url)
.then((res) => {
console.log(res);
})
.catch(console.log);
console.log('from web request');
createWindow();
return destroyAuthWin();
});
win.on('authenticated', () => {
console.log('WE HAVE AUTHENTICATED');
destroyAuthWin();
});
win.on('closed', () => {
win = null;
});
}
function destroyAuthWin() {
if (!win) return;
win.close();
win = null;
}
// logout logic: removed for simplicity
ipcMain.on('profileRequest', (event, arg) => {
//event.reply('profileResponse', getProfile());
event.returnValue = getProfile();
});
const showWindow = async () => {
try {
await refreshTokens();
return createWindow();
} catch (err) {
createAuthWindow();
}
};
this is my auth services file:
let accessToken = null;
let profile = {};
let refreshToken = null;
export function getAccessToken() {
return accessToken;
}
export function getProfile() {
return profile;
}
export function getAuthenticationURL() {
return (
'https://' +
auth0Domain +
'/authorize?' +
'scope=openid%20profile%20offline_access&' +
'response_type=code&' +
'client_id=' +
clientId +
'&' +
'redirect_uri=' +
redirectUri
);
}
export async function refreshTokens() {
const refreshToken = await keytar.getPassword(keytarService, keytarAccount);
if (refreshToken) {
const refreshOptions = {
method: 'POST',
url: `https://${auth0Domain}/oauth/token`,
headers: { 'content-type': 'application/json' },
data: {
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: refreshToken,
},
};
try {
const response = await axios(refreshOptions);
res = response;
accessToken = response.data.access_token;
profile = jwtDecode(response.data.id_token);
} catch (error) {
await logout();
throw error;
}
} else {
throw new Error('No available refresh token.');
}
}
export async function loadTokens(callbackURL) {
console.log('loading tokens:');
console.log(callbackURL);
res = callbackURL;
const urlParts = url.parse(callbackURL, true);
const query = urlParts.query;
console.log(query);
const exchangeOptions = {
grant_type: 'authorization_code',
client_id: clientId,
code: query.code,
redirect_uri: redirectUri,
};
const options = {
method: 'POST',
url: `https://${auth0Domain}/oauth/token`,
headers: {
'content-type': 'application/json',
},
data: JSON.stringify(exchangeOptions),
};
try {
const response = await axios(options);
console.log('from token:');
console.log(response);
res = response;
accessToken = response.data.access_token;
profile = jwtDecode(response.data.id_token);
refreshToken = response.data.refresh_token;
console.log(getProfile());
if (refreshToken) {
await keytar.setPassword(keytarService, keytarAccount, refreshToken);
}
} catch (error) {
await logout();
throw error;
}
}
I have a file in my components folder called "Auth.jsx" which has a "get profile" methods which interacts with the main process to get the profile
const getProfile = () => {
return ipcRenderer.sendSync('profileRequest', true);
};
After I package the electron app, the getProfile method always returns null/undefined.
Here are the auth0 logs:
Auth0 Logs
It shows that there is a successful Login and Exchange.
Finally, here's my webpack file: "webpack.config.main.prod.babel"
/**
* Webpack config for production electron main process
*/
import path from 'path';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
import TerserPlugin from 'terser-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import baseConfig from './webpack.config.base';
import CheckNodeEnv from '../scripts/CheckNodeEnv';
import DeleteSourceMaps from '../scripts/DeleteSourceMaps';
import dotenv from 'dotenv';
CheckNodeEnv('production');
DeleteSourceMaps();
const devtoolsConfig =
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
export default merge(baseConfig, {
...devtoolsConfig,
mode: 'production',
target: 'electron-main',
entry: './src/main.dev.ts',
output: {
path: path.join(__dirname, '../../'),
filename: './src/main.prod.js',
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode:
process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
openAnalyzer: process.env.OPEN_ANALYZER === 'true',
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: true,
START_MINIMIZED: false,
/*
other environment variables, including auth0 domain name and clientID
*/
}),
],
/**
* Disables webpack processing of __dirname and __filename.
* If you run the bundle in node.js it falls back to these values of node.js.
* https://github.com/webpack/webpack/issues/2010
*/
node: {
__dirname: false,
__filename: false,
},
});
Im suspecting the problem might have something to do with webpack since it's only the packaged version of the application that doesn't work properly. Im not sure exactly what the problem is, whether is a problem in the code or if I need to specifically change something within my Auth0 dashboard. If you have any suggestions or any ideas on how to debug let me know!
I had the exact same issue! I fixed it by changing the import method for jwt-decode from require to import
import jwtDecode from 'jwt-decode'

Typescript Mocha Testing, How to solve TypeError: is not a function?

Currently I have the following class:
import * as winston from 'winston';
const { combine, timestamp, printf, label, json} = winston.format;
import {isEmpty, isNil} from 'lodash';
import {Log} from './Log';
export class LoggingService {
public static initializeKeys() {
this.keys = {tag: 'tag'};
}
public static intialize() {
this.initializeKeys();
const maskFormat = winston.format((meta) => {
meta[this.keys.tag] = 'WebProxyConsumer';
return meta;
})();
const jsonLog = printf((info) => {
return JSON.stringify(info);
});
this.logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
jsonLog
),
transports: [
new winston.transports.Console(),
new winston.transports.File( { filename: 'error.log', level: 'error', maxsize: 10000000})
],
exceptionHandlers: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'exceptions.log', maxsize: 10000000 })
]
});
}
public static getDefaultLogger() {
return this.logger;
}
public static error(error, label, data) {
if (isNil(this.logger)) {
LoggingService.intialize();
}
let logObj = new Log(null, null);
logObj.level = 'error';
if (!isNil(label)) {
logObj.label = label;
}
if (!isNil(data)) {
if (typeof data === 'string') {
logObj.message = data;
} else {
Object.keys(data).forEach((key) => {
const value = data[key];
if (logObj.hasOwnProperty(key)) {
logObj[key] = value;
} else {
logObj.data[key] = value;
}
});
}
}
if (error instanceof Error) {
if (error.hasOwnProperty('message')) {
logObj.message += ' Error Message: ' + error.message;
}
if (error.hasOwnProperty('stack')) {
logObj.error_stack = error.stack;
}
}
if (typeof error === 'string') {
logObj.message += ' Error Message: ' + error;
}
if (isNil(logObj.device_id)) {
delete logObj.device_id;
}
if (isNil(logObj.data) || isEmpty(logObj.data)) {
delete logObj.data;
}
this.logger.log(logObj);
}
public static info(data, label) {
if (isNil(this.logger)) {
LoggingService.intialize();
}
let logObj = new Log(null, null);
if (!isNil(label)) {
logObj.label = label;
}
if (!isNil(data)) {
if (typeof data === 'string') {
logObj.message = data;
} else {
Object.keys(data).forEach((key) => {
const value = data[key];
if (logObj.hasOwnProperty(key)) {
logObj[key] = value;
} else {
logObj.data[key] = value;
}
});
}
if (isNil(logObj.device_id)) {
delete logObj.device_id;
}
if (isNil(logObj.data) || isEmpty(logObj.data)) {
delete logObj.data;
}
this.logger.log(logObj);
}
}
private static logger: winston.Logger;
private static keys: any;
}
I'm using mocha for unit testing and so far this is my unit test for the class:
describe('LoggingService Tests', () => {
const loggingService = new LoggingService();
const loggingServiceProto = Object.getPrototypeOf(loggingService);
it('Checking LoggingService Initialization', () => {
expect(loggingServiceProto).to.not.be.null;
expect(loggingServiceProto.combine).to.not.be.null;
expect(loggingServiceProto.timestamp).to.not.be.null;
expect(loggingServiceProto.printf).to.not.be.null;
expect(loggingServiceProto.logger).to.not.be.null;
expect(loggingServiceProto.keys).to.not.be.null;
})
it('Checking initializeKeys', () => {
expect(loggingServiceProto.initializeKeys()).to.not.be.null;
})
it('Checking initialize', () => {
expect(loggingServiceProto.intialize()).to.not.be.null;
expect(loggingServiceProto.logger).to.not.be.null;
})
it('Checking getDefaultLogger', () => {
expect(loggingServiceProto.getDefaultLogger()).to.not.be.null;
})
})
The importing for mocha is correct and for my first test 'Checking LoggingService Initialization', I'm successfully passing. That's to say I'm able to initialize my class without a problem. The problem is with the rest of the tests I'm running. For those, I get the following errors:
TypeError: loggingServiceProto.initializeKeys is not a function
TypeError: loggingServiceProto.intialize is not a function
TypeError: loggingServiceProto.getDefaultLogger is not a function
Would anyone know why this is happening? These functions are defined and I'm not experiencing this issue with any other classes I'm testing using mocha.
Any advice would greatly be appreciated!
something to do with tsconfig. remove tsconfig and see if test passes.
the test command should look like:
mocha -r ts-node/register src/**/*.spec.ts

How do I deal with localStorage in jest tests?

I keep getting "localStorage is not defined" in Jest tests which makes sense but what are my options? Hitting brick walls.
Great solution from #chiedo
However, we use ES2015 syntax and I felt it was a little cleaner to write it this way.
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = String(value);
}
removeItem(key) {
delete this.store[key];
}
}
global.localStorage = new LocalStorageMock;
Figured it out with help from this: https://groups.google.com/forum/#!topic/jestjs/9EPhuNWVYTg
Setup a file with the following contents:
var localStorageMock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key];
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
},
removeItem: function(key) {
delete store[key];
}
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Then you add the following line to your package.json under your Jest configs
"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",
Answer:
Currently (Jul '22) localStorage can not be mocked or spied on by jest as you usually would, and as outlined in the create-react-app docs. This is due to changes made in jsdom. You can read about it in the jest and jsdom issue trackers.
As a workaround, you can spy on the prototype instead:
// does not work:
jest.spyOn(localStorage, "setItem");
localStorage.setItem = jest.fn();
// either of these lines will work, different syntax that does the same thing:
jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();
// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();
A note on spying on the prototype:
Spying on an instance gives you the ability to observe and mock behaviour for a specific object.
Spying on the prototype, on the other hand, will observe/manipulate every instance of that class all at once. Unless you have a special usecase, this is probably not what you want.
However, in this case it makes no difference, because there only exists a single instance of localStorage.
If using create-react-app, there is a simpler and straightforward solution explained in the documentation.
Create src/setupTests.js and put this in it :
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn()
};
global.localStorage = localStorageMock;
Tom Mertz contribution in a comment below :
You can then test that your localStorageMock's functions are used by doing something like
expect(localStorage.getItem).toBeCalledWith('token')
// or
expect(localStorage.getItem.mock.calls.length).toBe(1)
inside of your tests if you wanted to make sure it was called. Check out https://facebook.github.io/jest/docs/en/mock-functions.html
Unfortunately, the solutions that I've found here didn't work for me.
So I was looking at Jest GitHub issues and found this thread
The most upvoted solutions were these ones:
const spy = jest.spyOn(Storage.prototype, 'setItem');
// or
Storage.prototype.getItem = jest.fn(() => 'bla');
A better alternative which handles undefined values (it doesn't have toString()) and returns null if value doesn't exist. Tested this with react v15, redux and redux-auth-wrapper
class LocalStorageMock {
constructor() {
this.store = {}
}
clear() {
this.store = {}
}
getItem(key) {
return this.store[key] || null
}
setItem(key, value) {
this.store[key] = value
}
removeItem(key) {
delete this.store[key]
}
}
global.localStorage = new LocalStorageMock
or you just take a mock package like this:
https://www.npmjs.com/package/jest-localstorage-mock
it handles not only the storage functionality but also allows you test if the store was actually called.
If you are looking for a mock and not a stub, here is the solution I use:
export const localStorageMock = {
getItem: jest.fn().mockImplementation(key => localStorageItems[key]),
setItem: jest.fn().mockImplementation((key, value) => {
localStorageItems[key] = value;
}),
clear: jest.fn().mockImplementation(() => {
localStorageItems = {};
}),
removeItem: jest.fn().mockImplementation((key) => {
localStorageItems[key] = undefined;
}),
};
export let localStorageItems = {}; // eslint-disable-line import/no-mutable-exports
I export the storage items for easy initialization. I.E. I can easily set it to an object
In the newer versions of Jest + JSDom it is not possible to set this, but the localstorage is already available and you can spy on it it like so:
const setItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem');
For Jest, React & TypeScript users:
I created a mockLocalStorage.ts
export const mockLocalStorage = () => {
const setItemMock = jest.fn();
const getItemMock = jest.fn();
beforeEach(() => {
Storage.prototype.setItem = setItemMock;
Storage.prototype.getItem = getItemMock;
});
afterEach(() => {
setItemMock.mockRestore();
getItemMock.mockRestore();
});
return { setItemMock, getItemMock };
};
My component:
export const Component = () => {
const foo = localStorage.getItem('foo')
localStorage.setItem('bar', 'true')
return <h1>{foo}</h1>
}
then in my tests I use it like so:
import React from 'react';
import { mockLocalStorage } from '../../test-utils';
import { Component } from './Component';
const { getItemMock, setItemMock } = mockLocalStorage();
it('fetches something from localStorage', () => {
getItemMock.mockReturnValue('bar');
render(<Component />);
expect(getItemMock).toHaveBeenCalled();
expect(getByText(/bar/i)).toBeInTheDocument()
});
it('expects something to be set in localStorage' () => {
const value = "true"
const key = "bar"
render(<Component />);
expect(setItemMock).toHaveBeenCalledWith(key, value);
}
I found this solution from github
var localStorageMock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key] || null;
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
You can insert this code in your setupTests and it should work fine.
I tested it in a project with typesctipt.
A bit more elegant solution using TypeScript and Jest.
interface Spies {
[key: string]: jest.SpyInstance
}
describe('→ Local storage', () => {
const spies: Spies = {}
beforeEach(() => {
['setItem', 'getItem', 'clear'].forEach((fn: string) => {
const mock = jest.fn(localStorage[fn])
spies[fn] = jest.spyOn(Storage.prototype, fn).mockImplementation(mock)
})
})
afterEach(() => {
Object.keys(spies).forEach((key: string) => spies[key].mockRestore())
})
test('→ setItem ...', async () => {
localStorage.setItem( 'foo', 'bar' )
expect(localStorage.getItem('foo')).toEqual('bar')
expect(spies.setItem).toHaveBeenCalledTimes(1)
})
})
You can use this approach, to avoid mocking.
Storage.prototype.getItem = jest.fn(() => expectedPayload);
Object.defineProperty(window, "localStorage", {
value: {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
},
});
or
jest.spyOn(Object.getPrototypeOf(localStorage), "getItem");
jest.spyOn(Object.getPrototypeOf(localStorage), "setItem");
As #ck4 suggested documentation has clear explanation for using localStorage in jest. However the mock functions were failing to execute any of the localStorage methods.
Below is the detailed example of my react component which make uses of abstract methods for writing and reading data,
//file: storage.js
const key = 'ABC';
export function readFromStore (){
return JSON.parse(localStorage.getItem(key));
}
export function saveToStore (value) {
localStorage.setItem(key, JSON.stringify(value));
}
export default { readFromStore, saveToStore };
Error:
TypeError: _setupLocalStorage2.default.setItem is not a function
Fix:
Add below mock function for jest (path: .jest/mocks/setUpStore.js )
let mockStorage = {};
module.exports = window.localStorage = {
setItem: (key, val) => Object.assign(mockStorage, {[key]: val}),
getItem: (key) => mockStorage[key],
clear: () => mockStorage = {}
};
Snippet is referenced from here
To do the same in the Typescript, do the following:
Setup a file with the following contents:
let localStorageMock = (function() {
let store = new Map()
return {
getItem(key: string):string {
return store.get(key);
},
setItem: function(key: string, value: string) {
store.set(key, value);
},
clear: function() {
store = new Map();
},
removeItem: function(key: string) {
store.delete(key)
}
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Then you add the following line to your package.json under your Jest configs
"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",
Or you import this file in your test case where you want to mock the localstorage.
describe('getToken', () => {
const Auth = new AuthService();
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1yIEpvc2VwaCIsImlkIjoiNWQwYjk1Mzg2NTVhOTQ0ZjA0NjE5ZTA5IiwiZW1haWwiOiJ0cmV2X2pvc0Bob3RtYWlsLmNvbSIsInByb2ZpbGVVc2VybmFtZSI6Ii9tcmpvc2VwaCIsInByb2ZpbGVJbWFnZSI6Ii9Eb3Nlbi10LUdpci1sb29rLWN1dGUtbnVrZWNhdDMxNnMtMzExNzAwNDYtMTI4MC04MDAuanBnIiwiaWF0IjoxNTYyMzE4NDA0LCJleHAiOjE1OTM4NzYwMDR9.YwU15SqHMh1nO51eSa0YsOK-YLlaCx6ijceOKhZfQZc';
beforeEach(() => {
global.localStorage = jest.fn().mockImplementation(() => {
return {
getItem: jest.fn().mockReturnValue(token)
}
});
});
it('should get the token from localStorage', () => {
const result = Auth.getToken();
expect(result).toEqual(token);
});
});
Create a mock and add it to the global object
At least as of now, localStorage can be spied on easily on your jest tests, for example:
const spyRemoveItem = jest.spyOn(window.localStorage, 'removeItem')
And that's it. You can use your spy as you are used to.
This worked for me and just one code line
const setItem = jest.spyOn(Object.getPrototypeOf(localStorage), 'setItem');
2021, typescript
class LocalStorageMock {
store: { [k: string]: string };
length: number;
constructor() {
this.store = {};
this.length = 0;
}
/**
* #see https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
* #returns
*/
key = (idx: number): string => {
const values = Object.values(this.store);
return values[idx];
};
clear() {
this.store = {};
}
getItem(key: string) {
return this.store[key] || null;
}
setItem(key: string, value: string) {
this.store[key] = String(value);
}
removeItem(key: string) {
delete this.store[key];
}
}
export default LocalStorageMock;
you can then use it with
global.localStorage = new LocalStorageMock();
Riffed off some other answers here to solve it for a project with Typescript. I created a LocalStorageMock like this:
export class LocalStorageMock {
private store = {}
clear() {
this.store = {}
}
getItem(key: string) {
return this.store[key] || null
}
setItem(key: string, value: string) {
this.store[key] = value
}
removeItem(key: string) {
delete this.store[key]
}
}
Then I created a LocalStorageWrapper class that I use for all access to local storage in the app instead of directly accessing the global local storage variable. Made it easy to set the mock in the wrapper for tests.
As mentioned in a comment by Niket Pathak,
starting jest#24 / jsdom#11.12.0 and above, localStorage is mocked automatically.
An update for 2022.
Jest#24+ has ability to mock local storage automatically. However, the dependency needed no longer ships with it by default.
npm i -D jest-environment-jsdom
You also need to change your Jest test mode:
// jest.config.cjs
module.exports = {
...
testEnvironment: "jsdom",
...
};
Now localStorage will already be mocked for you.
Example:
// myStore.js
const saveLocally = (key, value) => {
localStorage.setItem(key, value)
};
Test:
// myStore.spec.ts
import { saveLocally } from "./myStore.js"
it("saves key-value pair", () => {
let key = "myKey";
let value = "myValue";
expect(localStorage.getItem(key)).toBe(null);
saveLocally(key, value);
expect(localStorage.getItem(key)).toBe(value);
};
The following solution is compatible for testing with stricter TypeScript, ESLint, TSLint, and Prettier config: { "proseWrap": "always", "semi": false, "singleQuote": true, "trailingComma": "es5" }:
class LocalStorageMock {
public store: {
[key: string]: string
}
constructor() {
this.store = {}
}
public clear() {
this.store = {}
}
public getItem(key: string) {
return this.store[key] || undefined
}
public setItem(key: string, value: string) {
this.store[key] = value.toString()
}
public removeItem(key: string) {
delete this.store[key]
}
}
/* tslint:disable-next-line:no-any */
;(global as any).localStorage = new LocalStorageMock()
HT/ https://stackoverflow.com/a/51583401/101290 for how to update global.localStorage
There is no need to mock localStorage - just use the jsdom environment so that your tests run in browser-like conditions.
In your jest.config.js,
module.exports = {
// ...
testEnvironment: "jsdom"
}
none of the answers above worked for me. So after some digging this is what I got to work. Credit goes to a few sources and other answers as well.
https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/
https://github.com/facebook/jest/issues/6798#issuecomment-440988627
https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c
https://github.com/facebook/jest/issues/6798#issuecomment-514266034
My full gist: https://gist.github.com/ar-to/01fa07f2c03e7c1b2cfe6b8c612d4c6b
/**
* Build Local Storage object
* #see https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/ for source
* #see https://stackoverflow.com/a/32911774/9270352 for source
* #returns
*/
export const fakeLocalStorage = () => {
let store: { [key: string]: string } = {}
return {
getItem: function (key: string) {
return store[key] || null
},
setItem: function (key: string, value: string) {
store[key] = value.toString()
},
removeItem: function (key: string) {
delete store[key]
},
clear: function () {
store = {}
},
}
}
/**
* Mock window properties for testing
* #see https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c for source
* #see https://github.com/facebook/jest/issues/6798#issuecomment-514266034 for sample implementation
* #see https://developer.mozilla.org/en-US/docs/Web/API/Window#properties for window properties
* #param { string } property window property string but set to any due to some warnings
* #param { Object } value for property
*
* #example
*
* const testLS = {
* id: 5,
* name: 'My Test',
* }
* mockWindowProperty('localStorage', fakeLocalStorage())
* window.localStorage.setItem('currentPage', JSON.stringify(testLS))
*
*/
const mockWindowProperty = (property: string | any, value: any) => {
const { [property]: originalProperty } = window
delete window[property]
beforeAll(() => {
Object.defineProperty(window, property, {
configurable: true,
writable: true,
value,
})
})
afterAll(() => {
window[property] = originalProperty
})
}
export default mockWindowProperty
In my case, I needed to set the localStorage value before I check it.
So what I did is
const data = { .......}
const setLocalStorageValue = (name: string, value: any) => {
localStorage.setItem(name, JSON.stringify(value))
}
describe('Check X class', () => {
setLocalStorageValue('Xname', data)
const xClass= new XClass()
console.log(xClass.initiate()) ; // it will work
})
2022 December: Nx 14 with Angular 14 Jest.
We have an test-setup.ts file in every app and libs folder. We setting local storage mock globaly.
import 'jest-preset-angular/setup-jest';
Storage.prototype.getItem = jest.fn();
Storage.prototype.setItem = jest.fn();
Storage.prototype.removeItem = jest.fn();
Then localStorage.service.spec.ts file looking like this:
import { LocalStorageService } from './localstorage.service';
describe('LocalStorageService', () => {
let localStorageService: LocalStorageService;
beforeEach(() => {
localStorageService = new LocalStorageService();
});
it('should set "identityKey" in localStorage', async () => {
localStorageService.saveData('identityKey', '99');
expect(window.localStorage.setItem).toHaveBeenCalled();
expect(window.localStorage.setItem).toHaveBeenCalledWith('identityKey', '99');
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
});
it('should get "identityKey" from localStorage', async () => {
localStorageService.getData('identityKey');
expect(window.localStorage.getItem).toHaveBeenCalled();
expect(window.localStorage.getItem).toHaveBeenCalledWith('identityKey');
expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
});
it('should remove "identityKey" from localStorage', async () => {
localStorageService.removeData('identityKey');
expect(window.localStorage.removeItem).toHaveBeenCalled();
expect(window.localStorage.removeItem).toHaveBeenCalledWith('identityKey');
expect(window.localStorage.removeItem).toHaveBeenCalledTimes(1);
});
});
P.S. Sorry for bad indentation, this SatckOverflow window s*cks.
First: I created a file named localStorage.ts(localStorage.js)
class LocalStorageMock {
store: Store;
length: number;
constructor() {
this.store = {};
this.length = 0;
}
key(n: number): any {
if (typeof n === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present."
);
}
if (n >= Object.keys(this.store).length) {
return null;
}
return Object.keys(this.store)[n];
}
getItem(key: string): Store | null {
if (!Object.keys(this.store).includes(key)) {
return null;
}
return this.store[key];
}
setItem(key: string, value: any): undefined {
if (typeof key === 'undefined' && typeof value === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 0 present."
);
}
if (typeof value === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 1 present."
);
}
if (!key) return undefined;
this.store[key] = value.toString() || '';
this.length = Object.keys(this.store).length;
return undefined;
}
removeItem(key: string): undefined {
if (typeof key === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present."
);
}
delete this.store[key];
this.length = Object.keys(this.store).length;
return undefined;
}
clear(): undefined {
this.store = {};
this.length = 0;
return undefined;
}
}
export const getLocalStorageMock = (): any => {
return new LocalStorageMock();
};
global.localStorage = new LocalStorageMock();
Then create a test file named session.test.ts(session.test.js)
import { getLocalStorageMock } from '../localstorage';
describe('session storage', () => {
let localStorage;
beforeEach(() => {
localStorage = getLocalStorageMock();
});
describe('getItem', () => {
it('should return null if the item is undefined', () => {
expect(localStorage.getItem('item')).toBeNull();
});
it("should return '' instead of null", () => {
localStorage.setItem('item', '');
expect(localStorage.getItem('item')).toBe('');
});
it('should return navid', () => {
localStorage.setItem('item', 'navid');
expect(localStorage.getItem('item')).toBe('navid');
});
});
});
This worked for me,
delete global.localStorage;
global.localStorage = {
getItem: () =>
}

Resources