Rails: Testing file downloads with request specs

tl;dr

Prefer request specs over end-to-end tests (Capybara) to joyfully test file downloads!

Why?

Testing file downloads via Capybara is not easy and results in slow and fragile tests. We tried different approaches and the best one is just okay.

Tests for file downloads via Capybara ...

  • ... are slow,
  • ... are fragile (breaks CI, breaks if Selenium driver changes, ...),
  • ... need workarounds for your specia...

Geordi 10.0.0 released

10.0.0 2024-03-07

Compatible changes

  • console command: You can now globally disable the IRB multiline feature by setting irb_flags: --nomultiline in ~/.config/geordi/global.yml. All configured irb_flags are automatically passed on to the console IRB.
  • console command: Ctrl + C now properly exits a local Rails console
  • rspec and cucumber commands: Run specs even if the automatic chromedriver update fails
  • Improve detection of IRB version
  • Add new hints to 'Did you know'

Breaking changes

  • dump command: Drop...

Grid by Example: a website about CSS Grid

Rachel Andrew has built a website about CSS Grid.

  • Video tutorials
  • More than 30 layout examples for feature demonstration
  • Layout patterns for copy-paste use
  • All grouped by topic: "Placing items onto the grid", "Sizing of tracks and items" etc. with video, linked articles, examples each

Chaining Capybara matchers in RSpec

You can chain multiple Capybara matchers on the page or any element:

expect(page)
  .to have_content('Example Course')
  .and have_css('.course.active')
  .and have_button('Start')

