Había dos listados: una tabla ordenable para el catálogo y una lista aparte para los discos. Cada arreglo había que hacerlo dos veces y el resultado era que la portada estaba en una y el año en la otra. Ahora es un solo componente, y lo que cambia son las columnas de contexto, que se encienden por props: dentro de un disco, la columna «Álbum» diría diez veces lo mismo. Solo recibe canciones. El estilo, el disco y la portada los resuelve contra el catálogo que ya baja en `page.data`, así que quien la usa no tiene que armar filas a mano, que era lo que obligaba a repetir código. Cada fila lleva su portada —con la ampliada al pasar el ratón—, y debajo del título van los géneros y las etiquetas en vez del primer verso: la letra está entera en la ficha, y aquí lo que sirve para elegir es de qué está hecho el tema. Las etiquetas se cortan por número, con un «+2» al final: recortarlas con un degradado dejaba «Autoengaño» en «Autoeng», que no parece una elección sino algo roto. La cola ya se puede llenar. Hasta ahora solo se podía sustituir —cada play la vaciaba y la llenaba con su contexto—, así que no había forma de armar una escucha cogiendo temas de discos distintos. `encolar()` es la otra mitad, y con la cola vacía arranca, porque un botón que no hace nada es peor que no tenerlo. Los géneros de una canción entran en el modelo: `cancion_genero` existía en la base y no llegaba a la web. Son cosa distinta del estilo —el estilo es cómo se ordena la obra aquí y es uno solo; los géneros son de qué está hecha la canción y pueden ser varios— y llevan al glosario. De paso: las letras largas van a dos columnas de 37ch, que es el mínimo que deja caber dos y a la vez el verso más largo del catálogo —importa afinarlo, porque en una letra dónde acaba cada verso es parte del texto—; la página baja de 2808 a 2023 píxeles. Contacto sale de la barra de menú y se queda solo en el pie. Y el titular pasa a «Letrista y creador musical». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
4d2a2072c2
commit
fa4de8b2f6
@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Añadir un tema al final de la cola de reproducción.
|
||||||
|
*
|
||||||
|
* A diferencia de favoritos, esto no toca el servidor: la cola vive en el
|
||||||
|
* navegador y se pierde al cerrar. Por eso no hace falta cuenta y por eso es
|
||||||
|
* un botón y no un formulario.
|
||||||
|
*
|
||||||
|
* Da acuse de recibo. Sin él, pulsar «añadir a la cola» cuando ya hay algo
|
||||||
|
* sonando no cambia nada de lo que se ve —la cola está plegada— y parece que
|
||||||
|
* el botón está roto.
|
||||||
|
*/
|
||||||
|
import Icono from './Icono.svelte';
|
||||||
|
import { reproductor } from '$lib/player/player.svelte';
|
||||||
|
import { aPista } from '$lib/player/pista';
|
||||||
|
import type { Cancion } from '$lib/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cancion: Cancion;
|
||||||
|
/** Solo el icono, para una fila de una tabla. */
|
||||||
|
compacto?: boolean;
|
||||||
|
tamano?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { cancion, compacto = false, tamano = 18 }: Props = $props();
|
||||||
|
|
||||||
|
const pista = $derived(aPista(cancion));
|
||||||
|
const sinAudio = $derived(!pista.audio);
|
||||||
|
const yaEsta = $derived(reproductor.cola.some((p) => p.id === pista.id));
|
||||||
|
|
||||||
|
let acusado = $state(false);
|
||||||
|
let reloj: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
function anadir(): void {
|
||||||
|
reproductor.encolar([pista]);
|
||||||
|
acusado = true;
|
||||||
|
clearTimeout(reloj);
|
||||||
|
reloj = setTimeout(() => (acusado = false), 1600);
|
||||||
|
}
|
||||||
|
|
||||||
|
const texto = $derived(
|
||||||
|
sinAudio
|
||||||
|
? `«${cancion.titulo}» todavía no tiene audio`
|
||||||
|
: yaEsta
|
||||||
|
? `«${cancion.titulo}» ya está en la cola`
|
||||||
|
: `Añadir «${cancion.titulo}» a la cola`
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
La envoltura está posicionada a propósito. El acuse de abajo es
|
||||||
|
`position: absolute` —lo pone `.visually-hidden`— y, sin un ancestro
|
||||||
|
posicionado cerca, su bloque contenedor acaba siendo la página entera: dentro
|
||||||
|
de una tabla que se desplaza a lo ancho, eso se sale del marco y estira el
|
||||||
|
documento trescientos píxeles a la derecha.
|
||||||
|
-->
|
||||||
|
<span class="envoltura">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="boton"
|
||||||
|
class:compacto
|
||||||
|
class:puesta={yaEsta}
|
||||||
|
disabled={sinAudio}
|
||||||
|
title={texto}
|
||||||
|
aria-label={texto}
|
||||||
|
onclick={anadir}
|
||||||
|
>
|
||||||
|
<Icono nombre="anadir-cola" {tamano} />
|
||||||
|
{#if !compacto}
|
||||||
|
<span>{yaEsta ? 'En la cola' : 'A la cola'}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
El acuse se anuncia, además de verse: quien no mira la pantalla necesita
|
||||||
|
saber que la pulsación ha hecho algo.
|
||||||
|
-->
|
||||||
|
<span class="visually-hidden" role="status">
|
||||||
|
{acusado ? `«${cancion.titulo}» añadida a la cola.` : ''}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.envoltura {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
padding: var(--boton-relleno);
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: var(--radius-boton);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--boton-texto);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton:hover:not(:disabled),
|
||||||
|
.boton:focus-visible {
|
||||||
|
color: var(--c-text);
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton.puesta {
|
||||||
|
color: var(--c-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boton.compacto {
|
||||||
|
padding: var(--boton-relleno-corto);
|
||||||
|
font-size: var(--boton-texto-corto);
|
||||||
|
border: none;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.boton {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,325 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Listado de temas.
|
|
||||||
*
|
|
||||||
* Todas las canciones que recibe forman un unico contexto de reproduccion:
|
|
||||||
* al pulsar play sobre cualquiera, la cola se llena con la lista entera y
|
|
||||||
* sigue sonando sola hasta el final, como en un album.
|
|
||||||
*/
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import BotonCarrito from './BotonCarrito.svelte';
|
|
||||||
import BotonReproducir from './BotonReproducir.svelte';
|
|
||||||
import Etiquetas from './Etiquetas.svelte';
|
|
||||||
import SinPortada from './SinPortada.svelte';
|
|
||||||
import { aPistas } from '$lib/player/pista';
|
|
||||||
import { reproductor } from '$lib/player/player.svelte';
|
|
||||||
import type { Cancion } from '$lib/types';
|
|
||||||
import { listaDeNombres, segundosADuracion } from '$lib/utils/format';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
canciones: Cancion[];
|
|
||||||
/** Numera los temas segun su posicion en el disco. */
|
|
||||||
numerar?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { canciones, numerar = true }: Props = $props();
|
|
||||||
|
|
||||||
const pistas = $derived(aPistas(canciones));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* La portada ampliada al pasar el ratón por encima de la miniatura.
|
|
||||||
*
|
|
||||||
* Va en `position: fixed` y no dentro de la fila porque una fila mide 64 px de
|
|
||||||
* alto: colgada de ahí, la tarjeta se saldría de la lista y quedaría a merced
|
|
||||||
* de cualquier `overflow` de los antepasados. Fija, solo depende de la
|
|
||||||
* ventana, y por eso se puede pegar al borde sin que nada la recorte.
|
|
||||||
*
|
|
||||||
* Solo con ratón. No se abre con el teclado a propósito: haría falta un punto
|
|
||||||
* de tabulación por fila —diez en un disco— para enseñar una imagen que ya
|
|
||||||
* está, más grande, en la ficha del tema a la que lleva el título.
|
|
||||||
*/
|
|
||||||
const LADO = 260;
|
|
||||||
const AIRE = 12;
|
|
||||||
/** El lado más el relleno y la línea del título: lo que mide de alto. */
|
|
||||||
const ALTO = LADO + 46;
|
|
||||||
|
|
||||||
let vista = $state<{ src: string; titulo: string; x: number; y: number } | null>(null);
|
|
||||||
|
|
||||||
function asomar(evento: MouseEvent, cancion: Cancion): void {
|
|
||||||
if (!cancion.portada) return;
|
|
||||||
|
|
||||||
const caja = (evento.currentTarget as HTMLElement).getBoundingClientRect();
|
|
||||||
const margen = 16;
|
|
||||||
|
|
||||||
/* A la derecha si cabe; si no, al otro lado. En una ventana estrecha no
|
|
||||||
cabe en ninguno de los dos, y entonces se pega al margen izquierdo. */
|
|
||||||
const derecha = caja.right + AIRE;
|
|
||||||
const izquierda = caja.left - AIRE - LADO;
|
|
||||||
const x =
|
|
||||||
derecha + LADO <= window.innerWidth - margen
|
|
||||||
? derecha
|
|
||||||
: izquierda >= margen
|
|
||||||
? izquierda
|
|
||||||
: margen;
|
|
||||||
|
|
||||||
/* Centrada en la fila, sin salirse por arriba ni por abajo. */
|
|
||||||
const centro = caja.top + caja.height / 2 - ALTO / 2;
|
|
||||||
const y = Math.min(Math.max(centro, margen), window.innerHeight - ALTO - margen);
|
|
||||||
|
|
||||||
vista = { src: cancion.portada, titulo: cancion.titulo, x, y };
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if canciones.length === 0}
|
|
||||||
<p class="muted">Todavía no hay temas publicados en esta sección.</p>
|
|
||||||
{:else}
|
|
||||||
<ol class="lista" aria-label="Temas">
|
|
||||||
{#each canciones as cancion, i (cancion.slug)}
|
|
||||||
<li class="fila" class:sonando={reproductor.esActual(cancion.slug)}>
|
|
||||||
<div class="control">
|
|
||||||
{#if numerar && cancion.numero !== null}
|
|
||||||
<span class="numero muted" aria-hidden="true">{cancion.numero}</span>
|
|
||||||
{/if}
|
|
||||||
<BotonReproducir {pistas} indice={i} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
La portada del tema, no la del disco: en un álbum donde cada canción
|
|
||||||
tiene la suya, la lista es lo único que las enseña juntas. Va muda
|
|
||||||
—`alt` vacío, `muda` en el hueco— porque el título está justo al lado
|
|
||||||
y repetirlo solo alarga la lectura con un lector de pantalla.
|
|
||||||
-->
|
|
||||||
<a
|
|
||||||
class="mini"
|
|
||||||
href={resolve('/canciones/[slug]', { slug: cancion.slug })}
|
|
||||||
tabindex="-1"
|
|
||||||
onmouseenter={(e) => asomar(e, cancion)}
|
|
||||||
onmouseleave={() => (vista = null)}
|
|
||||||
>
|
|
||||||
{#if cancion.portada}
|
|
||||||
<img src={cancion.portada} alt="" width="72" height="72" loading="lazy" />
|
|
||||||
{:else}
|
|
||||||
<SinPortada titulo={cancion.titulo} lado={72} muda />
|
|
||||||
{/if}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="datos">
|
|
||||||
<a class="titulo" href={resolve('/canciones/[slug]', { slug: cancion.slug })}>
|
|
||||||
{cancion.titulo}
|
|
||||||
</a>
|
|
||||||
<!--
|
|
||||||
Géneros y etiquetas, no el primer verso. La letra ya está en la
|
|
||||||
ficha, entera; aquí lo que sirve es saber de qué está hecho cada
|
|
||||||
tema para elegir cuál abrir.
|
|
||||||
-->
|
|
||||||
<div class="senas">
|
|
||||||
{#if cancion.instrumental}
|
|
||||||
<span class="marca">Instrumental</span>
|
|
||||||
{/if}
|
|
||||||
{#each cancion.generos as g (g.slug)}
|
|
||||||
<a class="genero" href={resolve('/generos/[slug]', { slug: g.slug })}>{g.nombre}</a>
|
|
||||||
{/each}
|
|
||||||
<Etiquetas
|
|
||||||
etiquetas={cancion.etiquetas}
|
|
||||||
titulo="Etiquetas de {cancion.titulo}"
|
|
||||||
menudas
|
|
||||||
/>
|
|
||||||
{#if cancion.generos.length === 0 && cancion.etiquetas.length === 0}
|
|
||||||
<span class="muted sin">{listaDeNombres(cancion.interpretes)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="final">
|
|
||||||
<BotonCarrito {cancion} compacto />
|
|
||||||
<span class="duracion muted">
|
|
||||||
{cancion.duracion ? segundosADuracion(cancion.duracion) : '--:--'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
{#if vista}
|
|
||||||
<!--
|
|
||||||
`aria-hidden`: es la misma portada de la miniatura de al lado, en grande.
|
|
||||||
Para quien no la ve no añade nada, y anunciarla interrumpiría la lectura
|
|
||||||
de la lista cada vez que el ratón roza una fila.
|
|
||||||
-->
|
|
||||||
<div
|
|
||||||
class="vista-previa"
|
|
||||||
style="--x: {vista.x}px; --y: {vista.y}px; --lado: {LADO}px"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<img src={vista.src} alt="" width={LADO} height={LADO} />
|
|
||||||
<p>{vista.titulo}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.lista {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-top: 1px solid var(--c-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fila {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-2) var(--sp-1);
|
|
||||||
border-bottom: 1px solid var(--c-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini {
|
|
||||||
display: block;
|
|
||||||
width: 72px;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini img {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--radius-tarjeta);
|
|
||||||
background: var(--c-bg-sunken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* `pointer-events: none` para que la tarjeta no se cruce con el ratón: si lo
|
|
||||||
* capturase, al salir de la miniatura hacia ella el `mouseleave` la cerraría
|
|
||||||
* y volvería a abrirse en bucle.
|
|
||||||
*/
|
|
||||||
.vista-previa {
|
|
||||||
position: fixed;
|
|
||||||
top: var(--y);
|
|
||||||
left: var(--x);
|
|
||||||
z-index: 40;
|
|
||||||
width: var(--lado);
|
|
||||||
padding: var(--sp-2);
|
|
||||||
border: 1px solid var(--c-border);
|
|
||||||
border-radius: var(--radius-tarjeta);
|
|
||||||
background: var(--c-bg-raised);
|
|
||||||
box-shadow: 0 12px 32px rgb(0 0 0 / 0.45);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vista-previa img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: calc(var(--radius-tarjeta) - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vista-previa p {
|
|
||||||
margin: var(--sp-2) 0 0;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--c-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fila:hover {
|
|
||||||
background: var(--c-bg-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fila.sonando .titulo {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.numero {
|
|
||||||
width: 1.5em;
|
|
||||||
text-align: right;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datos {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titulo {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titulo:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* En una línea y sin envolver: en una lista de diez temas, una fila que crece
|
|
||||||
* porque su canción tiene una etiqueta más rompe el ritmo de todas. Lo que no
|
|
||||||
* cabe se recorta con el degradado del final.
|
|
||||||
*/
|
|
||||||
.senas {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
min-width: 0;
|
|
||||||
margin-top: var(--sp-1);
|
|
||||||
overflow: hidden;
|
|
||||||
mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.senas :global(.etiquetas) {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.senas > *,
|
|
||||||
.senas :global(li) {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* El género es del glosario, no del catálogo: se distingue con el oro. */
|
|
||||||
.genero {
|
|
||||||
padding: 0.2em 0.7em;
|
|
||||||
border: 1px solid var(--c-accent);
|
|
||||||
border-radius: var(--radius-boton);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--c-accent);
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.genero:hover,
|
|
||||||
.genero:focus-visible {
|
|
||||||
background: color-mix(in oklab, var(--c-accent) 14%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sin {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marca {
|
|
||||||
border: 1px solid var(--c-border);
|
|
||||||
border-radius: var(--radius-boton);
|
|
||||||
padding: 0 0.4em;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--c-text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.duracion {
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
min-width: 3.2em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { page } from '@vitest/browser/context';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { render } from 'vitest-browser-svelte';
|
|
||||||
import ListaCanciones from './ListaCanciones.svelte';
|
|
||||||
import type { Cancion } from '$lib/types';
|
|
||||||
|
|
||||||
function cancion(parcial: Partial<Cancion> = {}): Cancion {
|
|
||||||
return {
|
|
||||||
slug: 'como-si-nada',
|
|
||||||
titulo: 'Como si nada',
|
|
||||||
estilo: 'baladas-romanticas',
|
|
||||||
album: 'como-si-nada',
|
|
||||||
numero: 1,
|
|
||||||
audio: 'como-si-nada/01-como-si-nada.wav',
|
|
||||||
duracion: 252,
|
|
||||||
ano: 2024,
|
|
||||||
letra: ['Senza Paura'],
|
|
||||||
musica: ['Senza Paura'],
|
|
||||||
interpretes: ['Marta Cifuentes'],
|
|
||||||
destacada: false,
|
|
||||||
etiquetas: [],
|
|
||||||
generos: [],
|
|
||||||
enlaces: {},
|
|
||||||
instrumental: false,
|
|
||||||
idioma: 'es',
|
|
||||||
inspiracion: [],
|
|
||||||
extracto: [],
|
|
||||||
versiones: [],
|
|
||||||
alaVenta: true,
|
|
||||||
registro: { reparto: [] },
|
|
||||||
videos: [],
|
|
||||||
...parcial
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ListaCanciones', () => {
|
|
||||||
it('pinta el titulo y la duracion de cada tema', async () => {
|
|
||||||
render(ListaCanciones, { canciones: [cancion()] });
|
|
||||||
|
|
||||||
await expect.element(page.getByRole('link', { name: 'Como si nada' })).toBeInTheDocument();
|
|
||||||
await expect.element(page.getByText('4:12')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('enlaza cada tema con su ficha', async () => {
|
|
||||||
render(ListaCanciones, { canciones: [cancion()] });
|
|
||||||
|
|
||||||
await expect
|
|
||||||
.element(page.getByRole('link', { name: 'Como si nada' }))
|
|
||||||
.toHaveAttribute('href', '/canciones/como-si-nada');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('enseña de qué está hecho el tema, no su letra', async () => {
|
|
||||||
// El primer verso ya no va aquí: la letra está entera en la ficha, y en
|
|
||||||
// una lista lo que sirve para elegir es el género y las etiquetas.
|
|
||||||
render(ListaCanciones, {
|
|
||||||
canciones: [
|
|
||||||
cancion({
|
|
||||||
extracto: ['Entras en la casa y no preguntas nada,'],
|
|
||||||
generos: [{ slug: 'bolero', nombre: 'Bolero' }],
|
|
||||||
etiquetas: ['Desamor']
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect.element(page.getByRole('link', { name: 'Bolero' })).toBeInTheDocument();
|
|
||||||
await expect.element(page.getByRole('link', { name: 'Desamor' })).toBeInTheDocument();
|
|
||||||
expect(document.body.textContent).not.toContain('Entras en la casa');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cae en los intérpretes cuando no hay género ni etiqueta', async () => {
|
|
||||||
render(ListaCanciones, {
|
|
||||||
canciones: [cancion({ generos: [], etiquetas: [], interpretes: ['Marta Cifuentes'] })]
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect.element(page.getByText('Marta Cifuentes')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marca las piezas instrumentales en vez de acreditar la letra', async () => {
|
|
||||||
render(ListaCanciones, {
|
|
||||||
canciones: [cancion({ slug: 'preludio-i', titulo: 'Preludio I', instrumental: true })]
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect.element(page.getByText(/Instrumental/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('desactiva el play de los temas sin audio', async () => {
|
|
||||||
// Se publica la ficha y la letra aunque el audio aun no exista; lo que no
|
|
||||||
// puede pasar es que el boton prometa algo que no va a sonar.
|
|
||||||
render(ListaCanciones, { canciones: [cancion({ audio: undefined })] });
|
|
||||||
|
|
||||||
await expect
|
|
||||||
.element(page.getByRole('button', { name: 'Reproducir Como si nada' }))
|
|
||||||
.toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('muestra la duracion desconocida como guiones', async () => {
|
|
||||||
render(ListaCanciones, { canciones: [cancion({ duracion: undefined })] });
|
|
||||||
|
|
||||||
await expect.element(page.getByText('--:--')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('avisa cuando no hay temas', async () => {
|
|
||||||
render(ListaCanciones, { canciones: [] });
|
|
||||||
|
|
||||||
await expect.element(page.getByText(/no hay temas publicados/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in new issue