Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions src/Elm/Kernel/Test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*

import Elm.Kernel.Utils exposing (Tuple0)
import Elm.Kernel.Utils exposing (Tuple0, Tuple2)
import File exposing (FileNotFound, GeneralFileError, IsDirectory, PathEscapesDirectory)
import Result exposing (Err, Ok)

*/


function _Test_runThunk(thunk)
{
try {
Expand All @@ -16,3 +16,96 @@ function _Test_runThunk(thunk)
return __Result_Err(err.toString());
}
}


const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const process = require('node:process');
const crypto = require('node:crypto');

function _Test_readFile(filePath)
{
// Test for this early as `resolve` will strip training slashes
if (filePath.slice(-1) == path.sep) {
return __Result_Err(__File_IsDirectory);
}

// Protect against reading files above the "tests" directory
const testsPath = path.resolve("tests");
const fullPath = path.resolve(testsPath, filePath);

if (!fullPath.startsWith(testsPath))
{
return __Result_Err(__File_PathEscapesDirectory);
}

try {
return __Result_Ok(__Utils_Tuple2(fullPath, fs.readFileSync(fullPath, { encoding: 'utf8' })));
}
catch (err)
{
if (err.code == "ENOENT"){
return __Result_Err(__File_FileNotFound);
}
else {
return __Result_Err(__File_GeneralFileError(err.toString()));
}
}
}

function WriteFile(root, filePath, contents)
{
// Test for this early as `resolve` will strip training slashes
if (filePath.slice(-1) == path.sep) {
return __Result_Err(__File_IsDirectory);
}

// Protect against writing files above the root directory
const fullPath = path.resolve(root, filePath);

if (!fullPath.startsWith(root))
{
return __Result_Err(__File_PathEscapesDirectory);
}

const fullDir = path.dirname(fullPath);

// Note that this does not throw an error if the directory exists
fs.mkdirSync(fullDir, {recursive: true});

try {
fs.writeFileSync(fullPath, contents);
return __Result_Ok(fullPath);
}
catch (err)
{
return __Result_Err(__File_GeneralFileError(err.toString()));
}
}

var _Test_writeFile = F2(function(filePath, contents)
{
return WriteFile(path.resolve("tests"), filePath, contents);
})

var tempDir = null;
var _Test_writeTempFile = F2(function(filePath, contents)
{
if (tempDir === null)
{
tempDir = os.tmpdir() + "/" + crypto.randomUUID();
fs.mkdirSync(tempDir);
}

return WriteFile(tempDir, filePath, contents);
})

var overwriteGoldenFiles = null;
function _Test_overwriteGoldenFiles(unused)
{
if (overwriteGoldenFiles === null)
overwriteGoldenFiles = process.env.OVERWRITE_GOLDEN_FILES == '1';

return overwriteGoldenFiles;
}
79 changes: 79 additions & 0 deletions src/Expect.elm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Expect exposing
, lessThan, atMost, greaterThan, atLeast
, FloatingPointTolerance(..), within, notWithin
, ok, err, equalLists, equalDicts, equalSets
, equalToFile
, pass, fail, onFail
)

Expand Down Expand Up @@ -43,6 +44,11 @@ or both. For an in-depth look, see our [Guide to Floating Point Comparison](#gui
@docs ok, err, equalLists, equalDicts, equalSets


## Golden Files

@docs equalToFile


## Customizing

These functions will let you build your own expectations.
Expand Down Expand Up @@ -103,6 +109,7 @@ Another example is comparing values that are on either side of zero. `0.0001` is
-}

import Dict exposing (Dict)
import File
import Set exposing (Set)
import Test.Distribution
import Test.Expectation
Expand Down Expand Up @@ -576,6 +583,78 @@ equalSets expected actual =
reportCollectionFailure "Expect.equalSets" expected actual missingKeys extraKeys


{-| Tests the a String is equal to the contents of the file stored at the file path.

If the file does not exist, it will be created and this test will pass.

If the file does exist, then this test will pass if its contents are equal to the actual string.

All file paths are scoped to be within the "tests/" directory.

-}
equalToFile : String -> String -> Expectation
equalToFile filePath actual =
let
writeGoldenFile () =
case File.writeFile filePath actual of
Err File.FileNotFound ->
-- Impossible
pass

Err File.IsDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a directory instead of a file", reason = Custom }

