chore: fix and migrate unit tests to vitest

This commit is contained in:
Christian Andolf
2025-06-27 13:35:41 +02:00
parent a91c28096d
commit ff40ef72c4
26 changed files with 818 additions and 2835 deletions

View File

@@ -22,6 +22,7 @@ DEPLOY_PRIME_URL="test"
NEXTAUTH_SECRET="test"
NEXT_PUBLIC_PUBLIC_URL="test"
NEXTAUTH_URL="test"
NODE_ENV="test"
REVALIDATE_SECRET="test"
SEAMLESS_LOGIN_DA="test"
SEAMLESS_LOGIN_DE="test"

View File

@@ -1,14 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithIntl() {
const intl = await getIntl()
return <h1>{intl.formatMessage({
defaultMessage: "some value goes here",
description: {}
})}</h1>
}

View File

@@ -1,27 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
const someId = 'someId'
const data = {
type: 'something'
}
return (
<h1>
{intl.formatMessage({
defaultMessage: someId,
description: {}
})}
{intl.formatMessage({
defaultMessage: data.type,
description: {}
})}
</h1>
)
}

View File

@@ -1,21 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
return (
<h1>
{true ? intl.formatMessage({
defaultMessage: "Some true string",
description: {}
}) : intl.formatMessage({
defaultMessage: "Some false string",
description: {}
})}
</h1>
)
}

View File

@@ -1,19 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
const variable = "some value"
return (
<h1>
{intl.formatMessage({
defaultMessage: `String with {variable}`,
description: {}
}, { variable: variable })}
</h1>
)
}

View File

@@ -1,11 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithIntl() {
const intl = await getIntl()
return <h1>{intl.formatMessage({ id: "some value goes here" })}</h1>
}

View File

@@ -1,21 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
const someId = 'someId'
const data = {
type: 'something'
}
return (
<h1>
{intl.formatMessage({ id: someId })}
{intl.formatMessage({ id: data.type })}
</h1>
)
}

View File

@@ -1,17 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
return (
<h1>
{intl.formatMessage(
true ? { id: "Some true string" } : { id: "Some false string" }
)}
</h1>
)
}

View File

@@ -1,16 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithTernaryInside() {
const intl = await getIntl()
const variable = "some value"
return (
<h1>
{intl.formatMessage({ id: `String with ${variable}` })}
</h1>
)
}

View File

@@ -1,13 +0,0 @@
// Run the lokalise.ts through Jiti
import { fileURLToPath } from "node:url"
import createJiti from "jiti"
const lokalise = createJiti(fileURLToPath(import.meta.url))("./lokalise.ts")
lokalise.codemod([
"**/*.{ts,tsx}",
"!**/codemods/lokalise/**", // ignore itself
"!**/node_modules/**", // ignore node_modules
])

View File

@@ -1,134 +0,0 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, test } from "@jest/globals"
import { IndentationText, Project } from "ts-morph"
import { processSourceFile } from "./lokalise"
describe("Lokalise codemod", () => {
test("serverComponent: intl.formatMessage with only id", () => {
const input = readFileSync(
join(__dirname, "./fixtures/serverComponentOnlyId.tsx"),
{
encoding: "utf-8",
}
)
const expected = readFileSync(
join(__dirname, "./expectations/serverComponentOnlyId.tsx"),
{
encoding: "utf-8",
}
)
const project = new Project({
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
},
})
const sourceFile = project.createSourceFile(
"./fixtures/serverComponent.tsx",
input
)
processSourceFile(sourceFile)
const result = sourceFile.getFullText()
expect(result).toBe(expected)
})
test("serverComponent: intl.formatMessage with ternary inside", () => {
const input = readFileSync(
join(__dirname, "./fixtures/serverComponentWithTernaryInside.tsx"),
{
encoding: "utf-8",
}
)
const expected = readFileSync(
join(__dirname, "./expectations/serverComponentWithTernaryInside.tsx"),
{
encoding: "utf-8",
}
)
const project = new Project({
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
},
})
const sourceFile = project.createSourceFile(
"./fixtures/serverComponentWithTernaryInside.tsx",
input
)
processSourceFile(sourceFile)
const result = sourceFile.getFullText()
expect(result).toBe(expected)
})
test("serverComponent: intl.formatMessage with variables", () => {
const input = readFileSync(
join(__dirname, "./fixtures/serverComponentWithVariables.tsx"),
{
encoding: "utf-8",
}
)
const expected = readFileSync(
join(__dirname, "./expectations/serverComponentWithVariables.tsx"),
{
encoding: "utf-8",
}
)
const project = new Project({
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
},
})
const sourceFile = project.createSourceFile(
"./fixtures/serverComponentWithVariables.tsx",
input
)
processSourceFile(sourceFile)
const result = sourceFile.getFullText()
expect(result).toBe(expected)
})
test("serverComponent: intl.formatMessage with variable for id", () => {
const input = readFileSync(
join(__dirname, "./fixtures/serverComponentVariableId.tsx"),
{
encoding: "utf-8",
}
)
const expected = readFileSync(
join(__dirname, "./expectations/serverComponentVariableId.tsx"),
{
encoding: "utf-8",
}
)
const project = new Project({
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
},
})
const sourceFile = project.createSourceFile(
"./fixtures/serverComponentVariableId.tsx",
input
)
processSourceFile(sourceFile)
const result = sourceFile.getFullText()
expect(result).toBe(expected)
})
})

