test(cypress): convention de viewports telephone/tablette
Some checks failed
Some checks failed
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 <noreply@anthropic.com>
This commit is contained in:
parent
0ecd7c1c84
commit
967be6df4a
5 changed files with 90 additions and 11 deletions
47
apps/web/cypress/component/Dialog.cy.tsx
Normal file
47
apps/web/cypress/component/Dialog.cy.tsx
Normal file
|
|
@ -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(
|
||||||
|
<Dialog title="Titre" onClose={() => {}}>
|
||||||
|
Contenu
|
||||||
|
</Dialog>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Dialog title="Titre" onClose={() => {}}>
|
||||||
|
Contenu
|
||||||
|
</Dialog>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
// width (some stuck to the left edge with a lopsided gap, others correctly
|
// width (some stuck to the left edge with a lopsided gap, others correctly
|
||||||
// full-bleed).
|
// full-bleed).
|
||||||
|
|
||||||
|
import { PHONE_VIEWPORT } from "../support/viewports";
|
||||||
|
|
||||||
const authenticatedProfile = {
|
const authenticatedProfile = {
|
||||||
id: 1,
|
id: 1,
|
||||||
firstName: "Alice",
|
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", () => {
|
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("/");
|
cy.visit("/");
|
||||||
|
|
||||||
// The desktop sidebar isn't just hidden — it's not in the DOM at all.
|
// 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) => {
|
cy.get(".app-bottom-nav").should(($nav) => {
|
||||||
const rect = $nav[0].getBoundingClientRect();
|
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.
|
// A short bar flush against the bottom edge, not a tall rail.
|
||||||
expect(rect.height).to.be.lessThan(120);
|
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");
|
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");
|
cy.get(".bottom-sheet").should("not.exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the desktop sidebar (not the bottom nav) above the phone breakpoint", () => {
|
it("keeps the desktop sidebar (not the bottom nav) at a tablet viewport", () => {
|
||||||
cy.viewport(1000, 700);
|
cy.setViewport("tablet");
|
||||||
cy.visit("/");
|
cy.visit("/");
|
||||||
|
|
||||||
cy.get(".app-sidebar").should("exist");
|
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)", () => {
|
describe("Responsive breakpoint — page content under 640px (issue #39)", () => {
|
||||||
const PHONE: [number, number] = [375, 812];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
interceptAuth();
|
interceptAuth();
|
||||||
});
|
});
|
||||||
|
|
@ -266,7 +266,7 @@ describe("Responsive breakpoint — page content under 640px (issue #39)", () =>
|
||||||
expect(s.paddingLeft).to.equal("32px");
|
expect(s.paddingLeft).to.equal("32px");
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.viewport(...PHONE);
|
cy.setViewport("phone");
|
||||||
cy.visit("/");
|
cy.visit("/");
|
||||||
cy.get(".app-content").should(($el) => {
|
cy.get(".app-content").should(($el) => {
|
||||||
const s = getComputedStyle($el[0]);
|
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("/");
|
cy.visit("/");
|
||||||
|
|
||||||
// The grid wrapper is the scroll container, and its content is genuinely
|
// 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).
|
// body).
|
||||||
cy.document()
|
cy.document()
|
||||||
.its("documentElement.scrollWidth")
|
.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
|
// 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.
|
// be permanently visible or a planned recipe can't be removed at all.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { mount } from "cypress/react18";
|
||||||
// components (Checkbox, Radio, Dialog…) are styled through it, not their
|
// components (Checkbox, Radio, Dialog…) are styled through it, not their
|
||||||
// own scoped CSS, so mounting one without it would test unstyled markup.
|
// own scoped CSS, so mounting one without it would test unstyled markup.
|
||||||
import "../../src/styles/global.scss";
|
import "../../src/styles/global.scss";
|
||||||
|
// Registers `cy.setViewport("phone" | "tablet")` — see viewports.ts.
|
||||||
|
import "./viewports";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
// Cypress support file — global config and custom commands will go here
|
// Cypress support file — global config and custom commands will go here
|
||||||
// as features (and their e2e specs) get added.
|
// as features (and their e2e specs) get added.
|
||||||
export {};
|
import "./viewports";
|
||||||
|
|
|
||||||
30
apps/web/cypress/support/viewports.ts
Normal file
30
apps/web/cypress/support/viewports.ts
Normal file
|
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("setViewport", (preset: ViewportPreset) => {
|
||||||
|
const { width, height } = preset === "phone" ? PHONE_VIEWPORT : TABLET_VIEWPORT;
|
||||||
|
cy.viewport(width, height);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue