Compare commits

..

5 Commits

Author SHA1 Message Date
Adriano 21d9755e37 Migrazione: corregge il titolo Performance Class troncato a "Class"
Nei DB esistenti (prod + dev) service.performance-class.title era rimasto
"Class", mostrando "CLASS" come H1 della pagina /services/performance-class
e nelle card di /services e home. Il seed è INSERT OR IGNORE e non lo
aggiornava; una nuova installazione era invece corretta.

Aggiunge una migrazione guardata dal vecchio valore (idempotente, non tocca
le modifiche del pannello) che allinea a "Performance Class". Verificata su
copia del DB di produzione: titolo corretto, 40 edit del pannello intatti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:41:37 +02:00
Adriano ee54e9a1f4 Migrazione: allinea i bottoni promo col vecchio testo-freccia
Il commit 1522d97 ha spostato la freccia "→" dei bottoni promo nel CSS
(::after) e il maiuscolo in text-transform, cambiando il seed in minuscolo
senza freccia. Ma il seed è INSERT OR IGNORE: sui DB esistenti (produzione)
le righe restavano a "LE NOSTRE PROPOSTE →" / "ACQUISTA →", e il ::after
avrebbe prodotto una doppia freccia.

Aggiunge una migrazione guardata dal vecchio valore (idempotente, non tocca
le righe modificate dal pannello) che le allinea al seed corrente. Verificata
su copia del DB di produzione: frecce corrette e 40 edit del pannello intatti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:28:20 +02:00
Adriano 62e0120ea2 Footer: rimuove il form contatti e riorganizza in 3 colonne
- Rimossa la colonna "invia messaggio" (nome, cognome, telefono, mail,
  messaggio) e l'import ContactForm inutilizzato.
- Griglia a 3 colonne con contenuto centrato, logo incluso.
- Training e Servizi allineati alla riga "Promuoviamo il movimento...".
- Altezza del footer ridotta (padding compattato).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:48:49 +02:00
Adriano 2f94fc0373 Allinea test stale e migrazione nav dopo riordino menu
I test erano rimasti indietro rispetto a due modifiche intenzionali:
- size 'xxl' è ora un valore valido (whitelist + CSS): i test lo usavano
  come esempio di valore da rifiutare → sostituito con 'xxxl'.
- il menu è stato riordinato (commit 5ff4771): la voce "Servizi" è passata
  da nav.3 a nav.2. Allineata la voce di migrazione OLD_SECTION_DEFAULTS a
  global.nav.2.label e il relativo test. Impatto nullo sui DB reali (nessuna
  riga ha ancora value 'Programmi'), solo coerenza semantica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:17:37 +02:00
Adriano e6c493b756 Contatti obbligatori, hero senza foto, menu Promo2 solo admin
- Contatti: nome, cognome, telefono e mail resi obbligatori (validazione
  client + server); il form del footer ora include cognome e telefono.
- Home hero: rimossa la foto (file conservato su disco), fascia ridotta
  da 100vh a padding contenuto, testo allargato.
