util/jasmine-matchers.js

const pixelmatch = require('pixelmatch'),
    path = require('path'),
    fs = require('fs'),
    pngjs = require('pngjs').PNG,
    getCallingPath = require('./get-calling-path');

function getImageMatch(actual, expected) {
    const actualImg = pngjs.sync.read(fs.readFileSync(actual)),
        expectedImg = pngjs.sync.read(fs.readFileSync(expected));

    if (actualImg.width !== expectedImg.width || actualImg.height !== expectedImg.height) {
        throw new Error('Test image and emulator image sizes did not match! Cannot compare! Images should be 256x240 pngs.');
    }

    const numDiff = pixelmatch(actualImg.data, expectedImg.data, null, actualImg.width, actualImg.height);

    return (1 - (numDiff / (actualImg.width * actualImg.height))) * 100;
}

//This class exists entirely for documentation reasons.
/**
 * @alias JasmineMatchers
 * @classdesc
 * Adds matchers to the jasmine object, allowing images to be compared to other images. These extend off of the matchers that
 * are built into Jasmine. {@link https://jasmine.github.io/api/4.0/matchers Jasmine Matchers}
 */
class NesTestJasmineMatchers {

    /**
     * Compares an image generated by the emulator with another image you provide. This will succeed if at least
     * 80% of the pixels in the old image match the new one. 
     * @param {String} actual The path to the image generated by the emulator. (This is what goes in expect())
     * @param {String} expected  The path to the image on your system to compare with. (This is what goes in.toBeSimilarToImage())
     * @returns Success if the images are at least 80% similar, otherwise failure and an error message.
     * @example
     * <caption>Compares two images, to verify whether they are at least 80% similar.</caption>
     *  // Take a screenshot of the intro screen
     *  const testImage = await emulator.takeScreenshot('intro-screen.png');
     * 
     *  // Do a comparison that they're similar (at least 80% the same)
     *  expect(testImage).toBeSimilarToImage('./data/intro-screenshot.png');
     */
    static toBeSimilarToImage(actual, expected) {}

    /**
     * Compares an image generated by the emulator with another image you provide. This will succeed only if
     * every pixel in the image matches.
     * @param {String} actual The path to the image generated by the emulator. (This is what goes in expect())
     * @param {String} expected  The path to the image on your system to compare with. (This is what goes in.toBeSimilarToImage())
     * @returns Success if all pixels match, otherwse failure.
     * @example
     * <caption>Compares two images, to verify whether they are at least 80% similar.</caption>
     *  // Take a screenshot of the intro screen
     *  const testImage = await emulator.takeScreenshot('intro-screen.png');
     * 
     *  // Do a comparison that shows that all pixels match.
     *  expect(testImage).toBeIdenticalToImage('./data/intro-screenshot.png');
     */

    static toBeIdenticalToImage(actual, expected) {}

    /**
     * Install the jasmine matchers in this class using jasmine globals. This must be run before methods such as
     * expect(a).toBeSimilarToImage(b). (It is automatically run when using the `nes-test` binary.)
     * @returns Undefined
     */
    static installMatchers() {
        jasmine.addMatchers({
            toBeSimilarToImage: function(matchersUtil) {
                return {
                    compare: function(actual, expected) {
                        const realCwd = getCallingPath(2);
                        const resNumber = getImageMatch(actual, path.join(realCwd, expected));
    
                        if (resNumber >= 80) {
                            return {
                                pass: true
                            };
                        } else {
                            return {
                                pass: false,
                                message: "Expected 80% of pixels to be the same, actual percentage was " + resNumber + "%"
                            }
                        }
                    }
                }
            },
            toBeIdenticalToImage: function(matchersUtil) {
                return {
                    compare: function(actual, expected) {
                        const realCwd = getCallingPath(2);
                        const resNumber = getImageMatch(actual, path.join(realCwd, expected));
    
                        if (resNumber === 100) {
                            return {
                                pass: true
                            };
                        } else {
                            return {
                                pass: false,
                                message: "Expected an exact image match, however the real match was " + resNumber + "%"
                            }
                        }
                    }
                }
            }
        })
    
    }
}

module.exports = NesTestJasmineMatchers;