ScreenshotNeo

BlogGuides

Selenium WebDriver Ruby Project Directory and File Structure

A practical Selenium WebDriver Ruby directory layout, setup files, test runners, page objects, lifecycle hooks, and troubleshooting guidance.

By the ScreenshotNeo team30 September 20269 min read

Selenium WebDriver Ruby Project Directory and File Structure

Direct answer: A maintainable Selenium WebDriver Ruby project usually starts with a root Gemfile, an optional runner configuration, and tests under spec/ or test/. Put shared setup in spec/spec_helper.rb (or an equivalent helper), reusable page objects in pages/, and cross-cutting helpers in support/. This is a practical convention, not a directory tree required by Selenium.

Selenium’s official Ruby example uses Bundler, RSpec, a shared helper, a Chrome driver created in a before hook, and quit in cleanup. Selenium also identifies Minitest as a valid Ruby runner. See the official installation guide and code-organization guidance.

my_selenium_project/
├── Gemfile
├── Gemfile.lock
├── .rspec                  # optional RSpec defaults
├── Rakefile                # optional task shortcuts
├── spec/
│   ├── spec_helper.rb      # shared driver setup and teardown
│   ├── example_spec.rb
│   └── pages/              # optional: page objects used by specs
├── pages/                  # optional alternative location for page objects
├── support/                # optional shared waits, data, and helpers
├── screenshots/            # optional failure artifacts (often gitignored)
└── tmp/                    # optional local run output (gitignored)

For a one-off automation script, a single Ruby file and a Gemfile are enough. Add folders when repetition, test count, or team ownership makes separation useful. Do not create empty layers simply to match a template.

What each file is for

Path Purpose When to add it
Gemfile Declares selenium-webdriver and development dependencies. Any Bundler-managed project.
spec/ RSpec examples and their helper. When using RSpec.
test/ Minitest files and helper code. When using Minitest instead of RSpec.
spec_helper.rb Creates drivers, configures shared behavior, and guarantees cleanup. When multiple examples share setup.
pages/ Page-object classes that hide locators and browser actions. When the same page interactions appear in several tests.
support/ Wait helpers, environment loading, API fixtures, and custom matchers. When helpers do not belong to one page.
.rspec Stores RSpec command-line defaults such as requiring the helper. When you want consistent local and CI commands.
Rakefile Provides repeatable tasks such as rake test. When the project has multiple checks or CI entry points.
A typical Ruby Selenium run connects setup, browser actions, assertions, and failure artifacts.
A typical Ruby Selenium run connects setup, browser actions, assertions, and failure artifacts.

Set up the Ruby project

1. Check Ruby and create the project

The current Selenium Ruby bindings README states that MRI Ruby 3.3 or newer is supported. Verify the requirement against the Selenium version you select because compatibility changes over time. The same documentation says Selenium Manager handles browser-driver installation for a basic setup.

ruby --version
mkdir my_selenium_project
cd my_selenium_project
bundle init

2. Define dependencies in Gemfile

source 'https://rubygems.org'

gem 'selenium-webdriver'
gem 'rspec'
gem 'rake'

Install and initialize RSpec:

bundle install
bundle exec rspec --init

The generated files can be kept at the project root. If you prefer an explicit helper load, put this in .rspec:

--require spec_helper
--format progress

Minimal runnable RSpec structure

spec/spec_helper.rb

require 'selenium-webdriver'

RSpec.configure do |config|
  config.before(:each) do
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument('--headless=new')
    options.add_argument('--window-size=1440,1000')
    @driver = Selenium::WebDriver.for(:chrome, options: options)
  end

  config.after(:each) do |example|
    if example.exception && @driver
      FileUtils.mkdir_p('screenshots')
      @driver.save_screenshot("screenshots/#{example.full_description.gsub(/[^0-9A-Za-z_-]+/, '_')}.png")
    end
    @driver&.quit
  end
end

Add require 'fileutils' at the top if you use the failure-screenshot block:

require 'fileutils'

spec/example_spec.rb

require 'spec_helper'

RSpec.describe 'Selenium Ruby project' do
  it 'opens a page and checks its title' do
    @driver.get('https://example.com')
    expect(@driver.title).to eq('Example Domain')
  end
end

Run it with:

bundle exec rspec

The official Selenium example creates a driver before each example and quits it afterward. The Ruby bindings quick start also demonstrates using ensure so cleanup runs after an error. Both patterns prevent orphaned browser processes.

One-off script layout

A script does not need RSpec:

require 'selenium-webdriver'

driver = Selenium::WebDriver.for(:chrome)
begin
  driver.get('https://example.com')
  puts driver.title
ensure
  driver.quit
end

Use this shape for a small diagnostic or scheduled task. Move setup into a runner helper when several scripts need the same browser options, authentication, waits, or reporting.

Page objects and shared support

Page objects keep selectors and page-specific actions out of examples. They should expose user-level operations rather than turning every test into a collection of CSS selectors.

# pages/login_page.rb
class LoginPage
  def initialize(driver)
    @driver = driver
  end

  def open
    @driver.get('https://example.com/login')
    self
  end

  def sign_in(email, password)
    @driver.find_element(css: '[name=email]').send_keys(email)
    @driver.find_element(css: '[name=password]').send_keys(password)
    @driver.find_element(css: 'button[type=submit]').click
  end
end
# spec/login_spec.rb
require 'spec_helper'
require_relative '../pages/login_page'

RSpec.describe 'login' do
  it 'allows a valid user to sign in' do
    LoginPage.new(@driver).open.sign_in('user@example.test', 'secret')
    expect(@driver.current_url).to include('/dashboard')
  end
end

