@testgorilla/tgo-ai-interview-test 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/testgorilla-tgo-ai-interview-test.mjs +478 -0
- package/fesm2022/testgorilla-tgo-ai-interview-test.mjs.map +1 -0
- package/lib/components/ai-interview-test/ai-interview-test.component.d.ts +44 -0
- package/lib/components/interview-stream/interview-stream.component.d.ts +46 -0
- package/lib/components/interview-video/interview-video.component.d.ts +16 -0
- package/lib/models/index.d.ts +3 -0
- package/lib/models/question-component.d.ts +5 -0
- package/lib/models/translations.d.ts +1 -0
- package/package.json +29 -12
- package/.eslintrc.json +0 -46
- package/jest.config.ts +0 -29
- package/ng-package.json +0 -16
- package/project.json +0 -48
- package/src/lib/components/ai-interview-test/ai-interview-test.component.html +0 -42
- package/src/lib/components/ai-interview-test/ai-interview-test.component.scss +0 -167
- package/src/lib/components/ai-interview-test/ai-interview-test.component.spec.ts +0 -212
- package/src/lib/components/ai-interview-test/ai-interview-test.component.ts +0 -192
- package/src/lib/components/interview-stream/interview-stream.component.html +0 -9
- package/src/lib/components/interview-stream/interview-stream.component.scss +0 -5
- package/src/lib/components/interview-stream/interview-stream.component.spec.ts +0 -259
- package/src/lib/components/interview-stream/interview-stream.component.ts +0 -320
- package/src/lib/components/interview-video/interview-video.component.html +0 -8
- package/src/lib/components/interview-video/interview-video.component.scss +0 -7
- package/src/lib/components/interview-video/interview-video.component.spec.ts +0 -140
- package/src/lib/components/interview-video/interview-video.component.ts +0 -67
- package/src/lib/models/index.ts +0 -13
- package/src/lib/models/question-component.ts +0 -13
- package/src/lib/models/translations.ts +0 -3
- package/src/test-setup.ts +0 -76
- package/tsconfig.json +0 -20
- package/tsconfig.lib.json +0 -20
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -17
- /package/{src/assets → assets}/i18n/en.json +0 -0
- /package/{src/index.ts → index.d.ts} +0 -0
- /package/{src/lib/components/index.ts → lib/components/index.d.ts} +0 -0
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, signal } from '@angular/core';
|
|
2
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
4
|
-
import { TranslocoService, TranslocoTestingModule } from '@ngneat/transloco';
|
|
5
|
-
import {
|
|
6
|
-
MediaService,
|
|
7
|
-
Question,
|
|
8
|
-
SelectedMediaDevices,
|
|
9
|
-
TestResultRead,
|
|
10
|
-
ThemeService,
|
|
11
|
-
} from '@testgorilla/tgo-test-shared';
|
|
12
|
-
import { ButtonComponentModule, IconComponentModule } from '@testgorilla/tgo-ui';
|
|
13
|
-
import { of, Subject } from 'rxjs';
|
|
14
|
-
import { AiInterviewTestComponent } from './ai-interview-test.component';
|
|
15
|
-
|
|
16
|
-
describe('AiInterviewTestComponent', () => {
|
|
17
|
-
let component: AiInterviewTestComponent;
|
|
18
|
-
let fixture: ComponentFixture<AiInterviewTestComponent>;
|
|
19
|
-
let transLocoServiceMock: Partial<jest.Mocked<TranslocoService>>;
|
|
20
|
-
let getMediaStreamSpy: jest.SpyInstance;
|
|
21
|
-
let submissionStateChangedSpy: jest.SpyInstance;
|
|
22
|
-
let mediaAccessChanged$: Subject<SelectedMediaDevices>;
|
|
23
|
-
let setSelectedMediaDevicesSpy: jest.SpyInstance;
|
|
24
|
-
let loadingStateChangedSpy: jest.SpyInstance;
|
|
25
|
-
|
|
26
|
-
const mockedTranslations = { testTranslationKey: 'test-translation' };
|
|
27
|
-
const mockStreamUrl = 'https://stream.com';
|
|
28
|
-
|
|
29
|
-
const mockedTest = {
|
|
30
|
-
is_preview_mode: false,
|
|
31
|
-
} as TestResultRead;
|
|
32
|
-
|
|
33
|
-
let hasMediaPermission = true;
|
|
34
|
-
|
|
35
|
-
class MediaServiceMock {
|
|
36
|
-
private recordSubject = new Subject();
|
|
37
|
-
|
|
38
|
-
isRecording = signal(false);
|
|
39
|
-
|
|
40
|
-
setSelectedMediaDevices(): void {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getMediaStream() {
|
|
45
|
-
return Promise.resolve();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
checkPermission() {
|
|
49
|
-
return Promise.resolve(hasMediaPermission);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
beforeEach(async () => {
|
|
54
|
-
transLocoServiceMock = {
|
|
55
|
-
selectTranslateObject: jest.fn().mockReturnValue(of(mockedTranslations)),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
-
const mockedMediaService = new MediaServiceMock() as any as MediaService;
|
|
60
|
-
|
|
61
|
-
await TestBed.configureTestingModule({
|
|
62
|
-
imports: [
|
|
63
|
-
TranslocoTestingModule.forRoot({
|
|
64
|
-
translocoConfig: {
|
|
65
|
-
availableLangs: ['en'],
|
|
66
|
-
defaultLang: 'en',
|
|
67
|
-
reRenderOnLangChange: true,
|
|
68
|
-
},
|
|
69
|
-
preloadLangs: true,
|
|
70
|
-
}),
|
|
71
|
-
IconComponentModule,
|
|
72
|
-
ButtonComponentModule,
|
|
73
|
-
NoopAnimationsModule,
|
|
74
|
-
AiInterviewTestComponent,
|
|
75
|
-
],
|
|
76
|
-
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
|
|
77
|
-
providers: [
|
|
78
|
-
{ provide: TranslocoService, useValue: transLocoServiceMock },
|
|
79
|
-
{ provide: MediaService, useValue: mockedMediaService },
|
|
80
|
-
{
|
|
81
|
-
provide: ChangeDetectorRef,
|
|
82
|
-
useValue: { markForCheck: jest.fn() },
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
provide: ThemeService,
|
|
86
|
-
useValue: {
|
|
87
|
-
uiTheme: 'dark',
|
|
88
|
-
getCompanyColor: jest.fn().mockReturnValue('#D410AA'),
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
})
|
|
93
|
-
.overrideComponent(AiInterviewTestComponent, {
|
|
94
|
-
set: {
|
|
95
|
-
template: '',
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
.compileComponents();
|
|
99
|
-
|
|
100
|
-
fixture = TestBed.createComponent(AiInterviewTestComponent);
|
|
101
|
-
component = fixture.componentInstance;
|
|
102
|
-
|
|
103
|
-
getMediaStreamSpy = jest.spyOn(mockedMediaService, 'getMediaStream');
|
|
104
|
-
setSelectedMediaDevicesSpy = jest.spyOn(mockedMediaService, 'setSelectedMediaDevices');
|
|
105
|
-
submissionStateChangedSpy = jest.spyOn(component.submissionStateChanged, 'emit');
|
|
106
|
-
|
|
107
|
-
loadingStateChangedSpy = jest.spyOn(component.loadingStateChanged, 'emit');
|
|
108
|
-
|
|
109
|
-
mediaAccessChanged$ = new Subject<SelectedMediaDevices>();
|
|
110
|
-
component.mediaAccessChanged = mediaAccessChanged$.asObservable();
|
|
111
|
-
hasMediaPermission = true;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
fixture.destroy();
|
|
116
|
-
jest.clearAllMocks();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('when component is initialized with and with question video content', () => {
|
|
120
|
-
beforeEach(() => {
|
|
121
|
-
component.question = {
|
|
122
|
-
text: ``,
|
|
123
|
-
} as Question;
|
|
124
|
-
component.test = mockedTest;
|
|
125
|
-
component.conversationUrl = mockStreamUrl;
|
|
126
|
-
component.ngOnInit();
|
|
127
|
-
fixture.detectChanges();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should set the translations', () => {
|
|
131
|
-
expect(component.translations).toEqual(mockedTranslations);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should init stream', () => {
|
|
135
|
-
expect(getMediaStreamSpy).toHaveBeenCalled();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should return stream content url', () => {
|
|
139
|
-
expect(component.conversationUrl).toBe(mockStreamUrl);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should has candidateStreamReady = false by default', () => {
|
|
143
|
-
expect(component.candidateVideoStreamReady()).toBe(false);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('when video is loaded', () => {
|
|
147
|
-
beforeEach(() => {
|
|
148
|
-
component.onVideoLoad();
|
|
149
|
-
fixture.detectChanges();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should set candidateStreamReady to true', () => {
|
|
153
|
-
expect(component.candidateVideoStreamReady()).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('when mediaAccessChange is called', () => {
|
|
158
|
-
const changedAudioDeviceId = 'test_audio_changed';
|
|
159
|
-
const changedVideoDeviceId = 'test_video_changed';
|
|
160
|
-
beforeEach(() => {
|
|
161
|
-
mediaAccessChanged$.next({
|
|
162
|
-
audioDeviceId: changedAudioDeviceId,
|
|
163
|
-
videoDeviceId: changedVideoDeviceId,
|
|
164
|
-
});
|
|
165
|
-
fixture.detectChanges();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should call mediaService.setSelectedMediaDevices() method with new devices Ids', () => {
|
|
169
|
-
expect(setSelectedMediaDevicesSpy).toHaveBeenCalledWith({
|
|
170
|
-
audioDeviceId: changedAudioDeviceId,
|
|
171
|
-
videoDeviceId: changedVideoDeviceId,
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should reinitialize video stream', () => {
|
|
176
|
-
expect(getMediaStreamSpy).toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe('when interviewStarted is called', () => {
|
|
181
|
-
beforeEach(() => {
|
|
182
|
-
component.interviewStarted();
|
|
183
|
-
fixture.detectChanges();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should set isInterviewInProgress to true', () => {
|
|
187
|
-
expect(component.isInterviewInProgress()).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should emit loadingStateChanged with false', () => {
|
|
191
|
-
expect(loadingStateChangedSpy).toHaveBeenCalledWith(false);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe('when interviewEnded is called', () => {
|
|
195
|
-
beforeEach(() => {
|
|
196
|
-
component.interviewEnded();
|
|
197
|
-
fixture.detectChanges();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should set isInterviewInProgress to false', () => {
|
|
201
|
-
expect(component.isInterviewInProgress()).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('should emit submissionStateChanged with empty text', () => {
|
|
205
|
-
expect(submissionStateChangedSpy).toHaveBeenCalledWith({
|
|
206
|
-
text: '',
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { InterviewStreamComponent } from '../interview-stream/interview-stream.component';
|
|
2
|
-
import { firstValueFrom, Observable, takeUntil } from 'rxjs';
|
|
3
|
-
import { transition, style, animate, trigger } from '@angular/animations';
|
|
4
|
-
import {
|
|
5
|
-
ElementRef,
|
|
6
|
-
OnDestroy,
|
|
7
|
-
signal,
|
|
8
|
-
ViewChild,
|
|
9
|
-
Component,
|
|
10
|
-
EventEmitter,
|
|
11
|
-
Input,
|
|
12
|
-
Output,
|
|
13
|
-
inject,
|
|
14
|
-
OnInit,
|
|
15
|
-
Inject,
|
|
16
|
-
ChangeDetectorRef,
|
|
17
|
-
} from '@angular/core';
|
|
18
|
-
import { Subject } from 'rxjs';
|
|
19
|
-
import {
|
|
20
|
-
ButtonComponentModule,
|
|
21
|
-
DialogService,
|
|
22
|
-
IconComponentModule,
|
|
23
|
-
} from '@testgorilla/tgo-ui';
|
|
24
|
-
import { CommonModule } from '@angular/common';
|
|
25
|
-
import {
|
|
26
|
-
TRANSLOCO_SCOPE,
|
|
27
|
-
TranslocoModule,
|
|
28
|
-
TranslocoScope,
|
|
29
|
-
TranslocoService,
|
|
30
|
-
} from '@ngneat/transloco';
|
|
31
|
-
import {
|
|
32
|
-
AudioAnimationComponent,
|
|
33
|
-
Question,
|
|
34
|
-
TestResultRead,
|
|
35
|
-
SelectedMediaDevices,
|
|
36
|
-
ISubmissionState,
|
|
37
|
-
ROOT_TRANSLATIONS_SCOPE,
|
|
38
|
-
MediaService,
|
|
39
|
-
ThemeService,
|
|
40
|
-
TranslocoLazyModuleUtils,
|
|
41
|
-
getAvailableLangs,
|
|
42
|
-
} from '@testgorilla/tgo-test-shared';
|
|
43
|
-
import { IQuestionDataContract } from '../../models';
|
|
44
|
-
|
|
45
|
-
@Component({
|
|
46
|
-
selector: 'tgo-ai-interview-test',
|
|
47
|
-
templateUrl: './ai-interview-test.component.html',
|
|
48
|
-
styleUrl: './ai-interview-test.component.scss',
|
|
49
|
-
animations: [
|
|
50
|
-
trigger('fadeInFadeOut', [
|
|
51
|
-
transition(':enter', [
|
|
52
|
-
style({ opacity: 0 }),
|
|
53
|
-
animate('600ms', style({ opacity: 1 })),
|
|
54
|
-
]),
|
|
55
|
-
transition(':leave', [animate('600ms', style({ opacity: 0 }))]),
|
|
56
|
-
]),
|
|
57
|
-
],
|
|
58
|
-
imports: [
|
|
59
|
-
TranslocoModule,
|
|
60
|
-
ButtonComponentModule,
|
|
61
|
-
IconComponentModule,
|
|
62
|
-
CommonModule,
|
|
63
|
-
AudioAnimationComponent,
|
|
64
|
-
InterviewStreamComponent,
|
|
65
|
-
],
|
|
66
|
-
providers: [
|
|
67
|
-
TranslocoLazyModuleUtils.getScopeProvider('tgo-ai-interview-test', getAvailableLangs(), ROOT_TRANSLATIONS_SCOPE, (lang: string) => {
|
|
68
|
-
// Fetch from app assets; demo app copies the library assets to
|
|
69
|
-
// /assets/tgo-ai-interview-test via project.json.
|
|
70
|
-
const url = new URL(`assets/tgo-ai-interview-test/i18n/${lang}.json`, document.baseURI).href;
|
|
71
|
-
return fetch(url).then((res) => res.json());
|
|
72
|
-
}),
|
|
73
|
-
DialogService,
|
|
74
|
-
ThemeService,
|
|
75
|
-
]
|
|
76
|
-
})
|
|
77
|
-
export class AiInterviewTestComponent
|
|
78
|
-
implements OnInit, OnDestroy, IQuestionDataContract
|
|
79
|
-
{
|
|
80
|
-
@ViewChild('video') videoElement?: ElementRef<HTMLVideoElement>;
|
|
81
|
-
@ViewChild('audio') audioElement?: ElementRef<HTMLAudioElement>;
|
|
82
|
-
@Input({ required: true }) question!: Question;
|
|
83
|
-
@Input({ required: true }) test!: TestResultRead;
|
|
84
|
-
@Input() isFirstQuestion?: boolean = false;
|
|
85
|
-
@Input() selectedMediaDevices?: SelectedMediaDevices;
|
|
86
|
-
@Input() conversationUrl?: string;
|
|
87
|
-
@Input() mediaAccessChanged?: Observable<SelectedMediaDevices> | undefined;
|
|
88
|
-
|
|
89
|
-
@Output() submissionStateChanged =
|
|
90
|
-
new EventEmitter<ISubmissionState | null>();
|
|
91
|
-
@Output() loadingStateChanged: EventEmitter<boolean> =
|
|
92
|
-
new EventEmitter<boolean>();
|
|
93
|
-
@Output() requestMediaAccess: EventEmitter<void> = new EventEmitter<void>();
|
|
94
|
-
|
|
95
|
-
isInterviewInProgress = signal(false);
|
|
96
|
-
candidateVideoStreamReady = signal(false);
|
|
97
|
-
translations: { [key: string]: string } = {};
|
|
98
|
-
hasMediaPermissions = signal(false);
|
|
99
|
-
|
|
100
|
-
private unsubscribe$ = new Subject<void>();
|
|
101
|
-
private mediaService = inject(MediaService);
|
|
102
|
-
private translocoService = inject(TranslocoService);
|
|
103
|
-
private cdr = inject(ChangeDetectorRef);
|
|
104
|
-
private themeService = inject(ThemeService);
|
|
105
|
-
|
|
106
|
-
companyColor = this.themeService.getCompanyColor();
|
|
107
|
-
|
|
108
|
-
constructor(
|
|
109
|
-
@Inject(TRANSLOCO_SCOPE) private translationScope: TranslocoScope
|
|
110
|
-
) {}
|
|
111
|
-
|
|
112
|
-
ngOnInit(): void {
|
|
113
|
-
this.initMediaAccessSubscription();
|
|
114
|
-
this.loadingStateChanged.emit(true);
|
|
115
|
-
this.mediaService.setSelectedMediaDevices(this.selectedMediaDevices);
|
|
116
|
-
void this.checkMediaPermissions();
|
|
117
|
-
|
|
118
|
-
void this.setTranslations();
|
|
119
|
-
void this.initVideoStream();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
ngOnDestroy() {
|
|
123
|
-
this.unsubscribe$.next();
|
|
124
|
-
this.unsubscribe$.complete();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
onVideoLoad() {
|
|
128
|
-
this.candidateVideoStreamReady.set(true);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interviewStarted() {
|
|
132
|
-
this.isInterviewInProgress.set(true);
|
|
133
|
-
this.loadingStateChanged.emit(false);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interviewEnded() {
|
|
137
|
-
this.isInterviewInProgress.set(false);
|
|
138
|
-
this.submissionStateChanged.emit({
|
|
139
|
-
text: '',
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async checkMediaPermissions() {
|
|
144
|
-
if (
|
|
145
|
-
!(await this.mediaService.checkPermission({ audio: true, video: false }))
|
|
146
|
-
) {
|
|
147
|
-
this.hasMediaPermissions.set(false);
|
|
148
|
-
this.requestMediaAccess.emit();
|
|
149
|
-
return;
|
|
150
|
-
} else {
|
|
151
|
-
this.hasMediaPermissions.set(true);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async initVideoStream() {
|
|
156
|
-
try {
|
|
157
|
-
const stream = await this.mediaService.getMediaStream({
|
|
158
|
-
video: true,
|
|
159
|
-
audio: false,
|
|
160
|
-
});
|
|
161
|
-
if (this.videoElement) {
|
|
162
|
-
this.videoElement.nativeElement.srcObject = stream;
|
|
163
|
-
await this.videoElement?.nativeElement.play();
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error('Error initializing video stream:', error);
|
|
167
|
-
this.candidateVideoStreamReady.set(false);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private async setTranslations() {
|
|
172
|
-
this.translations = await firstValueFrom(
|
|
173
|
-
this.translocoService.selectTranslateObject(
|
|
174
|
-
`TEST`,
|
|
175
|
-
{},
|
|
176
|
-
this.translationScope as string
|
|
177
|
-
)
|
|
178
|
-
);
|
|
179
|
-
this.cdr.markForCheck();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private initMediaAccessSubscription() {
|
|
183
|
-
this.mediaAccessChanged
|
|
184
|
-
?.pipe(takeUntil(this.unsubscribe$))
|
|
185
|
-
.subscribe((selectedMediaDevices) => {
|
|
186
|
-
this.mediaService.setSelectedMediaDevices(selectedMediaDevices);
|
|
187
|
-
this.checkMediaPermissions();
|
|
188
|
-
void this.initVideoStream();
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
2
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
-
import {
|
|
4
|
-
DailyEventObjectFatalError,
|
|
5
|
-
DailyEventObjectNoPayload,
|
|
6
|
-
DailyEventObjectParticipant,
|
|
7
|
-
DailyEventObjectParticipantLeft,
|
|
8
|
-
DailyEventObjectParticipants,
|
|
9
|
-
DailyEventObjectTrack,
|
|
10
|
-
DailyParticipant,
|
|
11
|
-
} from '@daily-co/daily-js';
|
|
12
|
-
import { InterviewStreamComponent } from './interview-stream.component';
|
|
13
|
-
|
|
14
|
-
class MediaStreamTrackMock {
|
|
15
|
-
kind: string;
|
|
16
|
-
enabled: boolean;
|
|
17
|
-
id: string;
|
|
18
|
-
constructor(kind = 'video') {
|
|
19
|
-
this.kind = kind;
|
|
20
|
-
this.enabled = true;
|
|
21
|
-
this.id = `${kind}-track-${Math.random().toString(36).substring(2, 15)}`;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('InterviewStreamComponent', () => {
|
|
26
|
-
let component: InterviewStreamComponent;
|
|
27
|
-
let fixture: ComponentFixture<InterviewStreamComponent>;
|
|
28
|
-
let streamStartSpy: jest.SpyInstance;
|
|
29
|
-
let streamEndSpy: jest.SpyInstance;
|
|
30
|
-
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
// Mock MediaStream for InterviewVideoComponent
|
|
33
|
-
class MediaStreamMock {
|
|
34
|
-
tracks: MediaStreamTrack[] = [];
|
|
35
|
-
constructor(initialTracks: MediaStreamTrack[]) {
|
|
36
|
-
this.tracks = [...initialTracks];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
addTrack(track: MediaStreamTrack) {
|
|
40
|
-
this.tracks.push(track);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
removeTrack(track: MediaStreamTrack) {
|
|
44
|
-
const index = this.tracks.findIndex(t => t.id === track.id);
|
|
45
|
-
if (index !== -1) {
|
|
46
|
-
this.tracks.splice(index, 1);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
getVideoTracks() {
|
|
50
|
-
return this.tracks.filter(t => t.kind === 'video');
|
|
51
|
-
}
|
|
52
|
-
getAudioTracks() {
|
|
53
|
-
return this.tracks.filter(t => t.kind === 'audio');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
Object.defineProperty(global, 'MediaStream', {
|
|
58
|
-
writable: true,
|
|
59
|
-
value: jest.fn().mockImplementation(tracks => new MediaStreamMock(tracks)),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await TestBed.configureTestingModule({
|
|
63
|
-
imports: [InterviewStreamComponent],
|
|
64
|
-
schemas: [NO_ERRORS_SCHEMA],
|
|
65
|
-
}).compileComponents();
|
|
66
|
-
|
|
67
|
-
fixture = TestBed.createComponent(InterviewStreamComponent);
|
|
68
|
-
component = fixture.componentInstance;
|
|
69
|
-
streamStartSpy = jest.spyOn(component.streamStart, 'emit');
|
|
70
|
-
streamEndSpy = jest.spyOn(component.streamEnd, 'emit');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
afterEach(() => {
|
|
74
|
-
fixture.destroy();
|
|
75
|
-
jest.clearAllMocks();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('when component is initialized with conversationUrl', () => {
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
component.conversationUrl = 'https://example.com/conversation';
|
|
81
|
-
component.ngOnInit();
|
|
82
|
-
fixture.detectChanges();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should initialize the call object', () => {
|
|
86
|
-
expect(component.callObject).toBeDefined();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('when candidateJoinMeeting event is triggered', () => {
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
component['candidateJoinMeeting']({} as DailyEventObjectParticipants);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should emit streamStart event', () => {
|
|
95
|
-
expect(streamStartSpy).toHaveBeenCalled();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should set candidateJoined to true', () => {
|
|
99
|
-
expect(component.candidateJoined()).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('when candidateLeaveMeeting event is triggered', () => {
|
|
104
|
-
beforeEach(() => {
|
|
105
|
-
component['handleLeftMeeting']({} as DailyEventObjectNoPayload);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should emit streamEnd event', () => {
|
|
109
|
-
expect(streamEndSpy).toHaveBeenCalled();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should set candidateJoined to false', () => {
|
|
113
|
-
expect(component.candidateJoined()).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('when participantJoined event is triggered', () => {
|
|
118
|
-
const testSessionId = '123';
|
|
119
|
-
const testUserName = 'Test User';
|
|
120
|
-
beforeEach(() => {
|
|
121
|
-
component['participantJoined']({
|
|
122
|
-
participant: {
|
|
123
|
-
user_name: testUserName,
|
|
124
|
-
session_id: testSessionId,
|
|
125
|
-
tracks: {},
|
|
126
|
-
} as DailyParticipant,
|
|
127
|
-
} as DailyEventObjectParticipant);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should set avatarParticipant with the participant data', () => {
|
|
131
|
-
expect(component.avatarParticipant?.userName).toEqual(testUserName);
|
|
132
|
-
expect(component.avatarParticipant?.sessionId).toEqual(testSessionId);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('when video ready track-started event is triggered', () => {
|
|
136
|
-
beforeEach(() => {
|
|
137
|
-
const videoTrack = new MediaStreamTrackMock('video') as MediaStreamTrack;
|
|
138
|
-
component.candidateJoined.set(true);
|
|
139
|
-
const event = {
|
|
140
|
-
action: 'track-started',
|
|
141
|
-
participant: {
|
|
142
|
-
user_name: testUserName,
|
|
143
|
-
session_id: testSessionId,
|
|
144
|
-
tracks: {
|
|
145
|
-
video: {
|
|
146
|
-
state: 'playable',
|
|
147
|
-
persistentTrack: videoTrack,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
} as unknown as DailyParticipant,
|
|
151
|
-
type: 'video',
|
|
152
|
-
} as DailyEventObjectTrack;
|
|
153
|
-
component['handleTrackStartedStopped'](event);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should set videoReady to true for the participant', () => {
|
|
157
|
-
expect(component.avatarParticipant?.videoReady).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should set videoTrack for the participant', () => {
|
|
161
|
-
expect(component.avatarParticipant?.videoTrack).toBeDefined();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should not set audioReady for the participant', () => {
|
|
165
|
-
expect(component.avatarParticipant?.audioReady).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should not set audioTrack for the participant', () => {
|
|
169
|
-
expect(component.avatarParticipant?.audioTrack).toBeUndefined();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('when audio ready track-started event is triggered', () => {
|
|
174
|
-
const testSessionId = '123';
|
|
175
|
-
const testUserName = 'Test User';
|
|
176
|
-
beforeEach(() => {
|
|
177
|
-
const audioTrack = new MediaStreamTrackMock('audio') as MediaStreamTrack;
|
|
178
|
-
component.candidateJoined.set(true);
|
|
179
|
-
const event = {
|
|
180
|
-
action: 'track-started',
|
|
181
|
-
participant: {
|
|
182
|
-
user_name: testUserName,
|
|
183
|
-
session_id: testSessionId,
|
|
184
|
-
tracks: {
|
|
185
|
-
audio: {
|
|
186
|
-
state: 'playable',
|
|
187
|
-
persistentTrack: audioTrack,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
} as unknown as DailyParticipant,
|
|
191
|
-
type: 'audio',
|
|
192
|
-
} as DailyEventObjectTrack;
|
|
193
|
-
component['handleTrackStartedStopped'](event);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should set audioReady to true for the participant', () => {
|
|
197
|
-
expect(component.avatarParticipant?.audioReady).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should set audioTrack for the participant', () => {
|
|
201
|
-
expect(component.avatarParticipant?.audioTrack).toBeDefined();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('should not set videoReady for the participant', () => {
|
|
205
|
-
expect(component.avatarParticipant?.videoReady).toBe(false);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should not set videoTrack for the participant', () => {
|
|
209
|
-
expect(component.avatarParticipant?.videoTrack).toBeUndefined();
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('when participantLeft event is triggered', () => {
|
|
215
|
-
let callObjectLeaveSpy: jest.SpyInstance;
|
|
216
|
-
beforeEach(() => {
|
|
217
|
-
const testSessionId = '123';
|
|
218
|
-
const testUserName = 'Test User';
|
|
219
|
-
if (component.callObject) {
|
|
220
|
-
callObjectLeaveSpy = jest.spyOn(component.callObject, 'leave');
|
|
221
|
-
}
|
|
222
|
-
component['handleParticipantLeft']({
|
|
223
|
-
participant: {
|
|
224
|
-
user_name: testUserName,
|
|
225
|
-
session_id: testSessionId,
|
|
226
|
-
tracks: {},
|
|
227
|
-
} as DailyParticipant,
|
|
228
|
-
} as DailyEventObjectParticipantLeft);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should call callObject.leave()', () => {
|
|
232
|
-
expect(callObjectLeaveSpy).toHaveBeenCalled();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('when handleError event is triggered', () => {
|
|
237
|
-
let callObjectLeaveSpy: jest.SpyInstance;
|
|
238
|
-
let consoleErrorSpy: jest.SpyInstance;
|
|
239
|
-
|
|
240
|
-
beforeEach(() => {
|
|
241
|
-
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
242
|
-
if (component.callObject) {
|
|
243
|
-
callObjectLeaveSpy = jest.spyOn(component.callObject, 'leave');
|
|
244
|
-
}
|
|
245
|
-
component['handleError']({
|
|
246
|
-
errorMsg: 'Test',
|
|
247
|
-
} as DailyEventObjectFatalError);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
afterEach(() => {
|
|
251
|
-
consoleErrorSpy.mockRestore();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should call callObject.leave()', () => {
|
|
255
|
-
expect(callObjectLeaveSpy).toHaveBeenCalled();
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
});
|