An RSpec matcher for validating Rails <meta/> tags

While writing a project for Wilcox Tech that we hope to open source soon, I had reason to test the content (or, in some cases, presence) of <meta/> tags in a Rails app.

I came across a good start in dB.’s blog article, Custom RSpec Meta Tag Validator. However, the article was from 2013, did not follow the modern RSpec protocol, and the matcher felt a bit off for my purposes.

What I really wanted was a matcher that felt like the built-in ones to Rails, such as assert_dom or assert_select. So, using dB.’s XPath query as a starting point, I wrote a new one.

I am under the assumption that dB.’s snippet is licensed CC-BY as is the rest of his post. I am therefore licensing my matcher under a dual-license, as I believe this to be legal: CC-BY with my authorship, or MIT (the standard license of most Rails-related Ruby Gems). To fully comply, you will need to acknowledge him as well.

Usage looks like this:

  describe 'meta product properties' do
before { assign(:item, create(:item, description: 'This is a test description.', price: 50.50)) }

it 'has the correct Open Graph type' do
render_item
expect(rendered).to have_meta 'og:type', 'product'
end

it 'has the description set' do
render_item
expect(rendered).to have_meta 'og:description', 'This is a test description'
end

it 'has the price set' do
render_item
expect(rendered).to have_meta 'product:price.amount', '50.50'
end
end

describe 'meta image properties' do
context 'with no photos' do
it 'does not have an og:image property' do
assign(:item, create(:item))
render_item
expect(rendered).not_to have_meta 'og:image'
end
end

context 'with a photo' do
before do
item = create(:item)
photo = mock_item_photo_for item
photo.description = ‘My Photo Description’
assign(:item, item)
end

it 'has an og:image property' do
render_item
expect(rendered).to have_meta 'og:image'
end

it 'has the og:image:alt property set to the photo description' do
render_item
expect(rendered).to have_meta 'og:image:alt', 'My Photo Description'
end
end
end

And here’s my spec/support/matchers/have_meta.rb:

# frozen_string_literal: true

class HaveMeta
attr_accessor :expected, :actual, :key

def initialize(*args)
raise ArgumentError, 'Need at least one argument' if args.empty?

@key = args.shift
@expected = args.shift
end

def diffable?
@expected.present?
end

def matches?(page)
meta = page.html.at_xpath("//head/meta[@property='#{@key}' or @name='#{@key}']")
return false if meta.blank?

return true if @expected.blank?

@actual = meta['content']
@actual.include? @expected
end

def failure_message
return "expected meta property '#{key}'" if @expected.blank?

"expected '#{key}' to contain '#{@expected}' in '#{@actual}'"
end

def failure_message_when_negated
return "expected not to find meta property '#{key}'" if @expected.blank?

"expected '#{key}' to not contain '#{@expected}' in '#{@actual}'"
end
end

# Determines if the rendered page has a given meta tag.
def have_meta(*args)
HaveMeta.new(*args)
end

Using MailWrap on macOS Monterey

Apple’s Mail Plug-In system is quite amazing, and has led to some innovative and brave developments as an MUA (Mail User Agent). These brave developments include: wrapping long lines like a decent MUA, quoting messages in replies correctly like a decent MUA, and an option to Wrap Text like a decent MUA.

I actually forgot to install MailWrap on my M1 when I built it up, and haven’t noticed mostly because I haven’t been posting to mailing lists lately. However, I feel deep personal shame for posting on lkml without remembering to install it first. Look at those long lines!

So I set out to install MailWrap. It certainly is a lot more difficult than it used to be.

#1: Allowing the installer to access ~/Library/Mail

The first time I tried to install MailWrap, I received the unhelpful message that access to ~/Library/Mail/Bundles was denied. This is because I had to grant Terminal the Full Disk Access permission.

You can do this in System Preferences under Security & Privacy. You’ll be helpfully reminded that you have to restart Terminal. Hope you don’t have six active SSH connections open, like I did!

#2: Using the correct UUID

Now we need to add the correct UUID to the Info.plist file. Open ~/Library/Mail/Bundles/MailWrap.mailbundle/Contents/Info.plist in your favourite plain-text editor. Scroll to where you’ll find “Supported10.16PluginCompatibilityUUIDs” and then add the following lines under the “</array>” line:

        <key>Supported12.2PluginCompatibilityUUIDs</key>
<array>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
</array>

Note that this says “12.2”; when 12.3 comes out, we will need to change this again.

#3: Sign and allow the bundle to run

Gatekeeper will try to keep you safe from untrusted code, which is generally a good thing. We can sign our bundle now:

$ cd ~/Library/Mail/Bundles
$ codesign -f -s - MailWrap.mailbundle

And now that it is signed properly, we can tell Gatekeeper to trust the signature:

$ sudo spctl --add --label “MailExtensions” MailWrap.mailbundle
$ sudo spctl --enable --label “MailExtensions”

Troubleshooting

Incompatible Plug-ins Disabled.  Mail has disabled the following plug-ins: MailWrap.mailbundle Contact the makers of these plug-ins for versions that are compatible with Mail 15.0.
Incompatible Plug-ins Disabled

If you receive this Incompatible Plug-ins Disabled message, then something has gone wrong with your UUIDs. You’ll need to try again and make sure that you’ve pasted those lines in the correct spot.

“MailWrap.mailbundle” is damaged and can’t be opened.  You should move it to the Bin.  Mail created this file on an unknown date.
“MailWrap.mailbundle” is damaged and can’t be opened.

I received this message when I edited the Info.plist file after running codesign. It means the CodeSignature doesn’t match the contents. You need to re-run the codesign command every time you change any file in the bundle to keep the signature updated.

“Mail” needs to be updated.  This app will not work with future versions of macOS and needs to be updated to improve compatibility.  Contact the developer for more information.
“Mail” needs to be updated.

This message is because MailWrap uses Python 2.7. Hopefully I will have some time to update it to Python 3 before the eventual removal of Python 2.7 from macOS. I’ve had success doing this before, so hopefully it goes well.

In conclusion

Now my emails are nice and wrapped and I’m not breaking a bunch of email clients in faraway lands. And all was quiet in the world. (Except not: the kernel is still broken, and Ukraine is still being invaded.)

I hope this post was useful to you. Happy hacking!

Designing a Sheet in Interface Builder

Quick tip time! This is an anecdote from a libre project I’m working on, specifically Auctions.

One of the things I am doing right now is implementing the Cocoa/Mac UI. I’m writing a flow using sheets for signing in to accounts. I had a lot of issues making the sheet accept input; it just wouldn’t let any of its fields become the first responder.

Poking around DuckDuckGo, I found a Stack Overflow question that seemed pretty interesting, and the answer was to override NSPanel‘s canBecomeKeyWindow method to return YES. I did some searching around in Apple’s Developer Documentation to see how the system determines when a window can become a key window, and I found this nugget:

A window that uses NSWindowStyleMaskBorderless can’t become key or main, unless the value of canBecomeKeyWindow or canBecomeMainWindow is YES. Note that you can set a window’s or panel’s style mask to NSWindowStyleMaskBorderless in Interface Builder by deselecting Title Bar in the Appearance section of the Attributes inspector.

Apple Developer Documentation

I had turned off Title Bar in Interface Builder as I thought it should be disabled since the window would be shown as a sheet. I re-enabled Title Bar, and voila! The sheet worked perfectly, and did not have a title bar when displayed as a sheet.