Skip to content

Conversation

@micahhahn
Copy link
Member

@micahhahn micahhahn commented Jul 31, 2025

This PR adds a new primitive equalToFile to the Expect module. It also adds a prettyPrintSingle method

This primitive allows us to write golden file / snapshot tests in Elm. These are particularly useful at capturing the current outputs of a program and then failing if the outputs change.

NoRedInk in particular has an interest in snapshotting the HTML at various points in elm-tests to a file. I've also added a prettyPrintSingle function to the query module which exposes the internal query pretty print function. I think this will be useful generally for testing to be able to capture focus of a query in a complicated test (especially with elm-program-test). NRI would also see value in dumping the entire HTML of a page to a golden file so that we can do visual diffs of our pages in their various states.

Note that the code its current state is very rough - the kernel code added presumes that the tests are being run in the context of a node environment. That's always true for node-test-runner but AFAIK not always true for elm-test-rs. I'm very open to any ideas that would make this more robust.

I'm also aware that doing the IO direclty inside of the elm-explorations/test probably prevents the test runners from doing proper watching on these golden files. Definitely open to ideas on approaches that would let the actual file IO be handled by the runner itself.

Fixes HACKY-1182
Fixes LLM-2737

What does a failure look like?

Since the whole file diff would be an absolute mess (without running our diff algorithm to only show the minimal diff). So instead I write the changed file to temp so that we can run git diff easily. I also add a little touch of honey and if the file ends in *.html then we show a link to open the before and after in the browser.

Running 85 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 610003615040 ./tests/Page/Learn/GuidedDrafts/MainSpec.elm

↓ Page.Learn.GuidedDrafts.MainSpec
↓ Page.Learn.GuidedDrafts
↓ an essay-based Guided Draft
↓ intro tours
✗ Student is introduced to the assignment with a tour

    ensureView:
    The contents of "page/learn/guided_drafts/tour.html" changed!
    
    To compare run: git diff --no-index /Users/micahhahn/code/noredink/NoRedInk/monolith/ui/tests/page/learn/guided_drafts/tour.html /tmp/5fa2e54b-d8a1-4cb0-88bf-924505a214c7/page/learn/guided_drafts/tour.html
    
    To visually compare run: open file:///Users/micahhahn/code/noredink/NoRedInk/monolith/ui/tests/page/learn/guided_drafts/tour.html file:///tmp/5fa2e54b-d8a1-4cb0-88bf-924505a214c7/page/learn/guided_drafts/tour.html
    
    To accept these changes delete "page/learn/guided_drafts/tour.html" or specify OVERWRITE_GOLDEN_FILES=1 when running elm-test

The git diff will look like this:

> git diff --no-index /Users/micahhahn/code/noredink/NoRedInk/monolith/ui/tests/tests/x.html /tmp/b26adeb4-495c-46d1-92ea-4583c497cbbb/tests/x.html
diff --git a/Users/micahhahn/code/noredink/NoRedInk/monolith/ui/tests/tests/x.html b/tmp/b26adeb4-495c-46d1-92ea-4583c497cbbb/tests/x.html
index 1ed0b3cfe7a..d299fdad074 100644
--- a/Users/micahhahn/code/noredink/NoRedInk/monolith/ui/tests/tests/x.html
+++ b/tmp/b26adeb4-495c-46d1-92ea-4583c497cbbb/tests/x.html
@@ -1499,7 +1499,7 @@
        <div aria-atomic="true" aria-live="assertive" data-elm>
        </div>
        <div aria-atomic="true" aria-live="polite" data-elm>
-            Writing Are
+            Writing Area
        </div>
        <ul aria-labelledby="assistive-technology-notification-center_Page-Learn-GuidedDrafts-Main-heading" data-nri-description="announcement-log" data-elm>
            <li data-elm>

And just to show off how powerful this will be with something like Chrome's split view (chrome://flags/#side-by-side):

Screen.Recording.2025-10-27.at.2.21.09.PM.mov

@micahhahn micahhahn marked this pull request as ready for review August 1, 2025 20:34
@micahhahn
Copy link
Member Author

(Even if we do end up merging this, I have no idea how we would vendor it)

@micahhahn
Copy link
Member Author

I've opened a pull request on the main repo as well just hopefully to get some discussion going: elm-explorations#249

@linear
Copy link

linear bot commented Aug 1, 2025

HACKY-1182 Micah Hahn

Copy link
Member

@omnibs omnibs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

elm-test-rs says it also runs tests using node, so it might just work there.

Copy link
Member

@juanedi juanedi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks!! 🤘

Copy link

@brian-carroll brian-carroll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff! Looks useful!

I guess the main worries are

  1. compatibility with other runtimes (as you pointed out)
  2. blocking I/O

Other runtimes

We are helped here by the fact that all JS runtimes need Node compatibility! I think in practice the node: prefix convention can solve it for us.

The only alternative I can think of is to pass around a record of functions, like the "handle" pattern in Haskell, defining different constants for each supported runtime. The tricky part is that you couldn't do the requires or imports in the global scope. You'd have to do them dynamically, the first time they're needed, and then cache them for the next time.

But the node: prefix is easier, and should work in all practical cases. Might be good to test with elm-test-rs --deno

