feat: test onboarding redirects

This commit is contained in:
Reto Aebersold 2023-11-14 10:25:09 +01:00 committed by Christian Cueni
parent bfeca6e8e0
commit 3644a0d77d
6 changed files with 171 additions and 19 deletions

View File

@ -68,6 +68,10 @@ export const itDelete = (url: RequestInfo) => {
return itPost(url, {}, { method: "DELETE" }); return itPost(url, {}, { method: "DELETE" });
}; };
export const itPut = (url: RequestInfo, data: unknown) => {
return itPost(url, data, { method: "PUT" });
};
const itGetPromiseCache = new Map<string, Promise<any>>(); const itGetPromiseCache = new Map<string, Promise<any>>();
export function bustItGetCache(key?: string) { export function bustItGetCache(key?: string) {

View File

@ -1,27 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import WizardPage from "@/components/onboarding/WizardPage.vue"; import WizardPage from "@/components/onboarding/WizardPage.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import type { Ref } from "vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import AvatarImage from "@/components/ui/AvatarImage.vue"; import AvatarImage from "@/components/ui/AvatarImage.vue";
import { useFileUpload } from "@/composables"; import { useFileUpload } from "@/composables";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useFetch } from "@/fetchHelpers"; import { itPut, useFetch } from "@/fetchHelpers";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { profileNextRoute } from "@/services/onboarding";
const { t } = useTranslation(); const { t } = useTranslation();
type Organisation = {
id: string;
name: string;
};
const user = useUserStore(); const user = useUserStore();
const route = useRoute(); const route = useRoute();
const { data: companies } = useFetch(() => "/api/core/organisations/"); const fetchResult = useFetch("/api/core/organisations/");
const organisations: Ref<Organisation[] | null> = fetchResult.data;
const selectedCompany = ref({ const selectedOrganisation = ref({
id: "0", id: "0",
name: t("a.Auswählen"), name: t("a.Auswählen"),
}); });
const validCompany = computed(() => {
return selectedCompany.value.id !== "0"; watch(
organisations,
(newOrganisations) => {
if (newOrganisations) {
const userOrganisation = newOrganisations.find((c) => c.id === user.organisation);
if (userOrganisation) {
selectedOrganisation.value = userOrganisation;
}
}
},
{ immediate: true }
);
const validOrganisation = computed(() => {
return selectedOrganisation.value.id !== "0";
}); });
const { const {
@ -35,18 +57,15 @@ watch(avatarFileInfo, (info) => {
console.log("fileInfo changed", info); console.log("fileInfo changed", info);
}); });
watch(selectedCompany, (company) => { watch(selectedOrganisation, (organisation) => {
console.log("company changed", company); console.log("organisation changed", organisation);
itPut("/api/core/me/", {
organisation: organisation.id,
});
}); });
const nextRoute = computed(() => { const nextRoute = computed(() => {
if (route.params.courseType === "uk") { return profileNextRoute(route.params.courseType);
return "setupComplete";
}
if (route.params.courseType === "vv") {
return "checkoutAddress";
}
return "";
}); });
</script> </script>
@ -62,7 +81,11 @@ const nextRoute = computed(() => {
andere Personen einfacher finden. andere Personen einfacher finden.
</p> </p>
<ItDropdownSelect v-if="companies" v-model="selectedCompany" :items="companies" /> <ItDropdownSelect
v-if="organisations"
v-model="selectedOrganisation"
:items="organisations"
/>
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24"> <div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
<div> <div>
@ -94,7 +117,7 @@ const nextRoute = computed(() => {
<template #footer> <template #footer>
<router-link v-slot="{ navigate }" :to="{ name: nextRoute }" custom> <router-link v-slot="{ navigate }" :to="{ name: nextRoute }" custom>
<button <button
:disabled="!validCompany" :disabled="!validOrganisation"
class="btn-blue flex items-center" class="btn-blue flex items-center"
role="link" role="link"
@click="navigate" @click="navigate"

View File

@ -0,0 +1,103 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, vi } from "vitest";
import { START_LOCATION } from "vue-router";
import { useUserStore } from "../../stores/user";
import { onboardingRedirect } from "../onboarding";
describe("Onboarding", () => {
afterEach(() => {
vi.restoreAllMocks();
});
beforeEach(() => {
setActivePinia(createPinia());
});
it("redirect guest", () => {
const user = useUserStore();
user.loggedIn = false;
const mock = vi.fn();
onboardingRedirect(routeLocation("accountConfirm", "uk"), START_LOCATION, mock);
expect(mock).toHaveBeenCalledWith({
name: "accountCreate",
params: { courseType: "uk" },
});
});
it("redirect logged-in user from accountCreate to accountConfirm", () => {
const user = useUserStore();
user.loggedIn = true;
const mockNext = vi.fn();
onboardingRedirect(routeLocation("accountCreate", "uk"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith({
name: "accountConfirm",
params: { courseType: "uk" },
});
});
it("UK: redirect to profile next route for logged-in user with organisation", () => {
const user = useUserStore();
user.loggedIn = true;
user.organisation = "1";
const mockNext = vi.fn();
onboardingRedirect(routeLocation("accountConfirm", "uk"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith({
name: "setupComplete",
params: { courseType: "uk" },
});
});
it("VV: redirect to profile next route for logged-in user with organisation", () => {
const user = useUserStore();
user.loggedIn = true;
user.organisation = "1";
const mockNext = vi.fn();
onboardingRedirect(routeLocation("accountConfirm", "vv"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith({
name: "checkoutAddress",
params: { courseType: "vv" },
});
});
it("no redirect for logged-in user without organisation to accountConfirm", () => {
const user = useUserStore();
user.loggedIn = true;
user.organisation = "";
const mockNext = vi.fn();
onboardingRedirect(routeLocation("accountConfirm", "uk"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith(); // No arguments passed means no redirection
});
it("no redirect for logged-in user to a non-relevant route", () => {
const user = useUserStore();
user.loggedIn = true;
const mockNext = vi.fn();
onboardingRedirect(routeLocation("someOtherRoute", "uk"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith(); // No arguments passed means no redirection
});
it("no redirect for guest on accountCreate page", () => {
const user = useUserStore();
user.loggedIn = false;
const mockNext = vi.fn();
onboardingRedirect(routeLocation("accountCreate", "uk"), START_LOCATION, mockNext);
expect(mockNext).toHaveBeenCalledWith(); // No arguments passed means no redirection
});
});
function routeLocation(name: string, courseType: string) {
return {
fullPath: "",
hash: "",
matched: [],
meta: {},
name: name,
params: {
courseType,
},
path: "",
query: {},
redirectedFrom: undefined,
};
}

View File

@ -1,25 +1,36 @@
import { profileNextRoute } from "@/services/onboarding";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router"; import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
import { START_LOCATION } from "vue-router";
export async function onboardingRedirect( export async function onboardingRedirect(
to: RouteLocationNormalized, to: RouteLocationNormalized,
from: RouteLocationNormalized, from: RouteLocationNormalized,
next: NavigationGuardNext next: NavigationGuardNext
) { ) {
const userStore = useUserStore(); const user = useUserStore();
// Guest // Guest
if (!userStore.loggedIn) { if (!user.loggedIn) {
if (to.name !== "accountCreate") { if (to.name !== "accountCreate") {
return next({ name: "accountCreate", params: to.params }); return next({ name: "accountCreate", params: to.params });
} }
return next(); return next();
} }
// Logged in // Member
if (to.name === "accountCreate") { if (to.name === "accountCreate") {
return next({ name: "accountConfirm", params: to.params }); return next({ name: "accountConfirm", params: to.params });
} }
// Maybe we can skip the account setup steps
if (
from === START_LOCATION &&
user.organisation &&
(to.name === "accountConfirm" || to.name === "accountProfile")
) {
return next({ name: profileNextRoute(to.params.courseType), params: to.params });
}
return next(); return next();
} }

View File

@ -0,0 +1,9 @@
export function profileNextRoute(courseType: string | string[]) {
if (courseType === "uk") {
return "setupComplete";
}
if (courseType === "vv") {
return "checkoutAddress";
}
return "";
}

View File

@ -25,6 +25,7 @@ export type UserState = {
email: string; email: string;
username: string; username: string;
avatar_url: string; avatar_url: string;
organisation: string;
is_superuser: boolean; is_superuser: boolean;
course_session_experts: string[]; course_session_experts: string[];
loggedIn: boolean; loggedIn: boolean;
@ -57,6 +58,7 @@ const initialUserState: UserState = {
username: "", username: "",
avatar_url: "", avatar_url: "",
is_superuser: false, is_superuser: false,
organisation: "",
course_session_experts: [], course_session_experts: [],
loggedIn: false, loggedIn: false,
language: defaultLanguage, language: defaultLanguage,