When you chain multiple matchers using and, [Capybara will retry the entire chain](https://github.com/teamcapybara/capybara/blob/c0cbf4024c1abd48b0c22c2930e7b05af58ab284/lib/capybara/rspec/matc...

UX details for web developers

A list of implementation details that make for a better / expected user experience. Have these in mind when implementing a web design.

This document outlines a non-exhaustive list of details that make a good (web) interface. It is a living document, periodically updated based on learnings. Some of these may be subjective, but most apply to all websites. The WAI-ARIA spec is deliberately not duplicated in this document. However, some accessibility guidelines may be pointed out.

Accessibility: Making non-standard elements interactive

A common cause of non-accessible web pages are elements that were made interactive via JavaScript but cannot be focused or activated with anything but the mouse.

❌ Bad example

Let's take a look at a common example:

<form filter>
  <input filter--query name="query" type="text">
  <span filter--reset>Clear Search</span>
</form>

The HTML above is being activated with an Unpoly compiler like this:

up.compiler('[filter]', function(filterForm) {
  const resetButton = filterForm.querySelec...

How to display an unsaved changes alert

All browsers implement an event named beforeunload. It is fired when the active window is closed and can be used to display an alert to warn the user about unsaved changes.

To trigger the alert, you have to call preventDefault() on the event.

Note

The beforeunload event is only dispatched when the user navigation makes a full page load, or if it closes the tab entirely. It will not be dispatched when navigating via JavaScript. In this case you need to ...

In Chrome 121+ the now supported spec-compliant scrollbar properties override the non-standard `-webkit-scrollbar-*` styles

Up until Chrome 120, scrollbars could only be styled using the various -webkit-scrollbar-* pseudo elements, e.g. to make the scrollbars have no arrows, be rounded, or with additional margin towards their container.

Starting with version 121, Chrome now also supports the spec-compliant properties scrollbar-width and scrollbar-color.
These allow less styling. You may only specify the track and thumb colors, and a non-specific width like auto, thin, or none.

Heads up: You should always use "current_window.resize_to" to resize the browser window in tests

I recently noticed a new kind of flaky tests on the slow free tier GitHub Action runners: Integration tests were running on smaller screen sizes than specified in the device metrics. The root cause was the use of Selenium's page.driver.resize_window_to methods, which by design does not block until the resizing process has settled:

We discussed this issue again recent...

Use <input type="number"> for numeric form fields

Any form fields where users enter numbers should be an <input type="number">.

Numeric inputs have several benefits over <input type="text">:

  • On mobile or tablet devices, number fields show a special virtual keyboard that shows mostly digit buttons.
  • Decimal values will be formatted using the user's language settings.
    For example, German users will see 1,23 for <input type="number" value="1.23">.
  • Values in the JavaScript API or when submitting forms to the server will always use a point as decimal separator (i.e. "1.23" eve...

CI Template for GitHub Actions

Usually our code lives on GitLab, therefore our documentation for CI testing is extensive in this environment. If you are tied to GitHub e.g. because your customer uses it, you may use the following GitHub Actions template for the CI integration. It includes jobs for rspec (parallelized using knapsack, unit + feature specs), rubocop, eslint, coverage and license_finder.

Note that GitHub does not allow the use of YAML anchors and aliases. You can instead use [compos...

Firefox ESR Release Calendar

The linked table shows the support lifecycle for Firefox Extended Support Releases (ESR) which we sometimes need to support for enterprise customers.

The ESR cadence works something like this:

  • Firefox ESR freezes the then-current Firefox version for a year.
  • During this year Mozilla backports security patches to the current ESR, but does not add features.
  • Two subsequent ESR releases overlap for three months. This way enterprises have a quarter to test the new version and migrate their clients.

How to configure Selenium WebDriver to not automatically close alerts or other browser dialogs

tl;dr

We recommend configuring Selenium's unhandled prompt behavior to { default: 'ignore' } with the monkey patch below.

When running tests in a real browser, we use Selenium. Each browser is controlled by a specific driver, e.g. Selenium::WebDriver::Chrome for Chrome.

There is one quirk to all drivers (at least those following the W3C webdriver spec) that can be impractical:
When any user prompt (like an alert) is encountered when trying to perform an action, they will [...

Best practices: Writing a Rails script (and how to test it)

A Rails script lives in lib/scripts and is run with bin/rails runner lib/scripts/.... They are a simple tool to perform some one-time actions on your Rails application. A Rails script has a few advantages over pasting some prepared code into a Rails console:

  • Version control
  • Part of the repository, so you can build on previous scripts for a similar task
  • You can have tests (see below)

Although not part of the application, your script is code and should adhere to the common quality standards (e.g. no spaghetti code). However, a scri...

Bash: How to count and sort requests by IP from the access logs

Example

87.140.79.42 - - [23/Jan/2024:09:00:46 +0100] "GET /monitoring/pings/ HTTP/1.1" 200 814 "-" "Ruby"
87.140.79.42 - - [23/Jan/2024:09:00:46 +0100] "GET /monitoring/pings/ HTTP/1.1" 200 814 "-" "Ruby"
87.140.79.41 - - [23/Jan/2024:09:00:46 +0100] "GET /monitoring/pings/ HTTP/1.1" 200 814 "-" "Ruby"
87.140.79.42 - - [23/Jan/2024:09:00:46 +0100] "GET /monitoring/pings/ HTTP/1.1" 200 814 "-" "Ruby"

Goal

Count and sort the number of requests for a single IP address.

Bash Command

awk '{ print $1}' test.log | sort...

Bash: How to grep logs for a pattern and expand it to the full request

Example

I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- : [4cdad7a4-8617-4bc9-84e9-c40364eea2e4] test
I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- : [4cdad7a4-8617-4bc9-84e9-c40364eea2e4] more
I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- : [6e047fb3-05df-4df7-808e-efa9fcd05f87] test
I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- : [6e047fb3-05df-4df7-808e-efa9fcd05f87] more
I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- : [53a240c1-489e-4936-bbeb-d6f77284cf38] nope
I, [2024-01-21T06:22:17.484221 #2698200]  INFO -- ...

Rails: Testing the number of database queries

There are a few tools to combat the dreaded n+1 queries. The bullet gem notifies you of missing eager-loading, and also if there is too much eager-loading. strict_loading in Rails 6.1+ forces developers to explicitly load associations on individual records, for a single association, for an entire model, or globally for all models.

But you can also actually **write spe...

Element.animate() to animate single elements with given keyframes

Today I learned that you can animate HTML elements using the Web Animation API's method .animate(keyframes, options) (which seems to be Baseline for all browsers since 2022).

const fadeIn = [{ opacity: 0 }, { opacity: 1 }] // this is a keyframe example

const container = document.querySelector('.animate-me')

const animation = container.animate(fadeIn, 2000) // I am just using the animation duration as option here but it can also be given an object

// the animation object can be used for things like querying timings or stat...

SASS: Adding, removing and converting units

Adding a unit

Multiply by 1x the unit:

$number = 13
$length = $number * 1px // => 13px

Removing a unit

Divide by 1x the unit:

$length = 13px
$number = $length / 1px // => 13

Converting a unit

the result of an addition or subtraction between two numbers of different units is expressed in the first member’s unit

Thus, to convert a number, add it to 0 of the desired unit:

$duration: .21s
$duration-in-milliseconds: 0ms + $duration // => 210ms

An example is storing a transition duration as CS...

When loading Yaml contents in Ruby, use the :freeze argument to deep-freeze everything

Ruby methods which load from a Yaml file, like YAML.safe_load or YAML.safe_load_file, support passing freeze: true to deep-freeze the entire contents from the Yaml file.
This is available by default on Ruby 3.0 and newer. On older Rubies, you can install psych 3.2.0 or newer for :freeze support.

As an example, consider the following Yaml file:

---
message:
  - hello
  - universe
foo:
  bar:
    baz: "example"

We can now load it as usual, but pass freeze: true.

>> test = YAML.safe_load_file('example.yml', fre...

Creating a self-signed certificate for local HTTPS development

Your development server is usually running on an insecure HTTP connection which is perfectly fine for development.

If you need your local dev server to be accessible via HTTPS for some reason, you need both a certificate and its key. For a local hostname, you need to create those yourself.
This card explains how to do that and how to make your browser trust the certificate so it does not show warnings for your own certificate.

Easy: self-signed certificate

To just create a certificate for localhost, you can use the following command....

Maintaining custom application tasks in Rails

Here are some hints on best practices to maintain your tasks in larger projects.

Rake Tasks vs. Scripts

  • The Rails default is using rake tasks for your application tasks. These live in lib/tasks/*.
  • In case you want to avoid rake for your tasks and just use plain ruby scripts, consider lib/scripts/* as folder.

Keeping tasks slim

For readability and testing it's easier to keep your tasks slim. We suggest to use folders inside the tasks or scripts folder.

Example for a task:

The slim task lib/tasks/gitlab.rb:

Rails: Using custom config files with the config_for method

You can use the config.x configuration in combination with config_for to configure global settings for your Rails 4.2+ application.

Example

In your config/application.rb assign the settings from e.g. config/settings.yml as follows:

module FooApplication
  class Application < Rails::Application
    config.x.settings = config_for(:settings)
  end
end

The config/settings.yml might look as follows:

shared: &shared
  email: info@example.com
...

Disable PostgreSQL's Write-Ahead Log to speed up tests

The linked article suggests an interesting way to speed up tests of Rails + Postgres apps:

PostgreSQL allows the creation of “unlogged” tables, which do not record data in the PostgreSQL Write-Ahead Log. This can make the tables faster, but significantly increases the risk of data loss if the database crashes. As a result, this should not be used in production environments. If you would like all created tables to be unlogged in the test environment you can add the following to your...