Merge pull request #418 from buster-so/cursor/create-unit-tests-for-schema-defaults-b3ac

Add comprehensive test suites for shared server type schemas
This commit is contained in:
Nate Kelley 2025-07-07 08:30:12 -06:00 committed by GitHub
commit 0b5c8cb2e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 2837 additions and 0 deletions

View File

@ -0,0 +1,306 @@
import { describe, expect, it } from 'vitest';
import {
ChatMessageSchema,
ReasoningMessageSchema,
ResponseMessageSchema,
} from './chat-message.types';
describe('ChatMessageSchema', () => {
it('should parse a valid complete chat message', () => {
const validMessage = {
id: 'msg-123',
request_message: {
request: 'What is the revenue?',
sender_id: 'user-123',
sender_name: 'John Doe',
sender_avatar: 'https://example.com/avatar.jpg',
},
response_messages: {},
response_message_ids: [],
reasoning_message_ids: [],
reasoning_messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
final_reasoning_message: null,
feedback: null,
is_completed: true,
};
const result = ChatMessageSchema.safeParse(validMessage);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('msg-123');
expect(result.data.is_completed).toBe(true);
}
});
it('should handle null request message', () => {
const messageWithNullRequest = {
id: 'msg-123',
request_message: null,
response_messages: {},
response_message_ids: [],
reasoning_message_ids: [],
reasoning_messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
final_reasoning_message: null,
feedback: null,
is_completed: false,
};
const result = ChatMessageSchema.safeParse(messageWithNullRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.request_message).toBeNull();
}
});
it('should handle optional fields in request message', () => {
const messageWithOptionalFields = {
id: 'msg-123',
request_message: {
request: 'What is the revenue?',
sender_id: 'user-123',
sender_name: 'John Doe',
// sender_avatar is optional
},
response_messages: {},
response_message_ids: [],
reasoning_message_ids: [],
reasoning_messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
final_reasoning_message: null,
feedback: null,
is_completed: false,
};
const result = ChatMessageSchema.safeParse(messageWithOptionalFields);
expect(result.success).toBe(true);
});
});
describe('ResponseMessageSchema', () => {
it('should parse text response message', () => {
const textMessage = {
id: 'resp-123',
type: 'text',
message: 'Here is your answer',
};
const result = ResponseMessageSchema.safeParse(textMessage);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('text');
if (result.data.type === 'text') {
expect(result.data.message).toBe('Here is your answer');
}
}
});
it('should parse file response message with metadata', () => {
const fileMessage = {
id: 'resp-456',
type: 'file',
file_type: 'metric',
file_name: 'revenue_analysis.yaml',
version_number: 1,
filter_version_id: 'filter-123',
metadata: [
{
status: 'completed',
message: 'Analysis complete',
timestamp: 1640995200000,
},
],
};
const result = ResponseMessageSchema.safeParse(fileMessage);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('file');
if (result.data.type === 'file') {
expect(result.data.file_type).toBe('metric');
expect(result.data.metadata).toHaveLength(1);
}
}
});
it('should handle optional metadata fields', () => {
const fileMessageWithoutOptionals = {
id: 'resp-456',
type: 'file',
file_type: 'dashboard',
file_name: 'sales_dashboard.yaml',
version_number: 2,
};
const result = ResponseMessageSchema.safeParse(fileMessageWithoutOptionals);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('file');
if (result.data.type === 'file') {
expect(result.data.file_type).toBe('dashboard');
expect(result.data.metadata).toBeUndefined();
expect(result.data.filter_version_id).toBeUndefined();
}
}
});
});
describe('ReasoningMessageSchema', () => {
it('should parse text reasoning message', () => {
const textReasoning = {
id: 'reason-123',
type: 'text',
title: 'Analyzing Data',
status: 'loading',
};
const result = ReasoningMessageSchema.safeParse(textReasoning);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('text');
expect(result.data.title).toBe('Analyzing Data');
expect(result.data.status).toBe('loading');
}
});
it('should parse files reasoning message with nested file objects', () => {
const filesReasoning = {
id: 'reason-456',
type: 'files',
title: 'Generated Files',
status: 'completed',
file_ids: ['file-1', 'file-2'],
files: {
'file-1': {
id: 'file-1',
file_type: 'metric',
file_name: 'revenue.yaml',
version_number: 1,
status: 'completed',
file: {
text: 'metric content here',
modified: [
[0, 10],
[20, 30],
],
},
},
'file-2': {
id: 'file-2',
file_type: 'dashboard',
file_name: 'dashboard.yaml',
status: 'loading',
file: {
text: 'dashboard content',
},
},
},
};
const result = ReasoningMessageSchema.safeParse(filesReasoning);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('files');
if (result.data.type === 'files') {
expect(result.data.file_ids).toHaveLength(2);
expect(result.data.files['file-1'].file_type).toBe('metric');
expect(result.data.files['file-1'].file.modified).toEqual([
[0, 10],
[20, 30],
]);
expect(result.data.files['file-2'].file.modified).toBeUndefined();
}
}
});
it('should parse pills reasoning message with nested pill containers', () => {
const pillsReasoning = {
id: 'reason-789',
type: 'pills',
title: 'Related Items',
status: 'completed',
pill_containers: [
{
title: 'Metrics',
pills: [
{
text: 'Revenue',
type: 'metric',
id: 'metric-1',
},
{
text: 'Profit',
type: 'metric',
id: 'metric-2',
},
],
},
{
title: 'Dashboards',
pills: [
{
text: 'Sales Overview',
type: 'dashboard',
id: 'dashboard-1',
},
],
},
],
};
const result = ReasoningMessageSchema.safeParse(pillsReasoning);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('pills');
if (result.data.type === 'pills') {
expect(result.data.pill_containers).toHaveLength(2);
expect(result.data.pill_containers[0].pills).toHaveLength(2);
expect(result.data.pill_containers[0].pills[0].type).toBe('metric');
expect(result.data.pill_containers[1].pills[0].text).toBe('Sales Overview');
}
}
});
it('should handle optional finished_reasoning field', () => {
const reasoningWithFinished = {
id: 'reason-123',
type: 'text',
title: 'Complete Analysis',
status: 'completed',
finished_reasoning: true,
};
const result = ReasoningMessageSchema.safeParse(reasoningWithFinished);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.finished_reasoning).toBe(true);
}
});
it('should handle all optional fields in text reasoning', () => {
const textWithOptionals = {
id: 'reason-999',
type: 'text',
title: 'Analysis',
secondary_title: 'Revenue Analysis',
message: 'Detailed analysis message',
message_chunk: 'Partial message',
status: 'loading',
finished_reasoning: false,
};
const result = ReasoningMessageSchema.safeParse(textWithOptionals);
expect(result.success).toBe(true);
if (result.success) {
if (result.data.type === 'text') {
expect(result.data.secondary_title).toBe('Revenue Analysis');
expect(result.data.message).toBe('Detailed analysis message');
expect(result.data.message_chunk).toBe('Partial message');
}
}
});
});

View File

