Les 124 proves de la lliçó anterior s'executen en jsdom, que és una imitació del navegador: no pinta res, totes les mesures valen zero, el service worker de 07-05 no existeix i l'index.html, l'estils.css i el manifest.json reals no s'han carregat ni una vegada. Un botó tapat per un altre element, un import sense extensió que el navegador rebutja, un CSS que amaga la columna de tasques fetes o un worker que serveix una versió antiga de l'aplicació: res d'això no apareix a la suite, i tot això deixa la Marta mirant una pantalla trencada. Aquesta lliçó tanca el mòdul obrint l'aplicació de debò, en un navegador de debò, i fent-la servir com la faria servir ella. Aprendràs l'arquitectura de Cypress i per què és diferent de la de qualsevol altre executor, l'anatomia d'una prova amb selectors estables, el model d'encadenament i espera automàtica que fa de cy.wait(3000) un antipatró, la intercepció de xarxa amb cy.intercept, la sembra d'estat amb cy.session, les ordres personalitzades i les fixtures, l'execució en integració contínua, una comparativa honesta amb Playwright, i —el més important— què no s'ha de provar en E2E.

Contingut

  1. Què valida una prova d'extrem a extrem
  2. El cost: lentes, fràgils i cares de mantenir
  3. Cypress: instal·lació i estructura del projecte
  4. L'arquitectura: dins del navegador
  5. L'executor interactiu davant del mode run
  6. Anatomia d'una prova
  7. Selectors estables: data-cy i per què no la classe CSS
  8. Encadenament i espera automàtica
  9. Per què cy.wait(3000) és un antipatró
  10. Interceptar la xarxa amb cy.intercept
  11. Sembrar l'estat inicial
  12. Ordres personalitzades i fixtures
  13. Recorregut 1: crear una tasca i veure-la al tauler
  14. Recorregut 2: completar-la i comprovar el resum
  15. Recorregut 3: filtrar per responsable amb la URL compartible
  16. Depurar una fallada: captures, vídeos i viatge en el temps
  17. Executar en integració contínua
  18. Accessibilitat automatitzada amb axe
  19. Cypress davant de Playwright
  20. Què NO provar en E2E
  21. Errors Habituals i Consells
  22. Exercicis
  23. Conclusió

  1. Què valida una prova d'extrem a extrem

Una prova E2E arrenca l'aplicació real en un navegador real i la fa servir des de fora, exactament com una persona. No li importa res del teu codi: l'única cosa que toca és la interfície.

I per això valida coses que cap de les 124 proves anteriors no pot tocar:

Només ho veu una prova E2E Per què
Que index.html carrega i enllaça bé els mòduls jsdom no carrega l'HTML real ni resol els import del navegador
Que el CSS no tapa ni amaga res jsdom no pinta: totes les mesures són zero
Que el botó és realment premible Un element tapat per un altre rep el clic en jsdom i no al navegador
Que els mòduls ES carreguen sense errors Un import './tasca' sense extensió falla només al navegador (08-02)
Que el service worker serveix el que toca jsdom no l'implementa
Que l'aplicació sencera arrenca Ningú ha executat mai js/app.js complet
Que el recorregut de negoci funciona de cap a cap És l'única prova que respon «això serveix?»

L'última fila és la clau. Totes les proves anteriors responen preguntes tècniques: aquesta funció calcula bé?, aquestes dues peces s'entenen? Una prova E2E respon l'única pregunta que li importa a la Marta: puc crear una tasca, marcar-la com a feta i veure el resum correcte?

flowchart LR
    subgraph J["Proves en jsdom (08-05)"]
        A["El teu codi JS"] --> B["DOM simulat"]
    end
    subgraph C["Prova E2E"]
        D["Navegador real"] --> E["index.html real"]
        E --> F["css/estils.css"]
        E --> G["js/app.js + 14 mòduls"]
        G --> H["Service worker"]
        G --> I["Xarxa"]
    end
    style C fill:#dbeafe,stroke:#1d4ed8

  1. El cost: lentes, fràgils i cares de mantenir

Si són tan convincents, per què no fer-ho tot així? Perquè el preu és alt, i convé veure'l amb números:

Dimensió Unitària Integració E2E
Temps per prova ~2 ms ~30 ms 2-10 s
Suite completa 0,9 s 3,7 s 3-15 min
Precisió en fallar La línia exacta El mòdul «alguna cosa del recorregut»
Fragilitat Molt baixa Mitjana Alta
Cost de manteniment Baix Mitjà Alt
Confiança que aporta Baixa Mitjana Màxima

Els tres problemes específics:

Són lentes. Cada prova arrenca un navegador, carrega l'aplicació, espera la xarxa i fa clics reals. Cent proves E2E són quinze minuts, i una suite de quinze minuts deixa d'executar-se a cada commit.

Són fràgils. Canviar el text d'un botó, moure un element o afegir una animació les pot trencar. I pateixen la flakiness de 08-05 multiplicada: xarxa real, temporitzadors reals, animacions, curses.

Quan fallen, no diuen on. «El recorregut de crear tasca falla» pot ser el formulari, el model, l'API, el render o el CSS. Toca investigar amb el mètode de 08-01.

D'aquí surt la conclusió que governa tota la lliçó: poques proves E2E, i molt ben triades. Tres o quatre recorreguts que representin el valor real del producte. Si et trobes escrivint la vintena, gairebé segur que estàs cobrint en E2E alguna cosa que pertany a un nivell inferior.

  1. Cypress: instal·lació i estructura del projecte

npm install --save-dev cypress
npx cypress open        # la primera vegada, crea l'estructura
nomada-tasques/
  cypress.config.js
  cypress/
    e2e/                        ← les proves
      crear-tasca.cy.js
      completar-tasca.cy.js
      filtrar-per-responsable.cy.js
    fixtures/                   ← dades de prova
      backlog.json
    support/
      commands.js               ← ordres personalitzades
      e2e.js                    ← s'executa abans de cada fitxer
    screenshots/                ← captures automàtiques de les fallades (no versionar)
    videos/                     ← gravacions (no versionar)
// cypress.config.js
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    // Base de totes les cy.visit(): permet escriure cy.visit('/')
    baseUrl: 'http://localhost:5173',

    // Mida de la finestra: fixar-la fa les proves reproduïbles
    viewportWidth: 1280,
    viewportHeight: 800,

    // Temps límit. 4 s per defecte sol bastar; puja'ls només amb motiu
    defaultCommandTimeout: 5000,
    requestTimeout: 8000,

    video: true,
    screenshotOnRunFailure: true,

    // Cypress reintenta un fitxer fallit a CI: mitiga la intermitència
    // sense ocultar-la del tot, perquè l'informe marca la prova com a inestable
    retries: { runMode: 2, openMode: 0 }
  }
});

I al .gitignore:

cypress/screenshots/
cypress/videos/

  1. L'arquitectura: dins del navegador

Aquí hi ha la diferència més important entre Cypress i la resta d'eines, i explica gairebé totes les seves virtuts i els seus límits.

La majoria dels executors E2E (Selenium, Playwright, Puppeteer) funcionen fora del navegador: envien ordres per un protocol d'automatització i esperen la resposta. Cypress, en canvi, s'executa dins del mateix bucle d'esdeveniments que la teva aplicació: carrega la teva pàgina en un iframe i corre al seu costat.

flowchart TD
    subgraph N["Navegador"]
        subgraph P["Procés de la pestanya"]
            C["Cypress<br/>la teva prova"]
            A["Nómada Tasques<br/>en un iframe"]
            C <-->|accés directe<br/>mateix bucle d'esdeveniments| A
        end
    end
    S["Procés de Node<br/>servidor, fitxers, tasques"] <-->|websocket| C
    style C fill:#bbf7d0,stroke:#15803d

Conseqüències a favor:

  • Espera automàtica de debò. Com que Cypress veu el DOM directament, sap quan un element apareix sense sondejar des de fora.
  • Accés al codi de l'aplicació. Pots espiar window, cridar funcions, interceptar fetch des de dins.
  • Depuració excepcional. Els missatges de la consola, els errors i l'estat del DOM a cada pas són a l'abast.
  • Instantànies del DOM a cada ordre: el «viatge en el temps» de l'apartat 16.

