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
Expand Up @@ -28,6 +28,7 @@ import {
actionGetExperiments,
actionSetGraphRange,
actionSetSearchKey,
actionSetSearchParams,
actionSetSearchString,
actionSetSortingType,
actionSetSortKey,
Expand Down Expand Up @@ -419,6 +420,30 @@ describe('ExperimentService', () => {
});
});

describe('#setSearchParams', () => {
it('should set localStorage items and dispatch actionSetSearchParams with the given inputs', () => {
const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT;
const searchString = 'lesson-stream (question-hint)';

service.setSearchParams(searchKey, searchString);

expect(mockLocalStorageService.setItem).toHaveBeenCalledWith(
ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_KEY,
searchKey
);
expect(mockLocalStorageService.setItem).toHaveBeenCalledWith(
ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_STRING,
searchString
);
expect(mockStore.dispatch).toHaveBeenCalledWith(
actionSetSearchParams({
searchKey,
searchString,
})
);
});
});

describe('#setSortKey', () => {
it('should set localStorage item and dispatch actionSetSortKey with the given input', () => {
const sortKey = EXPERIMENT_SORT_KEY.UPDATED_AT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ export class ExperimentService {
this.store$.dispatch(experimentAction.actionSetSearchString({ searchString }));
}

setSearchParams(searchKey: EXPERIMENT_SEARCH_KEY, searchString: string) {
this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_KEY, searchKey);
this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_STRING, searchString);
this.store$.dispatch(experimentAction.actionSetSearchParams({ searchKey, searchString }));
}

setSortKey(sortKey: EXPERIMENT_SORT_KEY) {
this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SORT_KEY, sortKey);
this.store$.dispatch(experimentAction.actionSetSortKey({ sortKey }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export const actionSetSearchKey = createAction(

export const actionSetSearchString = createAction('[Experiment] Set Search String', props<{ searchString: string }>());

export const actionSetSearchParams = createAction(
'[Experiment] Set Search Params',
props<{ searchKey: EXPERIMENT_SEARCH_KEY; searchString: string }>()
);

export const actionSetSortKey = createAction(
'[Experiment] Set Sort key value',
props<{ sortKey: EXPERIMENT_SORT_KEY }>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
actionRemoveExperimentStat,
actionSetSearchString,
actionSetSearchKey,
actionSetSearchParams,
actionFetchContextMetaData,
actionFetchContextMetaDataSuccess,
actionFetchContextMetaDataFailure,
Expand Down Expand Up @@ -1086,6 +1087,49 @@ describe('ExperimentEffects', () => {
}));
});

describe('#fetchExperimentOnSearchParamsChange$', () => {
it('should dispatch actionGetExperiments once when search params change on the experiments root page', fakeAsync(() => {
const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT;
const searchString = 'lesson-stream (question-hint)';
router.url = '/home';
store$.dispatch = jest.fn();

service.fetchExperimentOnSearchParamsChange$.subscribe();

actions$.next(
actionSetSearchParams({
searchKey,
searchString,
})
);

tick(0);

expect(store$.dispatch).toHaveBeenCalledTimes(1);
expect(store$.dispatch).toHaveBeenCalledWith(actionGetExperiments({ fromStarting: true }));
}));

it('should not dispatch actionGetExperiments when search params change outside the experiments root page', fakeAsync(() => {
const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT;
const searchString = 'lesson-stream (question-hint)';
router.url = '/home/detail/experiment-id';
store$.dispatch = jest.fn();

service.fetchExperimentOnSearchParamsChange$.subscribe();

actions$.next(
actionSetSearchParams({
searchKey,
searchString,
})
);

tick(0);

expect(store$.dispatch).not.toHaveBeenCalled();
}));
});

describe('#fetchContextMetaData$', () => {
it('should not do anything if contextMetaData object already exists', fakeAsync(() => {
let neverEmitted = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,20 @@ export class ExperimentEffects {
{ dispatch: false }
);

fetchExperimentOnSearchParamsChange$ = createEffect(
() =>
this.actions$.pipe(
ofType(experimentAction.actionSetSearchParams),
tap(() => {
const pathname = (this.router.url || '').split('?')[0].split('#')[0];
if (pathname === '/home') {
this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true }));
}
})
Comment thread
zackcl marked this conversation as resolved.
),
{ dispatch: false }
);
Comment thread
Copilot marked this conversation as resolved.

fetchContextMetaData$ = createEffect(() =>
this.actions$.pipe(
ofType(experimentAction.actionFetchContextMetaData),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
actionSetIsGraphLoading,
actionSetIsLoadingExperiment,
actionSetSearchKey,
actionSetSearchParams,
actionSetSearchString,
actionSetSkipExperiment,
actionSetSortingType,
Expand Down Expand Up @@ -558,6 +559,23 @@ describe('ExperimentsReducer', () => {
expect(newState.searchString).toEqual('test');
});

it('action "actionSetSearchParams" should set search key and search string', () => {
const previousState = { ...initialState };
previousState.searchKey = EXPERIMENT_SEARCH_KEY.ALL;
previousState.searchString = 'previous';

const testAction: Action = actionSetSearchParams({
searchKey: EXPERIMENT_SEARCH_KEY.DECISION_POINT,
searchString: 'lesson-stream (question-hint)',
});

const newState = experimentsReducer(previousState, testAction);

expect(newState).not.toBe(previousState);
expect(newState.searchKey).toEqual(EXPERIMENT_SEARCH_KEY.DECISION_POINT);
expect(newState.searchString).toEqual('lesson-stream (question-hint)');
});

it('action "actionSetSortKey" should set sort key', () => {
const previousState = { ...initialState };
previousState.sortKey = EXPERIMENT_SORT_KEY.NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ const reducer = createReducer(
})),
on(experimentsAction.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })),
on(experimentsAction.actionSetSearchString, (state, { searchString }) => ({ ...state, searchString })),
on(experimentsAction.actionSetSearchParams, (state, { searchKey, searchString }) => ({
...state,
searchKey,
searchString,
})),
on(experimentsAction.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })),
on(experimentsAction.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })),
on(experimentsAction.actionSetSkipExperiment, (state, { skipExperiment }) => ({ ...state, skipExperiment })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
[actionsTooltip]="restrictionTooltip"
[experimentState]="vm.experiment.state"
(rowAction)="onRowAction($event, vm.experiment.id, appContext)"
(decisionPointClick)="onDecisionPointClick($event)"
></app-experiment-decision-points-table>
}
</app-common-section-card>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Router } from '@angular/router';
import { EXPERIMENT_SEARCH_KEY } from 'upgrade_types';
import type { ExperimentDecisionPoint } from '../../../../../../../core/experiments/store/experiments.model';
import type { ExperimentService } from '../../../../../../../core/experiments/experiments.service';
import type { AuthService } from '../../../../../../../core/auth/auth.service';
import type { DialogService } from '../../../../../../../shared/services/common-dialog.service';
import type { DecisionPointHelperService } from '../../../../../../../core/experiments/decision-point-helper.service';