@ -0,0 +1,423 @@
import { describe, expect, it } from 'vitest';
import {
AssetPermissionRoleSchema,
BusterShareIndividualSchema,
ChatCreateHandlerRequestSchema,
ChatCreateRequestSchema,
ChatWithMessagesSchema,
} from './chat.types';
describe('AssetPermissionRoleSchema', () => {
it('should accept valid role values', () => {
const validRoles = ['viewer', 'editor', 'owner'];
for (const role of validRoles) {
const result = AssetPermissionRoleSchema.safeParse(role);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(role);
}
}
});
it('should reject invalid role values', () => {
const invalidRoles = ['admin', 'user', 'guest', '', 'VIEWER'];
for (const role of invalidRoles) {
const result = AssetPermissionRoleSchema.safeParse(role);
expect(result.success).toBe(false);
}
});
});
describe('BusterShareIndividualSchema', () => {
it('should parse valid individual sharing configuration', () => {
const validIndividual = {
email: 'user@example.com',
role: 'editor',
name: 'John Doe',
};
const result = BusterShareIndividualSchema.safeParse(validIndividual);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('user@example.com');
expect(result.data.role).toBe('editor');
expect(result.data.name).toBe('John Doe');
}
});
it('should handle optional name field', () => {
const individualWithoutName = {
email: 'test@example.com',
role: 'viewer',
};
const result = BusterShareIndividualSchema.safeParse(individualWithoutName);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('test@example.com');
expect(result.data.role).toBe('viewer');
expect(result.data.name).toBeUndefined();
}
});
it('should validate email format', () => {
const invalidEmails = ['invalid-email', 'test@', '@example.com', 'test.example.com', ''];
for (const email of invalidEmails) {
const individual = {
email,
role: 'viewer',
};
const result = BusterShareIndividualSchema.safeParse(individual);
expect(result.success).toBe(false);
}
});
it('should validate role field', () => {
const individual = {
email: 'valid@example.com',
role: 'invalidRole',
};
const result = BusterShareIndividualSchema.safeParse(individual);
expect(result.success).toBe(false);
});
});
describe('ChatWithMessagesSchema', () => {
it('should parse valid complete chat with messages', () => {
const validChat = {
id: 'chat-123',
title: 'Revenue Analysis Chat',
is_favorited: false,
message_ids: ['msg-1', 'msg-2'],
messages: {
'msg-1': {
id: 'msg-1',
request_message: {
request: 'What is the revenue?',
sender_id: 'user-123',
sender_name: 'John Doe',
sender_avatar: 'https://example.com/avatar.jpg',
},
response_messages: {},
response_message_ids: [],
reasoning_message_ids: [],
reasoning_messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
final_reasoning_message: null,
feedback: null,
is_completed: true,
},
},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
created_by: 'John Doe',
created_by_id: 'user-123',
created_by_name: 'John Doe',
created_by_avatar: 'https://example.com/avatar.jpg',
individual_permissions: [],
publicly_accessible: false,
public_expiry_date: '2024-12-31T23:59:59Z',
public_enabled_by: 'admin-456',
public_password: 'secret123',
permission: 'owner',
};
const result = ChatWithMessagesSchema.safeParse(validChat);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('chat-123');
expect(result.data.title).toBe('Revenue Analysis Chat');
expect(result.data.is_favorited).toBe(false);
expect(result.data.message_ids).toEqual(['msg-1', 'msg-2']);
expect(result.data.publicly_accessible).toBe(false);
}
});
it('should handle optional fields', () => {
const chatWithOptionals = {
id: 'chat-456',
title: 'Simple Chat',
is_favorited: true,
message_ids: [],
messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
created_by: 'Jane Doe',
created_by_id: 'user-456',
created_by_name: 'Jane Doe',
created_by_avatar: null, // nullable
publicly_accessible: true,
// Optional fields omitted
};
const result = ChatWithMessagesSchema.safeParse(chatWithOptionals);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('chat-456');
expect(result.data.created_by_avatar).toBeNull();
expect(result.data.individual_permissions).toBeUndefined();
expect(result.data.public_expiry_date).toBeUndefined();
expect(result.data.permission).toBeUndefined();
}
});
it('should validate nested individual permissions', () => {
const chatWithInvalidPermissions = {
id: 'chat-789',
title: 'Chat with Invalid Permissions',
is_favorited: false,
message_ids: [],
messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
created_by: 'User',
created_by_id: 'user-789',
created_by_name: 'User',
created_by_avatar: null,
individual_permissions: [
{
email: 'invalid-email', // Invalid email
role: 'editor',
},
],
publicly_accessible: false,
};
const result = ChatWithMessagesSchema.safeParse(chatWithInvalidPermissions);
expect(result.success).toBe(false);
});
it('should handle datetime validation for public_expiry_date', () => {
const chatWithValidDate = {
id: 'chat-date',
title: 'Date Test Chat',
is_favorited: false,
message_ids: [],
messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
created_by: 'User',
created_by_id: 'user-date',
created_by_name: 'User',
created_by_avatar: null,
publicly_accessible: true,
public_expiry_date: '2024-12-31T23:59:59.999Z', // Valid ISO datetime
};
const result = ChatWithMessagesSchema.safeParse(chatWithValidDate);
expect(result.success).toBe(true);
});
it('should reject invalid datetime for public_expiry_date', () => {
const chatWithInvalidDate = {
id: 'chat-bad-date',
title: 'Bad Date Test Chat',
is_favorited: false,
message_ids: [],
messages: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
created_by: 'User',
created_by_id: 'user-bad-date',
created_by_name: 'User',
created_by_avatar: null,
publicly_accessible: true,
public_expiry_date: '2024-12-31', // Invalid - not a complete datetime
};
const result = ChatWithMessagesSchema.safeParse(chatWithInvalidDate);
expect(result.success).toBe(false);
});
});
describe('ChatCreateRequestSchema', () => {
it('should parse valid complete request', () => {
const validRequest = {
prompt: 'Analyze revenue trends',
chat_id: 'chat-123',
message_id: 'msg-456',
asset_id: 'asset-789',
asset_type: 'metric_file',
metric_id: 'legacy-metric-123',
dashboard_id: 'legacy-dashboard-456',
};
const result = ChatCreateRequestSchema.safeParse(validRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.prompt).toBe('Analyze revenue trends');
expect(result.data.asset_id).toBe('asset-789');
expect(result.data.asset_type).toBe('metric_file');
}
});
it('should handle minimal request', () => {
const minimalRequest = {
prompt: 'Simple question',
};
const result = ChatCreateRequestSchema.safeParse(minimalRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.prompt).toBe('Simple question');
expect(result.data.chat_id).toBeUndefined();
expect(result.data.asset_id).toBeUndefined();
}
});
it('should handle empty request', () => {
const emptyRequest = {};
const result = ChatCreateRequestSchema.safeParse(emptyRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.prompt).toBeUndefined();
expect(result.data.chat_id).toBeUndefined();
}
});
it('should enforce asset_type when asset_id is provided', () => {
const requestWithAssetIdButNoType = {
prompt: 'Test prompt',
asset_id: 'asset-123',
// asset_type is missing - should fail validation
};
const result = ChatCreateRequestSchema.safeParse(requestWithAssetIdButNoType);
expect(result.success).toBe(false);
});
it('should allow asset_type without asset_id', () => {
const requestWithTypeButNoId = {
prompt: 'Test prompt',
asset_type: 'dashboard_file',
// asset_id is missing - should be fine
};
const result = ChatCreateRequestSchema.safeParse(requestWithTypeButNoId);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.asset_type).toBe('dashboard_file');
expect(result.data.asset_id).toBeUndefined();
}
});
it('should validate asset_type enum values', () => {
const validAssetTypes = ['metric_file', 'dashboard_file'];
const invalidAssetTypes = ['metric', 'dashboard', 'file', 'document'];
for (const assetType of validAssetTypes) {
const request = {
asset_id: 'test-id',
asset_type: assetType,
};
const result = ChatCreateRequestSchema.safeParse(request);
expect(result.success).toBe(true);
}
for (const assetType of invalidAssetTypes) {
const request = {
asset_id: 'test-id',
asset_type: assetType,
};
const result = ChatCreateRequestSchema.safeParse(request);
expect(result.success).toBe(false);
}
});
it('should handle legacy fields', () => {
const requestWithLegacyFields = {
prompt: 'Legacy test',
metric_id: 'metric-legacy-123',
dashboard_id: 'dashboard-legacy-456',
};
const result = ChatCreateRequestSchema.safeParse(requestWithLegacyFields);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.metric_id).toBe('metric-legacy-123');
expect(result.data.dashboard_id).toBe('dashboard-legacy-456');
}
});
});
describe('ChatCreateHandlerRequestSchema', () => {
it('should parse valid handler request', () => {
const validHandlerRequest = {
prompt: 'Handler test prompt',
chat_id: 'chat-handler-123',
message_id: 'msg-handler-456',
asset_id: 'asset-handler-789',
asset_type: 'metric_file',
};
const result = ChatCreateHandlerRequestSchema.safeParse(validHandlerRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.prompt).toBe('Handler test prompt');
expect(result.data.asset_id).toBe('asset-handler-789');
expect(result.data.asset_type).toBe('metric_file');
}
});
it('should handle minimal handler request', () => {
const minimalHandlerRequest = {};
const result = ChatCreateHandlerRequestSchema.safeParse(minimalHandlerRequest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.prompt).toBeUndefined();
expect(result.data.chat_id).toBeUndefined();
}
});
it('should not have legacy fields', () => {
const requestWithLegacyFields = {
prompt: 'Handler test',
metric_id: 'should-not-exist',
dashboard_id: 'should-not-exist',
};
const result = ChatCreateHandlerRequestSchema.safeParse(requestWithLegacyFields);
expect(result.success).toBe(true);
if (result.success) {
// Legacy fields should not be present in handler schema
expect('metric_id' in result.data).toBe(false);
expect('dashboard_id' in result.data).toBe(false);
}
});
it('should validate asset_type enum values for handler', () => {
const validAssetTypes = ['metric_file', 'dashboard_file'];
for (const assetType of validAssetTypes) {
const request = {
asset_id: 'test-id',
asset_type: assetType,
};
const result = ChatCreateHandlerRequestSchema.safeParse(request);
expect(result.success).toBe(true);
}
});
});