Conseqüències en contra, i convé conèixer-les abans de triar:

  • Un sol navegador per prova. No pots provar dues pestanyes ni dues sessions alhora.
  • Restriccions d'origen. Navegar entre dominis diferents dins d'una mateixa prova requereix cy.origin().
  • No hi ha pestanyes noves. Un enllaç amb target="_blank" cal gestionar-lo d'una altra manera.
  • La prova s'executa al navegador, així que el codi de Node (llegir un fitxer, llançar un procés) necessita cy.task().

Per a Nómada Tasques —una aplicació d'una sola pàgina, un sol origen— cap d'aquestes limitacions no fa nosa.

  1. L'executor interactiu davant del mode run

Cypress té dos modes, i es fan servir en moments diferents:

npx cypress open      # interactiu: navegador visible, recarrega en desar
npx cypress run       # sense interfície: per a CI, amb vídeo i captures
open (interactiu) run (consola)
Navegador Visible, es pot inspeccionar Sense interfície (headless)
En desar el fitxer Recarrega i reexecuta
Viatge en el temps No (però grava vídeo)
Velocitat Menor Major
Ús Escriure i depurar Integració contínua

Els scripts del projecte:

{
  "scripts": {
    "dev": "vite",
    "e2e:obrir": "cypress open",
    "e2e": "cypress run",
    "e2e:ci": "start-server-and-test dev http://localhost:5173 e2e"
  }
}

start-server-and-test arrenca el servidor de desenvolupament, espera que respongui a la URL indicada, llança les proves i en acabar mata el servidor. Sense ell, la integració contínua hauria d'arrencar el servidor en segon pla i endevinar quan està a punt, que és precisament el tipus d'espera per temps que causa intermitència.

  1. Anatomia d'una prova

// cypress/e2e/primera-ullada.cy.js
describe('Tauler de Nómada Tasques', () => {
  beforeEach(() => {
    cy.visit('/');                                    // carrega l'aplicació real
  });

  it('mostra les sis tasques del backlog en arrencar', () => {
    cy.get('[data-cy="targeta"]').should('have.length', 6);
    cy.contains('Redissenyar la sala polivalent').should('be.visible');
    cy.get('[data-cy="resum"]').should('contain', '45');
  });
});

Les ordres essencials:

Ordre Què fa
cy.visit(url) Carrega una pàgina
cy.get(selector) Selecciona elements, esperant que existeixin
cy.contains(text) Selecciona per text visible
cy.find(selector) Cerca dins de l'element actual
.click() Prem (comprova abans que sigui premible de debò)
.type(text) Escriu, tecla a tecla
.select(valor) Tria en un <select>
.clear() Buida un camp
.should(asserció) Comprova, reintentant fins al temps límit
.and(asserció) Encadena una altra asserció sobre el mateix
cy.url() La URL actual, per comprovar-la
cy.intercept(...) Intercepta peticions de xarxa

I describe, it, beforeEach són els mateixos de Jest: Cypress fa servir Mocha per sota, així que l'estructura et resulta familiar.

  1. Selectors estables: data-cy i per què no la classe CSS

A 08-05 la resposta era consultar per rol accessible. En E2E, la recomanació de Cypress és diferent i mereix entendre's: atributs dedicats a les proves.

<!-- A index.html i a les plantilles -->
<li class="tasca" data-cy="targeta" data-id="3" data-estat="pendent">
  <h3 class="tasca__titol" data-cy="titol">Actualitzar el web de reserves</h3>
  <button data-accio="avancar" data-cy="avancar">Començar</button>
</li>

<form id="form-tasca" data-cy="form-tasca">
  <input id="titol" name="titol" data-cy="camp-titol">
  <button type="submit" data-cy="crear">Crear tasca</button>
</form>
// ❌ Fràgil: es trenca en retocar el CSS o l'estructura
cy.get('.columna:nth-child(1) > ul > li:first-child .tasca__titol');
cy.get('#tauler > section > ul > li').first();

// ✅ Estable: l'atribut existeix NOMÉS per a les proves i ningú el toca en maquetar
cy.get('[data-cy="targeta"]').first().find('[data-cy="titol"]');
Selector Estabilitat Problema
.tasca__titol ❌ Baixa Canvia en refactoritzar el CSS
#form-tasca ⚠️ Mitjana Els id canvien i de vegades es reutilitzen
nth-child, > ❌ Molt baixa Qualsevol element nou el trenca
Text visible ⚠️ Mitjana Canvia en corregir la redacció o traduir
[data-cy] Alta Cap: existeix només per a això

I el rol accessible, que a 08-05 era la resposta? Continua sent excel·lent, i mereix fer-se servir per al que és l'experiència de l'usuari: cy.contains('button', 'Crear tasca') documenta que aquell botó diu això. La combinació que millor funciona és fer servir data-cy per localitzar i assercions sobre text i estat per verificar:

cy.get('[data-cy="crear"]')                        // localitzar: estable
  .should('be.visible')
  .and('contain', 'Crear tasca');                  // verificar: el que veu l'usuari

I un detall d'higiene: afegeix data-cy només on una prova ho necessiti. Sembrar l'HTML d'atributs que ningú fa servir és soroll.

  1. Encadenament i espera automàtica

Aquest és el model mental que cal interioritzar, perquè és diferent de tot l'anterior.

Les ordres de Cypress no s'executen quan les escrius: s'encuen. Tot el cos de l'it es recorre primer, encuant ordres, i després s'executen una a una, de manera asíncrona.

it("demostra la cua d'ordres", () => {
  cy.visit('/');                          // encuat 1
  cy.get('[data-cy="targeta"]');          // encuat 2
  console.log("això s'imprimeix PRIMER"); // ← s'executa ja, abans de l'1 i el 2
});

D'aquí surt l'error més habitual de qui comença:

// ❌ NO funciona: cy.get() no retorna l'element, retorna una cadena d'ordres
const targetes = cy.get('[data-cy="targeta"]');
expect(targetes.length).to.equal(6);      // undefined

// ✅ El valor es rep en un callback
cy.get('[data-cy="targeta"]').then(($targetes) => {
  expect($targetes).to.have.length(6);
});

// ✅ Millor encara: l'asserció encadenada, que a més REINTENTA
cy.get('[data-cy="targeta"]').should('have.length', 6);

L'espera automàtica és la segona peça. Cada ordre reintenta fins que es compleix la seva condició o s'esgota el temps límit:

  • cy.get('[data-cy="targeta"]') reintenta fins que l'element existeixi al DOM.
  • .should('be.visible') reintenta fins que sigui visible (no display:none, no visibility:hidden, amb mida).
  • .click() comprova abans que l'element existeixi, sigui visible, no estigui tapat per un altre, no estigui deshabilitat i no s'estigui movent (per una animació).

Aquesta comprovació de «no tapat per un altre» és una de les joies del nivell E2E: és exactament la fallada que jsdom no pot detectar. Si un modal transparent cobreix el botó, Cypress falla amb un missatge explícit assenyalant l'element que fa nosa.

CypressError: cy.click() failed because this element is being covered by another element:
<div class="overlay">...</div>

  1. Per què cy.wait(3000) és un antipatró

Amb el model anterior clar, la conclusió és inevitable:

// ❌ L'antipatró
cy.get('[data-cy="crear"]').click();
cy.wait(3000);                                    // "per si de cas triga"
cy.get('[data-cy="targeta"]').should('have.length', 7);

Els quatre problemes:

  1. Si triga més de 3 s, falla igualment. No has resolt res; has posat un límit arbitrari diferent del que ja tenies.
  2. Si triga 100 ms, has perdut 2,9 s. Multiplicat per vint esperes, és gairebé un minut per execució.
  3. Amaga un problema real. Per què triga? Pot ser una petició innecessària o una cursa, i l'espera fixa ho tapa.
  4. És inestable per naturalesa. El servidor de CI és més lent que el teu portàtil: el número que funciona en local no funciona allà.

L'alternativa és sempre la mateixa idea de 08-05: esperar una condició, no un temps.

// ✅ Esperar l'estat esperat: reintenta fins a 5 s i continua tan bon punt es compleix
cy.get('[data-cy="crear"]').click();
cy.get('[data-cy="targeta"]').should('have.length', 7);

