diff --git a/cypress/e2e/serviceNavigation.cy.ts b/cypress/e2e/serviceNavigation.cy.ts index 0b5c6d89..9eb1df0c 100644 --- a/cypress/e2e/serviceNavigation.cy.ts +++ b/cypress/e2e/serviceNavigation.cy.ts @@ -4,24 +4,11 @@ describe("Service Navigation", () => { describe("desktop", () => { beforeEach(() => { cy.resizeToDesktop(); - cy.visit("/index.html"); }); it("open/closes user settings", () => { - // User settings menu initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .as("toggle") - .should("be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .as("serviceNav") - .should("not.be.visible"); - - // Open user settings menu - cy.get("@toggle").click(); - cy.get("@toggle").ariaExpanded(true); - cy.get("@serviceNav").should("be.visible"); + cy.visit("/index.html"); + openServiceNavigation(); // Close user settings menu cy.get("@toggle").click(); @@ -30,20 +17,10 @@ describe("Service Navigation", () => { }); it("renders user settings in the service navigation", () => { - // User settings menu initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .as("toggle") - .should("be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .as("serviceNav") - .should("not.be.visible"); + cy.visit("/index.html"); + openServiceNavigation(); - // Open user settings menu - cy.get("@toggle").click(); cy.get("@serviceNav") - .should("be.visible") .find("a") .should((links) => expect( @@ -60,20 +37,8 @@ describe("Service Navigation", () => { // Apparently the triggering of the 'keydown' event does not work // when run headless it.skip("closes user settings on escape", () => { - // User settings menu initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .as("toggle") - .should("be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .as("serviceNav") - .should("not.be.visible"); - - // Open user settings menu - cy.get("@toggle").click(); - cy.get("@toggle").ariaExpanded(true); - cy.get("@serviceNav").should("be.visible"); + cy.visit("/index.html"); + openServiceNavigation(); // Close user settings menu cy.document().trigger("keydown", { key: "Escape" }); @@ -82,20 +47,8 @@ describe("Service Navigation", () => { }); it("closes user settings on select", () => { - // User settings menu initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .as("toggle") - .should("be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .as("serviceNav") - .should("not.be.visible"); - - // Open user settings menu - cy.get("@toggle").click(); - cy.get("@toggle").ariaExpanded(true); - cy.get("@serviceNav").should("be.visible"); + cy.visit("/index.html"); + openServiceNavigation(); // Close user settings menu cy.get("@serviceNav") @@ -107,20 +60,8 @@ describe("Service Navigation", () => { }); it("closes user settings on click away", () => { - // User settings menu initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .as("toggle") - .should("be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .as("serviceNav") - .should("not.be.visible"); - - // Open user settings menu - cy.get("@toggle").click(); - cy.get("@toggle").ariaExpanded(true); - cy.get("@serviceNav").should("be.visible"); + cy.visit("/index.html"); + openServiceNavigation(); // Close user settings menu cy.get("a.logo").click(); @@ -128,7 +69,10 @@ describe("Service Navigation", () => { cy.get("@serviceNav").should("not.be.visible"); }); - it("renders language switcher in the service navigation", () => { + it("renders language switcher in the service navigation for multilingual schools", () => { + interceptGuiLanguagesRequest(["de-CH", "fr-CH"]); + + cy.visit("/index.html"); cy.get("bkd-language-switcher") .find("a") .should((links) => @@ -137,28 +81,24 @@ describe("Service Navigation", () => { ).to.deep.eq(["de", "fr"]), ); }); + + it("does not render language switcher in the service navigation for monolingual schools", () => { + interceptGuiLanguagesRequest(["de-CH"]); + + cy.visit("/index.html"); + cy.get("bkd-language-switcher").should("not.exist"); + }); }); describe("mobile", () => { beforeEach(() => { cy.resizeToMobile(); - cy.visit("/index.html"); }); - it("renders user settings and language switcher in the mobile navigation", () => { - // Mobile navigation initially closed - cy.get("button[aria-label='Menü Benutzereinstellungen']") - .should("not.be.visible") - .ariaExpanded(false); - cy.get("bkd-user-settings") - .find('ul[role="menu"]') - .should("not.be.visible"); - - // Open mobile navigation - cy.get("button[aria-label='Menü']").as("toggle"); - cy.get("@toggle").click(); - cy.get("@toggle").ariaExpanded(true); - cy.get(".service-nav").as("serviceNav").should("be.visible"); + it("renders user settings and language switcher in the mobile navigation for multilingual schools", () => { + interceptGuiLanguagesRequest(["de-CH", "fr-CH"]); + cy.visit("/index.html"); + openMobileNavigation(); cy.get("@serviceNav") .find("a") @@ -175,5 +115,63 @@ describe("Service Navigation", () => { ]), ); }); + + it("renders user settings in the mobile navigation for monolingual schools", () => { + interceptGuiLanguagesRequest(["de-CH"]); + cy.visit("/index.html"); + openMobileNavigation(); + + cy.get("@serviceNav") + .find("a") + .should((links) => + expect( + links.toArray().map((link) => link.textContent?.trim()), + ).to.deep.eq([ + "Mein Profil", + "Einstellungen", + "Video-Tutorials", + "Logout", + ]), + ); + }); }); }); + +function interceptGuiLanguagesRequest(guiLanguage: string[]) { + cy.intercept( + "GET", + "https://eventotest.api/restApi/Configurations/SchoolAppNavigation", + { instanceName: "Test", guiLanguage }, + ); +} + +function openServiceNavigation() { + // User settings menu initially closed + cy.get("button[aria-label='Menü Benutzereinstellungen']") + .as("toggle") + .should("be.visible") + .ariaExpanded(false); + cy.get("bkd-user-settings") + .find('ul[role="menu"]') + .as("serviceNav") + .should("not.be.visible"); + + // Open user settings menu + cy.get("@toggle").click(); + cy.get("@toggle").ariaExpanded(true); + cy.get("@serviceNav").should("be.visible"); +} + +function openMobileNavigation() { + // Mobile navigation initially closed + cy.get("button[aria-label='Menü Benutzereinstellungen']") + .should("not.be.visible") + .ariaExpanded(false); + cy.get("bkd-user-settings").find('ul[role="menu"]').should("not.be.visible"); + + // Open mobile navigation + cy.get("button[aria-label='Menü']").as("toggle"); + cy.get("@toggle").click(); + cy.get("@toggle").ariaExpanded(true); + cy.get(".service-nav").as("serviceNav").should("be.visible"); +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 23d9db78..8a783554 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -77,7 +77,7 @@ Cypress.Commands.add( cy.intercept( "GET", "https://eventotest.api/restApi/Configurations/SchoolAppNavigation", - { instanceName: "Test" }, + { instanceName: "Test", guiLanguage: ["de-CH", "fr-CH"] }, ); cy.intercept( diff --git a/src/components/Header/MobileNav.ts b/src/components/Header/MobileNav.ts index a7ca484c..13a812fc 100644 --- a/src/components/Header/MobileNav.ts +++ b/src/components/Header/MobileNav.ts @@ -3,6 +3,7 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { map } from "lit/directives/map.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { when } from "lit/directives/when.js"; import { localized, msg } from "@lit/localize"; import { StateController } from "@lit-app/state"; import arrowDownIcon from "../../assets/icons/arrow-down.svg?raw"; @@ -316,7 +317,10 @@ export class MobileNav extends LitElement { this.renderSettingsItem.bind(this), )} - + ${when( + portalState.allowedLocales.length > 1, + () => html``, + )} `; diff --git a/src/components/Header/ServiceNav.ts b/src/components/Header/ServiceNav.ts index d3ec66c8..9dee196b 100644 --- a/src/components/Header/ServiceNav.ts +++ b/src/components/Header/ServiceNav.ts @@ -1,6 +1,9 @@ import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; import { localized, msg } from "@lit/localize"; +import { StateController } from "@lit-app/state"; +import { portalState } from "../../state/portal-state.ts"; import { theme } from "../../utils/theme"; @customElement("bkd-service-nav") @@ -54,13 +57,21 @@ export class ServiceNav extends LitElement { `, ]; + constructor() { + super(); + new StateController(this, portalState); + } + render() { return html` `; diff --git a/src/state/portal-state.ts b/src/state/portal-state.ts index 182a9c03..a09e070a 100644 --- a/src/state/portal-state.ts +++ b/src/state/portal-state.ts @@ -1,4 +1,3 @@ -import { msg } from "@lit/localize"; import { State, property, query } from "@lit-app/state"; import { App, @@ -7,7 +6,7 @@ import { NavigationItem, settings, } from "../settings"; -import { fetchInstanceName, fetchUserAccessInfo } from "../utils/fetch"; +import { fetchInstanceInfo, fetchUserAccessInfo } from "../utils/fetch"; import { getInitialLocale, getLocale, updateLocale } from "../utils/locale"; import { filterAllowed, getApp, getNavigationItem } from "../utils/navigation"; import { cleanupQueryParams, updateQueryParam } from "../utils/routing"; @@ -29,6 +28,9 @@ class PortalState extends State { @property({ value: "" }) instanceName!: string; + @property({ value: [] }) + allowedLocales!: ReadonlyArray; + @property({ value: [] }) navigation!: Navigation; @@ -68,6 +70,8 @@ class PortalState extends State { ); async init() { + await this.loadInstanceInfo(); + // Update initially await this.handleStateChange("locale", this.locale); @@ -160,7 +164,6 @@ class PortalState extends State { private async handleStateChange(key: string, value: any): Promise { if (key === "locale") { await this.updateLocale(value); - await this.loadInstanceName(); } if (key === "locale" || key === "navigationItemKey") { @@ -180,9 +183,15 @@ class PortalState extends State { } private async updateLocale(locale: PortalState["locale"]): Promise { - updateQueryParam(LOCALE_QUERY_PARAM, locale); + // Fall back to allowed language (i.e. for instances that only support one + // language) + this.locale = this.allowedLocales.includes(locale) + ? locale + : this.allowedLocales[0]; + + updateQueryParam(LOCALE_QUERY_PARAM, this.locale); try { - await updateLocale(locale); + await updateLocale(this.locale); } catch (error) { console.warn( "Unable to fetch/update locale (this may happen when interrupted by a redirect):", @@ -257,14 +266,13 @@ class PortalState extends State { this.rolesAndPermissions = [...roles, ...permissions]; } - private async loadInstanceName(): Promise { + private async loadInstanceInfo(): Promise { if (!tokenState.authenticated) return; try { - const instanceName = await fetchInstanceName(); - this.instanceName = [msg("Evento"), instanceName] - .filter(Boolean) - .join(" | "); + const { instanceName, allowedLocales } = await fetchInstanceInfo(); + this.allowedLocales = allowedLocales; + this.instanceName = ["Evento", instanceName].filter(Boolean).join(" | "); } catch (error) { console.warn( "Unable to fetch/update instance name (this may happen when interrupted by a redirect):", diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 4ed30e73..e317841c 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -23,11 +23,18 @@ export async function fetchUserAccessInfo(): Promise { }; } -export async function fetchInstanceName(): Promise { - const url = `${envSettings.apiServer}/Configurations/SchoolAppNavigation`; - const result = await fetchApi<{ instanceName: string }>(url); +type InstanceInfo = Readonly<{ + instanceName: string; + allowedLocales: ReadonlyArray; +}>; - return result?.instanceName || null; +export async function fetchInstanceInfo(): Promise { + const url = `${envSettings.apiServer}/Configurations/SchoolAppNavigation`; + const { instanceName, guiLanguage: allowedLocales } = await fetchApi<{ + instanceName: string; + guiLanguage: ReadonlyArray; + }>(url); + return { instanceName, allowedLocales }; } export type Substitution = Readonly<{