From 72bfc62d1d4913aae13f11bb4da3003a9ff786f0 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Thu, 22 Oct 2026 14:22:16 +1300 Subject: [PATCH] Add mobile layout tests and overflow helpers for better responsiveness --- spec/features/mobile_layout_spec.rb | 41 ++++++++++++++++++ spec/support/overflow_helpers.rb | 66 +++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 spec/features/mobile_layout_spec.rb create mode 100644 spec/support/overflow_helpers.rb diff --git a/spec/features/mobile_layout_spec.rb b/spec/features/mobile_layout_spec.rb new file mode 100644 index 000000000..730c18f2b --- /dev/null +++ b/spec/features/mobile_layout_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Layout on a phone', :js, :phone do + subject { page } + + let(:member) { create(:member) } + + before { resize_to_phone } + + shared_examples 'fits the screen' do + it { is_expected.to have_no_horizontal_overflow } + end + + context 'with the signed out home page' do + before { visit root_path } + + it_behaves_like 'fits the screen' + end + + context 'with a veteran gardener\'s dashboard' do + before do + create(:planting, owner: member, garden: member.gardens.first, planted_at: 3.years.ago) + create(:harvest, owner: member, harvested_at: 1.month.ago) + login_as member + visit root_path + end + + it_behaves_like 'fits the screen' + end + + context 'with the crops list' do + before do + create(:tomato) + visit crops_path + end + + it_behaves_like 'fits the screen' + end +end diff --git a/spec/support/overflow_helpers.rb b/spec/support/overflow_helpers.rb new file mode 100644 index 000000000..cdff7d84b --- /dev/null +++ b/spec/support/overflow_helpers.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Catches pages that are wider than a phone screen. A page overruns when +# something sticks out past the viewport's right edge, which makes the whole +# page scroll sideways. The matcher names the elements responsible, so a +# failure points at the markup to fix. +# +# resize_to_phone +# visit root_path +# expect(page).to have_no_horizontal_overflow +module OverflowHelpers + # Pixel 7, the phone the overrun in #4924 was reported on. + PHONE = { width: 412, height: 915 }.freeze + + # Elements whose right edge is past the viewport, unless they are hidden + # (such as the closed menu drawer) or an ancestor clips or scrolls them + # (overflow other than visible), as that overflow is contained. Reports the + # outermost offenders only. + FIND_OVERFLOWING = <<~JS + const limit = document.documentElement.clientWidth; + const clipped = (el) => { + for (let p = el.parentElement; p; p = p.parentElement) { + if (getComputedStyle(p).overflowX !== 'visible') return true; + } + return false; + }; + const offenders = [...document.body.querySelectorAll('*')].filter((el) => { + const box = el.getBoundingClientRect(); + return box.width > 0 && box.right > limit + 1 && getComputedStyle(el).visibility !== 'hidden' && !clipped(el); + }); + return offenders + .filter((el) => !offenders.includes(el.parentElement)) + .map((el) => { + const id = el.id ? `#${el.id}` : ''; + const classes = [...el.classList].map((name) => `.${name}`).join(''); + return `${el.tagName.toLowerCase()}${id}${classes} (right edge ${Math.round(el.getBoundingClientRect().right)}px of ${limit}px)`; + }); + JS + + def resize_to_phone + page.driver.browser.manage.window.resize_to(PHONE[:width], PHONE[:height]) + end + + def horizontal_overflow + page.evaluate_script("(() => { #{FIND_OVERFLOWING} })()") + end +end + +RSpec::Matchers.define :have_no_horizontal_overflow do + match do |page| + @offenders = page.evaluate_script("(() => { #{OverflowHelpers::FIND_OVERFLOWING} })()") + @offenders.empty? + end + + failure_message do + "expected page to fit the viewport width, but these overrun it:\n #{@offenders.join("\n ")}" + end +end + +RSpec.configure do |config| + config.include OverflowHelpers, type: :feature + config.after(:each, :phone) do + # Back to the desktop size the :js hook sets, as browser sessions are reused. + page.driver.browser.manage.window.resize_to(1920, 1080) + end +end