// ✅ Esperar una PETICIÓ concreta, no un temps
cy.intercept('POST', '**/tasques').as('crearTasca');
cy.get('[data-cy="crear"]').click();
cy.wait('@crearTasca');                        // ← aquest cy.wait SÍ que és correcte
cy.get('[data-cy="targeta"]').should('have.length', 7);

// ✅ Esperar que alguna cosa desaparegui
cy.get('[data-cy="carregant"]').should('not.exist');

Fixa't en la distinció: cy.wait('@alias') és correcte i cy.wait(3000) no. El primer espera un succés concret —aquella petició ha acabat— i continua tan bon punt passa. El segon compta el rellotge a cegues.

L'única excepció legítima al cy.wait(ms) és provar deliberadament que no passa una cosa durant un interval («el cercador no llança la petició abans de 300 ms»), i tot i així sol haver-hi una manera millor d'expressar-ho.

  1. Interceptar la xarxa amb cy.intercept

cy.intercept intercepta les peticions del navegador. Serveix per a tres coses diferents, i convé distingir-les:

A · Observar, sense modificar res:

cy.intercept('GET', '**/tasques').as('llistar');
cy.visit('/');
cy.wait('@llistar').its('response.statusCode').should('eq', 200);

B · Fixar la resposta (stubbing), perquè la prova no depengui del servidor:

cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');

C · Simular una fallada, que és el que en un servidor real és gairebé impossible:

cy.intercept('GET', '**/tasques', { statusCode: 500, body: { missatge: 'Error intern' } });
cy.intercept('GET', '**/tasques', { forceNetworkError: true });     // caiguda de xarxa
cy.intercept('GET', '**/tasques', { statusCode: 200, body: [], delay: 2000 });  // lentitud

La decisió de fons: servidor real o respostes fixes?

Servidor real cy.intercept amb dades fixes
Realisme Màxim: prova el contracte complet Mitjà: el servidor podria canviar sense que te n'assabentis
Velocitat Baixa Alta
Determinisme Baix: les dades canvien Alt: sempre el backlog canònic
Provocar errors Molt difícil Trivial
Depèn d'un altre equip No

La política recomanada, que és la que seguirem: el recorregut feliç principal contra un servidor real (encara que sigui un json-server local amb el backlog canònic, el de 07-02), i tota la resta —errors, casos límit, estats buits— amb cy.intercept. Així es comprova el contracte de debò almenys una vegada i la resta de les proves són ràpides i deterministes.

I aquí es cobra una decisió de 08-05: els gestors de MSW que vas escriure allà descriuen la mateixa API. Compartir aquella definició entre integració i E2E evita que les dues suites simulin servidors diferents i cap s'assembli al real.

  1. Sembrar l'estat inicial

Cada prova ha de partir d'un punt conegut: el mateix principi del beforeEach de 08-03, ara amb un navegador complet pel mig.

Cypress neteja cookies i emmagatzematge entre proves, però l'estat inicial cal posar-lo. Tres tècniques, de pitjor a millor:

1 · Per la interfície (lent, fràgil). Crear les sis tasques prement botons abans de cada prova. No ho facis mai: multiplica el temps i, si el formulari es trenca, fallen totes les proves pel mateix motiu.

2 · Sembrant localStorage directament. Ràpid i directe, aprofitant el format de 07-01:

// cypress/support/commands.js
Cypress.Commands.add('sembrarTauler', (tasques) => {
  cy.fixture('backlog.json').then((backlog) => {
    window.localStorage.setItem('nomada:tauler:v1', JSON.stringify({
      nom: 'Taller Nómada',
      versio: 1,
      tasques: tasques ?? backlog
    }));
  });
});

// Ús: se sembra ABANS de visitar, perquè l'app arrenqui amb aquestes dades
beforeEach(() => {
  cy.clearLocalStorage();
  cy.sembrarTauler();
  cy.visit('/');
});

3 · cy.session per al que és car de repetir. Posa a la memòria cau l'estat (cookies, localStorage, sessionStorage) després de la primera vegada i el restaura a les següents, sense tornar a executar els passos:

Cypress.Commands.add('sessioDeMarta', () => {
  cy.session('marta', () => {
    cy.visit('/');
    cy.get('[data-cy="usuari"]').select('Marta');
    cy.get('[data-cy="entrar"]').click();
    cy.url().should('include', '/tauler');
  }, {
    // Comprovació que la sessió restaurada continua sent vàlida
    validate() {
      cy.window().its('localStorage').invoke('getItem', 'nomada:usuari').should('eq', 'Marta');
    },
    cacheAcrossSpecs: true
  });
});

En una aplicació amb autenticació, cy.session és la diferència entre una suite de dotze minuts i una de tres.

I una nota sobre el service worker de 07-05, que en E2E pot confondre molt: si serveix una versió posada a la memòria cau de l'aplicació, els teus canvis no es veuen i les proves fallen de manera incomprensible. Convé desactivar-lo durant les proves:

// cypress/support/e2e.js
beforeEach(() => {
  // Impedir que el service worker serveixi una versió antiga durant les proves
  if (window.navigator?.serviceWorker) {
    cy.window().then((win) =>
      win.navigator.serviceWorker.getRegistrations()
        .then((registres) => registres.forEach((r) => r.unregister()))
    );
  }
});

I una prova específica, a part, que sí que comprovi el funcionament sense connexió. Barrejar totes dues coses produeix fallades que ningú entén.

  1. Ordres personalitzades i fixtures

Les ordres personalitzades estenen cy amb accions del domini. Converteixen una prova en una cosa que es llegeix com una descripció del comportament:

// cypress/support/commands.js

/** Crea una tasca omplint el formulari, com ho faria una persona. */
Cypress.Commands.add('crearTasca', ({
  titol, responsable = 'Marta', prioritat = 'mitjana',
  hores = 2, data = '2026-10-20', etiquetes = ''
} = {}) => {
  cy.get('[data-cy="camp-titol"]').clear().type(titol);
  cy.get('[data-cy="camp-responsable"]').select(responsable);
  cy.get('[data-cy="camp-prioritat"]').select(prioritat);
  cy.get('[data-cy="camp-hores"]').clear().type(String(hores));
  cy.get('[data-cy="camp-data"]').clear().type(data);
  if (etiquetes) cy.get('[data-cy="camp-etiquetes"]').clear().type(etiquetes);
  cy.get('[data-cy="crear"]').click();
});

/** Selecciona la targeta el títol de la qual contingui aquest text. */
Cypress.Commands.add('targeta', (titol) =>
  cy.contains('[data-cy="targeta"]', titol));

/** Asserció de domini: el resum mostra aquestes xifres. */
Cypress.Commands.add('resumHaDeDir', ({ obertes, hores }) => {
  cy.get('[data-cy="resum"]')
    .should('contain', `${obertes} obertes`)
    .and('contain', `${hores} h`);
});

Amb elles, una prova es llegeix així:

cy.crearTasca({ titol: 'Revisar els extintors', responsable: 'Marta', hores: 2 });
cy.targeta('Revisar els extintors').should('be.visible');
cy.resumHaDeDir({ obertes: 6, hores: 47 });

Les fixtures són fitxers JSON amb dades de prova, i aquí hi van els sis canònics:

// cypress/fixtures/backlog.json
[
  { "id": 1, "titol": "Redissenyar la sala polivalent", "responsable": "Iván",
    "prioritat": "alta", "estat": "en-curs", "etiquetes": ["espai", "disseny"],
    "horesEstimades": 12, "dataLimit": "2026-09-30", "revisor": "Marta" },
  { "id": 2, "titol": "Cartelleria del taller de serigrafia", "responsable": "Marta",
    "prioritat": "mitjana", "estat": "pendent", "etiquetes": ["serigrafia", "comunicació"],
    "horesEstimades": 6, "dataLimit": "2026-10-15", "revisor": null },
  { "id": 3, "titol": "Actualitzar el web de reserves", "responsable": "Lucía",
    "prioritat": "alta", "estat": "pendent", "etiquetes": ["web", "reserves"],
    "horesEstimades": 14, "dataLimit": "2026-10-02", "revisor": "Iván" },
  { "id": 4, "titol": "Inventari de tintes de serigrafia", "responsable": "Marta",
    "prioritat": "baixa", "estat": "feta", "etiquetes": ["serigrafia", "magatzem"],
    "horesEstimades": 3, "dataLimit": "2026-09-12", "revisor": null },
  { "id": 5, "titol": "Guia sobre enquadernació per a residents", "responsable": "Iván",
    "prioritat": "mitjana", "estat": "en-curs", "etiquetes": ["enquadernació", "documentació"],
    "horesEstimades": 8, "dataLimit": "2026-11-05", "revisor": "Lucía" },
  { "id": 6, "titol": "Pressupost de la fusteria", "responsable": "Iván",
    "prioritat": "alta", "estat": "pendent", "etiquetes": ["fusteria", "compres"],
    "horesEstimades": 5, "dataLimit": "2026-09-05", "revisor": "Marta" }
]
cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');

  1. Recorregut 1: crear una tasca i veure-la al tauler

