Files
zitadel/apps/login/src/lib/theme.test.ts
T
a864434b4b feat(login): update to nextjs 16, improve caching, change default colors (#11629)
# Which Problems Are Solved

Updated the project to use the latest versions of Next.js and React, and
implemented a SWR caching strategy for key settings API calls to improve
performance. Adjusted configuration to ensure compatibility with Next.js
16.

# How the Problems Are Solved

- Updated next to 16.1.6, react & react-dom to 19.2.4, and next-intl to
4.8.3.
- Implemented `API_CACHE_ENABLED` and `API_CACHE_CONFIG` (replacing
deprecated unstable_cacheLife) for:
  - getBrandingSettings
  - getSecuritySettings
  - getPasswordComplexitySettings
  - getLoginSettings ...
- Updated default theme colors from blue to a more neutral black and
white

# Additional changes

- Updated login docker image to node 24

---------

Co-authored-by: Ramon <[email protected]>
2026-03-17 16:01:50 +01:00

460 lines
17 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
APPEARANCE_STYLES,
ComponentRoundnessConfig,
DEFAULT_COMPONENT_ROUNDNESS,
DEFAULT_THEME,
getComponentRoundness,
getThemeConfig,
ROUNDNESS_CLASSES,
SPACING_STYLES,
ThemeAppearance,
ThemeLayout,
ThemeRoundness,
ThemeSpacing,
} from "./theme";
describe("Theme Configuration", () => {
const originalEnv = process.env;
beforeEach(() => {
// Reset environment variables before each test
process.env = { ...originalEnv };
});
afterEach(() => {
// Restore original environment variables
process.env = originalEnv;
});
describe("DEFAULT_COMPONENT_ROUNDNESS", () => {
it("should have correct default values for all components", () => {
expect(DEFAULT_COMPONENT_ROUNDNESS).toEqual({
card: "mid",
button: "mid",
input: "mid",
image: "mid",
avatar: "full",
avatarContainer: "full",
themeSwitch: "full",
});
});
it("should have all required component types", () => {
const requiredComponents: (keyof ComponentRoundnessConfig)[] = [
"card",
"button",
"input",
"image",
"avatar",
"avatarContainer",
"themeSwitch",
];
requiredComponents.forEach((component) => {
expect(DEFAULT_COMPONENT_ROUNDNESS).toHaveProperty(component);
});
});
});
describe("DEFAULT_THEME", () => {
it("should have all required properties", () => {
expect(DEFAULT_THEME).toEqual({
roundness: "mid",
componentRoundness: DEFAULT_COMPONENT_ROUNDNESS,
layout: "top-to-bottom",
appearance: "flat",
spacing: "regular",
});
});
it("should have valid default values", () => {
expect(DEFAULT_THEME.roundness).toBe("mid");
expect(DEFAULT_THEME.layout).toBe("top-to-bottom");
expect(DEFAULT_THEME.appearance).toBe("flat");
expect(DEFAULT_THEME.spacing).toBe("regular");
});
});
describe("getThemeConfig", () => {
it("should return default theme when no environment variables are set", () => {
delete process.env.NEXT_PUBLIC_THEME_ROUNDNESS;
delete process.env.NEXT_PUBLIC_THEME_LAYOUT;
delete process.env.NEXT_PUBLIC_THEME_APPEARANCE;
delete process.env.NEXT_PUBLIC_THEME_SPACING;
delete process.env.NEXT_PUBLIC_THEME_BACKGROUND_IMAGE;
const config = getThemeConfig();
expect(config.roundness).toBe(DEFAULT_THEME.roundness);
expect(config.layout).toBe(DEFAULT_THEME.layout);
expect(config.appearance).toBe(DEFAULT_THEME.appearance);
expect(config.spacing).toBe(DEFAULT_THEME.spacing);
expect(config.componentRoundness).toEqual(DEFAULT_COMPONENT_ROUNDNESS);
});
it("should use global roundness from environment variable", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
const config = getThemeConfig();
expect(config.roundness).toBe("full");
});
it("should apply global roundness to all components when env var is set", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "edgy";
const config = getThemeConfig();
expect(config.componentRoundness?.card).toBe("edgy");
expect(config.componentRoundness?.button).toBe("edgy");
expect(config.componentRoundness?.input).toBe("edgy");
expect(config.componentRoundness?.image).toBe("edgy");
expect(config.componentRoundness?.avatar).toBe("edgy");
expect(config.componentRoundness?.avatarContainer).toBe("edgy");
expect(config.componentRoundness?.themeSwitch).toBe("edgy");
});
it("should use component-specific defaults when global roundness is not set", () => {
const config = getThemeConfig();
expect(config.componentRoundness).toEqual(DEFAULT_COMPONENT_ROUNDNESS);
expect(config.componentRoundness?.avatar).toBe("full");
expect(config.componentRoundness?.avatarContainer).toBe("full");
expect(config.componentRoundness?.card).toBe("mid");
});
it("should use layout from environment variable", () => {
process.env.NEXT_PUBLIC_THEME_LAYOUT = "side-by-side";
const config = getThemeConfig();
expect(config.layout).toBe("side-by-side");
});
it("should use appearance from environment variable", () => {
process.env.NEXT_PUBLIC_THEME_APPEARANCE = "material";
const config = getThemeConfig();
expect(config.appearance).toBe("material");
});
it("should use spacing from environment variable", () => {
process.env.NEXT_PUBLIC_THEME_SPACING = "compact";
const config = getThemeConfig();
expect(config.spacing).toBe("compact");
});
it("should use background image from environment variable", () => {
const backgroundUrl = "https://example.com/image.jpg";
process.env.NEXT_PUBLIC_THEME_BACKGROUND_IMAGE = backgroundUrl;
const config = getThemeConfig();
expect(config.backgroundImage).toBe(backgroundUrl);
});
it("should have undefined background image when not set", () => {
const config = getThemeConfig();
expect(config.backgroundImage).toBeUndefined();
});
it("should combine multiple environment variables correctly", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
process.env.NEXT_PUBLIC_THEME_LAYOUT = "side-by-side";
process.env.NEXT_PUBLIC_THEME_APPEARANCE = "glass";
process.env.NEXT_PUBLIC_THEME_SPACING = "compact";
process.env.NEXT_PUBLIC_THEME_BACKGROUND_IMAGE = "https://example.com/bg.png";
const config = getThemeConfig();
expect(config.roundness).toBe("full");
expect(config.layout).toBe("side-by-side");
expect(config.appearance).toBe("glass");
expect(config.spacing).toBe("compact");
expect(config.backgroundImage).toBe("https://example.com/bg.png");
});
});
describe("ROUNDNESS_CLASSES", () => {
it("should have classes for all roundness levels", () => {
expect(ROUNDNESS_CLASSES).toHaveProperty("edgy");
expect(ROUNDNESS_CLASSES).toHaveProperty("mid");
expect(ROUNDNESS_CLASSES).toHaveProperty("full");
});
it("should have classes for all component types in each roundness level", () => {
const components: (keyof ComponentRoundnessConfig)[] = [
"card",
"button",
"input",
"image",
"avatar",
"avatarContainer",
"themeSwitch",
];
const roundnessLevels: ThemeRoundness[] = ["edgy", "mid", "full"];
roundnessLevels.forEach((level) => {
components.forEach((component) => {
expect(ROUNDNESS_CLASSES[level]).toHaveProperty(component);
expect(typeof ROUNDNESS_CLASSES[level][component]).toBe("string");
expect(ROUNDNESS_CLASSES[level][component].length).toBeGreaterThan(0);
});
});
});
it("should have distinct classes for different roundness levels", () => {
const components: (keyof ComponentRoundnessConfig)[] = ["card", "button", "input"];
components.forEach((component) => {
// Each roundness level should have different classes for the same component
expect(ROUNDNESS_CLASSES.edgy[component]).not.toBe(ROUNDNESS_CLASSES.mid[component]);
expect(ROUNDNESS_CLASSES.mid[component]).not.toBe(ROUNDNESS_CLASSES.full[component]);
expect(ROUNDNESS_CLASSES.edgy[component]).not.toBe(ROUNDNESS_CLASSES.full[component]);
});
});
});
describe("getComponentRoundness", () => {
it("should return a valid CSS class string for any component", () => {
const cardClass = getComponentRoundness("card");
expect(typeof cardClass).toBe("string");
expect(cardClass.length).toBeGreaterThan(0);
});
it("should return different classes for different components with default config", () => {
const cardClass = getComponentRoundness("card");
const avatarClass = getComponentRoundness("avatar");
// Avatar defaults to full roundness, card to mid - they should be different
expect(cardClass).not.toBe(avatarClass);
});
it("should change output when global roundness environment variable changes", () => {
// Get default
const defaultCardClass = getComponentRoundness("card");
// Set to different roundness
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "edgy";
const edgyCardClass = getComponentRoundness("card");
// Should be different
expect(edgyCardClass).not.toBe(defaultCardClass);
// Try another roundness level
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
const fullCardClass = getComponentRoundness("card");
expect(fullCardClass).not.toBe(defaultCardClass);
expect(fullCardClass).not.toBe(edgyCardClass);
});
it("should return classes for all component types", () => {
const components: (keyof ComponentRoundnessConfig)[] = [
"card",
"button",
"input",
"image",
"avatar",
"avatarContainer",
"themeSwitch",
];
components.forEach((component) => {
const cssClass = getComponentRoundness(component);
expect(typeof cssClass).toBe("string");
expect(cssClass.length).toBeGreaterThan(0);
});
});
it("should apply global roundness to all components when set", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
const cardClass = getComponentRoundness("card");
const buttonClass = getComponentRoundness("button");
const avatarClass = getComponentRoundness("avatar");
// All should get classes from the "full" roundness level
expect(cardClass).toBe(ROUNDNESS_CLASSES.full.card);
expect(buttonClass).toBe(ROUNDNESS_CLASSES.full.button);
expect(avatarClass).toBe(ROUNDNESS_CLASSES.full.avatar);
});
it("should respect component-specific defaults when no global roundness is set", () => {
delete process.env.NEXT_PUBLIC_THEME_ROUNDNESS;
const avatarClass = getComponentRoundness("avatar");
const cardClass = getComponentRoundness("card");
// Avatar should use its default (full), card should use its default (mid)
expect(avatarClass).toBe(ROUNDNESS_CLASSES.full.avatar);
expect(cardClass).toBe(ROUNDNESS_CLASSES.mid.card);
});
});
describe("SPACING_STYLES", () => {
it("should have regular and compact spacing options", () => {
expect(SPACING_STYLES).toHaveProperty("regular");
expect(SPACING_STYLES).toHaveProperty("compact");
});
it("should have spacing and padding properties for each option", () => {
expect(SPACING_STYLES.regular).toHaveProperty("spacing");
expect(SPACING_STYLES.regular).toHaveProperty("padding");
expect(SPACING_STYLES.compact).toHaveProperty("spacing");
expect(SPACING_STYLES.compact).toHaveProperty("padding");
});
it("should have non-empty string values for all properties", () => {
expect(typeof SPACING_STYLES.regular.spacing).toBe("string");
expect(SPACING_STYLES.regular.spacing.length).toBeGreaterThan(0);
expect(typeof SPACING_STYLES.regular.padding).toBe("string");
expect(SPACING_STYLES.regular.padding.length).toBeGreaterThan(0);
expect(typeof SPACING_STYLES.compact.spacing).toBe("string");
expect(SPACING_STYLES.compact.spacing.length).toBeGreaterThan(0);
expect(typeof SPACING_STYLES.compact.padding).toBe("string");
expect(SPACING_STYLES.compact.padding.length).toBeGreaterThan(0);
});
it("should have different values between regular and compact", () => {
expect(SPACING_STYLES.regular.spacing).not.toBe(SPACING_STYLES.compact.spacing);
expect(SPACING_STYLES.regular.padding).not.toBe(SPACING_STYLES.compact.padding);
});
});
describe("APPEARANCE_STYLES", () => {
it("should have flat, material, and glass appearance options", () => {
expect(APPEARANCE_STYLES).toHaveProperty("flat");
expect(APPEARANCE_STYLES).toHaveProperty("material");
expect(APPEARANCE_STYLES).toHaveProperty("glass");
});
it("should have required properties for each appearance", () => {
const requiredProperties = ["card", "button", "idp-button", "typography", "background"];
Object.values(APPEARANCE_STYLES).forEach((style) => {
requiredProperties.forEach((prop) => {
expect(style).toHaveProperty(prop);
// @ts-ignore - dynamic property access
expect(typeof style[prop]).toBe("string");
// @ts-ignore - dynamic property access
expect(style[prop].length).toBeGreaterThan(0);
});
});
});
it("should have different styles for different appearances", () => {
// Each appearance should have distinct card styles
expect(APPEARANCE_STYLES.flat.card).not.toBe(APPEARANCE_STYLES.material.card);
expect(APPEARANCE_STYLES.material.card).not.toBe(APPEARANCE_STYLES.glass.card);
expect(APPEARANCE_STYLES.flat.card).not.toBe(APPEARANCE_STYLES.glass.card);
// Each appearance should have distinct button styles
expect(APPEARANCE_STYLES.flat.button).not.toBe(APPEARANCE_STYLES.material.button);
expect(APPEARANCE_STYLES.material.button).not.toBe(APPEARANCE_STYLES.glass.button);
expect(APPEARANCE_STYLES.flat.button).not.toBe(APPEARANCE_STYLES.glass.button);
});
it("should have idp-button styles for all appearances", () => {
expect(APPEARANCE_STYLES.flat["idp-button"]).toBeDefined();
expect(typeof APPEARANCE_STYLES.flat["idp-button"]).toBe("string");
expect(APPEARANCE_STYLES.material["idp-button"]).toBeDefined();
expect(typeof APPEARANCE_STYLES.material["idp-button"]).toBe("string");
expect(APPEARANCE_STYLES.glass["idp-button"]).toBeDefined();
expect(typeof APPEARANCE_STYLES.glass["idp-button"]).toBe("string");
});
});
describe("Type Safety", () => {
it("should accept valid ThemeRoundness values", () => {
const validValues: ThemeRoundness[] = ["edgy", "mid", "full"];
validValues.forEach((value) => {
expect(["edgy", "mid", "full"]).toContain(value);
});
});
it("should accept valid ThemeLayout values", () => {
const validValues: ThemeLayout[] = ["side-by-side", "top-to-bottom"];
validValues.forEach((value) => {
expect(["side-by-side", "top-to-bottom"]).toContain(value);
});
});
it("should accept valid ThemeAppearance values", () => {
const validValues: ThemeAppearance[] = ["flat", "material", "glass"];
validValues.forEach((value) => {
expect(["flat", "material", "glass"]).toContain(value);
});
});
it("should accept valid ThemeSpacing values", () => {
const validValues: ThemeSpacing[] = ["regular", "compact"];
validValues.forEach((value) => {
expect(["regular", "compact"]).toContain(value);
});
});
});
describe("Integration Tests", () => {
it("should work correctly when switching between different roundness levels", () => {
// Get classes for different roundness levels
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "edgy";
const edgyClass = getComponentRoundness("card");
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "mid";
const midClass = getComponentRoundness("card");
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
const fullClass = getComponentRoundness("card");
// All should be different
expect(edgyClass).not.toBe(midClass);
expect(midClass).not.toBe(fullClass);
expect(edgyClass).not.toBe(fullClass);
// All should be valid strings
expect(typeof edgyClass).toBe("string");
expect(typeof midClass).toBe("string");
expect(typeof fullClass).toBe("string");
});
it("should maintain consistency across all theme properties", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
process.env.NEXT_PUBLIC_THEME_LAYOUT = "side-by-side";
process.env.NEXT_PUBLIC_THEME_APPEARANCE = "material";
process.env.NEXT_PUBLIC_THEME_SPACING = "compact";
const config = getThemeConfig();
// Verify all properties are set correctly
expect(config.roundness).toBe("full");
expect(config.layout).toBe("side-by-side");
expect(config.appearance).toBe("material");
expect(config.spacing).toBe("compact");
// Verify component roundness is applied globally
expect(config.componentRoundness?.card).toBe("full");
expect(config.componentRoundness?.button).toBe("full");
});
it("should handle empty string environment variables by using defaults", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "";
process.env.NEXT_PUBLIC_THEME_LAYOUT = "";
const config = getThemeConfig();
expect(config.roundness).toBe(DEFAULT_THEME.roundness);
expect(config.layout).toBe(DEFAULT_THEME.layout);
});
});
});