Tuesday, 8 November 2016

Unit testing GlassController methods and Sitecore rendering parameters

I finally had some time to work on getting some unit tests set up for the project I'm working on, which is using Glass Mapper (handy in pre-Habitat projects).  I'd done some testing using Glass Mapper before (using FakeDb, which is awesome), but wanted to take things in a Habitat-style direction (xUnit, NSubstitute) and also hadn't done anything around testing rendering parameters.

Yes, you can stub ISitecoreContext and tell it what to return when your controllers use it (to grab datasources and items), but I wanted to really streamline as much as possible and not change any of my controller's uses of GetDatasourceItem<IMyInterface>() and GetRenderingParameters<IMyInterface>() - after all why not make use of GlassController and these methods if you're using Glass?

Glass gives us a nice way to stub datasource and rendering strings out through the RenderingContextWrapper property of GlassController (which is an IRenderingContext).  By creating a mock class we can pass through any datasource and rendering parameters (as a query string).

public class MockRenderingContext : IRenderingContext
{
    private readonly string _datasourceId;

    private readonly string _renderingParams;

    public MockRenderingContext(string datasourceId, string renderingParams)
    {
        _datasourceId = datasourceId;
        _renderingParams = renderingParams;
    }

    public string GetRenderingParameters()
    {
        return _renderingParams;
    }

    public string GetDataSource()
    {
        return _datasourceId;
    }

    public bool HasDataSource => !string.IsNullOrEmpty(_datasourceId);
}

We can assign this easily to our controller (which inherits from GlassController) in our test class:

ControllerToTest controller = new ControllerToTest(sitecoreContext)
{
    RenderingContextWrapper = new MockRenderingContext("/sitecore/content/mydatasource", "RenderingParam=1234")
};

All good so far.  This lets us grab the datasource, as it uses the SitecoreContext.GetItem<T>(datasource) to grab the relevant item.  Not so for the rendering parameters, unfortunately...  This calls GlassHtml.GetRenderingParameters<T>(renderingParametersString)!

So, what's the problem? I hear you ask.

Well, digging into what happens next (in GlassHtml): the rendering parameters string is split into KeyValuePairs, the template ID is retrieved from the Glass cache, and it ends up at:

public T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
{
    var item = Utilities.CreateFakeItem(null, renderParametersTemplateId, SitecoreContext.Database, "renderingParameters");

    using (new SecurityDisabler())
    {
        using (new EventDisabler())
        {
            using (new VersionCountDisabler())
            {
                item.Editing.BeginEdit();

                foreach (var key in parameters.AllKeys)
                {
                    item[key] = parameters[key];
                }

                T obj = SitecoreContext.Cast(item);

                item.Editing.EndEdit();
                item.Delete(); //added for clean up
                return obj;
            }
        }
    }
}


Now apparently because we're using FakeDb, the item that is dynamically built (to be cast into our parameters model) is not populated correctly!  So, let's override the appropriate method and do it the FakeDb way:

public class FakeGlassHtml : GlassHtml
{
    private readonly Db _db;

    public FakeGlassHtml(ISitecoreContext context, Db db) : base(context)
    {
        _db = db;
    }

    /// <summary>
    /// Method overridden to call our new method (below)
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of parameters</param>
    /// <returns>Populated parameters object</returns>
    public override T GetRenderingParameters<T>(NameValueCollection parameters)
    {
        if (parameters == null)
        {
            return default(T);
        }

        var config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        if (config == null)
        {
            SitecoreContext.GlassContext.Load(new OnDemandLoader<SitecoreTypeConfiguration>(typeof(T)));
        }

        config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        return GetRenderingParameters<T>(parameters, config?.TemplateId);
    }

    /// <summary>
    /// New method using FakeDB to create rendering parameters in the same way as the original Glass Mapper code
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of the parameters</param>
    /// <param name="renderParametersTemplateId">Rendering parameters template ID</param>
    /// <returns>Populated parameters object</returns>
    public new T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
    {
        if (renderParametersTemplateId.IsNull)
        {
            return default(T);
        }

        ID newId = ID.NewID;
        DbItem tempItem = new DbItem("temp", newId, renderParametersTemplateId);
        
        foreach (string key in parameters.AllKeys)
        {
            tempItem.Fields.Add(new DbField(key) { Value = parameters[key] });
        }

        _db.Add(tempItem);
        
        Item item = _db.GetItem(newId);

        T obj = SitecoreContext.Cast<T>(item);

        // Clean up
        item.Delete();
        (_db as CirrusDb)?.Remove(newId);

        return obj;
    }
}

Those of you with a keen eye might have noticed mention of DbWithRemove at the bottom there.  Seems that FakeDb has no remove method, even though its DataStorage class inside does.  I've just created a simple class to enable us to clean up our temporary item as per the original Glass code.

public class CirrusDb : Db
{
    public void Remove(ID itemId)
    {
        DataStorage.RemoveFakeItem(itemId);
    }
}

So now putting it all together in our test method:

using (Db db = SetupTestsite())
{
    using (new SiteContextSwitcher(FakeSite()))
    {
        ISitecoreContext sitecoreContext = new SitecoreContext(db.Database);
        
        ComponentsController controller = new ComponentsController(sitecoreContext)
        {
            RenderingContextWrapper = new MockRenderingContext(datasourceId, renderingParameterQueryString),
            GlassHtml = new FakeGlassHtml(sitecoreContext, db)
        };

        ViewResult result = controller.MyComponentMethod() as ViewResult;

        // Check your view and model

        Assert.NotNull(result);
        Assert.Equal("~/Views/Components/MyComponent.cshtml", result.ViewName);
        IMyModel model = result.ViewData.Model as IMyModel;
        Assert.NotNull(model);
        // ... etc.
    }
}

Hopefully some time soon I'll get to work on a Sitecore 8.2 project and get to try out testing with some of the new interfaces!

Monday, 17 October 2016

Polyfill for CSS backdrop-filter (on whole page)

On my latest project, which was based on Bootstrap, the client (/ designer) wanted to substitute the default gray background of the modal window with a nice gaussian blur.
Default Bootstrap modal
Bootstrap modal with gaussian blur over background
Fortunately, literally a couple of weeks beforehand I'd skimmed a reddit post on a very similar thing (except the modal is blurred instead of the background).  CSS for browsers that support it, some sort of SVG fallback for those which don't.  Easy, I thought naively.  On closer inspection you'll notice that all the other methods actually blur one particular image which is used as the background.  This won't work in our case, we need to blur the entire rest of the page.

Ok first off, let's get to the code for browsers which support the backdrop-filter tag (as of this post, that's only iOS Safari, and Chrome if you have the "experimental web platform features" flag checked).  We can use the CSS @supports at-rule to make sure this is only handled by browsers which support it (and an inverse rule for the remainder).  This is pretty well supported, but you can always polyfill it as well.

@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
    .modal-backdrop.in {
        opacity: 1;
        background-color: transparent;
        -webkit-backdrop-filter: blur(4px);
        backdrop-filter: blur(4px);
    }
}

(the screenshot above is using this code in Chrome - with the flag enabled)

In all the digging through other posts on workarounds using SVG, and testing out various solutions I had a sudden moment of clarity (or madness).  What if I somehow got an image of the entire page and set that as the (blurred) background-image? Well, a SO search result shows that it's very much possible, and html2canvas is the framework to use.

Shut up and give me the code!

Ok ok...

The CSS:

@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
    .modal-backdrop {
        background-color: initial;
        -webkit-transition: all .25s ease-in-out;
        -moz-transition: all .25s ease-in-out;
        transition: all .25s ease-in-out;
        opacity: 0;
    }
}

And the Javascript:

if (!CSS.supports('backdrop-filter', 'blur(4px)') && !CSS.supports('-webkit-backdrop-filter', 'blur(4px)')) {
    $('[data-target="#myModal"]').click(function () {
        html2canvas(document.body).then(function (canvas) {
            $('.modal-backdrop').css({
                "background-image": 'url(' + canvas.toDataURL("image/png") + ')',
                "filter": "blur(4px)",
                "opacity": "1"
            });
        });
    });
}
Where [data-target="#myModal"] is the link/button/whatever that opens your modal.

Boom! We have a nice .25 second transition to a blurred screenshot of the page as our modal background.  By testing for support, theoretically we should have nice degradation back to the default Bootstrap modal (for things like IE10).

Note: I have not tested this in all browsers, or even many browsers, just the latest of Chrome, Firefox, Edge, and IE.  If you have a lot of components on your page then html2canvas can take a while to render the image.  Use (or don't) after testing per the requirements of your solution.

Monday, 10 October 2016

"Invalid/missing hash was encountered" for documents

Reviewing our logs recently we discovered that we were getting an unusually large number of messages to the tune of
ERROR MediaRequestProtection: An invalid/missing hash value was encountered.

As we all know, Sitecore 7.5 added the concept of a hash which is required when resizing media items, so naturally we assumed that some images were being included incorrectly on a page somewhere, or the secret was incorrectly configured.  However, on closer inspection we realised that it wasn't actually images at all, but documents (which certainly aren't being resized, and surely don't need a hash).

Documents (mostly PDFs) were being uploaded to the media library, and then in a rich text field somewhere on the site, an author was linking to the document using insert link -> media item.

Upon reaching out to Sitecore, they were quick to reply that this was actually a bug in the current version of Sitecore (8.1 update 2 - 160302).  So if you're having a similar issue and want to clean out your logs a bit, you can contact support for the patch, and/or use the reference number 438674.