El primer dels tres recorreguts crítics. Comprova el camí complet: formulari → validació → model → API → render → persistència.

// cypress/e2e/crear-tasca.cy.js
describe('Recorregut 1 · Crear una tasca', () => {
  beforeEach(() => {
    cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']);   // data canònica congelada
    cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');
    cy.visit('/');
    cy.wait('@llistar');
    cy.get('[data-cy="targeta"]').should('have.length', 6);  // punt de partida conegut
  });

  it('La Marta crea una tasca i la veu aparèixer a la columna de pendents', () => {
    cy.intercept('POST', '**/tasques', {
      statusCode: 201,
      body: { id: 7, titol: 'Revisar els extintors', responsable: 'Marta', prioritat: 'mitjana',
              estat: 'pendent', etiquetes: ['seguretat'], horesEstimades: 2,
              dataLimit: '2026-10-20', revisor: null }
    }).as('crear');

    cy.crearTasca({ titol: 'Revisar els extintors', responsable: 'Marta',
                    hores: 2, data: '2026-10-20', etiquetes: 'seguretat' });

    // 1 · La petició ha sortit amb les dades correctes
    cy.wait('@crear').its('request.body').should('deep.include', {
      titol: 'Revisar els extintors',
      responsable: 'Marta',
      horesEstimades: 2
    });

    // 2 · La targeta apareix, a la columna correcta i amb el seu contingut
    cy.targeta('Revisar els extintors')
      .should('be.visible')
      .and('have.attr', 'data-estat', 'pendent')
      .within(() => {
        cy.contains('Marta').should('be.visible');
        cy.contains('2 h').should('be.visible');
        cy.contains('seguretat').should('be.visible');
      });

    cy.get('[data-cy="columna-pendent"] [data-cy="targeta"]').should('have.length', 4);

    // 3 · El resum s'ha actualitzat: 47 h obertes (45 + 2)
    cy.get('[data-cy="resum"]').should('contain', '47');

    // 4 · El formulari s'ha buidat i el focus ha tornat al primer camp
    cy.get('[data-cy="camp-titol"]').should('have.value', '').and('be.focused');
  });

  it('sobreviu a una recàrrega: la tasca continua sent-hi', () => {
    cy.intercept('POST', '**/tasques', { statusCode: 201, body: { id: 7, titol: 'Revisar els extintors',
      responsable: 'Marta', prioritat: 'mitjana', estat: 'pendent', etiquetes: [],
      horesEstimades: 2, dataLimit: '2026-10-20', revisor: null } });

    cy.crearTasca({ titol: 'Revisar els extintors' });
    cy.targeta('Revisar els extintors').should('be.visible');

    cy.reload();                                  // ← la prova que no existia en jsdom

    cy.targeta('Revisar els extintors').should('be.visible');
  });

  it("un títol buit mostra l'error accessible i no crea res", () => {
    cy.get('[data-cy="camp-hores"]').type('2');
    cy.get('[data-cy="camp-data"]').type('2026-10-20');
    cy.get('[data-cy="crear"]').click();

    cy.get('[role="alert"]').should('be.visible').and('contain', 'títol');
    cy.get('[data-cy="camp-titol"]')
      .should('have.attr', 'aria-invalid', 'true')
      .and('be.focused');
    cy.get('[data-cy="targeta"]').should('have.length', 6);     // res creat
  });

  it("un 500 del servidor mostra l'error i el botó de reintentar (07-03)", () => {
    cy.intercept('POST', '**/tasques', { statusCode: 500, body: { missatge: 'Error intern' } })
      .as('crearFallit');

    cy.crearTasca({ titol: 'Revisar els extintors' });
    cy.wait('@crearFallit');

    cy.get('[role="alert"]').should('be.visible').and('contain', 'servidor');
    cy.get('[data-cy="reintentar"]').should('be.visible');

    // I en reintentar amb el servidor recuperat, funciona
    cy.intercept('POST', '**/tasques', { statusCode: 201, body: { id: 7, titol: 'Revisar els extintors',
      responsable: 'Marta', prioritat: 'mitjana', estat: 'pendent', etiquetes: [],
      horesEstimades: 2, dataLimit: '2026-10-20', revisor: null } }).as('crearOk');

    cy.get('[data-cy="reintentar"]').click();
    cy.wait('@crearOk');

    cy.targeta('Revisar els extintors').should('be.visible');
    cy.get('[role="alert"]').should('not.exist');
  });
});

Quatre coses d'aquesta suite que només són possibles en E2E: la recàrrega amb cy.reload(), que comprova la persistència real de cap a cap; el should('be.focused'), que verifica la gestió del focus en un navegador de debò; la comprovació que la petició ha sortit amb el cos correcte; i el cicle complet error → reintent → èxit amb el servidor canviant de comportament a mitja prova.

cy.clock(data, ['Date']) congela només Date i deixa els temporitzadors reals funcionant: així la tasca 6 continua apareixent vençuda el dia que s'executi la prova, sense trencar les animacions ni el debounce.

  1. Recorregut 2: completar-la i comprovar el resum

El segon recorregut cobreix les transicions R6 i el recàlcul de les xifres, amb el cicle estat → render de 06-06.

// cypress/e2e/completar-tasca.cy.js
describe('Recorregut 2 · Completar una tasca', () => {
  beforeEach(() => {
    cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']);
    cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');
    cy.intercept('PATCH', '**/tasques/*', (peticio) => {
      peticio.reply({ statusCode: 200, body: { ...peticio.body, id: Number(peticio.url.split('/').pop()) } });
    }).as('actualitzar');
    cy.visit('/');
    cy.wait('@llistar');
  });

  it('La Marta porta una tasca de pendent a feta i el resum quadra a cada pas', () => {
    // Estat inicial: 5 obertes, 45 h
    cy.resumHaDeDir({ obertes: 5, hores: 45 });

    // ── Pas 1 · pendent → en-curs ──────────────────────────────────────
    cy.targeta('Cartelleria del taller de serigrafia').within(() => {
      cy.get('[data-cy="avancar"]').should('contain', 'Començar').click();
    });
    cy.wait('@actualitzar').its('request.body').should('deep.include', { estat: 'en-curs' });

    cy.targeta('Cartelleria del taller de serigrafia')
      .should('have.attr', 'data-estat', 'en-curs')
      .find('[data-cy="avancar"]').should('contain', 'Marcar feta');

    cy.resumHaDeDir({ obertes: 5, hores: 45 });      // continua oberta: no canvia

    // ── Pas 2 · en-curs → feta ─────────────────────────────────────────
    cy.targeta('Cartelleria del taller de serigrafia')
      .find('[data-cy="avancar"]').click();
    cy.wait('@actualitzar').its('request.body').should('deep.include', { estat: 'feta' });

    cy.resumHaDeDir({ obertes: 4, hores: 39 });      // 45 − 6

    // La targeta s'ha mogut a la columna de fetes
    cy.get('[data-cy="columna-feta"]')
      .should('contain', 'Cartelleria del taller de serigrafia');
    cy.get('[data-cy="columna-feta"] [data-cy="targeta"]').should('have.length', 2);
  });

  it('una tasca feta no es pot reobrir: el botó està desactivat (R6)', () => {
    cy.targeta('Inventari de tintes de serigrafia').within(() => {
      cy.get('[data-cy="avancar"]')
        .should('be.disabled')
        .and('contain', 'Completada');
    });

    cy.resumHaDeDir({ obertes: 5, hores: 45 });      // res no ha canviat
  });

  it('el repartiment per responsable es recalcula en completar', () => {
    cy.get('[data-cy="carrega-Iván"]').should('contain', '25');

    // La tasca 1 és de l'Iván: en-curs, 12 h
    cy.targeta('Redissenyar la sala polivalent').find('[data-cy="avancar"]').click();
    cy.wait('@actualitzar');

    cy.get('[data-cy="carrega-Iván"]').should('contain', '13');   // 25 − 12
    cy.resumHaDeDir({ obertes: 4, hores: 33 });
  });

  it('si el servidor rebutja el canvi, la interfície reverteix (optimistic UI de 07-03)', () => {
    cy.intercept('PATCH', '**/tasques/2', { statusCode: 500, body: { missatge: 'Error' } })
      .as('fallada');

    cy.targeta('Cartelleria del taller de serigrafia').find('[data-cy="avancar"]').click();
    cy.wait('@fallada');

    // La targeta torna al seu estat anterior i s'avisa
    cy.targeta('Cartelleria del taller de serigrafia')
      .should('have.attr', 'data-estat', 'pendent');
    cy.get('[role="alert"]').should('be.visible');
    cy.resumHaDeDir({ obertes: 5, hores: 45 });
  });
});