View File

@@ -1,228 +0,0 @@
import {
type Expression,
type Node,
type ObjectLiteralExpression,
Project,
type SourceFile,
ts,
} from "ts-morph"
export function codemod(paths: string) {
const project = new Project()
project.addSourceFilesAtPaths(paths)
project.getSourceFiles().forEach((file) => {
processSourceFile(file)
file.saveSync()
})
}
export function processSourceFile(file: SourceFile): void {
file.getDescendantsOfKind(ts.SyntaxKind.CallExpression).forEach((call) => {
const expression = call.getExpression().getText()
if (expression === "intl.formatMessage") {
const formatMessageArgs = call.getArguments()
if (formatMessageArgs.length > 0) {
const firstFormatMessageArg = formatMessageArgs[0]
// Handle object literal in the first argument
if (
firstFormatMessageArg.getKind() ===
ts.SyntaxKind.ObjectLiteralExpression
) {
const expressionVariables = getVariableArguments(
firstFormatMessageArg.asKindOrThrow(
ts.SyntaxKind.ObjectLiteralExpression
)
)
if (expressionVariables === "{}") {
// No variables
transformObjectLiteral(
firstFormatMessageArg.asKindOrThrow(
ts.SyntaxKind.ObjectLiteralExpression
)
)
} else {
// Handle variables
const expressionReplacement = `intl.formatMessage(${transformObjectLiteralAndReturn(
firstFormatMessageArg.asKindOrThrow(
ts.SyntaxKind.ObjectLiteralExpression
)
)}, ${expressionVariables})`
call.replaceWithText(expressionReplacement)
}
}
// Handle ternary expressions in the first argument
else if (
firstFormatMessageArg.getKind() ===
ts.SyntaxKind.ConditionalExpression
) {
const conditional = firstFormatMessageArg.asKindOrThrow(
ts.SyntaxKind.ConditionalExpression
)
const whenTrue = conditional.getWhenTrue()
const whenFalse = conditional.getWhenFalse()
// Check for variables in message
const varArgsWhenTrue = getVariableArguments(whenTrue)
const varArgsWhenFalse = getVariableArguments(whenFalse)
// Replacements
const whenTrueReplacement =
varArgsWhenTrue !== "{}"
? `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)}, ${varArgsWhenTrue})`
: `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)})`
const whenFalseReplacement =
varArgsWhenFalse !== "{}"
? `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)}, ${varArgsWhenFalse})`
: `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)})`
// Replace the ternary expression
call.replaceWithText(
`${conditional.getCondition().getText()} ? ${whenTrueReplacement} : ${whenFalseReplacement}`
)
}
}
}
})
// Format the file using its existing formatting rules
file.formatText({
indentSize: 2,
})
}
// Helper function to transform object literals
function transformObjectLiteral(objectLiteral: ObjectLiteralExpression): void {
const idProperty = objectLiteral
.getProperties()
.find((prop) => {
const p = prop.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
return p.getName() === "id"
})
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
if (idProperty) {
const idValue = idProperty.getInitializer()?.getText()
if (idValue) {
// Add defaultMessage
if (
!objectLiteral
.getProperties()
.some(
(prop) => "getName" in prop && prop.getName() === "defaultMessage"
)
) {
objectLiteral.addPropertyAssignment({
name: "defaultMessage",
initializer: idValue,
})
}
// Remove the id property
idProperty.remove()
}
}
// Add description if not present
// if (
// !objectLiteral
// .getProperties()
// .some((prop) => "getName" in prop && prop.getName() === "description")
// ) {
// objectLiteral.addPropertyAssignment({
// name: "description",
// initializer: `{}`,
// })
// }
// Extract variables from the defaultMessage if present
const defaultMessageProp = objectLiteral
.getProperties()
.find((prop) => "getName" in prop && prop.getName() === "defaultMessage")
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
if (defaultMessageProp) {
const defaultMessageValue = defaultMessageProp.getInitializer()?.getText()
if (defaultMessageValue) {
const extractedVariables =
extractVariablesFromTemplateString(defaultMessageValue)
if (extractedVariables.length > 0) {
// Replace the variables in the defaultMessage with FormatJS placeholders
const transformedMessage = replaceWithFormatJSPlaceholders(
defaultMessageValue,
extractedVariables
)
defaultMessageProp.setInitializer(`${transformedMessage}`)
}
}
}
}
// Helper function to extract variables from a template string
function extractVariablesFromTemplateString(templateString: string): string[] {
const regex = /\${(.*?)}/g
const variables: string[] = []
let match
// Find all variable references in the template literal
while ((match = regex.exec(templateString)) !== null) {
variables.push(match[1].trim())
}
return variables
}
// Helper function to replace variables with FormatJS placeholders
function replaceWithFormatJSPlaceholders(
templateString: string,
variables: string[]
): string {
let transformedMessage = templateString
variables.forEach((variable) => {
transformedMessage = transformedMessage.replace(
`\${${variable}}`,
`{${variable}}`
)
})
return transformedMessage
}
// Helper function to get the variables from the ternary branch
function getVariableArguments(exp: Expression): string {
const idProp = exp
.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression)
.getProperties()
.find((prop) => "getName" in prop && prop.getName() === "id")
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
if (idProp) {
const extractedVariables = extractVariablesFromTemplateString(
idProp.getText()
)
if (extractedVariables.length > 0) {
return `{ ${extractedVariables.map((v) => `${v}: ${v}`).join(", ")} }`
}
}
return "{}" // Return empty if no variables found
}
// Helper function to transform object literals and return text
function transformObjectLiteralAndReturn(object: Node): string {
if (object.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
const obj = object.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression)
transformObjectLiteral(obj)
return obj.getText() // Return the transformed object literal as text
}
return object.getText()
}

