Add cypress test for mentor invitation

This commit is contained in:
Daniel Egger 2024-07-18 09:56:44 +02:00
parent f150751219
commit 3f02fd254a
16 changed files with 999 additions and 268 deletions

View File

@ -104,12 +104,13 @@ const noLearningMentors = computed(() =>
</div>
</div>
<div class="bg-white px-4 py-2">
<main>
<main data-cy="my-mentors-list">
<div>
<div
v-for="invitation in invitations"
:key="invitation.id"
class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16"
:data-cy="`mentor-${invitation.email}`"
>
<div class="flex flex-col md:flex-grow md:flex-row">
<div class="flex items-center space-x-2">
@ -178,6 +179,7 @@ const noLearningMentors = computed(() =>
<button
:disabled="!validEmail"
class="btn-primary mt-8"
data-cy="invite-mentor-button"
@click="inviteMentor()"
>
{{ $t("a.Einladung abschicken") }}

View File

@ -1,25 +1,36 @@
<script setup lang="ts">
import { useCSRFFetch } from "@/fetchHelpers";
import { itPost } from "@/fetchHelpers";
import { getLearningMentorUrl } from "@/utils/utils";
import { onMounted, ref } from "vue";
const props = defineProps<{
courseId: string;
invitationId: string;
}>();
const { data, error } = useCSRFFetch(
`/api/mentor/${props.courseId}/invitations/accept`,
{
onFetchError(ctx) {
ctx.error = ctx.data;
return ctx;
},
}
)
.post({
const loaded = ref<boolean>(false);
const responseData = ref<any>(null);
const hasError = ref<boolean>(false);
const errorMessage = ref<string>("");
onMounted(async () => {
const url = `/api/mentor/${props.courseId}/invitations/accept`;
itPost(url, {
invitation_id: props.invitationId,
})
.json();
.then((data) => {
responseData.value = data;
})
.catch((error) => {
hasError.value = true;
if (error.toString().includes("404")) {
errorMessage.value = "Einladung bereits akzeptiert";
}
})
.finally(() => {
loaded.value = true;
});
});
</script>
<template>
@ -28,9 +39,9 @@ const { data, error } = useCSRFFetch(
<header class="mb-8 mt-12">
<h1 class="mb-8">{{ $t("a.Einladung") }}</h1>
</header>
<main>
<main v-if="loaded">
<div class="bg-white p-6">
<template v-if="error">
<template v-if="hasError">
{{
$t(
"a.Die Einladung konnte nicht akzeptiert werden. Bitte melde dich beim Support."
@ -50,8 +61,8 @@ const { data, error } = useCSRFFetch(
</a>
</li>
</ul>
<div v-if="error.message" class="my-4">
{{ $t("a.Fehlermeldung") }}: {{ error.message }}
<div v-if="errorMessage" class="my-4">
{{ $t("a.Fehlermeldung") }}: {{ errorMessage }}
</div>
</template>
<template v-else>
@ -61,11 +72,16 @@ const { data, error } = useCSRFFetch(
"
>
<template #name>
<b>{{ data.user.first_name }} {{ data.user.last_name }}</b>
<b>
{{ responseData.user.first_name }} {{ responseData.user.last_name }}
</b>
</template>
</i18next>
<div class="mt-4">
<a class="underline" :href="getLearningMentorUrl(data.course_slug)">
<a
class="underline"
:href="getLearningMentorUrl(responseData.course_slug)"
>
{{ $t("a.Übersicht anschauen") }}
</a>
</div>

View File

@ -46,7 +46,7 @@ const signUpURL = computed(() => getSignUpURL(constructParams()));
</a>
<p class="mb-4 mt-12">{{ $t("a.Hast du schon ein Konto?") }}</p>
<a :href="loginURL" class="btn-secondary">
<a :href="loginURL" class="btn-secondary" data-cy="login-button">
{{ $t("a.Anmelden") }}
</a>
</template>

View File

@ -23,8 +23,9 @@ export default defineConfig(({ mode }) => {
],
define: {},
server: {
host: true,
port: 5173,
hmr: { port: 5173 },
strictPort: true,
},
resolve: {
alias: {

View File

@ -1,8 +1,10 @@
const { defineConfig } = require("cypress");
const { cloudPlugin } = require("cypress-cloud/plugin");
import { defineConfig } from "cypress";
import { cloudPlugin } from "cypress-cloud/plugin";
import tasks from "./cypress/plugins/index.mjs";
module.exports = defineConfig({
export default defineConfig({
projectId: "RVEZS1",
chromeWebSecurity: false,
watchForFileChanges: false,
video: true,
viewportWidth: 1280,
@ -19,6 +21,7 @@ module.exports = defineConfig({
e2e: {
// experimentalSessionAndOrigin: true,
setupNodeEvents(on, config) {
tasks(on, config);
return cloudPlugin(on, config);
},
baseUrl: "http://localhost:8001",

View File

@ -0,0 +1,57 @@
import { login } from "../helpers";
import { TEST_STUDENT1_VV_USER_ID } from "../../consts";
describe("mentorInvitation.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
it("Teilnehmer macht lädt Lernbegleitung ein; Lernbegleitung akzeptiert Einladung", () => {
login("student-vv@eiger-versicherungen.ch", "test");
cy.visit("/course/versicherungsvermittler-in/learn");
cy.get("[data-cy=navigation-learning-mentor-link]").click();
cy.get('[data-cy="lm-invite-mentor-button"]').click();
cy.get("#mentor-email").type("empty@example.com");
cy.get('[data-cy="invite-mentor-button"]').click();
cy.get('[data-cy="mentor-empty@example.com"]').should(
"contain",
"Die Einladung wurde noch nicht angenommen.",
);
cy.task(
"runSql",
"select target_url from learning_mentor_mentorinvitation where email = 'empty@example.com'",
).then((res) => {
const invitationUrl = res.rows[0].target_url;
console.log(invitationUrl);
cy.visit("/");
cy.get('[data-cy="header-profile"]').click();
cy.get('[data-cy="logout-button"]').click();
cy.wait(1000);
// try to accept invitation
cy.visit(invitationUrl);
cy.get('[data-cy="login-button"]').click();
cy.get("#username").type("empty@example.com");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.get(".bg-white").should(
"contain",
"Du hast die Einladung von Viktor Vollgas erfolgreich akzeptiert.",
);
cy.contains("Übersicht anschauen").click();
cy.get('[data-cy="lm-my-mentees"]').should(
"contain",
"Personen, die du begleitest",
);
cy.get('[data-cy="lm-my-mentees"]').should(
"contain",
"student-vv@eiger-versicherungen.ch",
);
});
});
});

View File

@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,9 @@
import { runSql } from "./tasks.mjs";
export default (on, config) => {
on("task", {
runSql,
});
return config;
};

18
cypress/plugins/tasks.mjs Normal file
View File

@ -0,0 +1,18 @@
import pg from "pg";
const cypressDatabaseUrl = process.env?.CYPRESS_DATABASE_URL || "postgres://postgres@localhost:5432/vbv_lernwelt_cypress";
if(!cypressDatabaseUrl) {
throw new Error(
"CYPRESS_DATABASE_URL must be set"
);
}
export async function runSql(sqlString) {
// I could not make postgres.js make work, so I use pg directly
const client = new pg.Client(cypressDatabaseUrl);
await client.connect();
const res = await client.query(sqlString);
await client.end();
return res;
}

1061
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@
"prettier": "npm run prettier --prefix client"
},
"devDependencies": {
"cypress": "^12.15.0",
"cypress-cloud": "^1.7.4"
"cypress": "^12.17.4",
"cypress-cloud": "^1.10.2",
"pg": "^8.12.0"
}
}

View File

@ -43,7 +43,7 @@ from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentFeedbackUK,
@ -145,6 +145,7 @@ def command(
CourseCompletionFeedback.objects.all().delete()
LearningMentor.objects.all().delete()
MentorInvitation.objects.all().delete()
User.objects.all().update(organisation=Organisation.objects.first())
User.objects.all().update(language="de")
User.objects.all().update(additional_json_data={})

View File

@ -34,5 +34,5 @@ class LearningMentorAdmin(admin.ModelAdmin):
@admin.register(MentorInvitation)
class MentorInvitationAdmin(admin.ModelAdmin):
list_display = ["id", "email", "participant", "created"]
readonly_fields = ["id", "created"]
readonly_fields = ["id", "created", "email", "participant"]
search_fields = ["email"]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.20 on 2024-07-18 08:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("learning_mentor", "0006_auto_20240319_1058"),
]
operations = [
migrations.AddField(
model_name="mentorinvitation",
name="target_url",
field=models.CharField(blank=True, default="", max_length=255),
),
]

View File

@ -34,6 +34,7 @@ class MentorInvitation(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField()
participant = models.ForeignKey(CourseSessionUser, on_delete=models.CASCADE)
target_url = models.CharField(max_length=255, blank=True, default="")
def __str__(self):
return f"{self.email} ({self.participant})"

View File

@ -143,6 +143,8 @@ def create_invitation(request, course_session_id: int):
invitation = serializer.save()
target_url = f"/lernbegleitung/{course_session_id}/invitation/{invitation.id}"
invitation.target_url = target_url
invitation.save()
if course_session.course.configuration.is_uk:
template = EmailTemplate.PRAXISBILDNER_INVITATION