L'última prova és especialment valuosa: comprovar una reversió optimista exigeix que el model, la vista i la capa de xarxa col·laborin correctament davant d'una fallada. Cap prova unitària ni d'integració no cobreix aquest recorregut complet amb el navegador real pel mig.

  1. Recorregut 3: filtrar per responsable amb la URL compartible

El tercer cobreix l'encaminador de 07-06 i una propietat de producte concreta: que la URL es pugui compartir.

// cypress/e2e/filtrar-per-responsable.cy.js
describe('Recorregut 3 · Filtrar per responsable amb URL compartible', () => {
  beforeEach(() => {
    cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']);
    cy.intercept('GET', '**/tasques*', { fixture: 'backlog.json' }).as('llistar');
  });

  it("L'Iván filtra pel seu nom i la URL reflecteix el filtre", () => {
    cy.visit('/');
    cy.wait('@llistar');
    cy.get('[data-cy="targeta"]').should('have.length', 6);

    cy.get('[data-cy="filtre-Iván"]').click();

    cy.get('[data-cy="targeta"]').should('have.length', 3);
    cy.get('[data-cy="targeta"]').each(($t) => {
      cy.wrap($t).should('contain', 'Iván');
    });

    cy.url().should('include', 'responsable=Iv%C3%A1n');
    cy.get('[data-cy="filtre-Iván"]').should('have.attr', 'aria-pressed', 'true');

    // El filtre és PRESENTACIÓ: el resum continua sent del tauler complet
    cy.resumHaDeDir({ obertes: 5, hores: 45 });
  });

  it('la URL filtrada, oberta directament, mostra ja el filtre aplicat', () => {
    cy.visit('/?responsable=Lucía');
    cy.wait('@llistar');

    cy.get('[data-cy="targeta"]').should('have.length', 1);
    cy.targeta('Actualitzar el web de reserves').should('be.visible');
    cy.get('[data-cy="filtre-Lucía"]').should('have.attr', 'aria-pressed', 'true');
  });

  it('el botó enrere del navegador restaura el filtre anterior', () => {
    cy.visit('/');
    cy.wait('@llistar');

    cy.get('[data-cy="filtre-Iván"]').click();
    cy.get('[data-cy="targeta"]').should('have.length', 3);

    cy.get('[data-cy="filtre-Marta"]').click();
    cy.get('[data-cy="targeta"]').should('have.length', 2);

    cy.go('back');                                       // ← impossible en jsdom

    cy.url().should('include', 'responsable=Iv%C3%A1n');
    cy.get('[data-cy="targeta"]').should('have.length', 3);

    cy.go('back');

    cy.url().should('not.include', 'responsable');
    cy.get('[data-cy="targeta"]').should('have.length', 6);
  });

  it("el cercador filtra sense embrutar l'historial", () => {
    cy.visit('/');
    cy.wait('@llistar');

    cy.get('[data-cy="cercador"]').type('fuster');

    cy.get('[data-cy="targeta"]').should('have.length', 1);   // espera el debounce sola
    cy.url().should('include', 'q=fuster');

    cy.go('back');                                            // replaceState: torna a l'inici

    cy.url().should('not.include', 'q=');
  });

  it("l'enllaç copiat porta a la mateixa vista", () => {
    cy.visit('/?responsable=Iv%C3%A1n&ordre=data');
    cy.wait('@llistar');

    cy.get('[data-cy="targeta"]').should('have.length', 3);
    cy.get('[data-cy="targeta"]').first().should('contain', 'Pressupost de la fusteria');
  });
});

Tres coses aquí són exclusives d'E2E: cy.go('back') amb l'historial real del navegador; la comprovació que ?responsable=Iván oberta en fred funciona, que és el que fa que l'enllaç sigui de debò compartible; i l'espera del debounce sense cap cy.wait(300), perquè l'asserció reintenta fins que la llista canvia.

  1. Depurar una fallada: captures, vídeos i viatge en el temps

Quan una prova E2E falla a CI, tens quatre eines:

1 · La captura automàtica. Cypress desa un PNG a cypress/screenshots/ en el moment exacte de la fallada, amb el nom del fitxer i de la prova.

2 · El vídeo. Amb video: true, l'execució completa queda gravada. És la manera més ràpida de veure què va passar abans de la fallada.

3 · El viatge en el temps (mode interactiu). El panell esquerre llista totes les ordres executades; en passar el ratolí per una, el navegador mostra el DOM tal com era en aquell instant. És literalment rebobinar la prova, i no té equivalent en cap altra eina.

4 · Les ordres de depuració, que connecten amb tot el de 08-01:

cy.get('[data-cy="targeta"]').debug();      // pausa i imprimeix el subjecte a la consola
cy.pause();                                  // atura la prova; es reprèn a mà
cy.get('[data-cy="resum"]').then(($r) => {
  debugger;                                  // el depurador de 08-01, amb l'element a mà
});
cy.window().its('tauler').invoke('resum', '2026-09-20').then(console.table);

Aquesta última línia és un pont preciós amb la lliçó que va obrir el mòdul: cy.window() dóna accés al window real de l'aplicació, així que pots inspeccionar el model viu des de la prova i abocar-lo amb console.table.

I una funcionalitat molt útil a CI: retries a la configuració. Amb runMode: 2, una prova que falla es reintenta dues vegades abans de donar-la per fallida, i l'informe la marca com a inestable. És un equilibri raonable: absorbeix la intermitència inevitable de l'entorn real (un servidor que triga, un recurs que es retarda) sense ocultar-la, perquè la marca d'inestable continua allà perquè la investiguis. El que no s'ha de fer mai és pujar el número fins que tot passi.

  1. Executar en integració contínua

Ampliem el flux de 08-02, ara amb les tres capes de proves:

# .github/workflows/qualitat.yml
name: Qualitat

on:
  push:
    branches: [master]
  pull_request:

jobs:
  # ── Treball 1 · ràpid: anàlisi estàtica i proves de Jest ───────────────
  verificar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run format:check
      - run: npm test -- --coverage --ci

  # ── Treball 2 · lent: extrem a extrem ──────────────────────────────────
  e2e:
    runs-on: ubuntu-latest
    needs: verificar             # ← només si el que és barat ja ha passat: estalvia minuts
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm

      # L'acció oficial instal·la Cypress, posa el seu binari a la memòria cau,
      # arrenca el servidor, espera que respongui i executa les proves
      - name: Executar Cypress
        uses: cypress-io/github-action@v6
        with:
          start: npm run dev
          wait-on: 'http://localhost:5173'
          wait-on-timeout: 120
          browser: chrome

      # Si alguna cosa falla, pugem les proves del delicte
      - name: Desar captures
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-captures
          path: cypress/screenshots
          retention-days: 7

      - name: Desar vídeos
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cypress-videos
          path: cypress/videos
          retention-days: 3

Tres decisions d'aquest flux:

  • needs: verificar encadena els treballs: si el lint falla, no es gasten minuts en E2E. El que és barat primer.
  • wait-on espera que el servidor respongui de debò, en lloc de dormir un nombre de segons.
  • if: failure() puja les captures només quan hi ha fallada, que és quan serveixen; els vídeos es pugen sempre però caduquen en tres dies.

