chore: fix and migrate unit tests to vitest
This commit is contained in:
@@ -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"
|
||||
@@ -52,4 +53,4 @@ NEXT_PUBLIC_SENTRY_ENVIRONMENT="test"
|
||||
NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE="0"
|
||||
SITEMAP_SYNC_SECRET="test"
|
||||
|
||||
BRANCH="test"
|
||||
BRANCH="test"
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
])
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "@jest/globals"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
@@ -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" }),
|
||||
}))
|
||||
@@ -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"
|
||||
|
||||
27
apps/scandic-web/tests/utils.tsx
Normal file
27
apps/scandic-web/tests/utils.tsx
Normal 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 }
|
||||
@@ -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"
|
||||
|
||||
30
apps/scandic-web/vitest-setup.ts
Normal file
30
apps/scandic-web/vitest-setup.ts
Normal 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(),
|
||||
},
|
||||
})
|
||||
25
apps/scandic-web/vitest.config.js
Normal file
25
apps/scandic-web/vitest.config.js
Normal 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"],
|
||||
},
|
||||
})
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user