319 lines
9.2 KiB
TypeScript
319 lines
9.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { Option, Some, None, some, none } from '../';
|
|
import {fail} from "node:assert";
|
|
|
|
describe('Option isSome/isNone - Runtime Tests', () => {
|
|
describe('isSome() method', () => {
|
|
it('should return true for Some instances', () => {
|
|
// Arrange
|
|
const testCases = [
|
|
some(42),
|
|
some('hello'),
|
|
some(true),
|
|
some({ id: 1 }),
|
|
some([1, 2, 3]),
|
|
some(null),
|
|
some(undefined),
|
|
some(0),
|
|
some(false),
|
|
some('')
|
|
];
|
|
|
|
testCases.forEach(option => {
|
|
// Act & Assert
|
|
expect(option.isSome()).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should return false for None instances', () => {
|
|
// Arrange
|
|
const noneOption = none();
|
|
|
|
// Act & Assert
|
|
expect(noneOption.isSome()).toBe(false);
|
|
});
|
|
|
|
it('should enable access to value property after type narrowing', () => {
|
|
// Arrange
|
|
const option: Option<string> = some('hello') as Option<string>;
|
|
|
|
// Act & Assert
|
|
if (option.isSome()) {
|
|
expect(option.value).toBe('hello');
|
|
expect(typeof option.value).toBe('string');
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
});
|
|
|
|
it('should work with complex objects', () => {
|
|
// Arrange
|
|
const complexObject = {
|
|
id: 1,
|
|
name: 'test',
|
|
nested: { data: [1, 2, 3] },
|
|
fn: (x: number) => x * 2
|
|
};
|
|
const option: Option<typeof complexObject> = some(complexObject) as Option<typeof complexObject>;
|
|
|
|
// Act & Assert
|
|
if (option.isSome()) {
|
|
expect(option.value).toBe(complexObject);
|
|
expect(option.value.id).toBe(1);
|
|
expect(option.value.name).toBe('test');
|
|
expect(option.value.nested.data).toEqual([1, 2, 3]);
|
|
expect(option.value.fn(5)).toBe(10);
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
});
|
|
|
|
it('should work with falsy values that are still Some', () => {
|
|
// Arrange
|
|
const falsy: Option<number | boolean | string | null | undefined>[] = [
|
|
some(0) as Option<number>,
|
|
some(false) as Option<boolean>,
|
|
some('') as Option<string>,
|
|
some(null) as Option<null>,
|
|
some(undefined) as Option<undefined>
|
|
];
|
|
|
|
falsy.forEach(option => {
|
|
// Act & Assert
|
|
expect(option.isSome()).toBe(true);
|
|
if (option.isSome()) {
|
|
// Value should be accessible even if falsy
|
|
expect(option.value !== undefined || option.value === undefined).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('isNone() method', () => {
|
|
it('should return true for None instances', () => {
|
|
// Arrange
|
|
const noneOption = none();
|
|
const noneAsOption: Option<any> = none() as Option<any>;
|
|
|
|
// Act & Assert
|
|
expect(noneOption.isNone()).toBe(true);
|
|
expect(noneAsOption.isNone()).toBe(true);
|
|
});
|
|
|
|
it('should return false for Some instances', () => {
|
|
// Arrange
|
|
const testCases = [
|
|
some(42),
|
|
some('hello'),
|
|
some(true),
|
|
some({ id: 1 }),
|
|
some([1, 2, 3]),
|
|
some(null),
|
|
some(undefined),
|
|
some(0),
|
|
some(false),
|
|
some('')
|
|
];
|
|
|
|
testCases.forEach(option => {
|
|
// Act & Assert
|
|
expect(option.isNone()).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('should work with different generic types', () => {
|
|
// Arrange
|
|
const numberNone: Option<number> = none() as Option<number>;
|
|
const stringNone: Option<string> = none() as Option<string>;
|
|
const objectNone: Option<{ id: number }> = none() as Option<{ id: number }>;
|
|
|
|
// Act & Assert
|
|
expect(numberNone.isNone()).toBe(true);
|
|
expect(stringNone.isNone()).toBe(true);
|
|
expect(objectNone.isNone()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Mutual exclusivity', () => {
|
|
it('should be mutually exclusive for Some instances', () => {
|
|
// Arrange
|
|
const testCases = [
|
|
some(42),
|
|
some('hello'),
|
|
some(null),
|
|
some(undefined),
|
|
some(false),
|
|
some(0)
|
|
];
|
|
|
|
testCases.forEach(option => {
|
|
// Act & Assert
|
|
expect(option.isSome()).toBe(true);
|
|
expect(option.isNone()).toBe(false);
|
|
expect(option.isSome() && option.isNone()).toBe(false);
|
|
// @ts-expect-error - TypeScript should not allow this
|
|
expect(option.isSome() || option.isNone()).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should be mutually exclusive for None instances', () => {
|
|
// Arrange
|
|
const noneOption = none();
|
|
|
|
// Act & Assert
|
|
expect(noneOption.isSome()).toBe(false);
|
|
expect(noneOption.isNone()).toBe(true);
|
|
// @ts-expect-error - TypeScript should not allow this
|
|
expect(noneOption.isSome() && noneOption.isNone()).toBe(false);
|
|
expect(noneOption.isSome() || noneOption.isNone()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Type narrowing in control flow', () => {
|
|
it('should enable type-safe value access in if statements', () => {
|
|
// Arrange
|
|
const option: Option<{ name: string; age: number }> = some({ name: 'John', age: 30 }) as Option<{ name: string; age: number }>;
|
|
|
|
// Act & Assert
|
|
if (option.isSome()) {
|
|
expect(option.value.name).toBe('John');
|
|
expect(option.value.age).toBe(30);
|
|
expect(typeof option.value.name).toBe('string');
|
|
expect(typeof option.value.age).toBe('number');
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
});
|
|
|
|
it('should work with early returns', () => {
|
|
// Arrange
|
|
function processOption(option: Option<number>): string {
|
|
if (option.isNone()) {
|
|
return 'none';
|
|
}
|
|
return `value: ${option.value}`;
|
|
}
|
|
|
|
const someOption: Option<number> = some(42) as Option<number>;
|
|
const noneOption: Option<number> = none() as Option<number>;
|
|
|
|
// Act
|
|
const someResult = processOption(someOption);
|
|
const noneResult = processOption(noneOption);
|
|
|
|
// Assert
|
|
expect(someResult).toBe('value: 42');
|
|
expect(noneResult).toBe('none');
|
|
});
|
|
|
|
it('should work in complex conditional logic', () => {
|
|
// Arrange
|
|
const option1: Option<number> = some(10) as Option<number>;
|
|
const option2: Option<string> = some('test') as Option<string>;
|
|
const option3: Option<boolean> = none() as Option<boolean>;
|
|
|
|
// Act & Assert
|
|
if (option1.isSome() && option2.isSome()) {
|
|
expect(option1.value + option2.value.length).toBe(14);
|
|
} else {
|
|
fail('Expected both options to be Some');
|
|
}
|
|
|
|
if (option1.isSome() && option3.isNone()) {
|
|
expect(option1.value).toBe(10);
|
|
} else {
|
|
fail('Expected option1 to be Some and option3 to be None');
|
|
}
|
|
});
|
|
|
|
it('should work with negated conditions', () => {
|
|
// Arrange
|
|
const someOption: Option<string> = some('hello') as Option<string>;
|
|
const noneOption: Option<string> = none() as Option<string>;
|
|
|
|
// Act & Assert
|
|
if (!someOption.isNone()) {
|
|
expect(someOption.value).toBe('hello');
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
|
|
if (!noneOption.isSome()) {
|
|
expect(noneOption.isNone()).toBe(true);
|
|
} else {
|
|
fail('Expected option to be None');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Performance and consistency', () => {
|
|
it('should return consistent results across multiple calls', () => {
|
|
// Arrange
|
|
const someOption = some(42);
|
|
const noneOption = none();
|
|
|
|
// Act & Assert - Multiple calls should return same result
|
|
expect(someOption.isSome()).toBe(someOption.isSome());
|
|
expect(someOption.isNone()).toBe(someOption.isNone());
|
|
expect(noneOption.isSome()).toBe(noneOption.isSome());
|
|
expect(noneOption.isNone()).toBe(noneOption.isNone());
|
|
});
|
|
|
|
it('should be callable without side effects', () => {
|
|
// Arrange
|
|
const option = some({ counter: 0 });
|
|
|
|
// Act
|
|
const before = option.value.counter;
|
|
option.isSome();
|
|
option.isNone();
|
|
option.isSome();
|
|
const after = option.value.counter;
|
|
|
|
// Assert
|
|
expect(before).toBe(after);
|
|
});
|
|
});
|
|
|
|
describe('Edge cases', () => {
|
|
it('should handle deeply nested objects', () => {
|
|
// Arrange
|
|
const deepObject = {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
value: 'deep'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const option: Option<typeof deepObject> = some(deepObject) as Option<typeof deepObject>;
|
|
|
|
// Act & Assert
|
|
if (option.isSome()) {
|
|
expect(option.value.level1.level2.level3.value).toBe('deep');
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
});
|
|
|
|
it('should handle arrays with mixed types', () => {
|
|
// Arrange
|
|
const mixedArray = [1, 'string', true, null, { id: 1 }];
|
|
const option: Option<typeof mixedArray> = some(mixedArray) as Option<typeof mixedArray>;
|
|
|
|
// Act & Assert
|
|
if (option.isSome()) {
|
|
expect(option.value).toHaveLength(5);
|
|
expect(option.value[0]).toBe(1);
|
|
expect(option.value[1]).toBe('string');
|
|
expect(option.value[2]).toBe(true);
|
|
expect(option.value[3]).toBeNull();
|
|
expect(option.value[4]).toEqual({ id: 1 });
|
|
} else {
|
|
fail('Expected option to be Some');
|
|
}
|
|
});
|
|
});
|
|
});
|