I una recomanació de calendari, perquè el temps de CI és diners i paciència:

Quan Què s'executa
En desar (editor) Prettier + ESLint
Pre-commit ESLint + Prettier sobre el que està preparat
Cada push / PR Lint + Jest complet + els tres recorreguts E2E
Cada nit E2E ampliat, diversos navegadors, accessibilitat
Abans de desplegar Tot, contra l'entorn de preproducció

  1. Accessibilitat automatitzada amb axe

axe és un motor d'auditoria d'accessibilitat que s'integra amb Cypress i comprova automàticament desenes de regles de les WCAG:

npm install --save-dev cypress-axe axe-core
// cypress/support/e2e.js
import 'cypress-axe';
it("el tauler no té problemes greus d'accessibilitat", () => {
  cy.visit('/');
  cy.injectAxe();

  cy.checkA11y(null, {
    includedImpacts: ['critical', 'serious']    // comença pel que és greu, no per tot
  });
});

it('el formulari amb errors continua sent accessible', () => {
  cy.visit('/');
  cy.injectAxe();

  cy.get('[data-cy="crear"]').click();          // provoca els errors de validació

  cy.checkA11y('[data-cy="form-tasca"]');       // auditar només aquella regió
});

El que axe detecta: contrast insuficient, imatges sense text alternatiu, camps sense etiqueta, atributs ARIA mal fets servir, encapçalaments desordenats, elements interactius inassolibles amb el teclat.

I el que convé tenir clar: les eines automàtiques detecten al voltant d'un terç dels problemes reals d'accessibilitat. No comproven si l'ordre de lectura té sentit, si un text alternatiu és útil o si l'aplicació es pot fer servir amb un lector de pantalla. Són un terra mínim, no un certificat. Dit això, aquest terç és barat d'aconseguir i cobreix les fallades més freqüents, així que mereix ser a la suite.

Fixa't a més en una coherència agradable: consultar per rol accessible a 08-05 i auditar amb axe aquí empenyen en la mateixa direcció. Un HTML que es deixa consultar per rol sol passar axe sense esforç.

  1. Cypress davant de Playwright

Playwright és l'altra eina seriosa de l'ecosistema. Comparativa honesta:

Cypress Playwright
Arquitectura Dins del navegador Fora, per protocol d'automatització
Navegadors Chrome, Edge, Firefox, Electron; WebKit experimental Chromium, Firefox i WebKit de sèrie
Diverses pestanyes / finestres No
Diversos orígens en una prova Amb cy.origin() Natiu
Paral·lelisme De pagament al seu servei, o configurat a mà Gratuït i incorporat
API Encadenada, amb cua implícita async/await estàndard
Espera automàtica
Depuració Excepcional: viatge en el temps Molt bona: trace viewer, mode UI
Corba d'aprenentatge Més suau Una mica més pronunciada
Emulació mòbil Limitada Completa, amb dispositius predefinits
Gravar una prova Amb extensió codegen incorporat

La diferència d'API es veu d'una ullada:

// Cypress: cua d'ordres, sense await
cy.visit('/');
cy.get('[data-cy="crear"]').click();
cy.get('[data-cy="targeta"]').should('have.length', 7);

// Playwright: async/await estàndard, com la resta del teu JavaScript
await page.goto('/');
await page.getByTestId('crear').click();
await expect(page.getByTestId('targeta')).toHaveCount(7);

Quan triar cadascun:

  • Cypress: aplicació d'una sola pàgina i un sol origen, equip que comença amb E2E, prioritat en l'experiència de depuració. És el cas de Nómada Tasques.
  • Playwright: cal provar en WebKit (Safari) de debò, diverses pestanyes o sessions simultànies, emulació mòbil seriosa, o paral·lelisme gratuït a CI.

I l'important per a tu: els conceptes són els mateixos i es transfereixen sencers. Selectors estables, espera per condició i mai per temps, interceptar la xarxa, sembrar l'estat, poques proves ben triades. Canvia la sintaxi; no canvia res del que has après en aquesta lliçó.

  1. Què NO provar en E2E

Tan important com saber escriure-les és saber quan no fer-ho. Cada prova E2E de més és temps de CI, manteniment i una font potencial d'intermitència.

No ho provis en E2E Prova-ho a… Per què
Les nou transicions de R6 Unitària (08-03) Nou proves de 2 ms davant de nou de 5 s, i amb millor diagnòstic
Els valors frontera d'hores (0, 1, 40, 41) Unitària Combinatòria: E2E és el pitjor lloc per a molts casos
Que toJSON/desDeJSON conserven els camps Integració (08-05) No necessita navegador
Cada missatge d'error de validació Integració Un en E2E basta per verificar que el mecanisme funciona
Tots els camins de demanarJson Unitària amb dobles (08-04) Set escenaris que no requereixen interfície
Estils concrets: colors, marges Proves visuals de regressió Una altra eina, un altre problema
Que un text digui exactament aquesta frase Enlloc Es trenca amb cada retoc de redacció
Combinacions exhaustives de filtres Integració En E2E, una de representativa

La regla de decisió:

flowchart TD
    A["Necessita un navegador REAL:<br/>CSS, focus, historial, recàrrega, SW?"] -->|No| B["No és E2E<br/>Unitària o integració"]
    A -->|Sí| C["És un recorregut que<br/>importa al negoci?"]
    C -->|No| D["Probablement sobra"]
    C -->|Sí| E["Ja està cobert per<br/>un altre recorregut E2E?"]
    E -->|Sí| F["Amplia l'existent"]
    E -->|No| G["✅ Escriu-la"]
    style G fill:#bbf7d0,stroke:#15803d
    style B fill:#fde68a,stroke:#b45309

Per això Nómada Tasques té tres recorreguts E2E i no trenta: crear, completar i filtrar. Són els tres que, si deixessin de funcionar, farien que la Marta no pogués fer servir l'aplicació. Tota la resta està coberta per 124 proves que triguen quatre segons.

Errors Habituals i Consells

  • cy.wait(3000) per "assegurar" que alguna cosa s'ha carregat. És lent, fràgil i tapa el problema real. Espera la condició (.should()) o l'àlies de la petició (cy.wait('@llistar')).
  • Intentar fer servir el valor retornat per cy.get(). Les ordres s'encuen i no retornen elements: fes servir .then() o, millor, .should().
  • Seleccionar per classe CSS o per nth-child. La prova es trenca amb cada retoc de maquetació. Fes servir data-cy.
  • Escriure trenta proves E2E. La suite passa de tres a trenta minuts, deixa d'executar-se a cada commit i tot el valor es perd. Poques i ben triades.
  • Dependre de l'estat que ha deixat la prova anterior. Cada it ha de partir d'un punt conegut: sembra al beforeEach i neteja l'emmagatzematge.
  • Crear les dades per la interfície abans de cada prova. Multiplica el temps i fa que una fallada del formulari tombi tota la suite. Sembra per localStorage o per API.
  • Oblidar el service worker. Pot servir una versió antiga i provocar fallades incomprensibles. Desregistra'l a les proves i dedica-li una prova pròpia.
  • Provar en E2E el que ja cobreix una unitària. Les nou transicions de R6 en E2E són 45 segons per saber el que ja sabies en 20 mil·lisegons.
  • Pujar retries fins que tot passi. Converteix un problema visible en un d'invisible. Dos reintents a CI són un amortidor raonable; més és amagar la brossa sota la catifa.
  • Consell: la data, congelada. cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']) manté la tasca vençuda vençuda el dia que sigui, sense trencar animacions ni debounce.
  • Consell: escriu els recorreguts com els explicaria la Marta. «Creo una tasca, la veig a pendents, la marco feta i el resum baixa de 45 a 39 hores.» Si la prova no es pot narrar així, probablement no és un recorregut E2E.
  • Consell: quan un recorregut falli, fes servir el vídeo abans que el codi. Veure què ha passat estalvia la meitat del pas «reproduir» del mètode de 08-01.

Exercicis