Put generic code such as an explicit wait wrapper, environment-variable parsing, or test data factories under support/. Avoid putting credentials in page objects or checked-in files; read them from CI secrets or environment variables.

RSpec versus Minitest

Selenium documents both RSpec and Minitest. RSpec supplies the before, after, and example structure shown above. Minitest is a lightweight alternative and can use the same driver lifecycle:

require 'minitest/autorun'
require 'selenium-webdriver'

class ExampleTest < Minitest::Test
  def setup
    @driver = Selenium::WebDriver.for(:chrome)
  end

  def teardown
    @driver&.quit
  end

  def test_title
    @driver.get('https://example.com')
    assert_equal 'Example Domain', @driver.title
  end
end

Choose the runner already familiar to your team, the conventions used by the repository, and the hooks you need. Selenium does not publish a benchmark or declare one runner universally best.

Configuration patterns that scale

Keep environment-specific settings outside tests

# support/browser.rb
require 'selenium-webdriver'

module Browser
  def self.build
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument('--headless=new') if ENV.fetch('HEADLESS', 'true') == 'true'
    options.add_argument("--window-size=#{ENV.fetch('VIEWPORT', '1440,1000')}")
    Selenium::WebDriver.for(:chrome, options: options)
  end
end

Require this module from the runner helper and use Browser.build. Keep URLs, credentials, and browser flags in environment variables or CI configuration.

Wait for state, not arbitrary time

Prefer an explicit wait for a condition over a long sleep. A short delay can still be useful for a known animation, but sleeps make suites slower and less reliable when machines vary.

wait = Selenium::WebDriver::Wait.new(timeout: 10)
button = wait.until do
  element = @driver.find_element(css: 'button.submit')
  element if element.displayed? && element.enabled?
end
button.click

Parallel runs and artifacts

If CI runs examples in parallel, create a separate driver per worker and include the worker identifier in screenshot and log filenames. Never share one driver instance between concurrent examples. Store failure screenshots and page source as build artifacts, and clean generated files from version control with .gitignore.

Common errors and fixes

Error or symptom Likely cause Fix
cannot load such file -- selenium-webdriver Bundler did not install the gem or the command bypassed Bundler. Run bundle install and execute with bundle exec.
Browser driver session cannot start Browser, Ruby, Selenium, or platform versions are incompatible. Check the Selenium Ruby support note, update compatible components, and inspect the first driver error line.
Driver processes remain after tests An exception bypassed cleanup. Use RSpec after hooks or an ensure block and call @driver&.quit.
ElementNotInteractableError The element is hidden, disabled, covered, or not yet ready. Wait for visibility and enabled state; scroll or close an overlay; verify the selector.
NoSuchElementError The selector is wrong, the page changed, or content is inside an iframe. Inspect page source, wait for the element, and switch to the correct frame before locating it.
Tests pass locally but fail in CI Different viewport, headless behavior, timing, locale, or missing environment variables. Set explicit window size and browser flags, use condition waits, and log the URL, title, and relevant environment configuration.
Authentication leaks between examples Cookies or local storage are reused. Create a fresh driver per example or clear state deliberately in setup.
Scraping requests are blocked The target detects automation or its terms prohibit automated access. Review the site’s terms and robots guidance, reduce request load, and use an allowed access method. Selenium documentation cautions that some sites block Selenium.
ScreenshotNeo clears common consent and overlay elements before capture.
ScreenshotNeo clears common consent and overlay elements before capture.

Performance, reliability, and cost

  • Startup: Creating a browser for every example gives isolation but costs time. Grouping related checks can reduce startup overhead when state isolation remains safe.
  • Selectors: Stable attributes such as data-testid are less fragile than deeply nested CSS paths.
  • waits: Explicit condition waits reduce both flaky failures and unnecessary fixed delays.
  • Artifacts: Capture screenshots and HTML only on failure to keep CI storage and transfer small.
  • Parallelism: More workers can reduce wall-clock time, but each browser consumes CPU and memory; size CI runners accordingly.
  • Dependencies: Commit Gemfile.lock for repeatable application or CI builds, and review Selenium and browser updates deliberately.
  • Cost: Selenium itself is an open-source library; your main operational costs are browser infrastructure, CI minutes, storage, and any external service used for capture or testing.

Or skip the browser setup

If your goal is a clean screenshot rather than an interactive browser test, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options. The same endpoint supports full-page or element capture, dark mode, device presets, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture, and usage reporting.

cURL

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
    timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

An MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Practical checklist

  • Declare Selenium and the selected runner in Gemfile.
  • Choose spec/ for RSpec or test/ for Minitest.
  • Centralize driver creation and guaranteed cleanup.
  • Use explicit waits and stable selectors.
  • Move repeated interactions into page objects.
  • Keep secrets and environment-specific values outside source files.
  • Save failure artifacts and isolate parallel workers.
  • Commit the lockfile when reproducibility matters.
  • Review target-site terms before scraping or automated access.

FAQ

Does Selenium require a specific directory structure?

No. Selenium documents setup and examples, while directory names such as spec/, pages/, and support/ are project conventions.

Should page objects be under spec/ or at the root?

Either works. Keep them where your load paths and team conventions make ownership clear; consistency matters more than the exact location.

Can I use Selenium without RSpec?

Yes. A plain Ruby script or Minitest suite can use the same WebDriver API and must still guarantee driver cleanup.

Do I need to download ChromeDriver manually?

The current Selenium Ruby bindings documentation says Selenium Manager automatically handles browser-driver installation. Verify behavior for the Selenium and browser versions in your environment.

When should I add a support/ directory?

Add it when shared waits, configuration, data builders, or reporting helpers are used by multiple tests. A small script does not need it.