From dc975fecbf2bbaad03ae23171c47c3d1153156af Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Mon, 14 Mar 2022 08:43:51 +0800 Subject: [PATCH] refactor: convert Jest infrastructure to TS (#6910) --- jest.config.mjs | 6 +- jest/{emptyModule.js => emptyModule.ts} | 2 +- ...ormalizer.js => snapshotPathNormalizer.ts} | 126 +++++++----------- website/src/data/__tests__/user.test.ts | 11 +- 4 files changed, 59 insertions(+), 86 deletions(-) rename jest/{emptyModule.js => emptyModule.ts} (90%) rename jest/{snapshotPathNormalizer.js => snapshotPathNormalizer.ts} (58%) diff --git a/jest.config.mjs b/jest.config.mjs index c469fed92f..d973ca87d2 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -35,13 +35,13 @@ export default { errorOnDeprecated: true, moduleNameMapper: { // Jest can't resolve CSS or asset imports - '^.+\\.(css|jpe?g|png|svg)$': '/jest/emptyModule.js', + '^.+\\.(css|jpe?g|png|svg|webp)$': '/jest/emptyModule.ts', // Using src instead of lib, so we always get fresh source '@docusaurus/(browserContext|BrowserOnly|ComponentCreator|constants|docusaurusContext|ExecutionEnvironment|Head|Interpolate|isInternalUrl|Link|Noop|renderRoutes|router|Translate|use.*)': '@docusaurus/core/src/client/exports/$1', // Maybe point to a fixture? - '@generated/.*': '/jest/emptyModule.js', + '@generated/.*': '/jest/emptyModule.ts', // TODO use "projects" + multiple configs if we work on another theme? '@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1', '@site/(.*)': 'website/$1', @@ -50,7 +50,7 @@ export default { '@docusaurus/plugin-content-docs/client': '@docusaurus/plugin-content-docs/src/client/index.ts', }, - snapshotSerializers: ['/jest/snapshotPathNormalizer.js'], + snapshotSerializers: ['/jest/snapshotPathNormalizer.ts'], snapshotFormat: { printBasicPrototype: false, }, diff --git a/jest/emptyModule.js b/jest/emptyModule.ts similarity index 90% rename from jest/emptyModule.js rename to jest/emptyModule.ts index ec757d00e9..2687fdf661 100644 --- a/jest/emptyModule.js +++ b/jest/emptyModule.ts @@ -5,4 +5,4 @@ * LICENSE file in the root directory of this source tree. */ -module.exports = {}; +export default {}; diff --git a/jest/snapshotPathNormalizer.js b/jest/snapshotPathNormalizer.ts similarity index 58% rename from jest/snapshotPathNormalizer.js rename to jest/snapshotPathNormalizer.ts index 7d3fcd7d26..f9f69cc90f 100644 --- a/jest/snapshotPathNormalizer.js +++ b/jest/snapshotPathNormalizer.ts @@ -9,82 +9,57 @@ // Forked from https://github.com/tribou/jest-serializer-path/blob/master/lib/index.js // Added some project-specific handlers -const _ = require('lodash'); -const {escapePath} = require('@docusaurus/utils'); -const {version} = require('@docusaurus/core/package.json'); -const os = require('os'); -const path = require('path'); -const fs = require('fs'); +import _ from 'lodash'; +import {escapePath} from '@docusaurus/utils'; +import {version} from '@docusaurus/core/package.json'; +import os from 'os'; +import path from 'path'; +import fs from 'fs'; -module.exports = { - print(val, serialize) { - let normalizedValue = val; - - if (_.isError(normalizedValue)) { - const message = normalizePaths(normalizedValue.message); - const error = new Error(message); - - // Clone hidden props - const ownProps = Object.getOwnPropertyNames(error); - // eslint-disable-next-line no-restricted-syntax - for (const index in ownProps) { - if (Object.prototype.hasOwnProperty.call(ownProps, index)) { - const key = ownProps[index]; - - error[key] = normalizePaths(normalizedValue[key]); - } - } - - // Clone normal props - // eslint-disable-next-line no-restricted-syntax - for (const index in normalizedValue) { - if (Object.prototype.hasOwnProperty.call(normalizedValue, index)) { - error[index] = normalizePaths(normalizedValue[index]); - } - } - - normalizedValue = error; - } else if (typeof normalizedValue === 'object') { - normalizedValue = _.cloneDeep(normalizedValue); - - Object.keys(normalizedValue).forEach((key) => { - normalizedValue[key] = normalizePaths(normalizedValue[key]); - }); - } else { - normalizedValue = normalizePaths(normalizedValue); - } +export function print( + val: unknown, + serialize: (val: unknown) => string, +): string { + if (val instanceof Error) { + const message = normalizePaths(val.message); + const error = new Error(message); + const allKeys = [ + ...Object.getOwnPropertyNames(error), + ...Object.keys(val), + ] as (keyof Error)[]; + allKeys.forEach((key) => { + error[key] = normalizePaths(val[key]) as never; + }); + return serialize(error); + } else if (val && typeof val === 'object') { + const normalizedValue = _.cloneDeep(val) as Record; + Object.keys(normalizedValue).forEach((key) => { + normalizedValue[key] = normalizePaths(normalizedValue[key]); + }); return serialize(normalizedValue); - }, - test(val) { - let has = false; + } + return serialize(normalizePaths(val)); +} - if (val && typeof val === 'object') { - // val.message is non-enumerable in an error - if (val.message && shouldUpdate(val.message)) { - has = true; - } - - Object.keys(val).forEach((key) => { - if (shouldUpdate(val[key])) { - has = true; - } - }); - } else if (shouldUpdate(val)) { - has = true; - } - - return has; - }, - normalizePaths, - getRealPath, -}; +export function test(val: unknown): boolean { + return ( + (typeof val === 'object' && + val && + Object.keys(val).some((key) => + shouldUpdate((val as Record)[key]), + )) || + // val.message is non-enumerable in an error + (val instanceof Error && shouldUpdate(val.message)) || + shouldUpdate(val) + ); +} /** * Normalize paths across platforms. * Filters must be ran on all platforms to guard against false positives */ -function normalizePaths(value) { +function normalizePaths(value: T): T { if (typeof value !== 'string') { return value; } @@ -101,7 +76,7 @@ function normalizePaths(value) { const homeRealRelativeToTempReal = path.relative(tempDirReal, homeDirReal); const homeRealRelativeToTemp = path.relative(tempDir, homeDirReal); - const runner = [ + const runner: ((val: string) => string)[] = [ // Replace process.cwd with (val) => val.split(cwdReal).join(''), (val) => val.split(cwd).join(''), @@ -149,28 +124,23 @@ function normalizePaths(value) { (val) => val.replace(/\\(?!")/g, '/'), ]; - let result = value; + let result = value as string; runner.forEach((current) => { result = current(result); }); - return result; + return result as T & string; } -function shouldUpdate(value) { - if (typeof value !== 'string') { - return false; - } - +function shouldUpdate(value: unknown) { // return true if value is different from normalized value - return normalizePaths(value) !== value; + return typeof value === 'string' && normalizePaths(value) !== value; } -function getRealPath(pathname) { +function getRealPath(pathname: string) { try { // eslint-disable-next-line no-restricted-properties const realPath = fs.realpathSync(pathname); - return realPath; } catch (error) { return pathname; diff --git a/website/src/data/__tests__/user.test.ts b/website/src/data/__tests__/user.test.ts index ab8ddf593b..1efe0f04da 100644 --- a/website/src/data/__tests__/user.test.ts +++ b/website/src/data/__tests__/user.test.ts @@ -57,10 +57,13 @@ describe('users data', () => { .message('') .required(), // The preview should be jest/emptyModule - preview: Joi.object({}).unknown(false).required().messages({ - 'object.base': - 'The image should be hosted on Docusaurus site, and not use remote HTTP or HTTPS URLs. It must be imported with require().', - }), + preview: Joi.object({default: Joi.any()}) + .unknown(false) + .required() + .messages({ + 'object.base': + 'The image should be hosted on Docusaurus site, and not use remote HTTP or HTTPS URLs. It must be imported with require().', + }), tags: Joi.array() .items(...TagList) .required(),