El teu projecte fa el que promet: el domini compleix les regles, les dades sobreviuen a la recàrrega, l'aplicació parla amb una API i treballa sense connexió. I ara arriba la pregunta incòmoda, la que separa una demo d'un producte: com saps que ho continua fent demà? Perquè tens migracions que poden corrompre dades silenciosament, sincronització amb curses que només apareixen amb mala xarxa, i vistes que poden retenir memòria en navegar — tres coses que no es veuen mirant la pantalla i que, quan fallen, fallen al dispositiu d'una altra persona. En aquesta lliçó portes al projecte propi tot el que vas aprendre al Mòdul 8, però amb una diferència important: allà les proves se't donaven escrites, i aquí has de decidir què provar, en quin nivell i fins on. Veuràs com definir una estratègia de proves amb la piràmide aplicada als teus mòduls concrets i un objectiu de cobertura raonat en lloc de cosmètic; com provar el domini exhaustivament amb proves parametritzades de les quinze regles; com provar la capa de dades amb dobles, temporitzadors falsos i fetch simulat, incloses les migracions i la cua; com provar la vista consultant per rol i també el recorregut amb teclat; com triar tres recorreguts d'extrem a extrem i per què no més; com muntar la integració contínua completa amb pressupost de rendiment; com depurar tres errades que apareixeran de debò al teu projecte, amb la tècnica de reproduir primer i escriure una prova que falli; com automatitzar i complementar les proves d'accessibilitat; i com mesurar el teu projecte contra la línia base de Nómada Tasques. En acabar tindràs la suite completa en verd a la CI i l'informe d'accessibilitat i rendiment.
Contingut
- L'estratègia de proves: decidir què es prova i on
- La piràmide aplicada als teus mòduls concrets
- Un objectiu de cobertura raonat, no cosmètic
- Provar el domini: les quinze regles parametritzades
- Provar les transicions i els casos límit
- Provar la capa de dades amb dobles i temporitzadors falsos
- Provar les migracions i la cua offline
- Provar la vista amb jsdom i Testing Library
- Provar el recorregut amb teclat
- Tres recorreguts d'extrem a extrem, i per què no més
- Integració contínua amb GitHub Actions
- Lighthouse CI i el pressupost de rendiment
- Depurar el projecte propi: el mètode aplicat
- Errada 1: la migració que corromp dades
- Errada 2: la condició de cursa en sincronitzar
- Errada 3: la fuita de memòria en navegar
- Proves d'accessibilitat: automàtiques i manuals
- Mesurar el rendiment del projecte propi
- Les proves intermitents
- Errors Habituals i Consells
- Exercicis
- Conclusió
- L'estratègia de proves: decidir què es prova i on
Abans d'escriure ni una prova més, cal respondre tres preguntes. Escriure-les a docs/estrategia-proves.md és mitja hora que estalvia setmanes de proves mal col·locades.
Pregunta 1 · Què és el que no pot fallar mai?
A Òrbita, la resposta és concreta: les regles de negoci i la persistència. Que un botó estigui dos píxels a l'esquerra és un defecte cosmètic; que una migració perdi les tasques d'algú és la mort del producte. Les proves es concentren on l'errada és catastròfica, no on és fàcil escriure-les.
Pregunta 2 · Què canvia sovint i què no?
El que canvia molt —la maquetació, els textos, les classes CSS— no ha d'estar acoblat a les proves, o cada retoc visual trencarà vint proves que no detecten cap errada real. El que canvia poc —les regles, els contractes— es pot provar a fons sense cost de manteniment.
Pregunta 3 · Quant costa cada prova i quant val?
| Nivell | Cost d'escriure | Cost d'executar | Cost de mantenir | Confiança que dona |
|---|---|---|---|---|
| Unitària de domini | Baix | Mil·lisegons | Baix | Alta sobre una regla |
| Integració amb DOM | Mitjà | Desenes de ms | Mitjà | Alta sobre un flux |
| Extrem a extrem | Alt | Segons | Alt | Molt alta sobre el sistema |
La conclusió operativa: moltes unitàries, bastants d'integració, molt poques d'extrem a extrem. No és una preferència estètica; és una conseqüència directa de la taula.
La regla de decisió que resol gairebé tots els casos:
Prova en el nivell més baix que capti l'errada que et preocupa.
Si et preocupa que R13 no bloquegi el tancament, és una prova unitària de domini: no cal un navegador. Si et preocupa que el botó no estigui deshabilitat quan toca, és d'integració amb DOM. Si et preocupa que crear una tasca, recarregar i veure-la no funcioni de punta a punta, és d'extrem a extrem. Provar R13 des de Cypress és cent vegades més lent i dona la mateixa informació.
- La piràmide aplicada als teus mòduls concrets
La piràmide de proves genèrica no ajuda gaire. Aquesta sí, perquè anomena els teus fitxers:
| Mòdul | Nivell principal | Nre. orientatiu | Eina | Què es comprova |
|---|---|---|---|---|
domini/tasca.js |
Unitària (Node) | ~35 | Jest | R1–R6, R8–R10, invariants, toJSON/desDeJSON |
domini/usuari.js |
Unitària (Node) | ~12 | Jest | R11, R15 (rols), normalització |
domini/arbre.js |
Unitària (Node) | ~22 | Jest | R12: construcció, fulles, hores, cicles, profunditat |
domini/tauler.js |
Unitària (Node) | ~25 | Jest | R7, R13, R15 (càrrega), resum, filtres |
domini/regles.js |
— | 0 | — | Són constants; es proven a través de qui les fa servir |
dades/repositori-*.js |
Contracte + integració | ~10 × 3 | Jest + jsdom | El contracte comú contra les tres implementacions |
dades/migracions.js |
Unitària amb fitxers reals | ~14 | Jest | Cada salt, idempotència, versió futura, validesa |
dades/http.js |
Unitària amb fetch simulat |
~15 | Jest + doble | Codis, temps esgotat, reintents, cancel·lació |
dades/cua.js |
Unitària amb temporitzadors falsos | ~12 | Jest + fake timers |
Ordre, persistència, intents, idempotència |
aplicacio/casos-us.js |
Integració | ~18 | Jest + dobles | Estats, reversió optimista, errors |
aplicacio/selectors.js |
Unitària pura | ~14 | Jest | Filtres, càrrega per setmana, derivats |
vista/*-vista.js |
Integració amb DOM | ~30 | Testing Library | Consulta per rol, esdeveniments, accessibilitat, destruir |
vista/formulari-tasca.js |
Integració amb DOM | ~16 | Testing Library + user-event |
Validació, focus, aria-invalid, teclat |
| Recorreguts complets | Extrem a extrem | 3 | Cypress | Els tres fluxos que defineixen el producte |
Total orientatiu: unes 250 proves, de les quals tres són d'extrem a extrem. Nómada Tasques tenia 124 proves i 3 recorreguts amb menys funcionalitats; l'ordre de magnitud és coherent.
Dues observacions sobre aquesta taula:
regles.jsno es prova directament. Són constants. Una prova que comprova queMAX_HORES === 40no verifica res: reescriu el valor en un altre lloc. El que es prova és que la regla s'aplica, i això passa atasca.js.- La proporció vista/domini és d'aproximadament 1 a 2. Si el teu projecte tingués més proves de vista que de domini, seria senyal que la lògica és a la vista — exactament el que l'arquitectura de 11-01 pretenia evitar. La distribució de les teves proves és un diagnòstic de la teva arquitectura.
- Un objectiu de cobertura raonat, no cosmètic
La cobertura mesura quin percentatge del codi s'executa durant les proves. És una mètrica útil i perillosa a parts iguals, i convé entendre per què.
El que la cobertura sí que diu: quin codi no està provat en absolut. Un 0 % a migracions.js és una alarma real.
El que la cobertura no diu: si el que és provat està ben provat. Aquesta prova dona 100 % de cobertura i no verifica res:
test('crea una tasca', () => {
const tasca = new Tasca(dadesValides);
expect(tasca).toBeDefined(); // ← sempre passa; no comprova res util
});Per això un objectiu únic per a tot el projecte és un mal objectiu. El raonable és per capa, i amb criteri:
// jest.config.js
export default {
projects: [
{ displayName: 'domini', testEnvironment: 'node', testMatch: ['<rootDir>/test/domini/**/*.test.js'] },
{ displayName: 'dades', testEnvironment: 'jsdom', testMatch: ['<rootDir>/test/dades/**/*.test.js'] },
{ displayName: 'vista', testEnvironment: 'jsdom', testMatch: ['<rootDir>/test/vista/**/*.test.js'] }
],
collectCoverageFrom: ['src/**/*.js', '!src/main.js', '!src/dades/llavor.js'],
coverageThreshold: {
global: { branches: 75, functions: 80, lines: 80 },
'./src/domini/': { branches: 95, functions: 95, lines: 95 },
'./src/dades/migracions.js': { branches: 100, functions: 100, lines: 100 },
'./src/aplicacio/': { branches: 85, functions: 85, lines: 85 },
'./src/vista/': { branches: 60, functions: 65, lines: 65 }
}
};La justificació de cada llindar, que és el que cal escriure a la teva estratègia:
| Ruta | Llindar | Per què |
|---|---|---|
domini/ |
95 % | És on viu el valor i on una errada és una regla incomplerta. Barat de provar |
migracions.js |
100 % | Un camí no provat és un camí que pot destruir dades irreversiblement |
aplicacio/ |
85 % | Orquestració; importen els camins d'error, que són els que s'obliden |
vista/ |
65 % | Provar cada branca de maquetació és car i fràgil. Es proven els fluxos, no els píxels |
| Global | 80 % | Conseqüència dels anteriors, no un objectiu en si |
I la mètrica que de debò importa no és el percentatge: és la cobertura de les regles. Mantén una taula a docs/estrategia-proves.md:
| Regla | Cas que passa | Cas que falla | Cas límit | Fitxer |
|---|---|---|---|---|
| R1 | ✅ | ✅ | ✅ id després d'esborrar l'últim | domini/tasca.test.js |
| R2 | ✅ | ✅ | ✅ només espais i només tabuladors | domini/tasca.test.js |
| R3 | ✅ | ✅ | ✅ 0, 0.5, 40, 40.1 | domini/tasca.test.js |
| … | ||||
| R12 | ✅ | ✅ | ✅ els tres tipus de cicle | domini/arbre.test.js |
| R13 | ✅ | ✅ | ✅ sense subtasques | domini/tauler.test.js |
| R14 | ✅ | ✅ | ✅ intent de modificar l'historial | domini/historial.test.js |
| R15 | ✅ | ✅ | ✅ setmana 53, canvi d'any | domini/tauler.test.js |
Quinze regles × tres casos = quaranta-cinc proves mínimes. Aquesta taula és un objectiu molt més honest que «85 % de cobertura», perquè no es pot aprovar sense verificar res.
- Provar el domini: les quinze regles parametritzades
Les proves parametritzades són l'eina natural per a les regles, perquè una regla és una afirmació sobre un conjunt de casos.
// test/domini/regles-validacio.test.js
import { Tasca } from '../../src/domini/tasca.js';
import { ErrorDeValidacio } from '../../src/domini/errors.js';
import { tascaValida } from '../ajudes/fabriques.js';
describe.each([
// regla, camp, valor, valid?, nota
['R2', 'titol', 'Revisar el torn', true, 'normal'],
['R2', 'titol', '', false, 'buit'],
['R2', 'titol', ' ', false, 'nomes espais'],
['R2', 'titol', '\t\n', false, 'nomes blancs'],
['R2', 'titol', 'a', true, 'un caracter'],
['R3', 'horesEstimades', 0, false, 'zero'],
['R3', 'horesEstimades', 0.5, true, 'minim raonable'],
['R3', 'horesEstimades', 40, true, 'limit exacte'],
['R3', 'horesEstimades', 40.1, false, 'just per sobre'],
['R3', 'horesEstimades', -3, false, 'negatiu'],
['R3', 'horesEstimades', NaN, false, 'no numeric'],
['R3', 'horesEstimades', '12', false, 'text: no es converteix'],
['R8', 'responsableId', null, true, 'sense assignar'],
['R8', 'responsableId', '', false, 'cadena buida prohibida'],
['R4', 'dataLimit', '2020-01-01', false, 'anterior a la creacio']
])('%s · %s = %p (%s)', (regla, camp, valor, valid) => {
test(valid ? 'accepta el valor' : 'es rebutja amb ErrorDeValidacio', () => {
const construir = () => new Tasca({ ...tascaValida(), [camp]: valor });
if (valid) {
expect(construir).not.toThrow();
} else {
expect(construir).toThrow(ErrorDeValidacio);
try { construir(); } catch (e) { expect(e.camp).toBe(camp); }
}
});
});Quatre decisions d'aquesta taula que la fan valuosa:
1 · La taula és llegible com a especificació. Qualsevol que la llegeixi sap exactament què accepta i què rebutja el model, sense llegir el codi de Tasca.
2 · Cada regla té els seus límits exactes. Per a R3: 0 no, 0.5 sí, 40 sí, 40.1 no. Les errades dels desenvolupadors viuen als > que haurien de ser >=, i només es cacen provant el valor exacte del límit.
3 · Es comprova error.camp, no només el tipus. Aquesta dada és el que permet al formulari marcar el camp correcte (11-02). Si no es prova, un dia algú posa camp: 'hores' en lloc de 'horesEstimades' i el formulari deixa de ressaltar res, en silenci.
4 · Hi ha casos de tipus, no només de valor. '12' com a text i NaN són els que se sobrepassen per validacions ingènues com if (hores > 40). Amb NaN, aquesta comparació és false i el valor passa.
El cas R14 mereix una prova especial, perquè la seva invariant no és sobre un valor sinó sobre l'estructura:
test('R14 · una entrada d historial no es pot modificar', () => {
const entrada = historial.registrar({ tascaId: 1, camp: 'estat', abans: 'pendent', despres: 'en-curs' });
expect(() => { entrada.despres = 'feta'; }).toThrow(TypeError); // congelada
expect(historial.llistar(1)).toHaveLength(1);
});
test('R14 · una correccio genera una entrada nova, no modifica l anterior', () => {
historial.registrar({ tascaId: 1, camp: 'hores', abans: 10, despres: 99 });
historial.registrar({ tascaId: 1, camp: 'hores', abans: 99, despres: 12 });
expect(historial.llistar(1)).toHaveLength(2);
expect(historial.llistar(1)[0].despres).toBe(99); // la primera continua dient 99
});
- Provar les transicions i els casos límit
La matriu de transicions de R6 es prova sencera, com a 11-02. Però hi ha tres tipus de cas límit que s'obliden sistemàticament i que convé enumerar:
Els límits numèrics. Per a cada comparació del teu domini, prova el valor exacte, un per sota i un per sobre:
| Regla | Valors obligatoris |
|---|---|
| R3 (0 < h ≤ 40) | 0, 0.01, 40, 40.01 |
| R7 (≤ 40 h/setmana) | 39.5, 40, 40.5 |
| R12 (profunditat ≤ 3) | 3 nivells (vàlid), 4 (rebutjat) |
Els límits de col·lecció. Per a cada funció que rep una llista:
| Cas | Per què importa |
|---|---|
| Llista buida | reduce sense valor inicial llança; l'estat buit de la interfície en depèn |
| Un element | Els algorismes que comparen parells fallen aquí |
| Tots iguals | Ordenacions inestables, agrupacions degenerades |
Amb null intercalat |
Dades migrades imperfectes |
Els límits temporals. Els que més errades produeixen i menys es proven:
describe.each([
['2026-01-01', 2026, 1, 'any que comenca en dijous'],
['2027-01-01', 2026, 53, 'l 1 de gener cau a la setmana 53 de l any anterior'],
['2028-02-29', 2028, 9, 'any de traspas'],
['2026-12-31', 2026, 53, 'ultim dia de l any']
])('setmana ISO de %s', (data, any, setmana) => {
test(`es ${any}-S${setmana}`, () => {
expect(setmanaISO(data)).toEqual({ any, setmana });
});
});La segona fila és la que trenca gairebé totes les implementacions casolanes de setmana ISO, i afecta directament R15 i l'informe de càrrega. Si el teu projecte fa servir setmanes, aquesta prova no és opcional.
I un cas que només apareix en projectes amb arbres: l'arbre degenerat. Una tasca amb 200 subtasques en un sol nivell, i una cadena de 3 nivells amb una fulla a cadascun. Tots dos exerciten camins diferents de la teva recursivitat.
- Provar la capa de dades amb dobles i temporitzadors falsos
Aquí s'aplica tot el de 08-04. La regla que ho governa: se substitueix el que és lent, no determinista o extern; mai la lògica que estàs provant.
| Què se substitueix | Amb què | Per què |
|---|---|---|
fetch |
Doble que retorna respostes controlades | Sense xarxa, sense servidor, determinista |
| Temporitzadors | jest.useFakeTimers() |
Un reintent de 8 s triga 0 ms a la prova |
Date |
jest.setSystemTime() |
Les proves de venciment no depenen del dia |
localStorage |
El de jsdom, netejat a beforeEach |
Aïllament entre proves |
crypto.randomUUID |
Doble amb seqüència predictible | Poder afirmar sobre els ids |
6.1 fetch simulat
// test/ajudes/fetch-simulat.js
export function simularFetch(respostes) {
const crides = [];
global.fetch = jest.fn(async (url, opcions = {}) => {
crides.push({ url, metode: opcions.method ?? 'GET', cos: opcions.body, capcaleres: opcions.headers });
const seguent = respostes.shift();
if (!seguent) throw new Error(`fetch inesperat a ${url}`);
if (seguent.error) throw seguent.error;
return {
ok: seguent.status < 400,
status: seguent.status,
json: async () => seguent.cos,
text: async () => JSON.stringify(seguent.cos)
};
});
return { crides };
}El detall de throw new Error('fetch inesperat') és important: una crida de més que no esperaves és una errada que vols veure, no una cosa que hagi de retornar undefined i produir un error confús més endavant.
test('un 500 es reintenta i el segon intent te exit', async () => {
const { crides } = simularFetch([
{ status: 500, cos: { missatge: 'Servidor no disponible' } },
{ status: 200, cos: [{ id: 1, titol: 'Revisar el torn' }] }
]);
const repo = new RepositoriApi('http://api.local');
const tasques = await repo.llistarTasques();
expect(tasques).toHaveLength(1);
expect(crides).toHaveLength(2); // es va reintentar exactament una vegada
});
test('un 400 NO es reintenta', async () => {
const { crides } = simularFetch([
{ status: 400, cos: { missatge: 'Falta el titol', codi: 'TITOL_REQUERIT' } }
]);
await expect(new RepositoriApi('http://api.local').crearTasca({}))
.rejects.toMatchObject({ name: 'ErrorDeApi', status: 400, codi: 'TITOL_REQUERIT' });
expect(crides).toHaveLength(1); // ni un intent mes
});Les dues asseveracions sobre crides.length són el cor d'aquestes proves. Sense elles, totes dues passarien encara que el reintent estigués mal implementat.
6.2 Temporitzadors falsos
test('el retroces exponencial espera 300, 600 i 1200 ms', async () => {
jest.useFakeTimers();
simularFetch([{ status: 503 }, { status: 503 }, { status: 503 }, { status: 200, cos: [] }]);
const promesa = new RepositoriApi('http://api.local').llistarTasques();
await jest.advanceTimersByTimeAsync(300);
expect(global.fetch).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(600);
expect(global.fetch).toHaveBeenCalledTimes(3);
await jest.advanceTimersByTimeAsync(1200);
expect(global.fetch).toHaveBeenCalledTimes(4);
await expect(promesa).resolves.toEqual([]);
jest.useRealTimers();
});advanceTimersByTimeAsync i no advanceTimersByTime. La versió asíncrona buida també la cua de microtasques entre avenços, i sense ella les promeses intermèdies no es resolen i la prova es penja. És un d'aquells detalls que costen una tarda la primera vegada, i té tot el sentit a la llum de 05-07: les microtasques i els temporitzadors són cues diferents.
Recorda restaurar els temporitzadors reals. Un jest.useFakeTimers() que es queda actiu contamina les proves següents amb errades aparentment aleatòries. Posa-ho en un afterEach global.
- Provar les migracions i la cua offline
7.1 Migracions
Ja vas veure les sis proves base a 11-03. Afegeix-hi aquestes tres, que cobreixen el que surt malament a la pràctica:
test('un document amb un camp desconegut no es perd ni es trenca', () => {
const ambExtra = { ...v1, dades: { ...v1.dades, experiment: [1, 2, 3] } };
expect(() => migrar(ambExtra)).not.toThrow();
});
test('migrar 500 tasques triga menys de 100 ms', () => {
const gran = generarDocumentV1({ tasques: 500 });
const inici = performance.now();
migrar(gran);
expect(performance.now() - inici).toBeLessThan(100);
});
test('cada salt de versio es prova per separat', () => {
const pas2 = MIGRACIONS.find((m) => m.a === 2);
const resultat = pas2.migrar(structuredClone(v1));
expect(resultat.dades.tasques[0]).not.toHaveProperty('responsable');
expect(resultat.dades.tasques[0]).toHaveProperty('responsableId');
});La segona és una prova de rendiment a la suite, i aquí sí que està justificada: la migració passa en arrencar, bloquejant la primera pintada. Una migració de dos segons arruïna l'LCP del pressupost de 11-01, i aquesta errada només apareix amb dades d'usuari real. Posar-li un llindar a la prova ho prevé.
7.2 La cua offline
describe('CuaDeCanvis', () => {
beforeEach(() => { localStorage.clear(); jest.useFakeTimers(); });
afterEach(() => jest.useRealTimers());
test('encua quan no hi ha xarxa i aplica en local igualment', async () => { /* … */ });
test('conserva l ordre en buidar', async () => {
const { crides } = simularFetch([{ status: 201, cos: { id: 7 } }, { status: 200, cos: {} }]);
cua.encuar({ tipus: 'crear', recurs: 'tasca', carregaUtil: { titol: 'A' } });
cua.encuar({ tipus: 'actualitzar', recurs: 'tasca', carregaUtil: { id: 7, estat: 'en-curs' } });
await sincronitzador.buidar();
expect(crides[0].metode).toBe('POST');
expect(crides[1].metode).toBe('PUT');
});
test('reenviar fa servir la MATEIXA clau d idempotencia', async () => {
simularFetch([{ error: new TypeError('Failed to fetch') }, { status: 201, cos: { id: 7 } }]);
const id = cua.encuar({ tipus: 'crear', recurs: 'tasca', carregaUtil: { titol: 'A' } });
await sincronitzador.buidar(); // falla, continua a la cua
await sincronitzador.buidar(); // reintenta
const claus = global.fetch.mock.calls.map(([, o]) => o.headers['Idempotency-Key']);
expect(claus[0]).toBe(claus[1]);
expect(claus[0]).toBe(id);
});
test('despres de 5 intents fallits, l entrada es marca com a necessita atencio', async () => { /* … */ });
test('la cua sobreviu a recrear l objecte', () => {
cua.encuar({ tipus: 'crear', recurs: 'tasca', carregaUtil: {} });
expect(new CuaDeCanvis('orbita:cua').pendents).toBe(1);
});
});La tercera prova és la que evita el duplicat de l'apartat 13 de 11-03, i és impossible de comprovar a mà de manera fiable. És l'exemple perfecte d'una errada que només es caça amb una prova automatitzada.
- Provar la vista amb jsdom i Testing Library
El principi de 08-05, que convé recordar perquè ho canvia tot:
Prova com fa servir l'aplicació una persona, no com està construïda per dins.
A la pràctica, això significa consultar per rol i per text accessible, mai per classe CSS ni per estructura.
| ❌ Fràgil i poc informatiu | ✅ Robust i significatiu |
|---|---|
container.querySelector('.tasca__btn') |
getByRole('button', { name: /marcar en curs/i }) |
document.querySelectorAll('.fila') |
getAllByRole('listitem') |
input.value = 'x'; input.dispatchEvent(...) |
await user.type(input, 'x') |
expect(el.className).toContain('error') |
expect(input).toHaveAttribute('aria-invalid', 'true') |
El benefici doble: la prova sobreviu a un canvi de classes CSS, i a més verifica accessibilitat de propina. Si getByRole('button', { name: /crear/i }) no troba res, és perquè el teu botó no és un botó o no té nom accessible — i això és una errada real que un querySelector hauria amagat.
// test/vista/tauler-vista.test.js
import { screen, within } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
describe('TaulerVista', () => {
let contenidor, emesos, vista;
beforeEach(() => {
document.body.innerHTML = '<div id="arrel"></div><div id="anuncis" role="status" aria-live="polite"></div>';
contenidor = document.getElementById('arrel');
emesos = [];
vista = crearTaulerVista(contenidor, { enEmetre: (e) => emesos.push(e) });
});
afterEach(() => vista.destruir());
test('pinta una fila per tasca, com a llista semantica', () => {
vista.render(estatAmb(3));
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
test('la tasca vencuda s identifica amb text, no nomes amb color', () => {
vista.render(estatAmb([tascaVencuda({ titol: 'Pressupost' })]));
const fila = screen.getByRole('listitem');
expect(within(fila).getByText(/vencuda/i)).toBeInTheDocument();
});
test('prement el boto s emet la intencio amb l id correcte', async () => {
const user = userEvent.setup();
vista.render(estatAmb([tasca({ id: 42, titol: 'Revisar el torn' })]));
await user.click(screen.getByRole('button', { name: /marcar en curs/i }));
expect(emesos).toEqual([{ accio: 'canviar-estat', id: 42, valor: 'en-curs' }]);
});
test('el nombre de tasques visibles s anuncia', () => {
vista.render(estatAmb(12));
expect(screen.getByRole('status')).toHaveTextContent(/12 tasques/i);
});
test('sense resultats mostra estat buit amb sortida', () => {
vista.render(estatAmbFiltreSenseResultats());
expect(screen.getByText(/cap tasca no coincideix/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /treure filtres/i })).toBeInTheDocument();
});
test('destruir deixa el contenidor buit i sense escoltes', () => {
vista.render(estatAmb(3));
const espia = jest.spyOn(contenidor, 'removeEventListener');
vista.destruir();
expect(contenidor).toBeEmptyDOMElement();
expect(espia).toHaveBeenCalled();
});
});Fixa't que la meitat d'aquestes proves verifiquen requisits d'accessibilitat i d'experiència —llista semàntica, text a més del color, anunci del recompte, estat buit amb sortida— que a 11-01 vas escriure com a criteris d'acceptació. El cercle es tanca: història → criteri → prova.
- Provar el recorregut amb teclat
Aquesta és la part que gairebé cap projecte de portafoli no té, i és la que més impressiona en una revisió.
test('es pot crear una tasca sense tocar el ratoli', async () => {
const user = userEvent.setup();
muntarAplicacio();
await user.tab(); // primer element enfocable
expect(screen.getByRole('button', { name: /nova tasca/i })).toHaveFocus();
await user.keyboard('{Enter}'); // obrir el dialeg
const dialeg = screen.getByRole('dialog', { name: /nova tasca/i });
expect(within(dialeg).getByLabelText(/titol/i)).toHaveFocus(); // focus a dins en obrir
await user.keyboard('Revisar el torn');
await user.tab();
await user.keyboard('8'); // hores
await user.keyboard('{Enter}'); // enviar amb Enter
expect(screen.getByRole('listitem', { name: /revisar el torn/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /nova tasca/i })).toHaveFocus(); // el focus TORNA
});
test('Escape tanca el dialeg i torna el focus al disparador', async () => {
const user = userEvent.setup();
muntarAplicacio();
const disparador = screen.getByRole('button', { name: /nova tasca/i });
await user.click(disparador);
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(disparador).toHaveFocus();
});
test('el focus no s escapa del dialeg obert', async () => {
const user = userEvent.setup();
muntarAplicacio();
await user.click(screen.getByRole('button', { name: /nova tasca/i }));
for (let i = 0; i < 12; i++) await user.tab(); // fer la volta diverses vegades
const dialeg = screen.getByRole('dialog');
expect(dialeg.contains(document.activeElement)).toBe(true);
});Les tres cobreixen les tres errades de focus més comunes: no entrar-hi en obrir, no tornar en tancar i escapar-se'n mentre és obert. Si fas servir <dialog> amb showModal(), les tres passen gairebé sense esforç — que és exactament l'argument de 11-02 sobre fer servir l'element correcte.
Advertiment sobre jsdom: jsdom implementa <dialog> de manera incompleta en algunes versions, i no calcula estils, així que no pot verificar visibilitat ni contrast. Les proves de teclat a jsdom cobreixen la lògica del focus; la verificació real que es veu on ets és manual o d'extrem a extrem.
- Tres recorreguts d'extrem a extrem, i per què no més
Les proves d'extrem a extrem (08-06) són les més valuoses per prova i les més cares per prova. Aquesta doble condició dicta l'estratègia: poques i molt ben triades.
| Cost | Detall |
|---|---|
| Execució | Segons per recorregut, davant de mil·lisegons |
| Manteniment | Qualsevol canvi de flux les trenca |
| Intermitència | Depenen de temps, xarxa i animacions |
| Diagnòstic | Quan fallen, diuen que alguna cosa falla, no què |
El criteri de selecció, en una frase: tria els recorreguts que, si es trenquessin, el producte no serviria per a res. Per a Òrbita:
Recorregut 1 · El cicle de vida complet d'una tasca.
// cypress/e2e/01-cicle-de-vida.cy.js
describe('Cicle de vida d una tasca', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('crea, descompon, avanca i tanca una tasca', () => {
cy.findByRole('button', { name: /nova tasca/i }).click();
cy.findByLabelText(/titol/i).type('Revisar el torn de fusta');
cy.findByLabelText(/hores/i).type('8');
cy.findByRole('button', { name: /crear tasca/i }).click();
cy.findByRole('listitem', { name: /revisar el torn/i }).as('tasca');
cy.get('@tasca').findByRole('button', { name: /afegir subtasca/i }).click();
cy.findByLabelText(/titol/i).type('Comprar el paper de vidre');
cy.findByRole('button', { name: /crear subtasca/i }).click();
// R13: no es pot tancar la mare amb la filla oberta
cy.get('@tasca').findByRole('button', { name: /marcar feta/i }).click();
cy.findByRole('alert').should('contain.text', 'Comprar el paper de vidre');
// Tancar la filla primer, i llavors si
cy.findByRole('listitem', { name: /comprar el paper de vidre/i })
.findByRole('button', { name: /marcar feta/i }).click();
cy.get('@tasca').findByRole('button', { name: /marcar feta/i }).click();
cy.get('@tasca').should('contain.text', 'Feta');
// Persistencia real
cy.reload();
cy.findByRole('listitem', { name: /revisar el torn/i }).should('contain.text', 'Feta');
});
});Aquest recorregut cobreix d'una vegada: crear amb validació, l'arbre, R13 aplicada de debò des de la interfície, el missatge d'error, la transició completa i la persistència després de recarregar. Un recorregut, sis riscos.
Recorregut 2 · Filtrar, compartir el filtre i veure l'informe. Cobreix l'estat compartit, la URL com a font de veritat, el càlcul de càrrega i l'estat buit.
Recorregut 3 · Treballar sense connexió i recuperar. Cobreix la cua, la persistència i la reconciliació:
it('els canvis sense connexio es sincronitzen en recuperar-la', () => {
cy.visit('/');
cy.intercept('POST', '**/tasques', { forceNetworkError: true }).as('senseXarxa');
cy.crearTasca('Pressupost de la fusteria');
cy.findByText(/1 canvi sense sincronitzar/i).should('be.visible');
cy.intercept('POST', '**/tasques', { statusCode: 201, body: { id: 99 } }).as('ambXarxa');
cy.window().then((win) => win.dispatchEvent(new Event('online')));
cy.wait('@ambXarxa');
cy.findByText(/sense sincronitzar/i).should('not.exist');
cy.findAllByRole('listitem', { name: /pressupost de la fusteria/i }).should('have.length', 1);
});Aquest últim should('have.length', 1) és la prova del duplicat: si la idempotència falla, n'apareixen dos.
Per què exactament tres. Amb tres recorreguts cobreixes els tres riscos sistèmics —el flux principal, l'estat compartit i la resiliència— en uns 30 segons d'execució. Amb vint tindries cinc minuts de CI, errades intermitents setmanals i una càrrega de manteniment que acabaria amb algú desactivant-los. Una suite d'extrem a extrem desactivada val zero, així que el nombre correcte és el màxim que pots mantenir sempre en verd. Tres ho és. Nómada Tasques també en tenia tres, amb menys funcionalitats: la proporció s'aguanta.
- Integració contínua amb GitHub Actions
La integració contínua executa npm run verificar a cada push, en una màquina neta. Aquest «màquina neta» és la meitat del valor: caça els «a la meva màquina funciona» que es deuen a un fitxer no confirmat o a una dependència global.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
# Cancella execucions antigues de la mateixa branca: estalvia minuts i dona resultats rellevants
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
qualitat:
name: Lint, format i proves
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # cacheja ~/.npm: de ~60 s a ~10 s
- name: Instal·lar dependències
run: npm ci # ci, no install: respecta el lockfile exactament
- name: Revisar el codi
run: npm run lint
- name: Comprovar el format
run: npm run format:check
- name: Proves amb cobertura
run: npm run test:cov -- --ci --maxWorkers=2
- name: Publicar l'informe de cobertura
if: always() # tambe si les proves han fallat
uses: actions/upload-artifact@v4
with:
name: cobertura
path: coverage/
- name: Compilar
run: npm run build
- name: Desar la compilació
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
e2e:
name: Recorreguts d'extrem a extrem
runs-on: ubuntu-latest
needs: qualitat # no gastar minuts si el lint ja ha fallat
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- name: Cypress
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run preview
wait-on: 'http://localhost:4173'
wait-on-timeout: 60
- name: Desar les captures de les errades
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-errades
path: cypress/screenshots/
accessibilitat-i-rendiment:
name: Lighthouse i axe
runs-on: ubuntu-latest
needs: qualitat
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run build
- name: Lighthouse CI
run: npx @lhci/[email protected] autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}Les decisions del fitxer, una a una:
| Línia | Per què |
|---|---|
concurrency amb cancel-in-progress |
Si fas tres push seguits, només s'executa l'últim. Estalvia minuts i evita resultats obsolets |
cache: npm |
Redueix la instal·lació de ~60 s a ~10 s. És la millora més barata que existeix |
npm ci en comptes de npm install |
Instal·la exactament el del package-lock.json. Reproduïble; install pot actualitzar versions menors |
--ci --maxWorkers=2 |
Els executors tenen 2 nuclis; més treballadors competeixen i alenteixen. --ci desactiva l'actualització automàtica d'instantànies |
if: always() a la cobertura |
Quan les proves fallen és just quan vols veure l'informe |
needs: qualitat als altres treballs |
No gastar cinc minuts de Cypress si el lint ha fallat en deu segons |
if: failure() a les captures |
Cypress desa captura i vídeo de l'errada: és el primer que miraràs |
La protecció de la branca. La CI no serveix de res si es pot fusionar en vermell. A la configuració del repositori, marca main com a protegida i exigeix que els tres treballs passin abans de fusionar. És un clic que converteix una recomanació en una garantia — la mateixa idea que la regla d'ESLint de 11-01.
- Lighthouse CI i el pressupost de rendiment
A 11-01 vas fixar el pressupost. Aquí el fas complir automàticament.
{
"ci": {
"collect": {
"staticDistDir": "./dist",
"numberOfRuns": 3,
"settings": { "preset": "desktop" }
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"categories:best-practices":["warn", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"resource-summary:script:size": ["error", { "maxNumericValue": 61440 }],
"resource-summary:total:count": ["warn", { "maxNumericValue": 10 }],
"color-contrast": ["error", { "minScore": 1 }],
"label": ["error", { "minScore": 1 }],
"button-name": ["error", { "minScore": 1 }],
"unused-javascript": ["warn", { "maxLength": 1 }]
}
},
"upload": { "target": "temporary-public-storage" }
}
}Quatre observacions:
numberOfRuns: 3. Lighthouse té soroll; amb una sola execució tindries errades aleatòries. Tres i pren la mediana, exactament la disciplina de mesura de 09-01.errordavant dewarn. El que trenca la compilació és el del pressupost signat; la resta informa. Si tot és error, acabaràs desactivant-lo sencer el primer divendres complicat.resource-summary:script:sizea 61440 són els 60 kB del pressupost de 11-01, en bytes. Aquesta única línia és la que impedeix que un dia algú afegeixi una llibreria de gràfics i ningú no se n'assabenti.- Les tres auditories d'accessibilitat amb
minScore: 1—contrast, etiquetes, noms de botó— són les que més es degraden amb el temps. Exigir perfecció en aquestes tres és assumible i evita l'erosió.
Quan el pressupost falla, hi ha tres sortides legítimes i una d'il·legítima:
| Sortida | Quan |
|---|---|
| Optimitzar el canvi | Gairebé sempre. Aplica el Mòdul 9 |
| Renunciar a la funcionalitat | Si el seu valor no compensa el seu cost |
| Pujar el pressupost conscientment i anotar per què | Si el producte ha crescut de manera legítima |
| ~~Desactivar la comprovació~~ | Mai. És com s'arriba a aplicacions de 2 MB |
- Depurar el projecte propi: el mètode aplicat
La lliçó 08-01 va donar el mètode. Aquí s'aplica a errades teves, i el canvi important és que ningú no et dirà on és el problema.
El procediment, en sis passos:
flowchart TD
A["1 · Reproduir<br/>de manera fiable"] --> B["2 · Reduir al<br/>cas minim"]
B --> C["3 · Escriure una prova<br/>QUE FALLI"]
C --> D["4 · Formar una hipotesi<br/>concreta i falsable"]
D --> E["5 · Comprovar-la<br/>amb una sola mesura"]
E -->|Falsa| D
E -->|Certa| F["6 · Corregir · La prova<br/>es posa en verd"]
F --> G["La prova es queda:<br/>regressio coberta"]
style C fill:#fee2e2,stroke:#b91c1c
style G fill:#dcfce7,stroke:#16a34a
El pas 3 és el que gairebé tothom es salta, i és el més rendible dels sis. Escriure la prova abans de corregir té quatre beneficis simultanis:
- Demostra que has entès l'errada. Si no pots escriure la prova, no saps què està malament: només tens un símptoma.
- Et diu quan has acabat. Sense ella, «sembla que ja funciona» és tot el que tens.
- Evita la reaparició. Les errades corregides sense prova tornen, i tornen en el pitjor moment.
- Documenta el cas rar. D'aquí a un any, aquesta prova explicarà per què el codi té aquella línia aparentment innecessària.
El pas 4 mereix un matís. Una hipòtesi útil és falsable i concreta: «l'id que arriba al PUT és undefined perquè el servidor retorna _id». Una hipòtesi inútil és «alguna cosa passa amb els ids». La primera es comprova amb una mesura; la segona et porta a tocar coses a veure si s'arregla, que és la manera més cara de depurar que existeix.
Els tres apartats següents recorren tres errades que apareixeran al teu projecte. No són exemples inventats: són les conseqüències directes del que has construït a 11-02 i 11-03.
- Errada 1: la migració que corromp dades
El símptoma. Un usuari de prova (o tu, amb el teu perfil de desenvolupament antic) actualitza l'aplicació i veu el tauler amb les tasques, però totes apareixen sense responsable i l'informe de càrrega surt a zero. No hi ha cap error a consola.
Pas 1 · Reproduir. L'errada no passa amb dades noves, només amb dades antigues. La reproducció exigeix el document original — i aquí es cobra la còpia de seguretat de 11-03:
// A la consola: recuperar la copia previa a la migracio
const antic = localStorage.getItem('orbita:tauler:copia:v1');
console.log(JSON.parse(antic).dades.tasques.slice(0, 2));[
{ id: 1, titol: 'Redissenyar la sala', responsable: 'Iván', horesEstimades: 12 },
{ id: 2, titol: 'Cartelleria', responsable: 'Marta', horesEstimades: 6 }
]Les dades antigues sí que tenen responsable. Per tant, ho perd la migració.
Pas 2 · Reduir. Al cas mínim: dues tasques i un usuari.
Pas 3 · La prova que falla:
test('la migracio v1→v2 conserva l assignacio de responsable', () => {
const v1 = {
versio: 1,
dades: {
usuaris: [{ id: 'u-ivan', nom: 'Iván' }],
tasques: [{ id: 1, titol: 'Redissenyar la sala', responsable: 'Iván', horesEstimades: 12 }]
}
};
const resultat = migrar(v1);
expect(resultat.dades.tasques[0].responsableId).toBe('u-ivan'); // ← FALLA: rep null
});Pas 4 · Hipòtesi. El mapa perNom no troba 'Iván'. Possibles causes: (a) els usuaris no estan carregats quan corre la migració, (b) el nom no coincideix exactament, (c) l'ordre de les migracions és incorrecte.
Pas 5 · Comprovar amb una mesura:
migrar: (doc) => {
const perNom = new Map(doc.dades.usuaris.map((u) => [u.nom, u.id]));
console.log('claus del mapa:', [...perNom.keys()]);
console.log('cerco:', JSON.stringify(doc.dades.tasques[0].responsable));
// …
}Ja hi és. Els dos textos es veuen idèntics a la pantalla i no són iguals: un fa servir el caràcter precompost í (U+00ED) i l'altre una i seguida d'un accent combinant (U+0301). És un clàssic de les dades que han passat per sistemes operatius o navegadors diferents.
Pas 6 · Corregir. Normalitzant Unicode a tots dos costats:
const normalitzar = (s) => s?.normalize('NFC').trim();
const perNom = new Map(doc.dades.usuaris.map((u) => [normalitzar(u.nom), u.id]));
// …
responsableId: t.responsable ? (perNom.get(normalitzar(t.responsable)) ?? null) : nullI les tres lliçons generalitzables:
- Emparellar per text és fràgil. Emparella per identificador, sempre que sigui possible. Si no queda més remei, normalitza (
normalize('NFC'),trim(), i valoratoLocaleLowerCase()). - Una errada silenciosa és pitjor que una de sorollosa. La migració posava
nullsense avisar. Hauria de comptar quantes assignacions no ha pogut resoldre i registrar-ho:registrar('migracio v2: 6 de 8 responsables sense resoldre'). Un avís hauria revelat l'errada el primer dia. - La còpia de seguretat va salvar la investigació. Sense ella, les dades originals haurien desaparegut i només tindries el símptoma.
La prova de regressió que es queda, amb el cas Unicode explícit:
test.each([
['Iván', 'Iván'], // combinant vs precompost
['Lucía ', 'Lucía'], // espai sobrant
])('empareja %p amb %p tot i la diferencia de codificacio', (enUsuari, enTasca) => {
const doc = documentV1({ usuari: enUsuari, responsableEnTasca: enTasca });
expect(migrar(doc).dades.tasques[0].responsableId).not.toBeNull();
});
- Errada 2: la condició de cursa en sincronitzar
El símptoma. Amb la xarxa lenta, de vegades —no sempre— en marcar una tasca com a feta, torna a aparèixer com a pendent al cap d'un segon. No passa a la teva màquina amb xarxa ràpida. No hi ha error a consola.
Aquest és el pitjor tipus d'errada: intermitent i depenent de l'entorn. I la primera regla és no intentar depurar-la fins a poder reproduir-la a voluntat.
Pas 1 · Reproduir. Amb el panell Network en Slow 3G, surt una de cada tres vegades. Encara no n'hi ha prou. Per fer-ho determinista, cal controlar els temps:
// A la consola, retardar artificialment el PUT
const original = window.fetch;
window.fetch = async (url, opcions) => {
if (opcions?.method === 'PUT') await new Promise((r) => setTimeout(r, 3000));
return original(url, opcions);
};Amb tres segons de retard, passa sempre. Ja és reproduïble.
Pas 2 · Reduir. La seqüència mínima resulta ser:
t=0.0 s Marcar feta → optimista: feta → surt el PUT (trigara 3 s)
t=0.5 s Es refresca la llista → surt el GET (rapid)
t=0.8 s Arriba el GET → el servidor encara diu PENDENT → es pinta pendent
t=3.0 s Arriba el PUT → confirma fetaLa llista es refresca amb dades anteriors al meu canvi i trepitja l'actualització optimista.
Pas 3 · La prova que falla, amb temporitzadors falsos per fer-la determinista:
test('un refresc en vol no reverteix una actualitzacio optimista', async () => {
jest.useFakeTimers();
const repo = repositoriAmbLatencies({ PUT: 3000, GET: 300 });
const magatzem = crearMagatzem(estatAmb([tasca({ id: 1, estat: 'pendent' })]));
const marcat = marcarFeta(magatzem, repo, 1); // no esperem
await jest.advanceTimersByTimeAsync(500);
const refresc = refrescarTasques(magatzem, repo); // es llanca enmig
await jest.advanceTimersByTimeAsync(3000);
await Promise.all([marcat, refresc]);
expect(magatzem.obtenir().tasques[0].estat).toBe('feta'); // ← FALLA: 'pendent'
jest.useRealTimers();
});Pas 4 · Hipòtesi. El GET substitueix la llista sencera amb el que retorna el servidor, sense tenir en compte que hi ha canvis locals pendents de confirmar.
Pas 5 · Confirmat: refrescarTasques fa magatzem.actualitzar({ tasques: delServidor }).
Pas 6 · Corregir. Tres solucions possibles, i convé veure per què se'n tria una:
| Solució | Com | Valoració |
|---|---|---|
| No refrescar mentre hi hagi enviaments en vol | Bandera global | Fràgil; bloqueja refrescs legítims |
| Fusionar respectant el pendent | El refresc no trepitja les tasques amb canvi en vol | La correcta |
| Versionar i descartar el vell | Cada tasca amb versio; s'ignora l'anterior |
La més robusta, si l'API l'ofereix |
export function fusionarAmbPendents(delServidor, actuals, idsPendents) {
return delServidor.map((remota) =>
idsPendents.has(remota.id)
? actuals.find((t) => t.id === remota.id) // conservar el local optimista
: remota
);
}Les tres lliçons generalitzables:
- Una errada intermitent gairebé sempre és una cursa. Quan alguna cosa passa «de vegades», busca dues operacions asíncrones que puguin acabar en ordre diferent.
- Reproduir exigeix controlar el temps. Retardar peticions a propòsit converteix «una de cada tres vegades» en «sempre», i això és el que fa possible depurar.
- Qualsevol estat que se substitueix sencer és sospitós.
actualitzar({ tasques: noves })descarta informació que pot ser més recent. Substituir sempre ha de ser una decisió, no el camí per defecte.
- Errada 3: la fuita de memòria en navegar
El símptoma. Després de navegar entre tauler, informe i historial una estona, l'aplicació es torna lenta. Al principi no es nota; després de vint navegacions, cada canvi de pantalla triga visiblement més.
Pas 1 · Reproduir i mesurar, amb el protocol de 09-03:
- Obrir el panell Memory en mode incògnit.
- Instantània 1 (línia base).
- Navegar 20 vegades entre les tres pantalles.
- Forçar la recol·lecció d'escombraries (la icona de la paperera).
- Instantània 2.
- Comparar per Objects allocated between snapshots.
El resultat:
| Instantània | Heap | Detached nodes | TaulerVista |
|---|---|---|---|
| 1 (base) | 12,4 MB | 0 | 1 |
| 2 (després de 20 navegacions) | 38,9 MB | 4.812 | 21 |
Vint-i-una instàncies de TaulerVista vives. N'hi hauria d'haver una. Els 4.812 nodes separats són el DOM de les vint anteriors, retingut per alguna cosa.
Pas 2 · Trobar el retenidor. Al panell Memory, seleccionar una instància i mirar Retainers:
El magatzem està retenint vint funcions subscriptores, cadascuna amb la seva clausura sobre la seva vista i el seu DOM.
Pas 3 · La prova que falla:
test('destruir la vista la dona de baixa del magatzem', () => {
const magatzem = crearMagatzem(estatInicial());
const vista = crearTaulerVista(document.createElement('div'), { magatzem, enEmetre: () => {} });
expect(magatzem.nombreDeSubscriptors).toBe(1);
vista.destruir();
expect(magatzem.nombreDeSubscriptors).toBe(0); // ← FALLA: continua a 1
});
test('navegar deu vegades no acumula subscriptors', () => {
const magatzem = crearMagatzem(estatInicial());
for (let i = 0; i < 10; i++) {
const v = crearTaulerVista(document.createElement('div'), { magatzem, enEmetre: () => {} });
v.destruir();
}
expect(magatzem.nombreDeSubscriptors).toBe(0); // ← FALLA: 10
});Pas 4 i 5 · La causa. El codi era aquest:
// ❌ Se subscriu i llenca la funcio de baixa
magatzem.subscriure((estat) => actualitzar(estat));
function destruir() {
contenidor.replaceChildren(); // neteja el DOM… la subscripcio continua viva
}Pas 6 · Corregir, desant totes les baixes:
const baixes = [];
baixes.push(magatzem.subscriure((estat) => actualitzar(estat)));
baixes.push(connectarControlador(contenidor, enEmetre));
baixes.push(canal.subscriure(enMissatge));
const observador = new IntersectionObserver(enVeures);
baixes.push(() => observador.disconnect());
const temporitzador = setInterval(refrescar, 30_000);
baixes.push(() => clearInterval(temporitzador));
function destruir() {
baixes.forEach((baixa) => baixa());
baixes.length = 0;
contenidor.replaceChildren();
ultimEstat = null; // deixar anar la referencia a l estat
}Les quatre lliçons generalitzables:
- Tot el que es connecta s'ha de desconnectar. Escoltes, subscripcions, temporitzadors, observadors, sockets. El patró «desar la baixa en un array» converteix
destruir()en tres línies sempre iguals. - La fuita no es veu, es mesura. Ningú no detecta 26 MB mirant la pantalla. Sense el protocol de tres instantànies de 09-03, això es descobreix quan un usuari diu «va lenta després d'una estona».
- És comprovable en una prova unitària. No cal el panell Memory per prevenir la reaparició: n'hi ha prou amb exposar
nombreDeSubscriptorsi comptar. Aquesta prova triga un mil·lisegon i corre a cadapush. - Afegeix el comptador de navegacions a la teva llista de comprovació. Entrar i sortir tres vegades de cada pantalla, amb el comptador d'escoltes a la vista, hauria de ser part del tancament de cada increment.
- Proves d'accessibilitat: automàtiques i manuals
Hi ha una xifra que convé tenir present: les eines automàtiques detecten al voltant d'un terç dels problemes reals d'accessibilitat. Són imprescindibles i no són suficients.
17.1 La part automàtica: axe
// test/accessibilitat/pantalles.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe.each([
['tauler', muntarTauler],
['formulari', muntarFormulariObert],
['informe', muntarInforme],
['historial', muntarHistorial],
['calendari', muntarCalendari],
['estat buit', muntarSenseResultats],
['estat d error', muntarAmbError]
])('Accessibilitat · %s', (nom, muntar) => {
test('sense incidencies d axe', async () => {
const { container } = muntar();
expect(await axe(container)).toHaveNoViolations();
});
});Els dos últims casos —estat buit i estat d'error— són els que ningú no prova i on més incidències apareixen, perquè es maqueten de pressa i sense revisar.
I a Cypress, sobre l'aplicació real (que sí que calcula estils, i per tant sí que detecta contrast):
// cypress/e2e/accessibilitat.cy.js
it('el tauler no te incidencies greus', () => {
cy.visit('/');
cy.injectAxe();
cy.checkA11y(null, { includedImpacts: ['critical', 'serious'] });
});17.2 La part manual: els dos terços restants
Cap eina no detecta que l'ordre de tabulació sigui absurd, que un text alternatiu digui «imatge1», o que el flux sigui impossible de seguir sense veure la pantalla. Això es prova a mà, amb aquesta llista:
| # | Prova | Com | Què buscar | Temps |
|---|---|---|---|---|
| 1 | Només teclat | Desa el ratolí, recorre tota l'aplicació | Tot assolible, focus visible, ordre lògic, sense paranys | 10 min |
| 2 | Focus en diàlegs | Obrir i tancar-los tots | Hi entra en obrir, hi torna en tancar, Escape funciona | 3 min |
| 3 | Lector de pantalla | NVDA (Windows), VoiceOver (macOS), Orca (Linux) | Amb els ulls tancats: pots crear una tasca? | 20 min |
| 4 | Zoom al 200 % | Ctrl i roda | Res no es talla, res no desapareix, sense barra horitzontal | 3 min |
| 5 | Només text ampliat | Font del navegador al 200 % | El disseny aguanta sense solapar-se | 3 min |
| 6 | Sense color | Filtre d'escala de grisos del sistema | Distingeixes vençuda, sobrecàrrega i estat? | 2 min |
| 7 | Moviment reduït | prefers-reduced-motion activat |
Les animacions es desactiven | 2 min |
| 8 | Contrast | DevTools sobre cada text | ≥ 4,5:1 normal, ≥ 3:1 gran i interfície | 5 min |
Uns 50 minuts per revisió completa. Fes-la en tancar cada fita, no al final.
El punt 3 fa por la primera vegada i és el que més ensenya. Consells per començar: aprèn cinc dreceres (llegir-ho tot, element següent, encapçalament següent, llista d'enllaços, llista d'encapçalaments), tanca els ulls de debò, i intenta completar una tasca concreta. El que descobriràs gairebé segur: que els teus encapçalaments no formen una estructura navegable, que els teus botons d'icona no diuen res, i que quan alguna cosa canvia a la pantalla no te n'assabentes.
17.3 L'informe d'accessibilitat
Un lliurable de la fita, a docs/accessibilitat.md:
# Informe d'accessibilitat — Òrbita v1.0
Data: 2026-11-14 · Revisor: <el teu nom>
## Automàtic (axe 4.x)
| Pantalla | Crítiques | Greus | Moderades | Menors |
|---|---|---|---|---|
| Tauler | 0 | 0 | 1 | 2 |
| Formulari | 0 | 0 | 0 | 1 |
| Informe | 0 | 0 | 0 | 0 |
| Historial | 0 | 0 | 0 | 1 |
| Estat buit | 0 | 0 | 0 | 0 |
## Manual
| Prova | Resultat | Notes |
|---|---|---|
| Només teclat | ✅ | Recorregut complet verificat |
| Focus en diàlegs | ✅ | `<dialog>` natiu |
| Lector de pantalla (NVDA) | ⚠️ | La taula de l'informe es llegeix lenta amb moltes columnes |
| Zoom 200 % | ✅ | |
| Sense color | ✅ | Vençuda i sobrecàrrega amb text |
| Contrast | ✅ | Mínim mesurat: 4,8:1 |
## Limitacions conegudes
- El calendari no està optimitzat per a lector de pantalla amb més de 30 tasques/mes.
- No provat amb lectors de pantalla mòbils.
## Propers passos
- [ ] Afegir resum textual de l'informe abans de la taula
- [ ] Provar amb TalkBack a AndroidAquest document, amb les seves limitacions declarades, val més en una entrevista que una puntuació de 100 a Lighthouse. Demostra que has mirat de debò.
- Mesurar el rendiment del projecte propi
Aplica el protocol de 09-01 al teu projecte: mediana de 15 repeticions després de 5 d'escalfament, CPU 4×, xarxa Slow 4G, incògnit, mateixa màquina.
Genera dades realistes primer. Mesurar amb sis tasques no diu res:
// src/dades/llavor-gran.js — nomes en desenvolupament
export function generarCarregaRealista({ tasques = 500, subtasquesPer = 2, historial = 3000 } = {}) {
// Distribucio semblant a la real: 60 % pendents, 25 % en curs, 15 % fetes
// Dates repartides en 6 mesos; 20 % sense data; 8 % vencudes
// 4 usuaris, un d ells inactiu
}I la taula de resultats, amb la referència de Nómada Tasques al costat:
| # | Mesura | Pressupost (11-01) | Òrbita | Nómada Tasques | Veredicte |
|---|---|---|---|---|---|
| 1 | JS inicial comprimit | ≤ 60 kB | 54,1 kB | 58,3 kB | ✅ |
| 2 | Peticions primera pantalla | ≤ 4 | 4 | 3 | ✅ |
| 3 | LCP (mòbil simulat) | ≤ 2,5 s | 2,1 s | 1,9 s | ✅ |
| 4 | CLS | ≤ 0,1 | 0,03 | 0,02 | ✅ |
| 5 | INP en filtrar | ≤ 200 ms | 61 ms | 42 ms | ✅ |
| 6 | render() amb 500 tasques |
≤ 50 ms | 44 ms | 31 ms (600) | ✅ |
| 7 | Nodes DOM | ≤ 1.500 | 1.380 | 1.194 | ✅ |
| 8 | Memòria després de 3 cicles | ≈ 0 | +0,4 MB | sense fuites | ✅ |
| 9 | Rendiment Lighthouse | ≥ 90 | 94 | — | ✅ |
Com interpretar la comparació honestament, que és la part que importa:
- Òrbita és una mica més pesada que Nómada Tasques, i ho ha de ser. Té tres pantalles més, arbre de subtasques, historial i cua de sincronització. Estar dins del pressupost amb més funcionalitat és el resultat correcte.
- Estar per sota del pressupost no és motiu per relaxar-se: és marge per al que vingui. Anota el marge: 5,9 kB de JavaScript i 400 ms d'LCP.
- Si estàs fora, la taula et diu on mirar. Cada fila té la seva lliçó del Mòdul 9:
render()alt → 09-04; JS alt → 09-05; memòria → 09-03; INP → 09-02.
I una regla contra l'autoengany: mesura en mode incògnit, sense extensions i sense el servidor de desenvolupament. Vite en desenvolupament serveix mòduls sense empaquetar, i mesurar allà dona números que no tenen res a veure amb producció. Es mesura sobre npm run build i npm run preview.
- Les proves intermitents
Una prova intermitent (o flaky) passa unes vegades i falla d'altres sense que canviï el codi. Són el pitjor enemic d'una suite, per una raó concreta: destrueixen la confiança. Així que la gent comença a rellançar la CI «a veure si aquesta vegada passa», les proves deixen de ser un senyal i passen a ser soroll.
Les causes, amb la seva solució:
| Causa | Símptoma típic | Solució |
|---|---|---|
| Dependència del temps real | Falla els dilluns, o a final de mes | jest.setSystemTime(); injectar avui |
| Esperes fixes | cy.wait(500) que de vegades no n'hi ha prou |
Esperar per condició: cy.findByText(...) |
| Ordre entre proves | Passa sola, falla en grup | Netejar a beforeEach; no compartir estat |
| Asincronia sense esperar | Falla en màquines lentes | await de debò; findBy* en comptes de getBy* |
| Aleatorietat | Falla una de cada vint | Substituir Math.random i crypto.randomUUID |
| Animacions | L'element «no és visible» | prefers-reduced-motion a proves; desactivar transicions |
| Concurrència | Falla només a la CI | Executar en sèrie el que comparteix recurs |
| Recursos externs | Falla si la xarxa va malament | Simular; mai no cridar un servei real |
El protocol quan apareix una intermitent:
- No la rellancis mai i continuïs. Aquest és l'hàbit que arruïna les suites.
- Etiqueta-la i executa-la en bucle per mesurar-ne la freqüència:
npx jest --testNamePattern="nom" --runInBand --repeat=50 - Aïlla la causa amb la taula de dalt. Gairebé sempre és temps o estat compartit.
- Corregeix la causa, no el símptoma. Afegir
cy.wait(2000)no arregla res: fa la prova més lenta i continua fallant en una màquina més lenta. - Si no la pots arreglar avui, marca-la amb
test.skipi obre una incidència amb data. Una prova desactivada i anotada és honesta; una prova intermitent activa és corrosiva.
I la regla d'higiene: executa la suite completa cinc vegades seguides abans de donar per bona una fita. Si les cinc passen, tens una suite fiable. Si en falla una, tens una intermitent que ja coneixes — i és molt millor conèixer-la ara que un divendres durant un desplegament.
Errors Habituals i Consells
Provar la implementació en lloc del comportament. Una prova que comprova que s'ha cridat un mètode intern es trenca amb cada refactorització sense detectar cap errada real. Prova el que entra i el que surt, o el que veu l'usuari. La regla pràctica: si en reorganitzar el codi per dins, sense canviar-ne el comportament, es trenquen proves, aquestes proves estaven malament.
Perseguir el 100 % de cobertura. Els últims punts percentuals acostumen a ser branques defensives que no s'executen mai, i forçar-los produeix proves artificials que no aporten confiança. Puja el llistó on el risc és alt —domini i migracions— i accepta menys on el cost és alt i el risc baix.
Escriure vint proves d'extrem a extrem. Són lentes, fràgils i intermitents. Amb vint, la CI triga cinc minuts i algú acabarà desactivant-les. Tres de ben triades donen gairebé la mateixa confiança sistèmica a una fracció del cost.
querySelector per classe CSS a les proves de vista. Acobla la prova a la maquetació i no verifica accessibilitat. getByRole fa totes dues coses bé i falla quan el teu HTML no és semàntic, que és informació valuosa.
No netejar l'estat entre proves. localStorage, temporitzadors falsos, dobles de fetch i el DOM s'arrosseguen d'una prova a una altra i produeixen errades que depenen de l'ordre. beforeEach que ho neteja tot, sempre.
Fer servir advanceTimersByTime amb codi asíncron. No buida la cua de microtasques i la prova es penja o falla sense explicació. advanceTimersByTimeAsync i await.
Corregir una errada sense escriure abans la prova que falla. Perds els quatre avantatges: la demostració que ho vas entendre, el criteri de finalització, la protecció contra la reaparició i la documentació del cas rar. És l'hàbit que més separa qui depura amb mètode de qui toca coses a veure què passa.
Depurar una errada intermitent sense fer-la determinista primer. «De vegades passa» no es pot depurar. Controla el temps, les respostes i l'aleatorietat fins que passi sempre; llavors comença a investigar.
Rellançar la CI fins que passi. Cada vegada que ho fas, la teva suite perd valor. Tracta cada intermitent com una errada real, perquè acostuma a ser-ho: la cursa que produeix l'errada a la CI existeix també en producció.
Creure que axe n'hi ha prou. Detecta al voltant d'un terç dels problemes. Un teclat i vint minuts amb un lector de pantalla troben coses que cap eina no veu.
Consell · Executa les proves en mode vigilància mentre programes. npm run test:watch amb Jest executa només el que està afectat pel que acabes de desar. El cicle de retroalimentació baixa de minuts a menys d'un segon, i això canvia com treballes.
Consell · Escriu el nom de la prova com una frase que descrigui el comportament. 'rebutja tancar una tasca amb subtasques obertes', no 'test canviarEstat 3'. Quan falli a la CI d'aquí a tres mesos, el nom serà tota la informació que tindràs al principi.
Consell · Mantén una carpeta test/ajudes/ cuidada. Fàbriques d'entitats, muntatge de l'aplicació, fetch simulat, generadors de dades. Cada minut invertit allà es multiplica pel nombre de proves que escriuràs després.
Consell · Afegeix el nombre de proves i la cobertura al README. «251 proves · 3 recorreguts E2E · 94 % de cobertura al domini» és un senyal immediat de serietat per a qui miri el teu repositori, i a tu et recorda mantenir-ho.
Exercicis
Aquests exercicis són la fita H5 del teu projecte: la suite completa en verd a la CI i l'informe d'accessibilitat i rendiment.
Exercici 1 — L'estratègia i la suite completa.
- Escriu
docs/estrategia-proves.mdamb: les tres preguntes de l'apartat 1 respostes per al teu projecte, la taula de la piràmide amb els teus mòduls i els seus números, i els llindars de cobertura per capa amb la seva justificació. - Configura Jest amb projectes separats:
nodeper a domini ijsdomper a la resta, ambcoverageThresholdper ruta. - Completa la taula de cobertura de regles: les 15 regles × (cas que passa, cas que falla, cas límit). Mínim 45 proves de domini.
- Escriu proves parametritzades amb
describe.eachper a almenys: la validació de camps, la matriu de transicions completa i els casos de data (inclosa la setmana 53). - Prova la capa de dades amb
fetchsimulat i temporitzadors falsos: codis d'estat, reintents selectius, temps esgotat, cancel·lació i retrocés exponencial verificat amb temps. - Prova les migracions (mínim 9 proves, incloses idempotència, validesa per al domini i la de rendiment) i la cua (mínim 6, inclosa la clau d'idempotència compartida entre reintents).
- Prova la vista amb Testing Library consultant sempre per rol o text accessible, incloses almenys tres proves de recorregut amb teclat (entrada de focus, retorn de focus, sense escapada).
- Escriu exactament tres recorreguts de Cypress i justifica per escrit per què aquests tres.
Exercici 2 — La integració contínua completa.
- Escriu
.github/workflows/ci.ymlamb els tres treballs: qualitat, extrem a extrem i Lighthouse, ambconcurrency, memòria cau d'npm,needsi pujada d'artefactes. - Configura
lighthouserc.jsonamb el teu pressupost de 11-01 traduït a asseveracions, distinginterrordewarn. - Activa la protecció de la branca
mainexigint els tres treballs. - Provoca una errada a propòsit de cada tipus i comprova que la CI la detecta: un error de lint, una prova trencada, una regressió de cobertura per sota del llindar, i un augment de mida del paquet per sobre del pressupost (importa una llibreria gran i treu-la després).
- Documenta al README l'estat de la CI amb la seva insígnia.
Exercici 3 — Les tres errades, l'informe i la línia base.
- Reprodueix les tres errades dels apartats 14, 15 i 16 al teu propi projecte. Si alguna no es dona de manera natural, provoca-la: treu la normalització d'una migració, retarda artificialment un
PUT, o elimina una baixa de subscripció. - Per a cadascuna, documenta a
docs/depuracio.md: símptoma, procediment de reproducció, cas mínim, prova que falla, hipòtesi (incloses les descartades), causa arrel i correcció. - Deixa les tres proves de regressió a la suite.
- Executa la revisió d'accessibilitat completa: axe automatitzat sobre les 7 pantalles (inclosos estat buit i d'error) i les 8 proves manuals. Escriu
docs/accessibilitat.mdamb el format de l'apartat 17.3, incloses les limitacions conegudes. - Genera dades realistes (≥ 500 tasques, ≥ 3.000 entrades d'historial) i mesura la teva línia base amb el protocol de 09-01 sobre la compilació de producció. Escriu
docs/rendiment.mdamb la taula de 9 files comparant pressupost, el teu resultat i Nómada Tasques. - Executa la suite completa cinc vegades seguides. Si alguna falla, troba i arregla la intermitent abans de donar la fita per tancada.
Solucions
Criteris d'acceptació de l'exercici 1 — Suite
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | Les proves de domini corren sense jsdom | testEnvironment: 'node' i totes passen |
| 2 | Les 15 regles tenen els seus 3 casos | Taula completa, sense buits |
| 3 | Els límits numèrics exactes estan provats | 40 i 40.1 apareixen a la suite |
| 4 | La setmana 53 està provada | Prova explícita de l'1 de gener |
| 5 | Els reintents són selectius | Prova que compta crides per a 500 i per a 400 |
| 6 | Els temps de retrocés es verifiquen | Amb temporitzadors falsos |
| 7 | La idempotència es prova | Mateixa clau en dos intents |
| 8 | La vista es consulta per rol | grep -r "querySelector" test/vista/ no retorna res rellevant |
| 9 | Hi ha ≥ 3 proves de teclat | Entrada, retorn i atrapament de focus |
| 10 | Hi ha exactament 3 recorreguts E2E | Amb justificació escrita |
| 11 | Cobertura de domini ≥ 95 % | npm run test:cov |
| 12 | Cobertura de migracions = 100 % | Ídem |
Rúbrica de l'exercici 1 (27 punts)
| Dimensió | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| Estratègia documentada | No existeix | Llista de proves | Amb nivells | Amb les 3 preguntes i llindars justificats |
| Cobertura de regles | < 8 regles | 12 regles | Les 15 | Les 15 amb límits exactes |
| Parametrització | Tot repetit | Alguna taula | Taules on toca | Les taules es llegeixen com a especificació |
| Dobles | Cap | fetch simulat |
A més temporitzadors | A més Date, crypto i comptador de crides |
| Migracions | Sense provar | Camí feliç | Amb dades reals | Amb idempotència, validesa i rendiment |
| Proves de vista | Per CSS | Per text | Per rol | Per rol i verificant accessibilitat |
| Teclat | No | 1 prova | 3 proves | Recorregut complet sense ratolí |
| E2E | 0 o > 5 | 3 sense justificar | 3 justificats | 3 que cobreixen els 3 riscos sistèmics |
| Fiabilitat | Intermitents conegudes | Alguna anotada | Suite estable | 5 execucions seguides en verd |
Llindar: 19/27, amb obligatòriament ≥ 2 a «Cobertura de regles» i a «Fiabilitat».
Criteris d'acceptació de l'exercici 2 — CI
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | La CI es dispara a cada PR i a main |
Veure execucions a la pestanya Actions |
| 2 | Un error de lint la posa en vermell | Provocar-lo |
| 3 | Una prova trencada la posa en vermell | Provocar-lo |
| 4 | Baixar la cobertura la posa en vermell | Esborrar una prova del domini |
| 5 | Superar el pressupost de bytes la posa en vermell | Importar una llibreria gran |
| 6 | Els artefactes es pugen també en fallar | if: always() verificat |
| 7 | Els treballs lents depenen del ràpid | needs: present |
| 8 | La memòria cau funciona | Segona execució clarament més ràpida |
| 9 | main està protegida |
No es pot fusionar en vermell |
| 10 | El temps total és raonable | < 5 minuts |
Criteris d'acceptació de l'exercici 3 — Depuració i informes
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | Les tres errades estan documentades | docs/depuracio.md amb les set seccions cadascuna |
| 2 | Cadascuna té la seva prova de regressió | Són a la suite i fallen si es reverteix la correcció |
| 3 | Es documenten les hipòtesis descartades | No només l'encertada: el procés importa |
| 4 | axe sobre 7 pantalles, 0 crítiques i 0 greus | Informe amb la taula |
| 5 | Les 8 proves manuals estan fetes | Amb resultat i notes, no només marcades |
| 6 | El lector de pantalla es va fer servir de debò | La secció de notes ho demostra |
| 7 | Hi ha limitacions declarades | Un informe sense limitacions és sospitós |
| 8 | La línia base es va mesurar sobre producció | npm run build + preview, no dev |
| 9 | Les dades eren realistes | ≥ 500 tasques |
| 10 | La comparació amb Nómada Tasques està interpretada | No només la taula: què significa cada diferència |
| 11 | Totes les files dins del pressupost, o justificades | Amb pla de correcció si alguna falla |
| 12 | Cinc execucions seguides en verd | Sense intermitents |
Rúbrica global de la fita H5 (24 punts)
| Dimensió | Pes | Què s'avalua |
|---|---|---|
| Estratègia i nivells | 4 | Documentada, justificada, aplicada amb coherència |
| Cobertura de regles | 5 | Les 15 amb els seus tres casos i els seus límits exactes |
| Capa de dades | 4 | Dobles, temporitzadors, migracions, cua, idempotència |
| Vista i teclat | 4 | Per rol, recorregut complet, destruir verificat |
| CI | 3 | Els tres treballs, pressupostos actius, branca protegida |
| Depuració | 2 | Les tres errades amb mètode i regressió |
| Accessibilitat i rendiment | 2 | Els dos informes amb limitacions declarades |
Llindar: 17/24. I una condició addicional que no es compensa: la suite ha d'estar en verd cinc vegades seguides. Una suite intermitent no està acabada, per moltes proves que tingui.
Autoavaluació de la fita H5:
| Pregunta | Sí / No |
|---|---|
| Sabria dir, per a cada regla, en quin fitxer és la seva prova? | |
| Les meves proves de vista continuen passant si canvio totes les classes CSS? | |
| He fet servir un lector de pantalla amb la meva pròpia aplicació? | |
| La meva CI es posa en vermell si el paquet creix 30 kB? | |
| He escrit la prova abans de corregir les tres errades? | |
| He executat la suite cinc vegades seguides sense ni una errada? | |
| La meva línia base està mesurada sobre la compilació de producció? |
Conclusió
Has convertit «funciona a la meva màquina» en «funciona, i hi ha una màquina que ho comprova a cada push».
Tens una estratègia de proves construïda sobre tres preguntes —què no pot fallar mai, què canvia sovint, i quant costa i quant val cada prova— i sobre una regla de decisió que resol gairebé tots els casos: prova en el nivell més baix que capti l'errada que et preocupa. Tens la piràmide traduïda als teus fitxers concrets, amb unes 250 proves de les quals només tres són d'extrem a extrem, i amb una observació que és un diagnòstic gratuït: si tinguessis més proves de vista que de domini, la teva lògica estaria al lloc equivocat.
Saps fer servir la cobertura sense deixar-te enganyar per ella: diu què no està provat, no si el que és provat està ben provat. Per això els teus llindars són per capa i justificats —95 % a domini, 100 % a migracions, 65 % a vista— i per això la mètrica que de debò governa no és el percentatge sinó la taula de regles × tres casos: quinze regles, quaranta-cinc proves mínimes, i cap buit que es pugui aprovar sense verificar res.
Saps provar el domini amb taules parametritzades que es llegeixen com a especificació, amb els límits exactes on viuen les errades —40 i 40.1, no 20—, amb els casos de tipus que se sobrepassen per les validacions ingènues —'12' i NaN—, comprovant error.camp i no només el tipus d'error, i amb les tres famílies de casos límit que tothom oblida: numèrics, de col·lecció i temporals, inclosa la setmana 53 que trenca gairebé totes les implementacions casolanes de setmana ISO.
Saps provar la capa de dades substituint només el que és lent, extern i no determinista: fetch simulat que falla davant de crides inesperades, temporitzadors falsos amb advanceTimersByTimeAsync —i no la versió síncrona, que deixa les microtasques sense buidar—, rellotge fixat, i asseveracions sobre el nombre de crides, que és el que distingeix una prova de reintents real d'una que passaria igual amb el reintent trencat. Amb les migracions provades contra documents reals, inclosa la prova de rendiment que evita que una migració lenta arruïni l'LCP, i amb la cua provada en el que és impossible verificar a mà: que el reenviament fa servir la mateixa clau d'idempotència.
Saps provar la vista com la fa servir una persona: per rol i per text accessible, mai per classe CSS, amb el doble benefici de sobreviure als canvis de maquetació i de verificar accessibilitat de propina. I saps provar el recorregut amb teclat, que gairebé cap projecte no té, cobrint les tres errades de focus clàssiques —no entrar-hi, no tornar, escapar-se'n—, que amb <dialog> natiu passen gairebé soles.
Tens tres recorreguts d'extrem a extrem triats amb un criteri explícit —els que, si es trenquessin, deixarien el producte inservible—, cobrint el cicle de vida complet amb R13 aplicada de debò, l'estat compartit a la URL amb l'informe, i la resiliència sense connexió amb la comprovació del duplicat. I saps per què no vint: una suite d'extrem a extrem desactivada val zero, així que el nombre correcte és el màxim que pots mantenir sempre en verd.
Tens la integració contínua completa i comentada línia a línia: concurrency que cancel·la l'obsolet, memòria cau d'npm, npm ci en comptes d'install, artefactes que es pugen també quan falla —que és justament quan els necessites—, treballs lents que depenen del ràpid, i la protecció de branca que converteix la recomanació en garantia. Amb Lighthouse CI fent complir el teu pressupost de 11-01 traduït a asseveracions, tres execucions pel soroll, error només on vas signar, i la línia de 61.440 bytes que impedeix que un dia entri una llibreria de gràfics sense que ningú no se n'assabenti.
I saps depurar el teu propi projecte amb un mètode de sis passos el tercer pas del qual —escriure la prova que falla abans de corregir— és el que gairebé tothom es salta i el que dona quatre beneficis alhora. Ho has aplicat a tres errades reals: la migració que corrompia dades per emparellar noms amb normalització Unicode diferent, amb les seves tres lliçons —emparellar per identificador, no fallar en silenci, i que la còpia de seguretat va salvar la investigació—; la condició de cursa en què un refresc en vol trepitjava una actualització optimista, on reproduir va exigir controlar el temps per convertir «una de cada tres vegades» en «sempre»; i la fuita de memòria de vint-i-una vistes vives retingudes per subscripcions sense baixa, que no es veu però es mesura, i que es prevé per sempre amb una prova unitària d'un mil·lisegon.
Saps que les eines automàtiques d'accessibilitat detecten al voltant d'un terç dels problemes, i tens les dues meitats: axe sobre les set pantalles —inclosos l'estat buit i el d'error, que ningú no prova i on més incidències hi ha— i les vuit proves manuals de cinquanta minuts, amb el lector de pantalla i els ulls tancats com la que més ensenya. Amb un informe que declara les seves limitacions, que val més que un 100 de Lighthouse.
Tens la teva línia base mesurada sobre la compilació de producció, amb dades realistes, comparada amb Nómada Tasques fila a fila, i la saps interpretar: ser una mica més pesat amb més funcionalitat és correcte, estar per sota del pressupost és marge i no permís, i cada fila fora de rang apunta a la seva lliçó del Mòdul 9.
I saps tractar les proves intermitents com el que són: errades reals que destrueixen la confiança en la suite. Amb les vuit causes i les seves solucions, el protocol de no rellançar mai, i la regla d'higiene de cinc execucions seguides abans de tancar una fita.
La fita H5 està tancada. El teu projecte funciona, està provat, es mesura i es vigila sol. Li falta l'única cosa que converteix un repositori en un producte: que altres persones el puguin fer servir. Compilar per a producció sense filtrar ni un secret, triar on allotjar-lo, configurar memòria cau, seguretat i HTTPS, desplegar de manera contínua amb possibilitat de revertir, i vigilar-lo quan ja no és a la teva màquina, és Desplegament del Projecte.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