Exercici 1 — El quart recorregut: treballar sense connexió. Escriu cypress/e2e/sense-connexio.cy.js que comprovi la PWA de 07-05 de cap a cap: (a) amb l'aplicació carregada i el service worker actiu, posar el navegador sense connexió amb cy.intercept i forceNetworkError i comprovar que el tauler continua visible amb les sis tasques i apareix un avís de mode sense connexió; (b) que un canvi d'estat fet sense connexió s'aplica a la interfície i s'encua; (c) que en restaurar la xarxa el canvi encuat s'envia i l'avís desapareix. Justifica per què aquest recorregut només es pot provar en E2E i quines precaucions cal prendre amb el service worker entre proves.

Exercici 2 — Ordres personalitzades i assercions de domini. Amplia cypress/support/commands.js amb cinc ordres que facin que les proves es llegeixin com una descripció de negoci: cy.sembrarBacklog(canvis) (sembra el backlog canònic permetent modificar tasques concretes), cy.targeta(titol), cy.avancar(titol) (prem el botó d'avançar d'aquella targeta i espera que la petició acabi), cy.filtrarPer(responsable) i cy.resumHaDeDir({ obertes, hores, vencudes }). Reescriu després el recorregut 2 fent-les servir i compara la llegibilitat de les dues versions.

Exercici 3 — Repartir una suite entre els tres nivells. Per a cadascuna d'aquestes vuit comprovacions, decideix a quin nivell ha de viure (unitària, integració o E2E), justifica-ho en una frase i escriu la primera línia de la prova corresponent:

  1. Una tasca amb horesEstimades: 41 llança ErrorDeValidacio.
  2. El botó «Crear tasca» no està tapat per la barra de filtres en una finestra de 1280×800.
  3. toJSON() inclou el camp privat #estat.
  4. En prémer «Començar», el resum passa de 45 a 45 h i el botó canvia de text.
  5. Un 500 a llistarTasques produeix ErrorDeApi amb codi: 'servidor'.
  6. L'enllaç ?responsable=Iván obert en fred mostra 3 targetes.
  7. reconciliar() reaprofita el node existent en lloc de recrear-lo.
  8. El debounce del cercador espera 300 ms des de l'última pulsació.

Solucions

Solució 1

// cypress/e2e/sense-connexio.cy.js
describe('Recorregut 4 · Treballar sense connexió (07-05)', () => {
  beforeEach(() => {
    // Precaució 1: partir sempre d'un service worker net, o una versió posada
    // a la memòria cau d'una execució anterior serviria HTML antic i les
    // fallades serien incomprensibles.
    cy.window().then((win) => {
      if (win.navigator.serviceWorker) {
        return win.navigator.serviceWorker.getRegistrations()
          .then((regs) => Promise.all(regs.map((r) => r.unregister())));
      }
    });

    cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']);
    cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');
    cy.visit('/');
    cy.wait('@llistar');

    // Precaució 2: esperar que el SW estigui ACTIU abans de tallar la xarxa,
    // o la prova mesuraria el cas "sense SW", que és un escenari diferent.
    cy.window().its('navigator.serviceWorker.ready').should('exist');
    cy.get('[data-cy="targeta"]').should('have.length', 6);
  });

  it("(a) sense connexió, el tauler continua visible i s'avisa", () => {
    cy.intercept('GET', '**/tasques', { forceNetworkError: true }).as('caiguda');

    cy.reload();

    cy.get('[data-cy="targeta"]').should('have.length', 6);      // servit pel SW
    cy.get('[data-cy="avis-offline"]')
      .should('be.visible')
      .and('contain', 'sense connexió');
  });

  it("(b) un canvi sense connexió s'aplica a la interfície i s'encua", () => {
    cy.intercept('PATCH', '**/tasques/*', { forceNetworkError: true }).as('patchCaigut');

    cy.targeta('Cartelleria del taller de serigrafia').find('[data-cy="avancar"]').click();
    cy.wait('@patchCaigut');

    // La interfície reflecteix el canvi (optimista) i avisa que està pendent
    cy.targeta('Cartelleria del taller de serigrafia')
      .should('have.attr', 'data-estat', 'en-curs')
      .and('have.class', 'tasca--pendent-de-sincronitzar');
    cy.get('[data-cy="cua-pendent"]').should('contain', '1');
  });

  it("(c) en recuperar la xarxa, el que està encuat s'envia i l'avís desapareix", () => {
    cy.intercept('PATCH', '**/tasques/*', { forceNetworkError: true }).as('patchCaigut');
    cy.targeta('Cartelleria del taller de serigrafia').find('[data-cy="avancar"]').click();
    cy.wait('@patchCaigut');
    cy.get('[data-cy="cua-pendent"]').should('contain', '1');

    // La xarxa torna
    cy.intercept('PATCH', '**/tasques/*', (p) => p.reply({ statusCode: 200, body: p.body }))
      .as('patchOk');
    cy.window().then((win) => win.dispatchEvent(new Event('online')));

    cy.wait('@patchOk').its('request.body').should('deep.include', { estat: 'en-curs' });
    cy.get('[data-cy="cua-pendent"]').should('not.exist');
    cy.get('[data-cy="avis-offline"]').should('not.exist');
  });
});

Per què només es pot provar en E2E: jsdom no implementa service workers ni Cache API, així que la part central del recorregut —que l'aplicació se serveixi des de la memòria cau després d'una recàrrega sense xarxa— és literalment inobservable a 08-05. A més calen una recàrrega real (cy.reload()), l'esdeveniment online del navegador i el cicle de vida complet del worker.

Precaucions: desregistrar el SW a cada beforeEach per no arrossegar memòries cau entre proves; esperar serviceWorker.ready abans de tallar la xarxa; i mantenir aquest fitxer separat dels altres tres recorreguts, perquè un SW actiu interfereix amb les respostes de cy.intercept i produeix fallades difícils de diagnosticar.

Solució 2

// cypress/support/commands.js

/**
 * Sembra el backlog canònic a localStorage, permetent modificar
 * tasques concretes per id: cy.sembrarBacklog({ 2: { estat: 'en-curs' } })
 */
Cypress.Commands.add('sembrarBacklog', (canvis = {}) => {
  cy.fixture('backlog.json').then((backlog) => {
    const tasques = backlog.map((t) => ({ ...t, ...(canvis[t.id] ?? {}) }));
    cy.window().then((win) => {
      win.localStorage.setItem('nomada:tauler:v1',
        JSON.stringify({ nom: 'Taller Nómada', versio: 1, tasques }));
    });
  });
});

/** La targeta el títol de la qual conté aquest text. */
Cypress.Commands.add('targeta', (titol) =>
  cy.contains('[data-cy="targeta"]', titol));

/** Prem "avançar" en aquella targeta i espera que la petició acabi. */
Cypress.Commands.add('avancar', (titol) => {
  cy.intercept('PATCH', '**/tasques/*').as('avancarPeticio');
  cy.targeta(titol).find('[data-cy="avancar"]').click();
  cy.wait('@avancarPeticio');                     // mai cy.wait(ms)
  return cy.targeta(titol);                       // retorna la targeta, encadenable
});

/** Aplica el filtre d'una persona i espera que la llista s'estabilitzi. */
Cypress.Commands.add('filtrarPer', (responsable) => {
  cy.get(`[data-cy="filtre-${responsable}"]`).click()
    .should('have.attr', 'aria-pressed', 'true');
  cy.url().should('include', `responsable=${encodeURIComponent(responsable)}`);
});

/** Asserció de domini sobre el panell de resum. */
Cypress.Commands.add('resumHaDeDir', ({ obertes, hores, vencudes }) => {
  cy.get('[data-cy="resum"]').within(() => {
    if (obertes !== undefined) cy.contains(`${obertes} obertes`).should('be.visible');
    if (hores !== undefined) cy.contains(`${hores} h`).should('be.visible');
    if (vencudes !== undefined) cy.contains(`${vencudes} vençuda`).should('be.visible');
  });
});
// El recorregut 2, reescrit: es llegeix com ho explicaria la Marta
describe('Recorregut 2 · Completar una tasca', () => {
  beforeEach(() => {
    cy.clock(new Date('2026-09-20T09:00:00Z'), ['Date']);
    cy.intercept('GET', '**/tasques', { fixture: 'backlog.json' }).as('llistar');
    cy.visit('/');
    cy.wait('@llistar');
  });

  it('La Marta porta la cartelleria de pendent a feta', () => {
    cy.resumHaDeDir({ obertes: 5, hores: 45, vencudes: 1 });

    cy.avancar('Cartelleria del taller de serigrafia')
      .should('have.attr', 'data-estat', 'en-curs');
    cy.resumHaDeDir({ obertes: 5, hores: 45 });

    cy.avancar('Cartelleria del taller de serigrafia')
      .should('have.attr', 'data-estat', 'feta');
    cy.resumHaDeDir({ obertes: 4, hores: 39 });
  });

  it('una tasca ja feta no es pot reobrir (R6)', () => {
    cy.targeta('Inventari de tintes de serigrafia')
      .find('[data-cy="avancar"]').should('be.disabled');
  });
});

