@auxiora/canvas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/canvas-server.d.ts +21 -0
- package/dist/canvas-server.d.ts.map +1 -0
- package/dist/canvas-server.js +73 -0
- package/dist/canvas-server.js.map +1 -0
- package/dist/canvas-session.d.ts +41 -0
- package/dist/canvas-session.d.ts.map +1 -0
- package/dist/canvas-session.js +135 -0
- package/dist/canvas-session.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/canvas-server.ts +93 -0
- package/src/canvas-session.ts +170 -0
- package/src/index.ts +18 -0
- package/src/types.ts +94 -0
- package/tests/canvas-server.test.ts +155 -0
- package/tests/canvas-session.test.ts +354 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CanvasSession, type CanvasSessionOptions } from './canvas-session.js';
|
|
2
|
+
import type { CanvasEvent, CanvasEventType } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type ServerEventHandler = (event: CanvasEvent) => void;
|
|
5
|
+
|
|
6
|
+
export interface CanvasServerOptions {
|
|
7
|
+
maxSessions?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CanvasServer {
|
|
11
|
+
private sessions: Map<string, CanvasSession> = new Map();
|
|
12
|
+
private globalListeners: Map<CanvasEventType, Set<ServerEventHandler>> = new Map();
|
|
13
|
+
private maxSessions: number;
|
|
14
|
+
|
|
15
|
+
constructor(options: CanvasServerOptions = {}) {
|
|
16
|
+
this.maxSessions = options.maxSessions ?? 100;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createSession(options?: CanvasSessionOptions): CanvasSession {
|
|
20
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
21
|
+
throw new Error(`Maximum sessions (${this.maxSessions}) reached`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const session = new CanvasSession(options);
|
|
25
|
+
this.sessions.set(session.id, session);
|
|
26
|
+
|
|
27
|
+
// Forward session events to global listeners
|
|
28
|
+
const eventTypes: CanvasEventType[] = [
|
|
29
|
+
'object:added',
|
|
30
|
+
'object:updated',
|
|
31
|
+
'object:removed',
|
|
32
|
+
'canvas:cleared',
|
|
33
|
+
'canvas:snapshot',
|
|
34
|
+
'canvas:resized',
|
|
35
|
+
'interaction:click',
|
|
36
|
+
'interaction:input',
|
|
37
|
+
'viewer:joined',
|
|
38
|
+
'viewer:left',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const eventType of eventTypes) {
|
|
42
|
+
session.on(eventType, (event) => {
|
|
43
|
+
const handlers = this.globalListeners.get(eventType);
|
|
44
|
+
if (handlers) {
|
|
45
|
+
for (const handler of handlers) {
|
|
46
|
+
handler(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSession(id: string): CanvasSession | undefined {
|
|
56
|
+
return this.sessions.get(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
destroySession(id: string): boolean {
|
|
60
|
+
const session = this.sessions.get(id);
|
|
61
|
+
if (!session) return false;
|
|
62
|
+
|
|
63
|
+
session.clear();
|
|
64
|
+
this.sessions.delete(id);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getSessions(): CanvasSession[] {
|
|
69
|
+
return Array.from(this.sessions.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getSessionCount(): number {
|
|
73
|
+
return this.sessions.size;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
on(type: CanvasEventType, handler: ServerEventHandler): void {
|
|
77
|
+
if (!this.globalListeners.has(type)) {
|
|
78
|
+
this.globalListeners.set(type, new Set());
|
|
79
|
+
}
|
|
80
|
+
this.globalListeners.get(type)!.add(handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
off(type: CanvasEventType, handler: ServerEventHandler): void {
|
|
84
|
+
this.globalListeners.get(type)?.delete(handler);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
destroy(): void {
|
|
88
|
+
for (const id of this.sessions.keys()) {
|
|
89
|
+
this.destroySession(id);
|
|
90
|
+
}
|
|
91
|
+
this.globalListeners.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import type {
|
|
3
|
+
CanvasObject,
|
|
4
|
+
CanvasEvent,
|
|
5
|
+
CanvasEventType,
|
|
6
|
+
CanvasSnapshot,
|
|
7
|
+
ViewerInfo,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
export type EventHandler = (event: CanvasEvent) => void;
|
|
11
|
+
|
|
12
|
+
export interface CanvasSessionOptions {
|
|
13
|
+
id?: string;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CanvasSession {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
private objects: Map<string, CanvasObject> = new Map();
|
|
21
|
+
private viewers: Map<string, ViewerInfo> = new Map();
|
|
22
|
+
private listeners: Map<CanvasEventType, Set<EventHandler>> = new Map();
|
|
23
|
+
private nextZIndex: number = 1;
|
|
24
|
+
private width: number;
|
|
25
|
+
private height: number;
|
|
26
|
+
readonly createdAt: string;
|
|
27
|
+
|
|
28
|
+
constructor(options: CanvasSessionOptions = {}) {
|
|
29
|
+
this.id = options.id ?? nanoid(12);
|
|
30
|
+
this.width = options.width ?? 1920;
|
|
31
|
+
this.height = options.height ?? 1080;
|
|
32
|
+
this.createdAt = new Date().toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
addObject(obj: Omit<CanvasObject, 'id' | 'zIndex' | 'createdAt' | 'updatedAt'> & { id?: string }): CanvasObject {
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const object = {
|
|
38
|
+
...obj,
|
|
39
|
+
id: obj.id ?? nanoid(10),
|
|
40
|
+
zIndex: this.nextZIndex++,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
updatedAt: now,
|
|
43
|
+
} as CanvasObject;
|
|
44
|
+
|
|
45
|
+
this.objects.set(object.id, object);
|
|
46
|
+
this.emit({ type: 'object:added', sessionId: this.id, objectId: object.id, data: object });
|
|
47
|
+
return object;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateObject(id: string, updates: Partial<Omit<CanvasObject, 'id' | 'type' | 'createdAt'>>): CanvasObject | null {
|
|
51
|
+
const existing = this.objects.get(id);
|
|
52
|
+
if (!existing) return null;
|
|
53
|
+
|
|
54
|
+
const updated = {
|
|
55
|
+
...existing,
|
|
56
|
+
...updates,
|
|
57
|
+
id: existing.id,
|
|
58
|
+
type: existing.type,
|
|
59
|
+
createdAt: existing.createdAt,
|
|
60
|
+
updatedAt: new Date().toISOString(),
|
|
61
|
+
} as CanvasObject;
|
|
62
|
+
|
|
63
|
+
this.objects.set(id, updated);
|
|
64
|
+
this.emit({ type: 'object:updated', sessionId: this.id, objectId: id, data: updated });
|
|
65
|
+
return updated;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeObject(id: string): boolean {
|
|
69
|
+
const existed = this.objects.delete(id);
|
|
70
|
+
if (existed) {
|
|
71
|
+
this.emit({ type: 'object:removed', sessionId: this.id, objectId: id });
|
|
72
|
+
}
|
|
73
|
+
return existed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getObject(id: string): CanvasObject | undefined {
|
|
77
|
+
return this.objects.get(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getObjects(): CanvasObject[] {
|
|
81
|
+
return Array.from(this.objects.values()).sort((a, b) => a.zIndex - b.zIndex);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getObjectCount(): number {
|
|
85
|
+
return this.objects.size;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.objects.clear();
|
|
90
|
+
this.nextZIndex = 1;
|
|
91
|
+
this.emit({ type: 'canvas:cleared', sessionId: this.id });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
snapshot(): CanvasSnapshot {
|
|
95
|
+
const snap: CanvasSnapshot = {
|
|
96
|
+
sessionId: this.id,
|
|
97
|
+
objects: this.getObjects(),
|
|
98
|
+
width: this.width,
|
|
99
|
+
height: this.height,
|
|
100
|
+
takenAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
this.emit({ type: 'canvas:snapshot', sessionId: this.id, data: snap });
|
|
103
|
+
return snap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
resize(width: number, height: number): void {
|
|
107
|
+
this.width = width;
|
|
108
|
+
this.height = height;
|
|
109
|
+
this.emit({
|
|
110
|
+
type: 'canvas:resized',
|
|
111
|
+
sessionId: this.id,
|
|
112
|
+
data: { width, height },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getSize(): { width: number; height: number } {
|
|
117
|
+
return { width: this.width, height: this.height };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addViewer(id: string, name?: string): ViewerInfo {
|
|
121
|
+
const viewer: ViewerInfo = {
|
|
122
|
+
id,
|
|
123
|
+
name,
|
|
124
|
+
joinedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
this.viewers.set(id, viewer);
|
|
127
|
+
this.emit({ type: 'viewer:joined', sessionId: this.id, data: viewer });
|
|
128
|
+
return viewer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
removeViewer(id: string): boolean {
|
|
132
|
+
const existed = this.viewers.delete(id);
|
|
133
|
+
if (existed) {
|
|
134
|
+
this.emit({ type: 'viewer:left', sessionId: this.id, data: { id } });
|
|
135
|
+
}
|
|
136
|
+
return existed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getViewers(): ViewerInfo[] {
|
|
140
|
+
return Array.from(this.viewers.values());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getViewerCount(): number {
|
|
144
|
+
return this.viewers.size;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
on(type: CanvasEventType, handler: EventHandler): void {
|
|
148
|
+
if (!this.listeners.has(type)) {
|
|
149
|
+
this.listeners.set(type, new Set());
|
|
150
|
+
}
|
|
151
|
+
this.listeners.get(type)!.add(handler);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
off(type: CanvasEventType, handler: EventHandler): void {
|
|
155
|
+
this.listeners.get(type)?.delete(handler);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private emit(event: Omit<CanvasEvent, 'timestamp'>): void {
|
|
159
|
+
const fullEvent: CanvasEvent = {
|
|
160
|
+
...event,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
const handlers = this.listeners.get(event.type);
|
|
164
|
+
if (handlers) {
|
|
165
|
+
for (const handler of handlers) {
|
|
166
|
+
handler(fullEvent);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CanvasObjectType,
|
|
3
|
+
CanvasObjectBase,
|
|
4
|
+
TextObject,
|
|
5
|
+
ImageObject,
|
|
6
|
+
DrawingPoint,
|
|
7
|
+
DrawingObject,
|
|
8
|
+
WidgetObject,
|
|
9
|
+
InteractiveElementKind,
|
|
10
|
+
InteractiveObject,
|
|
11
|
+
CanvasObject,
|
|
12
|
+
CanvasEventType,
|
|
13
|
+
CanvasEvent,
|
|
14
|
+
CanvasSnapshot,
|
|
15
|
+
ViewerInfo,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
export { CanvasSession, type CanvasSessionOptions, type EventHandler } from './canvas-session.js';
|
|
18
|
+
export { CanvasServer, type CanvasServerOptions, type ServerEventHandler } from './canvas-server.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type CanvasObjectType = 'text' | 'image' | 'drawing' | 'widget' | 'interactive';
|
|
2
|
+
|
|
3
|
+
export interface CanvasObjectBase {
|
|
4
|
+
id: string;
|
|
5
|
+
type: CanvasObjectType;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
zIndex: number;
|
|
11
|
+
visible: boolean;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TextObject extends CanvasObjectBase {
|
|
18
|
+
type: 'text';
|
|
19
|
+
content: string;
|
|
20
|
+
fontSize: number;
|
|
21
|
+
fontFamily: string;
|
|
22
|
+
color: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ImageObject extends CanvasObjectBase {
|
|
26
|
+
type: 'image';
|
|
27
|
+
src: string;
|
|
28
|
+
alt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DrawingPoint {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DrawingObject extends CanvasObjectBase {
|
|
37
|
+
type: 'drawing';
|
|
38
|
+
points: DrawingPoint[];
|
|
39
|
+
strokeColor: string;
|
|
40
|
+
strokeWidth: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WidgetObject extends CanvasObjectBase {
|
|
44
|
+
type: 'widget';
|
|
45
|
+
widgetType: string;
|
|
46
|
+
props: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type InteractiveElementKind = 'button' | 'input' | 'select' | 'checkbox' | 'slider';
|
|
50
|
+
|
|
51
|
+
export interface InteractiveObject extends CanvasObjectBase {
|
|
52
|
+
type: 'interactive';
|
|
53
|
+
elementKind: InteractiveElementKind;
|
|
54
|
+
label: string;
|
|
55
|
+
value: string;
|
|
56
|
+
options?: string[];
|
|
57
|
+
disabled: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type CanvasObject = TextObject | ImageObject | DrawingObject | WidgetObject | InteractiveObject;
|
|
61
|
+
|
|
62
|
+
export type CanvasEventType =
|
|
63
|
+
| 'object:added'
|
|
64
|
+
| 'object:updated'
|
|
65
|
+
| 'object:removed'
|
|
66
|
+
| 'canvas:cleared'
|
|
67
|
+
| 'canvas:snapshot'
|
|
68
|
+
| 'canvas:resized'
|
|
69
|
+
| 'interaction:click'
|
|
70
|
+
| 'interaction:input'
|
|
71
|
+
| 'viewer:joined'
|
|
72
|
+
| 'viewer:left';
|
|
73
|
+
|
|
74
|
+
export interface CanvasEvent {
|
|
75
|
+
type: CanvasEventType;
|
|
76
|
+
sessionId: string;
|
|
77
|
+
objectId?: string;
|
|
78
|
+
data?: unknown;
|
|
79
|
+
timestamp: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CanvasSnapshot {
|
|
83
|
+
sessionId: string;
|
|
84
|
+
objects: CanvasObject[];
|
|
85
|
+
width: number;
|
|
86
|
+
height: number;
|
|
87
|
+
takenAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ViewerInfo {
|
|
91
|
+
id: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
joinedAt: string;
|
|
94
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CanvasServer } from '../src/canvas-server.js';
|
|
3
|
+
import type { CanvasEvent, TextObject } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
describe('CanvasServer', () => {
|
|
6
|
+
let server: CanvasServer;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
server = new CanvasServer({ maxSessions: 5 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
server.destroy();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('createSession', () => {
|
|
17
|
+
it('should create a new session', () => {
|
|
18
|
+
const session = server.createSession();
|
|
19
|
+
expect(session.id).toBeTruthy();
|
|
20
|
+
expect(server.getSessionCount()).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should create session with custom options', () => {
|
|
24
|
+
const session = server.createSession({ id: 'custom', width: 800, height: 600 });
|
|
25
|
+
expect(session.id).toBe('custom');
|
|
26
|
+
expect(session.getSize()).toEqual({ width: 800, height: 600 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw when max sessions reached', () => {
|
|
30
|
+
for (let i = 0; i < 5; i++) {
|
|
31
|
+
server.createSession();
|
|
32
|
+
}
|
|
33
|
+
expect(() => server.createSession()).toThrow('Maximum sessions (5) reached');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getSession', () => {
|
|
38
|
+
it('should return existing session', () => {
|
|
39
|
+
const session = server.createSession({ id: 'my-session' });
|
|
40
|
+
expect(server.getSession('my-session')).toBe(session);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return undefined for non-existent session', () => {
|
|
44
|
+
expect(server.getSession('nonexistent')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('destroySession', () => {
|
|
49
|
+
it('should remove session', () => {
|
|
50
|
+
const session = server.createSession({ id: 'to-destroy' });
|
|
51
|
+
expect(server.destroySession('to-destroy')).toBe(true);
|
|
52
|
+
expect(server.getSession('to-destroy')).toBeUndefined();
|
|
53
|
+
expect(server.getSessionCount()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return false for non-existent session', () => {
|
|
57
|
+
expect(server.destroySession('nonexistent')).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('getSessions', () => {
|
|
62
|
+
it('should return all sessions', () => {
|
|
63
|
+
server.createSession({ id: 'a' });
|
|
64
|
+
server.createSession({ id: 'b' });
|
|
65
|
+
const sessions = server.getSessions();
|
|
66
|
+
expect(sessions).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('global event forwarding', () => {
|
|
71
|
+
it('should forward object:added events from sessions', () => {
|
|
72
|
+
const handler = vi.fn();
|
|
73
|
+
server.on('object:added', handler);
|
|
74
|
+
|
|
75
|
+
const session = server.createSession();
|
|
76
|
+
session.addObject({
|
|
77
|
+
type: 'text',
|
|
78
|
+
x: 0, y: 0, width: 100, height: 100, visible: true,
|
|
79
|
+
content: 'Test', fontSize: 14, fontFamily: 'serif', color: '#000',
|
|
80
|
+
} as Omit<TextObject, 'id' | 'zIndex' | 'createdAt' | 'updatedAt'>);
|
|
81
|
+
|
|
82
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
83
|
+
expect(handler.mock.calls[0][0].sessionId).toBe(session.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should forward events from multiple sessions', () => {
|
|
87
|
+
const handler = vi.fn();
|
|
88
|
+
server.on('object:added', handler);
|
|
89
|
+
|
|
90
|
+
const s1 = server.createSession({ id: 'session-1' });
|
|
91
|
+
const s2 = server.createSession({ id: 'session-2' });
|
|
92
|
+
|
|
93
|
+
s1.addObject({
|
|
94
|
+
type: 'text',
|
|
95
|
+
x: 0, y: 0, width: 100, height: 100, visible: true,
|
|
96
|
+
content: 'A', fontSize: 14, fontFamily: 'serif', color: '#000',
|
|
97
|
+
} as Omit<TextObject, 'id' | 'zIndex' | 'createdAt' | 'updatedAt'>);
|
|
98
|
+
|
|
99
|
+
s2.addObject({
|
|
100
|
+
type: 'text',
|
|
101
|
+
x: 0, y: 0, width: 100, height: 100, visible: true,
|
|
102
|
+
content: 'B', fontSize: 14, fontFamily: 'serif', color: '#000',
|
|
103
|
+
} as Omit<TextObject, 'id' | 'zIndex' | 'createdAt' | 'updatedAt'>);
|
|
104
|
+
|
|
105
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should forward canvas:cleared events', () => {
|
|
109
|
+
const handler = vi.fn();
|
|
110
|
+
server.on('canvas:cleared', handler);
|
|
111
|
+
|
|
112
|
+
const session = server.createSession();
|
|
113
|
+
session.clear();
|
|
114
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should forward viewer events', () => {
|
|
118
|
+
const joinHandler = vi.fn();
|
|
119
|
+
const leaveHandler = vi.fn();
|
|
120
|
+
server.on('viewer:joined', joinHandler);
|
|
121
|
+
server.on('viewer:left', leaveHandler);
|
|
122
|
+
|
|
123
|
+
const session = server.createSession();
|
|
124
|
+
session.addViewer('v1', 'Alice');
|
|
125
|
+
expect(joinHandler).toHaveBeenCalledOnce();
|
|
126
|
+
|
|
127
|
+
session.removeViewer('v1');
|
|
128
|
+
expect(leaveHandler).toHaveBeenCalledOnce();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should unregister global handler', () => {
|
|
132
|
+
const handler = vi.fn();
|
|
133
|
+
server.on('object:added', handler);
|
|
134
|
+
server.off('object:added', handler);
|
|
135
|
+
|
|
136
|
+
const session = server.createSession();
|
|
137
|
+
session.addObject({
|
|
138
|
+
type: 'text',
|
|
139
|
+
x: 0, y: 0, width: 100, height: 100, visible: true,
|
|
140
|
+
content: 'Test', fontSize: 14, fontFamily: 'serif', color: '#000',
|
|
141
|
+
} as Omit<TextObject, 'id' | 'zIndex' | 'createdAt' | 'updatedAt'>);
|
|
142
|
+
|
|
143
|
+
expect(handler).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('destroy', () => {
|
|
148
|
+
it('should remove all sessions', () => {
|
|
149
|
+
server.createSession({ id: 'a' });
|
|
150
|
+
server.createSession({ id: 'b' });
|
|
151
|
+
server.destroy();
|
|
152
|
+
expect(server.getSessionCount()).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|