Err File.PathEscapesDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a path that would escape the tests/ directory", reason = Custom }

Err (File.GeneralFileError fileError) ->
Test.Expectation.fail { description = "Expect.equalToFile encountered a general file error: " ++ fileError, reason = Custom }

Ok _ ->
pass
in
if File.overwriteGoldenFiles () then
writeGoldenFile ()

else
case File.readFile filePath of
Err File.FileNotFound ->
writeGoldenFile ()

Err File.IsDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a directory instead of a file", reason = Custom }

Err File.PathEscapesDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a path that would escape the tests/ directory", reason = Custom }

Err (File.GeneralFileError fileError) ->
Test.Expectation.fail { description = "Expect.equalToFile encountered a general file error: " ++ fileError, reason = Custom }

Ok ( existingAbsolutePath, contents ) ->
if actual == contents then
pass

else
case File.writeTempFile filePath actual of
Ok newAbsolutePath ->
let
message =
[ Just <| "The contents of \"" ++ filePath ++ "\" changed!"
, Just <| "To compare run: git diff --no-index " ++ existingAbsolutePath ++ " " ++ newAbsolutePath
, if String.endsWith ".html" filePath then
Just <| "To visually compare run: open file://" ++ existingAbsolutePath ++ " file://" ++ newAbsolutePath

else
Nothing
, Just <| "To accept these changes delete \"" ++ filePath ++ "\" or specify OVERWRITE_GOLDEN_FILES=1 when running elm-test"
]
in
Test.Expectation.fail { description = String.join "\n\n" (List.filterMap identity message), reason = Custom }

_ ->
Test.Expectation.fail { description = "Expect.equalToFile encountered an unexpected error", reason = Custom }


{-| Always passes.

import Json.Decode exposing (decodeString, int)
Expand Down
52 changes: 52 additions & 0 deletions src/File.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module File exposing (AbsolutePath, FileError(..), RelativePath, overwriteGoldenFiles, readFile, writeFile, writeTempFile)

import Elm.Kernel.Test


type FileError
= FileNotFound
| IsDirectory
| PathEscapesDirectory
| GeneralFileError String


type alias RelativePath =
String


type alias AbsolutePath =
String


{-| Read the contents of the filePath relative to "tests/"
-}
readFile : RelativePath -> Result FileError ( AbsolutePath, String )
readFile =
Elm.Kernel.Test.readFile


{-| Write the contents of the second argument to the file path in the first argument relative to "tests/"

Returns the absolute file path if successful.

-}
writeFile : RelativePath -> String -> Result FileError AbsolutePath
writeFile =
Elm.Kernel.Test.writeFile


{-| Write the contents of the second argument to the file path in the first argument relative to a temp directory

Returns the absolute file path if successful.

-}
writeTempFile : RelativePath -> String -> Result FileError AbsolutePath
writeTempFile =
Elm.Kernel.Test.writeTempFile


{-| Checks the OVERWRITE\_GOLDEN\_FILES environment variable
-}
overwriteGoldenFiles : () -> Bool
overwriteGoldenFiles =
Elm.Kernel.Test.overwriteGoldenFiles
18 changes: 18 additions & 0 deletions src/Test/Html/Query.elm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Test.Html.Query exposing
( Single, Multiple, fromHtml
, find, findAll, children, first, index, keep
, count, contains, has, hasNot, each
, prettyPrintSingle
)

{-| Querying HTML structure.
Expand All @@ -18,6 +19,10 @@ module Test.Html.Query exposing

@docs count, contains, has, hasNot, each

## Debugging

@docs prettyPrintSingle

-}

import Expect exposing (Expectation)
Expand Down Expand Up @@ -496,3 +501,16 @@ each : (Single msg -> Expectation) -> Multiple msg -> Expectation
each check (Internal.Multiple showTrace query) =
Internal.expectAll check query
|> failWithQuery showTrace "Query.each" query

{-| Pretty prints the result of a query as HTML if successful -}
prettyPrintSingle : Single msg -> Result String String
prettyPrintSingle (Internal.Single _ query) =
case Internal.traverse query of
Ok [ element ] ->
Ok <| Internal.prettyPrint element

Ok results ->
Err <| "Query.prettyPrintSingle expected exactly one result from query, but found " ++ String.fromInt (List.length results)

Err queryError ->
Err <| "Query.prettyPrintSingle " ++ Internal.queryErrorToString queryError