zachallaun / mneme

Snapshot testing for Elixir
https://hex.pm/packages/mneme
105 stars 5 forks source link

Idea: provide smart diffs for rendered html #101

Open tcoopman opened 1 month ago

tcoopman commented 1 month ago

This is just an idea and probably out of scope for this project. On the other hand, it would be really sweet.

Right now snapshot tests for rendered HTML are kind of useless, because the rendered html is just text.

Imagine if mneme could detect the output is html parse it, and maybe provide similar tools like for regular output.

Switching with j-k could hide attributes, or hide levels of children,..

I guess this might already be simulated manually by passing the rendered html to floki.

zachallaun commented 4 weeks ago

I think this is probably a long way off, but I agree that it would be cool, so I'll leave the issue open!

Perhaps one way you could signal something like this to Mneme is to have an auto_assert/2 that looks like:

auto_assert :html, some_call_that_renders_html()

and then Mneme looks up the ":html hook" and uses it to generate and compare patterns in some non-default way. There's a lot to consider though and a lot that those kind of hooks would need to be able to do (pattern generation, matching/comparisons, diffing, converting the assertion to an ex_unit-friendly format).

tcoopman commented 3 weeks ago

I was just playing around with Floki in combination with auto_assert. I feel like it could already deliver value, but mneme doesn't seem to generate an option to turn something like this:

 [
                                                  {"svg",
                                                   %{
                                                     "aria-hidden" => "true",
                                                     "class" =>
                                                       "size-5 stroke-zinc-500 group-has-[[data-disabled]]:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]",
                                                     "fill" => "none",
                                                     "viewbox" => "0 0 16 16"
                                                   },
                                                   [
                                                     {"path",
                                                      %{
                                                        "d" => "M5.75 10.75L8 13L10.25 10.75",
                                                        "strokelinecap" => "round",
                                                        "strokelinejoin" => "round",
                                                        "strokewidth" => "1.5"
                                                      }, []},
                                                     {"path",
                                                      %{
                                                        "d" => "M10.25 5.25L8 3L5.75 5.25",
                                                        "strokelinecap" => "round",
                                                        "strokelinejoin" => "round",
                                                        "strokewidth" => "1.5"
                                                      }, []}
                                                   ]}
                                                ]}

Into

                                                  {"svg", %{},
                                                   [
                                                     {"path",
                                                      %{}, []},
                                                     {"path",
                                                      %{}, []}
                                                   ]}
                                                ]}

Which would already help a bit I think

tcoopman commented 3 weeks ago

After some more toying, I've got something that's actually starting to get useful:

    x =
      render_async(view)
      |> Floki.parse_document!(attributes_as_maps: true)
      |> Floki.traverse_and_update(fn
        {tag, _attrs, children} -> {tag, [], children}
      end)
      |> Floki.find("main")
      |> Floki.raw_html()

    # |> Floki.text(sep: "-")

    auto_assert "<main><div><div><h1>\n  Publish a new workshop\n</h1><hr/><div><form><input/><section><div><h1>\n  \n          Workshop Name\n        \n</h1><p>\n  \n          This is the public name of the workshop.\n        \n</p></div><div><span><input/></span></div></section><hr/><section><div><h1>\n  \n          Workshop Description\n        \n</h1><p></p></div><div><span><textarea>\n</textarea></span></div></section><hr/><section><div><h1>\n  \n          Template\n        \n</h1><p>\n  \n          Pick a template for the new workshop.\n        \n</p></div><div><span><select><option>Workshop</option></select><span><svg><path></path><path></path></svg></span></span></div></section><hr/><div><button><span></span>Publish\n  \n</button></div></form></div></div></div></main>" <-
                  x

If I could get the output formatted a bit more nicely (without the \n - not sure why Floki is inserting them all...), that would already be great.

zachallaun commented 3 weeks ago

Regarding this:

{[
  {"svg", %{},
    [
      {"path",
      %{}, []},
      {"path",
      %{}, []}
    ]}
]}

As of #57, Mneme won't generate empty map patterns anymore, which is why this isn't being suggested.

I think the second idea is probably the way to go. In v0.10.0, if you select the """ pattern for the string, it will be retained if the output changes. Unless the strings are vastly difference, Mneme will use Myers diff to highlight the specific parts of strings that changed with underlines, which should be helpful!