jest.mock(
'@shared-component-lib',
() => ({
CommonSectionCardActionButtonsComponent: class CommonSectionCardActionButtonsComponent {},
CommonSectionCardComponent: class CommonSectionCardComponent {},
CommonSectionCardTitleHeaderComponent: class CommonSectionCardTitleHeaderComponent {},
}),
{ virtual: true }
);

jest.mock('../../../../../../../shared/services/common-dialog.service', () => ({
DialogService: class DialogService {},
}));

import { ExperimentDecisionPointsSectionCardComponent } from './experiment-decision-points-section-card.component';

describe('ExperimentDecisionPointsSectionCardComponent', () => {
let component: ExperimentDecisionPointsSectionCardComponent;
let experimentService: jest.Mocked<Pick<ExperimentService, 'setSearchParams'>>;
let router: jest.Mocked<Pick<Router, 'navigate'>>;

const decisionPoint = {
id: 'decision-point-1',
site: 'lesson-stream',
target: 'question-hint',
description: '',
order: 1,
createdAt: '',
updatedAt: '',
versionNumber: 1,
excludeIfReached: false,
} as ExperimentDecisionPoint;

beforeEach(() => {
experimentService = {
setSearchParams: jest.fn(),
};
router = {
navigate: jest.fn(),
};

component = new ExperimentDecisionPointsSectionCardComponent(
experimentService as unknown as ExperimentService,
{} as AuthService,
{} as DialogService,
{} as DecisionPointHelperService,
router as unknown as Router
);
});

it('should filter experiments by clicked decision point and navigate to the experiments root page', () => {
component.onDecisionPointClick(decisionPoint);

expect(experimentService.setSearchParams).toHaveBeenCalledWith(
EXPERIMENT_SEARCH_KEY.DECISION_POINT,
'lesson-stream (question-hint)'
);
expect(router.navigate).toHaveBeenCalledWith(['/home']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
import { UserPermission } from '../../../../../../../core/auth/store/auth.models';
import { AuthService } from '../../../../../../../core/auth/auth.service';
import { DialogService } from '../../../../../../../shared/services/common-dialog.service';
import { Router } from '@angular/router';
import { EXPERIMENT_SEARCH_KEY } from 'upgrade_types';
import { formatDecisionPointDisplay } from '../../../../experiment-decision-point.utils';

@Component({
selector: 'app-experiment-decision-points-section-card',
Expand All @@ -46,7 +49,8 @@ export class ExperimentDecisionPointsSectionCardComponent implements OnInit {
readonly experimentService: ExperimentService,
private readonly authService: AuthService,
private readonly dialogService: DialogService,
private readonly decisionPointHelperService: DecisionPointHelperService
private readonly decisionPointHelperService: DecisionPointHelperService,
private readonly router: Router
) {}

ngOnInit() {
Expand Down Expand Up @@ -89,6 +93,14 @@ export class ExperimentDecisionPointsSectionCardComponent implements OnInit {
this.dialogService.openEditDecisionPointModal(decisionPoint, experimentId, context);
}

onDecisionPointClick(decisionPoint: ExperimentDecisionPoint): void {
this.experimentService.setSearchParams(
EXPERIMENT_SEARCH_KEY.DECISION_POINT,
formatDecisionPointDisplay(decisionPoint)
);
this.router.navigate(['/home']);
}
Comment thread
zackcl marked this conversation as resolved.

onDeleteDecisionPoint(decisionPoint: ExperimentDecisionPoint): void {
const decisionPointDisplayName = `${decisionPoint.site}; ${decisionPoint.target}`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
{{ DECISION_POINT_TRANSLATION_KEYS.DECISION_POINT | translate }}
</th>
<td mat-cell *matCellDef="let decisionPoint" class="decision-point-column ft-14-400">
<span
<button
type="button"
[appTextOverflowTooltip]="getDecisionPoint(decisionPoint)"
matTooltipPosition="above"
class="line-clamp-1"
class="decision-point-link line-clamp-1"
(click)="onDecisionPointClick(decisionPoint)"
>
{{ getDecisionPoint(decisionPoint) }}
</span>
</button>
</td>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@
.decision-point-column {
width: 70%;
padding-left: 32px;

.decision-point-link {
border: 0;
background: transparent;
color: var(--black-2);
cursor: pointer;
font: inherit;
max-width: 100%;
padding: 0;
text-align: left;
text-decoration: underline;

&:focus-visible {
border-radius: 2px;
outline: 2px solid var(--blue);
outline-offset: 2px;
}
}
}

.exclude-if-reached-column {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { ExperimentDecisionPointsTableComponent } from './experiment-decision-points-table.component';
import type { ExperimentDecisionPoint } from '../../../../../../../../core/experiments/store/experiments.model';

describe('ExperimentDecisionPointsTableComponent', () => {
let component: ExperimentDecisionPointsTableComponent;
let fixture: ComponentFixture<ExperimentDecisionPointsTableComponent>;

const decisionPoint = {
id: 'decision-point-1',
site: 'lesson-stream',
target: 'question-hint',
description: '',
order: 1,
createdAt: '',
updatedAt: '',
versionNumber: 1,
excludeIfReached: false,
} as ExperimentDecisionPoint;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ExperimentDecisionPointsTableComponent, NoopAnimationsModule, TranslateModule.forRoot()],
}).compileComponents();

fixture = TestBed.createComponent(ExperimentDecisionPointsTableComponent);
component = fixture.componentInstance;
component.decisionPoints = [decisionPoint];
component.isLoading$ = of(false);
fixture.detectChanges();
});

it('should emit the clicked decision point when the decision point text is clicked', () => {
const emitSpy = jest.spyOn(component.decisionPointClick, 'emit');
const decisionPointButton: HTMLButtonElement = fixture.nativeElement.querySelector('.decision-point-link');

decisionPointButton.click();

expect(emitSpy).toHaveBeenCalledWith(decisionPoint);
});

it('should format decision points using the shared display format', () => {
expect(component.getDecisionPoint(decisionPoint)).toBe('lesson-stream (question-hint)');
});
});
Loading