core/templates/pages/contributor-dashboard-page/modal-templates/translation-suggestion-review-modal.component.spec.ts

Summary

Maintainability
F
1 mo
Test Coverage
// Copyright 2021 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 TranslationSuggestionReviewModalComponent.
 */

import {HttpClientTestingModule} from '@angular/common/http/testing';
import {
  ComponentFixture,
  fakeAsync,
  flush,
  TestBed,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {TranslationSuggestionReviewModalComponent} from './translation-suggestion-review-modal.component';
import {ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA} from '@angular/core';
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {AppConstants} from 'app.constants';
import {AlertsService} from 'services/alerts.service';
import {ContributionAndReviewService} from '../services/contribution-and-review.service';
import {LanguageUtilService} from 'domain/utilities/language-util.service';
import {SiteAnalyticsService} from 'services/site-analytics.service';
import {ThreadDataBackendApiService} from 'pages/exploration-editor-page/feedback-tab/services/thread-data-backend-api.service';
import {UserService} from 'services/user.service';
import {UserInfo} from 'domain/user/user-info.model';
import {OverlayModule} from '@angular/cdk/overlay';
import {
  MatSnackBar,
  MatSnackBarModule,
  MatSnackBarRef,
} from '@angular/material/snack-bar';
import {of, Subject} from 'rxjs';
// This throws "TS2307". We need to
// suppress this error because rte-output-display is not strictly typed yet.
// @ts-ignore
import {RteOutputDisplayComponent} from 'rich_text_components/rte-output-display.component';
import {UndoSnackbarComponent} from 'components/custom-snackbar/undo-snackbar.component';
import {PlatformFeatureService} from 'services/platform-feature.service';

class MockChangeDetectorRef {
  detectChanges(): void {}
}

class MockMatSnackBarRef {
  instance = {message: ''};
  afterDismissed = () => of({action: '', dismissedByAction: false});
  onAction = () => of(undefined);
  dismiss = () => of(undefined);
}

class MockPlatformFeatureService {
  status = {
    CdAllowUndoingTranslationReview: {
      isEnabled: false,
    },
  };
}

describe('Translation Suggestion Review Modal Component', function () {
  let fixture: ComponentFixture<TranslationSuggestionReviewModalComponent>;
  let component: TranslationSuggestionReviewModalComponent;
  let alertsService: AlertsService;
  let contributionAndReviewService: ContributionAndReviewService;
  let languageUtilService: LanguageUtilService;
  let siteAnalyticsService: SiteAnalyticsService;
  let threadDataBackendApiService: ThreadDataBackendApiService;
  let userService: UserService;
  let activeModal: NgbActiveModal;
  let changeDetectorRef: MockChangeDetectorRef = new MockChangeDetectorRef();
  let snackBarSpy: jasmine.Spy;
  let snackBar: MatSnackBar;
  let mockPlatformFeatureService = new MockPlatformFeatureService();

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule, OverlayModule, MatSnackBarModule],
      declarations: [
        TranslationSuggestionReviewModalComponent,
        UndoSnackbarComponent,
      ],
      providers: [
        NgbActiveModal,
        AlertsService,
        ContributionAndReviewService,
        LanguageUtilService,
        SiteAnalyticsService,
        ThreadDataBackendApiService,
        UserService,
        {
          provide: ChangeDetectorRef,
          useValue: changeDetectorRef,
        },
        MatSnackBar,
        {
          provide: PlatformFeatureService,
          useValue: mockPlatformFeatureService,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(
      TranslationSuggestionReviewModalComponent
    );
    snackBar = TestBed.inject(MatSnackBar);
    component = fixture.componentInstance;
    activeModal = TestBed.inject(NgbActiveModal);
    alertsService = TestBed.inject(AlertsService);
    siteAnalyticsService = TestBed.inject(SiteAnalyticsService);
    threadDataBackendApiService = TestBed.inject(ThreadDataBackendApiService);
    userService = TestBed.inject(UserService);
    contributionAndReviewService = TestBed.inject(ContributionAndReviewService);
    languageUtilService = TestBed.inject(LanguageUtilService);
    spyOn(
      siteAnalyticsService,
      'registerContributorDashboardViewSuggestionForReview'
    );
    spyOn(languageUtilService, 'getAudioLanguageDescription').and.returnValue(
      'audio_language_description'
    );

    snackBarSpy = spyOn(snackBar, 'openFromComponent').and.returnValue(
      new MockMatSnackBarRef() as unknown as MatSnackBarRef<unknown>
    );

    component.contentContainer = new ElementRef({offsetHeight: 150});
    component.translationContainer = new ElementRef({offsetHeight: 150});
    component.contentPanel = new RteOutputDisplayComponent(
      null,
      null,
      new ElementRef({offsetHeight: 200}),
      null
    );
    component.translationPanel = new RteOutputDisplayComponent(
      null,
      null,
      new ElementRef({offsetHeight: 200}),
      null
    );
  });

  describe('when initializing the modal ', () => {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_1',
      target_id: '1',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p><p>&nbsp;</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content</p><p>&nbsp;</p>',
    };
    const suggestion2 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_2',
      target_id: '2',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content CHANGED</p>',
    };
    const suggestion3 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_3',
      target_id: '3',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content CHANGED</p>',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };
    const contribution3 = {
      suggestion: suggestion3,
      details: {
        topic_name: 'topic_3',
        story_title: 'story_3',
        chapter_title: 'chapter_3',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
      suggestion_3: contribution3,
    };
    const editedContent = {
      html: '<p>In Hindi</p>',
    };

    beforeEach(() => {
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.editedContent = editedContent;
    });

    it(
      'should be able to navigate to both previous suggestion and ' +
        'next suggestion if initial suggestion is in middle of list',
      () => {
        component.initialSuggestionId = 'suggestion_2';
        component.ngOnInit();

        expect(component.activeSuggestionId).toBe('suggestion_2');
        expect(component.skippedContributionIds).toEqual(['suggestion_1']);
        expect(component.remainingContributionIds).toEqual(['suggestion_3']);
      }
    );

    it(
      'should be able to navigate to only previous suggestion ' +
        'if initial suggestion is the last suggestion of the list',
      () => {
        component.initialSuggestionId = 'suggestion_3';
        component.ngOnInit();

        expect(component.activeSuggestionId).toBe('suggestion_3');
        expect(component.skippedContributionIds.sort()).toEqual([
          'suggestion_1',
          'suggestion_2',
        ]);
        expect(component.remainingContributionIds).toEqual([]);
      }
    );

    it(
      'should be able to navigate to only next suggestion ' +
        'if initial suggestion is in first suggestion of the list',
      () => {
        component.initialSuggestionId = 'suggestion_1';
        component.ngOnInit();

        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.skippedContributionIds).toEqual([]);
        expect(component.remainingContributionIds.sort()).toEqual([
          'suggestion_2',
          'suggestion_3',
        ]);
      }
    );
  });

  describe('when reviewing suggestion when flag CdAllowUndoingTranslationReview is enabled', function () {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';
    const suggestion1 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_1',
      target_id: '1',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p><p>&nbsp;</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content</p><p>&nbsp;</p>',
    };

    const suggestion2 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_2',
      target_id: '2',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content CHANGED</p>',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
    };

    const editedContent = {
      html: '<p>In Hindi</p>',
    };

    const userInfo = new UserInfo(
      ['USER_ROLE'],
      true,
      false,
      false,
      false,
      true,
      'en',
      'username1',
      'tester@example.com',
      true
    );

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.editedContent = editedContent;
      mockPlatformFeatureService.status.CdAllowUndoingTranslationReview.isEnabled =
        true;
    });

    it('should call user service at initialization.', function () {
      const userInfoSpy = spyOn(
        userService,
        'getUserInfoAsync'
      ).and.returnValue(Promise.resolve(userInfo));

      const contributionRightsDataSpy = spyOn(
        userService,
        'getUserContributionRightsDataAsync'
      ).and.returnValue(
        Promise.resolve({
          can_review_translation_for_language_codes: ['ar'],
          can_review_voiceover_for_language_codes: [],
          can_review_questions: false,
          can_suggest_questions: false,
        })
      );
      component.ngOnInit();
      expect(userInfoSpy).toHaveBeenCalled();
      expect(contributionRightsDataSpy).toHaveBeenCalled();
    });

    it('should throw error if username is invalid', fakeAsync(() => {
      const defaultUserInfo = new UserInfo(
        ['GUEST'],
        false,
        false,
        false,
        false,
        false,
        null,
        null,
        null,
        false
      );
      spyOn(userService, 'getUserInfoAsync').and.returnValue(
        Promise.resolve(defaultUserInfo)
      );

      expect(() => {
        component.ngOnInit();
        tick();
      }).toThrowError();
      flush();
    }));

    it('should initialize $scope properties after controller is initialized', function () {
      component.ngOnInit();
      expect(component.subheading).toBe(subheading);
      expect(component.reviewable).toBe(reviewable);
      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewMessage).toBe('');
    });

    it(
      'should register Contributor Dashboard view suggestion for review ' +
        'event after controller is initialized',
      function () {
        component.ngOnInit();
        expect(
          siteAnalyticsService.registerContributorDashboardViewSuggestionForReview
        ).toHaveBeenCalledWith('Translation');
      }
    );

    it('should notify user on failed suggestion update', function () {
      component.ngOnInit();
      const error = new Error('Error');
      expect(component.errorFound).toBeFalse();
      expect(component.errorMessage).toBe('');

      component.showTranslationSuggestionUpdateError(error);

      expect(component.errorFound).toBeTrue();
      expect(component.errorMessage).toBe('Invalid Suggestion: Error');
    });

    it(
      'should accept suggestion in suggestion modal service when clicking' +
        ' on accept and review next suggestion button',
      function () {
        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        // Suggestion 1's exploration_content_html matches its content_html.
        expect(component.hasExplorationContentChanged()).toBe(false);

        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');
        spyOn(component, 'showSnackbar');
        spyOn(component, 'startCommitTimeout');

        component.reviewMessage = 'Review message example';
        component.translationUpdated = true;
        component.acceptAndReviewNext();

        expect(component.activeSuggestionId).toBe('suggestion_2');
        expect(component.activeSuggestion).toEqual(suggestion2);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        component.commitQueuedSuggestion();

        // Suggestion 2's exploration_content_html does not match its
        // content_html.
        expect(component.hasExplorationContentChanged()).toBe(true);
        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'accept',
          'Review message example: ' +
            '(Note: This suggestion was submitted with reviewer edits.)',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();

        component.reviewMessage = 'Review message example 2';
        component.translationUpdated = false;
        component.acceptAndReviewNext();

        component.commitQueuedSuggestion();

        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '2',
          'suggestion_2',
          'accept',
          'Review message example 2',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();
        expect(activeModal.close).toHaveBeenCalledWith([
          'suggestion_1',
          'suggestion_2',
        ]);
      }
    );

    it(
      'should set suggestion review message to auto-generated note when ' +
        'suggestion is accepted with edits and no user-supplied review message',
      fakeAsync(() => {
        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(component, 'resolveSuggestionAndUpdateModal');
        spyOn(component, 'startCommitTimeout');
        spyOn(component, 'showSnackbar');
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(alertsService, 'addSuccessMessage');

        component.translationUpdated = true;
        component.acceptAndReviewNext();

        tick();

        expect(component.hasQueuedSuggestion).toBeTrue();
        expect(component.queuedSuggestion).toEqual({
          target_id: '1',
          suggestion_id: 'suggestion_1',
          action_status: 'accept',
          commit_message: 'hint section of "StateName" card',
          reviewer_message:
            '(Note: This suggestion was submitted with ' + 'reviewer edits.)',
        });
      })
    );

    it(
      'should accept the queued suggestion when the timer has expired' +
        'review button',
      () => {
        component.ngOnInit();

        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');
        spyOn(component, 'resolveSuggestionAndUpdateModal').and.stub();
        spyOn(component, 'startCommitTimeout').and.stub();
        spyOn(component, 'showSnackbar').and.stub();

        component.acceptAndReviewNext();

        expect(component.hasQueuedSuggestion).toBeTrue();
        expect(component.queuedSuggestion).toEqual({
          target_id: '1',
          suggestion_id: 'suggestion_1',
          action_status: AppConstants.ACTION_ACCEPT_SUGGESTION,
          reviewer_message: '',
          commit_message: 'hint section of "StateName" card',
        });

        component.commitQueuedSuggestion();
      }
    );

    it('should reject the queued suggestion when the timer has expired', () => {
      component.ngOnInit();

      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewable).toBe(reviewable);
      expect(component.reviewMessage).toBe('');
      expect(component.isUndoFeatureEnabled).toBeTrue();

      spyOn(
        siteAnalyticsService,
        'registerContributorDashboardAcceptSuggestion'
      );
      spyOn(
        contributionAndReviewService,
        'reviewExplorationSuggestion'
      ).and.callFake(
        (
          targetId,
          suggestionId,
          action,
          reviewMessage,
          commitMessage,
          successCallback,
          errorCallback
        ) => {
          return Promise.resolve(successCallback(suggestionId));
        }
      );
      spyOn(activeModal, 'close');
      spyOn(alertsService, 'addSuccessMessage');
      spyOn(component, 'resolveSuggestionAndUpdateModal').and.stub();
      spyOn(component, 'startCommitTimeout').and.stub();
      spyOn(component, 'showSnackbar').and.stub();

      component.rejectAndReviewNext('rejected');

      expect(component.hasQueuedSuggestion).toBeTrue();
      expect(component.queuedSuggestion).toEqual({
        target_id: '1',
        suggestion_id: 'suggestion_1',
        action_status: AppConstants.ACTION_REJECT_SUGGESTION,
        reviewer_message: 'rejected',
      });

      component.commitQueuedSuggestion();
    });

    it('should undo the queued suggestion when clicked on undo button', () => {
      component.ngOnInit();
      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewable).toBe(reviewable);
      expect(component.reviewMessage).toBe('');
      component.resolvedSuggestionIds = ['suggestion_1'];

      component.hasQueuedSuggestion = true;
      component.queuedSuggestion = {
        target_id: '1',
        suggestion_id: 'suggestion_1',
        action_status: 'accept',
        commit_message: 'hint section of "StateName" card',
        reviewer_message:
          '(Note: This suggestion was submitted with' + 'reviewer edits.)',
      };
      component.removedSuggestion = contribution1;
      component.undoReviewAction();

      expect(component.hasQueuedSuggestion).toBeFalse();
      expect(component.queuedSuggestion).toBe(undefined);
    });

    it(
      'should allow the reviewer to fix the suggestion if the backend pre' +
        ' accept/reject validation failed',
      function () {
        const responseMessage = 'Pre accept validation failed.';

        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        spyOn(component, 'revertSuggestionResolution');
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardRejectSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.reject(errorCallback(responseMessage));
          }
        );
        spyOn(alertsService, 'addWarning');

        component.reviewMessage = 'Review message example';
        component.queuedSuggestion = {
          target_id: '1',
          suggestion_id: 'suggestion_1',
          action_status: 'accept',
          commit_message: 'hint section of "StateName" card',
          reviewer_message:
            '(Note: This suggestion was submitted with' + 'reviewer edits.)',
        };
        component.hasQueuedSuggestion = true;
        component.commitQueuedSuggestion();

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

    it('should show the pop up bar when suggestion is queued', () => {
      spyOn(component, 'commitQueuedSuggestion').and.callThrough();
      component.hasQueuedSuggestion = true;
      component.showSnackbar();

      expect(snackBarSpy.calls.mostRecent().returnValue.instance.message).toBe(
        'Suggestion queued'
      );
    });

    it(
      'should commit the queued suggestion when' + ' the snackbar is dismissed',
      () => {
        const commitQueuedSuggestionSpy = spyOn(
          component,
          'commitQueuedSuggestion'
        ).and.callThrough();

        let afterDismissedObservable = new Subject<void>();
        let snackBarRefMock = {
          instance: {message: ''},
          afterDismissed: () => afterDismissedObservable.asObservable(),
          onAction: () => of(null),
        };

        snackBarSpy.and.returnValue(snackBarRefMock);

        component.showSnackbar();
        component.hasQueuedSuggestion = true;

        afterDismissedObservable.next();
        afterDismissedObservable.complete();

        expect(commitQueuedSuggestionSpy).toHaveBeenCalled();
      }
    );

    it(
      'should start commit timeout when clicking on accept and review' +
        'next suggestion button',
      () => {
        spyOn(window, 'setTimeout');
        spyOn(component, 'commitQueuedSuggestion');
        component.commitTimeout = undefined;

        component.startCommitTimeout();

        expect(window.setTimeout).toHaveBeenCalled();
        const timeoutCallback = (
          window.setTimeout as unknown as jasmine.Spy
        ).calls.mostRecent().args[0];
        expect(typeof timeoutCallback).toBe('function');

        timeoutCallback();

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

    it('should remove suggestion_id from resolvedSuggestionIds if it exists', () => {
      component.ngOnInit();
      component.resolvedSuggestionIds = ['suggestion_1', 'suggestion_2'];
      component.queuedSuggestion = {
        suggestion_id: 'suggestion_1',
        action_status: 'accept',
        target_id: '1',
        reviewer_message: '',
      };
      component.removedSuggestion = contribution1;

      component.revertSuggestionResolution();

      expect(component.resolvedSuggestionIds).toEqual(['suggestion_2']);
    });

    it(
      'should cancel suggestion in suggestion modal service when clicking ' +
        'on cancel suggestion button',
      function () {
        spyOn(activeModal, 'close');
        component.cancel();
        expect(activeModal.close).toHaveBeenCalledWith([]);
      }
    );

    it('should open the translation editor when the edit button is clicked', function () {
      component.editSuggestion();
      expect(component.startedEditing).toBe(true);
    });

    it('should close the translation editor when the cancel button is clicked', function () {
      component.cancelEdit();
      expect(component.startedEditing).toBe(false);
    });

    it('should expand the content area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      // The content area is contracted by default.
      expect(component.isContentExpanded).toBeFalse();
      // The content area should expand when the users clicks
      // on the 'View More' button.
      component.toggleExpansionState(0);

      expect(component.isContentExpanded).toBeTrue();
    });

    it('should contract the content area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      component.isContentExpanded = true;
      // The content area should contract when the users clicks
      // on the 'View Less' button.
      component.toggleExpansionState(0);

      expect(component.isContentExpanded).toBeFalse();
    });

    it('should expand the translation area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      // The translation area is contracted by default.
      expect(component.isTranslationExpanded).toBeFalse();
      // The translation area should expand when the users clicks
      // on the 'View More' button.
      component.toggleExpansionState(1);

      expect(component.isTranslationExpanded).toBeTrue();
    });

    it('should contract the translation area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      component.isTranslationExpanded = true;
      // The translation area should contract when the users clicks
      // on the 'View Less' button.
      component.toggleExpansionState(1);

      expect(component.isTranslationExpanded).toBeFalse();
    });

    it('should update translation when the update button is clicked', function () {
      component.ngOnInit();
      spyOn(
        contributionAndReviewService,
        'updateTranslationSuggestionAsync'
      ).and.callFake(
        (suggestionId, translationHtml, successCallback, errorCallback) => {
          return Promise.resolve(successCallback());
        }
      );

      component.updateSuggestion();

      expect(
        contributionAndReviewService.updateTranslationSuggestionAsync
      ).toHaveBeenCalledWith(
        'suggestion_1',
        component.editedContent.html,
        jasmine.any(Function),
        jasmine.any(Function)
      );
    });

    describe('isHtmlContentEqual', function () {
      it('should return true regardless of &nbsp; differences', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p><p>&nbsp;&nbsp;</p>',
            '<p>content</p><p> </p>'
          )
        ).toBe(true);
      });

      it('should return true regardless of new line differences', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p>\r\n\n<p>content2</p>',
            '<p>content</p><p>content2</p>'
          )
        ).toBe(true);
      });

      it('should return false if html content differ', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p>',
            '<p>content CHANGED</p>'
          )
        ).toBe(false);
      });

      it('should return false if array contents differ', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            ['<p>content1</p>', '<p>content2 CHANGED</p>']
          )
        ).toBe(false);
      });

      it('should return true if array contents are equal', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            ['<p>content1</p>', '<p>content2</p>']
          )
        ).toBe(true);
      });

      it('should return false if type is different', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            '<p>content2</p>'
          )
        ).toBe(false);
      });
    });
  });

  describe('when reviewing suggestions with deleted opportunites when flag CdAllowUndoingTranslationReview is enabled', function () {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: ['Translation1', 'Translation2'],
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: ['Translation1', 'Translation2 CHANGED'],
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: 'Translation',
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };

    const deletedContribution = {
      suggestion: suggestion2,
      details: null,
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_deleted: deletedContribution,
    };

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      mockPlatformFeatureService.status.CdAllowUndoingTranslationReview.isEnabled =
        true;
      component.ngOnInit();
    });

    it(
      'should reject suggestion in suggestion modal service when clicking ' +
        'on reject and review next suggestion button',
      function () {
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardRejectSuggestion'
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');

        spyOn(component, 'startCommitTimeout');
        spyOn(component, 'showSnackbar');
        spyOn(component, 'resolveSuggestionAndUpdateModal');

        component.reviewMessage = 'Review message example';
        component.rejectAndReviewNext(component.reviewMessage);
        component.commitQueuedSuggestion();

        expect(
          siteAnalyticsService.registerContributorDashboardRejectSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'reject',
          'Review message example',
          null,
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalledWith(
          'Suggestion rejected.'
        );
      }
    );
  });

  describe('when reviewing suggestion when flag CdAllowUndoingTranslationReview is disabled', function () {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';
    const suggestion1 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_1',
      target_id: '1',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p><p>&nbsp;</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content</p><p>&nbsp;</p>',
    };

    const suggestion2 = {
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      suggestion_id: 'suggestion_2',
      target_id: '2',
      target_type: 'target_type',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content CHANGED</p>',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
    };

    const editedContent = {
      html: '<p>In Hindi</p>',
    };

    const userInfo = new UserInfo(
      ['USER_ROLE'],
      true,
      false,
      false,
      false,
      true,
      'en',
      'username1',
      'tester@example.com',
      true
    );

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.editedContent = editedContent;
      mockPlatformFeatureService.status.CdAllowUndoingTranslationReview.isEnabled =
        false;
    });

    it('should call user service at initialization.', function () {
      const userInfoSpy = spyOn(
        userService,
        'getUserInfoAsync'
      ).and.returnValue(Promise.resolve(userInfo));

      const contributionRightsDataSpy = spyOn(
        userService,
        'getUserContributionRightsDataAsync'
      ).and.returnValue(
        Promise.resolve({
          can_review_translation_for_language_codes: ['ar'],
          can_review_voiceover_for_language_codes: [],
          can_review_questions: false,
          can_suggest_questions: false,
        })
      );
      component.ngOnInit();
      expect(userInfoSpy).toHaveBeenCalled();
      expect(contributionRightsDataSpy).toHaveBeenCalled();
    });

    it('should throw error if username is invalid', fakeAsync(() => {
      const defaultUserInfo = new UserInfo(
        ['GUEST'],
        false,
        false,
        false,
        false,
        false,
        null,
        null,
        null,
        false
      );
      spyOn(userService, 'getUserInfoAsync').and.returnValue(
        Promise.resolve(defaultUserInfo)
      );

      expect(() => {
        component.ngOnInit();
        tick();
      }).toThrowError();
      flush();
    }));

    it('should initialize $scope properties after controller is initialized', function () {
      component.ngOnInit();
      expect(component.subheading).toBe(subheading);
      expect(component.reviewable).toBe(reviewable);
      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewMessage).toBe('');
    });

    it(
      'should register Contributor Dashboard view suggestion for review ' +
        'event after controller is initialized',
      function () {
        component.ngOnInit();
        expect(
          siteAnalyticsService.registerContributorDashboardViewSuggestionForReview
        ).toHaveBeenCalledWith('Translation');
      }
    );

    it('should notify user on failed suggestion update', function () {
      component.ngOnInit();
      const error = new Error('Error');
      expect(component.errorFound).toBeFalse();
      expect(component.errorMessage).toBe('');

      component.showTranslationSuggestionUpdateError(error);

      expect(component.errorFound).toBeTrue();
      expect(component.errorMessage).toBe('Invalid Suggestion: Error');
    });

    it(
      'should accept suggestion in suggestion modal service when clicking' +
        ' on accept and review next suggestion button',
      function () {
        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        // Suggestion 1's exploration_content_html matches its content_html.
        expect(component.hasExplorationContentChanged()).toBe(false);

        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');

        component.reviewMessage = 'Review message example';
        component.translationUpdated = true;
        component.acceptAndReviewNext();

        expect(component.activeSuggestionId).toBe('suggestion_2');
        expect(component.activeSuggestion).toEqual(suggestion2);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        // Suggestion 2's exploration_content_html does not match its
        // content_html.
        expect(component.hasExplorationContentChanged()).toBe(true);
        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'accept',
          'Review message example: ' +
            '(Note: This suggestion was submitted with reviewer edits.)',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();

        component.reviewMessage = 'Review message example 2';
        component.translationUpdated = false;
        component.acceptAndReviewNext();

        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '2',
          'suggestion_2',
          'accept',
          'Review message example 2',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();
        expect(activeModal.close).toHaveBeenCalledWith([
          'suggestion_1',
          'suggestion_2',
        ]);
      }
    );

    it(
      'should set suggestion review message to auto-generated note when ' +
        'suggestion is accepted with edits and no user-supplied review message',
      function () {
        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(alertsService, 'addSuccessMessage');

        component.translationUpdated = true;
        component.acceptAndReviewNext();

        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'accept',
          '(Note: This suggestion was submitted with reviewer edits.)',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();
      }
    );

    it(
      'should reject suggestion in suggestion modal service when clicking ' +
        'on reject and review next suggestion button',
      function () {
        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardRejectSuggestion'
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');

        component.reviewMessage = 'Review message example';
        component.translationUpdated = true;
        component.rejectAndReviewNext(component.reviewMessage);

        expect(component.activeSuggestionId).toBe('suggestion_2');
        expect(component.activeSuggestion).toEqual(suggestion2);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        expect(
          siteAnalyticsService.registerContributorDashboardRejectSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'reject',
          'Review message example',
          null,
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();

        component.reviewMessage = 'Review message example 2';
        component.translationUpdated = false;
        component.rejectAndReviewNext(component.reviewMessage);

        expect(
          siteAnalyticsService.registerContributorDashboardRejectSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(alertsService.addSuccessMessage).toHaveBeenCalled();
        expect(activeModal.close).toHaveBeenCalledWith([
          'suggestion_1',
          'suggestion_2',
        ]);
      }
    );

    it(
      'should allow the reviewer to fix the suggestion if the backend pre' +
        ' accept/reject validation failed',
      function () {
        const responseMessage = 'Pre accept validation failed.';

        component.ngOnInit();
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardAcceptSuggestion'
        );
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardRejectSuggestion'
        );
        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.reject(errorCallback(responseMessage));
          }
        );
        spyOn(alertsService, 'addWarning');

        component.reviewMessage = 'Review message example';
        component.acceptAndReviewNext();

        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('Review message example');
        expect(
          siteAnalyticsService.registerContributorDashboardAcceptSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'accept',
          'Review message example',
          'hint section of "StateName" card',
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addWarning).toHaveBeenCalledWith(
          jasmine.stringContaining(responseMessage)
        );

        component.reviewMessage = 'Edited review message example';
        component.rejectAndReviewNext(component.reviewMessage);

        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('Edited review message example');
        expect(
          siteAnalyticsService.registerContributorDashboardRejectSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'reject',
          'Edited review message example',
          null,
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addWarning).toHaveBeenCalledWith(
          jasmine.stringContaining(responseMessage)
        );
      }
    );

    it(
      'should cancel suggestion in suggestion modal service when clicking ' +
        'on cancel suggestion button',
      function () {
        spyOn(activeModal, 'close');
        component.cancel();
        expect(activeModal.close).toHaveBeenCalledWith([]);
      }
    );

    it('should open the translation editor when the edit button is clicked', function () {
      component.editSuggestion();
      expect(component.startedEditing).toBe(true);
    });

    it('should close the translation editor when the cancel button is clicked', function () {
      component.cancelEdit();
      expect(component.startedEditing).toBe(false);
    });

    it('should expand the content area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      // The content area is contracted by default.
      expect(component.isContentExpanded).toBeFalse();
      // The content area should expand when the users clicks
      // on the 'View More' button.
      component.toggleExpansionState(0);

      expect(component.isContentExpanded).toBeTrue();
    });

    it('should contract the content area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      component.isContentExpanded = true;
      // The content area should contract when the users clicks
      // on the 'View Less' button.
      component.toggleExpansionState(0);

      expect(component.isContentExpanded).toBeFalse();
    });

    it('should expand the translation area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      // The translation area is contracted by default.
      expect(component.isTranslationExpanded).toBeFalse();
      // The translation area should expand when the users clicks
      // on the 'View More' button.
      component.toggleExpansionState(1);

      expect(component.isTranslationExpanded).toBeTrue();
    });

    it('should contract the translation area', () => {
      spyOn(component, 'toggleExpansionState').and.callThrough();
      component.isTranslationExpanded = true;
      // The translation area should contract when the users clicks
      // on the 'View Less' button.
      component.toggleExpansionState(1);

      expect(component.isTranslationExpanded).toBeFalse();
    });

    it('should update translation when the update button is clicked', function () {
      component.ngOnInit();
      spyOn(
        contributionAndReviewService,
        'updateTranslationSuggestionAsync'
      ).and.callFake(
        (suggestionId, translationHtml, successCallback, errorCallback) => {
          return Promise.resolve(successCallback());
        }
      );

      component.updateSuggestion();

      expect(
        contributionAndReviewService.updateTranslationSuggestionAsync
      ).toHaveBeenCalledWith(
        'suggestion_1',
        component.editedContent.html,
        jasmine.any(Function),
        jasmine.any(Function)
      );
    });

    describe('isHtmlContentEqual', function () {
      it('should return true regardless of &nbsp; differences', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p><p>&nbsp;&nbsp;</p>',
            '<p>content</p><p> </p>'
          )
        ).toBe(true);
      });

      it('should return true regardless of new line differences', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p>\r\n\n<p>content2</p>',
            '<p>content</p><p>content2</p>'
          )
        ).toBe(true);
      });

      it('should return false if html content differ', function () {
        expect(
          component.isHtmlContentEqual(
            '<p>content</p>',
            '<p>content CHANGED</p>'
          )
        ).toBe(false);
      });

      it('should return false if array contents differ', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            ['<p>content1</p>', '<p>content2 CHANGED</p>']
          )
        ).toBe(false);
      });

      it('should return true if array contents are equal', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            ['<p>content1</p>', '<p>content2</p>']
          )
        ).toBe(true);
      });

      it('should return false if type is different', function () {
        expect(
          component.isHtmlContentEqual(
            ['<p>content1</p>', '<p>content2</p>'],
            '<p>content2</p>'
          )
        ).toBe(false);
      });
    });
  });

  describe('when reviewing suggestions with deleted opportunites when flag CdAllowUndoingTranslationReview is disabled', function () {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: ['Translation1', 'Translation2'],
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: ['Translation1', 'Translation2 CHANGED'],
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: 'Translation',
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };

    const deletedContribution = {
      suggestion: suggestion2,
      details: null,
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_deleted: deletedContribution,
    };

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      mockPlatformFeatureService.status.CdAllowUndoingTranslationReview.isEnabled =
        false;
      component.ngOnInit();
    });

    it(
      'should reject suggestion in suggestion modal service when clicking ' +
        'on reject and review next suggestion button',
      function () {
        expect(component.activeSuggestionId).toBe('suggestion_1');
        expect(component.activeSuggestion).toEqual(suggestion1);
        expect(component.reviewable).toBe(reviewable);
        expect(component.reviewMessage).toBe('');

        spyOn(
          contributionAndReviewService,
          'reviewExplorationSuggestion'
        ).and.callFake(
          (
            targetId,
            suggestionId,
            action,
            reviewMessage,
            commitMessage,
            successCallback,
            errorCallback
          ) => {
            return Promise.resolve(successCallback(suggestionId));
          }
        );
        spyOn(
          siteAnalyticsService,
          'registerContributorDashboardRejectSuggestion'
        );
        spyOn(activeModal, 'close');
        spyOn(alertsService, 'addSuccessMessage');

        component.reviewMessage = 'Review message example';
        component.rejectAndReviewNext(component.reviewMessage);

        expect(
          siteAnalyticsService.registerContributorDashboardRejectSuggestion
        ).toHaveBeenCalledWith('Translation');
        expect(
          contributionAndReviewService.reviewExplorationSuggestion
        ).toHaveBeenCalledWith(
          '1',
          'suggestion_1',
          'reject',
          'Review message example',
          null,
          jasmine.any(Function),
          jasmine.any(Function)
        );
        expect(alertsService.addSuccessMessage).toHaveBeenCalledWith(
          'Suggestion rejected.'
        );
        expect(activeModal.close).toHaveBeenCalledWith(['suggestion_1']);
      }
    );
  });

  describe('when viewing suggestion', function () {
    const reviewable = false;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: ['Translation1', 'Translation2'],
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: ['Translation1', 'Translation2 CHANGED'],
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: 'Translation',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      target_type: 'target_type',
    };
    const obsoleteSuggestion = {
      suggestion_id: 'suggestion_3',
      target_id: '3',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      // This suggestion is obsolete.
      exploration_content_html: null,
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };
    const contribution3 = {
      suggestion: obsoleteSuggestion,
      details: {
        topic_name: 'topic_3',
        story_title: 'story_3',
        chapter_title: 'chapter_3',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
      suggestion_3: contribution3,
    };

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
    });

    it('should initialize $scope properties after controller is initialized', fakeAsync(function () {
      const messages = [
        {
          author_username: '',
          created_on_msecs: 0,
          entity_type: '',
          entity_id: '',
          message_id: 0,
          text: '',
          updated_status: '',
          updated_subject: '',
        },
        {
          author_username: 'Reviewer',
          created_on_msecs: 0,
          entity_type: '',
          entity_id: '',
          message_id: 0,
          text: 'Review Message',
          updated_status: 'fixed',
          updated_subject: null,
        },
      ];

      const fetchMessagesAsyncSpy = spyOn(
        threadDataBackendApiService,
        'fetchMessagesAsync'
      ).and.returnValue(Promise.resolve({messages: messages}));

      component.ngOnInit();
      component.refreshActiveContributionState();
      tick();

      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewable).toBe(reviewable);
      expect(component.subheading).toBe('topic_1 / story_1 / chapter_1');
      // Suggestion 1's exploration_content_html does not match its
      // content_html.
      expect(component.hasExplorationContentChanged()).toBe(true);
      expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_1');
      expect(component.reviewMessage).toBe('Review Message');
      expect(component.reviewer).toBe('Reviewer');
    }));

    it('should correctly determine whether the panel data is overflowing', fakeAsync(() => {
      // Pre-check.
      // The default values for the overflow states are false.
      expect(component.isContentOverflowing).toBeFalse();
      expect(component.isTranslationOverflowing).toBeFalse();
      // Setup.
      component.contentPanel.elementRef.nativeElement.offsetHeight = 100;
      component.translationPanel.elementRef.nativeElement.offsetHeight = 200;
      component.contentContainer.nativeElement.offsetHeight = 150;
      component.translationContainer.nativeElement.offsetHeight = 150;
      // Action.
      component.computePanelOverflowState();
      tick(0);
      // Expectations.
      expect(component.isContentOverflowing).toBeFalse();
      expect(component.isTranslationOverflowing).toBeTrue();
      // Change panel height to simulate changing of the modal data.
      component.contentPanel.elementRef.nativeElement.offsetHeight = 300;
      // Action.
      component.computePanelOverflowState();
      tick(0);
      // Expectations.
      expect(component.isContentOverflowing).toBeTrue();
      expect(component.isTranslationOverflowing).toBeTrue();
    }));

    it('should determine panel height after view initialization', () => {
      spyOn(component, 'computePanelOverflowState').and.callFake(() => {});

      component.ngAfterViewInit();

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

    it('should set Obsolete review message for obsolete suggestions', fakeAsync(function () {
      const fetchMessagesAsyncSpy = spyOn(
        threadDataBackendApiService,
        'fetchMessagesAsync'
      ).and.returnValue(Promise.resolve({messages: []}));
      component.initialSuggestionId = 'suggestion_3';

      component.ngOnInit();
      component.refreshActiveContributionState();
      tick();

      expect(component.activeSuggestionId).toBe('suggestion_3');
      expect(component.activeSuggestion).toEqual(obsoleteSuggestion);
      expect(component.reviewable).toBe(reviewable);
      expect(component.subheading).toBe('topic_3 / story_3 / chapter_3');
      // Suggestion 3's exploration_content_html does not match its
      // content_html.
      expect(component.hasExplorationContentChanged()).toBe(true);
      expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_3');
      expect(component.reviewMessage).toBe(
        AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG
      );
    }));
  });

  describe('when viewing suggestion', function () {
    const reviewable = false;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: ['Translation1', 'Translation2'],
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: ['Translation1', 'Translation2 CHANGED'],
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: 'Translation',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      target_type: 'target_type',
    };
    const obsoleteSuggestion = {
      suggestion_id: 'suggestion_3',
      target_id: '3',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      // This suggestion is obsolete.
      exploration_content_html: null,
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };
    const contribution3 = {
      suggestion: obsoleteSuggestion,
      details: {
        topic_name: 'topic_3',
        story_title: 'story_3',
        chapter_title: 'chapter_3',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
      suggestion_3: contribution3,
    };

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
    });

    it('should initialize $scope properties after controller is initialized', fakeAsync(function () {
      const messages = [
        {
          author_username: '',
          created_on_msecs: 0,
          entity_type: '',
          entity_id: '',
          message_id: 0,
          text: '',
          updated_status: '',
          updated_subject: '',
        },
        {
          author_username: 'Reviewer',
          created_on_msecs: 0,
          entity_type: '',
          entity_id: '',
          message_id: 0,
          text: 'Review Message',
          updated_status: 'fixed',
          updated_subject: null,
        },
      ];

      const fetchMessagesAsyncSpy = spyOn(
        threadDataBackendApiService,
        'fetchMessagesAsync'
      ).and.returnValue(Promise.resolve({messages: messages}));

      component.ngOnInit();
      component.refreshActiveContributionState();
      tick();

      expect(component.activeSuggestionId).toBe('suggestion_1');
      expect(component.activeSuggestion).toEqual(suggestion1);
      expect(component.reviewable).toBe(reviewable);
      expect(component.subheading).toBe('topic_1 / story_1 / chapter_1');
      // Suggestion 1's exploration_content_html does not match its
      // content_html.
      expect(component.hasExplorationContentChanged()).toBe(true);
      expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_1');
      expect(component.reviewMessage).toBe('Review Message');
      expect(component.reviewer).toBe('Reviewer');
    }));

    it('should correctly determine whether the panel data is overflowing', fakeAsync(() => {
      // Pre-check.
      // The default values for the overflow states are false.
      expect(component.isContentOverflowing).toBeFalse();
      expect(component.isTranslationOverflowing).toBeFalse();
      // Setup.
      component.contentPanel.elementRef.nativeElement.offsetHeight = 100;
      component.translationPanel.elementRef.nativeElement.offsetHeight = 200;
      component.contentContainer.nativeElement.offsetHeight = 150;
      component.translationContainer.nativeElement.offsetHeight = 150;
      // Action.
      component.computePanelOverflowState();
      tick(0);
      // Expectations.
      expect(component.isContentOverflowing).toBeFalse();
      expect(component.isTranslationOverflowing).toBeTrue();
      // Change panel height to simulate changing of the modal data.
      component.contentPanel.elementRef.nativeElement.offsetHeight = 300;
      // Action.
      component.computePanelOverflowState();
      tick(0);
      // Expectations.
      expect(component.isContentOverflowing).toBeTrue();
      expect(component.isTranslationOverflowing).toBeTrue();
    }));

    it('should determine panel height after view initialization', () => {
      spyOn(component, 'computePanelOverflowState').and.callFake(() => {});

      component.ngAfterViewInit();

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

    it('should set Obsolete review message for obsolete suggestions', fakeAsync(function () {
      const fetchMessagesAsyncSpy = spyOn(
        threadDataBackendApiService,
        'fetchMessagesAsync'
      ).and.returnValue(Promise.resolve({messages: []}));
      component.initialSuggestionId = 'suggestion_3';

      component.ngOnInit();
      component.refreshActiveContributionState();
      tick();

      expect(component.activeSuggestionId).toBe('suggestion_3');
      expect(component.activeSuggestion).toEqual(obsoleteSuggestion);
      expect(component.reviewable).toBe(reviewable);
      expect(component.subheading).toBe('topic_3 / story_3 / chapter_3');
      // Suggestion 3's exploration_content_html does not match its
      // content_html.
      expect(component.hasExplorationContentChanged()).toBe(true);
      expect(fetchMessagesAsyncSpy).toHaveBeenCalledWith('suggestion_3');
      expect(component.reviewMessage).toBe(
        AppConstants.OBSOLETE_TRANSLATION_SUGGESTION_REVIEW_MSG
      );
    }));
  });

  describe('when navigating through suggestions', function () {
    const reviewable = false;
    const subheading = 'topic_1 / story_1 / chapter_1';

    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: ['Translation1', 'Translation2'],
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: ['Translation1', 'Translation2 CHANGED'],
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: 'Translation',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: 'Translation',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      status: 'status',
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
    };

    const suggestionIdToContributionOne = {
      suggestion_1: contribution1,
    };

    beforeEach(() => {
      component.initialSuggestionId = 'suggestion_1';
      component.subheading = subheading;
      component.reviewable = reviewable;
    });

    it('should correctly set variables if there is only one item', () => {
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContributionOne
      );
      component.ngOnInit();

      expect(component.isFirstItem).toBeTrue();
      expect(component.isLastItem).toBeTrue();
      expect(component.remainingContributionIds.length).toEqual(0);
      expect(component.skippedContributionIds.length).toEqual(0);
    });

    it('should correctly set variables if there are multiple items', () => {
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.ngOnInit();

      expect(component.isFirstItem).toBeTrue();
      expect(component.isLastItem).toBeFalse();
      expect(component.remainingContributionIds.length).toEqual(1);
      expect(component.skippedContributionIds.length).toEqual(0);
    });

    it('should successfully navigate between items', () => {
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.ngOnInit();
      spyOn(component, 'refreshActiveContributionState').and.callThrough();

      expect(component.isFirstItem).toBeTrue();
      expect(component.isLastItem).toBeFalse();
      expect(component.remainingContributionIds).toEqual(['suggestion_2']);
      expect(component.skippedContributionIds.length).toEqual(0);
      expect(component.activeSuggestionId).toEqual('suggestion_1');

      component.goToPreviousItem();
      // As we are on the first item, goToPreviousItem shouldn't navigate.
      expect(component.isFirstItem).toBeTrue();
      expect(component.isLastItem).toBeFalse();
      expect(component.remainingContributionIds).toEqual(['suggestion_2']);
      expect(component.skippedContributionIds.length).toEqual(0);
      expect(component.activeSuggestionId).toEqual('suggestion_1');

      component.goToNextItem();

      expect(component.isFirstItem).toBeFalse();
      expect(component.isLastItem).toBeTrue();
      expect(component.remainingContributionIds.length).toEqual(0);
      expect(component.skippedContributionIds).toEqual(['suggestion_1']);
      expect(component.activeSuggestionId).toEqual('suggestion_2');
      expect(component.refreshActiveContributionState).toHaveBeenCalled();

      component.goToNextItem();
      // As we are on the last item, goToNextItem shoudn't navigate.
      expect(component.isFirstItem).toBeFalse();
      expect(component.isLastItem).toBeTrue();
      expect(component.remainingContributionIds.length).toEqual(0);
      expect(component.skippedContributionIds).toEqual(['suggestion_1']);
      expect(component.activeSuggestionId).toEqual('suggestion_2');

      component.goToPreviousItem();

      expect(component.isFirstItem).toBeTrue();
      expect(component.isLastItem).toBeFalse();
      expect(component.remainingContributionIds).toEqual(['suggestion_2']);
      expect(component.skippedContributionIds.length).toEqual(0);
      expect(component.activeSuggestionId).toEqual('suggestion_1');
      expect(component.refreshActiveContributionState).toHaveBeenCalled();
    });

    it(
      'should close the modal if the opportunity is' +
        ' deleted when navigating forward',
      () => {
        component.suggestionIdToContribution = angular.copy(
          suggestionIdToContribution
        );
        component.ngOnInit();
        spyOn(activeModal, 'close');
        component.allContributions.suggestion_2.details = null;

        component.goToNextItem();

        expect(activeModal.close).toHaveBeenCalledWith([]);
      }
    );

    it(
      'should close the modal if the opportunity is' +
        ' deleted when navigating backward',
      () => {
        component.suggestionIdToContribution = angular.copy(
          suggestionIdToContribution
        );
        component.ngOnInit();
        spyOn(activeModal, 'close');

        component.goToNextItem();

        expect(component.activeSuggestionId).toEqual('suggestion_2');
        // Delete the opportunity of the previous item.
        component.allContributions.suggestion_1.details = null;

        component.goToPreviousItem();

        expect(activeModal.close).toHaveBeenCalledWith([]);
      }
    );
  });

  describe('when set the schema constant', function () {
    const reviewable = true;
    const subheading = 'topic_1 / story_1 / chapter_1';
    const suggestion1 = {
      suggestion_id: 'suggestion_1',
      target_id: '1',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p><p>&nbsp;</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content</p><p>&nbsp;</p>',
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };
    const suggestion2 = {
      suggestion_id: 'suggestion_2',
      target_id: '2',
      suggestion_type: 'translate_content',
      change_cmd: {
        content_id: 'hint_1',
        content_html: '<p>content</p>',
        translation_html: 'Tradução',
        state_name: 'StateName',
        cmd: 'edit_state_property',
        data_format: 'html',
        language_code: 'language_code',
      },
      exploration_content_html: '<p>content CHANGED</p>',
      status: 'rejected',
      author_name: 'author_name',
      language_code: 'language_code',
      last_updated_msecs: 1559074000000,
      target_type: 'target_type',
    };

    const contribution1 = {
      suggestion: suggestion1,
      details: {
        topic_name: 'topic_1',
        story_title: 'story_1',
        chapter_title: 'chapter_1',
      },
    };
    const contribution2 = {
      suggestion: suggestion2,
      details: {
        topic_name: 'topic_2',
        story_title: 'story_2',
        chapter_title: 'chapter_2',
      },
    };

    const suggestionIdToContribution = {
      suggestion_1: contribution1,
      suggestion_2: contribution2,
    };

    const editedContent = {
      html: '<p>In Hindi</p>',
    };

    beforeEach(fakeAsync(() => {
      component.initialSuggestionId = 'suggestion_2';
      component.subheading = subheading;
      component.reviewable = reviewable;
      component.suggestionIdToContribution = angular.copy(
        suggestionIdToContribution
      );
      component.editedContent = editedContent;
      component.ngOnInit();
      tick();
    }));

    it('should get html schema', () => {
      expect(component.getHtmlSchema()).toEqual({
        type: 'html',
      });
    });

    it('should get unicode schema', () => {
      expect(component.getUnicodeSchema()).toEqual({
        type: 'unicode',
      });
    });

    it('should get set of strings schema', () => {
      expect(component.getSetOfStringsSchema()).toEqual({
        type: 'list',
        items: {
          type: 'unicode',
        },
      });
    });

    it('should invoke change detection when html is updated', () => {
      component.editedContent.html = 'old';
      spyOn(changeDetectorRef, 'detectChanges').and.callThrough();
      component.updateHtml('new');
      expect(component.editedContent.html).toEqual('new');
    });

    it('should not invoke change detection when html is not updated', () => {
      component.editedContent.html = 'old';
      spyOn(changeDetectorRef, 'detectChanges').and.callThrough();
      component.updateHtml('old');
      expect(component.editedContent.html).toEqual('old');
      expect(changeDetectorRef.detectChanges).toHaveBeenCalledTimes(0);
    });

    it('should check if the change cmd is deprecated', () => {
      const deprecatedCmd = 'add_translation';
      const validCmd = 'add_written_translation';

      component.activeSuggestion.change_cmd.cmd = validCmd;
      expect(component.isDeprecatedTranslationSuggestionCommand()).toBeFalse();

      component.activeSuggestion.change_cmd.cmd = deprecatedCmd;
      expect(component.isDeprecatedTranslationSuggestionCommand()).toBeTrue();
    });

    it('should check if translation contains HTML tags', () => {
      const translationWithTags = '<p>translation with tags</p>';
      const translationWithoutTags = 'translation without tags';

      component.translationHtml = translationWithTags;
      expect(component.doesTranslationContainTags()).toBeTrue();

      component.translationHtml = translationWithoutTags;
      expect(component.doesTranslationContainTags()).toBeFalse();
    });
  });

  it('should extract html related to images', () => {
    const content =
      '<oppia-noninteractive-image src="image1.jpg">' +
      'something</oppia-noninteractive-image>' +
      '<another-tag value="a"></another-tag>' +
      '<oppia-noninteractive-image src="image2.jpg"></oppia-noninteractive-image>';
    const expectedHtmlString =
      '<oppia-noninteractive-image src="image1.jpg">' +
      'something</oppia-noninteractive-image>' +
      '<oppia-noninteractive-image src="image2.jpg"></oppia-noninteractive-image>';

    const result = component.getImageInfoForSuggestion(content);

    expect(result).toEqual(expectedHtmlString);
  });
});