La versió amb ordres ocupa la meitat, no repeteix ni un sol selector i es pot llegir en veu alta a la Marta per validar que la prova comprova el que ella espera. Aquest últim punt és l'argument decisiu: una prova E2E que un no programador pot entendre és una prova que verifica el negoci, no la implementació.

Solució 3

# Nivell Justificació Primera línia
1 Unitària Lògica pura del model, sense DOM ni xarxa; és un valor frontera d'una taula expect(() => unaTasca({ horesEstimades: 41 })).toThrow(ErrorDeValidacio);
2 E2E Depèn del pintat i de posicions reals: jsdom no ho pot saber cy.get('[data-cy="crear"]').should('be.visible').click();
3 Unitària Serialització pura, sense col·laboradors expect(unaTasca({ estat: 'en-curs' }).toJSON()).toStrictEqual({ … });
4 Integració Vista + model + controlador col·laborant; no necessita navegador real await usuari.click(within(targeta).getByRole('button', { name: /començar/i }));
5 Unitària amb dobles Un cas de la taula d'errors de xarxa; es resol amb un fetch doble (08-04) xarxa.mockResolvedValue(respostaJson({ missatge: 'Error' }, { status: 500 }));
6 E2E Exigeix una URL real, una càrrega en fred i l'encaminador del navegador cy.visit('/?responsable=Iv%C3%A1n');
7 Integració Necessita un DOM, però no un navegador: toBe sobre la identitat del node expect(document.querySelector('[data-id="2"]')).toBe(nodeAbans);
8 Unitària És lògica de temporització pura, amb temporitzadors falsos (08-04) jest.advanceTimersByTime(299); expect(cercar).not.toHaveBeenCalled();

Observacions sobre el repartiment: només dues de les vuit són E2E, i totes dues pel mateix motiu —requereixen un navegador de debò per al pintat o per a l'historial—. Quatre són unitàries perquè són lògica pura, i dues són d'integració perquè necessiten un DOM però no un navegador. Aquest repartiment, aplicat sistemàticament, és el que manté una suite ràpida i fiable.

Conclusió

Nómada Tasques té la xarxa de seguretat completa. Saps què valida una prova d'extrem a extrem i cap altra: que index.html carrega, que els mòduls ES es resolen al navegador de debò, que el CSS no tapa el botó, que el focus va on toca, que una recàrrega conserva les dades, que l'historial funciona i que el service worker serveix el que cal. I saps què costen —segons per prova, minuts per suite, alta fragilitat, poca precisió en fallar—, d'on surt la regla que governa tot el nivell: poques i molt ben triades.

Coneixes Cypress a fons: la seva arquitectura dins del navegador, amb les virtuts que li dóna (espera automàtica real, accés al window de l'aplicació, viatge en el temps) i els límits que li imposa (una pestanya, un origen, cy.task per al de Node); l'executor interactiu per escriure i el mode run amb start-server-and-test per a CI; l'anatomia d'una prova amb cy.visit, cy.get, cy.contains, .click(), .type() i .should(); i els selectors estables amb data-cy, reservant les assercions sobre text i rol per verificar el que veu l'usuari. Has interioritzat el model de cua d'ordres —que explica per què const x = cy.get(...) no funciona— i l'espera automàtica, inclosa aquella comprovació de «l'element no està tapat per un altre» que és just el que jsdom no pot veure. I saps per què cy.wait(3000) és un antipatró mentre cy.wait('@llistar') és correcte: un compta el rellotge a cegues, l'altre espera un succés.

Domines cy.intercept en els seus tres usos —observar, fixar respostes i provocar fallades impossibles de reproduir contra un servidor real— amb la política d'un recorregut feliç contra el servidor de debò i tota la resta amb dades fixes. Sembres l'estat inicial per localStorage o amb cy.session en lloc de crear-lo per la interfície, desregistres el service worker perquè no serveixi versions antigues, i escrius ordres personalitzades i fixtures que fan que una prova es pugui llegir en veu alta a la Marta. Saps depurar una fallada amb captures, vídeo, viatge en el temps i cy.window() per inspeccionar el model viu; executar a GitHub Actions amb needs perquè el que és barat vagi primer i les captures es pugin només en fallar; afegir una auditoria d'accessibilitat amb axe sabent que cobreix al voltant d'un terç dels problemes reals; i triar entre Cypress i Playwright amb criteri, tenint clar que els conceptes es transfereixen sencers.

I sobretot has escrit els tres recorreguts crítics: la Marta crea una tasca i la veu aparèixer a pendents amb el resum pujant a 47 h, amb la seva comprovació que sobreviu a una recàrrega i el seu cicle error 500 → reintent → èxit; porta la cartelleria de pendent a en-curs a feta veient el resum baixar de 45 a 39 h i la càrrega de l'Iván de 25 a 13, amb la reversió optimista quan el servidor rebutja; i l'Iván filtra pel seu nom amb la URL reflectint el filtre, el botó enrere restaurant l'estat anterior i l'enllaç ?responsable=Iván funcionant obert en fred. Tres recorreguts, no trenta, perquè saps què no provar en E2E: les nou transicions de R6 són unitàries, el viatge de toJSON és d'integració, els set escenaris de demanarJson es cobreixen amb dobles, i en E2E només hi va el que necessita un navegador de debò i importa al negoci.

Amb això es tanca el Mòdul 8. Nómada Tasques ha passat de no tenir cap comprovació automàtica a tenir quatre capes de defensa: l'anàlisi estàtica d'ESLint i Prettier amb els seus hooks de Git, que caça les fallades de forma abans d'executar res; 48 proves unitàries que blinden el model sencer —validacions, transicions, venciments, serialització i els números canònics: 6 tasques, 48 h totals, 45 h obertes, esforç 124—; proves amb dobles que cobreixen xarxa, rellotge, emmagatzematge i aleatorietat sense sortir de la màquina; proves d'integració que verifiquen les costures entre model, dades i vista en un DOM sense navegador; i tres recorreguts E2E que confirmen que l'aplicació sencera funciona com la faria servir una persona. Cent vint-i-quatre proves en quatre segons, més tres recorreguts en un minut. I tens el mètode de depuració per quan alguna cosa falli igualment: reproduir, aïllar, formular una hipòtesi falsable, comprovar-la amb l'instrument adequat, corregir la causa i prevenir amb una prova.

Fixa't en el que ha canviat de debò. En començar el mòdul, canviar una línia feia por. Ara pots reescriure Tauler sencer, substituir el sistema de render, canviar la capa de dades… i saber en quatre segons si alguna cosa s'ha trencat. Això és el que fa possible el que ve ara. Perquè Nómada Tasques ja funciona i ja és correcta, però ningú no ha preguntat encara si és ràpida: quant triga a pintar-se amb sis-centes tasques en lloc de sis, quants fotogrames perd en desplaçar-se, quanta memòria reté, quant JavaScript descarrega abans de mostrar la primera targeta. I optimitzar sense xarxa de seguretat és la manera més eficaç de trencar coses subtilment: una memòria cau mal invalidada, una comparació que se salta un cas, un càlcul que es fa fora de lloc. Amb 124 proves al darrere, es pot tocar el rendiment sense por… però hi ha una condició prèvia que gairebé tothom es salta, i és exactament la mateixa disciplina que has après en depurar: primer mesurar, després actuar. Optimitzar per intuïció produeix el mateix resultat que depurar per intuïció —canvis que no convergeixen, complexitat afegida i cap millora comprovable—. Aquest és el Mòdul 9: Rendiment, que comença precisament per aquí: Mesurar Abans de Optimitzar.

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