Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AssessmentDetailComponent } from './assessment-detail.component'

describe('AssessmentDetailComponent', () => {
let component: AssessmentDetailComponent
let mockViewSvc: any
let mockHttp: any
let mockContentSvc: any
let mockCdr: any

beforeEach(() => {
mockViewSvc = {
getCompetencyAuthoringUrl: jest.fn().mockReturnValue('https://authoring.host/artifact.json'),
getAuthoringUrl: jest.fn().mockReturnValue('https://authoring.host/artifact.json'),
replaceToAuthUrl: jest.fn(quiz => quiz),
}
mockHttp = { get: jest.fn() }
mockContentSvc = { fetchContent: jest.fn() }
mockCdr = { detectChanges: jest.fn() }
const mockActivatedRoute = { snapshot: { queryParams: {} } } as any
const mockLogger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() } as any

component = new AssessmentDetailComponent(
mockViewSvc,
mockHttp,
mockContentSvc,
mockActivatedRoute,
mockLogger,
mockCdr,
)
})

describe('transformQuiz isAssessment default (non-competency, direct artifactUrl path)', () => {
it('honours an explicit isAssessment flag on the content', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, options: [] }],
}),
})

const result = await component['transformQuiz']({ artifactUrl: 'https://x/content/do_1/artifact.json', isAssessment: false })

expect(result.isAssessment).toBe(false)
})

it('defaults isAssessment to true when the content does not specify it', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, options: [] }],
}),
})

const result = await component['transformQuiz']({ artifactUrl: 'https://x/content/do_1/artifact.json' })

expect(result.isAssessment).toBe(true)
})

it('treats an explicit isAssessment: 0 as falsy but not nullish, so it is preserved', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, options: [] }],
}),
})

const result = await component['transformQuiz']({ artifactUrl: 'https://x/content/do_1/artifact.json', isAssessment: false })

expect(result.isAssessment).toBe(false)
})
})

describe('ngOnInit', () => {
it('assigns the transformed quiz data and triggers change detection', async () => {
component.content = { artifactUrl: 'https://x/content/do_1/artifact.json' }
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, options: [] }],
}),
})

await component.ngOnInit()

expect(component.assesmentdata.isAssessment).toBe(true)
expect(mockCdr.detectChanges).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export class AssessmentDetailComponent implements OnInit {
} else if (!question.multiSelection && question.questionType === undefined) {
question.questionType = 'mcq-sca'
}
quizJSON.isAssessment = content.isAssessment || false
quizJSON.isAssessment = content.isAssessment ?? true
})
if (!quizJSON.hasOwnProperty('passPercentage')) {
quizJSON.passPercentage = 60
Expand Down
115 changes: 115 additions & 0 deletions project/ws/viewer/src/lib/plugins/quiz/quiz.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { QuizComponent } from './quiz.component'

describe('QuizComponent', () => {
let component: QuizComponent
let mockHttp: any
let mockViewSvc: any

beforeEach(() => {
mockHttp = {
get: jest.fn(),
}
mockViewSvc = {
getCompetencyAuthoringUrl: jest.fn().mockReturnValue('https://authoring.host/artifact.json'),
}
component = new QuizComponent(
{} as any, // events
{} as any, // dialog
{} as any, // quizSvc
{} as any, // viewerSvc
{ snapshot: { queryParams: {} } } as any, // route
{} as any, // location
{} as any, // viewerDataSvc
{} as any, // playerStateService
{} as any, // router
{} as any, // contentSvc
{} as any, // loggerSvc
{} as any, // configSvc
mockHttp,
mockViewSvc,
)
component.artifactUrl = 'https://sphere.aastrika.org/content/do_123/artifact/quiz.json'
})

describe('transformQuiz', () => {
it('derives the authoring artifact url from artifactUrl and fetches it', async () => {
mockHttp.get.mockReturnValue({ toPromise: jest.fn().mockResolvedValue({ questions: [] }) })

await component['transformQuiz'](component.artifactUrl)

expect(mockViewSvc.getCompetencyAuthoringUrl).toHaveBeenCalledWith('/do_123/artifact/quiz.json')
expect(mockHttp.get).toHaveBeenCalledWith('https://authoring.host/artifact.json')
})

it('defaults undefined questionType to mcq-mca for multiSelection questions', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: true, options: [] }],
}),
})

const result = await component['transformQuiz'](component.artifactUrl)

expect(result.questions[0].questionType).toBe('mcq-mca')
})

it('defaults undefined questionType to mcq-sca for single-selection questions', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, options: [] }],
}),
})

const result = await component['transformQuiz'](component.artifactUrl)

expect(result.questions[0].questionType).toBe('mcq-sca')
})