View File

@ -0,0 +1,192 @@
import { describe, expect, it } from 'vitest';
import { CurrencySchema } from './currency.types';
describe('CurrencySchema', () => {
it('should parse valid currency object', () => {
const validCurrency = {
code: 'USD',
description: 'United States Dollar',
flag: '🇺🇸',
};
const result = CurrencySchema.safeParse(validCurrency);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('USD');
expect(result.data.description).toBe('United States Dollar');
expect(result.data.flag).toBe('🇺🇸');
}
});
it('should require all fields', () => {
const incompleteObjects = [
{
// missing code
description: 'Euro',
flag: '🇪🇺',
},
{
code: 'EUR',
// missing description
flag: '🇪🇺',
},
{
code: 'EUR',
description: 'Euro',
// missing flag
},
];
for (const obj of incompleteObjects) {
const result = CurrencySchema.safeParse(obj);
expect(result.success).toBe(false);
}
});
it('should validate that all fields are strings', () => {
const invalidTypes = [
{
code: 123, // should be string
description: 'Invalid Code',
flag: '🇺🇸',
},
{
code: 'USD',
description: true, // should be string
flag: '🇺🇸',
},
{
code: 'USD',
description: 'United States Dollar',
flag: null, // should be string
},
];
for (const obj of invalidTypes) {
const result = CurrencySchema.safeParse(obj);
expect(result.success).toBe(false);
}
});
it('should handle various currency examples', () => {
const currencies = [
{
code: 'EUR',
description: 'Euro',
flag: '🇪🇺',
},
{
code: 'GBP',
description: 'British Pound Sterling',
flag: '🇬🇧',
},
{
code: 'JPY',
description: 'Japanese Yen',
flag: '🇯🇵',
},
{
code: 'CAD',
description: 'Canadian Dollar',
flag: '🇨🇦',
},
{
code: 'AUD',
description: 'Australian Dollar',
flag: '🇦🇺',
},
];
for (const currency of currencies) {
const result = CurrencySchema.safeParse(currency);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe(currency.code);
expect(result.data.description).toBe(currency.description);
expect(result.data.flag).toBe(currency.flag);
}
}
});
it('should handle empty strings', () => {
const currencyWithEmptyStrings = {
code: '',
description: '',
flag: '',
};
const result = CurrencySchema.safeParse(currencyWithEmptyStrings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('');
expect(result.data.description).toBe('');
expect(result.data.flag).toBe('');
}
});
it('should handle long strings', () => {
const currencyWithLongStrings = {
code: 'VERYLONGCURRENCYCODE',
description:
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema',
flag: '🏳️‍🌈🏳️‍⚧️🇺🇳', // Multiple flag emojis
};
const result = CurrencySchema.safeParse(currencyWithLongStrings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('VERYLONGCURRENCYCODE');
expect(result.data.description).toBe(
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema'
);
expect(result.data.flag).toBe('🏳️‍🌈🏳️‍⚧️🇺🇳');
}
});
it('should handle special characters and unicode', () => {
const currencyWithSpecialChars = {
code: 'BTC-₿',
description: 'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}',
flag: '₿',
};
const result = CurrencySchema.safeParse(currencyWithSpecialChars);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('BTC-₿');
expect(result.data.description).toBe(
'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}'
);
expect(result.data.flag).toBe('₿');
}
});
it('should reject additional properties', () => {
const currencyWithExtraProps = {
code: 'USD',
description: 'United States Dollar',
flag: '🇺🇸',
extraProperty: 'This should not be allowed',
anotherExtra: 123,
};
const result = CurrencySchema.safeParse(currencyWithExtraProps);
// Zod by default allows additional properties unless .strict() is used
// Since the schema doesn't use .strict(), extra properties are allowed but ignored
expect(result.success).toBe(true);
if (result.success) {
// Extra properties should not be included in the result
expect('extraProperty' in result.data).toBe(false);
expect('anotherExtra' in result.data).toBe(false);
expect(result.data.code).toBe('USD');
expect(result.data.description).toBe('United States Dollar');
expect(result.data.flag).toBe('🇺🇸');
}
});
});

View File

