Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/client-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Lint and Format Check

on:
pull_request:
paths:
- 'client/**'
workflow_dispatch:

jobs:
client-test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18.18.0'

- name: Install dependencies
run: npm install
working-directory: client

- name: Run Client Jest tests
run: npm run test
working-directory: client





1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ It addresses the need for a flexible, collaborative space where users can visual
- Teams collaborating on product ideas, diagrams, and planning
- Individuals who want to sketch out concepts or ideas


---

## 💡 Example Use Cases
Expand Down
23 changes: 23 additions & 0 deletions client/__tests__/formatDateTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import formatDate from '../src/util/formatDate';

describe('formatDate', () => {
test('formats date string correctly', () => {
const input = '2023-07-16T14:30:00';
const expected = 'July 16 at 2:30 PM';
expect(formatDate(input)).toBe(expected);
});

test('handles different times of day', () => {
expect(formatDate('2023-07-16T09:15:00')).toBe('July 16 at 9:15 AM');
expect(formatDate('2023-07-16T23:45:00')).toBe('July 16 at 11:45 PM');
});

test('handles different dates', () => {
expect(formatDate('2023-01-01T12:00:00')).toBe('January 1 at 12:00 PM');
expect(formatDate('2023-12-31T00:00:00')).toBe('December 31 at 12:00 AM');
});

test('handles invalid date strings', () => {
expect(formatDate('invalid-date')).toBe('Invalid Date');
});
});
115 changes: 115 additions & 0 deletions client/__tests__/generateUserUniqueColorTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import generateColorFromString, {
getHashOfString,
normalizeHash,
} from "@/util/generateUserUniqueColor";

describe('Color Generation Functions', () => {
describe('getHashOfString', () => {
test('returns consistent hash for same input', () => {
const input = 'test@example.com';
const firstHash = getHashOfString(input);
const secondHash = getHashOfString(input);
expect(firstHash).toBe(secondHash);
});

test('returns different hashes for different inputs', () => {
const hash1 = getHashOfString('test1@example.com');
const hash2 = getHashOfString('test2@example.com');
expect(hash1).not.toBe(hash2);
});

test('handles empty string', () => {
const hash = getHashOfString('');
expect(hash).toBe(0);
});

test('returns positive numbers', () => {
const inputs = ['test', 'example@mail.com', 'longeremail@domain.com'];
inputs.forEach(input => {
const hash = getHashOfString(input);
expect(hash).toBeGreaterThanOrEqual(0);
});
});
});

describe('normalizeHash', () => {
test('returns value within specified range', () => {
const hash = 12345;
const min = 0;
const max = 360;
const normalized = normalizeHash(hash, min, max);
expect(normalized).toBeGreaterThanOrEqual(min);
expect(normalized).toBeLessThan(max);
});

test('returns integer values', () => {
const hash = 12345;
const normalized = normalizeHash(hash, 0, 100);
expect(Number.isInteger(normalized)).toBe(true);
});

test('handles different ranges', () => {
const testCases = [
{ hash: 12345, min: 0, max: 360 },
{ hash: 12345, min: 50, max: 75 },
{ hash: 12345, min: 25, max: 60 }
];

testCases.forEach(({ hash, min, max }) => {
const normalized = normalizeHash(hash, min, max);
expect(normalized).toBeGreaterThanOrEqual(min);
expect(normalized).toBeLessThan(max);
});
});
});

describe('generateColorFromString', () => {
test('returns valid HSL color string', () => {
const color = generateColorFromString('test@example.com');
expect(color).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
});

test('returns consistent colors for same input', () => {
const input = 'test@example.com';
const color1 = generateColorFromString(input);
const color2 = generateColorFromString(input);
expect(color1).toBe(color2);
});

test('returns different colors for different inputs', () => {
const color1 = generateColorFromString('test1@example.com');
const color2 = generateColorFromString('test2@example.com');
expect(color1).not.toBe(color2);
});

test('generates color with correct HSL ranges', () => {
const color = generateColorFromString('test@example.com');
const matches = color.match(/^hsl\((\d+), (\d+)%, (\d+)%\)$/);

expect(matches).not.toBeNull();
if (matches) {
const [, hue, saturation, lightness] = matches.map(Number);

expect(hue).toBeGreaterThanOrEqual(0);
expect(hue).toBeLessThan(360);

expect(saturation).toBeGreaterThanOrEqual(50);
expect(saturation).toBeLessThan(75);

expect(lightness).toBeGreaterThanOrEqual(25);
expect(lightness).toBeLessThan(60);
}
});

test('handles special characters', () => {
const specialChars = '!@#$%^&*()_+';
expect(() => generateColorFromString(specialChars)).not.toThrow();
expect(generateColorFromString(specialChars)).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
});

test('handles empty string', () => {
expect(() => generateColorFromString('')).not.toThrow();
expect(generateColorFromString('')).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
});
});
});
119 changes: 119 additions & 0 deletions client/__tests__/generateWhiteboardNameTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import generateWhiteboardName, { nameGenerators } from "@/util/generateWhiteboardName";

