exercism/elixir/dancing-dots/test/dancing_dots/animation_test.exs

222 lines
8.4 KiB
Elixir
Raw Permalink Normal View History

2024-03-08 08:36:14 +00:00
defmodule DancingDots.AnimationTest do
use ExUnit.Case
import ExUnit.CaptureIO
describe "Animation module" do
@tag task_id: 1
test "defines two required callbacks, init/1 and handle_frame/3" do
assert DancingDots.Animation.behaviour_info(:callbacks) == [{:init, 1}, {:handle_frame, 3}]
end
@tag task_id: 1
test "defines no optional callbacks" do
assert DancingDots.Animation.behaviour_info(:optional_callbacks) == []
end
@tag task_id: 2
test "defines a __using__ macro" do
Code.ensure_loaded(DancingDots.Animation)
assert macro_exported?(DancingDots.Animation, :__using__, 1)
end
@tag task_id: 2
test "__using__ provides a default implementation of init/1" do
defmodule TestAnimation1 do
use DancingDots.Animation
def handle_frame(dot, _frame_number, _opts), do: dot
end
assert function_exported?(TestAnimation1, :init, 1)
end
@tag task_id: 2
test "the default implementation of init/1 returns opts unchanged" do
defmodule TestAnimation2 do
use DancingDots.Animation
def handle_frame(dot, _frame_number, _opts), do: dot
end
assert TestAnimation2.init([]) == {:ok, []}
assert TestAnimation2.init(:anything) == {:ok, :anything}
assert TestAnimation2.init({1, 2, 3}) == {:ok, {1, 2, 3}}
end
@tag task_id: 2
test "the default implementation of init/1 can be overridden" do
defmodule TestAnimation3 do
use DancingDots.Animation
def init(_), do: {:ok, []}
def handle_frame(dot, _frame_number, _opts), do: dot
end
assert TestAnimation3.init(3) == {:ok, []}
assert TestAnimation3.init(:anything) == {:ok, []}
end
@tag task_id: 2
test "__using__ does not provide a default implementation of handle_frame/3" do
capture_io(:stderr, fn ->
Code.compile_quoted do
quote do
defmodule TestAnimation4 do
use DancingDots.Animation
end
end
end
end)
refute function_exported?(TestAnimation4, :handle_frame, 3)
end
@tag task_id: 2
test "__using__ sets Animation as the behaviour, emitting compilation warnings about missing callbacks" do
compilation_warnings =
capture_io(:stderr, fn ->
Code.compile_quoted do
quote do
defmodule TestAnimation5 do
use DancingDots.Animation
end
end
end
end)
assert compilation_warnings =~
"function handle_frame/3 required by behaviour DancingDots.Animation is not implemented"
end
end
describe "Flicker module" do
@tag task_id: 3
test "implements Animation behaviour" do
Code.ensure_loaded(DancingDots.Flicker)
assert function_exported?(DancingDots.Flicker, :init, 1)
assert function_exported?(DancingDots.Flicker, :handle_frame, 3)
end
@tag task_id: 3
test "uses the default init/1 implementation" do
assert DancingDots.Flicker.init([]) == {:ok, []}
assert DancingDots.Flicker.init(:anything) == {:ok, :anything}
assert DancingDots.Flicker.init({1, 2, 3}) == {:ok, {1, 2, 3}}
end
@tag task_id: 3
test "every 4th frame, handle_frame/3 sets the dot's opacity to half the original value" do
dot1 = %DancingDots.Dot{x: 10, y: 30, radius: 4, opacity: 1}
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 10, opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot1, 1, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 2, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 3, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 4, []) == %{dot1 | opacity: 0.5}
assert DancingDots.Flicker.handle_frame(dot1, 5, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 6, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 7, []) == %{dot1 | opacity: 1}
assert DancingDots.Flicker.handle_frame(dot1, 8, []) == %{dot1 | opacity: 0.5}
assert DancingDots.Flicker.handle_frame(dot2, 13, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 14, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 15, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 16, []) == %{dot2 | opacity: 0.3}
assert DancingDots.Flicker.handle_frame(dot2, 17, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 18, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 19, []) == %{dot2 | opacity: 0.6}
assert DancingDots.Flicker.handle_frame(dot2, 20, []) == %{dot2 | opacity: 0.3}
end
@tag task_id: 3
test "can be used in a dot group" do
dot1 = %DancingDots.Dot{x: 44, y: 44, radius: 2, opacity: 0.8}
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 3, opacity: 0.5}
{:ok, dot_group} =
DancingDots.DotGroup.new([dot1, dot2])
|> DancingDots.DotGroup.add_animation(DancingDots.Flicker, [])
assert dot_group ==
%DancingDots.DotGroup{
animations_with_opts: [{DancingDots.Flicker, []}],
dots: [dot1, dot2]
}
assert DancingDots.DotGroup.render_dots(dot_group, 4) == [
%{dot1 | opacity: 0.4},
%{dot2 | opacity: 0.25}
]
end
end
describe "Zoom module" do
@tag task_id: 4
test "implements Animation behaviour" do
Code.ensure_loaded(DancingDots.Zoom)
assert function_exported?(DancingDots.Zoom, :init, 1)
assert function_exported?(DancingDots.Zoom, :handle_frame, 3)
end
@tag task_id: 4
test "has a custom init/1 implementation that checks that a valid velocity was passed as an option" do
assert DancingDots.Zoom.init(velocity: 10) == {:ok, [velocity: 10]}
assert DancingDots.Zoom.init([]) ==
{:error,
"The :velocity option is required, and its value must be a number. Got: nil"}
assert DancingDots.Zoom.init(velocity: "7") ==
{:error,
"The :velocity option is required, and its value must be a number. Got: \"7\""}
end
@tag task_id: 4
test "the first frame, handle_frame/3 returns the dot unchanged" do
dot = %DancingDots.Dot{x: 90, y: 90, radius: 100, opacity: 0.5}
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 3) == dot
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 30) == dot
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: -7) == dot
end
@tag task_id: 4
test "every frame after the first, handle_frame/3 grows the dot's radius by the given positive velocity" do
dot = %DancingDots.Dot{x: 90, y: 90, radius: 100, opacity: 0.5}
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 3) == %{dot | radius: 100}
assert DancingDots.Zoom.handle_frame(dot, 2, velocity: 3) == %{dot | radius: 103}
assert DancingDots.Zoom.handle_frame(dot, 3, velocity: 3) == %{dot | radius: 106}
assert DancingDots.Zoom.handle_frame(dot, 101, velocity: 3) == %{dot | radius: 400}
end
@tag task_id: 4
test "every frame after the first, handle_frame/3 shrinks the dot's radius by the given negative velocity" do
dot = %DancingDots.Dot{x: 100, y: 0, radius: 400, opacity: 0.9}
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: -1) == %{dot | radius: 400}
assert DancingDots.Zoom.handle_frame(dot, 2, velocity: -1) == %{dot | radius: 399}
assert DancingDots.Zoom.handle_frame(dot, 3, velocity: -1) == %{dot | radius: 398}
assert DancingDots.Zoom.handle_frame(dot, 101, velocity: -1) == %{dot | radius: 300}
end
@tag task_id: 4
test "can be used in a dot group" do
dot1 = %DancingDots.Dot{x: 0, y: 0, radius: 100, opacity: 0.3}
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 150, opacity: 0.3}
{:ok, dot_group} =
DancingDots.DotGroup.new([dot1, dot2])
|> DancingDots.DotGroup.add_animation(DancingDots.Zoom, velocity: 10)
assert dot_group ==
%DancingDots.DotGroup{
animations_with_opts: [{DancingDots.Zoom, [velocity: 10]}],
dots: [dot1, dot2]
}
assert DancingDots.DotGroup.render_dots(dot_group, 50) == [
%{dot1 | radius: 590},
%{dot2 | radius: 640}
]
end
end
end