Files

220 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package editor
import (
"bytes"
stdctx "context"
"errors"
"image"
"os"
"path/filepath"
"testing"
"aego/art"
"aego/asset/importers/artsource"
"aego/asset/pipeline"
"aego/core/log"
"aego/editor/core/artedit"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/workspace"
"aego/editor/ext"
"aego/project"
"aego/ui/core"
)
func workspaceBlankTestHost(t *testing.T, p *project.Project, pl *pipeline.Pipeline) (*context.Editor, *workspace.Workspace, *ext.Registry) {
t.Helper()
commands := command.New()
docs := document.NewManager(document.Options{Root: p.Root, SourceWrite: pl.GuardSourceWrite})
ed := &context.Editor{Project: p, Pipeline: pl, Docs: docs, Commands: commands, UI: core.New(nil)}
ws := workspace.New(p.Root, nil)
for _, id := range []string{workspace.Design, workspace.Art} {
if err := ws.RegisterPreset(workspace.Preset{ID: id}); err != nil {
t.Fatal(err)
}
}
registry := ext.New(ext.Deps{Editor: ed, Workspace: ws, Commands: commands})
if err := registry.Run("render2d", imageHooks); err != nil {
t.Fatal(err)
}
ws.LoadLayout()
return ed, ws, registry
}
func workspaceBlankFixture(t *testing.T) (*project.Project, *pipeline.Pipeline, *context.Editor, *workspace.Workspace, *ext.Registry) {
t.Helper()
p, err := project.Create(t.TempDir(), "Blank canvas", false)
if err != nil {
t.Fatal(err)
}
importers := pipeline.NewImporterRegistry()
if err := importers.Register(artsource.New()); err != nil {
t.Fatal(err)
}
if err := importers.Register(artsource.NewTexture()); err != nil {
t.Fatal(err)
}
pl, err := pipeline.Open(p, importers, log.Nop())
if err != nil {
t.Fatal(err)
}
t.Cleanup(pl.Close)
if _, _, err := pl.Scan(); err != nil {
t.Fatal(err)
}
batch, err := pl.ImportAll()
if err != nil || batch == nil || len(batch.Failed) != 0 || len(batch.Retry) != 0 {
t.Fatalf("fixture import failed: batch=%+v, error=%v", batch, err)
}
ed, ws, registry := workspaceBlankTestHost(t, p, pl)
return p, pl, ed, ws, registry
}
func enterBlankArt(t *testing.T, ed *context.Editor, ws *workspace.Workspace, registry *ext.Registry) (*document.AssetDocument, *artedit.Document) {
t.Helper()
if !ws.Switch(workspace.Art) {
t.Fatal("Art workspace is unavailable")
}
registry.RunTicks()
doc := ed.Docs.AssetDocument(artWorkspaceBlankID)
if doc == nil || !doc.Transient {
t.Fatal("entering Art did not open an in-memory drawing")
}
model, ok := doc.Binding.Model.(*artedit.Document)
if !ok || !model.Draft {
t.Fatal("blank canvas did not bind the shared Art model")
}
return doc, model
}
// These tests describe the desired workflow; they are deferred, not evidence
// that this mixed tree has passed UI, CI, or real OS acceptance.
func TestArtWorkspaceBlankDrawSwitchAndSaveAsAsset(t *testing.T) {
_, pl, ed, ws, registry := workspaceBlankFixture(t)
doc, model := enterBlankArt(t, ed, ws, registry)
if doc.Dirty() || model.PendingRecovery || model.Session.Document.Info().Canvas.Width != 32 || model.Session.Document.Info().Canvas.Height != 32 {
t.Fatal("default blank must be a clean editable 32×32 canvas")
}
for _, name := range []string{"assets/drawing.aeart", "assets/drawing.aeart.meta"} {
if got, err := pl.SourceBytes(name); err != nil || got != nil {
t.Fatalf("entering Art created %s: %v", name, err)
}
}
if _, err := model.Execute(stdctx.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 4, Y: 5}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{R: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if !doc.Dirty() || model.History.Cursor() != 1 {
t.Fatal("painting did not use the document's shared dirty state and history")
}
if !ws.Switch(workspace.Design) {
t.Fatal("Design workspace is unavailable")
}
registry.RunTicks()
if !ws.Switch(workspace.Art) {
t.Fatal("could not return to Art")
}
registry.RunTicks()
if ed.Docs.AssetDocument(artWorkspaceBlankID) != doc || doc.Binding.Model != model || !doc.Dirty() || model.History.Cursor() != 1 {
t.Fatal("workspace switching replaced the dirty blank drawing")
}
if ws.Focused() != artHandlerID+"/"+doc.Asset.String() {
t.Fatal("returning to Art did not focus the existing drawing")
}
if err := ed.SaveAsset(doc); !errors.Is(err, document.ErrSavePending) {
t.Fatal("first Save did not wait for a path", err)
}
questions := ed.TakeQuestions()
if len(questions) != 1 || questions[0].Submit == nil {
t.Fatal("first Save did not offer an asset path")
}
questions[0].Submit([]string{"assets/first-drawing.aeart"})
id, ok := pl.Lookup("assets/first-drawing.aeart")
if !ok {
notice, _ := ed.LastNotice()
t.Fatalf("confirmed drawing was not imported: %+v", notice)
}
saved := ed.Docs.AssetDocument(id)
if saved == nil || saved.Transient || saved.Binding.Model != model || model.Draft || saved.Dirty() || model.History.Cursor() != 1 || ed.Docs.AssetDocument(artWorkspaceBlankID) != nil {
t.Fatal("first Save did not transfer the same clean session and history")
}
pixels, _, err := model.Capture(model.Session.Frame, art.Albedo)
if err != nil || pixels.NRGBAAt(4, 5).R != 255 || pixels.NRGBAAt(4, 5).A != 255 {
t.Fatal("the saved drawing lost its pixels", err)
}
if err := model.SessionUndo(); err != nil || !saved.Dirty() {
t.Fatal("Undo after first Save lost its history", err)
}
if err := model.SessionRedo(); err != nil || saved.Dirty() {
t.Fatal("Redo did not return to the saved generation", err)
}
}
func TestArtWorkspaceBlankSaveDoesNotOverwriteExistingFiles(t *testing.T) {
p, _, ed, ws, registry := workspaceBlankFixture(t)
doc, model := enterBlankArt(t, ed, ws, registry)
if _, err := model.Execute(stdctx.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 2, Y: 3}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{G: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if err := ed.SaveAsset(doc); !errors.Is(err, document.ErrSavePending) {
t.Fatal(err)
}
questions := ed.TakeQuestions()
if len(questions) != 1 || questions[0].Submit == nil {
t.Fatal("missing Save prompt")
}
target := filepath.Join(p.Root, "assets", "occupied.aeart")
other := []byte("another person's source")
if err := os.WriteFile(target, other, 0o600); err != nil {
t.Fatal(err)
}
questions[0].Submit([]string{"assets/occupied.aeart"})
if got, err := os.ReadFile(target); err != nil || !bytes.Equal(got, other) {
t.Fatal("Save overwrote an externally created destination", err)
}
if !doc.Dirty() || doc.Binding.Model != model || model.History.Cursor() != 1 {
t.Fatal("a rejected Save lost unsaved pixels or history")
}
if notice, ok := ed.LastNotice(); !ok || notice.Level != context.LevelError {
t.Fatal("the rejected destination was not reported")
}
}
func TestArtWorkspaceBlankRecoveryRequiresExplicitAction(t *testing.T) {
p, pl, ed, ws, registry := workspaceBlankFixture(t)
doc, model := enterBlankArt(t, ed, ws, registry)
if _, err := model.Execute(stdctx.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 3, Y: 6}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{B: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if err := ed.Docs.WriteAssetRecovery(doc); err != nil {
t.Fatal(err)
}
file := filepath.Join(p.Root, document.RecoveryDir, "asset-"+artWorkspaceBlankID.String()+".recovery")
before, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
// A fresh manager represents reopening the project after an interruption.
next, nextWS, nextRegistry := workspaceBlankTestHost(t, p, pl)
reopened, recovered := enterBlankArt(t, next, nextWS, nextRegistry)
if !recovered.PendingRecovery || reopened.Dirty() {
t.Fatal("an earlier blank was not offered for explicit recovery")
}
if _, err := recovered.Execute(stdctx.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 0, Y: 0}}, Brush: art.Brush{Size: 1}}); err == nil {
t.Fatal("editing bypassed the unresolved recovery")
}
if got, err := os.ReadFile(file); err != nil || !bytes.Equal(got, before) {
t.Fatal("opening the blank changed the recovery", err)
}
if err := recovered.Recover(); err != nil || !reopened.Dirty() || recovered.PendingRecovery {
t.Fatal("explicit blank recovery failed", err)
}
pixels, _, err := recovered.Capture(recovered.Session.Frame, art.Albedo)
if err != nil || pixels.NRGBAAt(3, 6).B != 255 || pixels.NRGBAAt(3, 6).A != 255 {
t.Fatal("explicit recovery lost the unsaved drawing", err)
}
if got, err := pl.SourceBytes("assets/drawing.aeart"); err != nil || got != nil {
t.Fatal("recovery created a project asset without Save", err)
}
}