next-page-tester
Version:
Enable DOM integration testing on Next.js pages
184 lines (183 loc) • 7.73 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setNextImageConfiguration = exports.glob = exports.getLocales = exports.isExternalRoute = exports.parseHTML = exports.executeWithFreshModules = exports.preservePredefinedSharedModulesIdentity = exports.useMountedState = exports.getPageExtensions = exports.findPagesDirectory = exports.defaultNextRoot = exports.removeFileExtension = exports.stringifyQueryString = exports.parseQueryString = exports.parseRoute = void 0;
const url_1 = require("url");
const react_1 = require("react");
const querystring_1 = __importDefault(require("querystring"));
const find_root_1 = __importDefault(require("find-root"));
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const stealthy_require_1 = __importDefault(require("stealthy-require"));
const tiny_glob_1 = __importDefault(require("tiny-glob"));
const normalize_path_1 = __importDefault(require("normalize-path"));
const nextConfig_1 = require("./nextConfig");
const _error_1 = require("./_error");
const normalize_locale_path_1 = require("next/dist/shared/lib/i18n/normalize-locale-path");
const image_config_1 = require("next/dist/shared/lib/image-config");
function parseRoute({ route, }) {
const { i18n } = (0, nextConfig_1.getNextConfig)();
const locales = i18n === null || i18n === void 0 ? void 0 : i18n.locales;
const urlObject = new url_1.URL(`http://test.com${route}`);
const { pathname: localePath } = urlObject;
const { pathname, detectedLocale } = (0, normalize_locale_path_1.normalizeLocalePath)(localePath, locales);
urlObject.pathname = pathname;
/*
* Next.js redirects by default routes with trailing slash to the counterpart without trailing slash
* @NOTE: Here we might handle Next.js trailingSlash option
* https://nextjs.org/docs/api-reference/next.config.js/trailing-slash
*/
if (pathname.endsWith('/') && pathname !== '/') {
urlObject.pathname = pathname.slice(0, -1);
}
return { urlObject, detectedLocale };
}
exports.parseRoute = parseRoute;
function parseQueryString({ queryString, }) {
const qs = queryString.startsWith('?')
? queryString.substring(1)
: queryString;
return querystring_1.default.parse(qs);
}
exports.parseQueryString = parseQueryString;
function stringifyQueryString({ object, leadingQuestionMark, }) {
const queryString = querystring_1.default.stringify(object);
if (leadingQuestionMark && queryString) {
return '?' + queryString;
}
return queryString;
}
exports.stringifyQueryString = stringifyQueryString;
function removeFileExtension({ path }) {
return path.replace(/\.[^/.]+$/, '');
}
exports.removeFileExtension = removeFileExtension;
exports.defaultNextRoot = (0, find_root_1.default)(process.cwd());
function findPagesDirectory({ nextRoot }) {
const pagesPaths = [
path_1.default.join(nextRoot, 'pages'),
path_1.default.join(nextRoot, 'src', 'pages'),
];
for (const path of pagesPaths) {
if ((0, fs_1.existsSync)(path)) {
return path;
}
}
throw new _error_1.InternalError(`Cannot find "pages" directory under: ${nextRoot}`);
}
exports.findPagesDirectory = findPagesDirectory;
function getPageExtensions() {
const config = (0, nextConfig_1.getNextConfig)();
return config.pageExtensions;
}
exports.getPageExtensions = getPageExtensions;
function useMountedState() {
const mountedRef = (0, react_1.useRef)(false);
const get = (0, react_1.useCallback)(() => mountedRef.current, []);
(0, react_1.useEffect)(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return get;
}
exports.useMountedState = useMountedState;
// @NOTE: It is crucial that these modules preserve their identity between client and server
// for document rendering to work correctly. For things to kick in early enough in the process we
// mark them as such in testHelpers.
const predefinedSharedModules = [
'react',
'next/dist/shared/lib/head-manager-context',
'next/dist/shared/lib/router-context',
'next/dist/shared/lib/runtime-config',
];
function preserveJestSharedModulesIdentity(modules) {
for (const mockModuleName of modules) {
// @NOTE for some reason Jest needs us to pre-import the modules
// we want to require with jest.requireActual
require(mockModuleName);
jest.mock(mockModuleName, () => jest.requireActual(mockModuleName));
}
}
function preservePredefinedSharedModulesIdentity() {
preserveJestSharedModulesIdentity(predefinedSharedModules);
}
exports.preservePredefinedSharedModulesIdentity = preservePredefinedSharedModulesIdentity;
function executeWithFreshModules(f, { sharedModules }) {
/* istanbul ignore else */
if (typeof jest !== 'undefined') {
let result;
preserveJestSharedModulesIdentity(sharedModules);
jest.isolateModules(() => {
result = f();
});
// @ts-expect-error result is surely defined here
return result;
}
// @NOTE this branch will never be executed by Jest
else {
return (0, stealthy_require_1.default)(require.cache, f, () => {
for (const moduleName of [
...predefinedSharedModules,
...sharedModules,
]) {
require(moduleName);
}
}, module);
}
}
exports.executeWithFreshModules = executeWithFreshModules;
function parseHTML(html) {
const domParser = new DOMParser();
return domParser.parseFromString(html, 'text/html');
}
exports.parseHTML = parseHTML;
const ABSOLUTE_URL_REGEXP = new RegExp('^(?:[a-z]+:)?//', 'i');
function isExternalRoute(route) {
return Boolean(route.match(ABSOLUTE_URL_REGEXP));
}
exports.isExternalRoute = isExternalRoute;
/**
* Get locale information for a given route
*/
function getLocales({ pageObject: { detectedLocale }, }) {
const { i18n } = (0, nextConfig_1.getNextConfig)();
return {
locales: i18n === null || i18n === void 0 ? void 0 : i18n.locales,
defaultLocale: i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale,
locale: detectedLocale || (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale),
};
}
exports.getLocales = getLocales;
/**
* Returns the absolute file paths matching a given glob pattern
* It normalizes both incoming and outcoming paths
*/
async function glob(pattern) {
const paths = await (0, tiny_glob_1.default)((0, normalize_path_1.default)(pattern), { absolute: true });
return paths.map((path) => (0, normalize_path_1.default)(path));
}
exports.glob = glob;
/**
* Set next/image configuration as implemented in:
* https://github.com/vercel/next.js/blob/v11.1.0/packages/next/client/image.tsx#L107
*/
function setNextImageConfiguration() {
const config = (0, nextConfig_1.getNextConfig)();
// @NOTE config.images is optional according to types but a default is always provided
const imageConfig = config.images
? {
deviceSizes: config.images.deviceSizes,
imageSizes: config.images.imageSizes,
path: config.images.path,
loader: config.images.loader,
domains: config.images.domains,
}
: /* istanbul ignore next */ image_config_1.imageConfigDefault;
// @ts-expect-error this is how Next.js seems to do
process.env.__NEXT_IMAGE_OPTS = imageConfig;
}
exports.setNextImageConfiguration = setNextImageConfiguration;