it('leaves an already-set questionType untouched', async () => {
mockHttp.get.mockReturnValue({
toPromise: jest.fn().mockResolvedValue({
questions: [{ questionId: 'q1', multiSelection: false, questionType: 'fitb', options: [] }],
}),
})

const result = await component['transformQuiz'](component.artifactUrl)

expect(result.questions[0].questionType).toBe('fitb')
})

it('resolves to undefined when the fetch fails, without throwing', async () => {
mockHttp.get.mockReturnValue({ toPromise: jest.fn().mockRejectedValue(new Error('network error')) })

const result = await component['transformQuiz'](component.artifactUrl)

expect(result).toBeUndefined()
})
})

describe('openOverviewDialog restart flow', () => {
it('re-fetches and applies the transformed questions before opening the assessment/quiz dialog', async () => {
const transformedQuestions = [{ questionId: 'q1', multiSelection: false, questionType: 'mcq-sca', options: [] }]
jest.spyOn(component as any, 'transformQuiz').mockResolvedValue({ questions: transformedQuestions })
const openAssesmentDialogSpy = jest.spyOn(component, 'openAssesmentDialog').mockImplementation(() => undefined)
const openQuizDialogSpy = jest.spyOn(component, 'openQuizDialog').mockImplementation(() => undefined)
component.quizJson = { timeLimit: 0, questions: [], isAssessment: true, passPercentage: 60 } as any

const dialogOverviewMock = {
afterClosed: () => ({
subscribe: (cb: (result: any) => Promise<void>) => cb({ event: 'restart' }),
}),
}
component.dialog = { closeAll: jest.fn(), open: jest.fn().mockReturnValue(dialogOverviewMock) } as any

component.openOverviewDialog()
// allow the async afterClosed callback (which awaits transformQuiz) to settle
await Promise.resolve()
await Promise.resolve()

expect(component['transformQuiz']).toHaveBeenCalledWith(component.artifactUrl)
expect(component.quizJson.questions).toBe(transformedQuestions)
expect(openAssesmentDialogSpy).toHaveBeenCalled()
expect(openQuizDialogSpy).not.toHaveBeenCalled()
})
})
})
28 changes: 25 additions & 3 deletions project/ws/viewer/src/lib/plugins/quiz/quiz.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
} from '@ws-widget/utils'
import moment from 'moment'
import _ from 'lodash'
import { HttpClient } from '@angular/common/http'
// import { SearchApiService } from '../../../../../app/src/lib/routes/search/apis/search-api.service'
@Component({
standalone: false,
Expand Down Expand Up @@ -132,7 +133,9 @@ export class QuizComponent implements OnInit, OnChanges, OnDestroy {
public router: Router,
private contentSvc: WidgetContentService,
private loggerSvc: LoggerService,
private configSvc: ConfigurationsService
private configSvc: ConfigurationsService,
private http: HttpClient,
private viewSvc: ViewerUtilService
) {

}
Expand Down Expand Up @@ -165,7 +168,7 @@ export class QuizComponent implements OnInit, OnChanges, OnDestroy {
data: overviewData,
})

this.dialogOverview.afterClosed().subscribe((result: any) => {
this.dialogOverview.afterClosed().subscribe(async (result: any) => {
// Release the ref as soon as the overview closes, otherwise the `!this.dialogOverview`
// guard above turns every later call into a no-op for the life of this component —
// which is why "Yes, Restart" never reopened the overview.
Expand Down Expand Up @@ -226,7 +229,8 @@ export class QuizComponent implements OnInit, OnChanges, OnDestroy {
})
}
} else {
// this.startQuiz()
let res = await this.transformQuiz(this.artifactUrl)
this.quizJson.questions = res.questions
if (get(this.quizJson, 'isAssessment')) {
this.openAssesmentDialog()
} else {
Expand All @@ -238,6 +242,24 @@ export class QuizComponent implements OnInit, OnChanges, OnDestroy {
}
}

private async transformQuiz(url: string): Promise<NSQuiz.IQuiz> {
const artifactUrl = this.viewSvc.getCompetencyAuthoringUrl(url.split('/content')[1])
let quizJSON: NSQuiz.IQuiz = await this.http
.get<any>(artifactUrl || '')
.toPromise()
.catch((_err: any) => {
})
if (quizJSON && quizJSON.questions) {
quizJSON.questions.forEach((question: NSQuiz.IQuestion) => {
if (question.multiSelection && question.questionType === undefined) {
question.questionType = 'mcq-mca'
} else if (!question.multiSelection && question.questionType === undefined) {
question.questionType = 'mcq-sca'
}
})
}
return quizJSON
}
scroll(qIndex: number) {
if (!this.sidenavOpenDefault) {
if (this.sideNav) {
Expand Down
Loading