@sneat/core 0.1.0
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/eslint.config.js +7 -0
- package/ng-package.json +7 -0
- package/package.json +14 -0
- package/project.json +38 -0
- package/src/index.ts +1 -0
- package/src/lib/analytics.interface.ts +34 -0
- package/src/lib/animations/form-animations.spec.ts +26 -0
- package/src/lib/animations/form-animations.ts +11 -0
- package/src/lib/animations/index.ts +2 -0
- package/src/lib/animations/list-animations.spec.ts +50 -0
- package/src/lib/animations/list-animations.ts +44 -0
- package/src/lib/app.service.ts +33 -0
- package/src/lib/constants.spec.ts +20 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/core-models.ts +12 -0
- package/src/lib/directives/index.ts +1 -0
- package/src/lib/directives/sneat-select-all-on-focus.directive.spec.ts +142 -0
- package/src/lib/directives/sneat-select-all-on-focus.directive.ts +36 -0
- package/src/lib/environment-config.ts +54 -0
- package/src/lib/eq.spec.ts +24 -0
- package/src/lib/eq.ts +1 -0
- package/src/lib/exclude-undefined.spec.ts +165 -0
- package/src/lib/exclude-undefined.ts +47 -0
- package/src/lib/form-field.ts +5 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/interfaces.spec.ts +116 -0
- package/src/lib/interfaces.ts +85 -0
- package/src/lib/location-href.spec.ts +53 -0
- package/src/lib/location-href.ts +9 -0
- package/src/lib/logging/interfaces.ts +19 -0
- package/src/lib/logging.spec.ts +132 -0
- package/src/lib/logging.ts +33 -0
- package/src/lib/nav/index.ts +2 -0
- package/src/lib/nav/nav-context.ts +16 -0
- package/src/lib/nav/routing-state.spec.ts +65 -0
- package/src/lib/nav/routing-state.ts +26 -0
- package/src/lib/services/index.ts +3 -0
- package/src/lib/services/ng-module-preloader.service.spec.ts +72 -0
- package/src/lib/services/ng-module-preloader.service.ts +125 -0
- package/src/lib/services/sneat-nav.service.spec.ts +95 -0
- package/src/lib/services/sneat-nav.service.ts +46 -0
- package/src/lib/services/top-menu.service.spec.ts +42 -0
- package/src/lib/services/top-menu.service.ts +19 -0
- package/src/lib/sneat-enum-keys.ts +2 -0
- package/src/lib/sneat-extensions.spec.ts +127 -0
- package/src/lib/sneat-extensions.ts +49 -0
- package/src/lib/store.spec.ts +156 -0
- package/src/lib/store.ts +54 -0
- package/src/lib/team-type.spec.ts +8 -0
- package/src/lib/team-type.ts +13 -0
- package/src/lib/testing/base-test-setup.ts +247 -0
- package/src/lib/testing/test-setup-light.ts +1 -0
- package/src/lib/testing/test-setup.ts +70 -0
- package/src/lib/types/age-group.ts +1 -0
- package/src/lib/types/gender.spec.ts +42 -0
- package/src/lib/types/gender.ts +12 -0
- package/src/lib/types/index.ts +2 -0
- package/src/lib/utils/datetimes.spec.ts +144 -0
- package/src/lib/utils/datetimes.ts +51 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- package/vite.config.mts +10 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IIdAndOptionalBriefAndOptionalDbo } from '../interfaces';
|
|
2
|
+
|
|
3
|
+
export type SpaceItem = 'happening' | 'contact' | 'document' | 'asset' | 'list';
|
|
4
|
+
|
|
5
|
+
export type DeleteOperationState = 'deleting' | 'deleted' | undefined;
|
|
6
|
+
|
|
7
|
+
export type INavContext<
|
|
8
|
+
Brief,
|
|
9
|
+
Dbo extends Brief,
|
|
10
|
+
> = IIdAndOptionalBriefAndOptionalDbo<Brief, Dbo>;
|
|
11
|
+
|
|
12
|
+
// export interface INavContext<Brief, Dto> {
|
|
13
|
+
// readonly id: string;
|
|
14
|
+
// readonly brief?: Brief | null;
|
|
15
|
+
// readonly dto?: Dto | null;
|
|
16
|
+
// }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { NavigationEnd, Router, Event } from '@angular/router';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
import { RoutingState } from './routing-state';
|
|
5
|
+
|
|
6
|
+
describe('RoutingState', () => {
|
|
7
|
+
let eventsSubject: Subject<Event>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
eventsSubject = new Subject();
|
|
11
|
+
TestBed.configureTestingModule({
|
|
12
|
+
providers: [
|
|
13
|
+
RoutingState,
|
|
14
|
+
{
|
|
15
|
+
provide: Router,
|
|
16
|
+
useValue: { events: eventsSubject },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should be created', () => {
|
|
23
|
+
const service = TestBed.inject(RoutingState);
|
|
24
|
+
expect(service).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false when no history exists', () => {
|
|
28
|
+
const service = TestBed.inject(RoutingState);
|
|
29
|
+
expect(service.hasHistory()).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return false after one navigation', () => {
|
|
33
|
+
const service = TestBed.inject(RoutingState);
|
|
34
|
+
const navEnd = new NavigationEnd(1, '/home', '/home');
|
|
35
|
+
eventsSubject.next(navEnd);
|
|
36
|
+
expect(service.hasHistory()).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return true after two navigations', () => {
|
|
40
|
+
const service = TestBed.inject(RoutingState);
|
|
41
|
+
|
|
42
|
+
const navEnd1 = new NavigationEnd(1, '/home', '/home');
|
|
43
|
+
eventsSubject.next(navEnd1);
|
|
44
|
+
|
|
45
|
+
const navEnd2 = new NavigationEnd(2, '/about', '/about');
|
|
46
|
+
eventsSubject.next(navEnd2);
|
|
47
|
+
|
|
48
|
+
expect(service.hasHistory()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle multiple navigations', () => {
|
|
52
|
+
const service = TestBed.inject(RoutingState);
|
|
53
|
+
|
|
54
|
+
const navEnd1 = new NavigationEnd(1, '/home', '/home');
|
|
55
|
+
eventsSubject.next(navEnd1);
|
|
56
|
+
|
|
57
|
+
const navEnd2 = new NavigationEnd(2, '/about', '/about');
|
|
58
|
+
eventsSubject.next(navEnd2);
|
|
59
|
+
|
|
60
|
+
const navEnd3 = new NavigationEnd(3, '/contact', '/contact');
|
|
61
|
+
eventsSubject.next(navEnd3);
|
|
62
|
+
|
|
63
|
+
expect(service.hasHistory()).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Injectable, inject } from '@angular/core';
|
|
2
|
+
import { NavigationEnd, Router } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
@Injectable({ providedIn: 'root' })
|
|
5
|
+
export class RoutingState {
|
|
6
|
+
private history: string[] = [];
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
const router = inject(Router);
|
|
10
|
+
|
|
11
|
+
router.events.subscribe({
|
|
12
|
+
next: (event) => {
|
|
13
|
+
if (event instanceof NavigationEnd) {
|
|
14
|
+
this.history = [...this.history, event.urlAfterRedirects];
|
|
15
|
+
if (this.history.length > 2) {
|
|
16
|
+
this.history.slice(this.history.length - 2, this.history.length);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public hasHistory(): boolean {
|
|
24
|
+
return this.history.length > 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
// import { describe, beforeEach, it, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { NgModulePreloaderService } from './ng-module-preloader.service';
|
|
5
|
+
|
|
6
|
+
describe('NgModulePreloaderService', () => {
|
|
7
|
+
beforeEach(() => TestBed.configureTestingModule({}));
|
|
8
|
+
|
|
9
|
+
it('should be created', () => {
|
|
10
|
+
const service: NgModulePreloaderService = TestBed.inject(
|
|
11
|
+
NgModulePreloaderService,
|
|
12
|
+
);
|
|
13
|
+
expect(service).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should add preload configs', () => {
|
|
17
|
+
const service: NgModulePreloaderService = TestBed.inject(
|
|
18
|
+
NgModulePreloaderService,
|
|
19
|
+
);
|
|
20
|
+
class MockModule {}
|
|
21
|
+
service.addPreloadConfigs({
|
|
22
|
+
id: 'test',
|
|
23
|
+
path: 'path/to/test',
|
|
24
|
+
type: MockModule,
|
|
25
|
+
});
|
|
26
|
+
// @ts-expect-error accessing private property for testing
|
|
27
|
+
expect(service.configs['test']).toEqual({
|
|
28
|
+
path: 'path/to/test',
|
|
29
|
+
type: MockModule,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should mark as preloaded', () => {
|
|
34
|
+
const service: NgModulePreloaderService = TestBed.inject(
|
|
35
|
+
NgModulePreloaderService,
|
|
36
|
+
);
|
|
37
|
+
service.markAsPreloaded('test-path');
|
|
38
|
+
// @ts-expect-error accessing private property for testing
|
|
39
|
+
expect(service.preloaded).toContain('test-path');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should warn when preload is called', () => {
|
|
43
|
+
const service: NgModulePreloaderService = TestBed.inject(
|
|
44
|
+
NgModulePreloaderService,
|
|
45
|
+
);
|
|
46
|
+
const warnSpy = vi
|
|
47
|
+
.spyOn(console, 'warn')
|
|
48
|
+
.mockImplementation(() => undefined);
|
|
49
|
+
service.preload(['path1']);
|
|
50
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
51
|
+
expect.stringContaining('Preloading is disabled'),
|
|
52
|
+
);
|
|
53
|
+
warnSpy.mockRestore();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should only warn once when preload is called multiple times', () => {
|
|
57
|
+
const service: NgModulePreloaderService = TestBed.inject(
|
|
58
|
+
NgModulePreloaderService,
|
|
59
|
+
);
|
|
60
|
+
const warnSpy = vi
|
|
61
|
+
.spyOn(console, 'warn')
|
|
62
|
+
.mockImplementation(() => undefined);
|
|
63
|
+
|
|
64
|
+
service.preload(['path1']);
|
|
65
|
+
service.preload(['path2']);
|
|
66
|
+
service.preload(['path3']);
|
|
67
|
+
|
|
68
|
+
// Should only warn once
|
|
69
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
70
|
+
warnSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Injectable, Type } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type PreloadPages =
|
|
4
|
+
| 'assets'
|
|
5
|
+
| 'contacts'
|
|
6
|
+
| 'members'
|
|
7
|
+
| 'real-estates'
|
|
8
|
+
| 'budget';
|
|
9
|
+
|
|
10
|
+
@Injectable({
|
|
11
|
+
providedIn: 'root',
|
|
12
|
+
})
|
|
13
|
+
export class NgModulePreloaderService {
|
|
14
|
+
private readonly preloaded: string[] = [];
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
'applicants': './pages/commune/contact/contacts/commune-contacts.page.module#CommuneContactsPageModule',
|
|
18
|
+
'assets': './pages/commune/asset/assets/assets.module#AssetsPageModule',
|
|
19
|
+
'commune-overview': './pages/commune/overview/commune-overview.module#CommuneOverviewPageModule',
|
|
20
|
+
'bills': '',
|
|
21
|
+
'budget': './pages/commune/budget/budget.module#BudgetPageModule',
|
|
22
|
+
'staff': './pages/commune/members/staff/staff.module#StaffPageModule',
|
|
23
|
+
'member': './pages/commune/member/member/commune-member.module#CommuneMemberPageModule',
|
|
24
|
+
'member-new': './pages/commune/member/member-new/member-new.module#MemberNewPageModule',
|
|
25
|
+
'members': './pages/commune/members/members/commune-members.module#CommuneMembersPageModule',
|
|
26
|
+
'group': './pages/commune/member-group/member-group/member-group.module#MemberGrpupPageModule',
|
|
27
|
+
'group-new': './pages/commune/member-group/member-group-new/member-group-new.module#MemberGroupNewPageModule',
|
|
28
|
+
'groups': './pages/commune/member-group/member-groups/member-groups.module#MemberGroupsPageModule',
|
|
29
|
+
'contact': './pages/commune/contact/contact/commune-contact.module#CommuneContactPageModule',
|
|
30
|
+
'contact-new': './pages/commune/contact/contact-new/contact-new.module#ContactNewPageModule',
|
|
31
|
+
'contacts': './pages/commune/contact/contacts/commune-contacts.page.module#CommuneContactsPageModule',
|
|
32
|
+
'document': './pages/commune/document/document/commune-document.module#CommuneDocumentPageModule',
|
|
33
|
+
'document-new': './pages/commune/document/document-new/document-new.module#DocumentNewPageModule',
|
|
34
|
+
'documents': './pages/commune/document/documents/commune-documents.module#CommuneDocumentsPageModule',
|
|
35
|
+
'landlords': './pages/commune/contact/contacts/commune-contacts.page.module#CommuneContactsPageModule',
|
|
36
|
+
'lists': './shared/listus/commune/list/lists/lists.module#ListsPageModule',
|
|
37
|
+
'pupils': './pages/commune/members/pupils/pupils.module#PupilsPageModule',
|
|
38
|
+
'real-estates': './pages/commune/asset/real-estate/real-estate.module#RealEstatePageModule',
|
|
39
|
+
'schedule': './pages/commune/calendar/schedule/schedule-page.module#SchedulePageModule',
|
|
40
|
+
'tenants': './pages/commune/contact/contacts/commune-contacts.page.module#CommuneContactsPageModule',
|
|
41
|
+
'terms': './pages/commune/term/terms/terms.module#TermsPageModule',
|
|
42
|
+
'tasks': './pages/commune/todo/tasks/tasks-page.module#TasksPageModule',
|
|
43
|
+
*/
|
|
44
|
+
private readonly configs: Record<
|
|
45
|
+
string,
|
|
46
|
+
{ path: string; type: Type<unknown> }
|
|
47
|
+
> = {}; // Use addPreloadConfigs
|
|
48
|
+
|
|
49
|
+
private warned = false;
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* https://blog.angularindepth.com/as-busy-as-a-bee-lazy-loading-in-the-angular-cli-d2812141637f
|
|
53
|
+
* https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e
|
|
54
|
+
*/
|
|
55
|
+
// constructor(
|
|
56
|
+
// // private readonly injector: Injector,
|
|
57
|
+
// // private loader: NgModuleFactoryLoader,
|
|
58
|
+
// ) {
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
public addPreloadConfigs(
|
|
62
|
+
...configs: {
|
|
63
|
+
id: string;
|
|
64
|
+
path: string;
|
|
65
|
+
type: Type<unknown>;
|
|
66
|
+
module?: string;
|
|
67
|
+
}[]
|
|
68
|
+
): void {
|
|
69
|
+
configs.forEach((config) => {
|
|
70
|
+
this.configs[config.id] = { path: config.path, type: config.type };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public markAsPreloaded(path: string): void {
|
|
75
|
+
this.preloaded.push(path);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public preload(paths: string[], ms = 1000): void {
|
|
79
|
+
if (!this.warned) {
|
|
80
|
+
this.warned = true;
|
|
81
|
+
console.warn(
|
|
82
|
+
`Preloading is disabled until migrated to Ivy (delay=${ms}ms)`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return; // TODO: Enable preloading once migrated to Angular Ivy
|
|
86
|
+
// setTimeout(
|
|
87
|
+
// () => {
|
|
88
|
+
// paths = paths.filter(p => !this.preloaded.includes(p));
|
|
89
|
+
// if (!paths.length) {
|
|
90
|
+
// return;
|
|
91
|
+
// }
|
|
92
|
+
// console.log('preloading:', paths);
|
|
93
|
+
// paths.forEach(p => {
|
|
94
|
+
// if (this.preloaded.includes(p)) {
|
|
95
|
+
// return;
|
|
96
|
+
// }
|
|
97
|
+
// const config = this.configs[p];
|
|
98
|
+
// if (!config) {
|
|
99
|
+
// console.error('Unknown preload id:', p);
|
|
100
|
+
// return;
|
|
101
|
+
// }
|
|
102
|
+
// this.loader.load(config.path)
|
|
103
|
+
// .then((factory) => {
|
|
104
|
+
// console.log('preloaded:', factory);
|
|
105
|
+
// this.preloaded.push(p);
|
|
106
|
+
// if (config.type) {
|
|
107
|
+
// try {
|
|
108
|
+
// const module = factory.create(this.injector);
|
|
109
|
+
// const cf = module.componentFactoryResolver.resolveComponentFactory(config.type);
|
|
110
|
+
// const cr = cf.create(this.injector);
|
|
111
|
+
// cr.destroy();
|
|
112
|
+
// } catch (e) {
|
|
113
|
+
// console.error(`Failed to create or destroy preloaded component ${config.path}:`, e);
|
|
114
|
+
// }
|
|
115
|
+
// }
|
|
116
|
+
// })
|
|
117
|
+
// .catch(err => {
|
|
118
|
+
// console.error('Failed to preload NG module:', err);
|
|
119
|
+
// });
|
|
120
|
+
// });
|
|
121
|
+
// },
|
|
122
|
+
// ms,
|
|
123
|
+
// );
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { NavigationEnd, Router, Event } from '@angular/router';
|
|
3
|
+
import { Location } from '@angular/common';
|
|
4
|
+
import { NavController } from '@ionic/angular/standalone';
|
|
5
|
+
import { Subject } from 'rxjs';
|
|
6
|
+
import { SneatNavService } from './sneat-nav.service';
|
|
7
|
+
|
|
8
|
+
describe('SneatNavService', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
TestBed.configureTestingModule({
|
|
11
|
+
providers: [
|
|
12
|
+
SneatNavService,
|
|
13
|
+
{
|
|
14
|
+
provide: Router,
|
|
15
|
+
useValue: { events: new Subject() },
|
|
16
|
+
},
|
|
17
|
+
{ provide: Location, useValue: { back: vi.fn() } },
|
|
18
|
+
{ provide: NavController, useValue: { pop: vi.fn() } },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should be created', () => {
|
|
24
|
+
expect(TestBed.inject(SneatNavService)).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should navigateByUrl when no previous page exists', () => {
|
|
28
|
+
const service = TestBed.inject(SneatNavService);
|
|
29
|
+
const router = TestBed.inject(Router);
|
|
30
|
+
router.navigateByUrl = vi.fn().mockReturnValue(Promise.resolve(true));
|
|
31
|
+
|
|
32
|
+
service.goBack('/home');
|
|
33
|
+
|
|
34
|
+
expect(router.navigateByUrl).toHaveBeenCalledWith('/home', undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call navController.pop when previous page exists', async () => {
|
|
38
|
+
const router = TestBed.inject(Router);
|
|
39
|
+
const eventsSubject = router.events as Subject<Event>;
|
|
40
|
+
const service = TestBed.inject(SneatNavService);
|
|
41
|
+
|
|
42
|
+
// Simulate navigation end
|
|
43
|
+
const navEnd = new NavigationEnd(1, '/prev', '/prev');
|
|
44
|
+
eventsSubject.next(navEnd);
|
|
45
|
+
|
|
46
|
+
const navController = TestBed.inject(NavController);
|
|
47
|
+
navController.pop = vi.fn().mockReturnValue(Promise.resolve(true));
|
|
48
|
+
|
|
49
|
+
service.goBack('/home');
|
|
50
|
+
|
|
51
|
+
expect(navController.pop).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should call location.back when navController.pop fails', async () => {
|
|
55
|
+
const router = TestBed.inject(Router);
|
|
56
|
+
const eventsSubject = router.events as Subject<Event>;
|
|
57
|
+
const service = TestBed.inject(SneatNavService);
|
|
58
|
+
|
|
59
|
+
// Simulate navigation end
|
|
60
|
+
const navEnd = new NavigationEnd(1, '/prev', '/prev');
|
|
61
|
+
eventsSubject.next(navEnd);
|
|
62
|
+
|
|
63
|
+
const navController = TestBed.inject(NavController);
|
|
64
|
+
navController.pop = vi.fn().mockReturnValue(Promise.resolve(false));
|
|
65
|
+
const location = TestBed.inject(Location);
|
|
66
|
+
|
|
67
|
+
service.goBack('/home');
|
|
68
|
+
|
|
69
|
+
// Wait for promise
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
71
|
+
|
|
72
|
+
expect(location.back).toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle navigation error when no previous page exists', async () => {
|
|
76
|
+
const service = TestBed.inject(SneatNavService);
|
|
77
|
+
const router = TestBed.inject(Router);
|
|
78
|
+
const consoleErrorSpy = vi
|
|
79
|
+
.spyOn(console, 'error')
|
|
80
|
+
.mockImplementation(() => undefined);
|
|
81
|
+
const error = new Error('Navigation failed');
|
|
82
|
+
router.navigateByUrl = vi.fn().mockReturnValue(Promise.reject(error));
|
|
83
|
+
|
|
84
|
+
service.goBack('/home');
|
|
85
|
+
|
|
86
|
+
// Wait for promise to reject
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
88
|
+
|
|
89
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
90
|
+
'SneatNavService.goBack() - failed to navigate',
|
|
91
|
+
error,
|
|
92
|
+
);
|
|
93
|
+
consoleErrorSpy.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Location } from '@angular/common';
|
|
2
|
+
import { Injectable, inject } from '@angular/core';
|
|
3
|
+
import {
|
|
4
|
+
NavigationBehaviorOptions,
|
|
5
|
+
NavigationEnd,
|
|
6
|
+
Router,
|
|
7
|
+
UrlTree,
|
|
8
|
+
} from '@angular/router';
|
|
9
|
+
import { NavController } from '@ionic/angular/standalone';
|
|
10
|
+
|
|
11
|
+
@Injectable({
|
|
12
|
+
providedIn: 'root',
|
|
13
|
+
})
|
|
14
|
+
export class SneatNavService {
|
|
15
|
+
private readonly router = inject(Router);
|
|
16
|
+
private readonly location = inject(Location);
|
|
17
|
+
private readonly navController = inject(NavController);
|
|
18
|
+
|
|
19
|
+
private previous?: NavigationEnd;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
const router = this.router;
|
|
23
|
+
|
|
24
|
+
router.events.subscribe((event) => {
|
|
25
|
+
if (event instanceof NavigationEnd) {
|
|
26
|
+
this.previous = event;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
goBack(url: string | UrlTree, extras?: NavigationBehaviorOptions): void {
|
|
32
|
+
if (this.previous) {
|
|
33
|
+
this.navController.pop().then((isPopped) => {
|
|
34
|
+
if (!isPopped) {
|
|
35
|
+
this.location.back();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
this.router
|
|
40
|
+
.navigateByUrl(url, extras)
|
|
41
|
+
.catch((err) =>
|
|
42
|
+
console.error('SneatNavService.goBack() - failed to navigate', err),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { TopMenuService } from './top-menu.service';
|
|
4
|
+
|
|
5
|
+
describe('TopMenuService', () => {
|
|
6
|
+
let service: TopMenuService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({});
|
|
10
|
+
service = TestBed.inject(TopMenuService);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should be created', () => {
|
|
14
|
+
expect(service).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should update visibility when visibilityChanged is called', () => {
|
|
18
|
+
const mockEvent = {
|
|
19
|
+
detail: { visible: true },
|
|
20
|
+
} as unknown as CustomEvent;
|
|
21
|
+
|
|
22
|
+
let isVisible: boolean | undefined;
|
|
23
|
+
let isHidden: boolean | undefined;
|
|
24
|
+
|
|
25
|
+
service.isTopMenuVisible.subscribe((v) => (isVisible = v));
|
|
26
|
+
service.isTopMenuHidden.subscribe((v) => (isHidden = v));
|
|
27
|
+
|
|
28
|
+
service.visibilityChanged(mockEvent);
|
|
29
|
+
|
|
30
|
+
expect(isVisible).toBe(true);
|
|
31
|
+
expect(isHidden).toBe(false);
|
|
32
|
+
|
|
33
|
+
const mockEventHidden = {
|
|
34
|
+
detail: { visible: false },
|
|
35
|
+
} as unknown as CustomEvent;
|
|
36
|
+
|
|
37
|
+
service.visibilityChanged(mockEventHidden);
|
|
38
|
+
|
|
39
|
+
expect(isVisible).toBe(false);
|
|
40
|
+
expect(isHidden).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
@Injectable({
|
|
5
|
+
providedIn: 'root',
|
|
6
|
+
})
|
|
7
|
+
export class TopMenuService {
|
|
8
|
+
private $isTopMenuVisible = new BehaviorSubject<boolean>(false);
|
|
9
|
+
private $isTopMenuHidden = new BehaviorSubject<boolean>(true);
|
|
10
|
+
|
|
11
|
+
public readonly isTopMenuVisible = this.$isTopMenuVisible.asObservable();
|
|
12
|
+
public readonly isTopMenuHidden = this.$isTopMenuHidden.asObservable();
|
|
13
|
+
|
|
14
|
+
public readonly visibilityChanged = (event: Event): void => {
|
|
15
|
+
const visible = !!(event as CustomEvent).detail['visible'];
|
|
16
|
+
this.$isTopMenuVisible.next(visible);
|
|
17
|
+
this.$isTopMenuHidden.next(!visible);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ISneatExtension,
|
|
4
|
+
defaultFamilyExtension,
|
|
5
|
+
defaultFamilyMemberExtensions,
|
|
6
|
+
} from './sneat-extensions';
|
|
7
|
+
|
|
8
|
+
describe('Sneat Extensions', () => {
|
|
9
|
+
describe('defaultFamilyExtension', () => {
|
|
10
|
+
it('should be an array', () => {
|
|
11
|
+
expect(Array.isArray(defaultFamilyExtension)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should have 4 extensions', () => {
|
|
15
|
+
expect(defaultFamilyExtension).toHaveLength(4);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should contain assets extension', () => {
|
|
19
|
+
const assets = defaultFamilyExtension.find((ext) => ext.id === 'assets');
|
|
20
|
+
expect(assets).toBeDefined();
|
|
21
|
+
expect(assets?.title).toBe('Assets');
|
|
22
|
+
expect(assets?.emoji).toBe('🏡');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should contain calendarium extension', () => {
|
|
26
|
+
const calendarium = defaultFamilyExtension.find(
|
|
27
|
+
(ext) => ext.id === 'calendarium',
|
|
28
|
+
);
|
|
29
|
+
expect(calendarium).toBeDefined();
|
|
30
|
+
expect(calendarium?.title).toBe('Calendar');
|
|
31
|
+
expect(calendarium?.emoji).toBe('🗓️');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should contain documents extension', () => {
|
|
35
|
+
const documents = defaultFamilyExtension.find(
|
|
36
|
+
(ext) => ext.id === 'documents',
|
|
37
|
+
);
|
|
38
|
+
expect(documents).toBeDefined();
|
|
39
|
+
expect(documents?.title).toBe('Documents');
|
|
40
|
+
expect(documents?.emoji).toBe('📄');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should contain sizes extension', () => {
|
|
44
|
+
const sizes = defaultFamilyExtension.find((ext) => ext.id === 'sizes');
|
|
45
|
+
expect(sizes).toBeDefined();
|
|
46
|
+
expect(sizes?.title).toBe('Sizes');
|
|
47
|
+
expect(sizes?.emoji).toBe('📏');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have all valid ISneatExtension objects', () => {
|
|
51
|
+
defaultFamilyExtension.forEach((ext: ISneatExtension) => {
|
|
52
|
+
expect(ext.id).toBeDefined();
|
|
53
|
+
expect(typeof ext.id).toBe('string');
|
|
54
|
+
expect(ext.title).toBeDefined();
|
|
55
|
+
expect(typeof ext.title).toBe('string');
|
|
56
|
+
expect(ext.emoji).toBeDefined();
|
|
57
|
+
expect(typeof ext.emoji).toBe('string');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('defaultFamilyMemberExtensions', () => {
|
|
63
|
+
it('should be an array', () => {
|
|
64
|
+
expect(Array.isArray(defaultFamilyMemberExtensions)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should have 4 extensions', () => {
|
|
68
|
+
expect(defaultFamilyMemberExtensions).toHaveLength(4);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should contain assets extension', () => {
|
|
72
|
+
const assets = defaultFamilyMemberExtensions.find(
|
|
73
|
+
(ext) => ext.id === 'assets',
|
|
74
|
+
);
|
|
75
|
+
expect(assets).toBeDefined();
|
|
76
|
+
expect(assets?.title).toBe('Assets');
|
|
77
|
+
expect(assets?.emoji).toBe('🏡');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should contain calendarium extension', () => {
|
|
81
|
+
const calendarium = defaultFamilyMemberExtensions.find(
|
|
82
|
+
(ext) => ext.id === 'calendarium',
|
|
83
|
+
);
|
|
84
|
+
expect(calendarium).toBeDefined();
|
|
85
|
+
expect(calendarium?.title).toBe('Calendar');
|
|
86
|
+
expect(calendarium?.emoji).toBe('🗓️');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should contain documents extension', () => {
|
|
90
|
+
const documents = defaultFamilyMemberExtensions.find(
|
|
91
|
+
(ext) => ext.id === 'documents',
|
|
92
|
+
);
|
|
93
|
+
expect(documents).toBeDefined();
|
|
94
|
+
expect(documents?.title).toBe('Documents');
|
|
95
|
+
expect(documents?.emoji).toBe('📄');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should contain sizes extension', () => {
|
|
99
|
+
const sizes = defaultFamilyMemberExtensions.find(
|
|
100
|
+
(ext) => ext.id === 'sizes',
|
|
101
|
+
);
|
|
102
|
+
expect(sizes).toBeDefined();
|
|
103
|
+
expect(sizes?.title).toBe('Sizes');
|
|
104
|
+
expect(sizes?.emoji).toBe('📏');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should have all valid ISneatExtension objects', () => {
|
|
108
|
+
defaultFamilyMemberExtensions.forEach((ext: ISneatExtension) => {
|
|
109
|
+
expect(ext.id).toBeDefined();
|
|
110
|
+
expect(typeof ext.id).toBe('string');
|
|
111
|
+
expect(ext.title).toBeDefined();
|
|
112
|
+
expect(typeof ext.title).toBe('string');
|
|
113
|
+
expect(ext.emoji).toBeDefined();
|
|
114
|
+
expect(typeof ext.emoji).toBe('string');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should have same extensions as defaultFamilyExtension', () => {
|
|
119
|
+
expect(defaultFamilyMemberExtensions.length).toBe(
|
|
120
|
+
defaultFamilyExtension.length,
|
|
121
|
+
);
|
|
122
|
+
defaultFamilyExtension.forEach((ext) => {
|
|
123
|
+
expect(defaultFamilyMemberExtensions).toContainEqual(ext);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|