@testgorilla/tgo-immersive-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.
Files changed (37) hide show
  1. package/.eslintrc.json +46 -0
  2. package/README.md +89 -0
  3. package/jest.config.ts +28 -0
  4. package/ng-package.json +16 -0
  5. package/package.json +25 -0
  6. package/project.json +37 -0
  7. package/src/assets/i18n/en.json +18 -0
  8. package/src/index.ts +4 -0
  9. package/src/lib/components/immersive-test/immersive-test.component.html +100 -0
  10. package/src/lib/components/immersive-test/immersive-test.component.scss +247 -0
  11. package/src/lib/components/immersive-test/immersive-test.component.spec.ts +581 -0
  12. package/src/lib/components/immersive-test/immersive-test.component.ts +279 -0
  13. package/src/lib/components/index.ts +6 -0
  14. package/src/lib/components/review-instructions-dialog/index.ts +1 -0
  15. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.html +39 -0
  16. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.scss +160 -0
  17. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.spec.ts +81 -0
  18. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.ts +80 -0
  19. package/src/lib/components/ringing-phone-animation/index.ts +1 -0
  20. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.html +16 -0
  21. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.scss +79 -0
  22. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.spec.ts +95 -0
  23. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.ts +54 -0
  24. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.sound.ts +2 -0
  25. package/src/lib/components/video-countdown/index.ts +1 -0
  26. package/src/lib/components/video-countdown/video-countdown.component.html +10 -0
  27. package/src/lib/components/video-countdown/video-countdown.component.scss +16 -0
  28. package/src/lib/components/video-countdown/video-countdown.component.spec.ts +59 -0
  29. package/src/lib/components/video-countdown/video-countdown.component.ts +102 -0
  30. package/src/lib/models/index.ts +9 -0
  31. package/src/lib/models/translations.ts +3 -0
  32. package/src/lib/services/index.ts +7 -0
  33. package/src/test-setup.ts +22 -0
  34. package/tsconfig.json +17 -0
  35. package/tsconfig.lib.json +15 -0
  36. package/tsconfig.lib.prod.json +10 -0
  37. package/tsconfig.spec.json +13 -0
