webcodr

macOS Catalina EDID Override AKA HDMI color fix

HDMI connections from your Mac to monitor can be a pain in the ass. There is a chance that macOS will detect your monitor as a TV and set the color space to YCbCr. You will get wrong colors and sometimes blurry fonts.

If you’re having this problem, like me, you know the fix: a patched EDID created with this little Ruby script.

The installation of this EDID override could be tedious since the release of El Capitan, as SIP won’t let you access the necessary system files. Just disable it in recovery mode, copy the file and enable it again. Sucks, but works just fine.

Now, Catalina is out for a few hours and has a new way to annoy people who need EDID overrides. All system-related directories and files are read-only, regardless of the status of SIP.

Fortunately Apple was not crazy enough to disable the write access completely.

Help is on the way, ETA 0 seconds!

  1. Patch your EDID
  2. Boot into recovery mode with CMD+R
  3. Login with your user
  4. Open Disk Utility, select your volume (in most cases Macintosh HD) and mount it with your password (yes, again)
  5. Close the Disk Utility and open a Terminal window
  6. Copy the directory with the patched EDID to /Volumes/$VOLUME_NAME/System/Library/Displays/Contents/Resources/Overrides
  7. Reboot and enjoy the right colors again

Here’s an example of the shell commands:

cd /Volumes/Macintosh\ HD/System/Library/Displays/Contents/Resources/Overrides
cp -rf /Volumes/Macintosh\ HD/Users/webcodr/DisplayVendorID-5a63 .

Don’t forget to use the correct volume and user names!

Dear Apple

Just add a simple solution to select the HDMI color space. A simple shell commando with sudo would suffice or at least let us use an override directory within the user library as it was possible many years ago. It just sucks to do this after every macOS upgrade and every time you improve system security, it gets harder.

Please, don’t forget us powers users …

Hello, Dark Mode

Dark mode for Android and iOS? Hold my beer …

It’s quite simple to implement. Every modern browser can evaluate media queries in JavaScript with window.matchMedia() and supports CSS variables.

I added the following to my application JavaScript file:

const preferColorSchemeResult
  = window.matchMedia('(prefers-color-scheme: dark)')

if (preferColorSchemeResult && preferColorSchemeResult.matches === true) {
  document.documentElement.setAttribute('data-theme', 'dark')
} else {
  document.documentElement.setAttribute('data-theme', 'light')
}

The script will set the data attribute theme on the document element (html) with the possible values dark or light depending on the result of the media query.

There’s no need for a polyfill, even IE 10 supports window.matchMedia()

Stylesheet changes is even simpler, since I already had introduced SCSS color variables a while ago. I just had to replace them with CSS variables.

// colors
$c_white: #fff;
$c_dark-grey: #4A4A4A;

:root {
  --container-background-color: #{$c_white};
  ...
}

[data-theme="dark"] {
  --container-background-color: #{darken($c_dark-grey, 20%)};
  ...
}

That’s basically it. If you use SCSS, please take notice to use interpolations to map the SCSS variables to CSS variables. This change in SassScript expressions was necessary to provide full compatibility with plain CSS.

Since the theme selection is fully automated, I will provide a toggle possibiliry in a future release for those of you who prefer the light mode. This can be easily achieved with a flag in local storage and some minor changes in the JavaScript part.

Snapshot Tests With Jest

Writing tests can sometimes be a tedious task. Mocks and assertions can be a pain in the ass. The latter is especially nasty when HTML is involved. Give me the second p element from the 30th div within an article in aside etc. – no thanks.

The creators of Jest (Facebook) have found a better way: Snapshot tests!

How does it work?

Take a look the following assertion:

it('should create a foo bar object', () => {
  const result = foo.bar()
  expect(result).toMatchSnapshot()
})

toMatchSnapshot() takes what ever you give to expect(), serializes it and saves it into a file. The next test run will compare the expected value to the stored snapshot and will fail if they don’t match. Jest shows a nicely formatted error message and diff view on failed tests.

This is really useful with generated HTML and/or testing UI behaviour. Just call the method and let it compare to the snapshot.

Updating snapshots

You added something to your code and the snapshot has to be updated? No problem:

jest --updateSnapshot

If you’re using the Jest watcher it’s even simpler. Just press u to update all snapshots or press i to update the snapshots interactively.

What about objects with generated values?

Here’s an example with an randomized id:

it('should fail every time', () => {
  const ship = {
    id: Math.floor(Math.random() * 20),
    name: 'USS Defiant'
  }

  expect(ship).toMatchSnapshot()
})

The id will change on every test run, so this test will fail every time. Well, shit? Nope. Jest got you covered:

it('should create a ship', () => {
  const ship = {
    id: Math.floor(Math.random() * 20),
    name: 'USS Defiant'
  }

  expect(ship).toMatchSnapshot({
    id: expect.any(Number)
  })
})

Jest will now only compare the type of the id and the test will pass.

For certain objects like a date, there is another possibility:

Date.now = jest.fn(() => 1528902424828)

A call of Date.now() will call the mock method and always return the same value.

Some advice

  1. Always commit your snapshots! If they are missing,CI systems will always create new snapshots and the tests will become useless.

  2. Snapshot tests are an awesome tool, but don’t be too lazy. They are no replacement for other assertion types, especially if you’re working test-driven. Rather use them alongside with your other tests.

  3. Write meaningful test names. Well, you heard that one before, didn’t you? Really, it helps a a lot when tests fail or you have to look inside a snapshot file. Jest takes a test name as an id inside a snapshot file. That’s why you have to update a snapshot after changing the name.

Introducing DeliveryGuy

I like the Fetch API. It’s supported by all modern browsers, easy to use and has some really good polyfills for older devices. But Fetch has one major flaw: it will only throw errors if there is a network problem.

That’s a really odd decision from my point of view. Nearly any HTTP library out there throws errors or rejects the promise in case of a HTTP error.

A Fetch response object has the property ok to determine if the server responded with an error, but that’s not very comfortable to use.

Since my team and I have decided to use Fetch in a Vue-based web app, I decided to create a little wrapper for much more convenience. Say hello to DeliveryGuy.

Usage

Well, surprise, it’s a Node module, so just use any package manager you like. My personal choice is yarn.

yarn install delivery-guy

Example

import { deliverJson } from 'delivery-guy'

const getItems = async () => {
  try {
    const items = await deliverJson('/api/items')
    console.log(items)
  } catch (e) {
    console.error(e.message)
    console.log('HTTP Status', e.response.status)
    console.log('Response Body'. e.responseBody)
  }
}

What’s going on here?

DeliveryGuy exports two main functions:

  • deliver() will return a response promise like fetch() does.
  • deliverJson() presumes your response body contains JSON. It’s basically a shortcut and returns the promise of Response.json().

Both will accept the same two parameters as fetch() does and pass them along.

If the server responds with a HTTP error, DeliveryGuy will throw an error.

Due to the inheritance limitations of built-in classes with ES5 I mentioned in my last post, it’s only possible to set custom properties of a custom error class.

DeliveryGuy provides additional two properties on an error object:

  • response has the original response object of a Fetch call.
  • responseBody contains the response body and will try to parse it as JSON. If JSON.parse fails, it will return the response body in its original state.

TL;DR

DeliveryGuy allows you comfortably call the Fetch API without a hassle on HTTP errors. Just use try/catch and you’re done.

Please let me know on GitHub if you have feedback, a feature request or found a bug. Thank you!

Why custom errors in JavaScript with Babel are broken

Have you ever tried to write a custom error class in JavaScript? Well, it does work to a certain extend. But if you want to add custom methods or call instanceof to determine the error type it will not work properly.

Here is a little example of a custom error class:

class MyError extends Error {
  constructor(foo = 'bar', ...params) {
    super(...params)
    
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, MyError)
    }
    
    this.foo = foo
  }
  
  getFoo() {
    return this.foo
  }
}

try {
  throw new MyError('myBar')
} catch(e) {
  console.log(e instanceof MyError) // -> false
  console.log(e.getFoo()) // -> Uncaught TypeError: e.getFoo is not a function
}

Works fine in any browser with ES6/ES2015 support, but if you transpile the example with Babel to ES5 and execute the code, you will get the results shown in the comments.

Why

Due to limitations of ES5 it’s not possible to inherit from built-in classes like Error, see the Babel docs.

Possible solution

The docs mention a plug-in called babel-plugin-transform-builtin-extend to resolve this issue, but if you have to support older browsers it may not help. In order to work the plug-in needs support for __proto__. Take a guess which browser does not support __proto__ … and of course, it’s the web developers best friend aka Internet Explorer. Thankfully it affects only version 10 and below.

Workaround

If it’s not feasable to use the plug-in, you can at least access properties set in the constructor. A call to e.foo in the example is possible, but e instanceof MyError will return false, since you will always get an instance of Error.

Conclusion

Nothing of this ideal. We have to wait until it’s possible to use ES6/ES2015 directly. Yes, we all could set our transpile targets to ES6/ES2015 today, but our clients usually won’t allow it. Some customer is always browsing the web with an ancient device/browser.