diff --git a/src/app/app.component.test.ts b/src/app/app.component.test.ts index ca22b6277..ed9bcc99f 100644 --- a/src/app/app.component.test.ts +++ b/src/app/app.component.test.ts @@ -16,17 +16,21 @@ import { AppComponent } from '@/app/app.component'; import { CoreEvents } from '@singletons/events'; import { CoreLang, CoreLangProvider } from '@services/lang'; -import { mockSingleton, renderComponent } from '@/testing/utils'; +import { mock, mockSingleton, renderComponent } from '@/testing/utils'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { Http } from '@singletons'; +import { of } from 'rxjs'; +import { CoreSite } from '@classes/sites/site'; +import { CoreUtils } from '@services/utils/utils'; describe('AppComponent', () => { let langProvider: CoreLangProvider; - let navigator: CoreNavigatorService; - beforeEach(() => { - navigator = mockSingleton(CoreNavigator, ['navigate']); - langProvider = mockSingleton(CoreLang, ['clearCustomStrings']); + langProvider = mockSingleton(CoreLang, mock({ getCurrentLanguage: async () => 'en' , clearCustomStrings: () => null })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockSingleton(Http, { get: () => of(null as any) }); }); it('should render', async () => { @@ -38,6 +42,7 @@ describe('AppComponent', () => { it('cleans up on logout', async () => { const fixture = await renderComponent(AppComponent); + const navigator: CoreNavigatorService = mockSingleton(CoreNavigator, ['navigate']); fixture.componentInstance.ngOnInit(); CoreEvents.trigger(CoreEvents.LOGOUT); @@ -46,4 +51,66 @@ describe('AppComponent', () => { expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); }); + it('adds ionic platform and theme classes', async () => { + const fixture = await renderComponent(AppComponent); + const siteUrl = 'https://campus.example.edu'; + const themeName = 'mytheme'; + const themeName2 = 'anothertheme'; + + fixture.componentInstance.ngOnInit(); + + expect(document.documentElement.classList.contains('ionic7')).toBe(true); + + const site = mock(new CoreSite('42', siteUrl, 'token', { info: { + sitename: 'Example Campus', + username: 'admin', + firstname: 'Admin', + lastname: 'User', + fullname: 'Admin User', + lang: 'en', + userid: 1, + siteurl: siteUrl, + userpictureurl: '', + theme: themeName, + functions: [], + } })); + + mockSingleton(CoreSites, { + getSite: () => Promise.resolve(site), + getCurrentSiteId: () => '42', + }); + + CoreEvents.trigger(CoreEvents.LOGIN, {}, '42'); + // Wait the event to be processed. + await CoreUtils.nextTick(); + + expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(true); + expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(false); + + if (site.infos) { + site.infos.theme = themeName2; + } + + CoreEvents.trigger(CoreEvents.SITE_UPDATED, site.infos , '42'); + + // Wait the event to be processed. + await CoreUtils.nextTick(); + + expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true); + expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false); + + CoreEvents.trigger(CoreEvents.LOGOUT); + + expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false); + expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(false); + + CoreEvents.trigger(CoreEvents.SITE_ADDED, site.infos , '42'); + + // Wait the event to be processed. + await CoreUtils.nextTick(); + + expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true); + expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false); + }); + }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f6176bf38..3670da649 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -36,10 +36,12 @@ import { CoreUrl } from '@singletons/url'; import { CoreLogger } from '@singletons/logger'; import { CorePromisedValue } from '@classes/promised-value'; import { register } from 'swiper/element/bundle'; +import { CoreSiteInfo, CoreSiteInfoResponse } from '@classes/sites/unauthenticated-site'; const MOODLE_SITE_URL_PREFIX = 'url-'; const MOODLE_VERSION_PREFIX = 'version-'; const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; +const MOODLE_SITE_THEME_PREFIX = 'theme-site-'; register(); @@ -68,7 +70,7 @@ export class AppComponent implements OnInit, AfterViewInit { CoreLang.clearCustomStrings(); // Remove version classes from body. - this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]); + this.removeSiteClasses(); // Go to sites page when user is logged out. await CoreNavigator.navigate('/login/sites', { reset: true }); @@ -122,11 +124,7 @@ export class AppComponent implements OnInit, AfterViewInit { const site = await CoreSites.getSite(data.siteId); const info = site.getInfo(); if (info) { - // Add version classes to body. - this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]); - - this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(info.release || '')); - this.addSiteUrlClass(info.siteurl); + this.addSiteClasses(info); } } @@ -142,11 +140,7 @@ export class AppComponent implements OnInit, AfterViewInit { if (data.siteId === CoreSites.getCurrentSiteId()) { this.loadCustomStrings(); - // Add version classes to body. - this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]); - - this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(data.release || '')); - this.addSiteUrlClass(data.siteurl); + this.addSiteClasses(data); } }); @@ -154,11 +148,7 @@ export class AppComponent implements OnInit, AfterViewInit { if (data.siteId === CoreSites.getCurrentSiteId()) { this.loadCustomStrings(); - // Add version classes to body. - this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]); - - this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(data.release || '')); - this.addSiteUrlClass(data.siteurl); + this.addSiteClasses(data); } }); @@ -320,6 +310,33 @@ export class AppComponent implements OnInit, AfterViewInit { } } + /** + * Convenience function to add site classes to html. + * + * @param siteInfo Site Info. + */ + protected addSiteClasses(siteInfo: CoreSiteInfo | CoreSiteInfoResponse): void { + // Add version classes to html tag. + this.removeSiteClasses(); + + this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || '')); + this.addSiteUrlClass(siteInfo.siteurl); + + if (siteInfo.theme) { + CoreDomUtils.toggleModeClass(MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true); + } + } + + /** + * Convenience function to remove all site mode classes form html. + */ + protected removeSiteClasses(): void { + // Remove version classes from html tag. + this.removeModeClasses( + [MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX, MOODLE_SITE_THEME_PREFIX], + ); + } + /** * Converts the provided URL into a CSS class that be used within the page. * This is primarily used to add the siteurl to the body tag as a CSS class. diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index f1c01ac63..89f45d9f0 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -1402,7 +1402,7 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & { enablecompletion?: number; // Completion enabled? 1: yes 0: no. completionnotify?: number; // 1: yes 0: no. lang?: string; // Forced course language. - theme?: string; // Fame of the forced theme. + theme?: string; // Name of the forced theme. marker?: number; // Current course marker. legacyfiles?: number; // If legacy files are enabled. calendartype?: string; // Calendar type.