@@ -0,0 +1,79 @@
1
+ .ringing-phone-animation-wrapper {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+
6
+ .ringing-phone-animation-container {
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: 10px;
12
+
13
+ .ringing-effect {
14
+ display: flex;
15
+ gap: 15px;
16
+ opacity: 1;
17
+ animation: blink-wrapper 1.4s infinite;
18
+ animation-delay: 0.8s;
19
+
20
+ .ringing-line {
21
+ width: 6px;
22
+ height: 16px;
23
+ background-color: white;
24
+ opacity: 0;
25
+ }
26
+
27
+ .first-line {
28
+ animation: blink-line 1.4s infinite;
29
+ animation-delay: 0.1s;
30
+ transform: rotate(-45deg);
31
+ }
32
+
33
+ .second-line {
34
+ animation: blink-line 1.4s infinite;
35
+ animation-delay: 0.3s;
36
+ transform: translate(0, -50%);
37
+ }
38
+
39
+ .third-line {
40
+ animation: blink-line 1.4s infinite;
41
+ animation-delay: 0.6s;
42
+ transform: rotate(45deg);
43
+ }
44
+ }
45
+
46
+ .answer-btn {
47
+ margin-top: 20px;
48
+ padding: 10px 20px;
49
+ background-color: white;
50
+ border: none;
51
+ border-radius: 20px;
52
+ cursor: pointer;
53
+ font-size: 16px;
54
+ }
55
+ }
56
+ }
57
+
58
+ @keyframes blink-line {
59
+ 0%,
60
+ 49% {
61
+ opacity: 0;
62
+ }
63
+ 50%,
64
+ 100% {
65
+ opacity: 1;
66
+ }
67
+ }
68
+
69
+ @keyframes blink-wrapper {
70
+ 0%,
71
+ 49% {
72
+ opacity: 1;
73
+ }
74
+ 50%,
75
+ 100% {
76
+ opacity: 0;
77
+ }
78
+ }
79
+
@@ -0,0 +1,95 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { SimpleChanges } from '@angular/core';
3
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
4
+ import { RingingPhoneAnimationComponent } from './ringing-phone-animation.component';
5
+ import SpyInstance = jest.SpyInstance;
6
+
7
+ describe('RingingPhoneAnimationComponent', () => {
8
+ let component: RingingPhoneAnimationComponent;
9
+ let fixture: ComponentFixture<RingingPhoneAnimationComponent>;
10
+
11
+ let playSpy: SpyInstance;
12
+ let pauseSpy: SpyInstance;
13
+
14
+ beforeEach(async () => {
15
+ await TestBed.configureTestingModule({
16
+ imports: [RingingPhoneAnimationComponent, CommonModule],
17
+ }).compileComponents();
18
+ });
19
+
20
+ beforeEach(() => {
21
+ fixture = TestBed.createComponent(RingingPhoneAnimationComponent);
22
+ component = fixture.componentInstance;
23
+
24
+ playSpy = jest
25
+ .spyOn(HTMLAudioElement.prototype, 'play')
26
+ .mockImplementation(() => Promise.resolve());
27
+ pauseSpy = jest.spyOn(HTMLAudioElement.prototype, 'pause').mockImplementation(() => undefined);
28
+
29
+ fixture.detectChanges();
30
+ });
31
+
32
+ it('should create the component', () => {
33
+ expect(component).toBeTruthy();
34
+ });
35
+
36
+ describe('when initializing the component', () => {
37
+ it('should initialize with default values', () => {
38
+ expect(component.isRinging).toBe(true);
39
+ expect(component.ringingSignal()).toBe(true);
40
+ });
41
+
42
+ it('should update "ringingSignal" when "isRinging" changes', () => {
43
+ const changes: SimpleChanges = {
44
+ isRinging: {
45
+ previousValue: false,
46
+ currentValue: true,
47
+ firstChange: false,
48
+ isFirstChange: () => false,
49
+ },
50
+ };
51
+
52
+ component.isRinging = false;
53
+ component.ngOnChanges(changes);
54
+
55
+ expect(component.ringingSignal()).toBe(false);
56
+ });
57
+
58
+ it('should call "playSpy" if "isRinging" changes to true', () => {
59
+ const changes: SimpleChanges = {
60
+ isRinging: {
61
+ previousValue: true,
62
+ currentValue: false,
63
+ firstChange: false,
64
+ isFirstChange: () => false,
65
+ },
66
+ };
67
+
68
+ component.isRinging = true;
69
+ component.ngOnChanges(changes);
70
+
71
+ expect(component.ringingSignal()).toBe(true);
72
+ expect(component.audio.loop).toBe(true);
73
+ expect(component.audio.volume).toBe(0.15);
74
+ expect(playSpy).toHaveBeenCalled();
75
+ });
76
+
77
+ it('should call "pauseSpy" if isRinging changes to false', () => {
78
+ const changes: SimpleChanges = {
79
+ isRinging: {
80
+ previousValue: false,
81
+ currentValue: true,
82
+ firstChange: false,
83
+ isFirstChange: () => false,
84
+ },
85
+ };
86
+
87
+ component.isRinging = false;
88
+ component.ngOnChanges(changes);
89
+
90
+ expect(component.ringingSignal()).toBe(false);
91
+ expect(component.audio.currentTime).toBe(0);
92
+ expect(pauseSpy).toHaveBeenCalled();
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,54 @@
1
+ import {
2
+ Component,
3
+ WritableSignal,
4
+ signal,
5
+ Input,
6
+ SimpleChanges,
7
+ OnChanges,
8
+ OnDestroy,
9
+ } from '@angular/core';
10
+ import { CommonModule } from '@angular/common';
11
+ import { ringingPhoneSound } from './ringing-phone-animation.sound';
12
+
13
+ @Component({
14
+ selector: 'tgo-ringing-phone-animation',
15
+ templateUrl: './ringing-phone-animation.component.html',
16
+ styleUrls: ['./ringing-phone-animation.component.scss'],
17
+ standalone: true,
18
+ imports: [CommonModule],
19
+ })
20
+ export class RingingPhoneAnimationComponent implements OnChanges, OnDestroy {
21
+ @Input() isRinging = true;
22
+
23
+ ringingSignal: WritableSignal<boolean> = signal(this.isRinging);
24
+ audio: HTMLAudioElement = new Audio(ringingPhoneSound);
25
+
26
+ ngOnChanges(changes: SimpleChanges): void {
27
+ if (changes['isRinging']) {
28
+ this.ringingSignal.set(this.isRinging);
29
+
30
+ if (this.isRinging) {
31
+ this.startRinging();
32
+ } else {
33
+ this.stopRinging();
34
+ }
35
+ }
36
+ }
37
+
38
+ ngOnDestroy(): void {
39
+ this.stopRinging();
40
+ }
41
+
42
+ private stopRinging(): void {
43
+ this.ringingSignal.set(false);
44
+ this.audio.pause();
45
+ this.audio.currentTime = 0;
46
+ }
47
+
48
+ private startRinging(): void {
49
+ this.audio.loop = true;
50
+ this.audio.volume = 0.15;
51
+ void this.audio.play();
52
+ }
53
+ }
54
+
@@ -0,0 +1,2 @@
1
+ export const ringingPhoneSound = `data:audio/mp3;base64,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`;
2
+
@@ -0,0 +1 @@
1
+ export * from './video-countdown.component';
@@ -0,0 +1,10 @@
1
+ <div class="video-countdown">
2
+ <div
3
+ class="video-countdown-element"
4
+ *ngFor="let num of [displayNum]"
5
+ @zoomInOut
6
+ >
7
+ {{ num.value }}
8
+ </div>
9
+ </div>
10
+
@@ -0,0 +1,16 @@
1
+ .video-countdown {
2
+ display: flex;
3
+ justify-content: center;
4
+
5
+ &-element {
6
+ position: absolute;
7
+ font-size: 80px;
8
+ font-weight: 700;
9
+ line-height: 1;
10
+ -webkit-user-select: none;
11
+ user-select: none;
12
+ opacity: 1;
13
+ backface-visibility: hidden;
14
+ }
15
+ }
16
+
@@ -0,0 +1,59 @@
1
+ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+ import { VideoCountdownComponent } from './video-countdown.component';
4
+
5
+ describe('VideoCountdownComponent', () => {
6
+ let component: VideoCountdownComponent;
7
+ let fixture: ComponentFixture<VideoCountdownComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [VideoCountdownComponent, NoopAnimationsModule],
12
+ }).compileComponents();
13
+ });
14
+
15
+ beforeEach(() => {
16
+ fixture = TestBed.createComponent(VideoCountdownComponent);
17
+ component = fixture.componentInstance;
18
+
19
+ fixture.detectChanges();
20
+ });
21
+
22
+ it('should create', () => {
23
+ expect(component).toBeTruthy();
24
+ });
25
+
26
+ it('should start counting on component init', () => {
27
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
28
+ component.ngOnInit();
29
+ expect(setTimeoutSpy).toHaveBeenCalled();
30
+ });
31
+
32
+ it('should set isActive to true on start', () => {
33
+ component.start();
34
+ expect(component.isActive).toBe(true);
35
+ });
36
+
37
+ it('should count down from startFrom to endAt', (done) => {
38
+ component.startFrom = 3;
39
+ component.endAt = 0;
40
+ component.interval = 100;
41
+ const finishSpy = jest.spyOn(component.finish, 'emit');
42
+
43
+ component.start();
44
+
45
+ setTimeout(() => {
46
+ expect(component.displayNum.value).toBe(0);
47
+ expect(finishSpy).toHaveBeenCalled();
48
+ expect(component.isActive).toBe(false);
49
+ done();
50
+ }, 1000);
51
+ });
52
+
53
+ it('should clean up on ngOnDestroy', () => {
54
+ const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
55
+ component.ngOnDestroy();
56
+ expect(clearTimeoutSpy).toHaveBeenCalledWith(component.timeoutId);
57
+ });
58
+ });
59
+
@@ -0,0 +1,102 @@
1
+ import { animate, style, transition, trigger } from '@angular/animations';
2
+ import { CommonModule } from '@angular/common';
3
+ import {
4
+ ChangeDetectionStrategy,
5
+ ChangeDetectorRef,
6
+ Component,
7
+ EventEmitter,
8
+ Input,
9
+ OnDestroy,
10
+ OnInit,
11
+ Output,
12
+ } from '@angular/core';
13
+
14
+ @Component({
15
+ selector: 'tgo-video-countdown',
16
+ templateUrl: './video-countdown.component.html',
17
+ styleUrls: ['./video-countdown.component.scss'],
18
+ animations: [
19
+ trigger('zoomInOut', [
20
+ transition(':enter', [
21
+ style({ transform: 'scale(0)', opacity: 0 }),
22
+ animate('300ms ease-out', style({ transform: 'scale(1.2)', opacity: 1 })),
23
+ animate('100ms ease-in', style({ transform: 'scale(1)' })),
24
+ ]),
25
+ transition(':leave', [
26
+ style({ opacity: 1 }),
27
+ animate('100ms ease-out', style({ transform: 'scale(1.2)', opacity: 0.5 })),
28
+ animate('200ms ease-in', style({ transform: 'scale(0)', opacity: 0 })),
29
+ ]),
30
+ ]),
31
+ ],
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ standalone: true,
34
+ imports: [CommonModule],
35
+ })
36
+ export class VideoCountdownComponent implements OnInit, OnDestroy {
37
+ @Input() startFrom = 5;
38
+ @Input() endAt = 0;
39
+ @Input() interval = 1325;
40
+ @Output() finish: EventEmitter<void> = new EventEmitter<void>();
41
+
42
+ isActive = false;
43
+ displayNum: { key: number; value: number } = {
44
+ key: this.startFrom,
45
+ value: this.startFrom,
46
+ };
47
+ timeoutId: ReturnType<typeof setTimeout> | null = null;
48
+
49
+ constructor(private cdr: ChangeDetectorRef) {}
50
+
51
+ ngOnInit(): void {
52
+ this.start();
53
+ }
54
+
55
+ start() {
56
+ this.isActive = true;
57
+ this.displayNum = { key: this.startFrom, value: this.startFrom };
58
+ this.scheduleAnimation();
59
+ }
60
+
61
+ ngOnDestroy(): void {
62
+ this.isActive = false;
63
+ if (this.timeoutId) {
64
+ clearTimeout(this.timeoutId);
65
+ }
66
+ }
67
+
68
+ private scheduleAnimation() {
69
+ this.timeoutId = setTimeout(() => {
70
+ this.anim();
71
+ }, this.interval);
72
+ }
73
+
74
+ private anim() {
75
+ if (!this.isActive) {
76
+ return;
77
+ }
78
+
79
+ this.updateNumber();
80
+ this.cdr.markForCheck();
81
+
82
+ if (this.displayNum && this.displayNum.value === this.endAt) {
83
+ this.finish.emit();
84
+ this.isActive = false;
85
+ return;
86
+ }
87
+
88
+ this.scheduleAnimation();
89
+ }
90
+
91
+ private updateNumber() {
92
+ if (this.displayNum && this.displayNum.value === this.endAt) {
93
+ this.displayNum = { key: this.startFrom, value: this.startFrom - 1 };
94
+ } else {
95
+ this.displayNum = {
96
+ key: this.displayNum.value - 1,
97
+ value: this.displayNum.value - 1,
98
+ };
99
+ }
100
+ }
101
+ }
102
+
@@ -0,0 +1,9 @@
1
+ // Re-export shared models
2
+ export {
3
+ Question,
4
+ TestResultRead,
5
+ SelectedMediaDevices,
6
+ ISubmissionState,
7
+ IQuestionDataContract,
8
+ } from '@testgorilla/tgo-test-shared';
9
+ export * from './translations';
@@ -0,0 +1,3 @@
1
+ // Re-export from shared
2
+ export { ROOT_TRANSLATIONS_SCOPE } from '@testgorilla/tgo-test-shared';
3
+
@@ -0,0 +1,7 @@
1
+ // Re-export shared services
2
+ export {
3
+ MediaService,
4
+ ThemeService,
5
+ TranslocoLazyModuleUtils,
6
+ getAvailableLangs,
7
+ } from '@testgorilla/tgo-test-shared';
@@ -0,0 +1,22 @@
1
+ import 'jest-preset-angular/setup-jest';
2
+
3
+ // Mock HTMLMediaElement.play() for jsdom
4
+ Object.defineProperty(HTMLMediaElement.prototype, 'play', {
5
+ writable: true,
6
+ value: jest.fn().mockResolvedValue(undefined),
7
+ });
8
+
9
+ // Mock @vimeo/player
10
+ jest.mock('@vimeo/player', () => {
11
+ return {
12
+ default: jest.fn().mockImplementation(() => ({
13
+ on: jest.fn(),
14
+ off: jest.fn(),
15
+ loadVideo: jest.fn(),
16
+ play: jest.fn(),
17
+ pause: jest.fn(),
18
+ destroy: jest.fn(),
19
+ })),
20
+ };
21
+ });
22
+
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022"
4
+ },
5
+ "files": [],
6
+ "include": [],
7
+ "references": [
8
+ {
9
+ "path": "./tsconfig.lib.json"
10
+ },
11
+ {
12
+ "path": "./tsconfig.spec.json"
13
+ }
14
+ ],
15
+ "extends": "../../tsconfig.base.json"
16
+ }
17
+
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "inlineSources": true,
8
+ "types": [
9
+ "node",
10
+ ]
11
+ },
12
+ "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"],
13
+ "include": ["src/**/*.ts"]
14
+ }
15
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.lib.json",
3
+ "compilerOptions": {
4
+ "declarationMap": false
5
+ },
6
+ "angularCompilerOptions": {
7
+ "compilationMode": "partial"
8
+ }
9
+ }
10
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "target": "es2016",
7
+ "types": ["jest", "node"]
8
+ },
9
+ "files": ["src/test-setup.ts"],
10
+ "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"],
11
+ "exclude": ["src/lib/models/test.ts"]
12
+ }
13
+