Blocking I/O

It's a pity that we need to use the sync versions of all the file operations, but there's no way out of it without rewriting everything. If users end up doing lots of snapshot testing, it could get slow, but at least it only blocks one worker thread and the others can make progress.
Actually it looks like elm-test-rs lets you specify the number of worker threads, so people could use that to increase the number of workers.

Comment on lines 21 to 22
const fs = require('fs');
const path = require('path');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably worth adding the node: prefix to these Node built-ins.
Node supports this, and Deno and Bun both have Node-compatible APIs that require it.
It might be worth testing with elm-test-rs --deno (https://github.com/mpizenberg/elm-test-rs?tab=readme-ov-file#deno-runtime)

Suggested change
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brian-carroll OK so I finally got around to looking into this and I was having trouble getting elm-test-rs --deno to run... have you ever done this successfully?

> elm-test-rs --deno ./tests/Page/Learn/GuidedDrafts/MainSpec.elm 
Error: Deno supervisor failed to start

Caused by:
    No such file or directory (os error 2)

Copy link

@brian-carroll brian-carroll Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't tried it when I made the comment.
I tried it just now and got the same message... and realised it meant I didn't have Deno installed! 😆
What do you get for which deno?

I installed it, tried again, and got an error that suggests elm-test-rs has not been updated to support more recent versions of Deno. Deno support seems to have been added 4 years ago. The current Deno version is 2.5.4. The latest v1 release is 1.46.3. elm-test-rs seems to have errors with both.

I still think importing from node:fs instead of just fs is the correct fix. It constitutes full support for both Deno and Bun as far as this repo is concerned, based on their own docs:
Deno's docs on Node compatibility
Bun's docs on Node compatibility

Beyond that, if users are still having issues, the problem is in another repo such as elm-test-rs.

❯ elm-test-rs --deno ./tests/Page/Learn/GuidedDrafts/MainSpec.elm
Warning `allow-hrtime` and `deny-hrtime` have been removed in Deno 2, as high resolution time is now always allowed
error: Uncaught (in promise) TypeError: Deno.read is not a function
    const num = await Deno.read(rid, buf);
                           ^
    at _readTillDone (file:///Users/briancarroll/Documents/NoRedInk/monolith/ui/elm-stuff/tests-0.19.1/js/deno_linereader.mjs:6:28)
    at Object.next (file:///Users/briancarroll/Documents/NoRedInk/monolith/ui/elm-stuff/tests-0.19.1/js/deno_linereader.mjs:25:38)
    at file:///Users/briancarroll/Documents/NoRedInk/monolith/ui/elm-stuff/tests-0.19.1/js/deno_supervisor.mjs:108:16

@linear
Copy link

linear bot commented Oct 24, 2025

@micahhahn micahhahn force-pushed the add-golden-file-tests branch from 71fef7c to 94a1f49 Compare October 27, 2025 18:56
@micahhahn micahhahn force-pushed the add-golden-file-tests branch from 94a1f49 to d7bac45 Compare October 27, 2025 19:07
@micahhahn
Copy link
Member Author

micahhahn commented Oct 27, 2025

@brian-carroll @juanedi @omnibs

Hey friends, given that Juliano is getting close on his work to allow patching elm libraries via nix I've put some more effort into polishing this PR to get it ready for prime time:

  1. On golden file difference, we now write the new contents to a temp file instead of dumping a giant diff to the user.
  2. The error message now shows a command to get the file diff using git diff ...
  3. The error message also shows a command to open both the old and new files in browser tabs
  4. We now support OVERWRITE_GOLDEN_FILES=1 by checking the ENV in node 🙈 (previously I though we would have to update the elm test runner for this, but we are already doing naughty things - why not just go a little further).

Thanks to juliano, you can now easily test what this will feel like by checking out this PR and running firstaide-update locally: https://github.com/NoRedInk/NoRedInk/pull/53132

src/Expect.elm Outdated

messageWithVisualDiff =
if String.endsWith ".html" filePath then
message ++ [ "To visually compare run: open file://" ++ existingAbsolutePath ++ " file://" ++ newAbsolutePath ]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how hard we're trying to make this cross-platform but open is MacOS specific.
On Linux I think it's xdg-open. I don't know the command on Windows, but I think the path would have backslashes that would have to be converted to forward slashes for the URL.
I think it's best not to worry about this for now though! If this ever gets upstreamed it would need to be tested and patched by someone on those platforms.

@micahhahn micahhahn force-pushed the add-golden-file-tests branch from f43cba3 to dd21c67 Compare November 5, 2025 17:46
@micahhahn
Copy link
Member Author

Ok one last change before merging this into master! I've modified the failure case to create a new file like "filename.failure.html" right next to the existing one instead of dumping it to temp. This more closely aligns with how golden files work for spec tests and hopefully will have better visibility if the user loses track of the elm-test output.

The write file call has been modified to try and delete the failure file if it exists - which also matches rspec behavior.

Copy link
Member

@juanedi juanedi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!! I do like Brian's suggestion :-)

@micahhahn micahhahn merged commit 5cf3974 into master Nov 6, 2025
@brian-carroll
Copy link

Well done on this @micahhahn, nice addition!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants