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,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,21 @@
<div
class="flex justify-between w-full"
matTooltip="{{ studentProgress.positionAndTitle }}"
matTooltipPosition="above"
>
@for (segment of segments; track segment.id) {
<div class="segment flex-1 h-8 relative mx-0.25">
@if (segment.id === currentSegment?.id) {
<span
class="mat-caption absolute top-0 left-1/2 -translate-x-1/2 flex min-w-full items-center justify-center @container"
>
<mat-icon class="mat-18 @max-[2.5rem]:!hidden">place</mat-icon>
<span>{{ studentProgress.nodePosition }}</span>
</span>
<div class="segment-bar primary-bg"></div>
} @else {
<div class="segment-bar bg-neutral-200"></div>
}
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss";

.segment-bar {
@apply h-3 absolute bottom-0 w-full;
}
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 style 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('primary-bg')).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 style 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('primary-bg')).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