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
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
<span class="flex justify-start items-center">
<span class="progress-wrapper" tabindex="0">
@if (period.periodId === -1) {
<mat-progress-bar
matTooltip="{{ nodeCompletion }}% completed (All periods)"
i18n-matTooltip
matTooltipPosition="above"
class="nav-item__progress"
mode="determinate"
value="{{ nodeCompletion }}"
/>
} @else {
<mat-progress-bar
matTooltip="{{ nodeCompletion }}% completed (Period: {{ period.periodName }})"
i18n-matTooltip
matTooltipPosition="above"
class="nav-item__progress"
mode="determinate"
value="{{ nodeCompletion }}"
/>
}
<span tabindex="0">
<mat-progress-bar
matTooltip="{{ nodeCompletion }}% completed"
i18n-matTooltip
matTooltipPosition="above"
class="nav-item__progress"
mode="determinate"
value="{{ nodeCompletion }}"
/>
</span>
<span class="nav-item__progress-value md-body-2 text-secondary hidden md:block"
>{{ nodeCompletion }}%</span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ import { MatTooltipModule } from '@angular/material/tooltip';
})
export class NavItemProgressComponent {
@Input() nodeCompletion: string;
@Input() period: any;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@if (workgroupsOnNode.length > 0) {
<div
class="flex items-center p-1 rounded-md notice-bg-bg"
tabindex="0"
matTooltip="{{ tooltipText }}"
matTooltipPosition="above"
matTooltipClass="multiline-tooltip"
#tooltip="matTooltip"
>
<mat-icon>person</mat-icon>
{{ workgroupsOnNode.length }}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TeamsOnNodeComponent } from './teams-on-node.component';
import { ClassroomStatusService } from '../../../assets/wise5/services/classroomStatusService';
import { ConfigService } from '../../../assets/wise5/services/configService';
import { ProjectService } from '../../../assets/wise5/services/projectService';
import { Subject } from 'rxjs';

describe('TeamsOnNodeComponent', () => {
let component: TeamsOnNodeComponent;
let fixture: ComponentFixture<TeamsOnNodeComponent>;
let classroomStatusService: jasmine.SpyObj<ClassroomStatusService>;
let configService: jasmine.SpyObj<ConfigService>;
let projectService: jasmine.SpyObj<ProjectService>;
let studentStatusReceivedSubject: Subject<void>;

const nodeId = 'node1';
const stepNodeId = 'node2';
const lessonNodeId = 'group1';
const periodId = 1;
const periodName = 'Period 1';

const createMockWorkgroup = (workgroupId: number) => ({
workgroupId,
periodId
});

beforeEach(async () => {
studentStatusReceivedSubject = new Subject<void>();
const classroomStatusServiceSpy = jasmine.createSpyObj('ClassroomStatusService', [
'getWorkgroupsOnNode'
]);
classroomStatusServiceSpy.studentStatusReceived$ = studentStatusReceivedSubject.asObservable();
const configServiceSpy = jasmine.createSpyObj('ConfigService', [
'getPermissions',
'getDisplayUsernamesByWorkgroupId'
]);
const projectServiceSpy = jasmine.createSpyObj('ProjectService', ['isApplicationNode']);

await TestBed.configureTestingModule({
imports: [TeamsOnNodeComponent],
providers: [
{ provide: ClassroomStatusService, useValue: classroomStatusServiceSpy },
{ provide: ConfigService, useValue: configServiceSpy },
{ provide: ProjectService, useValue: projectServiceSpy }
]
}).compileComponents();

classroomStatusService = TestBed.inject(
ClassroomStatusService
) as jasmine.SpyObj<ClassroomStatusService>;
configService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
projectService = TestBed.inject(ProjectService) as jasmine.SpyObj<ProjectService>;

fixture = TestBed.createComponent(TeamsOnNodeComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('ngOnInit', () => {
it('should subscribe to studentStatusReceived$ and call ngOnChanges', () => {
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

fixture.detectChanges();

spyOn(component, 'ngOnChanges');
studentStatusReceivedSubject.next();

expect(component.ngOnChanges).toHaveBeenCalled();
});
});

describe('ngOnDestroy', () => {
it('should unsubscribe from subscriptions', () => {
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

fixture.detectChanges();

spyOn(component['subscriptions'], 'unsubscribe');

component.ngOnDestroy();

expect(component['subscriptions'].unsubscribe).toHaveBeenCalled();
});
});

describe('ngOnChanges', () => {
describe('workgroups on node', () => {
it('should get workgroups on node for the specified period', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();

expect(classroomStatusService.getWorkgroupsOnNode).toHaveBeenCalledWith(nodeId, periodId);
expect(component['workgroupsOnNode']).toEqual(workgroups);
});
});

describe('tooltip text for step', () => {
beforeEach(() => {
component.nodeId = stepNodeId;
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
});

it('should create tooltip text for single team on step in specific period', () => {
const workgroups = [createMockWorkgroup(1)];
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('1 team on this step:');
});

it('should create tooltip text for multiple teams on step in specific period', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('3 teams on this step:');
});

it('should create tooltip text for single team on step in all periods', () => {
const workgroups = [createMockWorkgroup(1)];
component.period = { periodId: -1, periodName: 'All Periods' };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('1 team on this step:');
});

it('should create tooltip text for multiple teams on step in all periods', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
component.period = { periodId: -1, periodName: 'All Periods' };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('2 teams on this step:');
});
});

describe('tooltip text for lesson', () => {
beforeEach(() => {
component.nodeId = lessonNodeId;
projectService.isApplicationNode.and.returnValue(false);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
});

it('should create tooltip text for single team on lesson in specific period', () => {
const workgroups = [createMockWorkgroup(1)];
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('1 team on this lesson:');
});

it('should create tooltip text for multiple teams on lesson in specific period', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('2 teams on this lesson:');
});

it('should create tooltip text for single team on lesson in all periods', () => {
const workgroups = [createMockWorkgroup(1)];
component.period = { periodId: -1, periodName: 'All Periods' };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('1 team on this lesson:');
});

it('should create tooltip text for multiple teams on lesson in all periods', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
component.period = { periodId: -1, periodName: 'All Periods' };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);

component.ngOnChanges();

expect(component['tooltipText']).toBe('3 teams on this lesson:');
});
});

describe('tooltip text with student names', () => {
beforeEach(() => {
component.nodeId = stepNodeId;
component.period = { periodId, periodName };
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: true } as any);
});

it('should append student names to tooltip when permission is granted', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
configService.getDisplayUsernamesByWorkgroupId.and.callFake((workgroupId: number) => {
if (workgroupId === 1) return 'Alice, Bob';
if (workgroupId === 2) return 'Charlie, David';
return '';
});

component.ngOnChanges();

expect(component['tooltipText']).toBe(
'2 teams on this step:\nAlice, Bob\nCharlie, David\n'
);
});

it('should not append student names when permission is not granted', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();

expect(component['tooltipText']).toBe('2 teams on this step:');
expect(configService.getDisplayUsernamesByWorkgroupId).not.toHaveBeenCalled();
});
});
});

describe('template rendering', () => {
it('should display workgroup count when there are workgroups on node', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();
fixture.detectChanges();

const countElement = fixture.nativeElement.querySelector('div');
expect(countElement).toBeTruthy();
expect(countElement.textContent.trim()).toContain('3');
});

it('should not display anything when there are no workgroups on node', () => {
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();
fixture.detectChanges();

const countElement = fixture.nativeElement.querySelector('div');
expect(countElement).toBeFalsy();
});

it('should display mat-icon with person icon', () => {
const workgroups = [createMockWorkgroup(1)];
component.nodeId = nodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();
fixture.detectChanges();

const iconElement = fixture.nativeElement.querySelector('mat-icon');
expect(iconElement).toBeTruthy();
expect(iconElement.textContent.trim()).toBe('person');
});

it('should set tooltip text for mat-icon', () => {
const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
component.nodeId = stepNodeId;
component.period = { periodId, periodName };
classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
projectService.isApplicationNode.and.returnValue(true);
configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);

component.ngOnChanges();
fixture.detectChanges();

expect(component['tooltipText']).toBe('2 teams on this step:');
});
});
});
Loading