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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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);
+});