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

  1. L'estratègia de proves: decidir què es prova i on
  2. La piràmide aplicada als teus mòduls concrets
  3. Un objectiu de cobertura raonat, no cosmètic
  4. Provar el domini: les quinze regles parametritzades
  5. Provar les transicions i els casos límit
  6. Provar la capa de dades amb dobles i temporitzadors falsos
  7. Provar les migracions i la cua offline
  8. Provar la vista amb jsdom i Testing Library
  9. Provar el recorregut amb teclat
  10. Tres recorreguts d'extrem a extrem, i per què no més
  11. Integració contínua amb GitHub Actions
  12. Lighthouse CI i el pressupost de rendiment
  13. Depurar el projecte propi: el mètode aplicat
  14. Errada 1: la migració que corromp dades
  15. Errada 2: la condició de cursa en sincronitzar
  16. Errada 3: la fuita de memòria en navegar
  17. Proves d'accessibilitat: automàtiques i manuals
  18. Mesurar el rendiment del projecte propi
  19. Les proves intermitents
  20. Errors Habituals i Consells
  21. Exercicis
  22. Conclusió

  1. 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ó.

  1. 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.js no es prova directament. Són constants. Una prova que comprova que MAX_HORES === 40 no verifica res: reescriu el valor en un altre lloc. El que es prova és que la regla s'aplica, i això passa a tasca.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.

  1. 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.

  1. 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
});

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.
  • error davant de warn. 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:size a 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

  1. 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:

  1. Demostra que has entès l'errada. Si no pots escriure la prova, no saps què està malament: només tens un símptoma.
  2. Et diu quan has acabat. Sense ella, «sembla que ja funciona» és tot el que tens.
  3. Evita la reaparició. Les errades corregides sense prova tornen, i tornen en el pitjor moment.
  4. 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.

  1. 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 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));
  // …
}
claus del mapa: [ 'Iván' ]
cerco: "Iván"

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) : null

I les tres lliçons generalitzables:

  1. Emparellar per text és fràgil. Emparella per identificador, sempre que sigui possible. Si no queda més remei, normalitza (normalize('NFC'), trim(), i valora toLocaleLowerCase()).
  2. Una errada silenciosa és pitjor que una de sorollosa. La migració posava null sense 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.
  3. 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();
});

  1. 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 feta

La 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:

  1. 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.
  2. 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.
  3. 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.

  1. 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:

  1. Obrir el panell Memory en mode incògnit.
  2. Instantània 1 (línia base).
  3. Navegar 20 vegades entre les tres pantalles.
  4. Forçar la recol·lecció d'escombraries (la icona de la paperera).
  5. Instantània 2.
  6. 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:

TaulerVista
  └── context de subscriptor
        └── Set (subscriptors)
              └── Magatzem

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:

  1. 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.
  2. 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».
  3. És comprovable en una prova unitària. No cal el panell Memory per prevenir la reaparició: n'hi ha prou amb exposar nombreDeSubscriptors i comptar. Aquesta prova triga un mil·lisegon i corre a cada push.
  4. 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.

  1. 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

npm install --save-dev jest-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 Android

Aquest 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ò.

  1. 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.

  1. 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:

  1. No la rellancis mai i continuïs. Aquest és l'hàbit que arruïna les suites.
  2. Etiqueta-la i executa-la en bucle per mesurar-ne la freqüència:
    npx jest --testNamePattern="nom" --runInBand --repeat=50
    
  3. Aïlla la causa amb la taula de dalt. Gairebé sempre és temps o estat compartit.
  4. 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.
  5. Si no la pots arreglar avui, marca-la amb test.skip i 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.

  1. Escriu docs/estrategia-proves.md amb: 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ó.
  2. Configura Jest amb projectes separats: node per a domini i jsdom per a la resta, amb coverageThreshold per ruta.
  3. 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.
  4. Escriu proves parametritzades amb describe.each per a almenys: la validació de camps, la matriu de transicions completa i els casos de data (inclosa la setmana 53).
  5. Prova la capa de dades amb fetch simulat i temporitzadors falsos: codis d'estat, reintents selectius, temps esgotat, cancel·lació i retrocés exponencial verificat amb temps.
  6. 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).
  7. 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).
  8. Escriu exactament tres recorreguts de Cypress i justifica per escrit per què aquests tres.

Exercici 2 — La integració contínua completa.

  1. Escriu .github/workflows/ci.yml amb els tres treballs: qualitat, extrem a extrem i Lighthouse, amb concurrency, memòria cau d'npm, needs i pujada d'artefactes.
  2. Configura lighthouserc.json amb el teu pressupost de 11-01 traduït a asseveracions, distingint error de warn.
  3. Activa la protecció de la branca main exigint els tres treballs.
  4. 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).
  5. 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.

  1. 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ó.
  2. 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ó.
  3. Deixa les tres proves de regressió a la suite.
  4. 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.md amb el format de l'apartat 17.3, incloses les limitacions conegudes.
  5. 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.md amb la taula de 9 files comparant pressupost, el teu resultat i Nómada Tasques.
  6. 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats