test(e2e): add data-testid attributes to all Vue components

- Add data-testid to 18 Vue components (Pages, Blocks, Features, Layouts, Primitives)
- Naming convention: {component-kebab}-{element-description}
- 98 total data-testid attributes added
- Target elements: buttons, links, inputs, modals, navigation
- No logic/styling changes - attributes only
This commit is contained in:
Thorsten Bus 2026-03-01 22:45:13 +01:00
parent 3a1ba1fc7d
commit 4520c1ce5f
19 changed files with 98 additions and 16 deletions

View file

@ -169,7 +169,7 @@ const deleteArrangement = () => {
</script> </script>
<template> <template>
<div class="space-y-4 rounded-lg border border-gray-200 bg-white p-4"> <div data-testid="arrangement-configurator" class="space-y-4 rounded-lg border border-gray-200 bg-white p-4">
<div class="flex flex-wrap items-end gap-3"> <div class="flex flex-wrap items-end gap-3">
<div class="min-w-64 flex-1"> <div class="min-w-64 flex-1">
<label <label
@ -179,6 +179,7 @@ const deleteArrangement = () => {
Arrangement Arrangement
</label> </label>
<select <select
data-testid="arrangement-select"
id="arrangement-select" id="arrangement-select"
v-model="selectedId" v-model="selectedId"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
@ -194,6 +195,7 @@ const deleteArrangement = () => {
</div> </div>
<button <button
data-testid="arrangement-add-button"
type="button" type="button"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700"
@click="addArrangement" @click="addArrangement"
@ -202,6 +204,7 @@ const deleteArrangement = () => {
</button> </button>
<button <button
data-testid="arrangement-clone-button"
type="button" type="button"
class="rounded-md bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800" class="rounded-md bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800"
@click="cloneArrangement" @click="cloneArrangement"
@ -210,6 +213,7 @@ const deleteArrangement = () => {
</button> </button>
<button <button
data-testid="arrangement-delete-button"
type="button" type="button"
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-red-700" class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-red-700"
@click="deleteArrangement" @click="deleteArrangement"
@ -271,7 +275,7 @@ const deleteArrangement = () => {
:key="`${group.id}-${index}`" :key="`${group.id}-${index}`"
class="mb-2 flex items-center gap-2 rounded-md border border-gray-200 bg-white p-2" class="mb-2 flex items-center gap-2 rounded-md border border-gray-200 bg-white p-2"
> >
<span class="cursor-move text-gray-500"></span> <span data-testid="arrangement-drag-handle" class="cursor-move text-gray-500"></span>
<span <span
class="inline-flex rounded-full px-3 py-1 text-sm font-semibold text-white" class="inline-flex rounded-full px-3 py-1 text-sm font-semibold text-white"
@ -291,6 +295,7 @@ const deleteArrangement = () => {
</label> </label>
<button <button
data-testid="arrangement-remove-button"
type="button" type="button"
class="rounded px-2 py-1 text-xs font-semibold text-red-600 hover:bg-red-50" class="rounded px-2 py-1 text-xs font-semibold text-red-600 hover:bg-red-50"
@click="removeGroupAt(index)" @click="removeGroupAt(index)"

View file

@ -54,7 +54,7 @@ function handleSlideUpdated() {
</script> </script>
<template> <template>
<div class="information-block space-y-6"> <div data-testid="information-block" class="information-block space-y-6">
<!-- Block header --> <!-- Block header -->
<div class="border-b border-amber-200/60 pb-4"> <div class="border-b border-amber-200/60 pb-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -103,6 +103,7 @@ function handleSlideUpdated() {
<!-- Slide uploader information slides are GLOBAL (service_id = null) --> <!-- Slide uploader information slides are GLOBAL (service_id = null) -->
<SlideUploader <SlideUploader
data-testid="information-block-uploader"
type="information" type="information"
:service-id="null" :service-id="null"
:show-expire-date="true" :show-expire-date="true"
@ -111,6 +112,7 @@ function handleSlideUpdated() {
<!-- Slide grid with prominent expire dates --> <!-- Slide grid with prominent expire dates -->
<SlideGrid <SlideGrid
data-testid="information-block-grid"
:slides="informationSlides" :slides="informationSlides"
type="information" type="information"
:show-expire-date="true" :show-expire-date="true"

View file

@ -37,7 +37,7 @@ function handleSlideUpdated() {
</script> </script>
<template> <template>
<div class="moderation-block space-y-6"> <div data-testid="moderation-block" class="moderation-block space-y-6">
<!-- Block header --> <!-- Block header -->
<div class="border-b border-gray-200 pb-4"> <div class="border-b border-gray-200 pb-4">
<h3 class="text-lg font-semibold text-gray-900"> <h3 class="text-lg font-semibold text-gray-900">
@ -50,6 +50,7 @@ function handleSlideUpdated() {
<!-- Slide uploader --> <!-- Slide uploader -->
<SlideUploader <SlideUploader
data-testid="moderation-block-uploader"
type="moderation" type="moderation"
:service-id="serviceId" :service-id="serviceId"
:show-expire-date="false" :show-expire-date="false"
@ -58,6 +59,7 @@ function handleSlideUpdated() {
<!-- Slide grid --> <!-- Slide grid -->
<SlideGrid <SlideGrid
data-testid="moderation-block-grid"
:slides="moderationSlides" :slides="moderationSlides"
type="moderation" type="moderation"
:show-expire-date="false" :show-expire-date="false"

View file

@ -37,7 +37,7 @@ function handleSlideUpdated() {
</script> </script>
<template> <template>
<div class="sermon-block space-y-6"> <div data-testid="sermon-block" class="sermon-block space-y-6">
<!-- Block header --> <!-- Block header -->
<div class="border-b border-gray-200 pb-4"> <div class="border-b border-gray-200 pb-4">
<h3 class="text-lg font-semibold text-gray-900"> <h3 class="text-lg font-semibold text-gray-900">
@ -50,6 +50,7 @@ function handleSlideUpdated() {
<!-- Slide uploader --> <!-- Slide uploader -->
<SlideUploader <SlideUploader
data-testid="sermon-block-uploader"
type="sermon" type="sermon"
:service-id="serviceId" :service-id="serviceId"
:show-expire-date="false" :show-expire-date="false"
@ -58,6 +59,7 @@ function handleSlideUpdated() {
<!-- Slide grid --> <!-- Slide grid -->
<SlideGrid <SlideGrid
data-testid="sermon-block-grid"
:slides="sermonSlides" :slides="sermonSlides"
type="sermon" type="sermon"
:show-expire-date="false" :show-expire-date="false"

View file

@ -167,7 +167,7 @@ const toastClasses = () => {
</script> </script>
<template> <template>
<div class="space-y-4"> <div data-testid="songs-block" class="space-y-4">
<Transition <Transition
enter-active-class="transition duration-200 ease-out" enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0" enter-from-class="translate-y-1 opacity-0"
@ -187,6 +187,7 @@ const toastClasses = () => {
</Transition> </Transition>
<div <div
data-testid="songs-block-song-card"
v-for="serviceSong in sortedSongs()" v-for="serviceSong in sortedSongs()"
:key="serviceSong.id" :key="serviceSong.id"
class="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm" class="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm"
@ -242,6 +243,7 @@ const toastClasses = () => {
</div> </div>
<button <button
data-testid="songs-block-request-button"
type="button" type="button"
class="inline-flex items-center rounded-md border border-amber-300 bg-white px-3 py-2 text-xs font-semibold text-amber-800 transition hover:bg-amber-100" class="inline-flex items-center rounded-md border border-amber-300 bg-white px-3 py-2 text-xs font-semibold text-amber-800 transition hover:bg-amber-100"
@click="requestCreation(serviceSong.id)" @click="requestCreation(serviceSong.id)"
@ -256,6 +258,7 @@ const toastClasses = () => {
Song suchen Song suchen
</label> </label>
<input <input
data-testid="songs-block-search-input"
v-model="searchTerms[serviceSong.id]" v-model="searchTerms[serviceSong.id]"
type="text" type="text"
placeholder="Titel oder CCLI eingeben" placeholder="Titel oder CCLI eingeben"
@ -268,6 +271,7 @@ const toastClasses = () => {
Song aus DB auswaehlen Song aus DB auswaehlen
</label> </label>
<select <select
data-testid="songs-block-song-select"
v-model="selectedSongIds[serviceSong.id]" v-model="selectedSongIds[serviceSong.id]"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500" class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
> >
@ -285,6 +289,7 @@ const toastClasses = () => {
</div> </div>
<button <button
data-testid="songs-block-assign-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700" class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700"
@click="assignSong(serviceSong.id)" @click="assignSong(serviceSong.id)"
@ -308,6 +313,7 @@ const toastClasses = () => {
class="inline-flex items-center gap-2 text-sm font-medium text-gray-800" class="inline-flex items-center gap-2 text-sm font-medium text-gray-800"
> >
<input <input
data-testid="songs-block-translation-checkbox"
v-model="translationValues[serviceSong.id]" v-model="translationValues[serviceSong.id]"
type="checkbox" type="checkbox"
class="rounded border-gray-300 text-emerald-600 focus:ring-emerald-500" class="rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
@ -327,6 +333,7 @@ const toastClasses = () => {
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button
data-testid="songs-block-preview-button"
type="button" type="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
@click="showPlaceholder" @click="showPlaceholder"
@ -334,6 +341,7 @@ const toastClasses = () => {
Vorschau Vorschau
</button> </button>
<button <button
data-testid="songs-block-download-button"
type="button" type="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
@click="showPlaceholder" @click="showPlaceholder"

View file

@ -81,6 +81,7 @@ const iconColors = {
<div class="mt-6 flex justify-end gap-3"> <div class="mt-6 flex justify-end gap-3">
<button <button
data-testid="confirm-dialog-cancel-button"
type="button" type="button"
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2" class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
@click="emit('cancel')" @click="emit('cancel')"
@ -88,6 +89,7 @@ const iconColors = {
{{ cancelLabel }} {{ cancelLabel }}
</button> </button>
<button <button
data-testid="confirm-dialog-confirm-button"
type="button" type="button"
class="rounded-lg px-4 py-2 text-sm font-medium text-white shadow-sm transition focus:outline-none focus:ring-2 focus:ring-offset-2" class="rounded-lg px-4 py-2 text-sm font-medium text-white shadow-sm transition focus:outline-none focus:ring-2 focus:ring-offset-2"
:class="variantClasses[variant]" :class="variantClasses[variant]"

View file

@ -141,7 +141,7 @@ function isExpired(expireDate) {
</script> </script>
<template> <template>
<div class="slide-grid"> <div data-testid="slide-grid" class="slide-grid">
<!-- Empty state --> <!-- Empty state -->
<div <div
v-if="sortedSlides.length === 0" v-if="sortedSlides.length === 0"
@ -192,6 +192,7 @@ function isExpired(expireDate) {
<!-- Delete button overlay --> <!-- Delete button overlay -->
<button <button
data-testid="slide-grid-delete-button"
@click.stop="promptDelete(slide)" @click.stop="promptDelete(slide)"
class="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-lg bg-black/50 text-white/80 opacity-0 backdrop-blur-sm transition-all duration-200 hover:bg-red-600 hover:text-white group-hover:opacity-100" class="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-lg bg-black/50 text-white/80 opacity-0 backdrop-blur-sm transition-all duration-200 hover:bg-red-600 hover:text-white group-hover:opacity-100"
title="Löschen" title="Löschen"
@ -203,6 +204,7 @@ function isExpired(expireDate) {
<!-- Full image link overlay --> <!-- Full image link overlay -->
<a <a
data-testid="slide-grid-fullimage-link"
v-if="fullImageUrl(slide)" v-if="fullImageUrl(slide)"
:href="fullImageUrl(slide)" :href="fullImageUrl(slide)"
target="_blank" target="_blank"
@ -285,6 +287,7 @@ function isExpired(expireDate) {
class="flex items-center gap-1.5" class="flex items-center gap-1.5"
> >
<input <input
data-testid="slide-grid-expire-input"
v-model="editingExpireValue" v-model="editingExpireValue"
type="date" type="date"
class="w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-amber-400 focus:outline-none focus:ring-1 focus:ring-amber-400/40" class="w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-amber-400 focus:outline-none focus:ring-1 focus:ring-amber-400/40"
@ -292,6 +295,7 @@ function isExpired(expireDate) {
@keydown.escape="cancelEditExpire" @keydown.escape="cancelEditExpire"
/> />
<button <button
data-testid="slide-grid-expire-save"
@click="saveExpireDate(slide)" @click="saveExpireDate(slide)"
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-500 text-white shadow-sm transition hover:bg-amber-600" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-500 text-white shadow-sm transition hover:bg-amber-600"
title="Speichern" title="Speichern"
@ -301,6 +305,7 @@ function isExpired(expireDate) {
</svg> </svg>
</button> </button>
<button <button
data-testid="slide-grid-expire-cancel"
@click="cancelEditExpire" @click="cancelEditExpire"
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-500 shadow-sm transition hover:bg-gray-50" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-500 shadow-sm transition hover:bg-gray-50"
title="Abbrechen" title="Abbrechen"

View file

@ -122,7 +122,7 @@ function dismissError() {
</script> </script>
<template> <template>
<div class="slide-uploader"> <div data-testid="slide-uploader" class="slide-uploader">
<!-- Expire date picker for information slides --> <!-- Expire date picker for information slides -->
<div <div
v-if="showExpireDate" v-if="showExpireDate"
@ -132,6 +132,7 @@ function dismissError() {
Ablaufdatum für neue Folien Ablaufdatum für neue Folien
</label> </label>
<input <input
data-testid="slide-uploader-expire-input"
v-model="expireDate" v-model="expireDate"
type="date" type="date"
class="rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-700 shadow-sm transition focus:border-amber-400 focus:outline-none focus:ring-2 focus:ring-amber-400/30" class="rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-700 shadow-sm transition focus:border-amber-400 focus:outline-none focus:ring-2 focus:ring-amber-400/30"
@ -156,6 +157,7 @@ function dismissError() {
</svg> </svg>
<span class="flex-1">{{ uploadError }}</span> <span class="flex-1">{{ uploadError }}</span>
<button <button
data-testid="slide-uploader-error-dismiss"
@click="dismissError" @click="dismissError"
class="shrink-0 rounded-lg p-0.5 text-red-400 transition hover:text-red-600" class="shrink-0 rounded-lg p-0.5 text-red-400 transition hover:text-red-600"
> >
@ -196,6 +198,7 @@ function dismissError() {
<!-- Dropzone --> <!-- Dropzone -->
<Vue3Dropzone <Vue3Dropzone
data-testid="slide-uploader-dropzone"
v-model="files" v-model="files"
:multiple="true" :multiple="true"
:accept="acceptedTypes" :accept="acceptedTypes"

View file

@ -210,7 +210,7 @@ onUnmounted(() => {
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4 sm:p-8" class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4 sm:p-8"
@click="closeOnBackdrop" @click="closeOnBackdrop"
> >
<div class="relative w-full max-w-4xl rounded-xl bg-white shadow-2xl"> <div data-testid="song-edit-modal" class="relative w-full max-w-4xl rounded-xl bg-white shadow-2xl">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4"> <div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@ -299,6 +299,7 @@ onUnmounted(() => {
</Transition> </Transition>
<button <button
data-testid="song-edit-modal-close-button"
type="button" type="button"
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600" class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
@click="emit('close')" @click="emit('close')"
@ -371,6 +372,7 @@ onUnmounted(() => {
<p class="text-red-600">{{ error }}</p> <p class="text-red-600">{{ error }}</p>
<button <button
data-testid="song-edit-modal-error-close-button"
type="button" type="button"
class="mt-4 rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300" class="mt-4 rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300"
@click="emit('close')" @click="emit('close')"
@ -399,6 +401,7 @@ onUnmounted(() => {
Titel Titel
</label> </label>
<input <input
data-testid="song-edit-modal-title-input"
id="song-edit-title" id="song-edit-title"
v-model="title" v-model="title"
type="text" type="text"
@ -417,6 +420,7 @@ onUnmounted(() => {
CCLI-ID CCLI-ID
</label> </label>
<input <input
data-testid="song-edit-modal-ccli-input"
id="song-edit-ccli" id="song-edit-ccli"
v-model="ccliId" v-model="ccliId"
type="text" type="text"
@ -464,6 +468,7 @@ onUnmounted(() => {
Copyright-Text Copyright-Text
</label> </label>
<textarea <textarea
data-testid="song-edit-modal-copyright-textarea"
id="song-edit-copyright" id="song-edit-copyright"
v-model="copyrightText" v-model="copyrightText"
rows="3" rows="3"

View file

@ -92,7 +92,7 @@ const closeOnBackdrop = (e) => {
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4 sm:p-8" class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4 sm:p-8"
@click="closeOnBackdrop" @click="closeOnBackdrop"
> >
<div class="relative w-full max-w-3xl rounded-xl bg-white shadow-2xl"> <div data-testid="song-preview-modal" class="relative w-full max-w-3xl rounded-xl bg-white shadow-2xl">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4"> <div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<div> <div>
@ -107,6 +107,7 @@ const closeOnBackdrop = (e) => {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<a <a
data-testid="song-preview-modal-pdf-link"
:href="pdfUrl" :href="pdfUrl"
class="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-indigo-700" class="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-indigo-700"
target="_blank" target="_blank"
@ -128,6 +129,7 @@ const closeOnBackdrop = (e) => {
</a> </a>
<button <button
data-testid="song-preview-modal-close-button"
type="button" type="button"
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600" class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
@click="emit('close')" @click="emit('close')"
@ -325,6 +327,7 @@ const contrastColor = (hexColor) => {
> >
<p class="text-red-600">{{ error }}</p> <p class="text-red-600">{{ error }}</p>
<button <button
data-testid="song-preview-modal-error-close-button"
type="button" type="button"
class="mt-4 rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300" class="mt-4 rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300"
@click="close" @click="close"
@ -391,6 +394,7 @@ const contrastColor = (hexColor) => {
<!-- Close Button --> <!-- Close Button -->
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<button <button
data-testid="song-preview-modal-bottom-close-button"
type="button" type="button"
class="rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300" class="rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300"
@click="close" @click="close"

View file

@ -67,6 +67,7 @@ function triggerSync() {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Logo / App Name --> <!-- Logo / App Name -->
<Link <Link
data-testid="auth-layout-logo"
:href="route('dashboard')" :href="route('dashboard')"
class="mr-6 flex items-center gap-2.5 transition-opacity hover:opacity-80" class="mr-6 flex items-center gap-2.5 transition-opacity hover:opacity-80"
> >
@ -83,12 +84,14 @@ function triggerSync() {
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden items-center gap-1 sm:flex"> <div class="hidden items-center gap-1 sm:flex">
<NavLink <NavLink
data-testid="auth-layout-nav-services"
:href="route('services.index')" :href="route('services.index')"
:active="route().current('services.*')" :active="route().current('services.*')"
> >
Services Services
</NavLink> </NavLink>
<NavLink <NavLink
data-testid="auth-layout-nav-songs"
v-if="$page.props.ziggy?.routes?.['songs.index']" v-if="$page.props.ziggy?.routes?.['songs.index']"
:href="route('songs.index')" :href="route('songs.index')"
:active="route().current('songs.*')" :active="route().current('songs.*')"
@ -111,6 +114,7 @@ function triggerSync() {
<!-- Sync Info & Button --> <!-- Sync Info & Button -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span
data-testid="auth-layout-sync-timestamp"
v-if="formattedSyncDate" v-if="formattedSyncDate"
class="text-xs text-gray-400" class="text-xs text-gray-400"
> >
@ -118,6 +122,7 @@ function triggerSync() {
</span> </span>
<button <button
data-testid="auth-layout-sync-button"
@click="triggerSync" @click="triggerSync"
:disabled="syncing" :disabled="syncing"
class="group inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700 focus:outline-none focus:ring-2 focus:ring-amber-400/40 focus:ring-offset-1 disabled:cursor-wait disabled:opacity-60" class="group inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700 focus:outline-none focus:ring-2 focus:ring-amber-400/40 focus:ring-offset-1 disabled:cursor-wait disabled:opacity-60"
@ -142,6 +147,7 @@ function triggerSync() {
<Dropdown align="right" width="48"> <Dropdown align="right" width="48">
<template #trigger> <template #trigger>
<button <button
data-testid="auth-layout-user-dropdown-trigger"
type="button" type="button"
class="flex items-center gap-2 rounded-lg p-1.5 text-sm transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-amber-400/40" class="flex items-center gap-2 rounded-lg p-1.5 text-sm transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-amber-400/40"
> >
@ -180,6 +186,7 @@ function triggerSync() {
{{ user?.email }} {{ user?.email }}
</div> </div>
<DropdownLink <DropdownLink
data-testid="auth-layout-logout-link"
:href="route('logout')" :href="route('logout')"
method="post" method="post"
as="button" as="button"
@ -194,6 +201,7 @@ function triggerSync() {
<!-- Mobile Hamburger --> <!-- Mobile Hamburger -->
<div class="-me-2 flex items-center sm:hidden"> <div class="-me-2 flex items-center sm:hidden">
<button <button
data-testid="auth-layout-mobile-hamburger"
@click="showingNavigationDropdown = !showingNavigationDropdown" @click="showingNavigationDropdown = !showingNavigationDropdown"
class="inline-flex items-center justify-center rounded-lg p-2 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus:bg-gray-100 focus:outline-none" class="inline-flex items-center justify-center rounded-lg p-2 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus:bg-gray-100 focus:outline-none"
> >
@ -231,12 +239,14 @@ function triggerSync() {
<!-- Mobile Navigation --> <!-- Mobile Navigation -->
<div class="space-y-1 pb-3 pt-2"> <div class="space-y-1 pb-3 pt-2">
<ResponsiveNavLink <ResponsiveNavLink
data-testid="auth-layout-mobile-nav-services"
:href="route('services.index')" :href="route('services.index')"
:active="route().current('services.*')" :active="route().current('services.*')"
> >
Services Services
</ResponsiveNavLink> </ResponsiveNavLink>
<ResponsiveNavLink <ResponsiveNavLink
data-testid="auth-layout-mobile-nav-songs"
v-if="$page.props.ziggy?.routes?.['songs.index']" v-if="$page.props.ziggy?.routes?.['songs.index']"
:href="route('songs.index')" :href="route('songs.index')"
:active="route().current('songs.*')" :active="route().current('songs.*')"
@ -248,6 +258,7 @@ function triggerSync() {
<!-- Mobile Sync --> <!-- Mobile Sync -->
<div class="border-t border-gray-100 px-4 py-3"> <div class="border-t border-gray-100 px-4 py-3">
<button <button
data-testid="auth-layout-mobile-sync-button"
@click="triggerSync" @click="triggerSync"
:disabled="syncing" :disabled="syncing"
class="flex w-full items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-amber-50 hover:text-amber-700 disabled:opacity-60" class="flex w-full items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-amber-50 hover:text-amber-700 disabled:opacity-60"
@ -286,6 +297,7 @@ function triggerSync() {
<div class="mt-3 space-y-1"> <div class="mt-3 space-y-1">
<ResponsiveNavLink <ResponsiveNavLink
data-testid="auth-layout-mobile-logout-link"
:href="route('logout')" :href="route('logout')"
method="post" method="post"
as="button" as="button"

View file

@ -8,7 +8,7 @@ import { Link } from '@inertiajs/vue3';
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0" class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0"
> >
<div> <div>
<Link href="/"> <Link data-testid="guest-layout-logo-link" href="/">
<ApplicationLogo class="h-20 w-20 fill-current text-gray-500" /> <ApplicationLogo class="h-20 w-20 fill-current text-gray-500" />
</Link> </Link>
</div> </div>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <div data-testid="main-layout">
<slot /> <slot />
</div> </div>
</template> </template>

View file

@ -23,6 +23,7 @@ defineProps({
<a <a
:href="route('auth.churchtools')" :href="route('auth.churchtools')"
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
data-testid="login-oauth-button"
> >
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
@ -31,6 +32,7 @@ defineProps({
</a> </a>
<button <button
data-testid="login-test-button"
v-if="canDevLogin" v-if="canDevLogin"
@click="router.post(route('dev-login'))" @click="router.post(route('dev-login'))"
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-amber-500 px-6 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2" class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-amber-500 px-6 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"

View file

@ -21,7 +21,7 @@ import { Head } from '@inertiajs/vue3';
class="overflow-hidden bg-white shadow-sm sm:rounded-lg" class="overflow-hidden bg-white shadow-sm sm:rounded-lg"
> >
<div class="p-6 text-gray-900"> <div class="p-6 text-gray-900">
Du bist angemeldet als {{ $page.props.auth.user.name }}. <span data-testid="dashboard-welcome-text">Du bist angemeldet als {{ $page.props.auth.user.name }}.</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -123,6 +123,7 @@ function blockBadgeLabel(key) {
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
data-testid="service-edit-back-icon-button"
type="button" type="button"
class="group flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-400 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-600" class="group flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-400 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-600"
@click="goBack" @click="goBack"
@ -150,6 +151,7 @@ function blockBadgeLabel(key) {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
data-testid="service-edit-back-button"
type="button" type="button"
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm font-medium text-gray-600 shadow-sm transition-all hover:border-gray-300 hover:bg-gray-50" class="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm font-medium text-gray-600 shadow-sm transition-all hover:border-gray-300 hover:bg-gray-50"
@click="goBack" @click="goBack"
@ -174,6 +176,7 @@ function blockBadgeLabel(key) {
> >
<!-- Block header (clickable) --> <!-- Block header (clickable) -->
<button <button
data-testid="service-edit-block-toggle"
type="button" type="button"
class="flex w-full items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-gray-50/60" class="flex w-full items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-gray-50/60"
@click="toggleBlock(block.key)" @click="toggleBlock(block.key)"

View file

@ -176,12 +176,12 @@ function stateIconClass(isDone) {
</Transition> </Transition>
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm"> <div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<div v-if="services.length === 0" class="p-8 text-center text-sm text-gray-500"> <div v-if="services.length === 0" data-testid="service-list-empty" class="p-8 text-center text-sm text-gray-500">
Aktuell gibt es keine heutigen oder kommenden Services. Aktuell gibt es keine heutigen oder kommenden Services.
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table data-testid="service-list-table" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">Titel</th> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">Titel</th>
@ -195,7 +195,7 @@ function stateIconClass(isDone) {
</thead> </thead>
<tbody class="divide-y divide-gray-100 bg-white"> <tbody class="divide-y divide-gray-100 bg-white">
<tr v-for="service in services" :key="service.id" class="align-top hover:bg-gray-50/60"> <tr v-for="service in services" :key="service.id" :data-testid="`service-list-row-${service.id}`" class="align-top hover:bg-gray-50/60">
<td class="px-4 py-4"> <td class="px-4 py-4">
<div class="font-medium text-gray-900">{{ service.title }}</div> <div class="font-medium text-gray-900">{{ service.title }}</div>
<div class="mt-1 text-xs text-gray-500">{{ formatDate(service.date) }}</div> <div class="mt-1 text-xs text-gray-500">{{ formatDate(service.date) }}</div>
@ -251,6 +251,7 @@ function stateIconClass(isDone) {
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<template v-if="service.finalized_at"> <template v-if="service.finalized_at">
<button <button
data-testid="service-list-reopen-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md border border-amber-300 bg-amber-50 px-3 py-1.5 text-xs font-semibold text-amber-800 transition hover:bg-amber-100" class="inline-flex items-center justify-center rounded-md border border-amber-300 bg-amber-50 px-3 py-1.5 text-xs font-semibold text-amber-800 transition hover:bg-amber-100"
@click="reopenService(service.id)" @click="reopenService(service.id)"
@ -258,6 +259,7 @@ function stateIconClass(isDone) {
Wieder öffnen Wieder öffnen
</button> </button>
<button <button
data-testid="service-list-download-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
@click="downloadService(service.id)" @click="downloadService(service.id)"
@ -268,6 +270,7 @@ function stateIconClass(isDone) {
<template v-else> <template v-else>
<button <button
data-testid="service-list-edit-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
@click="router.get(route('services.edit', service.id))" @click="router.get(route('services.edit', service.id))"
@ -275,6 +278,7 @@ function stateIconClass(isDone) {
Bearbeiten Bearbeiten
</button> </button>
<button <button
data-testid="service-list-finalize-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-emerald-50 px-3 py-1.5 text-xs font-semibold text-emerald-800 transition hover:bg-emerald-100" class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-emerald-50 px-3 py-1.5 text-xs font-semibold text-emerald-800 transition hover:bg-emerald-100"
:disabled="finalizing" :disabled="finalizing"
@ -327,6 +331,7 @@ function stateIconClass(isDone) {
<div class="mt-5 flex justify-end gap-3"> <div class="mt-5 flex justify-end gap-3">
<button <button
data-testid="service-list-confirm-cancel-button"
type="button" type="button"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
@click="cancelFinalize" @click="cancelFinalize"
@ -334,6 +339,7 @@ function stateIconClass(isDone) {
Abbrechen Abbrechen
</button> </button>
<button <button
data-testid="service-list-confirm-submit-button"
type="button" type="button"
class="rounded-md border border-emerald-300 bg-emerald-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-emerald-700" class="rounded-md border border-emerald-300 bg-emerald-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-emerald-700"
@click="confirmFinalize" @click="confirmFinalize"

View file

@ -193,6 +193,7 @@ function pageRange() {
<!-- Upload Area --> <!-- Upload Area -->
<div <div
data-testid="song-list-upload-area"
class="group relative mb-6 cursor-pointer overflow-hidden rounded-xl border-2 border-dashed transition-all duration-300" class="group relative mb-6 cursor-pointer overflow-hidden rounded-xl border-2 border-dashed transition-all duration-300"
:class="isDragging :class="isDragging
? 'border-amber-400 bg-amber-50/80 shadow-lg shadow-amber-100/50' ? 'border-amber-400 bg-amber-50/80 shadow-lg shadow-amber-100/50'
@ -203,6 +204,7 @@ function pageRange() {
@click="triggerFileInput" @click="triggerFileInput"
> >
<input <input
data-testid="song-list-file-input"
ref="fileInput" ref="fileInput"
type="file" type="file"
multiple multiple
@ -257,6 +259,7 @@ function pageRange() {
</svg> </svg>
</div> </div>
<input <input
data-testid="song-list-search-input"
v-model="search" v-model="search"
type="text" type="text"
placeholder="Songs durchsuchen (Name oder CCLI-ID)…" placeholder="Songs durchsuchen (Name oder CCLI-ID)…"
@ -271,6 +274,7 @@ function pageRange() {
leave-to-class="opacity-0" leave-to-class="opacity-0"
> >
<button <button
data-testid="song-list-search-clear"
v-if="search" v-if="search"
@click="search = ''" @click="search = ''"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-400 hover:text-gray-600" class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-400 hover:text-gray-600"
@ -394,6 +398,7 @@ function pageRange() {
<div class="flex items-center justify-end gap-1.5 opacity-0 transition-opacity group-hover:opacity-100"> <div class="flex items-center justify-end gap-1.5 opacity-0 transition-opacity group-hover:opacity-100">
<!-- Bearbeiten --> <!-- Bearbeiten -->
<button <button
data-testid="song-list-edit-button"
type="button" type="button"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700"
title="Bearbeiten" title="Bearbeiten"
@ -407,6 +412,7 @@ function pageRange() {
<!-- Übersetzen --> <!-- Übersetzen -->
<a <a
data-testid="song-list-translate-link"
:href="`/songs/${song.id}/translate`" :href="`/songs/${song.id}/translate`"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700"
title="Übersetzen" title="Übersetzen"
@ -419,6 +425,7 @@ function pageRange() {
<!-- Herunterladen --> <!-- Herunterladen -->
<button <button
data-testid="song-list-download-button"
type="button" type="button"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700"
title="Als .pro herunterladen" title="Als .pro herunterladen"
@ -432,6 +439,7 @@ function pageRange() {
<!-- Löschen --> <!-- Löschen -->
<button <button
data-testid="song-list-delete-button"
type="button" type="button"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-red-300 hover:bg-red-50 hover:text-red-700" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-red-300 hover:bg-red-50 hover:text-red-700"
title="Löschen" title="Löschen"
@ -461,6 +469,7 @@ function pageRange() {
<nav class="flex items-center gap-1"> <nav class="flex items-center gap-1">
<button <button
data-testid="song-list-pagination-prev"
:disabled="meta.current_page <= 1" :disabled="meta.current_page <= 1"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-xs text-gray-600 shadow-sm transition-all hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-xs text-gray-600 shadow-sm transition-all hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
@click="goToPage(meta.current_page - 1)" @click="goToPage(meta.current_page - 1)"
@ -488,6 +497,7 @@ function pageRange() {
</template> </template>
<button <button
data-testid="song-list-pagination-next"
:disabled="meta.current_page >= meta.last_page" :disabled="meta.current_page >= meta.last_page"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-xs text-gray-600 shadow-sm transition-all hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-xs text-gray-600 shadow-sm transition-all hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
@click="goToPage(meta.current_page + 1)" @click="goToPage(meta.current_page + 1)"
@ -539,6 +549,7 @@ function pageRange() {
<div class="mt-5 flex items-center justify-end gap-2.5"> <div class="mt-5 flex items-center justify-end gap-2.5">
<button <button
data-testid="song-list-delete-cancel-button"
type="button" type="button"
class="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50" class="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50"
@click="cancelDelete" @click="cancelDelete"
@ -546,6 +557,7 @@ function pageRange() {
Abbrechen Abbrechen
</button> </button>
<button <button
data-testid="song-list-delete-confirm-button"
type="button" type="button"
class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-red-700 disabled:opacity-60" class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-red-700 disabled:opacity-60"
:disabled="deleting" :disabled="deleting"
@ -565,6 +577,7 @@ function pageRange() {
<!-- Song Edit Modal --> <!-- Song Edit Modal -->
<SongEditModal <SongEditModal
data-testid="song-list-edit-modal"
:show="showEditModal" :show="showEditModal"
:song-id="editSongId" :song-id="editSongId"
@close="closeEditModal" @close="closeEditModal"

View file

@ -161,6 +161,7 @@ function rowsForSlide(slide) {
</div> </div>
<button <button
data-testid="translate-back-button"
type="button" type="button"
class="inline-flex items-center rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
@click="router.visit('/songs')" @click="router.visit('/songs')"
@ -180,12 +181,14 @@ function rowsForSlide(slide) {
<div class="mt-4 grid gap-3 md:grid-cols-[1fr,auto]"> <div class="mt-4 grid gap-3 md:grid-cols-[1fr,auto]">
<input <input
data-testid="translate-url-input"
v-model="sourceUrl" v-model="sourceUrl"
type="url" type="url"
placeholder="https://beispiel.de/lyrics" placeholder="https://beispiel.de/lyrics"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500" class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
> >
<button <button
data-testid="translate-fetch-button"
type="button" type="button"
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60" class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="isFetching" :disabled="isFetching"
@ -200,6 +203,7 @@ function rowsForSlide(slide) {
Text manuell einfuegen Text manuell einfuegen
</label> </label>
<textarea <textarea
data-testid="translate-source-textarea"
v-model="sourceText" v-model="sourceText"
rows="10" rows="10"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500" class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
@ -207,6 +211,7 @@ function rowsForSlide(slide) {
/> />
<div class="mt-3 flex justify-end"> <div class="mt-3 flex justify-end">
<button <button
data-testid="translate-apply-button"
type="button" type="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
@click="applyManualText" @click="applyManualText"
@ -244,6 +249,7 @@ function rowsForSlide(slide) {
</div> </div>
<button <button
data-testid="translate-save-button"
type="button" type="button"
class="inline-flex items-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60" class="inline-flex items-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="isSaving" :disabled="isSaving"
@ -292,6 +298,7 @@ function rowsForSlide(slide) {
Original Original
</label> </label>
<textarea <textarea
data-testid="translate-original-textarea"
:value="slide.text_content" :value="slide.text_content"
:rows="rowsForSlide(slide)" :rows="rowsForSlide(slide)"
readonly readonly
@ -304,6 +311,7 @@ function rowsForSlide(slide) {
Uebersetzung Uebersetzung
</label> </label>
<textarea <textarea
data-testid="translate-translation-textarea"
:value="slide.translated_text" :value="slide.translated_text"
:rows="rowsForSlide(slide)" :rows="rowsForSlide(slide)"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500" class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"