Ruby: Retrieving and processing files via Selenium and JavaScript

This card shows an uncommon way to retrieve a file using selenium where JavaScript is used to return a binary data array to Ruby code.

The following code example retrieves a PDF but the approach also works for other file types.

require "selenium-webdriver"

selenium_driver = Selenium::WebDriver.for :chrome
selenium_driver.navigate.to('https://example.com')
link_to_pdf = 'https://blobs.example.com/random-pdf'

binary_data_array = selenium_driver.execute_script(<<-JS, link_to_pdf)
  const response = await fetch(arguments[0])

  if (!r...

Terser is good at minifying JavaScript

Terser is a really good minifier ("compressor") for JavaScript code. I'm often surprised by the thoughtfulness of its compressed output.

Let's take this function:

function fn() {
  if (a) {
    return 'foo'
  } else if (b) {
    return 'foo'
  } else {
    return c()
  }
}

console.log(fn())

Terser will reduce this to the following code:

console.log(a||b?"foo":c())

Note how:

  • The if statement has been replaced by a tertiary expression. This is often less readable, but it doesn't matter in c...

Getter and setter functions for JavaScript properties

JavaScript objects can have getter and setter functions that are called when a property is read from or written to.

For example, if you'd like an object that has a virtual person.fullName attribute that dynamically composes person.firstName and person.lastName:

var person = {

  firstName: 'Guybrush',

  lastName: 'Threepwood',

  get fullName() {
    return this.firstName + " " + this.lastName;
  },
  
  set fullName(name) {
    var parts = name.split(" ");
    this.firstName = parts[0];
    this.lastName = parts[1];
  }

};
`...

Shorthand function properties in ES6

Here is an ES5 object literal with two string properties and a function property:

let user = { 
  firstName: 'Max',
  lastName: 'Muster',
  fullName: function() { return this.firstName + ' ' + this.lastName }
}

user.fullName() // => 'Max Muster'

In ES6 we can define a function property using the following shorthand syntax:

let user = { 
  firstName: 'Max',
  lastName: 'Muster',
  fullName() { return this.firstName + ' ' + this.lastName }
}

user.fullName() // => 'Max Muster'

We can also define a gette...

Firefox cancels any JavaScript events at a fieldset[disabled]

If you try to listen to events on elements that are nested inside a <fieldset disabled>, Firefox will stop event propagation once the event reaches the fieldset. Chrome and IE/Edge will propagate events.

Since we often bind event listeners to document this can be annoying.

You could solve it by...

Using regular expressions in JavaScript

Regular expressions in Javascript are represented by a RegExp object. There also is a regex literal as in many other languages: /regex/. However, they are used slightly differently.

Regex literal

  • Usage: /foo+/
  • Shorthand for creating a regular expression object

RegExp() object

  • Usage: RegExp("foo+") or new RegExp("foo+")
  • No surrounding slashes required (they're the literal markers)
  • Since the argument is a string, backslashes need to be escaped as well: RegExp("\\d+")

Gotchas

  • Regex objects [never eq...

JavaScript: Testing whether the browser is online or offline

You can use the code below to check whether the browser can make connections to the current site:

await isOnline() // resolves to true or false

The code

The isOnline() function below checks if you can make real requests by re-fetching your site's favicon. If the favicon cannot be downloaded within 6 seconds, it considers your connection to be offline.

async function isOnline({ path, timeout } = {}) {
  if (!navigator.onLine) return false

  path ||= document.querySelect...

Simple debounce in vanilla JavaScript

Debouncing a method call delays its execution until after a specified time has passed.
If it's called again before that time has passed, its execution is delayed again.

This technique is commonly used to improve performance when code would be run more often than it needs to.
One example for that are scroll event handlers in JavaScript: You want to react to a user scrolling, but it's enough to do that when they have stopped scrolling.

Here is a small JavaScript function that you can use for that:

function debounce(callback...

JavaScript: Calling a function with a variable number of arguments

This card describes how to pass an array with multiple element to a JavaScript function, so that the first array element becomes the first function argument, the second element becomes the second argument, etc.

Note how this is different from passing the entire array as the first argument. Compare these two different ways of calling fun() in Ruby:

# Ruby
array = [1, 2, 3]
fun(array)  # same as fun([1, 2, 3]) (1 argument)
fun(*array) # same as fun(1, 2, 3)   (3 arguments)

Depending on your culture the spreading of array eleme...

esbuild: Compressing JavaScript harder with Terser

esbuild comes with a minifier that is good enough for most cases. If you're looking to squeeze out as many bytes as possible, you can consider compressing with Terser instead.

Using Terser will increase your build times significantly, but produce the smallest output:

| | Terser (3 pass) | Terser (1 pass) | esbuild |
|----------------------------|-----------------------|------------------|-------...

Operators "in" and "of" are very inconsistent between CoffeeScript and JavaScript

CoffeeScript and JavaScript (ECMAScript) both have operators in and of. Each language use them for more than one purpose. There is not a single case where the same operator can be used for the same purpose in both languages.

Check if an object (or its prototype) has a property

CoffeeScript

var hasFoo = 'foo' of object

JavaScript

var hasFoo = 'foo' in object;

Iterate through all properties of an object
================================...

JavaScript: Detecting the end of native smooth scrolling

When you use native smooth scrolling there is no built-in method to detect the end of the scrolling animation. Methods like scrollTo() don't return a promise. We may eventually get a scrollend event, but that is still some time away.

Until then I'm using the ...

tesseract.js: Pure Javascript OCR for 62 Languages

This might be relevant for us since we're often managing customer documents in our apps.

I played around with the library and this is what I found:

  • A 200 DPI scan of an English letter (500 KB JPEG) was processed in ~6 seconds on my desktop PC. It does the heavy lifting in a Web worker so you're rendering thread isn't blocked.
  • It detected maybe 95% of the text flawlessly. It has difficulties with underlined text or tight table borders.
  • When you feed ...

How to accept or deny JavaScript confirmation dialogs in Capybara/Selenium

These methods are available to you:

page.driver.browser.switch_to.alert.accept
page.driver.browser.switch_to.alert.dismiss
page.driver.browser.switch_to.alert.text # the confirmation text

Spreewald gives you steps like these:

When I confirm the browser dialog
When I cancel the browser dialog

Note that recent versions of Selenium will automatically dismiss confirmation dialogs. [Here is how to fix that](https://makandracards.com/makandra/617366-configure-selenium-webdriv...

How to find child nodes that match a selector with JavaScript

Using querySelector or querySelectorAll in JavaScript, you can easily find descendants of a node that match a given selector.

But what if you want to find only children (i.e. direct descendants) of an element?
Easy: use :scope. It references the element on which DOM API methods are being called:

element.querySelectorAll(':scope > .your-selector')

Example

Consider this HTML

<body>
  <div id="container1">
    <div id="container1a">foo</div>
    <div id="container1b">bar</div>
    <div id="container1c">baz</...

Setting expiry dates for images, JavaScript and CSS

When deploying Rails applications you might have noticed that JS and CSS are not cached by all browsers.

In order to force Apache to add expiry dates to its response, add the attached .htaccess to the public directory. This will add a header such as Expires: Thu, 07 Oct 2010 07:21:45 GMT to the httpd response.

Configuring Apache

Check that you have mod_expires enabled. You need it for the attached .htaccess to work:

sudo a2enmod expires

Configuring Nginx

You can add this:

Javascript: Avoid using innerHTML for unsafe arguments

Make sure that you use the correct property when editing an HTML attribute. Using innerHTML with unsafe arguments makes your application vulnerable to XSS.

  • textContent: Sets the content of a Node (arguments are HTML-safe escaped)
  • innerHTML: Sets the HTML of an Element (arguments are not escaped and may not contain user content)

Hierarchy

This hierarchy gives you a better understanding, where the textContent and the innerHTML properties are defined. It also includes (just for completeness) the innerText property, whi...

JavaScript: Listening to a class getting added

Reacting on a class getting added can be done with a mutation observer. Example:

const items = document.querySelectorAll('.item')
const expectedClass = 'active'
const activeObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.target.classList.contains(expectedClass) {
      // Do something
    }
  })
})
items.forEach(item => activeObserver.observe(item, { attributes: true, attributeFilter: ['class'] }))

Note that this is not a generic solution – it makes a few assumptions to simplif...

Managing vendor libraries with the Rails asset pipeline

The benefit of the Rails asset pipeline is that it compiles your stylesheets and javascripts to a single file, respectively. However, the consequences are startling if you don't understand them. Among others, the raw asset pipeline requires you to have all your asset libraries in the same folder, which quickly becomes confusing as your set of assets grows. To overcome this, we have two different solutions.

Custom solution

We are using a custom workaround to keep library files apart in their own directories. To avoid b...

JavaScript: New Features in ES2021

tl;dr

With ES2021 you now can use str.replaceAll(), Promise.any(), logical assignment operators, numeric separators and WeakRef on all major browsers except IE11.

replaceAll

JavaScript's replace(searchValue, replaceValueOrFn) by default replaces only the first match of a given String or RegExp.
When supplying a RegExp as the searchValue argument, you can specify the g ("global") modifier, but you have to remember doing that, hence using replace when you expect global replacement is prone to errors.
When supplying st...

How to read the current breakpoint tier(s) in JavaScript

To read the current breakpoint tier in JavaScript, employ this CSS:

:root {
  --current-breakpoint-tier: xs;
  
  @media (min-width: $screen-sm-min) {
    --current-breakpoint-tier: sm;
  }
  @media (min-width: $screen-md-min) {
    --current-breakpoint-tier: md;
  }
  @media (min-width: $screen-lg-min) {
    --current-breakpoint-tier: lg;
  }
  @media (min-width: $screen-xl-min) {
    --current-breakpoint-tier: xl;
  }
  @media (min-width: $screen-xxl-min) {
    --current-breakpoint-tier: xxl;
  }
}

Then use this JavaScript:

Adding Jasmine JavaScript specs to a Webpack(er) project

The goal is to get Jasmine specs running in a Rails project using Webpacker, with the browser based test runner. Should be easily adaptable to a pure Webpack setup.

Image

Step 1: Install Jasmine

yarn add jasmine-core

Step 2: Add two separate packs

Since we do not want to mix Jasmine into our regular Javascript, we will create two additional packs. The first only contains Jasmine and the test runner. The second will contain our normal application code and the specs themselves.

We cannot...

Jasmine: Expecting objects as method invocation arguments

To check if a method has been called in Jasmine, you first need to spy on it:

let spy = spyOn(window, 'alert')
codeThatAlerts()
expect(window.alert).toHaveBeenCalledWith('Important message')

To expect an object of a given type, pass the constructor function to jasmine.any():

expect(spy).toHaveBeenCalledWith(jasmine.any(Object))
expect(spy).toHaveBeenCalledWith(jasmine.any(String))
expect(spy).toHaveBeenCalledWith(jasmine.any(Number))

To expect an object with given key/value properties, use `jasmine.objectContaining(...

JavaScript has a native event emitter

Suppose you want to implement a publish/subscribe pattern in your Frontend application to react to data changes and events. First, you might be looking for an event emitter library.

Today I learned that vanilla JavaScript comes with a native event emitter, which I've been indirectly using forever. There's no need for extra code!

const emitter = new EventTarget()

emitter.addEventListener('YOLO', () => {
  console.log('YOLO');
})

// invoke attached event listeners
emitter.dispatchEvent(new Event('YOLO'));