Files
zitadel/apps/login/src/components/avatar.test.tsx
T
Max PeintnerandGitHub 434aeb275a feat(login): comprehensive theme system (#10848)
# Which Problems Are Solved

This PR introduces a comprehensive theme customization system for the
login application with responsive behavior and enhanced visual options.

<img width="1122" height="578" alt="Screenshot 2025-08-19 at 09 55 24"
src="https://github.com/user-attachments/assets/cdcc8948-533d-4e13-bf45-fdcc24acfb2b"
/>

# How the Problems Are Solved

## ✨ Features Added

- **🔄 Responsive Layout System**: Automatic switching between
side-by-side and top-to-bottom layouts based on screen size
- **🖼️ Background Image Support**: Custom background images configurable
via environment variables
- **⚙️ Theme Configuration**: Complete theme system with roundness,
spacing, appearance, and layout options
- **📱 Mobile-First Design**: Intelligent layout adaptation for different
screen sizes
- **🎯 Enhanced Typography**: Improved visual hierarchy with larger
titles in side-by-side mode

## 🏗️ Architecture

- **Server-Safe Theme Functions**: Theme configuration accessible on
both server and client
- **SSR-Safe Hooks**: Proper hydration handling for responsive layouts
- **Component Separation**: Clear boundaries between server and client
components
- **Two-Section Layout**: Consistent content structure across all login
pages

## 🔧 Configuration Options

All theme options are configurable via environment variables:

- `NEXT_PUBLIC_THEME_ROUNDNESS`: `edgy` | `mid` | `full`
- `NEXT_PUBLIC_THEME_LAYOUT`: `side-by-side` | `top-to-bottom`
- `NEXT_PUBLIC_THEME_APPEARANCE`: `flat` | `material`
- `NEXT_PUBLIC_THEME_SPACING`: `regular` | `compact`
- `NEXT_PUBLIC_THEME_BACKGROUND_IMAGE`: Custom background image URL

## 📄 Pages Updated

Updated all major login pages to use the new two-section responsive
layout:
- Login name entry
- Password verification
- MFA verification
- User registration
- Account selection
- Device authorization
- Logout confirmation

## 📚 Documentation

- **THEME_ARCHITECTURE.md**: Complete technical documentation of the
theme system
- **THEME_CUSTOMIZATION.md**: User-friendly guide with examples and
troubleshooting

## 🚀 Benefits

- **Better UX**: Responsive design that works seamlessly across all
devices
- **Brand Flexibility**: Easy customization to match any brand identity
- **Maintainable Code**: Clean separation of concerns and
well-documented architecture
- **Future-Proof**: Extensible system for additional theme options


<img width="580" height="680" alt="Screenshot 2025-08-19 at 09 22 23"
src="https://github.com/user-attachments/assets/9de8da37-6d56-4fe9-b337-5d8ad2a3ba59"
/>
<img width="599" height="689" alt="Screenshot 2025-08-19 at 09 23 45"
src="https://github.com/user-attachments/assets/26a30cc7-4017-4f4b-8b87-a49466c42b94"
/>
<img width="595" height="681" alt="Screenshot 2025-08-19 at 09 23 17"
src="https://github.com/user-attachments/assets/a3d31088-4545-4f36-aafe-1aae1253d677"
/>
2025-10-10 10:26:06 +02:00

169 lines
6.0 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render } from "@testing-library/react";
import { Avatar, getInitials } from "./avatar";
// Mock next-themes
vi.mock("next-themes", () => ({
useTheme: () => ({
resolvedTheme: "light",
}),
}));
// Mock next/image
vi.mock("next/image", () => ({
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
}));
describe("Avatar Component", () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
describe("getInitials", () => {
it("should get initials from full name", () => {
const initials = getInitials("John Doe", "[email protected]");
expect(initials).toBe("JD");
});
it("should get single initial from single name", () => {
const initials = getInitials("John", "[email protected]");
expect(initials).toBe("J");
});
it("should get initials from loginName when name is empty", () => {
const initials = getInitials("", "[email protected]");
expect(initials.length).toBeGreaterThan(0);
});
it("should handle loginName with underscore separator", () => {
const initials = getInitials("", "[email protected]");
expect(initials).toBe("jd");
});
it("should handle loginName with dash separator", () => {
const initials = getInitials("", "[email protected]");
expect(initials).toBe("jd");
});
it("should handle loginName with dot separator", () => {
const initials = getInitials("", "[email protected]");
expect(initials).toBe("jd");
});
it("should get initials from username part of email", () => {
const initials = getInitials("", "[email protected]");
expect(initials.length).toBeGreaterThan(0);
});
});
describe("Component Rendering", () => {
it("should render avatar with initials", () => {
const { container } = render(<Avatar name="John Doe" loginName="[email protected]" />);
expect(container.querySelector(".avatar, [class*='avatar'], div")).toBeTruthy();
});
it("should render with different sizes", () => {
const sizes: Array<"small" | "base" | "large"> = ["small", "base", "large"];
sizes.forEach((size) => {
const { container } = render(<Avatar size={size} name="Test User" loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
});
it("should render with shadow prop", () => {
const { container } = render(<Avatar name="Test User" loginName="[email protected]" shadow={true} />);
expect(container.firstChild).toBeTruthy();
});
it("should render without shadow prop", () => {
const { container } = render(<Avatar name="Test User" loginName="[email protected]" shadow={false} />);
expect(container.firstChild).toBeTruthy();
});
it("should render with image URL", () => {
const { container } = render(
<Avatar name="Test User" loginName="[email protected]" imageUrl="https://example.com/avatar.jpg" />,
);
expect(container.firstChild).toBeTruthy();
});
});
describe("Theme Integration", () => {
it("should apply theme-based roundness", () => {
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
const avatar = container.firstChild as HTMLElement;
// Should render with some styling
expect(avatar).toBeTruthy();
});
it("should respect theme roundness changes", () => {
// Default theme
const { container: container1 } = render(<Avatar name="Test User" loginName="[email protected]" />);
const avatar1 = container1.firstChild as HTMLElement;
// Change theme
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "edgy";
const { container: container2 } = render(<Avatar name="Test User" loginName="[email protected]" />);
const avatar2 = container2.firstChild as HTMLElement;
// Both should render
expect(avatar1).toBeTruthy();
expect(avatar2).toBeTruthy();
});
it("should render with full roundness", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
it("should render with mid roundness", () => {
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "mid";
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
});
describe("Color Generation", () => {
it("should generate consistent colors for same loginName", () => {
const { container: container1 } = render(<Avatar name="Test" loginName="[email protected]" />);
const { container: container2 } = render(<Avatar name="Test" loginName="[email protected]" />);
expect(container1.firstChild).toBeTruthy();
expect(container2.firstChild).toBeTruthy();
});
it("should render for different loginNames", () => {
const { container: container1 } = render(<Avatar name="User 1" loginName="[email protected]" />);
const { container: container2 } = render(<Avatar name="User 2" loginName="[email protected]" />);
expect(container1.firstChild).toBeTruthy();
expect(container2.firstChild).toBeTruthy();
});
});
describe("Props Validation", () => {
it("should handle null name", () => {
const { container } = render(<Avatar name={null} loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
it("should handle undefined name", () => {
const { container } = render(<Avatar name={undefined} loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
it("should require loginName", () => {
const { container } = render(<Avatar name="Test" loginName="[email protected]" />);
expect(container.firstChild).toBeTruthy();
});
});
});