@ -0,0 +1,354 @@
import { describe, expect, it } from 'vitest';
import {
BarAndLineAxisSchema,
ChartEncodesSchema,
ComboChartAxisSchema,
PieChartAxisSchema,
ScatterAxisSchema,
} from './axisInterfaces';
describe('BarAndLineAxisSchema', () => {
it('should apply default values when parsing empty object', () => {
const result = BarAndLineAxisSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual([]);
expect(result.data.y).toEqual([]);
expect(result.data.category).toEqual([]);
// tooltip is optional and will be undefined when not provided
expect(result.data.tooltip).toBeUndefined();
}
});
it('should apply nested defaults for each property', () => {
const result = BarAndLineAxisSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
// Test that arrays are empty by default
expect(result.data.x).toEqual([]);
expect(result.data.y).toEqual([]);
expect(result.data.category).toEqual([]);
expect(result.data.tooltip).toBeUndefined();
}
});
it('should override defaults when values are provided', () => {
const customAxis = {
x: ['date'],
y: ['revenue', 'profit'],
category: ['region'],
tooltip: ['description'],
};
const result = BarAndLineAxisSchema.safeParse(customAxis);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['date']);
expect(result.data.y).toEqual(['revenue', 'profit']);
expect(result.data.category).toEqual(['region']);
expect(result.data.tooltip).toEqual(['description']);
}
});
it('should handle partial overrides', () => {
const partialAxis = {
x: ['timestamp'],
y: ['sales'],
};
const result = BarAndLineAxisSchema.safeParse(partialAxis);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['timestamp']);
expect(result.data.y).toEqual(['sales']);
// Defaults should be preserved for missing fields
expect(result.data.category).toEqual([]);
expect(result.data.tooltip).toBeUndefined();
}
});
it('should handle explicit null tooltip', () => {
const axisWithNullTooltip = {
x: ['date'],
y: ['value'],
tooltip: null,
};
const result = BarAndLineAxisSchema.safeParse(axisWithNullTooltip);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tooltip).toBeNull();
}
});
});
describe('ScatterAxisSchema', () => {
it('should apply default values including size array', () => {
const result = ScatterAxisSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
x: [],
y: [],
category: [],
size: [],
tooltip: null,
});
}
});
it('should handle size as tuple or empty array', () => {
const withSizeTuple = {
x: ['x_val'],
y: ['y_val'],
size: ['population'],
};
const result = ScatterAxisSchema.safeParse(withSizeTuple);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.size).toEqual(['population']);
}
});
it('should validate size array length', () => {
const withEmptySize = {
size: [],
};
const result = ScatterAxisSchema.safeParse(withEmptySize);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.size).toEqual([]);
}
});
it('should handle nested defaults properly', () => {
const partialConfig = {
x: ['x_axis'],
};
const result = ScatterAxisSchema.safeParse(partialConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['x_axis']);
expect(result.data.y).toEqual([]);
expect(result.data.category).toEqual([]);
expect(result.data.size).toEqual([]);
expect(result.data.tooltip).toBeNull();
}
});
});
describe('ComboChartAxisSchema', () => {
it('should apply default values including y2 axis', () => {
const result = ComboChartAxisSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual([]);
expect(result.data.y).toEqual([]);
expect(result.data.y2).toEqual([]);
expect(result.data.category).toEqual([]);
expect(result.data.tooltip).toBeUndefined();
}
});
it('should handle dual y-axis configuration', () => {
const dualAxisConfig = {
x: ['month'],
y: ['revenue'],
y2: ['profit_margin'],
category: ['product_line'],
};
const result = ComboChartAxisSchema.safeParse(dualAxisConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['month']);
expect(result.data.y).toEqual(['revenue']);
expect(result.data.y2).toEqual(['profit_margin']);
expect(result.data.category).toEqual(['product_line']);
expect(result.data.tooltip).toBeUndefined();
}
});
it('should apply defaults for missing secondary axis', () => {
const primaryAxisOnly = {
x: ['date'],
y: ['sales'],
};
const result = ComboChartAxisSchema.safeParse(primaryAxisOnly);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['date']);
expect(result.data.y).toEqual(['sales']);
expect(result.data.y2).toEqual([]); // Default for secondary y-axis
expect(result.data.category).toEqual([]);
expect(result.data.tooltip).toBeUndefined();
}
});
});
describe('PieChartAxisSchema', () => {
it('should apply default values for pie chart axis', () => {
const result = PieChartAxisSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
x: [],
y: [],
tooltip: null,
});
}
});
it('should handle pie chart specific configuration', () => {
const pieConfig = {
x: ['category'],
y: ['value1', 'value2'], // Multiple values for rings
tooltip: ['description', 'percentage'],
};
const result = PieChartAxisSchema.safeParse(pieConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['category']);
expect(result.data.y).toEqual(['value1', 'value2']);
expect(result.data.tooltip).toEqual(['description', 'percentage']);
}
});
it('should handle missing tooltip configuration', () => {
const basicPieConfig = {
x: ['segment'],
y: ['amount'],
};
const result = PieChartAxisSchema.safeParse(basicPieConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['segment']);
expect(result.data.y).toEqual(['amount']);
expect(result.data.tooltip).toBeNull(); // Default
}
});
});
describe('ChartEncodesSchema union', () => {
it('should accept valid BarAndLineAxis', () => {
const barLineAxis = {
x: ['date'],
y: ['revenue'],
category: [],
tooltip: null,
};
const result = ChartEncodesSchema.safeParse(barLineAxis);
expect(result.success).toBe(true);
});
it('should accept valid ScatterAxis', () => {
const scatterAxis = {
x: ['x_value'],
y: ['y_value'],
category: [],
size: ['population'],
tooltip: null,
};
const result = ChartEncodesSchema.safeParse(scatterAxis);
expect(result.success).toBe(true);
});
it('should accept valid ComboChartAxis', () => {
const comboAxis = {
x: ['month'],
y: ['sales'],
y2: ['profit'],
category: [],
tooltip: null,
};
const result = ChartEncodesSchema.safeParse(comboAxis);
expect(result.success).toBe(true);
});
it('should accept valid PieChartAxis', () => {
const pieAxis = {
x: ['category'],
y: ['value'],
tooltip: null,
};
const result = ChartEncodesSchema.safeParse(pieAxis);
expect(result.success).toBe(true);
});
it('should reject truly invalid axis configurations', () => {
// The union is permissive, so we need truly invalid data to make it fail
const invalidAxis = {
x: 'not an array', // This should fail since x should be an array
};
const result = ChartEncodesSchema.safeParse(invalidAxis);
expect(result.success).toBe(false);
});
});
describe('Nested defaults behavior', () => {
it('should deeply apply defaults to all axis schemas', () => {
const schemas = [
BarAndLineAxisSchema,
ScatterAxisSchema,
ComboChartAxisSchema,
PieChartAxisSchema,
];
for (const schema of schemas) {
const result = schema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
// All schemas should have x and y as empty arrays by default
expect(result.data.x).toEqual([]);
expect(result.data.y).toEqual([]);
// Tooltip should be null by default
expect('tooltip' in result.data ? result.data.tooltip : null).toBeNull();
}
}
});
it('should preserve defaults when partially overriding nested objects', () => {
const partialOverride = {
x: ['custom_x'],
// y, category, tooltip should get defaults
};
const result = BarAndLineAxisSchema.safeParse(partialOverride);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.x).toEqual(['custom_x']); // Overridden
expect(result.data.y).toEqual([]); // Default
expect(result.data.category).toEqual([]); // Default
expect(result.data.tooltip).toBeUndefined(); // Default
}
});
});

View File

