Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
107fe09
add new avatar colors
Jul 12, 2025
08bdc68
generate random whiteboard names
Jul 12, 2025
3e7328f
remove rag from compose.yml
Jul 12, 2025
61d7683
implement invite users, setup realtime service
leon-liang Jul 12, 2025
9c3cc13
fix useGetMe
leon-liang Jul 12, 2025
a367e9d
Merge branch 'develop' into 71-implement-collaborative-features
leon-liang Jul 12, 2025
881c207
readd collaboration top bar
leon-liang Jul 12, 2025
316c8c8
rename dtos
leon-liang Jul 12, 2025
83407f3
Merge remote-tracking branch 'refs/remotes/origin/72-ui-extension-col…
leon-liang Jul 12, 2025
431dbed
on delete cascade user whiteboard access
leon-liang Jul 12, 2025
d47be0e
spotlessApply
leon-liang Jul 12, 2025
f4ac156
ui improvements
leon-liang Jul 13, 2025
7dcf4fe
ui improvements
leon-liang Jul 13, 2025
b7f9fbe
create custom cursor component with rendered logic
Jul 13, 2025
6fdd3dc
publish and subscribe sockets
leon-liang Jul 13, 2025
864315e
add cursor array for all users
Jul 13, 2025
2c31d97
fix upper menu bug
Jul 13, 2025
b237a5f
setup kafka
leon-liang Jul 13, 2025
3d3c546
Merge remote-tracking branch 'refs/remotes/origin/72-ui-extension-col…
leon-liang Jul 14, 2025
a2f2d1c
simple publish and subscribe
leon-liang Jul 14, 2025
1d66a6c
working version
leon-liang Jul 14, 2025
1c5bbbf
Access dialog, fix viewport (#81)
xhulia028 Jul 15, 2025
03636a0
some tuning
leon-liang Jul 15, 2025
d0ebde0
remove commit interval
leon-liang Jul 15, 2025
1ca8d03
replace kafka with redis pub sub
leon-liang Jul 15, 2025
5ca04f3
remove userid from subscribe to whiteboard events
leon-liang Jul 15, 2025
a19eb0a
disable access for invitees
Jul 15, 2025
866ba54
Merge remote-tracking branch 'origin/fix-general-bugs' into 71-implem…
leon-liang Jul 15, 2025
365a96d
create deployment files
leon-liang Jul 16, 2025
8e771a8
Merge branch 'develop' into 71-implement-collaborative-features
leon-liang Jul 16, 2025
84a00d6
fix deployment files
leon-liang Jul 16, 2025
0d92c51
add server tests
Jul 16, 2025
7657115
add tests for client and test workflow
Jul 16, 2025
20b3c03
format and lint
Jul 16, 2025
9bb7d51
generate
Jul 16, 2025
8b89d6c
format
Jul 16, 2025
c00efeb
WhiteboardResponse
Jul 16, 2025
229bd60
remove dependency
Jul 16, 2025
88f4d18
Merge remote-tracking branch 'origin/71-implement-collaborative-featu…
Jul 16, 2025
52163fc
dummy commit
Jul 16, 2025
32b81c7
Merge remote-tracking branch 'origin/develop' into add-client-server-…
Jul 20, 2025
cf3f38c
format and lint
Jul 20, 2025
418fd41
delete dialog
Jul 20, 2025
921afcd
hide style bar text node
Jul 20, 2025
c9c19a9
package lock
Jul 20, 2025
9847132
package lock
Jul 20, 2025
18744b7
delete package-lock.json in root
leon-liang Jul 20, 2025
cae6e7f
regenerate package-lock
leon-liang Jul 20, 2025
07f2ce0
Merge remote-tracking branch 'origin/develop' into add-client-server-…
leon-liang Jul 20, 2025
91a316d
json
Jul 20, 2025
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