Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface QuestionThread {
messageIds?: string[],
name?: string,
lastMessageDate?: Date,
assignmentName?: string,
}

/**
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/app/pages/chat-view/chat-view.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<div class="sidebar">
<button mat-stroked-button
class="toggle-button"
(click)="toggleChatVisibility()">
(click)="toggleChatVisibility()"
[disabled]="userType === 'teacher' && !validChatId">
{{ showOtherChats ? USER_CHATS : OTHER_CHATS }}
</button>

Expand Down
3 changes: 1 addition & 2 deletions frontend/src/app/pages/chat-view/chat-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ export class ChatViewComponent implements OnInit, OnChanges {
}
return thread;
});
// Visibility change of a thread in the list doesn't require re-validating current selection
// unless the selected thread itself is modified in a way that affects its validity (e.g., deleted).
// Visibility change of a thread in the list doesn't require reloding the whole list.
// For visibility, the badge will just update.
});
}
Expand Down
223 changes: 210 additions & 13 deletions frontend/src/app/services/questionThread.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
import {
QuestionThreadResponse,
QuestionThreadResponseSingle
} from '../interfaces/questionThread/questionThreadResponse';
}
from '../interfaces/questionThread/questionThreadResponse';
import { QuestionThreadService } from './questionThread.service';
import { AuthenticationService } from './authentication.service';
import { ErrorService } from './error.service';
import { AssignmentService } from './assignment.service';
import { MessageService } from './message.service';
import { ClassesService } from './classes.service';
import { LearningObjectService } from './learningObject.service';
import { LearningObjectService } from '../services/learningObject.service';
import { UserType } from '../interfaces/user';
import { Assignment } from '../interfaces/assignment';
import { Class } from '../interfaces/classes/class';
Expand All @@ -39,6 +40,8 @@ describe('QuestionThreadService', () => {
isClosed: false,
visibility: VisibilityType.GROUP,
messageIds: [],
lastMessageDate: new Date(),
name: 'Default Thread Name'
};

const newThreadPayload: NewQuestionThread = {
Expand Down Expand Up @@ -142,25 +145,219 @@ describe('QuestionThreadService', () => {
});
});

it('filterThreads filters and sorts for teacher without showOtherChats', () => {
const threads: QuestionThread[] = [
{ ...questionThread, id: '1', creatorId: 'u2', lastMessageDate: new Date(1) },
{ ...questionThread, id: '2', creatorId: 'u1', lastMessageDate: new Date(2) }
];
const res = service.filterThreads(threads, 'u1', 'a1', false);
expect(res.map(t => t.id)).toEqual(['2', '1']);
});

it('getThreadTitle combines class, assignment and LO names', done => {
const thread: QuestionThread = { ...questionThread, assignmentId: 'a1', learningObjectId: 'lo1' };
const assignmentMock = { classId: 'c1', className: 'CN', name: 'AN' };
const classMock = { name: 'ClassName' };
lo.getTitleOrFallback.and.returnValue(of('LOTitle'));

assignment.retrieveAssignmentById.and.returnValue(of(assignmentMock as Assignment));
classes.classWithId.and.returnValue(of(classMock as Class));
lo.getTitleOrFallback.and.returnValue(of('LOTitle'));

service.getThreadTitle(thread).subscribe((title: string) => {
expect(title).toBe('ClassName > AN > LOTitle');
done();
});
});
});

describe('loadSideBarQuestionThreads', () => {
const dummyThreads: QuestionThread[] = [questionThread];

beforeEach(() => {
spyOn(service, 'initializeAllThreads').and.returnValue(of(void 0));
spyOn(service, 'filterAndNameThreads').and.returnValue(of(dummyThreads));
});

it('should call initializeAllThreads then filterAndNameThreads', done => {
assignment.retrieveAssignments.and.returnValue(of([]));

service.loadSideBarQuestionThreads('assignment-id', false).subscribe(res => {
// Corrected access to private property using 'unknown' first
expect((service as unknown as { allThreadsCache: QuestionThread[] }).allThreadsCache).toBeDefined();
expect(service.filterAndNameThreads).toHaveBeenCalledWith(
(service as unknown as { allThreadsCache: QuestionThread[] }).allThreadsCache,
'creator-id',
'assignment-id',
false
);
expect(res).toEqual(dummyThreads);
done();
});
});
});

describe('filterAndNameThreads', () => {
const assignA = { id: 'assignment-id', name: 'AN', classId: 'c1', className: 'CN' } as Assignment;

// Type the spy directly
let getThreadNameWithTimestampSpy: jasmine.Spy<(thread: QuestionThread, assignmentName: string) => Observable<string>>;

beforeEach(() => {
assignment.retrieveAssignments.and.returnValue(of([assignA]));

getThreadNameWithTimestampSpy = spyOn(
service as unknown as { getThreadNameWithTimestamp: (thread: QuestionThread, assignmentName: string) => Observable<string> },
'getThreadNameWithTimestamp'
).and.callFake((thread: QuestionThread, assignmentName: string) => {
return of(`${assignmentName} - generated for ${thread.id}`);
});
});

it('should filter, name unnamed threads, and sort by lastMessageDate descending', done => {
const namedThread: QuestionThread = {
...questionThread,
id: '1',
name: 'Existing Name',
assignmentId: 'assignment-id',
assignmentName: 'AN',
lastMessageDate: new Date(1)
};
const unnamedThread: QuestionThread = {
...questionThread,
id: '2',
name: '',
assignmentId: 'assignment-id',
assignmentName: 'AN',
lastMessageDate: new Date(2)
};

service.filterAndNameThreads([namedThread, unnamedThread], 'creator-id', 'assignment-id', false)
.subscribe(result => {
expect(result.length).toBe(2);
expect(result[0].id).toBe('2');
expect(result[0].name).toBe('AN - generated for 2');
expect(result[1].id).toBe('1');
expect(result[1].name).toBe('Existing Name');

expect(getThreadNameWithTimestampSpy).toHaveBeenCalledTimes(1);
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(unnamedThread, 'AN');
done();
});
});

it('should skip naming when all threads already have names', done => {
const t1 = { ...questionThread, id: '1', name: 'N1', lastMessageDate: new Date(1), assignmentId: 'assignment-id', assignmentName: 'AN' };
const t2 = { ...questionThread, id: '2', name: 'N2', lastMessageDate: new Date(2), assignmentId: 'assignment-id', assignmentName: 'AN' };

service.filterAndNameThreads([t1, t2], 'creator-id', 'assignment-id', false)
.subscribe(result => {
expect(result.map(t => t.id)).toEqual(['2', '1']);
expect(getThreadNameWithTimestampSpy).not.toHaveBeenCalled();
done();
});
});

it('should correctly filter threads based on showOtherChats and userType (student)', done => {
auth.retrieveUserType.and.returnValue(UserType.STUDENT);

const groupVisibleThread: QuestionThread = {
...questionThread,
id: 'group1',
visibility: VisibilityType.GROUP,
creatorId: 'other-creator',
assignmentId: 'assignment-id',
assignmentName: 'AN',
name: ''
};
const privateVisibleThread: QuestionThread = {
...questionThread,
id: 'private1',
visibility: VisibilityType.PRIVATE,
creatorId: 'creator-id',
assignmentId: 'assignment-id',
assignmentName: 'AN',
name: ''
};
const privateVisibleOtherUserThread: QuestionThread = {
...questionThread,
id: 'private2',
visibility: VisibilityType.PRIVATE,
creatorId: 'some-other-creator',
assignmentId: 'assignment-id',
assignmentName: 'AN',
name: ''
};
const otherAssignmentThread: QuestionThread = {
...questionThread,
id: 'otherAssign',
visibility: VisibilityType.GROUP,
creatorId: 'creator-id',
assignmentId: 'another-assignment-id',
assignmentName: 'OA',
name: ''
};

const studentThreadsScenario1 = [groupVisibleThread, privateVisibleThread, privateVisibleOtherUserThread, otherAssignmentThread];

service.filterAndNameThreads(studentThreadsScenario1, 'creator-id', 'assignment-id', true)
.subscribe(result => {
expect(result.length).toBe(2);
expect(result.some(t => t.id === 'group1')).toBeTrue();
expect(result.some(t => t.id === 'otherAssign')).toBeTrue();
});

const studentThreadsScenario2 = [groupVisibleThread, privateVisibleThread, privateVisibleOtherUserThread, otherAssignmentThread];
service.filterAndNameThreads(studentThreadsScenario2, 'creator-id', 'assignment-id', false)
.subscribe(result => {
expect(result.length).toBe(2);
expect(result.some(t => t.id === 'private1')).toBeTrue();
expect(result.some(t => t.id === 'otherAssign')).toBeTrue();
done();
});
});


it('should correctly filter threads based on showOtherChats and userType (teacher)', done => {
auth.retrieveUserType.and.returnValue(UserType.TEACHER);

const groupVisibleThread: QuestionThread = {
...questionThread,
id: 'group1',
visibility: VisibilityType.GROUP,
creatorId: 'other-creator',
assignmentId: 'assignment-id',
assignmentName: 'AN',
name: ''
};
const privateVisibleThread: QuestionThread = {
...questionThread,
id: 'private1',
visibility: VisibilityType.PRIVATE,
creatorId: 'creator-id',
assignmentId: 'assignment-id',
assignmentName: 'AN',
name: ''
};
const otherAssignmentThread: QuestionThread = {
...questionThread,
id: 'otherAssign',
assignmentId: 'other-assignment-id',
assignmentName: 'OA',
name: ''
};

const allThreadsForTeacherTest = [groupVisibleThread, privateVisibleThread, otherAssignmentThread];

service.filterAndNameThreads(allThreadsForTeacherTest, 'creator-id', 'assignment-id', true)
.subscribe(result => {
expect(result.length).toBe(2);
expect(result.some(t => t.id === 'group1')).toBeTrue();
expect(result.some(t => t.id === 'private1')).toBeTrue();
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(groupVisibleThread, 'AN');
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(privateVisibleThread, 'AN');
});

service.filterAndNameThreads(allThreadsForTeacherTest, 'creator-id', 'assignment-id', false)
.subscribe(result => {
expect(result.length).toBe(3);
expect(result.some(t => t.id === 'group1')).toBeTrue();
expect(result.some(t => t.id === 'private1')).toBeTrue();
expect(result.some(t => t.id === 'otherAssign')).toBeTrue();
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(groupVisibleThread, 'AN');
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(privateVisibleThread, 'AN');
expect(getThreadNameWithTimestampSpy).toHaveBeenCalledWith(otherAssignmentThread, 'OA');
done();
});
});
});
});
Loading