Skip to content

Commit

Permalink
Fix part of oppia#18384: Add goals modal for learner dashboard redesi…
Browse files Browse the repository at this point in the history
…gn (oppia#21143)

* Feature flag and title

* Add button

* Added modal html file

* Added ts for modal

* Added modal functionality

* I18N keys

* Add goals styling

* Test cases for add goals modal

* Added test cases for add goals modal

* Added goals with api

* Added test cases in goals tab for openModal

* Corrected object type and flippedobject key and value

* Updated openModal with API calls andcompleted goals

* Corrected add goal modal tests and fixed bug in goals tab

* Updated goals tab tests

* Fixed new test cases for openModal and missing tag in goals tab

* Small screen UI modal errors and current goals not being modified properly

* Linter fixes

* Lint errors

* Type errors

* Linter fixes

* Fixed copyright notes in add-goals-modal.spec

* Rearranged I18N keys alphabetically for goals section

* Removed duplicate goal tab heading which is handled in getDashboardTabHeading and username variable

* Renamed ngFor variable in add-goals-modal
  • Loading branch information
amyyeung17 authored Nov 4, 2024
1 parent 7225eff commit b704e4b
Show file tree
Hide file tree
Showing 12 changed files with 766 additions and 101 deletions.
7 changes: 7 additions & 0 deletions assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,13 @@
"I18N_LEARNER_DASHBOARD_EVENING_GREETING": "Good Evening",
"I18N_LEARNER_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_PLAYED": "Last Played",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION": "Goals",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_ADD_BUTTON": "Add a goal",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_HEADING": "<[username]>'s Goals",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_INSTRUCTIONS": "Choose up to 5 topics of your interest, and then complete all chapters on the selected topics to achieve your learning goals.",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_CANCEL": "Cancel",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_HEADING": "Add or edit a goal",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_INSTRUCTIONS": "You can select up to 5 goals at a time",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_SAVE": "Save",
"I18N_LEARNER_DASHBOARD_GOLD_BADGE": "Gold",
"I18N_LEARNER_DASHBOARD_HOME_CLASSROOM_SECTION": "Topics available in Oppia's Classroom",
"I18N_LEARNER_DASHBOARD_HOME_CLASSROOM_SECTION_LINK": "Or Explore All Lessons in Classroom",
Expand Down
7 changes: 7 additions & 0 deletions assets/i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,13 @@
"I18N_LEARNER_DASHBOARD_EVENING_GREETING": "Good evening greeting on the learner dashboard.\n{{Identical|Good}}",
"I18N_LEARNER_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_PLAYED": "Option in drop down list for sorting explorations - This is an option for sorting explorations by Last Played",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION": "Text for the goals section in the learner dashboard.",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_ADD_BUTTON": "Button text for adding a goal in the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_HEADING": "Heading text for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_INSTRUCTIONS": "Instruction text on how to add goals for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_CANCEL": "Text for cancel button in modal to add goals for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_HEADING": "Heading text in modal to add goals for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_INSTRUCTIONS": "Instructions in modal to add goals for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_SAVE": "Text for the save button in modal to add goals for the goals section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_GOLD_BADGE": "Text for the gold badge in the learner dashboard.\n\n{{Identical|Gold}}",
"I18N_LEARNER_DASHBOARD_HOME_CLASSROOM_SECTION": "Text for the classroom section in the learner dashboard",
"I18N_LEARNER_DASHBOARD_HOME_CLASSROOM_SECTION_LINK": "Text for the classroom section link in the learner dashboard - when user clicks on link, they are taken to classroom page",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<div class="align-items-center d-flex flex-column h-100">
<h3 class="oppia-learner-dash-goals-modal-title" mat-dialog-title> {{ 'I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_HEADING' | translate }} </h3>
<p class="oppia-learner-dash-goals-modal-info"> {{ 'I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_INSTRUCTIONS' | translate }} </p>
<div class="oppia-learner-dash-goals-modal-content pt-1" mat-dialog-content>
<!--TODO(#18384): What if all the goals are completed, should they be disabled and checked?-->
<!--TODO(#18384): Topic name translate keys-->
<ng-container *ngFor="let topic of topics | keyvalue">
<mat-checkbox color="primary"
class="oppia-learner-dash-goals-checkbox"
(change)="onChange(topic.key)"
[checked]="checkedTopics.has(topic.key)"
[disabled]="checkedTopics.length === 5 && !checkedTopics.has(topic.key) || completedTopics.has(topic.key)">
{{ topic.value }}
</mat-checkbox>
</ng-container>
</div>
<div class="oppia-learner-dash-goals-modal-actions w-100" mat-dialog-actions>
<button (click)="onClose()" class="oppia-learner-dash-button--inverse oppia-learner-dash-button-sm oppia-learner-dash-goals-button--modal" mat-button> {{ 'I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_CANCEL' | translate }} </button>
<button (click)="onSubmit()" class="oppia-learner-dash-button--default oppia-learner-dash-button-sm oppia-learner-dash-goals-button--modal" mat-button> {{ 'I18N_LEARNER_DASHBOARD_GOALS_SECTION_MODAL_SAVE' | translate }} </button>
</div>
</div>

<style>
.oppia-learner-dash-goals-button--modal {
height: 44px;
width: 178px;
}

.oppia-learner-dash-goals-checkbox {
font-size: 16px;
gap: 0 12px;
}

.oppia-learner-dash-goals-modal-content {
display: grid;
gap: 0 4px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
height: 100%;
padding-top: 12px;
width: 100%;
}

.oppia-learner-dash-goals-modal-actions {
display: flex;
gap: 0 12px;
justify-content: end;
}

.oppia-learner-dash-goals-modal-title {
font-size: 20px;
font-weight: 500;
text-align: center;
}

.oppia-learner-dash-goals-modal-info {
font-size: 14px;
text-align: center;
}


@media screen and (max-width: 576px) {
.oppia-learner-dash-goals-modal-actions {
gap: 0;
}

.oppia-learner-dash-goals-button--modal {
max-width: 45%;
width: 147px;
}

.oppia-learner-dash-goals-modal-content {
align-items: center;
display: flex;
flex-direction: column;
gap: 12px 0;
height: fit-content;
padding: 12px;
}
}

@media screen and (max-width: 767px) {
.oppia-learner-dash-goals-modal-actions {
flex-grow: 1;
justify-content: center;
}

.oppia-learner-dash-goals-modal-content {
grid-template-columns: repeat(2, 1fr);
}
}

</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright 2024 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview Unit tests for AddGoalsModal
*/

import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FormsModule} from '@angular/forms';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
import {MockTranslatePipe} from 'tests/unit-test-utils';
import {AddGoalsModalComponent} from './add-goals-modal.component';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {By} from '@angular/platform-browser';
import {of} from 'rxjs';
const data = {
checkedTopics: new Set(),
completedTopics: new Set(),
topics: {
0: 'Addition',
1: 'Subtraction',
2: 'Multiplication',
3: 'Divsion',
4: 'Fractions',
5: 'Exponents',
},
};

describe('AddGoalsModalComponent', () => {
let component: AddGoalsModalComponent;
let fixture: ComponentFixture<AddGoalsModalComponent>;
let matDialogSpy: jasmine.SpyObj<MatDialogRef<AddGoalsModalComponent>>;
beforeEach(waitForAsync(() => {
matDialogSpy = jasmine.createSpyObj('MatDialogRef', [
'close',
'afterClosed',
]);
matDialogSpy.afterClosed.and.returnValue(of(true));
TestBed.configureTestingModule({
imports: [FormsModule, HttpClientTestingModule],
providers: [
{
provide: MatDialogRef,
useValue: matDialogSpy,
},
{
provide: MAT_DIALOG_DATA,
useValue: data,
},
],
declarations: [AddGoalsModalComponent, MockTranslatePipe],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(AddGoalsModalComponent);
component = fixture.componentInstance;
data.checkedTopics = new Set();
data.completedTopics = new Set();
fixture.detectChanges();
});

it('should create a component', () => {
expect(component).toBeTruthy();
expect(component.checkedTopics).toEqual(new Set());
expect(component.completedTopics).toEqual(new Set());
expect(component.topics).toEqual({
0: 'Addition',
1: 'Subtraction',
2: 'Multiplication',
3: 'Divsion',
4: 'Fractions',
5: 'Exponents',
});
const checkboxes = fixture.debugElement.queryAll(By.css('mat-checkbox'));

checkboxes.forEach(box => expect(box.nativeElement.checked).toBeFalse());
});

it('should intialize checked boxes of previously selected goals', () => {
component.checkedTopics = new Set(['0', '2']);
fixture.detectChanges();

const checkboxes = fixture.debugElement.queryAll(By.css('mat-checkbox'));
expect(checkboxes[0].nativeElement.checked).toBeTrue();
expect(checkboxes[2].nativeElement.checked).toBeTrue();
});

it('should add a goal id when checking an unchecked box', () => {
const firstCheckbox = fixture.debugElement.query(
By.css('mat-checkbox:first-child')
);
expect(firstCheckbox.nativeElement.checked).toBeFalse();
spyOn(component, 'onChange').and.callThrough();
firstCheckbox.triggerEventHandler('change', {target: {checked: true}});

fixture.detectChanges();

expect(component.onChange).toHaveBeenCalledWith('0');
expect(component.checkedTopics.has('0')).toBeTrue();
expect(firstCheckbox.nativeElement.checked).toBeTrue();
});

it('should remove a goal id when checking a checked box', () => {
component.checkedTopics = new Set(['0']);
const firstCheckbox = fixture.debugElement.query(
By.css('mat-checkbox:first-child')
);
fixture.detectChanges();
expect(firstCheckbox.nativeElement.checked).toBeTrue();

spyOn(component, 'onChange').and.callThrough();
firstCheckbox.triggerEventHandler('change', {target: {checked: false}});
fixture.detectChanges();

expect(component.onChange).toHaveBeenCalledWith('0');
expect(component.checkedTopics.has('0')).toBeFalse();
expect(firstCheckbox.nativeElement.checked).toBeFalse();
});

it('should disable checkbox if completed', () => {
component.completedTopics = new Set(['0']);
const firstCheckbox = fixture.debugElement.query(
By.css('mat-checkbox:first-child')
);
fixture.detectChanges();

expect(firstCheckbox.nativeElement.checked).toBeFalse();
expect(firstCheckbox.nativeElement.disabled).toBeTrue();
});

it('should close modal when cancel is clicked', () => {
expect(component).toBeTruthy();
const cancelButton = fixture.debugElement.query(
By.css(
'.oppia-learner-dash-goals-button--modal.oppia-learner-dash-button--inverse'
)
);
cancelButton.triggerEventHandler('click', null);

expect(matDialogSpy.close).toHaveBeenCalled();
});

it('should add and return new goals and close modal when save is clicked', () => {
expect(component).toBeTruthy();
expect(component.checkedTopics).toEqual(new Set());

const checkboxes = fixture.debugElement.queryAll(By.css('mat-checkbox'));
spyOn(component, 'onChange').and.callThrough();
checkboxes[1].triggerEventHandler('change', {target: {checked: true}});
checkboxes[2].triggerEventHandler('change', {target: {checked: true}});
checkboxes[3].triggerEventHandler('change', {target: {checked: true}});
fixture.detectChanges();

expect(component.onChange).toHaveBeenCalledTimes(3);

expect(component.onChange).toHaveBeenCalledWith('1');
expect(component.onChange).toHaveBeenCalledWith('2');
expect(component.onChange).toHaveBeenCalledWith('3');

const saveButton = fixture.debugElement.query(
By.css(
'.oppia-learner-dash-goals-button--modal.oppia-learner-dash-button--default'
)
);
saveButton.triggerEventHandler('click', null);

expect(matDialogSpy.close).toHaveBeenCalledWith(jasmine.any(Set));
const actualSet = matDialogSpy.close.calls.mostRecent()
.args[0] as Set<string>;
expect(actualSet).toEqual(new Set(['1', '2', '3']));
});

it('should return previously selected goals if there is no change when save is clicked', () => {
component.checkedTopics = new Set(['0', '2']);
fixture.detectChanges();

const saveButton = fixture.debugElement.query(
By.css(
'.oppia-learner-dash-goals-button--modal.oppia-learner-dash-button--default'
)
);
saveButton.triggerEventHandler('click', null);

const actualSet = matDialogSpy.close.calls.mostRecent()
.args[0] as Set<string>;
expect(actualSet).toEqual(new Set(['0', '2']));
});

it('should remove a goal id and return goals without it when save is clicked', () => {
component.checkedTopics = new Set(['0', '2']);
fixture.detectChanges();

const checkboxes = fixture.debugElement.queryAll(By.css('mat-checkbox'));
spyOn(component, 'onChange').and.callThrough();
checkboxes[2].triggerEventHandler('change', {target: {checked: false}});
fixture.detectChanges();

const saveButton = fixture.debugElement.query(
By.css(
'.oppia-learner-dash-goals-button--modal.oppia-learner-dash-button--default'
)
);
saveButton.triggerEventHandler('click', null);

const actualSet = matDialogSpy.close.calls.mostRecent()
.args[0] as Set<string>;
expect(actualSet).toEqual(new Set(['0']));
});
});
Loading

0 comments on commit b704e4b

Please sign in to comment.