doodle/pkg/drawtool/history_test.go
Noah Petherbridge c8620f871e Drawing Strokes and Undo/Redo Functionality
* Add new pkg/drawtool with utilities to abstract away drawing actions
  into Strokes and track undo/redo History for them.
* The freehand Pencil tool in EditorMode has been refactored to create a
  Stroke of Shape=Freehand and queue up its world pixels there instead
  of directly modifying the level chunker in real time. When the mouse
  button is released, the freehand Stroke is committed to the level
  chunker and added to the UndoHistory.
* UndoHistory is (temporarily) stored with the level.Level so it can
  survive trips to PlayScene and back, but is not stored as JSON on
  disk.
* Ctrl-Z and Ctrl-Y in EditorMode for undo and redo, respectively.
2019-07-03 16:25:23 -07:00

133 lines
3.5 KiB
Go

package drawtool
import (
"testing"
"git.kirsle.net/apps/doodle/lib/render"
)
func TestHistory(t *testing.T) {
// Test assertion helpers.
shouldBool := func(note string, expect, actual bool) {
if actual != expect {
t.Errorf(
"Unexpected boolean result (%s)\n"+
"Expected: %+v\n"+
" Got: %+v",
note,
expect,
actual,
)
}
}
shouldInt := func(note string, expect, actual int) {
if actual != expect {
t.Errorf(
"Unexpected integer result (%s)\n"+
"Expected: %+v\n"+
" Got: %+v",
note,
expect,
actual,
)
}
}
shouldPoint := func(note string, expect render.Point, actual *Stroke) {
if actual == nil {
t.Errorf("Missing history stroke for shouldPoint(%s)", note)
return
}
if actual.PointA != expect {
t.Errorf(
"Unexpected point result (%s)\n"+
"Expected: %+v\n"+
" Got: %+v",
note,
expect,
actual.PointA,
)
}
}
var H = NewHistory(10)
// Add and remove and re-add the first element.
H.AddStroke(&Stroke{
PointA: render.NewPoint(999, 999),
})
shouldInt("first element", 1, H.Size())
shouldBool("can undo first element", true, H.Undo())
shouldBool("latest should be null", true, H.Latest() == nil)
H = NewHistory(10)
shouldBool("can't Undo with fresh history", false, H.Undo())
shouldInt("size should be zero", 0, H.Size())
H.AddStroke(&Stroke{
PointA: render.NewPoint(1, 1),
})
shouldInt("after first stroke", 1, H.Size())
shouldPoint("head is the newest point", render.NewPoint(1, 1), H.Latest())
H.AddStroke(&Stroke{
PointA: render.NewPoint(2, 2),
})
shouldInt("after second stroke", 2, H.Size())
shouldPoint("head is the newest point", render.NewPoint(2, 2), H.Latest())
// Undo.
shouldBool("undo second stroke", true, H.Undo())
shouldInt("after undo the future stroke is still part of the size", 2, H.Size())
shouldPoint("after undo, the newest point", render.NewPoint(1, 1), H.Latest())
// Redo.
shouldBool("redo second stroke", true, H.Redo())
shouldInt("after redo second stroke, size is still the same", 2, H.Size())
shouldPoint("after redo, the newest point", render.NewPoint(2, 2), H.Latest())
// Another redo must fail.
shouldBool("redo when there is nothing to redo", false, H.Redo())
// Add a few more points.
for i := 3; i <= 6; i++ {
H.AddStroke(&Stroke{
PointA: render.NewPoint(int32(i), int32(i)),
})
}
shouldInt("after adding more strokes", 6, H.Size())
shouldPoint("last point added", render.NewPoint(6, 6), H.Latest())
// Undo a few times.
shouldBool("undo^1", true, H.Undo())
shouldBool("undo^2", true, H.Undo())
shouldBool("undo^3", true, H.Undo())
shouldInt("after a few undos, the size still contains future history", 6, H.Size())
// A new stroke invalidates the future history.
H.AddStroke(&Stroke{
PointA: render.NewPoint(7, 7),
})
shouldInt("after new history, size is recapped to tail", 4, H.Size())
shouldBool("can't Redo after new point added", false, H.Redo())
// Overflow past our history size to test rollover.
for i := 8; i <= 16; i++ {
H.AddStroke(&Stroke{
PointA: render.NewPoint(int32(i), int32(i)),
})
}
shouldInt("after tons of new history, size is capped out", 10, H.Size())
shouldPoint("after overflow, latest point", render.NewPoint(16, 16), H.Latest())
shouldPoint("after overflow, first point", render.NewPoint(7, 7), H.Oldest())
// Undo back to beginning.
for i := 0; i < H.Size(); i++ {
shouldBool("bulk undo to beginning", true, H.Undo())
}
shouldBool("after bulk undo, tail", true, H.Latest() == nil)
shouldBool("can't undo further", false, H.Undo())
}