Le prove nel browser a sezioni (idea di Andrea): scripts/verify.mjs rifà solo le prove i cui ingressi sono cambiati dall'ultimo verde (.verify-ok.json fuori da git); make verify le usa, make verify-plan spiega; i minigiochi entrano in make verify; due alla volta, perché con quattro le schermate vanno in timeout. Test del pianificatore (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -7,3 +7,4 @@ assets/generated/
|
||||
.venv/
|
||||
.env
|
||||
deploy.local.mk
|
||||
.verify-ok.json
|
||||
|
||||
@@ -16,6 +16,9 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p
|
||||
(`tools/slice_sprites.py`, `scripts/build-site.sh`, `scripts/bump.sh`, `scripts/check-public.sh`, action di Pages).
|
||||
- Nessun build step: ES modules statici, server locale con `python3 -m http.server`.
|
||||
- Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura.
|
||||
- Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi
|
||||
sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile
|
||||
o risorse). Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende).
|
||||
- Verifica visiva nel browser con Playwright e swiftshader prima di dichiarare finito qualcosa di grafico.
|
||||
- Pubblicare (tag/bump/push) solo quando Andrea lo chiede.
|
||||
- Persone vere nel gioco solo con consenso; niente nomi, personaggi o musiche di giochi esistenti.
|
||||
|
||||
@@ -23,7 +23,7 @@ DST ?= assets/scenes/$(notdir $(SRC))
|
||||
CROP ?=
|
||||
COLORS ?= 64
|
||||
|
||||
.PHONY: help serve open test browser verify check-public hooks bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \
|
||||
.PHONY: help serve open test browser verify verify-browser verify-plan check-public hooks bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \
|
||||
build up down restart logs ps shell image-size deploy deploy-logs runner clean distclean
|
||||
|
||||
## ------------------------------------------------------------ sviluppo
|
||||
@@ -50,7 +50,15 @@ test: ## Test della logica, della storia, della soluzione completa e del generat
|
||||
browser: ## Partita completa nel browser con Playwright (tre parti in processi paralleli, ~4,5 min)
|
||||
$(NODE) tests/browser.mjs
|
||||
|
||||
verify: test browser check-public ## Tutti i controlli
|
||||
verify: test check-public verify-browser ## Tutti i controlli: test unitari, file pubblicabili, prove nel browser cambiate (ALL=1: tutte)
|
||||
|
||||
# Le prove nel browser a sezioni: si rifanno solo quelle i cui ingressi sono cambiati dall'ultimo verde (.verify-ok.json,
|
||||
# fuori da git); make verify-browser ALL=1 le rifà tutte, make verify-plan dice solo cosa rifarebbe; JOBS=2 in parallelo (con 4 vanno in timeout).
|
||||
verify-browser: ## Le prove nel browser da rifare (ALL=1 tutte; PROVE="briscola flipper" solo quelle)
|
||||
$(NODE) scripts/verify.mjs $(if $(ALL),--all,) $(PROVE)
|
||||
|
||||
verify-plan: ## Dice quali prove nel browser rifarebbe make verify, e perché
|
||||
$(NODE) scripts/verify.mjs --plan
|
||||
|
||||
check-public: ## Prima di pubblicare: niente prompt, fogli grezzi, segreti o nomi della rete di casa (tutta la storia)
|
||||
bash scripts/check-public.sh
|
||||
|
||||
@@ -48,7 +48,7 @@ Oppure `make up` (Docker, su http://localhost:8080). Non aprire `index.html` con
|
||||
- `docs/`: `PROGETTO.md` (l'idea, i personaggi, la mappa) e `STATO.md` (dove siamo, cosa manca).
|
||||
|
||||
```zsh
|
||||
make verify # sintassi, test, prova nel browser: quello che gira prima di ogni versione
|
||||
make verify # test, file pubblicabili e le prove nel browser cambiate dall'ultimo verde (ALL=1: tutte)
|
||||
make voci # le voci delle battute nuove (XTTS-v2 su GPU)
|
||||
make bump # nuova versione: verifica, commit e tag
|
||||
make deploy # pubblica la versione: immagine sul registry di Gitea, il server la scarica
|
||||
|
||||
@@ -310,6 +310,13 @@ versione nuova.
|
||||
la frase è una riga sola coi puntini. «Schermo intero» nella Pausa (dove il browser lo permette). A 1040×590 la scena
|
||||
passa da 604 a 842 px di larghezza, a 1366×650 da 711 a 935; sui telefoni niente cambia. Prova in `browser.mjs`.
|
||||
|
||||
- **Prove a sezioni (1 novembre, idea di Andrea):** `scripts/verify.mjs` conosce gli ingressi di ogni prova nel browser
|
||||
(i file da cui dipende: i propri, più il motore comune; per la partita intera tutto il gioco tranne i minigiochi a sé)
|
||||
e rifà solo quelle la cui impronta è cambiata dall'ultimo verde, ricordato in `.verify-ok.json` fuori da git. Così
|
||||
`make bump` dopo una modifica alla briscola riprova la briscola e basta; dopo una modifica alla storia la partita intera
|
||||
e le scene speciali (ambient, veicoli, ingorgo, smarronata), non la briscola; dopo `style.css` o `game.js` tutto.
|
||||
Le prove dei minigiochi, che prima non entravano in `make verify`, ora ci stanno. `make verify-plan` spiega.
|
||||
|
||||
## Materiale ricevuto da Andrea (in `assets/raw/`, escluso da git)
|
||||
|
||||
| File | Cosa | Uso previsto |
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
// Le prove nel browser a sezioni (idea di Andrea, 1 novembre 2026): ogni prova ha i suoi ingressi, i file da cui dipende;
|
||||
// si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo
|
||||
// verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il
|
||||
// motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola.
|
||||
// node scripts/verify.mjs le prove da rifare, in parallelo (JOBS=2: con quattro Chromium swiftshader insieme le schermate vanno in timeout), e aggiorna i verdi
|
||||
// node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché
|
||||
// node scripts/verify.mjs --all tutte, senza guardare i verdi
|
||||
// node scripts/verify.mjs briscola solo quelle nominate (anche se verdi)
|
||||
// I test unitari e check-public non stanno qui: costano pochi secondi e girano sempre (make verify).
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { cpus } from 'node:os';
|
||||
|
||||
// Il motore comune a tutto: cambia questo e si riprova ogni minigioco.
|
||||
export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js',
|
||||
'tests/helpers.mjs', 'package-lock.json', 'scripts/verify.mjs'];
|
||||
// Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare),
|
||||
// test unitari (girano sempre, a parte).
|
||||
export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\/(?!verify\.mjs)/,
|
||||
/^tools\//, /^video\//, /^\.gitea\//, /^prompts\//, /^LICENSE$/, /^Dockerfile$/, /^nginx\.conf$/, /^docker-compose/, /^requirements\.txt$/,
|
||||
/^\.(gitignore|dockerignore|verify-ok\.json)$/, /^tests\/[^/]*\.test\.mjs$/, /^tests\/test_[^/]*\.py$/, /^deploy\.local\.mk$/, /(^|\/)__pycache__\//];
|
||||
|
||||
// Le prove. `core: true` = dipende da tutto il gioco tranne i file dei minigiochi a sé (che hanno la loro prova); altrimenti
|
||||
// solo dal motore e dai propri file: quelli che si chiamano come la prova (src/engine/flipper.js, flipper-render.js,
|
||||
// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `run` sono gli argomenti di node.
|
||||
export const SUITES = {
|
||||
principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' },
|
||||
minigiochi: { core: true, run: ['tests/browser.mjs', '--parte=minigiochi'], note: 'libro, bocce, tiri, pesca e biliardo dalla partita' },
|
||||
telefoni: { core: true, run: ['tests/browser.mjs', '--parte=telefoni'], note: 'multimedia, telefoni, la finestra di Andrea, debug' },
|
||||
ambient: { core: true, run: ['tests/ambient.browser.mjs'], note: 'tutte le scene nelle tre fasi e gli effetti' },
|
||||
veicoli: { core: true, run: ['tests/veicoli.browser.mjs'], note: 'i veicoli nelle scene' },
|
||||
ingorgo: { core: true, run: ['tests/ingorgo.browser.mjs'], note: "l'Ingorgo Sonoro" },
|
||||
smarronata: { core: true, run: ['tests/smarronata.browser.mjs'], note: 'la Smarronata del 1993' },
|
||||
briscola: { run: ['tests/briscola.browser.mjs'] },
|
||||
tressette: { run: ['tests/tressette.browser.mjs'] },
|
||||
scopa: { run: ['tests/scopa.browser.mjs'] },
|
||||
biliardo: { run: ['tests/biliardo.browser.mjs'] },
|
||||
flipper: { run: ['tests/flipper.browser.mjs'] },
|
||||
micro: { run: ['tests/micro.browser.mjs'] },
|
||||
cerbottana: { run: ['tests/cerbottana.browser.mjs'] },
|
||||
condor: { run: ['tests/condor.browser.mjs'] },
|
||||
campana: { run: ['tests/campana.browser.mjs'] },
|
||||
bruciate: { run: ['tests/bruciate.browser.mjs'] },
|
||||
gara: { run: ['tests/gara.browser.mjs'], extra: ['src/engine/corsa.js', 'src/story/piloti.js', /^assets\/[^/]*\/(gara|gp|pilot)/] },
|
||||
telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] },
|
||||
risveglio: { run: ['tests/risveglio.browser.mjs'] },
|
||||
};
|
||||
|
||||
const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i');
|
||||
const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path;
|
||||
export const ignored = path => IGNORED.some(r => r.test(path));
|
||||
export function ownFiles(name, files) {
|
||||
const s = SUITES[name], re = ownRe(name);
|
||||
return files.filter(p => re.test(p) || (s.extra || []).some(r => matches(p, r)));
|
||||
}
|
||||
// Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella).
|
||||
export function inputsOf(name, files) {
|
||||
const s = SUITES[name], live = files.filter(p => !ignored(p));
|
||||
if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live)])].sort();
|
||||
const others = Object.keys(SUITES).filter(n => !SUITES[n].core);
|
||||
const foreign = new Set(others.flatMap(n => ownFiles(n, live)));
|
||||
return live.filter(p => !foreign.has(p)).sort();
|
||||
}
|
||||
export const fingerprint = (paths, hashOf) => createHash('sha1').update(paths.map(p => `${p} ${hashOf(p)}`).join('\n')).digest('hex');
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const git = (...a) => execFileSync('git', a, { cwd: ROOT, encoding: 'utf8', maxBuffer: 1 << 26 });
|
||||
|
||||
// I file del progetto con l'impronta di ognuno: il blob dell'indice di git, o il contenuto attuale se modificato o nuovo.
|
||||
export function snapshot() {
|
||||
const hashes = new Map();
|
||||
for (const line of git('ls-files', '-s', '-z').split('\0')) if (line) { const [meta, path] = line.split('\t'); hashes.set(path, meta.split(' ')[1]); }
|
||||
const dirty = [];
|
||||
for (const line of git('status', '--porcelain', '-z', '--untracked-files=all').split('\0')) {
|
||||
if (!line) continue;
|
||||
const code = line.slice(0, 2), path = line.slice(3);
|
||||
if (code.includes('D')) hashes.delete(path); else dirty.push(path);
|
||||
}
|
||||
for (const path of dirty) if (existsSync(`${ROOT}/${path}`)) hashes.set(path, git('hash-object', '--', path).trim());
|
||||
return hashes;
|
||||
}
|
||||
|
||||
const OK_FILE = `${ROOT}/.verify-ok.json`;
|
||||
const readOk = () => { try { return JSON.parse(readFileSync(OK_FILE, 'utf8')); } catch { return {}; } };
|
||||
|
||||
export function plan(hashes, ok, names = Object.keys(SUITES)) {
|
||||
const files = [...hashes.keys()], out = [];
|
||||
for (const name of names) {
|
||||
const inputs = inputsOf(name, files), fp = fingerprint(inputs, p => hashes.get(p));
|
||||
const was = ok[name], changed = was?.fingerprint === fp ? [] : inputs.filter(p => !was?.hashes || was.hashes[p] !== hashes.get(p));
|
||||
out.push({ name, fp, inputs, changed, green: was?.fingerprint === fp, seconds: was?.seconds });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function run(name) {
|
||||
return new Promise(resolve => {
|
||||
const t0 = Date.now(), lines = [];
|
||||
const p = spawn(process.execPath, SUITES[name].run, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let rest = '';
|
||||
const onData = chunk => { const parts = (rest + chunk).split('\n'); rest = parts.pop(); for (const l of parts) { lines.push(l); console.log(`[${name}] ${l}`); } };
|
||||
p.stdout.on('data', onData); p.stderr.on('data', onData);
|
||||
p.on('close', code => { if (rest) console.log(`[${name}] ${rest}`); resolve({ name, code, seconds: Math.round((Date.now() - t0) / 1000) }); });
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2), all = args.includes('--all'), onlyPlan = args.includes('--plan');
|
||||
const named = args.filter(a => !a.startsWith('--'));
|
||||
for (const n of named) if (!SUITES[n]) { console.error(`prova sconosciuta: ${n} (${Object.keys(SUITES).join(', ')})`); process.exit(2); }
|
||||
const hashes = snapshot(), ok = readOk();
|
||||
const todo = plan(hashes, ok, named.length ? named : undefined).filter(p => all || named.length || !p.green);
|
||||
const green = Object.keys(SUITES).filter(n => !todo.some(t => t.name === n));
|
||||
if (!todo.length) { console.log(`Prove nel browser: tutte verdi dall'ultimo giro (${green.length}); niente da rifare.`); return; }
|
||||
for (const t of todo) {
|
||||
const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`;
|
||||
console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`);
|
||||
}
|
||||
if (green.length) console.log(`già verdi: ${green.join(', ')}`);
|
||||
if (onlyPlan) return;
|
||||
const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(2, Math.floor(cpus().length / 4)));
|
||||
const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = [];
|
||||
const worker = async () => { for (let t; (t = queue.shift());) {
|
||||
const r = await run(t.name); results.push(r);
|
||||
if (r.code === 0) {
|
||||
const now = readOk();
|
||||
now[t.name] = { fingerprint: t.fp, seconds: r.seconds, when: new Date().toISOString(), hashes: Object.fromEntries(t.inputs.map(p => [p, hashes.get(p)])) };
|
||||
writeFileSync(OK_FILE, JSON.stringify(now, null, 1) + '\n');
|
||||
}
|
||||
} };
|
||||
await Promise.all(Array.from({ length: Math.min(jobs, queue.length) }, worker));
|
||||
for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`);
|
||||
const failed = results.filter(r => r.code !== 0).map(r => r.name);
|
||||
if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); }
|
||||
console.log(`Prove nel browser OK: ${results.length} rifatte, ${green.length} già verdi.`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
|
||||
@@ -0,0 +1,72 @@
|
||||
// Le prove nel browser a sezioni (scripts/verify.mjs): ogni file del gioco finisce negli ingressi delle prove giuste,
|
||||
// e l'impronta cambia solo se cambia un ingresso.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts/verify.mjs';
|
||||
|
||||
const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md',
|
||||
'src/engine/game.js', 'src/engine/render.js', 'src/engine/ambient.js', 'src/engine/bocce.js', 'src/engine/nuovo.js',
|
||||
'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/corsa.js', 'src/story/piloti.js',
|
||||
'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png',
|
||||
'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md',
|
||||
'tests/helpers.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs',
|
||||
'scripts/verify.mjs', 'scripts/bump.sh', 'tools/slice_sprites.py', '.verify-ok.json'];
|
||||
const suitesWith = path => Object.keys(SUITES).filter(n => inputsOf(n, FILES).includes(path));
|
||||
const islands = Object.keys(SUITES).filter(n => !SUITES[n].core), cores = Object.keys(SUITES).filter(n => SUITES[n].core);
|
||||
|
||||
test('ogni prova nel browser di tests/ ha la sua voce, e una sola', () => {
|
||||
const runs = Object.values(SUITES).map(s => s.run[0]);
|
||||
for (const f of readdirSync('tests').filter(f => f.endsWith('.browser.mjs'))) {
|
||||
if (f === 'multimedia.browser.mjs') continue; // la chiama la parte «telefoni» di browser.mjs
|
||||
const n = runs.filter(r => r === `tests/${f}`).length;
|
||||
assert.equal(n, f === 'browser.mjs' ? 3 : 1, `tests/${f} è in ${n} prove`);
|
||||
}
|
||||
});
|
||||
|
||||
test('la briscola si riprova solo se cambia la briscola o il motore', () => {
|
||||
assert.deepEqual(suitesWith('src/engine/briscola.js'), ['briscola']);
|
||||
assert.deepEqual(suitesWith('src/engine/briscola-render.js'), ['briscola']);
|
||||
assert.deepEqual(suitesWith('assets/video/briscola.mp4'), ['briscola']);
|
||||
assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']);
|
||||
assert.deepEqual(suitesWith('docs/BRISCOLA.md'), [], 'i documenti non fanno rifare niente');
|
||||
assert.ok(!inputsOf('briscola', FILES).includes('src/story/story.js'), 'la storia non fa riprovare la briscola');
|
||||
assert.deepEqual(suitesWith('src/engine/corsa.js'), ['gara']);
|
||||
assert.deepEqual(suitesWith('src/story/piloti.js'), ['gara']);
|
||||
assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']);
|
||||
assert.deepEqual(suitesWith('assets/muro/index.html'), ['telefono']);
|
||||
assert.deepEqual(suitesWith('assets/closeups/micro-parcheggio.png'), ['micro']);
|
||||
});
|
||||
|
||||
test('la storia, le scene, i minigiochi dentro la partita e un modulo nuovo fanno rifare la partita intera, non i minigiochi a sé', () => {
|
||||
for (const path of ['src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'src/engine/bocce.js', 'src/engine/ambient.js', 'src/engine/nuovo.js', 'tests/browser.mjs']) {
|
||||
assert.deepEqual(suitesWith(path), cores, path);
|
||||
}
|
||||
});
|
||||
|
||||
test('il motore comune fa rifare tutto', () => {
|
||||
for (const path of ENGINE) assert.deepEqual(suitesWith(path), Object.keys(SUITES), path);
|
||||
assert.ok(islands.length >= 10 && cores.length >= 3);
|
||||
});
|
||||
|
||||
test('versione, package.json, Makefile, documenti, strumenti e test unitari non fanno rifare niente', () => {
|
||||
for (const path of ['src/version.js', 'package.json', 'Makefile', 'README.md', 'docs/STATO.md', 'scripts/bump.sh', 'tools/slice_sprites.py', 'tests/briscola.test.mjs', 'tests/story.test.mjs', '.verify-ok.json']) {
|
||||
assert.ok(ignored(path), `${path} dovrebbe essere ignorato`);
|
||||
assert.deepEqual(suitesWith(path), [], path);
|
||||
}
|
||||
});
|
||||
|
||||
test("l'impronta cambia solo se cambia un ingresso, e il piano dice quali", () => {
|
||||
const hashes = new Map(FILES.map(p => [p, 'h-' + p]));
|
||||
const before = plan(hashes, {});
|
||||
assert.ok(before.every(p => !p.green), 'mai provate: tutte da rifare');
|
||||
const ok = Object.fromEntries(before.map(p => [p.name, { fingerprint: p.fp, hashes: Object.fromEntries(p.inputs.map(f => [f, hashes.get(f)])) }]));
|
||||
assert.ok(plan(hashes, ok).every(p => p.green), 'niente cambiato: tutte verdi');
|
||||
hashes.set('docs/STATO.md', 'altro');
|
||||
assert.ok(plan(hashes, ok).every(p => p.green), 'un documento non cambia niente');
|
||||
hashes.set('src/engine/briscola.js', 'altro');
|
||||
const after = plan(hashes, ok);
|
||||
assert.deepEqual(after.filter(p => !p.green).map(p => p.name), ['briscola']);
|
||||
assert.deepEqual(after.find(p => p.name === 'briscola').changed, ['src/engine/briscola.js']);
|
||||
assert.notEqual(fingerprint(['a'], () => '1'), fingerprint(['a'], () => '2'));
|
||||
});
|
||||
Reference in New Issue
Block a user