Skip to content
Open
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,4 +1,4 @@
import { StudentProgress } from '../../../student-progress/student-progress.component';
import { StudentProgress } from '../../../student-progress/StudentProgress';

export class ClassroomMonitorTestHelper {
workgroupId1: number = 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="container" matTooltip="{{ studentProgress.positionAndTitle }}">
@for (segment of segments; track segment.id) {
@if (segment.id === currentSegment?.id) {
<div class="segment">
{{ studentProgress.nodePosition }}
<mat-icon>place</mat-icon>
<div class="segment-bar active"></div>
</div>
} @else {
<div class="segment">
<div class="segment-bar"></div>
</div>
}
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.container {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
height: 40px;
}

.segment {
flex: 1;
flex-direction: column;
}

.segment-bar {
height: 15px;
border: 1px solid black;
}

.active {
background-color: orange;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectLocationComponent } from './project-location.component';
import { ProjectService } from '../../services/projectService';
import { StudentProgress } from '../student-progress/StudentProgress';
import { ClassroomMonitorTestingModule } from '../classroom-monitor-testing.module';
import { Node } from '../../common/Node';

describe('ProjectLocationComponent', () => {
let component: ProjectLocationComponent;
let fixture: ComponentFixture<ProjectLocationComponent>;
let projectService: ProjectService;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ClassroomMonitorTestingModule, ProjectLocationComponent]
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ProjectLocationComponent);
component = fixture.componentInstance;
projectService = TestBed.inject(ProjectService);
});

describe('ngOnChanges()', () => {
describe('when project has multiple group nodes', () => {
beforeEach(() => {
spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([
{ id: 'group1', title: 'Segment 1' },
{ id: 'group2', title: 'Segment 2' },
{ id: 'group3', title: 'Segment 3' }
]);
spyOn(projectService, 'getParentGroup').and.returnValue({
id: 'group2',
title: 'Segment 2'
});
});

it('should set segments to group nodes', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
expect(component['segments'].length).toBe(3);
expect(component['segments'][0].id).toBe('group1');
});

it('should set current segment to parent group of current node', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
expect(component['currentSegment'].id).toBe('group2');
expect(projectService.getParentGroup).toHaveBeenCalledWith('node2');
});
});

describe('when project has single or no group nodes', () => {
beforeEach(() => {
spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([
{ id: 'group1', title: 'Main Group' }
]);
spyOn(projectService, 'getNode').and.returnValue({
id: 'node2',
title: 'Step 2',
type: 'node'
} as Node);
projectService.idToOrder = {
node1: { order: 1 },
node2: { order: 2 },
node3: { order: 3 },
group1: { order: 0 }
};
spyOn(projectService, 'getNodes').and.returnValue([
{ id: 'group1', type: 'group' },
{ id: 'node1', type: 'node' },
{ id: 'node2', type: 'node' },
{ id: 'node3', type: 'node' }
]);
});

it('should set segments to ordered step nodes', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2',
positionAndTitle: '2: Step Title'
});
component.ngOnChanges();
expect(component['segments'].length).toBe(3);
expect(component['segments'][0].id).toBe('node1');
expect(component['segments'][1].id).toBe('node2');
expect(component['segments'][2].id).toBe('node3');
});

it('should set current segment to current node', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2',
positionAndTitle: '2: Step Title'
});
component.ngOnChanges();
expect(component['currentSegment'].id).toBe('node2');
expect(projectService.getNode).toHaveBeenCalledWith('node2');
});

it('should filter out group nodes from segments', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node1',
nodePosition: '1',
positionAndTitle: '1: First Step'
});
component.ngOnChanges();
const hasGroupNode = component['segments'].some((segment) => segment.type === 'group');
expect(hasGroupNode).toBeFalse();
});

it('should sort nodes by order', () => {
projectService.idToOrder = {
node1: { order: 3 },
node2: { order: 1 },
node3: { order: 2 },
group1: { order: 0 }
};
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '1',
positionAndTitle: '1: Step Title'
});
component.ngOnChanges();
expect(component['segments'][0].id).toBe('node2');
expect(component['segments'][1].id).toBe('node3');
expect(component['segments'][2].id).toBe('node1');
});
});
});

describe('template rendering', () => {
beforeEach(() => {
spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([
{ id: 'group1', title: 'Segment 1' },
{ id: 'group2', title: 'Segment 2' },
{ id: 'group3', title: 'Segment 3' }
]);
spyOn(projectService, 'getParentGroup').and.returnValue({ id: 'group2', title: 'Segment 2' });
});

it('should display tooltip with position and title', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
expect(component.studentProgress.positionAndTitle).toBe('2.1: Step Title');
});

it('should render all segments', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
const segments = fixture.nativeElement.querySelectorAll('.segment');
expect(segments.length).toBe(3);
});

