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
- Què valida una prova d'extrem a extrem
- El cost: lentes, fràgils i cares de mantenir
- Cypress: instal·lació i estructura del projecte
- L'arquitectura: dins del navegador
- L'executor interactiu davant del mode
run - Anatomia d'una prova
- Selectors estables:
data-cyi per què no la classe CSS - Encadenament i espera automàtica
- Per què
cy.wait(3000)és un antipatró - Interceptar la xarxa amb
cy.intercept - Sembrar l'estat inicial
- Ordres personalitzades i fixtures
- Recorregut 1: crear una tasca i veure-la al tauler
- Recorregut 2: completar-la i comprovar el resum
- Recorregut 3: filtrar per responsable amb la URL compartible
- Depurar una fallada: captures, vídeos i viatge en el temps
- Executar en integració contínua
- Accessibilitat automatitzada amb axe
- Cypress davant de Playwright
- Què NO provar en E2E
- Errors Habituals i Consells
- Exercicis
- Conclusió
- 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
- 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.
- Cypress: instal·lació i estructura del projecte
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:
- 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, interceptarfetchdes 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.
- L'executor interactiu davant del mode
run
runCypress 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 capturesopen (interactiu) |
run (consola) |
|
|---|---|---|
| Navegador | Visible, es pot inspeccionar | Sense interfície (headless) |
| En desar el fitxer | Recarrega i reexecuta | — |
| Viatge en el temps | Sí | 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.
- 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.
- Selectors estables:
data-cy i per què no la classe CSS
data-cy i per què no la classe CSSA 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'usuariI 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.
- 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 (nodisplay:none, novisibility: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>
- Per què
cy.wait(3000) és un antipatró
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:
- Si triga més de 3 s, falla igualment. No has resolt res; has posat un límit arbitrari diferent del que ja tenies.
- Si triga 100 ms, has perdut 2,9 s. Multiplicat per vint esperes, és gairebé un minut per execució.
- Amaga un problema real. Per què triga? Pot ser una petició innecessària o una cursa, i l'espera fixa ho tapa.
- É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.
- Interceptar la xarxa amb
cy.intercept
cy.interceptcy.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:
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 }); // lentitudLa 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 | Sí | 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.
- 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.
- 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" }
]
- 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.
- 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.
- 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.
- 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.
- 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: 3Tres decisions d'aquest flux:
needs: verificarencadena els treballs: si el lint falla, no es gasten minuts en E2E. El que és barat primer.wait-onespera 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ó |
- 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:
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ç.
- 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 | Sí |
| 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 | Sí | Sí |
| 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çó.
- 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 servirdata-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
itha de partir d'un punt conegut: sembra albeforeEachi 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
localStorageo 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
retriesfins 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 nidebounce. - 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:
- Una tasca amb
horesEstimades: 41llançaErrorDeValidacio. - El botó «Crear tasca» no està tapat per la barra de filtres en una finestra de 1280×800.
toJSON()inclou el camp privat#estat.- En prémer «Començar», el resum passa de 45 a 45 h i el botó canvia de text.
- Un
500allistarTasquesprodueixErrorDeApiambcodi: 'servidor'. - L'enllaç
?responsable=Ivánobert en fred mostra 3 targetes. reconciliar()reaprofita el node existent en lloc de recrear-lo.- El
debouncedel 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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
