We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
LiveView 30 - Chapter 11: Your Turn
Published on: 2026-02-17
Tags:
elixir, Blog, Testing, LiveView, Game Site, Phoenix, Framework
Your Turn
Give It a Try
• Write tests for the core. Focus on writing unit tests for the individual
reducer functions of our core modules. Maybe reach for the reducer testing
pipelines we used in Chapter 10, Test Your Live Views, on page 311 to keep
your tests clean, flexible, and highly readable.
• Add a skewed rectangle1 puzzle, and a function called Board.skewed_rect/2,
like Board.rect/2 to support it.
• Add an optional direction argument to Pentomino.rotate/1 that allows both
clockwise and counterclockwise rotation.
Lets start with the tests
defmodule Pento.Game.PointTest do
use ExUnit.Case, async: true
alias Pento.Game.Point
@valid_x 2
@valid_y 3
@invalid_x "a"
@invalid_y "b"
setup [:create_point]
describe "new" do
test "valid x, y" do
assert {2, 3} = Point.new(@valid_x, @valid_y)
end
test "invalid x, y" do
assert_raise FunctionClauseError, fn ->
Point.new(@invalid_x, @invalid_y)
end
end
end
describe "transpose" do
test "transposes x and y", %{point: point} do
assert {3, 2} = Point.transpose(point)
end
test "flip", %{point: point} do
assert {2, 3} = Point.flip(point)
end
test "reflect", %{point: point} do
assert {4, 3} = Point.reflect(point)
end
test "center out of bounds", %{point: point} do
assert {-1, 0} = Point.center(point)
end
test "center" do
good_point = Point.new(4, 5)
assert {1, 2} = Point.center(good_point)
end
test "move", %{point: point} do
assert {3, 4} = Point.move(point, {1, 1})
end
test "rotate 0", %{point: point} do
assert {2, 3} = Point.rotate(point, 0)
end
test "rotate 90", %{point: point} do
assert {3, 4} = Point.rotate(point, 90)
end
test "rotate 180", %{point: point} do
assert {4, 3} = Point.rotate(point, 180)
end
test "rotate 270", %{point: point} do
assert {3, 2} = Point.rotate(point, 270)
end
test "rotate 360", %{point: point} do
assert {2, 3} = Point.rotate(point, 360)
end
end
describe "prepare" do
test "all", %{point: point} do
test_point =
point
|> Point.prepare(90, true, {1, 1})
assert {1, 2} = test_point
end
test "rotation: 90", %{point: point} do
test_point =
point
|> Point.prepare(90, false, {0, 0})
assert {0, 1} = test_point
end
test "rotation: 180", %{point: point} do
test_point =
point
|> Point.prepare(180, false, {0, 0})
assert {1, 0} = test_point
end
test "rotation: 270", %{point: point} do
test_point =
point
|> Point.prepare(270, false, {0, 0})
assert {0, -1} = test_point
end
test "360 rotation", %{point: point} do
test_point =
point
|> Point.prepare(360, false, {0, 0})
assert {-1, 0} = test_point
end
test "move", %{point: point} do
test_point =
point
|> Point.prepare(0, false, {1, 1})
assert {0, 1} = test_point
end
end
describe "idempotent" do
test "rotate 360", %{point: point} do
assert {2, 3} = Point.rotate(point, 360)
end
test "reflect twice", %{point: point} do
assert {2, 3} = point |> Point.reflect() |> Point.reflect()
end
test "transpose twice", %{point: point} do
assert {2, 3} = point |> Point.transpose() |> Point.transpose()
end
test "flip twice", %{point: point} do
assert {2, 3} = point |> Point.flip() |> Point.flip()
end
end
defp create_point(_context) do
{:ok, %{point: Point.new(@valid_x, @valid_y)}}
end
end
defmodule Pento.Game.ShapeTest do
use ExUnit.Case, async: true
alias Pento.Game.Shape
@valid_color :orange
@valid_name :p
@valid_points [{5, 4}, {6, 5}, {5, 5}, {6, 4}, {5, 6}]
describe "new" do
test "creates a shape no transformation" do
shape = Shape.new(@valid_name, 0, false, {5, 5})
assert shape.points == @valid_points
assert shape.color == @valid_color
end
test "creates a shape with rotation" do
shape = Shape.new(@valid_name, 90, false, {5, 5})
assert shape.points == [{4, 5}, {5, 4}, {5, 5}, {4, 4}, {6, 5}]
assert shape.color == @valid_color
end
test "creates a shape with rotation: 180" do
shape = Shape.new(@valid_name, 180, false, {5, 5})
assert shape.points == [{5, 6}, {4, 5}, {5, 5}, {4, 6}, {5, 4}]
assert shape.color == @valid_color
end
test "creates a shape with rotation: 270" do
shape = Shape.new(@valid_name, 270, false, {5, 5})
assert shape.points == [{6, 5}, {5, 6}, {5, 5}, {6, 6}, {4, 5}]
assert shape.color == @valid_color
end
test "creates a shape with reflection" do
shape = Shape.new(@valid_name, 0, true, {5, 5})
assert shape.points == [{5, 4}, {4, 5}, {5, 5}, {4, 4}, {5, 6}]
assert shape.color == @valid_color
end
test "creates a shape with rotation and reflection" do
shape = Shape.new(@valid_name, 90, true, {5, 5})
assert shape.points == [{6, 5}, {5, 4}, {5, 5}, {6, 4}, {4, 5}]
assert shape.color == @valid_color
end
test "invalid name" do
assert_raise FunctionClauseError, fn ->
Shape.new(:invalid, 0, false, {5, 5})
end
end
test "invalid rotation" do
assert_raise FunctionClauseError, fn ->
Shape.new(@valid_name, 45, false, {5, 5})
end
end
end
end
defmodule Pento.Game.PentominoTest do
use ExUnit.Case, async: true
alias Pento.Game.Pentomino
alias Pento.Game.Shape
describe "new" do
test "creates a shape with the correct points" do
pento = Pentomino.new(name: :l, rotation: 90, reflected: true, location: {5, 5})
shape = Shape.new(pento.name, pento.rotation, pento.reflected, pento.location)
assert_points_equal(shape.points, [{7, 5}, {6, 5}, {5, 5}, {4, 5}, {4, 4}])
end
test "invalid name" do
pento = Pentomino.new(name: :invalid, rotation: 0, reflected: false, location: {5, 5})
assert_raise FunctionClauseError, fn ->
Pentomino.to_shape(pento)
end
end
end
describe "rotate" do
test "rotates the pentomino 90 degrees" do
pento = Pentomino.new(rotation: 0)
rotated = Pentomino.rotate(pento)
assert rotated.rotation == 90
end
test "rotates the pentomino 180 degrees" do
pento = Pentomino.new(rotation: 90)
rotated = Pentomino.rotate(pento)
assert rotated.rotation == 180
end
test "rotates the pentomino 270 degrees" do
pento = Pentomino.new(rotation: 180)
rotated = Pentomino.rotate(pento)
assert rotated.rotation == 270
end
test "rotates the pentomino back to 0 degrees" do
pento = Pentomino.new(rotation: 270)
rotated = Pentomino.rotate(pento)
assert rotated.rotation == 0
end
test "rotates counterclockwise" do
pento = Pentomino.new(rotation: 0)
rotated = Pentomino.rotate(pento, :counterclockwise)
assert rotated.rotation == 270
end
test "rotates counterclockwise from 270 to 180" do
pento = Pentomino.new(rotation: 270)
rotated = Pentomino.rotate(pento, :counterclockwise)
assert rotated.rotation == 180
end
end
describe "flip" do
test "flips the pentomino" do
pento = Pentomino.new(reflected: false)
flipped = Pentomino.flip(pento)
assert flipped.reflected == true
end
test "flips the pentomino back" do
pento = Pentomino.new(reflected: true)
flipped = Pentomino.flip(pento)
assert flipped.reflected == false
end
end
describe "move" do
setup [:create_pento]
test "moves the pentomino up", %{pento: pento} do
moved = Pentomino.up(pento)
assert moved.location == {8, 7}
end
test "moves the pentomino down", %{pento: pento} do
moved = Pentomino.down(pento)
assert moved.location == {8, 9}
end
test "moves the pentomino left", %{pento: pento} do
moved = Pentomino.left(pento)
assert moved.location == {7, 8}
end
test "moves the pentomino right", %{pento: pento} do
moved = Pentomino.right(pento)
assert moved.location == {9, 8}
end
end
describe "to_shape" do
test ":i" do
pento = Pentomino.new(name: :i, rotation: 0, reflected: false, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{5, 3}, {5, 4}, {5, 5}, {5, 6}, {5, 7}])
end
test ":l" do
pento = Pentomino.new(name: :l, rotation: 90, reflected: true, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{7, 5}, {6, 5}, {5, 5}, {4, 5}, {4, 4}])
end
test ":t" do
pento = Pentomino.new(name: :t, rotation: 180, reflected: false, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{6, 6}, {5, 6}, {4, 6}, {5, 5}, {5, 4}])
end
test ":y" do
pento = Pentomino.new(name: :y, rotation: 270, reflected: true, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{3, 5}, {4, 4}, {4, 5}, {5, 5}, {6, 5}])
end
test ":n" do
pento = Pentomino.new(name: :n, rotation: 0, reflected: true, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{5, 3}, {5, 4}, {5, 5}, {4, 5}, {4, 6}])
end
test ":p" do
pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{4, 5}, {5, 4}, {5, 5}, {4, 4}, {6, 5}])
end
test ":w" do
pento = Pentomino.new(name: :w, rotation: 180, reflected: true, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{4, 6}, {4, 5}, {5, 5}, {5, 4}, {6, 4}])
end
test ":u" do
pento = Pentomino.new(name: :u, rotation: 180, reflected: true, location: {5, 5})
shape = Pentomino.to_shape(pento)
assert_points_equal(shape.points, [{4, 6}, {6, 6}, {4, 5}, {5, 5}, {6, 5}])
end
end
def create_pento(_content) do
{:ok, pento: Pentomino.new()}
end
defp assert_points_equal(actual, expected) do
assert MapSet.new(actual) == MapSet.new(expected),
"""
Points mismatch.
Actual: #{inspect(actual)}
Expected: #{inspect(expected)}
"""
end
end
defmodule Pento.Game.BoardTest do
use ExUnit.Case, async: true
alias Pento.Game.{Board, Pentomino}
@all_palettes [:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t]
@small_palette [:u, :v, :p]
describe "new board" do
test ":tiny" do
board = Board.new(:tiny)
assert_pallette_equal(board.palette, @small_palette)
assert_points_equal(board.points, for(x <- 1..5, y <- 1..3, do: {x, y}))
end
test ":default" do
board = Board.new(:default)
assert_pallette_equal(board.palette, @all_palettes)
assert_points_equal(board.points, for(x <- 1..10, y <- 1..6, do: {x, y}))
end
test ":medium" do
board = Board.new(:medium)
assert_pallette_equal(board.palette, @all_palettes)
assert_points_equal(board.points, for(x <- 1..12, y <- 1..5, do: {x, y}))
end
test ":wide" do
board = Board.new(:wide)
assert_pallette_equal(board.palette, @all_palettes)
assert_points_equal(board.points, for(x <- 1..15, y <- 1..4, do: {x, y}))
end
test ":widest" do
board = Board.new(:widest)
assert_pallette_equal(board.palette, @all_palettes)
assert_points_equal(board.points, for(x <- 1..20, y <- 1..3, do: {x, y}))
end
test ":skew" do
board = Board.new(:skew)
assert_pallette_equal(board.palette, @all_palettes)
expected_points =
for x <- 1..10, y <- 1..6, do: {x, rem(x + y, 2) + y}
assert_points_equal(board.points, expected_points)
end
end
describe "to_shape" do
setup [:create_board]
test "converts board to shape", %{board: board} do
shape = Board.to_shape(board)
assert shape.color == :purple
assert shape.name == :board
assert_points_equal(shape.points, board.points)
end
test "add pentomino to board and convert to shape", %{board: board} do
pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
board = %{board | active_pento: pento}
shape = Board.to_shape(board)
assert shape.color == :purple
assert shape.name == :board
assert_points_equal(shape.points, board.points)
end
test "add two pentominoes to board and convert to shape", %{board: board} do
pento1 = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
pento2 = Pentomino.new(name: :l, rotation: 180, reflected: true, location: {7, 7})
board = %{board | active_pento: pento1, completed_pentos: [pento2]}
shape = Board.to_shape(board)
assert shape.color == :purple
assert shape.name == :board
assert_points_equal(shape.points, board.points)
end
end
describe "active?" do
setup [:create_board]
test "active pentomino", %{board: board} do
pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
board = %{board | active_pento: pento}
assert Board.active?(board, :p)
end
test "no active pentomino", %{board: board} do
assert not Board.active?(board, :p)
end
test "active pentomino with different name", %{board: board} do
pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
board = %{board | active_pento: pento}
assert not Board.active?(board, :l)
end
end
defp create_board(_content) do
board = Board.new(:default)
{:ok, %{board: board}}
end
defp assert_points_equal(actual, expected) do
assert MapSet.new(actual) == MapSet.new(expected),
"""
Points mismatch.
Actual: #{inspect(actual)}
Expected: #{inspect(expected)}
"""
end
defp assert_pallette_equal(actual, expected) do
assert MapSet.new(actual) == MapSet.new(expected),
"""
Palette mismatch.
Actual: #{inspect(actual)}
Expected: #{inspect(expected)}
"""
end
end
Now for the skewed rectangle
def new(:skew), do: new(:all, skewed_rect())
def new(_), do: new(:default)
defp rect(x, y) do
for x <- 1..x, y <- 1..y, do: {x, y}
end
defp skewed_rect() do
for x <- 1..10, y <- 1..6, do: {x, rem(x + y, 2) + y}
end
Don’t worry the test for this are in the tests above.
Now for the other direction for the rotation.
def rotate(%{rotation: degrees} = p, clockwise \\ :clockwise) do
degrees = if clockwise == :counterclockwise, do: 360 - 90 + degrees, else: degrees + 90
%{p | rotation: rem(degrees, 360)}
end
This will work for either way. Also tests for this are above.