it('should display node position and icon for current segment', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
const segments = fixture.nativeElement.querySelectorAll('.segment');
const activeSegment = segments[1]; // group2 is the second segment
expect(activeSegment.textContent.trim()).toContain('2.1');
expect(activeSegment.querySelector('mat-icon')).toBeTruthy();
expect(activeSegment.querySelector('mat-icon').textContent).toBe('place');
});

it('should apply active class to current segment bar', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
const segments = fixture.nativeElement.querySelectorAll('.segment');
const activeSegmentBar = segments[1].querySelector('.segment-bar');
expect(activeSegmentBar.classList.contains('active')).toBeTrue();
});

it('should not display node position or icon for non-current segments', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
const segments = fixture.nativeElement.querySelectorAll('.segment');
const inactiveSegment = segments[0]; // group1 is the first segment
expect(inactiveSegment.querySelector('mat-icon')).toBeFalsy();
expect(inactiveSegment.textContent.trim()).not.toContain('2.1');
});

it('should not apply active class to non-current segment bars', () => {
component.studentProgress = new StudentProgress({
currentNodeId: 'node2',
nodePosition: '2.1',
positionAndTitle: '2.1: Step Title'
});
component.ngOnChanges();
fixture.detectChanges();
const segments = fixture.nativeElement.querySelectorAll('.segment');
const inactiveSegmentBar = segments[0].querySelector('.segment-bar');
expect(inactiveSegmentBar.classList.contains('active')).toBeFalse();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Component, inject, Input } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { ProjectService } from '../../services/projectService';
import { MatIcon } from '@angular/material/icon';
import { StudentProgress } from '../student-progress/StudentProgress';

@Component({
imports: [MatIcon, MatTooltip],
selector: 'project-location',
styleUrl: './project-location.component.scss',
templateUrl: './project-location.component.html'
})
export class ProjectLocationComponent {
private projectService: ProjectService = inject(ProjectService);

protected currentSegment: any;
protected segments: any[];
@Input() studentProgress: StudentProgress;

ngOnChanges(): void {
const groupNodes = this.projectService.getOrderedGroupNodes();
if (groupNodes.length > 1) {
this.segments = groupNodes;
this.currentSegment = this.projectService.getParentGroup(this.studentProgress.currentNodeId);
} else {
this.segments = this.getOrderedNodes();
this.currentSegment = this.projectService.getNode(this.studentProgress.currentNodeId);
}
}

private getOrderedNodes(): any[] {
const idToOrder = this.projectService.idToOrder;
return this.projectService
.getNodes()
.filter((node) => node.type !== 'group')
.sort((a, b) => idToOrder[a.id].order - idToOrder[b.id].order);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ProjectCompletion } from '../../common/ProjectCompletion';

export class StudentProgress {
currentNodeId: string;
periodId: string;
periodName: string;
workgroupId: number;
username: string;
firstName: string;
lastName: string;
nodePosition: string;
positionAndTitle: string;
order: number;
completion: ProjectCompletion;
completionPct: number;
score: number;
maxScore: number;
scorePct: number;

constructor(jsonObject: any = {}) {
for (const key of Object.keys(jsonObject)) {
this[key] = jsonObject[key];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
} @else {
<td class="heavy td--wrap">{{ student.username }}</td>
}
<td class="center td--wrap td--max-width">{{ student.position }}</td>
<td class="center td--wrap td--max-width">
<project-location [studentProgress]="student" />
</td>
<td class="flex justify-center items-center">
<project-progress
[completed]="student.completion.completedItems"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ClassroomMonitorTestingModule } from '../classroom-monitor-testing.modu
import { ClassroomMonitorTestHelper } from '../classroomMonitorComponents/shared/testing/ClassroomMonitorTestHelper';
import { StudentProgressComponent } from './student-progress.component';
import { provideRouter } from '@angular/router';
import { MockComponent } from 'ng-mocks';
import { ProjectLocationComponent } from '../project-location/project-location.component';

class SortTestParams {
constructor(
Expand All @@ -22,7 +24,11 @@ const { workgroupId1, workgroupId2, workgroupId3, workgroupId4, workgroupId5 } =
describe('StudentProgressComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ClassroomMonitorTestingModule, StudentProgressComponent],
imports: [
ClassroomMonitorTestingModule,
MockComponent(ProjectLocationComponent),
StudentProgressComponent
],
providers: [provideRouter([])]
}).compileComponents();
});
Expand Down
Loading
Loading