Files
syndarix/frontend/jest.setup.js
Felipe Cardoso 7b1bea2966 Refactor i18n integration and update tests for improved localization
- Updated test components (`PasswordResetConfirmForm`, `PasswordChangeForm`) to use i18n keys directly, ensuring accurate validation messages.
- Refined translations in `it.json` to standardize format and content.
- Replaced text-based labels with localized strings in `PasswordResetRequestForm` and `RegisterForm`.
- Introduced `generateLocalizedMetadata` utility and updated layout metadata generation for locale-aware SEO.
- Enhanced e2e tests with locale-prefixed routes and updated assertions for consistency.
- Added comprehensive i18n documentation (`I18N.md`) for usage, architecture, and testing.
2025-11-19 14:07:13 +01:00

320 lines
12 KiB
JavaScript

// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import 'whatwg-fetch'; // Polyfill fetch API
import { Crypto } from '@peculiar/webcrypto';
// Mock environment variables for tests
process.env.NEXT_PUBLIC_SITE_URL = 'http://localhost:3000';
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8000';
// Polyfill TransformStream for nock/msw
if (typeof global.TransformStream === 'undefined') {
const { TransformStream } = require('node:stream/web');
global.TransformStream = TransformStream;
}
// Mock window object
global.window = global.window || {};
// Mock BroadcastChannel for MSW
global.BroadcastChannel = class BroadcastChannel {
constructor(name) {
this.name = name;
}
postMessage() {}
close() {}
addEventListener() {}
removeEventListener() {}
};
// Mock IntersectionObserver for components that use viewport detection
global.IntersectionObserver = class IntersectionObserver {
constructor(callback) {
this.callback = callback;
}
observe() {
// Immediately trigger the callback with isIntersecting: true for tests
this.callback([{ isIntersecting: true }]);
}
unobserve() {}
disconnect() {}
};
// Use real Web Crypto API polyfill for Node environment
const cryptoPolyfill = new Crypto();
// Store references before assignment
const subtleRef = cryptoPolyfill.subtle;
const getRandomValuesRef = cryptoPolyfill.getRandomValues.bind(cryptoPolyfill);
// Use Object.defineProperty to ensure properties aren't overridden
if (!global.crypto) {
global.crypto = {};
}
Object.defineProperty(global.crypto, 'subtle', {
value: subtleRef,
writable: false,
configurable: true,
enumerable: true,
});
Object.defineProperty(global.crypto, 'getRandomValues', {
value: getRandomValuesRef,
writable: false,
configurable: true,
enumerable: true,
});
// Mock TextEncoder/TextDecoder if not available
if (typeof TextEncoder === 'undefined') {
global.TextEncoder = class TextEncoder {
encode(str) {
const buf = Buffer.from(str, 'utf-8');
return new Uint8Array(buf);
}
};
}
if (typeof TextDecoder === 'undefined') {
global.TextDecoder = class TextDecoder {
decode(arr) {
return Buffer.from(arr).toString('utf-8');
}
};
}
// Mock localStorage (must be on global to satisfy typeof checks)
global.localStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn(),
};
// Mock sessionStorage (must be on global to satisfy typeof checks)
global.sessionStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn(),
};
// Suppress console logs during tests (unless VERBOSE=true)
const VERBOSE = process.env.VERBOSE === 'true';
if (!VERBOSE) {
global.console = {
...console,
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
}
// Mock next-intl/server for server-side translations
jest.mock('next-intl/server', () => ({
getTranslations: jest.fn(async ({ locale: _locale, namespace: _namespace }) => {
return (key) => key;
}),
getMessages: jest.fn(async () => ({})),
}));
// Mock next-intl for all tests
jest.mock('next-intl', () => ({
useTranslations: (namespace) => {
// Return actual English translations for tests
const translations = {
auth: {
login: {
emailLabel: 'Email',
emailPlaceholder: 'Enter your email',
passwordLabel: 'Password',
passwordPlaceholder: 'Enter your password',
loginButton: 'Sign in',
loginButtonLoading: 'Signing in...',
forgotPassword: 'Forgot password?',
noAccount: "Don't have an account?",
registerLink: 'Sign up',
successMessage: 'Login successful',
},
register: {
firstNameLabel: 'First Name',
firstNamePlaceholder: 'Enter your first name',
lastNameLabel: 'Last Name',
lastNamePlaceholder: 'Enter your last name',
emailLabel: 'Email',
emailPlaceholder: 'Enter your email',
passwordLabel: 'Password',
passwordPlaceholder: 'Enter your password',
confirmPasswordLabel: 'Confirm Password',
confirmPasswordPlaceholder: 'Confirm your password',
registerButton: 'Create account',
registerButtonLoading: 'Creating account...',
hasAccount: 'Already have an account?',
loginLink: 'Sign in',
required: '*',
firstNameRequired: 'First name is required',
firstNameMinLength: 'First name must be at least 2 characters',
firstNameMaxLength: 'First name must not exceed 50 characters',
lastNameMaxLength: 'Last name must not exceed 50 characters',
passwordRequired: 'Password is required',
passwordMinLength: 'Password must be at least 8 characters',
passwordNumber: 'Password must contain at least one number',
passwordUppercase: 'Password must contain at least one uppercase letter',
confirmPasswordRequired: 'Please confirm your password',
passwordMismatch: 'Passwords do not match',
unexpectedError: 'An unexpected error occurred. Please try again.',
passwordRequirements: {
minLength: 'At least 8 characters',
hasNumber: 'Contains a number',
hasUppercase: 'Contains an uppercase letter',
},
},
passwordReset: {
emailLabel: 'Email',
emailPlaceholder: 'Enter your email',
sendResetLinkButton: 'Send reset link',
sendResetLinkButtonLoading: 'Sending...',
instructions:
'Enter your email address and we will send you a link to reset your password.',
successMessage:
'If an account exists with that email, you will receive a password reset link.',
unexpectedError: 'An unexpected error occurred. Please try again.',
backToLogin: 'Back to login',
rememberPassword: 'Remember your password?',
},
passwordResetConfirm: {
newPasswordLabel: 'New Password',
newPasswordPlaceholder: 'Enter your new password',
confirmPasswordLabel: 'Confirm Password',
confirmPasswordPlaceholder: 'Confirm your new password',
resetButton: 'Reset password',
resetButtonLoading: 'Resetting...',
instructions: 'Enter your new password below.',
successMessage: 'Your password has been successfully reset.',
backToLogin: 'Back to login',
rememberPassword: 'Remember your password?',
required: '*',
newPasswordRequired: 'New password is required',
newPasswordMinLength: 'Password must be at least 8 characters',
newPasswordNumber: 'Password must contain at least one number',
newPasswordUppercase: 'Password must contain at least one uppercase letter',
confirmPasswordRequired: 'Please confirm your password',
passwordMismatch: 'Passwords do not match',
unexpectedError: 'An unexpected error occurred. Please try again.',
passwordRequirements: {
minLength: 'At least 8 characters',
hasNumber: 'Contains a number',
hasUppercase: 'Contains an uppercase letter',
},
},
},
settings: {
password: {
title: 'Change Password',
subtitle: 'Update your password to keep your account secure',
currentPasswordLabel: 'Current Password',
currentPasswordPlaceholder: 'Enter your current password',
newPasswordLabel: 'New Password',
newPasswordPlaceholder: 'Enter your new password',
confirmPasswordLabel: 'Confirm New Password',
confirmPasswordPlaceholder: 'Confirm your new password',
updateButton: 'Update password',
updateButtonLoading: 'Updating...',
currentPasswordRequired: 'Current password is required',
newPasswordRequired: 'New password is required',
newPasswordMinLength: 'Password must be at least 8 characters',
newPasswordNumber: 'Password must contain at least one number',
newPasswordUppercase: 'Password must contain at least one uppercase letter',
newPasswordLowercase: 'Password must contain at least one lowercase letter',
newPasswordSpecial: 'Password must contain at least one special character',
confirmPasswordRequired: 'Please confirm your new password',
passwordMismatch: 'Passwords do not match',
unexpectedError: 'An unexpected error occurred. Please try again.',
passwordRequirements: {
minLength: 'At least 8 characters',
hasNumber: 'Contains a number',
hasUppercase: 'Contains an uppercase letter',
hasLowercase: 'Contains a lowercase letter',
hasSpecial: 'Contains a special character',
},
},
profile: {
title: 'Profile Settings',
subtitle: 'Manage your personal information',
firstNameLabel: 'First Name',
firstNamePlaceholder: 'Enter your first name',
lastNameLabel: 'Last Name',
lastNamePlaceholder: 'Enter your last name',
emailLabel: 'Email',
emailDescription: 'Email cannot be changed. Contact support if you need to update it.',
updateButton: 'Save changes',
updateButtonLoading: 'Saving...',
resetButton: 'Cancel',
firstNameRequired: 'First name is required',
firstNameMinLength: 'First name must be at least 2 characters',
firstNameMaxLength: 'First name must not exceed 50 characters',
lastNameMaxLength: 'Last name must not exceed 50 characters',
emailInvalid: 'Please enter a valid email address',
unexpectedError: 'An unexpected error occurred. Please try again.',
},
},
navigation: {
dashboard: 'Dashboard',
settings: 'Settings',
admin: 'Admin',
logout: 'Logout',
profile: 'Profile',
password: 'Password',
sessions: 'Sessions',
},
validation: {
required: 'This field is required',
email: 'Please enter a valid email address',
minLength: 'Must be at least 8 characters',
},
errors: {
validation: {
required: 'This field is required',
email: 'Please enter a valid email address',
passwordWeak: 'Password must contain at least one number and one uppercase letter',
},
},
};
// Helper to get nested value from object by dot notation
const get = (obj, path) => {
return path.split('.').reduce((acc, part) => acc?.[part], obj);
};
return (key) => {
const fullKey = namespace ? `${namespace}.${key}` : key;
return get(translations, fullKey) || key;
};
},
useLocale: () => 'en', // Default to English locale for tests
}));
// Reset storage mocks before each test
beforeEach(() => {
// Don't use clearAllMocks - it breaks the mocks
// Instead reset individual storage mock return values
if (global.localStorage && typeof global.localStorage.getItem.mockReset === 'function') {
global.localStorage.getItem.mockReset().mockReturnValue(null);
global.localStorage.setItem.mockReset();
global.localStorage.removeItem.mockReset();
}
if (global.sessionStorage && typeof global.sessionStorage.getItem.mockReset === 'function') {
global.sessionStorage.getItem.mockReset().mockReturnValue(null);
global.sessionStorage.setItem.mockReset();
global.sessionStorage.removeItem.mockReset();
}
});