View File

@@ -1,27 +0,0 @@
/* eslint-disable formatjs/enforce-description */
/* eslint-disable formatjs/enforce-default-message */
/* eslint-disable formatjs/no-id */
import { getIntl } from "@/i18n"
export default async function ServerComponentWithIntl() {
const intl = await getIntl()
const variable = "some value"
return (
<h1>
{true
? intl.formatMessage({
defaultMessage: "Some string",
description: {},
})
: intl.formatMessage(
{
defaultMessage: `Other string {variable}`,
description: {},
},
{ variable: variable }
)}
</h1>
)
}

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals"
import { describe, expect, it } from "vitest"
import accessBooking, {
ACCESS_GRANTED,

View File

@@ -1,4 +1,4 @@
import { beforeAll, describe, expect, it } from "@jest/globals"
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
import { getValidFromDate, getValidToDate } from "./getValidDates"
@@ -6,11 +6,11 @@ const NOW = new Date("2020-10-01T00:00:00Z")
describe("getValidFromDate", () => {
beforeAll(() => {
jest.useFakeTimers({ now: NOW })
vi.useFakeTimers({ now: NOW })
})
afterAll(() => {
jest.useRealTimers()
vi.useRealTimers()
})
describe("getValidFromDate", () => {

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals"
import { describe, expect, it } from "vitest"
import { getGroupedOpeningHours } from "./utils"
@@ -7,7 +7,7 @@ import type { IntlShape } from "react-intl"
// Mock IntlShape for testing
const mockIntl = {
formatMessage: ({ id }: { id: string }) => {
formatMessage: ({ defaultMessage }: { defaultMessage: string }) => {
const messages: Record<string, string> = {
Monday: "Monday",
Tuesday: "Tuesday",
@@ -19,7 +19,7 @@ const mockIntl = {
Closed: "Closed",
"Always open": "Always open",
}
return messages[id] || id
return messages[defaultMessage] || defaultMessage
},
} as IntlShape

View File

@@ -1,28 +1,24 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
import { describe, expect, test } from "@jest/globals" // importing because of type conflict with globals from Cypress
import { render, screen } from "@testing-library/react"
import { type UserEvent, userEvent } from "@testing-library/user-event"
import { User } from "@react-aria/test-utils"
import { userEvent } from "@testing-library/user-event"
import { FormProvider, useForm } from "react-hook-form"
import { afterEach, describe, expect, test } from "vitest"
import { Lang } from "@scandic-hotels/common/constants/language"
import { dt } from "@scandic-hotels/common/dt"
import { cleanup, render, screen } from "@/tests/utils"
import { getLocalizedMonthName } from "@/utils/dateFormatting"
import Date from "./index"
jest.mock("react-intl", () => ({
useIntl: () => ({
formatMessage: (message: { id: string }) => message.id,
formatNumber: (value: number) => value,
}),
}))
const testUtilUser = new User({ interactionType: "touch" })
interface FormWrapperProps {
defaultValues: Record<string, unknown>
children: React.ReactNode
onSubmit: (data: unknown) => void
onSubmit: (event: unknown) => void
}
function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) {
@@ -31,7 +27,7 @@ function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) {
})
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => onSubmit(data))}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{children}
<button type="submit">Submit</button>
</form>
@@ -39,19 +35,13 @@ function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) {
)
}
async function selectOption(user: UserEvent, name: RegExp, value: string) {
// since its not a proper Select element selectOptions from userEvent doesn't work
const select = screen.queryByRole("button", { name })
if (select) {
await user.click(select)
function selectOption(name: string, value: string) {
const selectTester = testUtilUser.createTester("Select", {
root: screen.getByTestId(name),
interactionType: "touch",
})
const option = screen.queryByRole("option", { name: value })
if (option) {
await user.click(option)
} else {
await user.click(select) // click select again to close it
}
}
selectTester.selectOption({ option: value })
}
const testCases = [
@@ -112,16 +102,19 @@ const testCases = [
]
describe("Date input", () => {
afterEach(cleanup)
test.each(testCases)(
"$description",
async ({ defaultValue, dateOfBirth, expectedOutput }) => {
const user = userEvent.setup()
const handleSubmit = jest.fn()
render(
<FormWrapper
defaultValues={{ dateOfBirth: defaultValue }}
onSubmit={handleSubmit}
onSubmit={(data) => {
expect(data).toEqual(expectedOutput)
}}
>
<Date name="dateOfBirth" />
</FormWrapper>
@@ -132,16 +125,12 @@ describe("Date input", () => {
const month = date.getMonth() + 1
const day = date.getDate()
await selectOption(user, /year/i, year.toString())
await selectOption(user, /month/i, getLocalizedMonthName(month, Lang.en))
await selectOption(user, /day/i, day.toString())
selectOption("year", year.toString())
selectOption("month", getLocalizedMonthName(month, Lang.en))
selectOption("day", day.toString())
const submitButton = screen.getByRole("button", { name: /submit/i })
await user.click(submitButton)
expect(handleSubmit).toHaveBeenCalledWith(
expect.objectContaining(expectedOutput)
)
}
)
})

View File

@@ -1,212 +0,0 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import nextJest from "next/jest.js"
import { createJsWithTsEsmPreset } from "ts-jest"
import type { Config } from "jest"
const presetConfig = createJsWithTsEsmPreset()
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
})
const config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/v8/61rbfxwx3jddjj8z_qxgm_tc0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
...presetConfig,
} satisfies Config
export default createJestConfig(config)

View File

@@ -1,10 +0,0 @@
import "@testing-library/jest-dom/jest-globals"
import "@testing-library/jest-dom"
import { jest } from "@jest/globals"
jest.mock("next/navigation", () => ({
useRouter: jest.fn(),
usePathname: jest.fn().mockReturnValue("/"),
useParams: jest.fn().mockReturnValue({ lang: "en" }),
}))

View File

@@ -15,8 +15,8 @@
"test:e2e:headless": "start-server-and-test test:setup http://127.0.0.1:3000/en/sponsoring \"cypress run --e2e\"",
"test:setup": "yarn build && yarn start",
"preinstall": "/bin/sh -c \"export $(cat .env.local | grep -v '^#' | xargs)\"",
"test": "node --experimental-vm-modules $(yarn bin jest)",
"test:watch": "node --experimental-vm-modules $(yarn bin jest) --watch",
"test": "vitest run",
"test:watch": "vitest",
"ci:build": "yarn lint && yarn test && yarn build",
"clean": "rm -rf .next",
"i18n:extract": "formatjs extract \"{actions,app,components,constants,contexts,env,hooks,i18n,lib,middlewares,netlify,providers,server,services,stores,utils}/**/*.{ts,tsx}\" --format i18n/tooling/formatter.mjs --out-file i18n/tooling/extracted.json",
@@ -120,14 +120,13 @@
"@eslint/js": "^9.26.0",
"@formatjs/cli": "^6.7.1",
"@lokalise/node-api": "^14.0.0",
"@react-aria/test-utils": "1.0.0-alpha.8",
"@scandic-hotels/common": "workspace:*",
"@scandic-hotels/typescript-config": "workspace:*",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/adm-zip": "^0.5.7",
"@types/jest": "^29.5.14",
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
"@types/jsonwebtoken": "^9",
"@types/lodash-es": "^4",
@@ -136,7 +135,9 @@
"@types/react-dom": "19.1.0",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react": "^4.6.0",
"adm-zip": "^0.5.16",
"babel-plugin-formatjs": "^10.5.39",
"cypress": "^14.3.3",
"dotenv": "^16.5.0",
"eslint": "^9",
@@ -144,20 +145,20 @@
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jiti": "^1.21.0",
"jsdom": "^26.1.0",
"json-sort-cli": "^4.0.9",
"lint-staged": "^15.5.2",
"netlify-plugin-cypress": "^2.2.1",
"prettier": "^3.5.3",
"schema-dts": "^1.1.5",
"start-server-and-test": "^2.0.11",
"ts-jest": "^29.3.2",
"ts-morph": "^25.0.1",
"ts-node": "^10.9.2",
"typescript": "5.8.3",
"typescript-plugin-css-modules": "^5.1.0"
"typescript-plugin-css-modules": "^5.1.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"engines": {
"node": "22"

View File

@@ -0,0 +1,27 @@
import { render, type RenderOptions } from "@testing-library/react"
import React, { type ReactElement } from "react"
import { Lang } from "@scandic-hotels/common/constants/language"
import messages from "@/i18n/dictionaries/en.json"
import ClientIntlProvider from "@/i18n/Provider"
function AllTheProviders({ children }: { children: React.ReactNode }) {
return (
<ClientIntlProvider
defaultLocale={Lang.en}
locale={Lang.en}
messages={messages}
>
{children}
</ClientIntlProvider>
)
}
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) => render(ui, { wrapper: AllTheProviders, ...options })
export * from "@testing-library/react"
export { customRender as render }

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "@jest/globals"
import { describe, expect, test } from "vitest"
import { z } from "zod"
import { parseSearchParams, serializeSearchParams } from "./searchParams"

View File

@@ -0,0 +1,30 @@
import { vi } from "vitest"
process.env.TZ = "UTC"
vi.mock("next/navigation", () => ({
useRouter: vi.fn(),
usePathname: vi.fn().mockReturnValue("/"),
useParams: vi.fn().mockReturnValue({ lang: "en" }),
}))
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
Object.defineProperty(window, "CSS", {
writable: true,
value: {
escape: vi.fn(),
},
})

View File

@@ -0,0 +1,25 @@
import react from "@vitejs/plugin-react"
import tsconfigPaths from "vite-tsconfig-paths"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [
tsconfigPaths(),
react({
babel: {
plugins: [
[
"formatjs",
{
idInterpolationPattern: "[sha512:contenthash:base64:6]",
},
],
],
},
}),
],
test: {
environment: "jsdom",
setupFiles: ["./vitest-setup.ts"],
},
})

View File

@@ -28,18 +28,17 @@ describe("getSearchTokens", () => {
}
const result = getSearchTokens(location as Location)
expect(result.toSorted()).toEqual(
[
"ångström",
"café",
"münchen",
"frånce",
"angstrom",
"cafe",
"munchen",
"france",
].toSorted()
)
expect(result).toEqual([
"angstrom",
"cafe",
"munchen",
"france",
"ångström",
"café",
"münchen",
"frånce",
])
})
it("should filter out empty or falsey tokens", () => {

2689
yarn.lock

File diff suppressed because it is too large Load Diff