@testgorilla/tgo-ai-interview-test 0.0.1
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/.eslintrc.json +46 -0
- package/README.md +91 -0
- package/jest.config.ts +29 -0
- package/ng-package.json +16 -0
- package/package.json +25 -0
- package/project.json +37 -0
- package/src/assets/i18n/en.json +19 -0
- package/src/index.ts +3 -0
- package/src/lib/components/ai-interview-test/ai-interview-test.component.html +42 -0
- package/src/lib/components/ai-interview-test/ai-interview-test.component.scss +167 -0
- package/src/lib/components/ai-interview-test/ai-interview-test.component.spec.ts +211 -0
- package/src/lib/components/ai-interview-test/ai-interview-test.component.ts +193 -0
- package/src/lib/components/index.ts +3 -0
- package/src/lib/components/interview-stream/interview-stream.component.html +9 -0
- package/src/lib/components/interview-stream/interview-stream.component.scss +5 -0
- package/src/lib/components/interview-stream/interview-stream.component.spec.ts +285 -0
- package/src/lib/components/interview-stream/interview-stream.component.ts +321 -0
- package/src/lib/components/interview-video/interview-video.component.html +8 -0
- package/src/lib/components/interview-video/interview-video.component.scss +7 -0
- package/src/lib/components/interview-video/interview-video.component.spec.ts +140 -0
- package/src/lib/components/interview-video/interview-video.component.ts +68 -0
- package/src/lib/models/index.ts +13 -0
- package/src/lib/models/question-component.ts +13 -0
- package/src/lib/models/translations.ts +3 -0
- package/src/test-setup.ts +28 -0
- package/tsconfig.json +17 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +13 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../../.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts"],
|
|
7
|
+
"extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
|
|
8
|
+
"rules": {
|
|
9
|
+
"@angular-eslint/directive-selector": [
|
|
10
|
+
"error",
|
|
11
|
+
{
|
|
12
|
+
"type": "attribute",
|
|
13
|
+
"prefix": "tgo",
|
|
14
|
+
"style": "camelCase"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"@angular-eslint/component-selector": [
|
|
18
|
+
"error",
|
|
19
|
+
{
|
|
20
|
+
"type": "element",
|
|
21
|
+
"prefix": "tgo",
|
|
22
|
+
"style": "kebab-case"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"files": ["*.html"],
|
|
29
|
+
"extends": ["plugin:@nx/angular-template"],
|
|
30
|
+
"rules": {}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"files": ["*.json"],
|
|
34
|
+
"parser": "jsonc-eslint-parser",
|
|
35
|
+
"rules": {
|
|
36
|
+
"@nx/dependency-checks": [
|
|
37
|
+
"error",
|
|
38
|
+
{
|
|
39
|
+
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @testgorilla/tgo-ai-interview-test
|
|
2
|
+
|
|
3
|
+
AI Interview component for TestGorilla assessments.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @testgorilla/tgo-ai-interview-test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { AiInterviewTestComponent } from '@testgorilla/tgo-ai-interview-test';
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
imports: [AiInterviewTestComponent],
|
|
18
|
+
template: `
|
|
19
|
+
<tgo-ai-interview-test
|
|
20
|
+
[question]="question"
|
|
21
|
+
[test]="test"
|
|
22
|
+
[isFirstQuestion]="false"
|
|
23
|
+
[conversationUrl]="conversationUrl"
|
|
24
|
+
[selectedMediaDevices]="selectedMediaDevices"
|
|
25
|
+
[mediaAccessChanged]="mediaAccessChanged"
|
|
26
|
+
(submissionStateChanged)="onSubmissionStateChanged($event)"
|
|
27
|
+
(loadingStateChanged)="onLoadingStateChanged($event)"
|
|
28
|
+
(requestMediaAccess)="onRequestMediaAccess()"
|
|
29
|
+
></tgo-ai-interview-test>
|
|
30
|
+
`,
|
|
31
|
+
})
|
|
32
|
+
export class MyComponent {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### Inputs
|
|
38
|
+
|
|
39
|
+
| Name | Type | Required | Default | Description |
|
|
40
|
+
| ------------------- | --------------------------------- | -------- | ------- | ------------------------------------------------ |
|
|
41
|
+
| question | Question | Yes | - | Question data containing text/media content |
|
|
42
|
+
| test | TestResultRead | Yes | - | Test configuration and metadata |
|
|
43
|
+
| isFirstQuestion | boolean | No | false | Whether this is the first question in the test |
|
|
44
|
+
| conversationUrl | string | No | - | Daily.co conversation URL for the AI interview |
|
|
45
|
+
| selectedMediaDevices| SelectedMediaDevices | No | - | Selected audio/video device IDs |
|
|
46
|
+
| mediaAccessChanged | Observable\<SelectedMediaDevices\>| No | - | Observable for media device changes |
|
|
47
|
+
|
|
48
|
+
### Outputs
|
|
49
|
+
|
|
50
|
+
| Name | Type | Description |
|
|
51
|
+
| -------------------- | --------------------------- | ---------------------------------------------- |
|
|
52
|
+
| submissionStateChanged| EventEmitter\<ISubmissionState \| null\> | Emits submission state when answer is submitted |
|
|
53
|
+
| loadingStateChanged | EventEmitter\<boolean\> | Emits loading state changes |
|
|
54
|
+
| requestMediaAccess | EventEmitter\<void\> | Emits when media access permission is needed |
|
|
55
|
+
|
|
56
|
+
## Peer Dependencies
|
|
57
|
+
|
|
58
|
+
This library requires the following peer dependencies:
|
|
59
|
+
|
|
60
|
+
- `@angular/common` ~18.2.13
|
|
61
|
+
- `@angular/core` ~18.2.13
|
|
62
|
+
- `@angular/animations` ~18.2.13
|
|
63
|
+
- `@angular/material` ~18.2.14 (for dialog support)
|
|
64
|
+
- `@ngneat/transloco` ~4.3.0
|
|
65
|
+
- `@testgorilla/tgo-ui` ~3.14.10
|
|
66
|
+
- `@daily-co/daily-js` ^0.79.0
|
|
67
|
+
- `rxjs` ~7.8.1
|
|
68
|
+
|
|
69
|
+
## Internal Dependencies
|
|
70
|
+
|
|
71
|
+
All required services, models, and components are included within this library:
|
|
72
|
+
|
|
73
|
+
- `MediaService` - Handles audio/video recording and playback
|
|
74
|
+
- `ThemeService` - Provides theme/company color configuration
|
|
75
|
+
- `Question`, `TestResultRead`, `SelectedMediaDevices`, `ISubmissionState` - Type definitions
|
|
76
|
+
- `AudioAnimationComponent`, `VideoCountdownComponent`, `VimeoVideoComponent` - UI components
|
|
77
|
+
- `InterviewStreamComponent` - Daily.co video call integration component
|
|
78
|
+
- `InterviewVideoComponent` - Video display component
|
|
79
|
+
- `ReviewInstructionsDialogComponent` - Dialog component for review instructions
|
|
80
|
+
- `TranslocoLazyModuleUtils`, `getAvailableLangs` - Translation utilities
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
- AI-powered interview via Daily.co video calls
|
|
85
|
+
- Candidate video recording
|
|
86
|
+
- Real-time video/audio stream handling
|
|
87
|
+
- Media device selection
|
|
88
|
+
- Translation support via Transloco
|
|
89
|
+
- Review instructions dialog
|
|
90
|
+
- Preview mode support
|
|
91
|
+
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'tgo-ai-interview-test',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
|
5
|
+
coverageDirectory: '../../coverage/packages/tgo-ai-interview-test',
|
|
6
|
+
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/*.spec.[jt]s?(x)'],
|
|
7
|
+
testPathIgnorePatterns: ['/node_modules/', '/src/lib/models/'],
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'^@testgorilla/tgo-ui$': '<rootDir>/../tgo-test-shared/src/test-mocks/tgo-ui.mock.ts',
|
|
10
|
+
},
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.(ts|mjs|js|html)$': [
|
|
13
|
+
'jest-preset-angular',
|
|
14
|
+
{
|
|
15
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
16
|
+
stringifyContentPathRegex: '\\.(html|svg)$',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
transformIgnorePatterns: [
|
|
21
|
+
'node_modules/(?!.*\\.mjs$|lodash-es|@testgorilla|ng2-charts|chart\\.js|chartjs-plugin-datalabels|@daily-co)',
|
|
22
|
+
],
|
|
23
|
+
snapshotSerializers: [
|
|
24
|
+
'jest-preset-angular/build/serializers/no-ng-attributes',
|
|
25
|
+
'jest-preset-angular/build/serializers/ng-snapshot',
|
|
26
|
+
'jest-preset-angular/build/serializers/html-comment',
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
package/ng-package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
|
3
|
+
"dest": "../../dist/packages/tgo-ai-interview-test",
|
|
4
|
+
"assets": [
|
|
5
|
+
{
|
|
6
|
+
"glob": "**/*",
|
|
7
|
+
"input": "./src/assets",
|
|
8
|
+
"output": "./assets"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"lib": {
|
|
12
|
+
"entryFile": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"allowedNonPeerDependencies": []
|
|
15
|
+
}
|
|
16
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testgorilla/tgo-ai-interview-test",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/common": "~18.2.13",
|
|
6
|
+
"@angular/core": "~18.2.13",
|
|
7
|
+
"@angular/animations": "~18.2.13",
|
|
8
|
+
"@ngneat/transloco": "~4.3.0",
|
|
9
|
+
"@testgorilla/tgo-ui": "~3.14.10",
|
|
10
|
+
"@testgorilla/tgo-test-shared": "~0.0.1",
|
|
11
|
+
"@daily-co/daily-js": "^0.79.0",
|
|
12
|
+
"jest-preset-angular": "~14.2.4",
|
|
13
|
+
"rxjs": "~7.8.1"
|
|
14
|
+
},
|
|
15
|
+
"typings": "src/lib/types",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"license": "PROPRIETARY",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "restricted",
|
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
},
|
|
22
|
+
"displayName": "AI Interview",
|
|
23
|
+
"description": "AI Interview component"
|
|
24
|
+
}
|
|
25
|
+
|
package/project.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tgo-ai-interview-test",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/tgo-ai-interview-test/src",
|
|
5
|
+
"prefix": "tgo",
|
|
6
|
+
"projectType": "library",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"targets": {
|
|
9
|
+
"build": {
|
|
10
|
+
"executor": "@nx/angular:package",
|
|
11
|
+
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
|
|
12
|
+
"options": {
|
|
13
|
+
"project": "packages/tgo-ai-interview-test/ng-package.json"
|
|
14
|
+
},
|
|
15
|
+
"configurations": {
|
|
16
|
+
"production": {
|
|
17
|
+
"tsConfig": "packages/tgo-ai-interview-test/tsconfig.lib.prod.json"
|
|
18
|
+
},
|
|
19
|
+
"development": {
|
|
20
|
+
"tsConfig": "packages/tgo-ai-interview-test/tsconfig.lib.json"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"defaultConfiguration": "production"
|
|
24
|
+
},
|
|
25
|
+
"test": {
|
|
26
|
+
"executor": "@nx/jest:jest",
|
|
27
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
28
|
+
"options": {
|
|
29
|
+
"jestConfig": "packages/tgo-ai-interview-test/jest.config.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"lint": {
|
|
33
|
+
"executor": "@nx/eslint:lint"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"TEST": {
|
|
3
|
+
"YOU": "You",
|
|
4
|
+
"RECORDING_STARTED": "Recording started",
|
|
5
|
+
"ANSWER_THE_QUESTION": "Answer the customer's question, then submit.",
|
|
6
|
+
"ANSWER_COMPLETED": "Submit answer",
|
|
7
|
+
"ANSWER": "Answer",
|
|
8
|
+
"CUSTOMER": "Customer",
|
|
9
|
+
"AUDIO_READY": "Your audio file is ready.",
|
|
10
|
+
"AUDIO_PREVIEW": "Audio is only available during practice questions.",
|
|
11
|
+
"REVIEW_INSTRUCTIONS": "Review Instructions",
|
|
12
|
+
"GET_READY": "Get ready to answer the customer's question.",
|
|
13
|
+
"TOOL_CALL": {
|
|
14
|
+
"ALL_FOR_TODAY": "That's all the questions for today. Thank you for your time.",
|
|
15
|
+
"NEXT_QUESTION": "Let's move to the next question."
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div class="ai-interview-test">
|
|
2
|
+
<div class="test-container">
|
|
3
|
+
<div
|
|
4
|
+
class="media-container"
|
|
5
|
+
[class.is-video-visible]="isInterviewInProgress()"
|
|
6
|
+
>
|
|
7
|
+
<div class="candidate-no-camera" *ngIf="!candidateVideoStreamReady()">
|
|
8
|
+
<h3> </h3>
|
|
9
|
+
<ui-icon name="User-profile-in-line" color="white" size="24"></ui-icon>
|
|
10
|
+
<h3 class="bold">{{ translations['YOU'] }}</h3>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="candidate-camera" [hidden]="!candidateVideoStreamReady()">
|
|
13
|
+
<video
|
|
14
|
+
height
|
|
15
|
+
#video
|
|
16
|
+
id="video"
|
|
17
|
+
playsinline
|
|
18
|
+
(loadedmetadata)="onVideoLoad()"
|
|
19
|
+
></video>
|
|
20
|
+
<h3 class="bold" *ngIf="candidateVideoStreamReady()">
|
|
21
|
+
{{ translations['YOU'] }}
|
|
22
|
+
</h3>
|
|
23
|
+
</div>
|
|
24
|
+
<tgo-audio-animation
|
|
25
|
+
*ngIf="isInterviewInProgress()"
|
|
26
|
+
[fakeData]="true"
|
|
27
|
+
></tgo-audio-animation>
|
|
28
|
+
<div class="interview-stream-container">
|
|
29
|
+
<tgo-interview-stream
|
|
30
|
+
*ngIf="conversationUrl && hasMediaPermissions()"
|
|
31
|
+
[selectedMediaDevices]="selectedMediaDevices"
|
|
32
|
+
[conversationUrl]="conversationUrl"
|
|
33
|
+
(streamStart)="interviewStarted()"
|
|
34
|
+
(streamEnd)="interviewEnded()"
|
|
35
|
+
(checkMediaPermissions)="checkMediaPermissions()"
|
|
36
|
+
[translations]="translations"
|
|
37
|
+
></tgo-interview-stream>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
@import '@testgorilla/tgo-ui/projects/tgo-canopy-ui/theme/variables';
|
|
2
|
+
|
|
3
|
+
$border-radius: 10px;
|
|
4
|
+
|
|
5
|
+
.ai-interview-test {
|
|
6
|
+
background-color: $black;
|
|
7
|
+
padding: 24px;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
min-height: calc(100vh - 80px);
|
|
11
|
+
|
|
12
|
+
h3 {
|
|
13
|
+
color: $tgo-white;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.test-container {
|
|
17
|
+
min-width: 360px;
|
|
18
|
+
width: 100%;
|
|
19
|
+
min-height: 300px;
|
|
20
|
+
height: min(725px, 100%, max(calc(100vw), calc(100vh), 200px));
|
|
21
|
+
max-width: calc(100vh * (16 / 9) - 80px - 48px);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
position: relative;
|
|
26
|
+
background-color: $black;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.media-container {
|
|
30
|
+
position: relative;
|
|
31
|
+
color: $tgo-white;
|
|
32
|
+
flex-grow: 1;
|
|
33
|
+
|
|
34
|
+
.interview-stream-container {
|
|
35
|
+
border-radius: $border-radius;
|
|
36
|
+
aspect-ratio: 16 / 9;
|
|
37
|
+
border: 4px solid $informative-40;
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.candidate-no-camera {
|
|
42
|
+
position: absolute;
|
|
43
|
+
width: 200px;
|
|
44
|
+
aspect-ratio: 16 / 9;
|
|
45
|
+
z-index: 3;
|
|
46
|
+
top: 32px;
|
|
47
|
+
left: 32px;
|
|
48
|
+
border-radius: $border-radius;
|
|
49
|
+
background-color: #1a47aa;
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
padding: 16px;
|
|
54
|
+
|
|
55
|
+
ui-icon {
|
|
56
|
+
margin: auto;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.candidate-camera {
|
|
61
|
+
position: absolute;
|
|
62
|
+
width: 200px;
|
|
63
|
+
top: 32px;
|
|
64
|
+
left: 32px;
|
|
65
|
+
z-index: 3;
|
|
66
|
+
|
|
67
|
+
video {
|
|
68
|
+
width: 200px;
|
|
69
|
+
border-radius: $border-radius;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h3 {
|
|
73
|
+
position: absolute;
|
|
74
|
+
bottom: 16px;
|
|
75
|
+
left: 16px;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
tgo-audio-animation {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 32px;
|
|
82
|
+
right: 32px;
|
|
83
|
+
z-index: 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tgo-vimeo-video {
|
|
87
|
+
display: none;
|
|
88
|
+
position: absolute;
|
|
89
|
+
height: 100%;
|
|
90
|
+
width: 100%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&.is-video-visible {
|
|
94
|
+
tgo-vimeo-video {
|
|
95
|
+
display: block;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
&.is-playing {
|
|
100
|
+
border: 4px solid $informative-40;
|
|
101
|
+
border-bottom-width: 3px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&.is-answering {
|
|
105
|
+
.candidate-camera,
|
|
106
|
+
.candidate-no-camera {
|
|
107
|
+
tgo-audio-animation {
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 16px;
|
|
110
|
+
right: 16px;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.candidate-camera video,
|
|
115
|
+
.candidate-no-camera {
|
|
116
|
+
border: 3px solid $informative-40;
|
|
117
|
+
border-radius: $border-radius;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
tgo-vimeo-video {
|
|
121
|
+
display: block;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.start,
|
|
126
|
+
.audio-info,
|
|
127
|
+
.overlay,
|
|
128
|
+
.answer {
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
align-items: center;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 40px;
|
|
134
|
+
height: 100%;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.preview {
|
|
138
|
+
height: 100%;
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
justify-content: flex-end;
|
|
143
|
+
padding: 24px;
|
|
144
|
+
|
|
145
|
+
p {
|
|
146
|
+
color: $grayscale-30;
|
|
147
|
+
margin: 8px 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
audio {
|
|
151
|
+
margin: 16px 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
&.hidden {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media screen and (max-width: 600px) {
|
|
162
|
+
.ai-interview-test {
|
|
163
|
+
padding: 0 24px;
|
|
164
|
+
margin-top: 16px;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
})
|
|
78
|
+
.overrideComponent(AiInterviewTestComponent, {
|
|
79
|
+
add: {
|
|
80
|
+
providers: [
|
|
81
|
+
{ provide: TranslocoService, useValue: transLocoServiceMock },
|
|
82
|
+
{ provide: MediaService, useValue: mockedMediaService },
|
|
83
|
+
{
|
|
84
|
+
provide: ChangeDetectorRef,
|
|
85
|
+
useValue: { markForCheck: jest.fn() },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
provide: ThemeService,
|
|
89
|
+
useValue: {
|
|
90
|
+
uiTheme: 'dark',
|
|
91
|
+
getCompanyColor: jest.fn().mockReturnValue('#D410AA'),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.compileComponents();
|
|
98
|
+
|
|
99
|
+
fixture = TestBed.createComponent(AiInterviewTestComponent);
|
|
100
|
+
component = fixture.componentInstance;
|
|
101
|
+
|
|
102
|
+
getMediaStreamSpy = jest.spyOn(mockedMediaService, 'getMediaStream');
|
|
103
|
+
setSelectedMediaDevicesSpy = jest.spyOn(mockedMediaService, 'setSelectedMediaDevices');
|
|
104
|
+
submissionStateChangedSpy = jest.spyOn(component.submissionStateChanged, 'emit');
|
|
105
|
+
|
|
106
|
+
loadingStateChangedSpy = jest.spyOn(component.loadingStateChanged, 'emit');
|
|
107
|
+
|
|
108
|
+
mediaAccessChanged$ = new Subject<SelectedMediaDevices>();
|
|
109
|
+
component.mediaAccessChanged = mediaAccessChanged$.asObservable();
|
|
110
|
+
hasMediaPermission = true;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
fixture.destroy();
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('when component is initialized with and with question video content', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
component.question = {
|
|
121
|
+
text: ``,
|
|
122
|
+
} as Question;
|
|
123
|
+
component.test = mockedTest;
|
|
124
|
+
component.conversationUrl = mockStreamUrl;
|
|
125
|
+
component.ngOnInit();
|
|
126
|
+
fixture.detectChanges();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should set the translations', () => {
|
|
130
|
+
expect(component.translations).toEqual(mockedTranslations);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should init stream', () => {
|
|
134
|
+
expect(getMediaStreamSpy).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return stream content url', () => {
|
|
138
|
+
expect(component.conversationUrl).toBe(mockStreamUrl);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should has candidateStreamReady = false by default', () => {
|
|
142
|
+
expect(component.candidateVideoStreamReady()).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('when video is loaded', () => {
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
component.onVideoLoad();
|
|
148
|
+
fixture.detectChanges();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should set candidateStreamReady to true', () => {
|
|
152
|
+
expect(component.candidateVideoStreamReady()).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('when mediaAccessChange is called', () => {
|
|
157
|
+
const changedAudioDeviceId = 'test_audio_changed';
|
|
158
|
+
const changedVideoDeviceId = 'test_video_changed';
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
mediaAccessChanged$.next({
|
|
161
|
+
audioDeviceId: changedAudioDeviceId,
|
|
162
|
+
videoDeviceId: changedVideoDeviceId,
|
|
163
|
+
});
|
|
164
|
+
fixture.detectChanges();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should call mediaService.setSelectedMediaDevices() method with new devices Ids', () => {
|
|
168
|
+
expect(setSelectedMediaDevicesSpy).toHaveBeenCalledWith({
|
|
169
|
+
audioDeviceId: changedAudioDeviceId,
|
|
170
|
+
videoDeviceId: changedVideoDeviceId,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should reinitialize video stream', () => {
|
|
175
|
+
expect(getMediaStreamSpy).toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('when interviewStarted is called', () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
component.interviewStarted();
|
|
182
|
+
fixture.detectChanges();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should set isInterviewInProgress to true', () => {
|
|
186
|
+
expect(component.isInterviewInProgress()).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should emit loadingStateChanged with false', () => {
|
|
190
|
+
expect(loadingStateChangedSpy).toHaveBeenCalledWith(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('when interviewEnded is called', () => {
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
component.interviewEnded();
|
|
196
|
+
fixture.detectChanges();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should set isInterviewInProgress to false', () => {
|
|
200
|
+
expect(component.isInterviewInProgress()).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should emit submissionStateChanged with empty text', () => {
|
|
204
|
+
expect(submissionStateChangedSpy).toHaveBeenCalledWith({
|
|
205
|
+
text: '',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|