@ -0,0 +1,369 @@
import { describe, expect, it } from 'vitest';
import {
BarColumnSettingsSchema,
ColumnSettingsSchema,
DEFAULT_COLUMN_SETTINGS,
DotColumnSettingsSchema,
LineColumnSettingsSchema,
} from './columnInterfaces';
describe('ColumnSettingsSchema', () => {
it('should apply all default values when parsing empty object', () => {
const result = ColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
showDataLabels: false,
showDataLabelsAsPercentage: false,
columnVisualization: 'bar',
lineWidth: 2,
lineStyle: 'line',
lineType: 'normal',
lineSymbolSize: 0,
barRoundness: 8,
});
}
});
it('should match DEFAULT_COLUMN_SETTINGS', () => {
const result = ColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(DEFAULT_COLUMN_SETTINGS);
}
});
it('should apply nested defaults for boolean flags', () => {
const result = ColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.showDataLabels).toBe(false);
expect(result.data.showDataLabelsAsPercentage).toBe(false);
}
});
it('should apply nested defaults for visualization settings', () => {
const result = ColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.columnVisualization).toBe('bar');
expect(result.data.lineWidth).toBe(2);
expect(result.data.lineStyle).toBe('line');
expect(result.data.lineType).toBe('normal');
expect(result.data.lineSymbolSize).toBe(0);
expect(result.data.barRoundness).toBe(8);
}
});
it('should override defaults when values are provided', () => {
const customSettings = {
showDataLabels: true,
columnVisualization: 'line',
lineWidth: 5,
barRoundness: 15,
};
const result = ColumnSettingsSchema.safeParse(customSettings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.showDataLabels).toBe(true);
expect(result.data.columnVisualization).toBe('line');
expect(result.data.lineWidth).toBe(5);
expect(result.data.barRoundness).toBe(15);
// Defaults should be preserved for non-overridden fields
expect(result.data.showDataLabelsAsPercentage).toBe(false);
expect(result.data.lineStyle).toBe('line');
expect(result.data.lineType).toBe('normal');
expect(result.data.lineSymbolSize).toBe(0);
}
});
it('should handle partial overrides for line settings', () => {
const lineSettings = {
columnVisualization: 'line',
lineWidth: 3,
lineType: 'smooth',
};
const result = ColumnSettingsSchema.safeParse(lineSettings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.columnVisualization).toBe('line');
expect(result.data.lineWidth).toBe(3);
expect(result.data.lineType).toBe('smooth');
// Other line defaults should be preserved
expect(result.data.lineStyle).toBe('line');
expect(result.data.lineSymbolSize).toBe(0);
// Non-line defaults should also be preserved
expect(result.data.barRoundness).toBe(8);
}
});
it('should handle partial overrides for bar settings', () => {
const barSettings = {
columnVisualization: 'bar',
barRoundness: 20,
showDataLabels: true,
};
const result = ColumnSettingsSchema.safeParse(barSettings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.columnVisualization).toBe('bar');
expect(result.data.barRoundness).toBe(20);
expect(result.data.showDataLabels).toBe(true);
// Other defaults should be preserved
expect(result.data.showDataLabelsAsPercentage).toBe(false);
expect(result.data.lineWidth).toBe(2);
}
});
it('should handle dot visualization settings', () => {
const dotSettings = {
columnVisualization: 'dot',
lineSymbolSize: 25,
};
const result = ColumnSettingsSchema.safeParse(dotSettings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.columnVisualization).toBe('dot');
expect(result.data.lineSymbolSize).toBe(25);
// All other defaults should be preserved
expect(result.data.showDataLabels).toBe(false);
expect(result.data.lineWidth).toBe(2);
expect(result.data.barRoundness).toBe(8);
}
});
it('should validate line width constraints', () => {
const invalidLineWidth = {
lineWidth: 25, // exceeds max of 20
};
const result = ColumnSettingsSchema.safeParse(invalidLineWidth);
expect(result.success).toBe(false);
});
it('should validate bar roundness constraints', () => {
const invalidBarRoundness = {
barRoundness: 60, // exceeds max of 50
};
const result = ColumnSettingsSchema.safeParse(invalidBarRoundness);
expect(result.success).toBe(false);
});
it('should validate symbol size constraints', () => {
const invalidSymbolSize = {
lineSymbolSize: 60, // exceeds max of 50
};
const result = ColumnSettingsSchema.safeParse(invalidSymbolSize);
expect(result.success).toBe(false);
});
});
describe('LineColumnSettingsSchema', () => {
it('should apply default values for line-specific settings', () => {
const result = LineColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
lineWidth: 2,
lineStyle: 'line',
lineType: 'normal',
lineSymbolSize: 0,
});
}
});
it('should handle line style variations', () => {
const areaLine = {
lineStyle: 'area',
lineType: 'step',
lineSymbolSize: 5,
};
const result = LineColumnSettingsSchema.safeParse(areaLine);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.lineStyle).toBe('area');
expect(result.data.lineType).toBe('step');
expect(result.data.lineSymbolSize).toBe(5);
expect(result.data.lineWidth).toBe(2); // Default preserved
}
});
it('should validate line width range', () => {
const validLineWidths = [1, 10, 20];
const invalidLineWidths = [0, 21];
for (const width of validLineWidths) {
const result = LineColumnSettingsSchema.safeParse({ lineWidth: width });
expect(result.success).toBe(true);
}
for (const width of invalidLineWidths) {
const result = LineColumnSettingsSchema.safeParse({ lineWidth: width });
expect(result.success).toBe(false);
}
});
});
describe('BarColumnSettingsSchema', () => {
it('should apply default values for bar-specific settings', () => {
const result = BarColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
barRoundness: 8,
});
}
});
it('should handle different roundness values', () => {
const roundedBar = {
barRoundness: 25,
};
const result = BarColumnSettingsSchema.safeParse(roundedBar);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.barRoundness).toBe(25);
}
});
it('should validate bar roundness range', () => {
const validRoundness = [0, 25, 50];
const invalidRoundness = [-1, 51];
for (const roundness of validRoundness) {
const result = BarColumnSettingsSchema.safeParse({ barRoundness: roundness });
expect(result.success).toBe(true);
}
for (const roundness of invalidRoundness) {
const result = BarColumnSettingsSchema.safeParse({ barRoundness: roundness });
expect(result.success).toBe(false);
}
});
});
describe('DotColumnSettingsSchema', () => {
it('should apply default values for dot-specific settings', () => {
const result = DotColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
lineSymbolSize: 10,
});
}
});
it('should handle different dot sizes', () => {
const largeDot = {
lineSymbolSize: 30,
};
const result = DotColumnSettingsSchema.safeParse(largeDot);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.lineSymbolSize).toBe(30);
}
});
it('should validate dot symbol size range', () => {
const validSizes = [1, 25, 50];
const invalidSizes = [0, 51];
for (const size of validSizes) {
const result = DotColumnSettingsSchema.safeParse({ lineSymbolSize: size });
expect(result.success).toBe(true);
}
for (const size of invalidSizes) {
const result = DotColumnSettingsSchema.safeParse({ lineSymbolSize: size });
expect(result.success).toBe(false);
}
});
});
describe('DEFAULT_COLUMN_SETTINGS', () => {
it('should be a valid ColumnSettings object', () => {
const result = ColumnSettingsSchema.safeParse(DEFAULT_COLUMN_SETTINGS);
expect(result.success).toBe(true);
});
it('should contain all expected default values', () => {
expect(DEFAULT_COLUMN_SETTINGS).toEqual({
showDataLabels: false,
showDataLabelsAsPercentage: false,
columnVisualization: 'bar',
lineWidth: 2,
lineStyle: 'line',
lineType: 'normal',
lineSymbolSize: 0,
barRoundness: 8,
});
});
it('should have correct types for all properties', () => {
expect(typeof DEFAULT_COLUMN_SETTINGS.showDataLabels).toBe('boolean');
expect(typeof DEFAULT_COLUMN_SETTINGS.showDataLabelsAsPercentage).toBe('boolean');
expect(typeof DEFAULT_COLUMN_SETTINGS.columnVisualization).toBe('string');
expect(typeof DEFAULT_COLUMN_SETTINGS.lineWidth).toBe('number');
expect(typeof DEFAULT_COLUMN_SETTINGS.lineStyle).toBe('string');
expect(typeof DEFAULT_COLUMN_SETTINGS.lineType).toBe('string');
expect(typeof DEFAULT_COLUMN_SETTINGS.lineSymbolSize).toBe('number');
expect(typeof DEFAULT_COLUMN_SETTINGS.barRoundness).toBe('number');
});
});
describe('Nested defaults interaction', () => {
it('should properly combine defaults from all visualization types', () => {
const result = ColumnSettingsSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
// Should have defaults from line, bar, and dot settings
expect(result.data.lineWidth).toBe(2); // From LineColumnSettings
expect(result.data.barRoundness).toBe(8); // From BarColumnSettings
expect(result.data.lineSymbolSize).toBe(0); // From main schema (different from DotColumnSettings default of 10)
}
});
it('should allow overriding specific visualization settings while preserving others', () => {
const mixedSettings = {
columnVisualization: 'line',
lineWidth: 4,
barRoundness: 12, // This should still be applied even though columnVisualization is 'line'
};
const result = ColumnSettingsSchema.safeParse(mixedSettings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.columnVisualization).toBe('line');
expect(result.data.lineWidth).toBe(4);
expect(result.data.barRoundness).toBe(12);
// Other defaults should be preserved
expect(result.data.lineStyle).toBe('line');
expect(result.data.showDataLabels).toBe(false);
}
});
});

View File

