import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { tenantService } from '../../services/tenant.service' import TenantCreateForm from '../TenantCreateForm' // Mock tenant service vi.mock('../../services/tenant.service', () => ({ tenantService: { createTenant: vi.fn(), }, })) describe('TenantCreateForm', () => { beforeEach(() => { vi.clearAllMocks() }) describe('Form Rendering', () => { it('should render all required form fields', () => { // Act render() // Assert - Basic Info Section expect(screen.getByLabelText(/^tenant code/i)).toBeInTheDocument() expect(screen.getByLabelText(/^company name \*$/i)).toBeInTheDocument() expect(screen.getByLabelText(/company name \(english\)/i)).toBeInTheDocument() expect(screen.getByLabelText(/^tax id$/i)).toBeInTheDocument() expect(screen.getByLabelText(/employee prefix/i)).toBeInTheDocument() // Assert - Contact Info Section expect(screen.getByLabelText(/phone/i)).toBeInTheDocument() expect(screen.getByLabelText(/address/i)).toBeInTheDocument() expect(screen.getByLabelText(/website/i)).toBeInTheDocument() // Assert - Plan Settings Section expect(screen.getByLabelText(/plan id/i)).toBeInTheDocument() expect(screen.getByLabelText(/max users/i)).toBeInTheDocument() expect(screen.getByLabelText(/storage quota/i)).toBeInTheDocument() // Assert - Admin Account Section expect(screen.getByLabelText(/admin username/i)).toBeInTheDocument() expect(screen.getByLabelText(/admin email/i)).toBeInTheDocument() expect(screen.getByLabelText(/admin name/i)).toBeInTheDocument() expect(screen.getByLabelText(/admin password/i)).toBeInTheDocument() // Assert - Buttons expect(screen.getByRole('button', { name: /create tenant/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument() }) it('should have default values for plan settings', () => { // Act render() // Assert const planIdInput = screen.getByLabelText(/plan id/i) as HTMLInputElement const maxUsersInput = screen.getByLabelText(/max users/i) as HTMLInputElement const storageQuotaInput = screen.getByLabelText(/storage quota/i) as HTMLInputElement expect(planIdInput.value).toBe('starter') expect(maxUsersInput.value).toBe('5') expect(storageQuotaInput.value).toBe('100') }) }) describe('Form Validation', () => { it('should show error when tenant code is empty', async () => { // Arrange const user = userEvent.setup() render() // Act const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/tenant code is required/i)).toBeInTheDocument() }) }) it('should show error when company name is empty', async () => { // Arrange const user = userEvent.setup() render() // Act const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/company name is required/i)).toBeInTheDocument() }) }) it('should validate tax id format (8 digits)', async () => { // Arrange const user = userEvent.setup() render() // Act const taxIdInput = screen.getByLabelText(/tax id/i) await user.type(taxIdInput, '123') // Invalid: less than 8 digits const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/tax id must be 8 digits/i)).toBeInTheDocument() }) }) it.skip('should validate email format', async () => { // Arrange const user = userEvent.setup() render() // Act - Fill required fields first await user.type(screen.getByLabelText(/^tenant code/i), 'TEST') await user.type(screen.getByLabelText(/^company name \*$/i), '測試公司') await user.type(screen.getByLabelText(/employee prefix/i), 'T') await user.type(screen.getByLabelText(/admin username/i), 'admin') await user.type(screen.getByLabelText(/admin email/i), 'invalidemail') // Missing @ await user.type(screen.getByLabelText(/admin name/i), 'Admin User') await user.type(screen.getByLabelText(/admin password/i), 'TempPass123!') const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { const errorElement = screen.queryByText(/invalid email format/i) expect(errorElement).toBeInTheDocument() }, { timeout: 3000 }) }) it('should validate password length (minimum 8 characters)', async () => { // Arrange const user = userEvent.setup() render() // Act const passwordInput = screen.getByLabelText(/admin password/i) await user.type(passwordInput, '12345') // Less than 8 characters const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/password must be at least 8 characters/i)).toBeInTheDocument() }) }) it('should validate employee prefix is required', async () => { // Arrange const user = userEvent.setup() render() // Act const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/employee prefix is required/i)).toBeInTheDocument() }) }) }) describe('Form Submission', () => { it('should submit form with valid data', async () => { // Arrange const user = userEvent.setup() const onSuccess = vi.fn() const mockResponse = { message: 'Tenant created successfully', tenant: { id: 1, code: 'TEST', name: '測試公司', keycloak_realm: 'porscheworld-test', }, admin_user: { id: 1, username: 'admin', email: 'admin@test.com', }, keycloak_realm: 'porscheworld-test', temporary_password: 'TempPass123!', } vi.mocked(tenantService.createTenant).mockResolvedValueOnce(mockResponse) render() // Act - Fill in form await user.type(screen.getByLabelText(/tenant code/i), 'TEST') await user.type(screen.getByLabelText(/^company name \*$/i), '測試公司') await user.type(screen.getByLabelText(/tax id/i), '12345678') await user.type(screen.getByLabelText(/employee prefix/i), 'T') await user.type(screen.getByLabelText(/admin username/i), 'admin') await user.type(screen.getByLabelText(/admin email/i), 'admin@test.com') await user.type(screen.getByLabelText(/admin name/i), 'Admin User') await user.type(screen.getByLabelText(/admin password/i), 'TempPass123!') const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(tenantService.createTenant).toHaveBeenCalledWith({ code: 'TEST', name: '測試公司', name_eng: '', tax_id: '12345678', prefix: 'T', tel: '', add: '', url: '', plan_id: 'starter', max_users: 5, storage_quota_gb: 100, admin_username: 'admin', admin_email: 'admin@test.com', admin_name: 'Admin User', admin_temp_password: 'TempPass123!', }) expect(onSuccess).toHaveBeenCalledWith(mockResponse) }) }) it('should handle API error gracefully', async () => { // Arrange const user = userEvent.setup() vi.mocked(tenantService.createTenant).mockRejectedValueOnce({ response: { data: { detail: "Tenant code 'TEST' already exists", }, }, }) render() // Act - Fill in minimum required fields await user.type(screen.getByLabelText(/tenant code/i), 'TEST') await user.type(screen.getByLabelText(/^company name \*$/i), '測試公司') await user.type(screen.getByLabelText(/employee prefix/i), 'T') await user.type(screen.getByLabelText(/admin username/i), 'admin') await user.type(screen.getByLabelText(/admin email/i), 'admin@test.com') await user.type(screen.getByLabelText(/admin name/i), 'Admin User') await user.type(screen.getByLabelText(/admin password/i), 'TempPass123!') const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert await waitFor(() => { expect(screen.getByText(/tenant code 'TEST' already exists/i)).toBeInTheDocument() }) }) it('should show loading state during submission', async () => { // Arrange const user = userEvent.setup() vi.mocked(tenantService.createTenant).mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)) ) render() // Act - Fill in minimum required fields await user.type(screen.getByLabelText(/tenant code/i), 'TEST') await user.type(screen.getByLabelText(/^company name \*$/i), '測試公司') await user.type(screen.getByLabelText(/employee prefix/i), 'T') await user.type(screen.getByLabelText(/admin username/i), 'admin') await user.type(screen.getByLabelText(/admin email/i), 'admin@test.com') await user.type(screen.getByLabelText(/admin name/i), 'Admin User') await user.type(screen.getByLabelText(/admin password/i), 'TempPass123!') const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert expect(screen.getByText(/creating/i)).toBeInTheDocument() expect(submitButton).toBeDisabled() }) it('should reset form after successful submission', async () => { // Arrange const user = userEvent.setup() const mockResponse = { message: 'Tenant created successfully', tenant: { id: 1, code: 'TEST', name: '測試公司' }, admin_user: { id: 1, username: 'admin' }, keycloak_realm: 'porscheworld-test', temporary_password: 'TempPass123!', } vi.mocked(tenantService.createTenant).mockResolvedValueOnce(mockResponse) render() // Act - Fill and submit form await user.type(screen.getByLabelText(/tenant code/i), 'TEST') await user.type(screen.getByLabelText(/^company name \*$/i), '測試公司') await user.type(screen.getByLabelText(/employee prefix/i), 'T') await user.type(screen.getByLabelText(/admin username/i), 'admin') await user.type(screen.getByLabelText(/admin email/i), 'admin@test.com') await user.type(screen.getByLabelText(/admin name/i), 'Admin User') await user.type(screen.getByLabelText(/admin password/i), 'TempPass123!') const submitButton = screen.getByRole('button', { name: /create tenant/i }) await user.click(submitButton) // Assert - Form should be reset await waitFor(() => { const codeInput = screen.getByLabelText(/tenant code/i) as HTMLInputElement const nameInput = screen.getByLabelText(/^company name \*$/i) as HTMLInputElement expect(codeInput.value).toBe('') expect(nameInput.value).toBe('') }) }) }) describe('Cancel Functionality', () => { it('should call onCancel when cancel button is clicked', async () => { // Arrange const user = userEvent.setup() const onCancel = vi.fn() render() // Act const cancelButton = screen.getByRole('button', { name: /cancel/i }) await user.click(cancelButton) // Assert expect(onCancel).toHaveBeenCalledTimes(1) }) }) })