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:
parent
3a1ba1fc7d
commit
4520c1ce5f
|
|
@ -169,7 +169,7 @@ const deleteArrangement = () => {
|
|||
</script>
|
||||
|
||||
<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="min-w-64 flex-1">
|
||||
<label
|
||||
|
|
@ -179,6 +179,7 @@ const deleteArrangement = () => {
|
|||
Arrangement
|
||||
</label>
|
||||
<select
|
||||
data-testid="arrangement-select"
|
||||
id="arrangement-select"
|
||||
v-model="selectedId"
|
||||
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>
|
||||
|
||||
<button
|
||||
data-testid="arrangement-add-button"
|
||||
type="button"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700"
|
||||
@click="addArrangement"
|
||||
|
|
@ -202,6 +204,7 @@ const deleteArrangement = () => {
|
|||
</button>
|
||||
|
||||
<button
|
||||
data-testid="arrangement-clone-button"
|
||||
type="button"
|
||||
class="rounded-md bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800"
|
||||
@click="cloneArrangement"
|
||||
|
|
@ -210,6 +213,7 @@ const deleteArrangement = () => {
|
|||
</button>
|
||||
|
||||
<button
|
||||
data-testid="arrangement-delete-button"
|
||||
type="button"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-red-700"
|
||||
@click="deleteArrangement"
|
||||
|
|
@ -271,7 +275,7 @@ const deleteArrangement = () => {
|
|||
:key="`${group.id}-${index}`"
|
||||
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
|
||||
class="inline-flex rounded-full px-3 py-1 text-sm font-semibold text-white"
|
||||
|
|
@ -291,6 +295,7 @@ const deleteArrangement = () => {
|
|||
</label>
|
||||
|
||||
<button
|
||||
data-testid="arrangement-remove-button"
|
||||
type="button"
|
||||
class="rounded px-2 py-1 text-xs font-semibold text-red-600 hover:bg-red-50"
|
||||
@click="removeGroupAt(index)"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function handleSlideUpdated() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="information-block space-y-6">
|
||||
<div data-testid="information-block" class="information-block space-y-6">
|
||||
<!-- Block header -->
|
||||
<div class="border-b border-amber-200/60 pb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
@ -103,6 +103,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide uploader — information slides are GLOBAL (service_id = null) -->
|
||||
<SlideUploader
|
||||
data-testid="information-block-uploader"
|
||||
type="information"
|
||||
:service-id="null"
|
||||
:show-expire-date="true"
|
||||
|
|
@ -111,6 +112,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide grid with prominent expire dates -->
|
||||
<SlideGrid
|
||||
data-testid="information-block-grid"
|
||||
:slides="informationSlides"
|
||||
type="information"
|
||||
:show-expire-date="true"
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function handleSlideUpdated() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="moderation-block space-y-6">
|
||||
<div data-testid="moderation-block" class="moderation-block space-y-6">
|
||||
<!-- Block header -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
|
|
@ -50,6 +50,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide uploader -->
|
||||
<SlideUploader
|
||||
data-testid="moderation-block-uploader"
|
||||
type="moderation"
|
||||
:service-id="serviceId"
|
||||
:show-expire-date="false"
|
||||
|
|
@ -58,6 +59,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide grid -->
|
||||
<SlideGrid
|
||||
data-testid="moderation-block-grid"
|
||||
:slides="moderationSlides"
|
||||
type="moderation"
|
||||
:show-expire-date="false"
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function handleSlideUpdated() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sermon-block space-y-6">
|
||||
<div data-testid="sermon-block" class="sermon-block space-y-6">
|
||||
<!-- Block header -->
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
|
|
@ -50,6 +50,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide uploader -->
|
||||
<SlideUploader
|
||||
data-testid="sermon-block-uploader"
|
||||
type="sermon"
|
||||
:service-id="serviceId"
|
||||
:show-expire-date="false"
|
||||
|
|
@ -58,6 +59,7 @@ function handleSlideUpdated() {
|
|||
|
||||
<!-- Slide grid -->
|
||||
<SlideGrid
|
||||
data-testid="sermon-block-grid"
|
||||
:slides="sermonSlides"
|
||||
type="sermon"
|
||||
:show-expire-date="false"
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ const toastClasses = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div data-testid="songs-block" class="space-y-4">
|
||||
<Transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
|
|
@ -187,6 +187,7 @@ const toastClasses = () => {
|
|||
</Transition>
|
||||
|
||||
<div
|
||||
data-testid="songs-block-song-card"
|
||||
v-for="serviceSong in sortedSongs()"
|
||||
:key="serviceSong.id"
|
||||
class="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm"
|
||||
|
|
@ -242,6 +243,7 @@ const toastClasses = () => {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-testid="songs-block-request-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"
|
||||
@click="requestCreation(serviceSong.id)"
|
||||
|
|
@ -256,6 +258,7 @@ const toastClasses = () => {
|
|||
Song suchen
|
||||
</label>
|
||||
<input
|
||||
data-testid="songs-block-search-input"
|
||||
v-model="searchTerms[serviceSong.id]"
|
||||
type="text"
|
||||
placeholder="Titel oder CCLI eingeben"
|
||||
|
|
@ -268,6 +271,7 @@ const toastClasses = () => {
|
|||
Song aus DB auswaehlen
|
||||
</label>
|
||||
<select
|
||||
data-testid="songs-block-song-select"
|
||||
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"
|
||||
>
|
||||
|
|
@ -285,6 +289,7 @@ const toastClasses = () => {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-testid="songs-block-assign-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"
|
||||
@click="assignSong(serviceSong.id)"
|
||||
|
|
@ -308,6 +313,7 @@ const toastClasses = () => {
|
|||
class="inline-flex items-center gap-2 text-sm font-medium text-gray-800"
|
||||
>
|
||||
<input
|
||||
data-testid="songs-block-translation-checkbox"
|
||||
v-model="translationValues[serviceSong.id]"
|
||||
type="checkbox"
|
||||
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">
|
||||
<button
|
||||
data-testid="songs-block-preview-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"
|
||||
@click="showPlaceholder"
|
||||
|
|
@ -334,6 +341,7 @@ const toastClasses = () => {
|
|||
Vorschau
|
||||
</button>
|
||||
<button
|
||||
data-testid="songs-block-download-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"
|
||||
@click="showPlaceholder"
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const iconColors = {
|
|||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
data-testid="confirm-dialog-cancel-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"
|
||||
@click="emit('cancel')"
|
||||
|
|
@ -88,6 +89,7 @@ const iconColors = {
|
|||
{{ cancelLabel }}
|
||||
</button>
|
||||
<button
|
||||
data-testid="confirm-dialog-confirm-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="variantClasses[variant]"
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ function isExpired(expireDate) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slide-grid">
|
||||
<div data-testid="slide-grid" class="slide-grid">
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-if="sortedSlides.length === 0"
|
||||
|
|
@ -192,6 +192,7 @@ function isExpired(expireDate) {
|
|||
|
||||
<!-- Delete button overlay -->
|
||||
<button
|
||||
data-testid="slide-grid-delete-button"
|
||||
@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"
|
||||
title="Löschen"
|
||||
|
|
@ -203,6 +204,7 @@ function isExpired(expireDate) {
|
|||
|
||||
<!-- Full image link overlay -->
|
||||
<a
|
||||
data-testid="slide-grid-fullimage-link"
|
||||
v-if="fullImageUrl(slide)"
|
||||
:href="fullImageUrl(slide)"
|
||||
target="_blank"
|
||||
|
|
@ -285,6 +287,7 @@ function isExpired(expireDate) {
|
|||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<input
|
||||
data-testid="slide-grid-expire-input"
|
||||
v-model="editingExpireValue"
|
||||
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"
|
||||
|
|
@ -292,6 +295,7 @@ function isExpired(expireDate) {
|
|||
@keydown.escape="cancelEditExpire"
|
||||
/>
|
||||
<button
|
||||
data-testid="slide-grid-expire-save"
|
||||
@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"
|
||||
title="Speichern"
|
||||
|
|
@ -301,6 +305,7 @@ function isExpired(expireDate) {
|
|||
</svg>
|
||||
</button>
|
||||
<button
|
||||
data-testid="slide-grid-expire-cancel"
|
||||
@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"
|
||||
title="Abbrechen"
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ function dismissError() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slide-uploader">
|
||||
<div data-testid="slide-uploader" class="slide-uploader">
|
||||
<!-- Expire date picker for information slides -->
|
||||
<div
|
||||
v-if="showExpireDate"
|
||||
|
|
@ -132,6 +132,7 @@ function dismissError() {
|
|||
Ablaufdatum für neue Folien
|
||||
</label>
|
||||
<input
|
||||
data-testid="slide-uploader-expire-input"
|
||||
v-model="expireDate"
|
||||
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"
|
||||
|
|
@ -156,6 +157,7 @@ function dismissError() {
|
|||
</svg>
|
||||
<span class="flex-1">{{ uploadError }}</span>
|
||||
<button
|
||||
data-testid="slide-uploader-error-dismiss"
|
||||
@click="dismissError"
|
||||
class="shrink-0 rounded-lg p-0.5 text-red-400 transition hover:text-red-600"
|
||||
>
|
||||
|
|
@ -196,6 +198,7 @@ function dismissError() {
|
|||
|
||||
<!-- Dropzone -->
|
||||
<Vue3Dropzone
|
||||
data-testid="slide-uploader-dropzone"
|
||||
v-model="files"
|
||||
:multiple="true"
|
||||
:accept="acceptedTypes"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
@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 -->
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -299,6 +299,7 @@ onUnmounted(() => {
|
|||
</Transition>
|
||||
|
||||
<button
|
||||
data-testid="song-edit-modal-close-button"
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
@click="emit('close')"
|
||||
|
|
@ -371,6 +372,7 @@ onUnmounted(() => {
|
|||
<p class="text-red-600">{{ error }}</p>
|
||||
|
||||
<button
|
||||
data-testid="song-edit-modal-error-close-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"
|
||||
@click="emit('close')"
|
||||
|
|
@ -399,6 +401,7 @@ onUnmounted(() => {
|
|||
Titel
|
||||
</label>
|
||||
<input
|
||||
data-testid="song-edit-modal-title-input"
|
||||
id="song-edit-title"
|
||||
v-model="title"
|
||||
type="text"
|
||||
|
|
@ -417,6 +420,7 @@ onUnmounted(() => {
|
|||
CCLI-ID
|
||||
</label>
|
||||
<input
|
||||
data-testid="song-edit-modal-ccli-input"
|
||||
id="song-edit-ccli"
|
||||
v-model="ccliId"
|
||||
type="text"
|
||||
|
|
@ -464,6 +468,7 @@ onUnmounted(() => {
|
|||
Copyright-Text
|
||||
</label>
|
||||
<textarea
|
||||
data-testid="song-edit-modal-copyright-textarea"
|
||||
id="song-edit-copyright"
|
||||
v-model="copyrightText"
|
||||
rows="3"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
@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 -->
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div>
|
||||
|
|
@ -107,6 +107,7 @@ const closeOnBackdrop = (e) => {
|
|||
|
||||
<div class="flex items-center gap-3">
|
||||
<a
|
||||
data-testid="song-preview-modal-pdf-link"
|
||||
: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"
|
||||
target="_blank"
|
||||
|
|
@ -128,6 +129,7 @@ const closeOnBackdrop = (e) => {
|
|||
</a>
|
||||
|
||||
<button
|
||||
data-testid="song-preview-modal-close-button"
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
@click="emit('close')"
|
||||
|
|
@ -325,6 +327,7 @@ const contrastColor = (hexColor) => {
|
|||
>
|
||||
<p class="text-red-600">{{ error }}</p>
|
||||
<button
|
||||
data-testid="song-preview-modal-error-close-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"
|
||||
@click="close"
|
||||
|
|
@ -391,6 +394,7 @@ const contrastColor = (hexColor) => {
|
|||
<!-- Close Button -->
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
data-testid="song-preview-modal-bottom-close-button"
|
||||
type="button"
|
||||
class="rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300"
|
||||
@click="close"
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ function triggerSync() {
|
|||
<div class="flex items-center gap-1">
|
||||
<!-- Logo / App Name -->
|
||||
<Link
|
||||
data-testid="auth-layout-logo"
|
||||
:href="route('dashboard')"
|
||||
class="mr-6 flex items-center gap-2.5 transition-opacity hover:opacity-80"
|
||||
>
|
||||
|
|
@ -83,12 +84,14 @@ function triggerSync() {
|
|||
<!-- Desktop Navigation -->
|
||||
<div class="hidden items-center gap-1 sm:flex">
|
||||
<NavLink
|
||||
data-testid="auth-layout-nav-services"
|
||||
:href="route('services.index')"
|
||||
:active="route().current('services.*')"
|
||||
>
|
||||
Services
|
||||
</NavLink>
|
||||
<NavLink
|
||||
data-testid="auth-layout-nav-songs"
|
||||
v-if="$page.props.ziggy?.routes?.['songs.index']"
|
||||
:href="route('songs.index')"
|
||||
:active="route().current('songs.*')"
|
||||
|
|
@ -111,6 +114,7 @@ function triggerSync() {
|
|||
<!-- Sync Info & Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
data-testid="auth-layout-sync-timestamp"
|
||||
v-if="formattedSyncDate"
|
||||
class="text-xs text-gray-400"
|
||||
>
|
||||
|
|
@ -118,6 +122,7 @@ function triggerSync() {
|
|||
</span>
|
||||
|
||||
<button
|
||||
data-testid="auth-layout-sync-button"
|
||||
@click="triggerSync"
|
||||
: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"
|
||||
|
|
@ -142,6 +147,7 @@ function triggerSync() {
|
|||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button
|
||||
data-testid="auth-layout-user-dropdown-trigger"
|
||||
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"
|
||||
>
|
||||
|
|
@ -180,6 +186,7 @@ function triggerSync() {
|
|||
{{ user?.email }}
|
||||
</div>
|
||||
<DropdownLink
|
||||
data-testid="auth-layout-logout-link"
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
|
|
@ -194,6 +201,7 @@ function triggerSync() {
|
|||
<!-- Mobile Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button
|
||||
data-testid="auth-layout-mobile-hamburger"
|
||||
@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"
|
||||
>
|
||||
|
|
@ -231,12 +239,14 @@ function triggerSync() {
|
|||
<!-- Mobile Navigation -->
|
||||
<div class="space-y-1 pb-3 pt-2">
|
||||
<ResponsiveNavLink
|
||||
data-testid="auth-layout-mobile-nav-services"
|
||||
:href="route('services.index')"
|
||||
:active="route().current('services.*')"
|
||||
>
|
||||
Services
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
data-testid="auth-layout-mobile-nav-songs"
|
||||
v-if="$page.props.ziggy?.routes?.['songs.index']"
|
||||
:href="route('songs.index')"
|
||||
:active="route().current('songs.*')"
|
||||
|
|
@ -248,6 +258,7 @@ function triggerSync() {
|
|||
<!-- Mobile Sync -->
|
||||
<div class="border-t border-gray-100 px-4 py-3">
|
||||
<button
|
||||
data-testid="auth-layout-mobile-sync-button"
|
||||
@click="triggerSync"
|
||||
: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"
|
||||
|
|
@ -286,6 +297,7 @@ function triggerSync() {
|
|||
|
||||
<div class="mt-3 space-y-1">
|
||||
<ResponsiveNavLink
|
||||
data-testid="auth-layout-mobile-logout-link"
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<Link data-testid="guest-layout-logo-link" href="/">
|
||||
<ApplicationLogo class="h-20 w-20 fill-current text-gray-500" />
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div data-testid="main-layout">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ defineProps({
|
|||
<a
|
||||
: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"
|
||||
data-testid="login-oauth-button"
|
||||
>
|
||||
<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" />
|
||||
|
|
@ -31,6 +32,7 @@ defineProps({
|
|||
</a>
|
||||
|
||||
<button
|
||||
data-testid="login-test-button"
|
||||
v-if="canDevLogin"
|
||||
@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"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Head } from '@inertiajs/vue3';
|
|||
class="overflow-hidden bg-white shadow-sm sm:rounded-lg"
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ function blockBadgeLabel(key) {
|
|||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
data-testid="service-edit-back-icon-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"
|
||||
@click="goBack"
|
||||
|
|
@ -150,6 +151,7 @@ function blockBadgeLabel(key) {
|
|||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
data-testid="service-edit-back-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"
|
||||
@click="goBack"
|
||||
|
|
@ -174,6 +176,7 @@ function blockBadgeLabel(key) {
|
|||
>
|
||||
<!-- Block header (clickable) -->
|
||||
<button
|
||||
data-testid="service-edit-block-toggle"
|
||||
type="button"
|
||||
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)"
|
||||
|
|
|
|||
|
|
@ -176,12 +176,12 @@ function stateIconClass(isDone) {
|
|||
</Transition>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<div class="font-medium text-gray-900">{{ service.title }}</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">
|
||||
<template v-if="service.finalized_at">
|
||||
<button
|
||||
data-testid="service-list-reopen-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"
|
||||
@click="reopenService(service.id)"
|
||||
|
|
@ -258,6 +259,7 @@ function stateIconClass(isDone) {
|
|||
Wieder öffnen
|
||||
</button>
|
||||
<button
|
||||
data-testid="service-list-download-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"
|
||||
@click="downloadService(service.id)"
|
||||
|
|
@ -268,6 +270,7 @@ function stateIconClass(isDone) {
|
|||
|
||||
<template v-else>
|
||||
<button
|
||||
data-testid="service-list-edit-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"
|
||||
@click="router.get(route('services.edit', service.id))"
|
||||
|
|
@ -275,6 +278,7 @@ function stateIconClass(isDone) {
|
|||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
data-testid="service-list-finalize-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"
|
||||
:disabled="finalizing"
|
||||
|
|
@ -327,6 +331,7 @@ function stateIconClass(isDone) {
|
|||
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<button
|
||||
data-testid="service-list-confirm-cancel-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"
|
||||
@click="cancelFinalize"
|
||||
|
|
@ -334,6 +339,7 @@ function stateIconClass(isDone) {
|
|||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
data-testid="service-list-confirm-submit-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"
|
||||
@click="confirmFinalize"
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ function pageRange() {
|
|||
|
||||
<!-- Upload Area -->
|
||||
<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="isDragging
|
||||
? 'border-amber-400 bg-amber-50/80 shadow-lg shadow-amber-100/50'
|
||||
|
|
@ -203,6 +204,7 @@ function pageRange() {
|
|||
@click="triggerFileInput"
|
||||
>
|
||||
<input
|
||||
data-testid="song-list-file-input"
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
|
|
@ -257,6 +259,7 @@ function pageRange() {
|
|||
</svg>
|
||||
</div>
|
||||
<input
|
||||
data-testid="song-list-search-input"
|
||||
v-model="search"
|
||||
type="text"
|
||||
placeholder="Songs durchsuchen (Name oder CCLI-ID)…"
|
||||
|
|
@ -271,6 +274,7 @@ function pageRange() {
|
|||
leave-to-class="opacity-0"
|
||||
>
|
||||
<button
|
||||
data-testid="song-list-search-clear"
|
||||
v-if="search"
|
||||
@click="search = ''"
|
||||
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">
|
||||
<!-- Bearbeiten -->
|
||||
<button
|
||||
data-testid="song-list-edit-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"
|
||||
title="Bearbeiten"
|
||||
|
|
@ -407,6 +412,7 @@ function pageRange() {
|
|||
|
||||
<!-- Übersetzen -->
|
||||
<a
|
||||
data-testid="song-list-translate-link"
|
||||
: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"
|
||||
title="Übersetzen"
|
||||
|
|
@ -419,6 +425,7 @@ function pageRange() {
|
|||
|
||||
<!-- Herunterladen -->
|
||||
<button
|
||||
data-testid="song-list-download-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"
|
||||
title="Als .pro herunterladen"
|
||||
|
|
@ -432,6 +439,7 @@ function pageRange() {
|
|||
|
||||
<!-- Löschen -->
|
||||
<button
|
||||
data-testid="song-list-delete-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"
|
||||
title="Löschen"
|
||||
|
|
@ -461,6 +469,7 @@ function pageRange() {
|
|||
|
||||
<nav class="flex items-center gap-1">
|
||||
<button
|
||||
data-testid="song-list-pagination-prev"
|
||||
: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"
|
||||
@click="goToPage(meta.current_page - 1)"
|
||||
|
|
@ -488,6 +497,7 @@ function pageRange() {
|
|||
</template>
|
||||
|
||||
<button
|
||||
data-testid="song-list-pagination-next"
|
||||
: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"
|
||||
@click="goToPage(meta.current_page + 1)"
|
||||
|
|
@ -539,6 +549,7 @@ function pageRange() {
|
|||
|
||||
<div class="mt-5 flex items-center justify-end gap-2.5">
|
||||
<button
|
||||
data-testid="song-list-delete-cancel-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"
|
||||
@click="cancelDelete"
|
||||
|
|
@ -546,6 +557,7 @@ function pageRange() {
|
|||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
data-testid="song-list-delete-confirm-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"
|
||||
:disabled="deleting"
|
||||
|
|
@ -565,6 +577,7 @@ function pageRange() {
|
|||
|
||||
<!-- Song Edit Modal -->
|
||||
<SongEditModal
|
||||
data-testid="song-list-edit-modal"
|
||||
:show="showEditModal"
|
||||
:song-id="editSongId"
|
||||
@close="closeEditModal"
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ function rowsForSlide(slide) {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-testid="translate-back-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"
|
||||
@click="router.visit('/songs')"
|
||||
|
|
@ -180,12 +181,14 @@ function rowsForSlide(slide) {
|
|||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-[1fr,auto]">
|
||||
<input
|
||||
data-testid="translate-url-input"
|
||||
v-model="sourceUrl"
|
||||
type="url"
|
||||
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"
|
||||
>
|
||||
<button
|
||||
data-testid="translate-fetch-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"
|
||||
:disabled="isFetching"
|
||||
|
|
@ -200,6 +203,7 @@ function rowsForSlide(slide) {
|
|||
Text manuell einfuegen
|
||||
</label>
|
||||
<textarea
|
||||
data-testid="translate-source-textarea"
|
||||
v-model="sourceText"
|
||||
rows="10"
|
||||
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">
|
||||
<button
|
||||
data-testid="translate-apply-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"
|
||||
@click="applyManualText"
|
||||
|
|
@ -244,6 +249,7 @@ function rowsForSlide(slide) {
|
|||
</div>
|
||||
|
||||
<button
|
||||
data-testid="translate-save-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"
|
||||
:disabled="isSaving"
|
||||
|
|
@ -292,6 +298,7 @@ function rowsForSlide(slide) {
|
|||
Original
|
||||
</label>
|
||||
<textarea
|
||||
data-testid="translate-original-textarea"
|
||||
:value="slide.text_content"
|
||||
:rows="rowsForSlide(slide)"
|
||||
readonly
|
||||
|
|
@ -304,6 +311,7 @@ function rowsForSlide(slide) {
|
|||
Uebersetzung
|
||||
</label>
|
||||
<textarea
|
||||
data-testid="translate-translation-textarea"
|
||||
:value="slide.translated_text"
|
||||
:rows="rowsForSlide(slide)"
|
||||
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
|
||||
|
|
|
|||
Loading…
Reference in a new issue