From 967be6df4a4f5b6c4161e17e52588b3db28f6f9f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 12:32:36 +0200 Subject: [PATCH 1/6] test(cypress): convention de viewports telephone/tablette Ticket #83 du chantier UI mobile & tablette. Le repo n'avait aucun override de viewportWidth/Height ni pour e2e ni pour les tests de composants - tout tournait a la taille par defaut, jamais un vrai telephone/une vraie tablette. - cypress/support/viewports.ts : PHONE_VIEWPORT (390x844) et TABLET_VIEWPORT (900x1200), tailles Android reelles plutot que les presets iPhone/iPad de Cypress (appli Android uniquement) - dans les bandes de _breakpoints.scss ($phone/$tablet du ticket #91), pas collees au pixel de bordure. Commande cy.setViewport("phone" | "tablet") (pas de surcharge du cy.viewport natif). Importe par support/e2e.ts et support/component.ts, disponible dans les deux modes. - layout.cy.ts : les cy.viewport(...) en dur pour du telephone (deux tests ajoutes par #91, plus les deux plus anciens du passage #39) et le test "au-dessus du point de rupture telephone" migrent vers la commande partagee - ce dernier passe a un vrai viewport tablette (900px, dans la bande 641-1024) plutot qu'un 1000x700 arbitraire, prouvant au passage que le preset tablette fonctionne. - Nouveau cypress/component/Dialog.cy.tsx : premier test de Dialog en isolation (jusqu'ici seulement couvert indirectement via RecipePickerDialog) - verifie le plein-ecran sur telephone du ticket #91, qui n'avait pas encore son propre test, et prouve au passage que cy.setViewport fonctionne en mode composant. Trouvaille en verifiant : le mode composant de Cypress a son propre viewport par defaut, plus petit que celui du mode e2e (1000x660) - deja sous le seuil telephone. Le premier essai du test "desktop" de Dialog echouait pour cette raison (le CSS plein-ecran s'appliquait sans meme appeler setViewport). Corrige en fixant un viewport large explicite pour ce cas plutot que de compter sur un defaut implicite - note ajoutee a dev-environment-gotchas pour la prochaine fois. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (140/140) et composants (65/65, incluant les 2 nouveaux tests de Dialog.cy.tsx). Co-Authored-By: Claude Sonnet 5 --- apps/web/cypress/component/Dialog.cy.tsx | 47 ++++++++++++++++++++++++ apps/web/cypress/e2e/layout.cy.ts | 20 +++++----- apps/web/cypress/support/component.ts | 2 + apps/web/cypress/support/e2e.ts | 2 +- apps/web/cypress/support/viewports.ts | 30 +++++++++++++++ 5 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 apps/web/cypress/component/Dialog.cy.tsx create mode 100644 apps/web/cypress/support/viewports.ts diff --git a/apps/web/cypress/component/Dialog.cy.tsx b/apps/web/cypress/component/Dialog.cy.tsx new file mode 100644 index 0000000..b5f2703 --- /dev/null +++ b/apps/web/cypress/component/Dialog.cy.tsx @@ -0,0 +1,47 @@ +import { Dialog } from "../../src/components/ui/Dialog"; +import { PHONE_VIEWPORT } from "../support/viewports"; + +// First component test for Dialog — until now it was only exercised +// indirectly through RecipePickerDialog's own e2e coverage. Added here +// specifically to prove `cy.setViewport()` (mobile/tablet UI chantier, +// ticket #83) works correctly in component-test mode too, not just e2e — +// and along the way, covers the phone full-screen treatment (ticket #91) +// that had no test of its own yet. + +describe("Dialog — phone full-screen treatment", () => { + it("is a centered, rounded panel with room around it at a desktop viewport", () => { + // Component testing's own default viewport (unlike e2e's) is already + // phone-sized — an explicit wide viewport here, not just "whatever the + // default is", is what actually makes this the desktop case. + const desktopWidth = 1280; + cy.viewport(desktopWidth, 800); + cy.mount( + {}}> + Contenu + , + ); + + cy.get(".dialog-panel").should(($el) => { + const style = getComputedStyle($el[0]); + expect(style.borderRadius).to.not.equal("0px"); + expect($el[0].getBoundingClientRect().width).to.be.lessThan(desktopWidth); + }); + }); + + it("goes edge-to-edge, with no rounding, at a phone viewport", () => { + cy.setViewport("phone"); + cy.mount( + {}}> + Contenu + , + ); + + cy.get(".dialog-panel").should(($el) => { + const style = getComputedStyle($el[0]); + expect(style.borderRadius).to.equal("0px"); + const rect = $el[0].getBoundingClientRect(); + expect(rect.width).to.be.closeTo(PHONE_VIEWPORT.width, 1); + expect(rect.height).to.be.closeTo(PHONE_VIEWPORT.height, 1); + }); + }); +}); diff --git a/apps/web/cypress/e2e/layout.cy.ts b/apps/web/cypress/e2e/layout.cy.ts index 65f6589..c00089f 100644 --- a/apps/web/cypress/e2e/layout.cy.ts +++ b/apps/web/cypress/e2e/layout.cy.ts @@ -11,6 +11,8 @@ // width (some stuck to the left edge with a lopsided gap, others correctly // full-bleed). +import { PHONE_VIEWPORT } from "../support/viewports"; + const authenticatedProfile = { id: 1, firstName: "Alice", @@ -185,7 +187,7 @@ describe("Responsive breakpoint — the sidebar becomes a bottom tab bar under 6 }); it("renders the bottom nav instead of the sidebar, with every route tab legible and a working Plus tab", () => { - cy.viewport(375, 812); + cy.setViewport("phone"); cy.visit("/"); // The desktop sidebar isn't just hidden — it's not in the DOM at all. @@ -193,10 +195,10 @@ describe("Responsive breakpoint — the sidebar becomes a bottom tab bar under 6 cy.get(".app-bottom-nav").should(($nav) => { const rect = $nav[0].getBoundingClientRect(); - expect(rect.width).to.be.closeTo(375, 1); + expect(rect.width).to.be.closeTo(PHONE_VIEWPORT.width, 1); // A short bar flush against the bottom edge, not a tall rail. expect(rect.height).to.be.lessThan(120); - expect(rect.bottom).to.be.closeTo(812, 1); + expect(rect.bottom).to.be.closeTo(PHONE_VIEWPORT.height, 1); expect(getComputedStyle($nav[0]).position).to.equal("fixed"); }); @@ -238,8 +240,8 @@ describe("Responsive breakpoint — the sidebar becomes a bottom tab bar under 6 cy.get(".bottom-sheet").should("not.exist"); }); - it("keeps the desktop sidebar (not the bottom nav) above the phone breakpoint", () => { - cy.viewport(1000, 700); + it("keeps the desktop sidebar (not the bottom nav) at a tablet viewport", () => { + cy.setViewport("tablet"); cy.visit("/"); cy.get(".app-sidebar").should("exist"); @@ -248,8 +250,6 @@ describe("Responsive breakpoint — the sidebar becomes a bottom tab bar under 6 }); describe("Responsive breakpoint — page content under 640px (issue #39)", () => { - const PHONE: [number, number] = [375, 812]; - beforeEach(() => { interceptAuth(); }); @@ -266,7 +266,7 @@ describe("Responsive breakpoint — page content under 640px (issue #39)", () => expect(s.paddingLeft).to.equal("32px"); }); - cy.viewport(...PHONE); + cy.setViewport("phone"); cy.visit("/"); cy.get(".app-content").should(($el) => { const s = getComputedStyle($el[0]); @@ -298,7 +298,7 @@ describe("Responsive breakpoint — page content under 640px (issue #39)", () => }, }); - cy.viewport(...PHONE); + cy.setViewport("phone"); cy.visit("/"); // The grid wrapper is the scroll container, and its content is genuinely @@ -313,7 +313,7 @@ describe("Responsive breakpoint — page content under 640px (issue #39)", () => // body). cy.document() .its("documentElement.scrollWidth") - .should("be.lte", 375 + 1); + .should("be.lte", PHONE_VIEWPORT.width + 1); // The per-recipe remove "✕" is hover-gated on desktop; on touch it must // be permanently visible or a planned recipe can't be removed at all. diff --git a/apps/web/cypress/support/component.ts b/apps/web/cypress/support/component.ts index 38f5f07..14335b1 100644 --- a/apps/web/cypress/support/component.ts +++ b/apps/web/cypress/support/component.ts @@ -3,6 +3,8 @@ import { mount } from "cypress/react18"; // components (Checkbox, Radio, Dialog…) are styled through it, not their // own scoped CSS, so mounting one without it would test unstyled markup. import "../../src/styles/global.scss"; +// Registers `cy.setViewport("phone" | "tablet")` — see viewports.ts. +import "./viewports"; declare global { namespace Cypress { diff --git a/apps/web/cypress/support/e2e.ts b/apps/web/cypress/support/e2e.ts index 7ef04fe..97b10d7 100644 --- a/apps/web/cypress/support/e2e.ts +++ b/apps/web/cypress/support/e2e.ts @@ -1,3 +1,3 @@ // Cypress support file — global config and custom commands will go here // as features (and their e2e specs) get added. -export {}; +import "./viewports"; diff --git a/apps/web/cypress/support/viewports.ts b/apps/web/cypress/support/viewports.ts new file mode 100644 index 0000000..f2ce9ce --- /dev/null +++ b/apps/web/cypress/support/viewports.ts @@ -0,0 +1,30 @@ +// Named viewport presets for phone/tablet coverage — the mobile/tablet UI +// chantier's testing convention (ticket #83), shared by both the e2e and +// component support files (see e2e.ts/component.ts, each of which imports +// this once for its side effect of registering `cy.setViewport`). +// +// Sized from real, representative Android devices rather than Cypress's own +// built-in presets (`cy.viewport("iphone-x")` etc.) — this app is +// Android-only (see the root README), so an Apple-named preset would be a +// odd source of truth to borrow pixel values from. Comfortably inside +// `_breakpoints.scss`'s `$phone`/`$tablet` bands (not flush against either +// edge) so a test genuinely exercises "a phone" / "a tablet", not the exact +// boundary pixel. +export const PHONE_VIEWPORT = { width: 390, height: 844 } as const; +export const TABLET_VIEWPORT = { width: 900, height: 1200 } as const; + +export type ViewportPreset = "phone" | "tablet"; + +declare global { + namespace Cypress { + interface Chainable { + /** `cy.viewport(width, height)` for one of the mobile/tablet UI chantier's two named presets — see PHONE_VIEWPORT/TABLET_VIEWPORT above for the exact values. */ + setViewport(preset: ViewportPreset): Chainable; + } + } +} + +Cypress.Commands.add("setViewport", (preset: ViewportPreset) => { + const { width, height } = preset === "phone" ? PHONE_VIEWPORT : TABLET_VIEWPORT; + cy.viewport(width, height); +}); From d0d66bbda51d2c945f2f5807e61048b4ce3381af Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 12:47:11 +0200 Subject: [PATCH 2/6] chore: relance la CI (contention runner partage sur Postgres) 24 echecs melant timeouts 10s, "Unique constraint failed on the fields: (key)" (race d'upsert concurrent au seed) et PrismaClientInitializationError "Can't reach database server" - combinaison connue de signatures de contention sur le Postgres partage du runner (voir ci-forgejo-runner-constraints), pas une regression. Ce ticket ne touche que des fichiers Cypress/apps-web, aucun code touchant la base. From 63615656ccff7ed1ebc4ca9b9efbe211f4ace384 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 13:05:40 +0200 Subject: [PATCH 3/6] chore: relance la CI (Postgres partage coupe en cours de run) Can't reach database server at postgres:5432 / Server has closed the connection - meme signature connue de contention runner que les relances precedentes (voir ci-forgejo-runner-constraints), pas une regression. From 80c5b957d6e0abf96e1a2669fa60a2d342225bf3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 13:27:46 +0200 Subject: [PATCH 4/6] chore: relance la CI (Postgres partage coupe en cours de run) Can't reach database server at postgres:5432 / Server has closed the connection - meme signature connue de contention runner que les relances precedentes (voir ci-forgejo-runner-constraints), pas une regression. From 8d754620664ad1dea75825bc60299408c20bbeaf Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 13:45:22 +0200 Subject: [PATCH 5/6] chore: relance la CI (Postgres partage coupe en cours de run) Can't reach database server at postgres:5432 / Server has closed the connection - meme signature connue de contention runner que les relances precedentes (voir ci-forgejo-runner-constraints), pas une regression. From c39fbaeb13ca0e2711183e044586669822df04b4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 15:18:50 +0200 Subject: [PATCH 6/6] chore: relance la CI (residu mineur de contention, 460/468 passants) Le wait-step a fonctionne (~21 min d'attente puis "proceeding"), mais un petit residu de contention subsiste : 460 passing / 8 failing, meme signature connue (timeout 10s + unique constraint sur key). Nette amelioration par rapport a avant le correctif (auparavant ~280-410 passing / 14-25 failing) - la file se vide au fur et a mesure que les autres PR terminent (#93 et #96 deja mergees).