From b0c897d5777c6c3152f84bb6bf832ffa974b8a4e Mon Sep 17 00:00:00 2001 From: cammo22 <78992276+cammo22@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:11:20 +0200 Subject: [PATCH] GUI v2: le tessere usano gli asset di resources/gui Anteprime degli stili, generi, categorie di Modifica, controlli (luoghi, meteo, stagioni, costumi, animali, espressioni), locandine delle ricette, modi, copertine LoRA, mascotte e sfondi. Gli asset passano da Vite (import.meta.glob ?url) e finiscono in app.asar; senza file la tessera resta con l'icona. Test: test/asset.mjs (in npm test) e test/asset-ui.mjs. --- CHANGELOG.md | 1 + package.json | 2 +- src/renderer/src/anteprime.ts | 53 ++++++++ src/renderer/src/asset.ts | 19 +++ .../src/componenti/ControlliCategoria.tsx | 3 +- src/renderer/src/componenti/Generi.tsx | 7 +- src/renderer/src/componenti/LabVista.tsx | 3 + src/renderer/src/componenti/ScaffaliStili.tsx | 3 +- src/renderer/src/componenti/SceltaLora.tsx | 9 +- src/renderer/src/componenti/Tessere.tsx | 25 +++- src/renderer/src/main.tsx | 3 + src/renderer/src/orbita.css | 31 +++++ src/renderer/src/pagine/Crea.tsx | 4 + src/renderer/src/pagine/Galleria.tsx | 2 +- src/renderer/src/pagine/Impostazioni.tsx | 1 + src/renderer/src/pagine/Setup.tsx | 3 + src/renderer/src/pagine/Studio.tsx | 4 +- .../src/pagine/modifica/SchedaIdee.tsx | 7 +- .../src/pagine/modifica/SelettoreModo.tsx | 3 +- src/renderer/src/pagine/modifica/Tela.tsx | 3 + test/asset-ui.mjs | 104 ++++++++++++++ test/asset.mjs | 127 ++++++++++++++++++ 22 files changed, 400 insertions(+), 17 deletions(-) create mode 100644 src/renderer/src/anteprime.ts create mode 100644 src/renderer/src/asset.ts create mode 100644 test/asset-ui.mjs create mode 100644 test/asset.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db17e0..100f945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ L'interfaccia nuova (spec in `docs/gui-v2-spec.md`) sopra l'engine v2, più gli - **Gestore LoRA**: pila con ordine e pesi. **Sei ricette** (Foglio, Mondi, Mix, Set, Formati, Rinnova) al posto del catalogo lungo. - **Modelli uncensored**: interruttore spento di serie, avviso da accettare, etichetta sulle voci. - **Asset** in `resources/gui` (111 WebP, con manifest: prompt, seed, modello). 60 sono scontornati per colore, da rifare con BiRefNet. +- **Asset nelle tessere**: le 111 immagini ora si vedono. Anteprime degli stili (28), generi, categorie di Modifica, luoghi, meteo e stagioni, costumi, animali, espressioni, locandine delle ricette, copertine dei LoRA senza anteprima, modi dello Studio, mascotte e sfondi (nebulosa, pianeta, aurora). Passano da Vite (`src/renderer/src/asset.ts`) e finiscono nell'installer dentro `app.asar`. Se un file manca o non si carica la tessera resta con l'icona. Prove: `test/asset.mjs` (in `npm test`) e `test/asset-ui.mjs`. - Non ancora provati con la GPU: qualità di SAM 2 e BiRefNet, riquadri dal vivo, LoRA su Klein. ## 1.1.0 — 2026-10-21 diff --git a/package.json b/package.json index b4dd86c..34629b2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "electron-vite build", "start": "electron-vite preview", "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json", - "test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs", + "test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs && node test/asset.mjs", "prendi-uv": "node scripts/prendi-uv.mjs", "dist": "npm run prendi-uv && npm run build && electron-builder --win --x64", "prova": "node test/prove.mjs", diff --git a/src/renderer/src/anteprime.ts b/src/renderer/src/anteprime.ts new file mode 100644 index 0000000..958493f --- /dev/null +++ b/src/renderer/src/anteprime.ts @@ -0,0 +1,53 @@ +// Da quale id (stile, controllo, genere, modo, ricetta…) all'immagine di resources/gui: SOLO le chiavi, nessun import di file, +// così si prova in Node (test/asset.mjs). Una chiave è «gruppo/id» e corrisponde a resources/gui//.webp. +// Gli URL veri li dà asset.ts (Vite li mette nell'installer). Se una chiave non ha il file la tessera resta con l'icona di prima. + +export type ChiaveAsset = string + +/** i generi che hanno la tessera in Crea (nell'ordine della spec) */ +export const GENERI_TESSERA = ['foto', 'ritratto', 'prodotto', 'poster', 'infografica', 'logo', 'social', 'miniatura', 'fumetto', 'invito', 'menu'] + +export const chiaveStile = (id: string): ChiaveAsset => `stili/${id}` +/** «libero» è il genere senza genere: ha la sua tessera */ +export const chiaveGenere = (id: string): ChiaveAsset => `generi/${id || 'libero'}` + +/** i controlli di Modifica che hanno la griglia con le anteprime → la cartella degli asset (le opzioni hanno lo stesso id del file) */ +export const GRUPPO_CONTROLLO: Record = { + animale: 'animali', costume: 'costumi', luogo: 'luoghi', meteo: 'meteo', stagione: 'meteo', espressione: 'facce' +} +/** gli id delle opzioni che nel file si chiamano in un altro modo */ +const NOME_FILE: Record> = { espressione: { ride: 'ridere' } } + +export function chiaveOpzione(controllo: string, opzione: string): ChiaveAsset | undefined { + const g = GRUPPO_CONTROLLO[controllo] + return g ? `${g}/${NOME_FILE[controllo]?.[opzione] ?? opzione}` : undefined +} + +/** le categorie di Modifica (rapide.ts) → tessera/ */ +const CATEGORIA_FILE: Record = { + persone: 'persone', vestiti: 'vestiti', situazioni: 'situazioni', prospettiva: 'prospettiva', luce: 'luce-colore', stili: 'stili', testo: 'testi', ritocco: 'ritocco' +} +export const chiaveCategoria = (id: string): ChiaveAsset | undefined => (CATEGORIA_FILE[id] ? `tessera/${CATEGORIA_FILE[id]}` : undefined) +export const CATEGORIE_CON_ASSET = Object.keys(CATEGORIA_FILE) + +/** i modi dello Studio (Crea, Modifica, Box, Ricette) */ +export const chiaveModo = (id: string): ChiaveAsset => `modi/${id}` +/** i modi di Modifica: Espandi e Estrai hanno la tessera, «Scrivi» usa quella di Modifica */ +export const chiaveModoModifica = (id: string): ChiaveAsset => (id === 'espandi' || id === 'estrai' ? `tessera/${id}` : 'modi/modifica') + +/** le sei ricette: l'id del workflow è «r-foglio»; Unisci e Come l'esempio sono le due metà di Mix */ +export const RICETTE_CON_ASSET = ['foglio', 'mondi', 'mix', 'set', 'formati', 'rinnova'] +export function chiaveRicetta(idWorkflow: string): ChiaveAsset | undefined { + const id = idWorkflow === 'unisci' || idWorkflow === 'esempio' ? 'mix' : idWorkflow.replace(/^r-/, '') + return RICETTE_CON_ASSET.includes(id) ? `ricette/${id}` : undefined +} + +/** la copertina generica di un LoRA senza anteprima: «Qwen» o «Klein» dalla famiglia; se serve a più famiglie o non si capisce → niente (resta la sigla colorata) */ +export function chiaveLora(l: { famiglie?: string[]; compatibile?: string }): ChiaveAsset | undefined { + const f = new Set((l.famiglie || []).map((x) => (x === 'qwen' ? 'qwen' : 'klein'))) + if (f.size === 1) return `lora/${[...f][0]}` + return !f.size && l.compatibile === 'si' ? 'lora/qwen' : undefined +} + +export const SFONDI = { nebulosa: 'sfondi/nebulosa', calda: 'sfondi/nebulosa-calda', stelle: 'sfondi/stelle', aurora: 'sfondi/aurora', eroe: 'sfondi/hero-pianeta', setup: 'sfondi/setup-pianeta' } as const +export const MASCOTTE = { vuoto: 'mascotte/fluttua', errore: 'mascotte/cartello', setup: 'mascotte/saluta' } as const diff --git a/src/renderer/src/asset.ts b/src/renderer/src/asset.ts new file mode 100644 index 0000000..9468875 --- /dev/null +++ b/src/renderer/src/asset.ts @@ -0,0 +1,19 @@ +// Gli URL degli asset della GUI v2 (resources/gui/**/*.webp). Vite li importa come URL: finiscono in out/renderer/assets e quindi +// nell'installer (electron-builder impacchetta out/**) senza extraResources né protocolli. I più piccoli di 4 kB diventano data: URI (CSP: img-src data:). +// La chiave è «gruppo/id» (anteprime.ts). Chi non c'è → undefined: la tessera ripiega sull'icona di prima, senza errori. +// Fuori da Vite (i test che compilano i componenti con esbuild per il rendering lato server) `import.meta.glob` non esiste: l'elenco resta vuoto. +let FILE: Record = {} +try { + FILE = import.meta.glob('../../../resources/gui/**/*.webp', { eager: true, query: '?url', import: 'default' }) as Record +} catch { + /* senza Vite: nessun asset, le tessere restano con le icone */ +} + +const URLS: Record = {} +for (const [percorso, url] of Object.entries(FILE)) { + const m = /resources\/gui\/(.+)\.webp$/.exec(percorso) + if (m) URLS[m[1]] = url +} + +export const urlAsset = (chiave: string | undefined): string | undefined => (chiave ? URLS[chiave] : undefined) +export const elencoAsset = (): string[] => Object.keys(URLS) diff --git a/src/renderer/src/componenti/ControlliCategoria.tsx b/src/renderer/src/componenti/ControlliCategoria.tsx index bf87d3f..557d80d 100644 --- a/src/renderer/src/componenti/ControlliCategoria.tsx +++ b/src/renderer/src/componenti/ControlliCategoria.tsx @@ -2,6 +2,7 @@ // campioni colore, il quadrante 3×3 e le righe di testo. Il valore sta nell'id dell'idea attiva: qui non c'è stato proprio. import { useEffect, useState, type JSX } from 'react' import { Tessera, Tessere } from './Tessere' +import { chiaveOpzione } from '../anteprime' import { conControllo, controlliDi, leggiMulti, scriviMulti, valoreDa, type Controllo, type Parte } from '../controlli' import type { Rapida } from '../rapide' @@ -70,7 +71,7 @@ function Multi(props: { c: Extract; valore: Record } return ( - {x.opzioni.map((o) => {o.icona}} nome={o.nome} su={p.valore[x.id] === o.id} titolo={`${o.nome} · un clic la accende, un altro la spegne`} onClick={() => imposta(x.id, o.id)} />)} + {x.opzioni.map((o) => {o.icona}} asset={chiaveOpzione(p.c.id, o.id)} nome={o.nome} su={p.valore[x.id] === o.id} titolo={`${o.nome} · un clic la accende, un altro la spegne`} onClick={() => imposta(x.id, o.id)} />)} ) })} diff --git a/src/renderer/src/componenti/Generi.tsx b/src/renderer/src/componenti/Generi.tsx index 12b5976..a5015e2 100644 --- a/src/renderer/src/componenti/Generi.tsx +++ b/src/renderer/src/componenti/Generi.tsx @@ -8,6 +8,7 @@ import { usaStato } from '../stato' import { I } from './Icone' import { Casella } from './Casella' import { Tessera, Tessere } from './Tessere' +import { chiaveGenere, GENERI_TESSERA } from '../anteprime' import { PRESET, campiCon, componiPreset, presetDa } from '../preset' const icona = (nome: string): JSX.Element => { @@ -16,7 +17,7 @@ const icona = (nome: string): JSX.Element => { } /** i generi che hanno la tessera, nell'ordine della spec */ -const GENERI = ['foto', 'ritratto', 'prodotto', 'poster', 'infografica', 'logo', 'social', 'miniatura', 'fumetto', 'invito', 'menu'] +const GENERI = GENERI_TESSERA /** sceglie un genere (o «Libero» con '') */ export function scegliGenere(id: string, prima: (prompt: string) => void): void { @@ -43,9 +44,9 @@ export function GrigliaGeneri({ prima }: { prima: (prompt: string) => void }): J {GENERI.map((id) => { const p = presetDa(id)! - return scegliGenere(id, prima)} /> + return scegliGenere(id, prima)} /> })} - scegliGenere('', prima)} /> + scegliGenere('', prima)} /> ) } diff --git a/src/renderer/src/componenti/LabVista.tsx b/src/renderer/src/componenti/LabVista.tsx index dd645de..a966776 100644 --- a/src/renderer/src/componenti/LabVista.tsx +++ b/src/renderer/src/componenti/LabVista.tsx @@ -3,6 +3,8 @@ import { useEffect, type JSX } from 'react' import { usaStato } from '../stato' import { api, urlFile } from '../api' import { I } from './Icone' +import { ImgAsset } from './Tessere' +import { chiaveRicetta } from '../anteprime' import { PrimaDopo } from './PrimaDopo' import { VistaZoom } from './VistaZoom' import { percentuale } from './Coda' @@ -29,6 +31,7 @@ export function Catalogo(p: { cerca: string; scelto: string | null; scegli: (id:
{voci.map((w) => ( ))} diff --git a/src/renderer/src/pagine/modifica/SchedaIdee.tsx b/src/renderer/src/pagine/modifica/SchedaIdee.tsx index 4ab3e5b..869bdd3 100644 --- a/src/renderer/src/pagine/modifica/SchedaIdee.tsx +++ b/src/renderer/src/pagine/modifica/SchedaIdee.tsx @@ -13,6 +13,7 @@ import { ControlliCategoria } from '../../componenti/ControlliCategoria' import { controlliDi, prefissoIdea, rapidaVisibile } from '../../controlli' import { FANTASIE } from '../../regista' import { iconaRapida } from '../../iconeRapide' +import { chiaveCategoria, chiaveModoModifica } from '../../anteprime' import { frasiStileModifica, stileDa, type Intensita } from '../../stili' import type { PropsScrivi } from './SchedaScrivi' @@ -42,13 +43,13 @@ export function SchedaIdee({ griglia, zona, categorie, cat, setCategoria, idee,

Idee sommale: il modello le mescola

- } nome="Estrai" sotto="persone e oggetti, un clic" titolo="Estrai una persona o un oggetto dalla foto: un clic sulla cosa, poi scegli l'uscita" onClick={() => setModifica({ modo: 'estrai' })} /> - } nome="Espandi" sotto="allarga oltre i bordi" titolo="Allarga la foto oltre i bordi" onClick={() => setModifica({ modo: 'espandi' })} /> + } asset={chiaveModoModifica('estrai')} nome="Estrai" sotto="persone e oggetti, un clic" titolo="Estrai una persona o un oggetto dalla foto: un clic sulla cosa, poi scegli l'uscita" onClick={() => setModifica({ modo: 'estrai' })} /> + } asset={chiaveModoModifica('espandi')} nome="Espandi" sotto="allarga oltre i bordi" titolo="Allarga la foto oltre i bordi" onClick={() => setModifica({ modo: 'espandi' })} /> {categorie.map((c) => { const Ic = (I as Record JSX.Element>)[c.icona] || I.crea - return } nome={c.nome} su={cat?.id === c.id} pallina={quante(c.id)} onClick={() => setCategoria(c.id)} /> + return } asset={chiaveCategoria(c.id)} nome={c.nome} su={cat?.id === c.id} pallina={quante(c.id)} onClick={() => setCategoria(c.id)} /> })} diff --git a/src/renderer/src/pagine/modifica/SelettoreModo.tsx b/src/renderer/src/pagine/modifica/SelettoreModo.tsx index affd7ab..c74c756 100644 --- a/src/renderer/src/pagine/modifica/SelettoreModo.tsx +++ b/src/renderer/src/pagine/modifica/SelettoreModo.tsx @@ -3,6 +3,7 @@ import { type JSX } from 'react' import { usaStato, type ModoModifica } from '../../stato' import { I } from '../../componenti/Icone' import { Tessera } from '../../componenti/Tessere' +import { chiaveModoModifica } from '../../anteprime' const MODI: { id: ModoModifica; nome: string; icona: () => JSX.Element; titolo: string }[] = [ { id: 'scrivi', nome: 'Scrivi', icona: I.modifica, titolo: 'Scrivi cosa cambiare: se segni una zona col pennello cambia solo lì, se no tutta la foto' }, @@ -15,7 +16,7 @@ export function SelettoreModo(): JSX.Element { return (
{MODI.map((x) => ( - } nome={x.nome} titolo={x.titolo} su={m.modo === x.id} onClick={() => setModifica({ modo: x.id })} /> + } asset={chiaveModoModifica(x.id)} nome={x.nome} titolo={x.titolo} su={m.modo === x.id} onClick={() => setModifica({ modo: x.id })} /> ))}
) diff --git a/src/renderer/src/pagine/modifica/Tela.tsx b/src/renderer/src/pagine/modifica/Tela.tsx index 9a123cc..7c08b14 100644 --- a/src/renderer/src/pagine/modifica/Tela.tsx +++ b/src/renderer/src/pagine/modifica/Tela.tsx @@ -4,6 +4,8 @@ import { type JSX, type RefObject } from 'react' import { usaStato, scegliFoto, type FotoBase } from '../../stato' import { api, urlFile } from '../../api' import { I } from '../../componenti/Icone' +import { ImgAsset } from '../../componenti/Tessere' +import { MASCOTTE } from '../../anteprime' import { Cursore } from '../../componenti/Controlli' import { TelaMaschera, type AnteprimaTela, type ComandiTela, type Strumento } from '../../componenti/TelaMaschera' import { Fusion } from '../../componenti/Fusion' @@ -118,6 +120,7 @@ export function TelaModifica(p: Props): JSX.Element { {griglia || estraiMode ? null : !base ? (
+
MODIFICA UNA FOTO

Trascina qui una foto, incollala con Ctrl+V, o scegline una.

Scrivi cosa cambiare, o scegli un'idea: cambia tutta la foto. Se prima segni col pennello una zona, cambia solo quella e il resto resta identico al pixel.

diff --git a/test/asset-ui.mjs b/test/asset-ui.mjs new file mode 100644 index 0000000..bf49cff --- /dev/null +++ b/test/asset-ui.mjs @@ -0,0 +1,104 @@ +// Prova nel browser (senza Electron né motore, con test/finto.js): le tessere mostrano davvero le immagini di resources/gui, +// le immagini si caricano (naturalWidth > 0), e se un'immagine si rompe la tessera ripiega sull'icona senza errori in console. +// npm run build, poi un server statico su out/renderer (porta 8791), poi: node test/asset-ui.mjs +import { chromium } from 'playwright' +import { readFileSync, mkdirSync } from 'node:fs' +import assert from 'node:assert/strict' + +const url = process.argv[2] || 'http://localhost:8791/index.html' +const out = 'test/.out' +mkdirSync(out, { recursive: true }) +const b = await chromium.launch() +const pg = await b.newPage({ viewport: { width: 1480, height: 1000 } }) +const errori = [] +pg.on('pageerror', (e) => errori.push(e.message)) +pg.on('console', (m) => { if (m.type() === 'error') errori.push(m.text()) }) +pg.on('requestfailed', (r) => errori.push(`richiesta fallita: ${r.url()}`)) +pg.on('response', (r) => { if (r.status() >= 400) errori.push(`${r.status()} ${r.url()}`) }) +await pg.addInitScript(readFileSync('test/finto.js', 'utf8')) +await pg.goto(url) +await pg.waitForTimeout(1500) + +/** quante immagini visibili dentro il selettore sono caricate bene (i pannelli degli altri modi restano montati ma nascosti) */ +const caricate = (sel) => pg.evaluate((s) => { + const imgs = [...document.querySelectorAll(s)].filter((i) => i.getClientRects().length > 0) + return { tot: imgs.length, ok: imgs.filter((i) => i.complete && i.naturalWidth > 0).length } +}, sel) +const aspetta = async (sel, n) => { + for (let i = 0; i < 20; i++) { + const c = await caricate(sel) + if (c.tot >= n && c.ok === c.tot) return c + await pg.waitForTimeout(150) + } + return caricate(sel) +} + +// Crea: 11 generi + Libero con l'icona 3D; lo stato vuoto con la mascotte e il pianeta +let c = await aspetta('.tessera-o .ic-img', 12) +console.log('Crea, generi:', c) +assert.ok(c.tot >= 12 && c.ok === c.tot, 'le 12 tessere dei generi hanno l\'immagine') +c = await aspetta('.mascotte-vuoto', 1) +assert.equal(c.ok, 1, 'mascotte nello stato vuoto') +c = await aspetta('.hero-pianeta', 1) +assert.equal(c.ok, 1, 'pianeta nello stato vuoto') +assert.equal((await caricate('.strip-modi .ic-modo')).ok, 4, 'i 4 modi nella strip') +await pg.screenshot({ path: `${out}/a1-crea.png` }) + +// le anteprime degli stili in Crea (sezione Stile, aperta di serie): a tutta tessera +c = await aspetta('.tessera-o.foto .pieno', 28) +console.log('Crea, stili:', c) +assert.equal(c.ok, 28, 'le 28 anteprime degli stili in Crea') +await pg.locator('[data-id="stile-pixel"]:visible').first().scrollIntoViewIfNeeded() +await pg.screenshot({ path: `${out}/a2-crea-stili.png` }) + +// Modifica: categorie con l'immagine, controlli con le anteprime +await pg.click('.strip-modi button[data-modo="modifica"]') +await pg.click('.attrezzi button:has-text("Dal PC")') +await pg.waitForTimeout(800) +c = await aspetta('[data-id^="cat-"] .ic-img', 8) +console.log('Modifica, categorie + estrai/espandi:', c) +assert.ok(c.tot >= 10 && c.ok === c.tot, 'le 8 categorie e Estrai/Espandi hanno l\'immagine') +for (const [cat, prefisso, n] of [['persone', 'ctl-animale-', 5], ['persone', 'ctl-espressione-', 6], ['vestiti', 'ctl-costume-', 9], ['situazioni', 'ctl-luogo-', 10], ['situazioni', 'ctl-meteo-', 6], ['situazioni', 'ctl-stagione-', 4]]) { + await pg.click(`button[data-id="cat-${cat}"]`) + await pg.waitForTimeout(400) + const k = await aspetta(`[data-id^="${prefisso}"] .ic-img`, n) + console.log(` ${prefisso}`, k) + assert.equal(k.tot, n, `${prefisso}: ${n} tessere con l'immagine`) + assert.equal(k.ok, n, `${prefisso}: tutte caricate`) +} +await pg.screenshot({ path: `${out}/a3-modifica-situazioni.png` }) +await pg.click('button[data-id="cat-stili"]') +await pg.waitForTimeout(500) +c = await aspetta('.tessera-o.foto .pieno', 28) +assert.equal(c.ok, 28, 'le 28 anteprime degli stili in Modifica') +await pg.locator('[data-id="stile-pixel"]:visible').first().scrollIntoViewIfNeeded() +await pg.screenshot({ path: `${out}/a4-modifica-stili.png` }) + +// Ricette: la locandina di ognuna delle sei +await pg.click('.strip-modi button[data-modo="ricette"]') +await pg.waitForTimeout(800) +c = await aspetta('.carta-wf .locandina-wf', 6) +console.log('Ricette:', c) +assert.equal(c.ok, 6, 'le sei ricette con la locandina') +await pg.screenshot({ path: `${out}/a5-ricette.png` }) + +// ripiego: se l'immagine di una tessera non si carica resta l'icona di prima, senza errori di pagina +await pg.click('.strip-modi button[data-modo="crea"]') +await pg.waitForTimeout(400) +await pg.route('**/assets/foto-*.webp', (r) => r.abort()) +await pg.reload() +await pg.waitForTimeout(1500) +const rotta = await pg.evaluate(() => { + const t = document.querySelector('[data-id="genere-foto"]') + return { img: !!t?.querySelector('img'), icona: !!t?.querySelector('.ic svg, .ic span'), classe: t?.className } +}) +console.log('con foto.webp bloccata:', rotta) +assert.equal(rotta.img, false, 'immagine rotta: non resta l\'') +assert.equal(rotta.icona, true, 'immagine rotta: torna l\'icona') +await pg.screenshot({ path: `${out}/a6-ripiego.png` }) + +const veri = errori.filter((e) => !/foto-.*\.webp|ERR_FAILED|Failed to load resource/.test(e)) +console.log('errori inattesi:', veri) +assert.deepEqual(veri, [], 'nessun errore di pagina o richiesta fallita') +await b.close() +console.log('asset-ui OK') diff --git a/test/asset.mjs b/test/asset.mjs new file mode 100644 index 0000000..357ad9d --- /dev/null +++ b/test/asset.mjs @@ -0,0 +1,127 @@ +// Prova degli asset della GUI v2 (resources/gui): ogni tessera con anteprima punta a un file che esiste davvero, i nomi dei file +// combaciano con gli id di stili.ts / controlli.ts / preset.ts / ricetteV2.ts, i file sono WebP veri, stanno nel manifest +// e i url() del CSS arrivano a un file. Senza interfaccia né Electron: node test/asset.mjs +import assert from 'node:assert/strict' +import { buildSync } from 'esbuild' +import { createRequire } from 'node:module' +import { resolve, join } from 'node:path' +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs' + +globalThis.window = { daprod: {} } +globalThis.localStorage = { getItem: () => null, setItem() {} } +mkdirSync('test/.out', { recursive: true }) +buildSync({ + stdin: { + contents: "export * from './src/renderer/src/anteprime'; export * from './src/renderer/src/controlli'; export * from './src/renderer/src/stili'; export { CATEGORIE } from './src/renderer/src/rapide'; export { PRESET } from './src/renderer/src/preset'; export { elencoWorkflow } from './src/renderer/src/workflow'", + resolveDir: process.cwd(), loader: 'ts' + }, + bundle: true, platform: 'node', format: 'cjs', jsx: 'automatic', alias: { '@shared': resolve('src/shared') }, outfile: 'test/.out/asset.cjs' +}) +const m = createRequire(import.meta.url)('./.out/asset.cjs') + +const RADICE = 'resources/gui' +const file = (chiave) => join(RADICE, `${chiave}.webp`) +const usate = new Set() +/** la chiave deve esistere come file; la registra fra quelle usate */ +const esiste = (chiave, dove) => { + assert.ok(chiave, `${dove}: nessuna chiave asset`) + assert.ok(existsSync(file(chiave)), `${dove}: manca ${file(chiave)}`) + usate.add(chiave) +} + +// ---- stili: 28, uno per file +assert.equal(m.STILI.length, 28) +for (const s of m.STILI) esiste(m.chiaveStile(s.id), `stile ${s.id}`) + +// ---- generi: ogni tessera è un genere vero (preset) e c'è «Libero» +const idPreset = new Set(m.PRESET.map((p) => p.id)) +assert.equal(m.GENERI_TESSERA.length, 11) +for (const g of m.GENERI_TESSERA) { + assert.ok(idPreset.has(g), `genere ${g}: non c'è in preset.ts`) + esiste(m.chiaveGenere(g), `genere ${g}`) +} +esiste(m.chiaveGenere(''), 'genere libero') + +// ---- controlli di Modifica con le anteprime: ogni opzione ha il suo file +let opzioni = 0 +for (const [id, gruppo] of Object.entries(m.GRUPPO_CONTROLLO)) { + const c = m.CONTROLLI.find((x) => x.id === id) + assert.ok(c, `controllo ${id}: non esiste in controlli.ts`) + for (const parte of c.parti.filter((p) => p.tipo === 'griglia')) { + for (const o of parte.opzioni) { + const k = m.chiaveOpzione(id, o.id) + assert.ok(k.startsWith(gruppo + '/'), `${id}/${o.id}: finisce nella cartella sbagliata (${k})`) + esiste(k, `controllo ${id}/${o.id}`) + opzioni++ + } + } +} +assert.equal(opzioni, 5 + 9 + 10 + 6 + 4 + 6, 'animali + costumi + luoghi + meteo + stagioni + espressioni') +// i controlli senza anteprima non devono avere la chiave (restano con l'icona) +assert.equal(m.chiaveOpzione('capelli', 'corti'), undefined) +assert.equal(m.chiaveOpzione('barba', 'rasata'), undefined) + +// ---- categorie di Modifica, Estrai/Espandi, modi +const idCategorie = new Set(m.CATEGORIE.map((c) => c.id)) +for (const id of m.CATEGORIE_CON_ASSET) { + assert.ok(idCategorie.has(id), `categoria ${id}: non esiste in rapide.ts`) + esiste(m.chiaveCategoria(id), `categoria ${id}`) +} +assert.equal(m.chiaveCategoria('zona-base'), undefined, 'la categoria Zona resta con l\'icona') +for (const id of ['scrivi', 'espandi', 'estrai']) esiste(m.chiaveModoModifica(id), `modo modifica ${id}`) +for (const id of ['crea', 'modifica', 'box', 'ricette']) esiste(m.chiaveModo(id), `modo ${id}`) + +// ---- ricette: le sei di ricetteV2 (l'elenco di workflow.ts, senza quelle dell'utente), più le due metà di Mix +const ricette = m.elencoWorkflow().filter((w) => !w.utente) +assert.deepEqual(ricette.map((w) => w.nome), ['Foglio', 'Mondi', 'Mix', 'Set', 'Formati', 'Rinnova']) +for (const w of ricette) esiste(m.chiaveRicetta(w.id), `ricetta ${w.id}`) +assert.equal(m.chiaveRicetta('unisci'), 'ricette/mix') +assert.equal(m.chiaveRicetta('esempio'), 'ricette/mix') +assert.equal(m.chiaveRicetta('mia-ricetta'), undefined, 'le ricette dell\'utente non hanno locandina') + +// ---- LoRA: la copertina generica della famiglia, e niente se non si capisce +assert.equal(m.chiaveLora({ famiglie: ['qwen'] }), 'lora/qwen') +assert.equal(m.chiaveLora({ famiglie: ['klein4b'] }), 'lora/klein') +assert.equal(m.chiaveLora({ famiglie: ['klein', 'klein-base', 'klein4b'] }), 'lora/klein') +assert.equal(m.chiaveLora({ famiglie: ['qwen', 'klein'] }), undefined) +assert.equal(m.chiaveLora({ compatibile: 'si' }), 'lora/qwen') +assert.equal(m.chiaveLora({ compatibile: '?' }), undefined) +esiste('lora/qwen', 'lora qwen'); esiste('lora/klein', 'lora klein') + +// ---- sfondi e mascotte +for (const k of [...Object.values(m.SFONDI), ...Object.values(m.MASCOTTE)]) esiste(k, `fisso ${k}`) + +// ---- i file: WebP veri, nel manifest, nessuno rotto; e quali non servono a nessuna tessera (solo da dire) +const manifest = JSON.parse(readFileSync(join(RADICE, 'manifest.json'), 'utf8')).asset +const tutti = [] +for (const g of readdirSync(RADICE)) { + const d = join(RADICE, g) + if (!statSync(d).isDirectory()) continue + for (const f of readdirSync(d)) if (f.endsWith('.webp')) tutti.push(`${g}/${f.slice(0, -5)}`) +} +assert.equal(tutti.length, 111, 'i WebP in resources/gui') +for (const k of tutti) { + const b = readFileSync(file(k)) + assert.ok(b.length > 200 && b.toString('latin1', 0, 4) === 'RIFF' && b.toString('latin1', 8, 12) === 'WEBP', `${k}: non è un WebP valido`) + assert.ok(manifest[k], `${k}: non è nel manifest`) +} +for (const k of usate) assert.ok(tutti.includes(k), `${k}: usato ma non trovato`) +const nonUsati = tutti.filter((k) => !usate.has(k) && !k.startsWith('sfondi/')) +console.log(`asset: ${tutti.length} file, ${usate.size} usati da tessere, sfondi e mascotte; senza tessera: ${nonUsati.length ? nonUsati.join(', ') : 'nessuno'}`) +assert.deepEqual(nonUsati, [], 'ogni asset deve servire a una tessera (o va tolto/documentato)') + +// ---- i url() del CSS arrivano a un file vero +let url = 0 +for (const css of ['src/renderer/src/orbita.css', 'src/renderer/src/stile.css']) { + for (const [, p] of readFileSync(css, 'utf8').matchAll(/url\(['"]?(\.\.\/[^'")]+)['"]?\)/g)) { + assert.ok(existsSync(resolve(css, '..', p)), `${css}: url(${p}) non porta a un file`) + url++ + } +} +assert.ok(url >= 5, 'i url() degli sfondi nel CSS') + +// ---- l'installer: gli asset passano da Vite (import ?url) e non da extraResources; il codice non punta a percorsi assoluti +const codice = readFileSync('src/renderer/src/asset.ts', 'utf8') +assert.ok(/import\.meta\.glob\('\.\.\/\.\.\/\.\.\/resources\/gui\/\*\*\/\*\.webp'/.test(codice), 'asset.ts importa resources/gui con Vite') +assert.ok(/query: '\?url'/.test(codice)) +console.log(`asset OK — ${usate.size} chiavi, ${opzioni} opzioni dei controlli, ${url} url() nel CSS`)