🐦‍⬛ Snapshot testing in Gleam

:memo: Improve `birdie.main` documentation

Changed files
+37 -27
src
+37 -27
src/birdie.gleam
··· 64 64 65 65 /// Performs a snapshot test with the given title, saving the content to a new 66 66 /// snapshot file. All your snapshots will be stored in a folder called 67 - /// `birdie_snapshots` in the project's root. 68 - /// 67 + /// `birdie_snapshots` in the project's root. 68 + /// 69 69 /// The test will fail if there already is an accepted snapshot with the same 70 70 /// title and a different content. 71 71 /// The test will also fail if there's no accepted snapshot with the same title 72 72 /// to make sure you will review new snapshots as well. 73 - /// 73 + /// 74 74 /// > 🚨 A snapshot is saved to a file named after its title, so all titles 75 75 /// > should be unique! Otherwise you'd end up comparing unrelated snapshots. 76 - /// 76 + /// 77 77 /// > 🐦‍⬛ To review all your snapshots interactively you can run 78 78 /// > `gleam run -m birdie`. 79 - /// > 79 + /// > 80 80 /// > To get an help text and all the available options you can run 81 81 /// > `gleam run -m birdie help`. 82 - /// 82 + /// 83 83 pub fn snap(content content: String, title title: String) -> Nil { 84 84 case do_snap(content, title) { 85 85 Ok(Same) -> Nil ··· 209 209 // --- FILE SYSTEM OPERATIONS -------------------------------------------------- 210 210 211 211 /// Save a new snapshot to a given path. 212 - /// 212 + /// 213 213 fn save(snapshot: Snapshot(New), to destination: String) -> Result(Nil, Error) { 214 214 // Just to make sure I'm not messing up something anywhere else in the code 215 215 // base: a new snapshot's destination MUST always end with a `.new` extension. ··· 229 229 } 230 230 231 231 /// Read an accepted snapshot which might be missing. 232 - /// 232 + /// 233 233 fn read_accepted(source: String) -> Result(Option(Snapshot(Accepted)), Error) { 234 234 case simplifile.read(source) { 235 235 Ok(content) -> ··· 245 245 } 246 246 247 247 /// Read a new snapshot. 248 - /// 248 + /// 249 249 /// > ℹ️ Notice the different return type compared to `read_accepted`: when we 250 250 /// > try to read a new snapshot we are sure it's there (because we've listed 251 251 /// > the directory or something else) so if it's not present that's an error 252 252 /// > and we don't return an `Ok(None)`. 253 - /// 253 + /// 254 254 fn read_new(source: String) -> Result(Snapshot(New), Error) { 255 255 case simplifile.read(source) { 256 256 Ok(content) -> ··· 262 262 263 263 /// List all the new snapshots in a folder. Every file is automatically 264 264 /// prepended with the folder so you get the full path of each file. 265 - /// 265 + /// 266 266 fn list_new_snapshots(in folder: String) -> Result(List(String), Error) { 267 267 case simplifile.read_directory(folder) { 268 268 Error(reason) -> Error(CannotReadSnapshots(reason: reason, folder: folder)) ··· 281 281 282 282 /// Finds the snapshots folder at the root of the project the command is run 283 283 /// into. If it's not present the folder is created automatically. 284 - /// 284 + /// 285 285 fn find_snapshots_folder() -> Result(String, Error) { 286 286 let result = result.map_error(find_project_root("."), CannotFindProjectRoot) 287 287 use project_root <- result.try(result) ··· 294 294 } 295 295 296 296 /// Returns the path to the project's root. 297 - /// 297 + /// 298 298 /// > ⚠️ This assumes that this is only ever run inside a Gleam's project and 299 299 /// > sooner or later it will reach a `gleam.toml` file. 300 300 /// > Otherwise this will end up in an infinite loop, I think. 301 - /// 301 + /// 302 302 fn find_project_root(path: String) -> Result(String, simplifile.FileError) { 303 303 let manifest = filepath.join(path, "gleam.toml") 304 304 case simplifile.verify_is_file(manifest) { ··· 323 323 324 324 /// Turns a snapshot's title into a file name stripping it of all dangerous 325 325 /// characters (or at least those I could think ok 😁). 326 - /// 326 + /// 327 327 fn file_name(title: String) -> String { 328 328 string.replace(each: "/", with: " ", in: title) 329 329 |> string.replace(each: "\\", with: " ") ··· 336 336 } 337 337 338 338 /// Returns the path where a new snapshot should be saved. 339 - /// 339 + /// 340 340 fn new_destination(snapshot: Snapshot(New), folder: String) -> String { 341 341 filepath.join(folder, file_name(snapshot.title)) <> ".new" 342 342 } 343 343 344 344 /// Strips the extension of a file (if it has one). 345 - /// 345 + /// 346 346 fn strip_extension(file: String) -> String { 347 347 case filepath.extension(file) { 348 348 Ok(extension) -> string.drop_right(file, string.length(extension) + 1) ··· 352 352 353 353 /// Turns a new snapshot path into the path of the corresponding accepted 354 354 /// snapshot. 355 - /// 355 + /// 356 356 fn to_accepted_path(file: String) -> String { 357 357 // This just replaces the `.new` extension with the `.accepted` one. 358 358 strip_extension(file) <> ".accepted" ··· 542 542 } 543 543 544 544 // This is an ugly hack that I need because `glam` currently doesn't take into 545 - // account color codes. 545 + // account color codes. 546 546 // Those are invisible but still contribute to the length of a string, so I 547 547 // have to artifically set the width to a higher limit to take into account 548 548 // the length of the color codes added to the lines' titles. ··· 591 591 592 592 // --- CLI COMMAND ------------------------------------------------------------- 593 593 594 - @deprecated("🚨 This is the entry point of the CLI tool. 595 - You should never call this function yourself, you should run `gleam run -m birdie` instead. 596 - Expect this function to disappear from the public API on future releases!") 594 + /// Reviews the snapshots in the project's folder. 595 + /// This function will behave differently depending on the command line 596 + /// arguments provided to the program. 597 + /// To have a look at all the available options you can run 598 + /// `gleam run -m birdie help`. 599 + /// 600 + /// > 🐦‍⬛ The recommended workflow is to first run your gleeunit tests with 601 + /// > `gleam test` and then review any new/failing snapshot manually running 602 + /// > `gleam run -m birdie`. 603 + /// > 604 + /// > And don't forget to commit your snapshots! Those should be treated as code 605 + /// > and checked with the vcs you're using. 606 + /// 597 607 pub fn main() -> Nil { 598 608 case argv.load().arguments { 599 609 [] | ["review"] -> report_status(review()) ··· 678 688 } 679 689 680 690 /// The choice the user can make when reviewing a snapshot. 681 - /// 691 + /// 682 692 type ReviewChoice { 683 693 AcceptSnapshot 684 694 RejectSnapshot ··· 688 698 /// Asks the user to make a choice: it first prints a reminder of the options 689 699 /// and waits for the user to choose one. 690 700 /// Will prompt again if the choice is not amongst the possible options. 691 - /// 701 + /// 692 702 fn ask_choice() -> Result(ReviewChoice, Error) { 693 703 io.println( 694 704 ansi.bold(ansi.green(" a")) ··· 792 802 // --- FFI --------------------------------------------------------------------- 793 803 794 804 /// Clear the screen. 795 - /// 805 + /// 796 806 @external(erlang, "birdie_ffi_erl", "clear") 797 807 fn clear() -> Nil 798 808 799 809 /// Move the cursor up a given number of lines. 800 - /// 810 + /// 801 811 @external(erlang, "birdie_ffi_erl", "cursor_up") 802 812 fn cursor_up(n: Int) -> Nil 803 813 804 814 /// Clear the line the cursor is currently on. 805 - /// 815 + /// 806 816 @external(erlang, "birdie_ffi_erl", "clear_line") 807 817 fn clear_line() -> Nil 808 818