@ -0,0 +1,412 @@
import { describe, expect, it } from 'vitest';
import { z } from 'zod/v4';
import { getDefaults, getDefaultsPartial } from './defaultHelpers';
describe('getDefaults', () => {
it('should extract defaults from simple schema', () => {
const SimpleSchema = z.object({
name: z.string().default('John'),
age: z.number().default(25),
active: z.boolean().default(true),
});
const defaults = getDefaults(SimpleSchema);
expect(defaults).toEqual({
name: 'John',
age: 25,
active: true,
});
});
it('should handle nested object schemas with defaults', () => {
const NestedSchema = z.object({
user: z
.object({
name: z.string().default('Default User'),
preferences: z
.object({
theme: z.string().default('light'),
notifications: z.boolean().default(true),
})
.default({
theme: 'light',
notifications: true,
}),
})
.default({
name: 'Default User',
preferences: {
theme: 'light',
notifications: true,
},
}),
settings: z
.object({
autoSave: z.boolean().default(false),
})
.default({
autoSave: false,
}),
});
const defaults = getDefaults(NestedSchema);
expect(defaults).toEqual({
user: {
name: 'Default User',
preferences: {
theme: 'light',
notifications: true,
},
},
settings: {
autoSave: false,
},
});
});
it('should handle array defaults', () => {
const ArraySchema = z.object({
tags: z.array(z.string()).default([]),
numbers: z.array(z.number()).default([1, 2, 3]),
items: z
.array(
z.object({
id: z.string(),
name: z.string().default('Item'),
})
)
.default([]),
});
const defaults = getDefaults(ArraySchema);
expect(defaults).toEqual({
tags: [],
numbers: [1, 2, 3],
items: [],
});
});
it('should handle optional fields without defaults', () => {
const OptionalSchema = z.object({
required: z.string().default('required'),
optional: z.string().optional(),
optionalWithDefault: z.string().optional().default('optional default'),
});
const defaults = getDefaults(OptionalSchema);
expect(defaults).toEqual({
required: 'required',
optionalWithDefault: 'optional default',
});
// Optional fields without defaults should not be present
expect('optional' in defaults).toBe(false);
});
it('should handle enum defaults', () => {
const EnumSchema = z.object({
status: z.enum(['pending', 'completed', 'failed']).default('pending'),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
const defaults = getDefaults(EnumSchema);
expect(defaults).toEqual({
status: 'pending',
priority: 'medium',
});
});
it('should handle union schemas with defaults', () => {
const UnionSchema = z.object({
value: z.union([z.string(), z.number()]).default('default string'),
nullableUnion: z.union([z.string(), z.null()]).default(null),
});
const defaults = getDefaults(UnionSchema);
expect(defaults).toEqual({
value: 'default string',
nullableUnion: null,
});
});
it('should handle record schemas with defaults', () => {
const RecordSchema = z.object({
metadata: z.record(z.string(), z.string()).default({}),
settings: z.record(z.string(), z.boolean()).default({ enabled: true }),
});
const defaults = getDefaults(RecordSchema);
expect(defaults).toEqual({
metadata: {},
settings: { enabled: true },
});
});
it('should handle complex nested schema with multiple default levels', () => {
const ComplexSchema = z.object({
config: z
.object({
display: z
.object({
theme: z.string().default('dark'),
fontSize: z.number().default(14),
})
.default({
theme: 'dark',
fontSize: 14,
}),
features: z
.object({
notifications: z.boolean().default(true),
autoSave: z.boolean().default(false),
advanced: z
.object({
debugMode: z.boolean().default(false),
logging: z.array(z.string()).default(['error', 'warn']),
})
.default({
debugMode: false,
logging: ['error', 'warn'],
}),
})
.default({
notifications: true,
autoSave: false,
advanced: {
debugMode: false,
logging: ['error', 'warn'],
},
}),
})
.default({
display: {
theme: 'dark',
fontSize: 14,
},
features: {
notifications: true,
autoSave: false,
advanced: {
debugMode: false,
logging: ['error', 'warn'],
},
},
}),
});
const defaults = getDefaults(ComplexSchema);
expect(defaults).toEqual({
config: {
display: {
theme: 'dark',
fontSize: 14,
},
features: {
notifications: true,
autoSave: false,
advanced: {
debugMode: false,
logging: ['error', 'warn'],
},
},
},
});
});
it('should handle schema with mixed required and optional fields', () => {
const MixedSchema = z.object({
id: z.string(), // required, no default
name: z.string().default('Unnamed'),
description: z.string().optional(),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.string()).optional(),
settings: z
.object({
enabled: z.boolean().default(true),
level: z.number().optional(),
})
.default({
enabled: true,
}),
});
// This should return empty object since 'id' is required but has no default
const defaults = getDefaults(MixedSchema);
// When there are required fields without defaults, getDefaults returns empty object
// because it can't create a valid complete object
expect(defaults).toEqual({});
});
it('should validate the returned defaults against the original schema when possible', () => {
const ValidatableSchema = z.object({
name: z.string().default('Valid Name'),
count: z.number().min(0).default(5),
enabled: z.boolean().default(true),
});
const defaults = getDefaults(ValidatableSchema);
// The defaults should be valid according to the original schema
const validationResult = ValidatableSchema.safeParse(defaults);
expect(validationResult.success).toBe(true);
if (validationResult.success) {
expect(validationResult.data).toEqual(defaults);
}
});
});
describe('getDefaultsPartial', () => {
it('should return only fields with explicit defaults', () => {
const MixedSchema = z.object({
id: z.string(), // required, no default
name: z.string().default('Default Name'),
description: z.string().optional(), // optional, no default
active: z.boolean().default(true),
tags: z.array(z.string()).default([]),
});
const partialDefaults = getDefaultsPartial(MixedSchema);
// Since getDefaultsPartial() uses .partial() then parse({}), it returns empty object
// when there are required fields without defaults
expect(partialDefaults).toEqual({});
});
it('should handle nested schemas in partial mode', () => {
const NestedSchema = z.object({
required: z.string(), // no default
config: z
.object({
theme: z.string().default('light'),
optional: z.string().optional(),
})
.default({
theme: 'light',
}),
optional: z.string().optional(),
});
const partialDefaults = getDefaultsPartial(NestedSchema);
// Since there's a required field without default, returns empty object
expect(partialDefaults).toEqual({});
});
it('should return empty object when no defaults exist', () => {
const NoDefaultsSchema = z.object({
id: z.string(),
name: z.string(),
optional: z.string().optional(),
nullable: z.string().nullable(),
});
const partialDefaults = getDefaultsPartial(NoDefaultsSchema);
expect(partialDefaults).toEqual({});
expect(Object.keys(partialDefaults)).toHaveLength(0);
});
it('should handle complex defaults in partial mode', () => {
const ComplexSchema = z.object({
required: z.string(),
settings: z
.object({
display: z
.object({
theme: z.string().default('auto'),
})
.default({
theme: 'auto',
}),
})
.default({
display: {
theme: 'auto',
},
}),
metadata: z.record(z.string(), z.string()).default({}),
optional: z.string().optional(),
});
const partialDefaults = getDefaultsPartial(ComplexSchema);
// Since there's a required field without default, returns empty object
expect(partialDefaults).toEqual({});
});
});
describe('Helper function edge cases', () => {
it('should handle empty schemas', () => {
const EmptySchema = z.object({});
const defaults = getDefaults(EmptySchema);
const partialDefaults = getDefaultsPartial(EmptySchema);
expect(defaults).toEqual({});
expect(partialDefaults).toEqual({});
});
it('should handle schemas with only required fields', () => {
const RequiredOnlySchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const defaults = getDefaults(RequiredOnlySchema);
const partialDefaults = getDefaultsPartial(RequiredOnlySchema);
expect(defaults).toEqual({});
expect(partialDefaults).toEqual({});
});
it('should handle null and undefined defaults correctly', () => {
const NullDefaultsSchema = z.object({
nullable: z.string().nullable().default(null),
optional: z.string().optional().default(undefined),
emptyString: z.string().default(''),
zero: z.number().default(0),
false: z.boolean().default(false),
});
const defaults = getDefaults(NullDefaultsSchema);
expect(defaults).toEqual({
nullable: null,
optional: undefined,
emptyString: '',
zero: 0,
false: false,
});
});
it('should handle transformed schemas', () => {
const TransformedSchema = z.object({
value: z
.string()
.default('raw')
.transform((val) => val.toUpperCase()),
count: z
.number()
.default(5)
.transform((val) => val * 2),
});
const defaults = getDefaults(TransformedSchema);
// Defaults should be applied before transformation in this test
// Note: The actual behavior might depend on how Zod handles defaults with transforms
expect(typeof defaults.value).toBe('string');
expect(typeof defaults.count).toBe('number');
});
});

View File

@ -0,0 +1,368 @@
import { describe, expect, it } from 'vitest';
import { DEFAULT_CHART_CONFIG } from './charts/chartConfigProps';
import { MetricSchema } from './metric.types';
describe('MetricSchema', () => {
it('should parse a valid metric with all required fields', () => {
const validMetric = {
id: 'metric-123',
type: 'metric',
name: 'Revenue Analysis',
version_number: 1,
description: 'Monthly revenue breakdown',
file_name: 'revenue_analysis.yaml',
time_frame: 'monthly',
dataset_id: 'dataset-456',
data_source_id: 'source-789',
dataset_name: 'Sales Data',
error: null,
data_metadata: {
row_count: 1000,
column_count: 5,
column_metadata: [
{
name: 'revenue',
min_value: 0,
max_value: 10000,
unique_values: 100,
simple_type: 'number',
type: 'float',
},
],
},
status: 'verified',
evaluation_score: 'High',
evaluation_summary: 'Excellent metric quality',
file: 'metric:\n name: Revenue\n sql: SELECT * FROM revenue',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
sent_by_id: 'user-123',
sent_by_name: 'John Doe',
sent_by_avatar_url: 'https://example.com/avatar.jpg',
sql: 'SELECT SUM(revenue) FROM sales',
dashboards: [],
collections: [],
versions: [],
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'owner',
};
const result = MetricSchema.safeParse(validMetric);
if (!result.success) {
console.error('Validation errors:', JSON.stringify(result.error.format(), null, 2));
}
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('metric-123');
expect(result.data.type).toBe('metric');
expect(result.data.name).toBe('Revenue Analysis');
}
});
it('should apply default chart_config when not provided', () => {
const metricWithoutChartConfig = {
id: 'metric-123',
type: 'metric',
name: 'Test Metric',
version_number: 1,
description: null,
file_name: 'test.yaml',
time_frame: 'daily',
dataset_id: 'dataset-1',
data_source_id: 'source-1',
dataset_name: 'Test Data',
error: null,
data_metadata: {
row_count: 100,
column_count: 3,
column_metadata: [
{
name: 'test_column',
min_value: 0,
max_value: 100,
unique_values: 50,
simple_type: 'number',
type: 'integer',
},
],
},
status: 'notRequested',
evaluation_score: 'Moderate',
evaluation_summary: 'Good metric',
file: 'metric content',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
sent_by_id: 'user-1',
sent_by_name: 'User One',
sent_by_avatar_url: null,
sql: null,
dashboards: [],
collections: [],
versions: [],
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'canView',
// chart_config is omitted, should get default
};
const result = MetricSchema.safeParse(metricWithoutChartConfig);
if (!result.success) {
console.error('Test 2 - Validation errors:', JSON.stringify(result.error.format(), null, 2));
}
expect(result.success).toBe(true);
if (result.success) {
// Should have the default chart config
expect(result.data.chart_config).toEqual(DEFAULT_CHART_CONFIG);
expect(result.data.chart_config.selectedChartType).toBe('table');
expect(result.data.chart_config.colors).toEqual(DEFAULT_CHART_CONFIG.colors);
expect(result.data.chart_config.gridLines).toBe(true);
}
});
it('should preserve custom chart_config when provided', () => {
const customChartConfig = {
selectedChartType: 'bar',
colors: ['#FF0000', '#00FF00'],
gridLines: false,
columnSettings: {
revenue: {
showDataLabels: true,
columnVisualization: 'line',
},
},
};
const metricWithCustomConfig = {
id: 'metric-123',
type: 'metric',
name: 'Custom Chart Metric',
version_number: 1,
description: null,
file_name: 'custom.yaml',
time_frame: 'weekly',
dataset_id: 'dataset-1',
data_source_id: 'source-1',
dataset_name: 'Custom Data',
error: null,
chart_config: customChartConfig,
data_metadata: {
row_count: 500,
column_count: 4,
column_metadata: [
{
name: 'custom_field',
min_value: 0,
max_value: 500,
unique_values: 250,
simple_type: 'number',
type: 'float',
},
],
},
status: 'verified',
evaluation_score: 'High',
evaluation_summary: 'Great metric',
file: 'custom metric content',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
sent_by_id: 'user-1',
sent_by_name: 'User One',
sent_by_avatar_url: null,
sql: 'SELECT * FROM custom_table',
dashboards: [],
collections: [],
versions: [],
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'canEdit',
};
const result = MetricSchema.safeParse(metricWithCustomConfig);
if (!result.success) {
console.error('Test 3 - Validation errors:', JSON.stringify(result.error.format(), null, 2));
}
expect(result.success).toBe(true);
if (result.success) {
// Should preserve custom config but apply defaults for missing fields
expect(result.data.chart_config.selectedChartType).toBe('bar');
expect(result.data.chart_config.colors).toEqual(['#FF0000', '#00FF00']);
expect(result.data.chart_config.gridLines).toBe(false);
expect(result.data.chart_config.columnSettings.revenue?.showDataLabels).toBe(true);
expect(result.data.chart_config.columnSettings.revenue?.columnVisualization).toBe('line');
// Should apply defaults for missing chart config fields
expect(result.data.chart_config.showLegend).toBeNull();
expect(result.data.chart_config.disableTooltip).toBe(false);
expect(result.data.chart_config.goalLines).toEqual([]);
}
});
it('should handle partial chart_config with deeply nested defaults', () => {
const partialChartConfig = {
selectedChartType: 'line',
columnSettings: {
sales: {
showDataLabels: true,
// Other column settings should get defaults
},
},
// Other chart config fields should get defaults
};
const metricWithPartialConfig = {
id: 'metric-456',
type: 'metric',
name: 'Partial Config Metric',
version_number: 2,
description: 'Testing partial config',
file_name: 'partial.yaml',
time_frame: 'quarterly',
dataset_id: 'dataset-2',
data_source_id: 'source-2',
dataset_name: 'Partial Data',
error: null,
chart_config: partialChartConfig,
data_metadata: {
row_count: 750,
column_count: 6,
column_metadata: [
{
name: 'sales',
min_value: 100,
max_value: 5000,
unique_values: 300,
simple_type: 'number',
type: 'decimal',
},
],
},
status: 'notVerified',
evaluation_score: 'Low',
evaluation_summary: 'Needs improvement',
file: 'partial config content',
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
sent_by_id: 'user-2',
sent_by_name: 'User Two',
sent_by_avatar_url: 'https://example.com/user2.jpg',
sql: 'SELECT AVG(sales) FROM transactions',
dashboards: [{ id: 'dash-1', name: 'Sales Dashboard' }],
collections: [{ id: 'coll-1', name: 'Sales Collection' }],
versions: [
{ version_number: 1, updated_at: '2024-01-01T00:00:00Z' },
{ version_number: 2, updated_at: '2024-01-15T00:00:00Z' },
],
individual_permissions: [],
public_expiry_date: '2024-12-31T00:00:00Z',
public_enabled_by: 'admin-1',
publicly_accessible: true,
public_password: 'secret123',
permission: 'canEdit',
};
const result = MetricSchema.safeParse(metricWithPartialConfig);
expect(result.success).toBe(true);
if (result.success) {
// Should preserve provided values
expect(result.data.chart_config.selectedChartType).toBe('line');
expect(result.data.chart_config.columnSettings.sales?.showDataLabels).toBe(true);
// Should apply defaults to column settings for provided column
expect(result.data.chart_config.columnSettings.sales?.showDataLabelsAsPercentage).toBe(false);
expect(result.data.chart_config.columnSettings.sales?.columnVisualization).toBe('bar');
expect(result.data.chart_config.columnSettings.sales?.lineWidth).toBe(2);
// Should apply defaults to top-level chart config
expect(result.data.chart_config.colors).toEqual(DEFAULT_CHART_CONFIG.colors);
expect(result.data.chart_config.gridLines).toBe(true);
expect(result.data.chart_config.showLegend).toBeNull();
expect(result.data.chart_config.columnLabelFormats).toEqual({});
}
});
it('should validate required enum values', () => {
const metricWithInvalidEnum = {
id: 'metric-789',
type: 'invalid_type', // Should be 'metric'
name: 'Invalid Metric',
// ... other required fields
};
const result = MetricSchema.safeParse(metricWithInvalidEnum);
expect(result.success).toBe(false);
});
it('should handle nullable fields correctly', () => {
const metricWithNulls = {
id: 'metric-null',
type: 'metric',
name: 'Null Fields Metric',
version_number: 1,
description: null, // nullable
file_name: 'null_test.yaml',
time_frame: 'yearly',
dataset_id: 'dataset-null',
data_source_id: 'source-null',
dataset_name: null, // nullable
error: null, // nullable
data_metadata: {
row_count: 0,
column_count: 0,
column_metadata: [],
},
status: 'notRequested',
evaluation_score: 'Moderate',
evaluation_summary: 'Test with nulls',
file: 'null content',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
sent_by_id: 'user-null',
sent_by_name: 'Null User',
sent_by_avatar_url: null, // nullable
sql: null, // nullable
dashboards: [],
collections: [],
versions: [],
individual_permissions: null, // nullable
public_expiry_date: null, // nullable
public_enabled_by: null, // nullable
publicly_accessible: false,
public_password: null, // nullable
permission: 'canView',
};
const result = MetricSchema.safeParse(metricWithNulls);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.description).toBeNull();
expect(result.data.dataset_name).toBeNull();
expect(result.data.error).toBeNull();
expect(result.data.sent_by_avatar_url).toBeNull();
expect(result.data.sql).toBeNull();
}
});
});
// Note: DEFAULT_METRIC tests removed because getDefaults() cannot work
// with schemas that have required fields without defaults

View File

@ -0,0 +1,413 @@
import { describe, expect, it } from 'vitest';
import {
ShareAssetTypeSchema,
ShareConfigSchema,
ShareIndividualSchema,
ShareRoleSchema,
} from './shareInterfaces';
describe('ShareRoleSchema', () => {
it('should accept valid role values', () => {
const validRoles = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
for (const role of validRoles) {
const result = ShareRoleSchema.safeParse(role);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(role);
}
}
});
it('should reject invalid role values', () => {
const invalidRoles = ['admin', 'user', 'guest', '', 'OWNER'];
for (const role of invalidRoles) {
const result = ShareRoleSchema.safeParse(role);
expect(result.success).toBe(false);
}
});
it('should be case sensitive', () => {
const caseVariations = ['Owner', 'OWNER', 'fullaccess', 'CANVIEW'];
for (const role of caseVariations) {
const result = ShareRoleSchema.safeParse(role);
expect(result.success).toBe(false);
}
});
});
describe('ShareAssetTypeSchema', () => {
it('should accept valid asset type values', () => {
const validTypes = ['metric', 'dashboard', 'collection', 'chat'];
for (const type of validTypes) {
const result = ShareAssetTypeSchema.safeParse(type);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(type);
}
}
});
it('should reject invalid asset type values', () => {
const invalidTypes = ['report', 'query', 'table', '', 'METRIC'];
for (const type of invalidTypes) {
const result = ShareAssetTypeSchema.safeParse(type);
expect(result.success).toBe(false);
}
});
it('should be case sensitive', () => {
const caseVariations = ['Metric', 'DASHBOARD', 'Collection', 'CHAT'];
for (const type of caseVariations) {
const result = ShareAssetTypeSchema.safeParse(type);
expect(result.success).toBe(false);
}
});
});
describe('ShareIndividualSchema', () => {
it('should parse valid individual sharing configuration', () => {
const validIndividual = {
email: 'user@example.com',
role: 'canEdit',
name: 'John Doe',
};
const result = ShareIndividualSchema.safeParse(validIndividual);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('user@example.com');
expect(result.data.role).toBe('canEdit');
expect(result.data.name).toBe('John Doe');
}
});
it('should handle optional name field', () => {
const individualWithoutName = {
email: 'test@example.com',
role: 'canView',
};
const result = ShareIndividualSchema.safeParse(individualWithoutName);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('test@example.com');
expect(result.data.role).toBe('canView');
expect(result.data.name).toBeUndefined();
}
});
it('should validate email format', () => {
const invalidEmails = ['invalid-email', 'test@', '@example.com', 'test.example.com', ''];
for (const email of invalidEmails) {
const individual = {
email,
role: 'canView',
};
const result = ShareIndividualSchema.safeParse(individual);
expect(result.success).toBe(false);
}
});
it('should validate role field', () => {
const individual = {
email: 'valid@example.com',
role: 'invalidRole',
};
const result = ShareIndividualSchema.safeParse(individual);
expect(result.success).toBe(false);
});
it('should require email and role fields', () => {
const missingEmail = {
role: 'canEdit',
name: 'John Doe',
};
const missingRole = {
email: 'user@example.com',
name: 'John Doe',
};
const emailResult = ShareIndividualSchema.safeParse(missingEmail);
expect(emailResult.success).toBe(false);
const roleResult = ShareIndividualSchema.safeParse(missingRole);
expect(roleResult.success).toBe(false);
});
it('should handle all valid role combinations', () => {
const validRoles = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
for (const role of validRoles) {
const individual = {
email: 'test@example.com',
role,
name: 'Test User',
};
const result = ShareIndividualSchema.safeParse(individual);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.role).toBe(role);
}
}
});
});
describe('ShareConfigSchema', () => {
it('should parse valid complete share configuration', () => {
const validConfig = {
individual_permissions: [
{
email: 'user1@example.com',
role: 'canEdit',
name: 'User One',
},
{
email: 'user2@example.com',
role: 'canView',
},
],
public_expiry_date: '2024-12-31T23:59:59Z',
public_enabled_by: 'admin@example.com',
publicly_accessible: true,
public_password: 'secretPassword123',
permission: 'owner',
};
const result = ShareConfigSchema.safeParse(validConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.individual_permissions).toHaveLength(2);
expect(result.data.individual_permissions?.[0].email).toBe('user1@example.com');
expect(result.data.individual_permissions?.[0].role).toBe('canEdit');
expect(result.data.publicly_accessible).toBe(true);
expect(result.data.permission).toBe('owner');
}
});
it('should handle null individual_permissions', () => {
const configWithNullPermissions = {
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'canView',
};
const result = ShareConfigSchema.safeParse(configWithNullPermissions);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.individual_permissions).toBeNull();
expect(result.data.public_expiry_date).toBeNull();
expect(result.data.public_enabled_by).toBeNull();
expect(result.data.publicly_accessible).toBe(false);
expect(result.data.public_password).toBeNull();
expect(result.data.permission).toBe('canView');
}
});
it('should handle empty individual_permissions array', () => {
const configWithEmptyPermissions = {
individual_permissions: [],
public_expiry_date: '2025-01-01T00:00:00Z',
public_enabled_by: 'system',
publicly_accessible: true,
public_password: null,
permission: 'fullAccess',
};
const result = ShareConfigSchema.safeParse(configWithEmptyPermissions);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.individual_permissions).toEqual([]);
expect(result.data.publicly_accessible).toBe(true);
expect(result.data.permission).toBe('fullAccess');
}
});
it('should validate all permission field values', () => {
const validPermissions = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
for (const permission of validPermissions) {
const config = {
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission,
};
const result = ShareConfigSchema.safeParse(config);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.permission).toBe(permission);
}
}
});
it('should reject invalid permission values', () => {
const invalidPermissions = ['admin', 'user', 'guest', 'OWNER'];
for (const permission of invalidPermissions) {
const config = {
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission,
};
const result = ShareConfigSchema.safeParse(config);
expect(result.success).toBe(false);
}
});
it('should require all fields', () => {
const incompleteConfigs = [
{
// missing individual_permissions
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'owner',
},
{
individual_permissions: null,
// missing public_expiry_date
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'owner',
},
{
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
// missing permission
},
];
for (const config of incompleteConfigs) {
const result = ShareConfigSchema.safeParse(config);
expect(result.success).toBe(false);
}
});
it('should validate individual permissions nested structure', () => {
const configWithInvalidIndividual = {
individual_permissions: [
{
email: 'invalid-email', // Invalid email format
role: 'canEdit',
},
],
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'owner',
};
const result = ShareConfigSchema.safeParse(configWithInvalidIndividual);
expect(result.success).toBe(false);
});
it('should handle complex individual permissions scenarios', () => {
const complexConfig = {
individual_permissions: [
{
email: 'owner@company.com',
role: 'owner',
name: 'Company Owner',
},
{
email: 'editor@company.com',
role: 'canEdit',
name: 'Editor User',
},
{
email: 'viewer@external.com',
role: 'canView',
// name is optional
},
{
email: 'filter@company.com',
role: 'canFilter',
name: 'Filter User',
},
],
public_expiry_date: '2024-06-30T23:59:59Z',
public_enabled_by: 'admin@company.com',
publicly_accessible: true,
public_password: 'complex_password_123!',
permission: 'fullAccess',
};
const result = ShareConfigSchema.safeParse(complexConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.individual_permissions).toHaveLength(4);
expect(result.data.individual_permissions?.[0].role).toBe('owner');
expect(result.data.individual_permissions?.[1].role).toBe('canEdit');
expect(result.data.individual_permissions?.[2].role).toBe('canView');
expect(result.data.individual_permissions?.[3].role).toBe('canFilter');
expect(result.data.individual_permissions?.[2].name).toBeUndefined();
}
});
it('should handle boolean publicly_accessible correctly', () => {
const publicConfig = {
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: true,
public_password: null,
permission: 'owner',
};
const privateConfig = {
individual_permissions: null,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
permission: 'owner',
};
const publicResult = ShareConfigSchema.safeParse(publicConfig);
const privateResult = ShareConfigSchema.safeParse(privateConfig);
expect(publicResult.success).toBe(true);
expect(privateResult.success).toBe(true);
if (publicResult.success) {
expect(publicResult.data.publicly_accessible).toBe(true);
}
if (privateResult.success) {
expect(privateResult.data.publicly_accessible).toBe(false);
}
});
});