describe('generateWhiteboardName', () => {
// Mock Math.random for deterministic testing
let mockMathRandom: jest.SpyInstance;

beforeEach(() => {
mockMathRandom = jest.spyOn(Math, 'random');
});

afterEach(() => {
mockMathRandom.mockRestore();
});

test('generates valid name format', () => {
const name = generateWhiteboardName();
expect(name).toMatch(/^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+$/);
});

test('generates name from project category', () => {
mockMathRandom
.mockReturnValueOnce(0) // For category selection
.mockReturnValueOnce(0) // For adjective selection
.mockReturnValueOnce(0); // For noun selection

const name = generateWhiteboardName();
expect(name).toBe('Active Project');
});

test('generates name from creative category', () => {
mockMathRandom
.mockReturnValueOnce(0.25) // For category selection
.mockReturnValueOnce(0) // For adjective selection
.mockReturnValueOnce(0); // For noun selection

const name = generateWhiteboardName();
expect(name).toBe('Creative Canvas');
});

test('generates name from tech category', () => {
mockMathRandom
.mockReturnValueOnce(0.5) // For category selection
.mockReturnValueOnce(0) // For adjective selection
.mockReturnValueOnce(0); // For noun selection

const name = generateWhiteboardName();
expect(name).toBe('Digital Hub');
});

test('generates name from nature category', () => {
mockMathRandom
.mockReturnValueOnce(0.75) // For category selection
.mockReturnValueOnce(0) // For adjective selection
.mockReturnValueOnce(0); // For noun selection

const name = generateWhiteboardName();
expect(name).toBe('Green Garden');
});

test('generates different names on subsequent calls', () => {
mockMathRandom.mockRestore();

const names = new Set();
for (let i = 0; i < 10; i++) {
names.add(generateWhiteboardName());
}
expect(names.size).toBeGreaterThan(1);
});

test('all generated names use words from nameGenerators', () => {
const name = generateWhiteboardName();
const [adjective, noun] = name.split(' ');

// Create sets of all possible adjectives and nouns
const allAdjectives = new Set(
Object.values(nameGenerators).flatMap(category => category.adjectives)
);
const allNouns = new Set(
Object.values(nameGenerators).flatMap(category => category.nouns)
);

expect(allAdjectives).toContain(adjective);
expect(allNouns).toContain(noun);
});

test('handles all possible combinations in each category', () => {
Object.entries(nameGenerators).forEach(([category, { adjectives, nouns }]) => {
adjectives.forEach((adj, adjIndex) => {
nouns.forEach((noun, nounIndex) => {
mockMathRandom
.mockReset()
.mockReturnValueOnce(Object.keys(nameGenerators).indexOf(category) / Object.keys(nameGenerators).length)
.mockReturnValueOnce(adjIndex / adjectives.length)
.mockReturnValueOnce(nounIndex / nouns.length);

const name = generateWhiteboardName();
expect(name).toBe(`${adj} ${noun}`);
});
});
});
});

test('name parts are properly capitalized', () => {
for (let i = 0; i < 10; i++) {
const name = generateWhiteboardName();
const [adjective, noun] = name.split(' ');

expect(adjective[0]).toMatch(/[A-Z]/);
expect(noun[0]).toMatch(/[A-Z]/);
}
});

test('generated names have exactly two words', () => {
for (let i = 0; i < 10; i++) {
const name = generateWhiteboardName();
expect(name.split(' ')).toHaveLength(2);
}
});
});
Loading
Loading