- Menu: la voce "Promo 2 (test)" è visibile solo agli admin loggati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:06:28 +02:00
11 changed files with 105 additions and 30 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ const { variant = 'full' } = Astro.props;
data-msg-error-network={t('form.msg.error-network')} data-msg-error-network={t('form.msg.error-network')}
> >
<input class="field" name="firstName" placeholder={t('form.field.first-name')} required maxlength="100" /> <input class="field" name="firstName" placeholder={t('form.field.first-name')} required maxlength="100" />
{variant === 'full' && <input class="field" name="lastName" placeholder={t('form.field.last-name')} maxlength="100" />} <input class="field" name="lastName" placeholder={t('form.field.last-name')} required maxlength="100" />
{variant === 'full' && <input class="field" name="phone" placeholder={t('form.field.phone')} maxlength="40" />} <input class="field" type="tel" name="phone" placeholder={t('form.field.phone')} required maxlength="40" />
<input class="field" type="email" name="email" placeholder={t('form.field.email')} required maxlength="200" /> <input class="field" type="email" name="email" placeholder={t('form.field.email')} required maxlength="200" />
<textarea class="field" name="message" placeholder={t('form.field.message')} required maxlength="5000"></textarea> <textarea class="field" name="message" placeholder={t('form.field.message')} required maxlength="5000"></textarea>
<input class="hp" type="text" name="website" tabindex="-1" autocomplete="off" /> <input class="hp" type="text" name="website" tabindex="-1" autocomplete="off" />
+6 -7
View File
@@ -1,6 +1,5 @@
--- ---
import { site } from '../data/site'; import { site } from '../data/site';
import ContactForm from './ContactForm.astro';
import { t, contentImageUrl } from '../lib/content'; import { t, contentImageUrl } from '../lib/content';
import T from './content/T.astro'; import T from './content/T.astro';
const year = new Date().getFullYear(); const year = new Date().getFullYear();
@@ -24,10 +23,6 @@ const year = new Date().getFullYear();
<T tag="footer.col.services-title" as="h4" /> <T tag="footer.col.services-title" as="h4" />
{site.footerServices.map((l, i) => <a href={l.href}>{t(`footer.services.${i + 1}.label`)}</a>)} {site.footerServices.map((l, i) => <a href={l.href}>{t(`footer.services.${i + 1}.label`)}</a>)}
</nav> </nav>
<div>
<T tag="footer.col.contact-title" as="h4" />
<ContactForm variant="footer" />
</div>
</div> </div>
<div class="ftr__bar"> <div class="ftr__bar">
<div class="container ftr__bar-in"> <div class="container ftr__bar-in">
@@ -42,7 +37,11 @@ const year = new Date().getFullYear();
.ftr a { color: #b9aea4; text-decoration: none; display: block; padding: 4px 0; } .ftr a { color: #b9aea4; text-decoration: none; display: block; padding: 4px 0; }
.ftr a:hover { color: #fff; } .ftr a:hover { color: #fff; }
.ftr h4 { color: #fff; text-transform: uppercase; letter-spacing: .18em; font-size: .78rem; margin-bottom: 18px; } .ftr h4 { color: #fff; text-transform: uppercase; letter-spacing: .18em; font-size: .78rem; margin-bottom: 18px; }
.ftr__grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr 1.3fr; gap: 40px; padding-block: 70px; } .ftr__grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr; gap: 40px; padding-block: 26px; text-align: center; align-items: start; }
/* Il logo è display:block (reset globale): lo centro nella colonna */
.ftr__grid img { margin-inline: auto; }
/* Training e Servizi allineati alla riga "Promuoviamo il movimento..." (logo+tagline ~110px) */
.ftr__grid nav { padding-top: 110px; }
.ftr__tag { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; color: #fff; } .ftr__tag { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; color: #fff; }
.ftr__hours { margin: 0; } .ftr__hours { margin: 0; }
.ftr__legal { margin-top: 14px; font-size: .78rem; opacity: .8; } .ftr__legal { margin-top: 14px; font-size: .78rem; opacity: .8; }
@@ -50,6 +49,6 @@ const year = new Date().getFullYear();
.ftr__bar { border-top: 1px solid rgba(255,255,255,.08); padding-block: 18px; } .ftr__bar { border-top: 1px solid rgba(255,255,255,.08); padding-block: 18px; }
.ftr__bar-in { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; } .ftr__bar-in { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
.ftr__bar-in a { display: inline; margin-left: 12px; } .ftr__bar-in a { display: inline; margin-left: 12px; }
@media (max-width: 991px) { .ftr__grid { grid-template-columns: 1fr 1fr; } } @media (max-width: 991px) { .ftr__grid { grid-template-columns: 1fr 1fr; } .ftr__grid nav { padding-top: 0; } }
@media (max-width: 600px) { .ftr__grid { grid-template-columns: 1fr; } } @media (max-width: 600px) { .ftr__grid { grid-template-columns: 1fr; } }
</style> </style>
+3 -1
View File
@@ -16,7 +16,9 @@ const areaHref = user?.role === 'superuser' ? '/admin/content' : '/admin';
<a href="/" class="hdr__logo"><img src={contentImageUrl('header.logo.image', '/img/logo-dark.png')} alt={t('header.logo.alt')} width="150" /></a> <a href="/" class="hdr__logo"><img src={contentImageUrl('header.logo.image', '/img/logo-dark.png')} alt={t('header.logo.alt')} width="150" /></a>
<nav class="hdr__nav" id="site-nav" aria-label="principale"> <nav class="hdr__nav" id="site-nav" aria-label="principale">
{site.navigation.map((item, i) => ( {site.navigation.map((item, i) => (
<a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{t(`global.nav.${i + 1}.label`)}</a> (!('adminOnly' in item && item.adminOnly) || user?.role === 'admin') && (
<a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{t(`global.nav.${i + 1}.label`)}</a>
)
))} ))}
</nav> </nav>
<div class="hdr__user"> <div class="hdr__user">
+5 -11
View File
@@ -1,10 +1,9 @@
--- ---
import hero1 from '../../assets/img/hero-1.png'; // Foto hero rimossa ma conservata su disco per riuso futuro: assets/img/hero-1.png
import T from '../content/T.astro'; import T from '../content/T.astro';
import TImg from '../content/TImg.astro';
import { t } from '../../lib/content'; import { t } from '../../lib/content';
const slides = [ const slides = [
{ titleTags: ['home.hero.title-line-1', 'home.hero.title-line-2', 'home.hero.title-line-3'], image: hero1, alt: 'Atleti InsanityLab' }, { titleTags: ['home.hero.title-line-1', 'home.hero.title-line-2', 'home.hero.title-line-3'] },
]; ];
--- ---
<section class="hero" aria-label="presentazione"> <section class="hero" aria-label="presentazione">
@@ -15,7 +14,6 @@ const slides = [
<h1>{s.titleTags.map((tag) => <T tag={tag} as="span" class="hero__line" />)}</h1> <h1>{s.titleTags.map((tag) => <T tag={tag} as="span" class="hero__line" />)}</h1>
<a class="btn" href="/services">{t('home.hero.cta')}</a> <a class="btn" href="/services">{t('home.hero.cta')}</a>
</div> </div>
<TImg tag="home.hero.image" src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
</div> </div>
</div> </div>
))} ))}
@@ -34,32 +32,28 @@ const slides = [
.hero__slide.is-active { display: block; } .hero__slide.is-active { display: block; }
/* Come nel Figma: figure a piena altezza ancorate in basso a destra, /* Come nel Figma: figure a piena altezza ancorate in basso a destra,
testo sovrapposto sulla sinistra. */ testo sovrapposto sulla sinistra. */
.hero__in { position: relative; display: flex; align-items: center; min-height: calc(100vh - var(--header-h)); } .hero__in { position: relative; display: flex; align-items: center; min-height: 0; padding-block: 90px; }
/* Lo span .hero__line è reso dal componente <T> e non riceve il data-astro-cid /* Lo span .hero__line è reso dal componente <T> e non riceve il data-astro-cid
di questo componente: si ancora a .hero__text (che il cid ce l'ha) + :global. */ di questo componente: si ancora a .hero__text (che il cid ce l'ha) + :global. */
.hero__text h1 :global(.hero__line) { display: block; } .hero__text h1 :global(.hero__line) { display: block; }
.hero__text { position: relative; z-index: 1; max-width: 46%; } .hero__text { position: relative; z-index: 1; max-width: 640px; }
.hero__text h1 { font-weight: 500; } .hero__text h1 { font-weight: 500; }
.hero__img { position: absolute; right: 0; bottom: 0; height: 100%; width: auto; max-width: 80%; object-fit: contain; object-position: right bottom; }
.hero__arrow { position: absolute; top: 50%; transform: translateY(-50%); background: none; border: 0; font-size: 3rem; color: var(--c-heading); cursor: pointer; } .hero__arrow { position: absolute; top: 50%; transform: translateY(-50%); background: none; border: 0; font-size: 3rem; color: var(--c-heading); cursor: pointer; }
.hero__arrow--prev { left: 12px; } .hero__arrow--next { right: 12px; } .hero__arrow--prev { left: 12px; } .hero__arrow--next { right: 12px; }
@media (max-width: 991px) { @media (max-width: 991px) {
.hero__in { flex-direction: column; align-items: flex-start; padding-block: 40px; min-height: 0; gap: 24px; } .hero__in { flex-direction: column; align-items: flex-start; padding-block: 40px; min-height: 0; gap: 24px; }
.hero__text { max-width: none; } .hero__text { max-width: none; }
.hero__img { position: static; max-width: 100%; height: auto; }
} }
/* Ingresso al load stile Slider Revolution del template */ /* Ingresso al load stile Slider Revolution del template */
@keyframes hero-up { from { opacity: 0; transform: translateY(60px); } to { opacity: 1; transform: none; } } @keyframes hero-up { from { opacity: 0; transform: translateY(60px); } to { opacity: 1; transform: none; } }
@keyframes hero-side { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: none; } }
:global(html.js) .hero__text h1 :global(.hero__line) { opacity: 0; animation: hero-up .8s ease forwards; } :global(html.js) .hero__text h1 :global(.hero__line) { opacity: 0; animation: hero-up .8s ease forwards; }
:global(html.js) .hero__text h1 :global(.hero__line:nth-child(1)) { animation-delay: .15s; } :global(html.js) .hero__text h1 :global(.hero__line:nth-child(1)) { animation-delay: .15s; }
:global(html.js) .hero__text h1 :global(.hero__line:nth-child(2)) { animation-delay: .3s; } :global(html.js) .hero__text h1 :global(.hero__line:nth-child(2)) { animation-delay: .3s; }
:global(html.js) .hero__text h1 :global(.hero__line:nth-child(3)) { animation-delay: .45s; } :global(html.js) .hero__text h1 :global(.hero__line:nth-child(3)) { animation-delay: .45s; }
:global(html.js) .hero__text .btn { opacity: 0; animation: hero-up .8s ease .7s forwards; } :global(html.js) .hero__text .btn { opacity: 0; animation: hero-up .8s ease .7s forwards; }
:global(html.js) .hero__img { opacity: 0; animation: hero-side 1s ease .2s forwards; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
:global(html.js) .hero__text h1 :global(.hero__line), :global(html.js) .hero__text .btn, :global(html.js) .hero__img { opacity: 1; animation: none; } :global(html.js) .hero__text h1 :global(.hero__line), :global(html.js) .hero__text .btn { opacity: 1; animation: none; }
} }
</style> </style>
+2 -1
View File
@@ -18,7 +18,8 @@ export const site = {
{ label: "Blog & Edugo", href: "/blog" }, { label: "Blog & Edugo", href: "/blog" },
{ label: "Promo ed eventi", href: "/promo-eventi" }, { label: "Promo ed eventi", href: "/promo-eventi" },
{ label: "Contatti", href: "/contact" }, { label: "Contatti", href: "/contact" },
{ label: "Promo 2 (test)", href: "/promo-eventi2" }, // Pagina di test: visibile solo agli admin loggati, nascosta al pubblico.
{ label: "Promo 2 (test)", href: "/promo-eventi2", adminOnly: true },
], ],
footerTraining: [ footerTraining: [
{ label: "One to one", href: "/training#one-to-one" }, { label: "One to one", href: "/training#one-to-one" },
+2
View File
@@ -24,6 +24,8 @@ export function validateContact(data: unknown):
}; };
if (value.website) return { ok: false, error: 'Richiesta non valida.' }; if (value.website) return { ok: false, error: 'Richiesta non valida.' };
if (!value.firstName) return { ok: false, error: 'Il nome è obbligatorio.' }; if (!value.firstName) return { ok: false, error: 'Il nome è obbligatorio.' };
if (!value.lastName) return { ok: false, error: 'Il cognome è obbligatorio.' };
if (!value.phone) return { ok: false, error: 'Il telefono è obbligatorio.' };
if (!EMAIL_RE.test(value.email)) return { ok: false, error: 'Inserisci una e-mail valida.' }; if (!EMAIL_RE.test(value.email)) return { ok: false, error: 'Inserisci una e-mail valida.' };
if (!value.message) return { ok: false, error: 'Il messaggio è obbligatorio.' }; if (!value.message) return { ok: false, error: 'Il messaggio è obbligatorio.' };
if (value.message.length > 5000) return { ok: false, error: 'Messaggio troppo lungo.' }; if (value.message.length > 5000) return { ok: false, error: 'Messaggio troppo lungo.' };
+19 -1
View File
@@ -91,7 +91,8 @@ export function createDb(path?: string): Database.Database {
// le righe rimaste identiche al valore originale del seed (mai modificate dal pannello): // le righe rimaste identiche al valore originale del seed (mai modificate dal pannello):
// il nuovo valore è quello del seed corrente per lo stesso tag. // il nuovo valore è quello del seed corrente per lo stesso tag.
const OLD_SECTION_DEFAULTS: [string, string][] = [ const OLD_SECTION_DEFAULTS: [string, string][] = [
['global.nav.3.label', 'Programmi'], // La voce "Servizi" (ex "Programmi") è la 2ª del menu dopo il riordino (commit 5ff4771).
['global.nav.2.label', 'Programmi'],
['footer.col.services-title', 'Programmi'], ['footer.col.services-title', 'Programmi'],
['home.services.title', 'Programmi'], ['home.services.title', 'Programmi'],
['home.services.body', 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'], ['home.services.body', 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'],
@@ -165,6 +166,23 @@ export function createDb(path?: string): Database.Database {
const updMetodoTx = db.transaction(() => { for (const [tag, oldV, newV] of METODO) upd02.run(newV, tag, oldV); }); const updMetodoTx = db.transaction(() => { for (const [tag, oldV, newV] of METODO) upd02.run(newV, tag, oldV); });
updMetodoTx(); updMetodoTx();
// Modifiche promo (commit 1522d97): la freccia "→" dei bottoni è passata al CSS (::after) e il
// maiuscolo a text-transform. Il seed è ora minuscolo e senza freccia. Le righe di prod ancora
// al vecchio default vanno allineate, altrimenti il CSS ::after produce una DOPPIA freccia.
// Guardato dal vecchio valore → idempotente, non tocca eventuali modifiche del pannello.
const PROMO_FRECCE: [string, string, string][] = [
['promo.hero.cta', 'LE NOSTRE PROPOSTE →', 'Le nostre proposte'],
['promo.buy-label', 'ACQUISTA →', 'Acquista'],
];
const updPromoTx = db.transaction(() => { for (const [tag, oldV, newV] of PROMO_FRECCE) upd02.run(newV, tag, oldV); });
updPromoTx();
// Fix: il titolo del servizio Performance Class era rimasto troncato a "Class" nei DB esistenti
// (valore vecchio mai allineato dal seed INSERT OR IGNORE), mostrando "CLASS" come H1 della
// pagina /services/performance-class e nelle card di /services e home. Guardato dal vecchio
// valore → idempotente, non tocca eventuali modifiche del pannello.
db.prepare("UPDATE content_blocks SET value = 'Performance Class' WHERE tag = 'service.performance-class.title' AND value = 'Class'").run();
// Modifiche 03: due ingressi dello stesso stabile → indirizzo esteso in footer e contatti. // Modifiche 03: due ingressi dello stesso stabile → indirizzo esteso in footer e contatti.
// Il tag global.contact.map-query è nuovo (inserito dal seed); qui si aggiorna solo l'indirizzo // Il tag global.contact.map-query è nuovo (inserito dal seed); qui si aggiorna solo l'indirizzo
// già esistente, se non modificato dal pannello. Idempotente. // già esistente, se non modificato dal pannello. Idempotente.
+5 -3
View File
@@ -2,13 +2,13 @@ import { describe, it, expect } from 'vitest';
import { validateContact } from '../src/lib/contact'; import { validateContact } from '../src/lib/contact';
describe('validateContact', () => { describe('validateContact', () => {
const good = { firstName: 'Mario', email: 'mario@example.com', message: 'Ciao, vorrei informazioni.' }; const good = { firstName: 'Mario', lastName: 'Rossi', phone: '3331234567', email: 'mario@example.com', message: 'Ciao, vorrei informazioni.' };
it('accetta payload valido e normalizza i campi opzionali', () => { it('accetta payload valido', () => {
const r = validateContact(good); const r = validateContact(good);
expect(r.ok).toBe(true); expect(r.ok).toBe(true);
if (r.ok) { if (r.ok) {
expect(r.value.lastName).toBe(''); expect(r.value.lastName).toBe('Rossi');
expect(r.value.email).toBe('mario@example.com'); expect(r.value.email).toBe('mario@example.com');
} }
}); });
@@ -19,6 +19,8 @@ describe('validateContact', () => {
it('rifiuta campi obbligatori mancanti o vuoti', () => { it('rifiuta campi obbligatori mancanti o vuoti', () => {
expect(validateContact({ ...good, firstName: ' ' }).ok).toBe(false); expect(validateContact({ ...good, firstName: ' ' }).ok).toBe(false);
expect(validateContact({ ...good, lastName: ' ' }).ok).toBe(false);
expect(validateContact({ ...good, phone: '' }).ok).toBe(false);
expect(validateContact({ ...good, message: '' }).ok).toBe(false); expect(validateContact({ ...good, message: '' }).ok).toBe(false);
expect(validateContact(null).ok).toBe(false); expect(validateContact(null).ok).toBe(false);
}); });
+1 -1
View File
@@ -21,7 +21,7 @@ describe('applyContentUpdate', () => {
it('404 su tag inesistente, 400 su styles fuori whitelist', () => { it('404 su tag inesistente, 400 su styles fuori whitelist', () => {
expect(applyContentUpdate(db, 'no.tag', { value: 'x' }, 'a')).toMatchObject({ ok: false, status: 404 }); expect(applyContentUpdate(db, 'no.tag', { value: 'x' }, 'a')).toMatchObject({ ok: false, status: 404 });
expect(applyContentUpdate(db, textTag, { styles: { size: 'xxl' } }, 'a')).toMatchObject({ ok: false, status: 400 }); expect(applyContentUpdate(db, textTag, { styles: { size: 'xxxl' } }, 'a')).toMatchObject({ ok: false, status: 400 });
}); });
it('sanifica i tag html e rifiuta value su tag image', () => { it('sanifica i tag html e rifiuta value su tag image', () => {
+59 -2
View File
@@ -44,7 +44,7 @@ describe('schema content_blocks e ruoli', () => {
ins.run('footer.col.programs-title', 'Colonna modificata'); ins.run('footer.col.programs-title', 'Colonna modificata');
// Riga mai toccata dal pannello: valore identico al vecchio seed → deve prendere il nuovo default. // Riga mai toccata dal pannello: valore identico al vecchio seed → deve prendere il nuovo default.
db1.prepare("INSERT INTO content_blocks (tag, type, value) VALUES ('programs.meta.title', 'text', 'Programmi')").run(); db1.prepare("INSERT INTO content_blocks (tag, type, value) VALUES ('programs.meta.title', 'text', 'Programmi')").run();
db1.prepare("UPDATE content_blocks SET value = 'Programmi' WHERE tag = 'global.nav.3.label'").run(); db1.prepare("UPDATE content_blocks SET value = 'Programmi' WHERE tag = 'global.nav.2.label'").run();
}); });
old(); old();
db1.close(); db1.close();
@@ -60,8 +60,10 @@ describe('schema content_blocks e ruoli', () => {
expect(val('footer.col.services-title')).toMatchObject({ value: 'Colonna modificata', updated_by: 'editor' }); expect(val('footer.col.services-title')).toMatchObject({ value: 'Colonna modificata', updated_by: 'editor' });
// Le righe rimaste al vecchio valore di default prendono il nuovo ("Servizi"). // Le righe rimaste al vecchio valore di default prendono il nuovo ("Servizi").
// Dopo il riordino del menu (commit 5ff4771) la voce "Servizi" è la 2ª: la migrazione
// aggiorna global.nav.2.label da 'Programmi' al seed corrente 'Servizi'.
expect(val('services.meta.title')).toMatchObject({ value: 'Servizi' }); expect(val('services.meta.title')).toMatchObject({ value: 'Servizi' });
expect(val('global.nav.3.label')).toMatchObject({ value: 'Servizi' }); expect(val('global.nav.2.label')).toMatchObject({ value: 'Servizi' });
// La riga rinominata ma modificata dal pannello resta invariata (già verificata sopra): // La riga rinominata ma modificata dal pannello resta invariata (già verificata sopra):
// home.services.title = 'Titolo modificato'. // home.services.title = 'Titolo modificato'.
@@ -82,6 +84,61 @@ describe('schema content_blocks e ruoli', () => {
} }
}); });
it('allinea i bottoni promo col vecchio testo-freccia al seed senza freccia', () => {
const dir = mkdtempSync(join(tmpdir(), 'il-db-promo-'));
const path = join(dir, 'test.db');
try {
// DB pre-refactor: valori con freccia letterale e maiuscolo (poi spostati nel CSS).
const db1 = createDb(path);
db1.prepare("UPDATE content_blocks SET value = 'LE NOSTRE PROPOSTE →' WHERE tag = 'promo.hero.cta'").run();
db1.prepare("UPDATE content_blocks SET value = 'ACQUISTA →' WHERE tag = 'promo.buy-label'").run();
// Riga modificata dal pannello: NON deve essere toccata.
db1.prepare("UPDATE content_blocks SET value = 'Compra ora', updated_by = 'editor' WHERE tag = 'promo.buy-label'").run();
db1.close();
// Riapertura: la migrazione allinea solo le righe rimaste al vecchio default con freccia.
const db2 = createDb(path);
const val = (tag: string) => (db2.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value;
expect(val('promo.hero.cta')).toBe('Le nostre proposte');
// promo.buy-label era stato editato dal pannello → resta invariato.
expect(val('promo.buy-label')).toBe('Compra ora');
db2.close();
// Idempotente: terza apertura non altera.
const db3 = createDb(path);
expect((db3.prepare("SELECT value FROM content_blocks WHERE tag = 'promo.hero.cta'").get() as { value: string }).value).toBe('Le nostre proposte');
db3.close();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('corregge il titolo Performance Class rimasto troncato a "Class"', () => {
const dir = mkdtempSync(join(tmpdir(), 'il-db-pc-'));
const path = join(dir, 'test.db');
try {
// DB con il vecchio valore troncato.
const db1 = createDb(path);
db1.prepare("UPDATE content_blocks SET value = 'Class', updated_by = NULL WHERE tag = 'service.performance-class.title'").run();
db1.close();
const db2 = createDb(path);
const val = (tag: string) => (db2.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value;
expect(val('service.performance-class.title')).toBe('Performance Class');
db2.close();
// Non deve toccare un valore modificato dal pannello.
const db3 = createDb(path);
db3.prepare("UPDATE content_blocks SET value = 'Titolo mio', updated_by = 'admin' WHERE tag = 'service.performance-class.title'").run();
db3.close();
const db4 = createDb(path);
expect((db4.prepare("SELECT value FROM content_blocks WHERE tag = 'service.performance-class.title'").get() as { value: string }).value).toBe('Titolo mio');
db4.close();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('migra un DB esistente senza colonna role', () => { it('migra un DB esistente senza colonna role', () => {
// simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile, // simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile,
// quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia) // quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia)
+1 -1
View File
@@ -9,7 +9,7 @@ describe('style presets', () => {
it('rifiuta gruppo sconosciuto e valore fuori lista', () => { it('rifiuta gruppo sconosciuto e valore fuori lista', () => {
expect(validateStyles({ font: 'comic' }).ok).toBe(false); expect(validateStyles({ font: 'comic' }).ok).toBe(false);
expect(validateStyles({ size: 'xxl' }).ok).toBe(false); expect(validateStyles({ size: 'xxxl' }).ok).toBe(false);
expect(validateStyles('non-oggetto').ok).toBe(false); expect(validateStyles('non-oggetto').ok).toBe(false);
}); });