Files
aego/editor/shell/art_layout_ux_test.go
sanspie 7fb4acc6e4
ci-platforms / darwin amd64 smoke (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m21s
ci / checks (push) Failing after 21m35s
ci / release (push) Skipped
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: size art timeline to its controls
2026-11-10 00:57:53 +03:00

162 lines
7.1 KiB
Go

package shell
import (
"reflect"
"strings"
"testing"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/workspace"
"aego/platform/input"
"aego/render/text"
"aego/ui/core"
"aego/ui/dock"
)
// Geometry checks are not a replacement for the deferred native screenshots,
// HiDPI testing, physical input, or validation of a saved user workspace.
func TestArtFactorySeparatesToolsCanvasLayersAndPalette(t *testing.T) {
w := workspace.New(t.TempDir(), nil)
for _, id := range []string{workspace.Assets, workspace.Scene, workspace.Hierarchy, workspace.Inspector, "art.tools", "art.options", "art.properties", "art.palette", "art.color", "art.layers", "art.channels", "art.selection", "art.info", "art.timeline", "art.preview", "art.asset_preview"} {
if err := w.Register(workspace.Desc{Panel: defaultLayoutPanel(id)}); err != nil {
t.Fatal(err)
}
}
for _, preset := range BuiltinWorkspaces() {
if err := w.RegisterPreset(preset); err != nil {
t.Fatal(err)
}
}
w.LoadLayout()
if !w.Switch(workspace.Art) {
t.Fatal("Art workspace is unavailable")
}
if w.Tree.Has(workspace.Hierarchy) || w.Tree.Has(workspace.Inspector) {
t.Fatal("scene panels occupy the Art factory layout")
}
tools, options := w.Tree.FindPanel("art.tools"), w.Tree.FindPanel("art.options")
if tools == 0 || options == 0 || tools == options {
t.Fatal("Art Tools and Tool Options must be independent panels")
}
if w.Tree.FindPanel(workspace.Assets) == 0 || w.Tree.FindPanel("art.timeline") == 0 || w.Tree.FindPanel(workspace.Assets) == w.Tree.FindPanel("art.timeline") {
t.Fatal("Assets and Animation Timeline must have separate bottom docks")
}
for _, size := range []mathx.Vec2{{X: 800, Y: 600}, {X: 1366, Y: 768}, {X: 1920, Y: 1080}, {X: 2560, Y: 1440}} {
w.Tree.Layout(mathx.Rect{W: size.X, H: size.Y})
var rects [9]mathx.Rect
for i, id := range []string{"art.tools", workspace.Scene, "art.layers", "art.palette", workspace.Assets, "art.preview", "art.options", "art.color", "art.timeline"} {
r, ok := w.Tree.PanelRect(id)
if !ok || r.W <= 0 || r.H <= 0 {
t.Fatalf("%v: %s is not visible: %+v", size, id, r)
}
rects[i] = r
}
if rects[0].W > size.X*.17 || rects[1].W < size.X*.5 || rects[1].H <= rects[4].H {
t.Fatalf("%v: unreasonable factory proportions: %+v", size, rects)
}
if rects[0].X != rects[6].X || rects[0].W != rects[6].W || rects[0].Y+rects[0].H > rects[6].Y {
t.Fatalf("%v: Tool Options must be below Art Tools: %+v", size, rects)
}
if rects[4].Y < rects[1].Y+rects[1].H {
t.Fatalf("%v: Assets must be below the canvas: %+v", size, rects)
}
if rects[4].Y != rects[8].Y || rects[4].H != rects[8].H || rects[4].X+rects[4].W > rects[8].X || rects[4].W < size.X*.12 || rects[8].W < min(320, size.X*.35) {
t.Fatalf("%v: Timeline and Assets must remain usable beside each other: %+v", size, rects)
}
if rects[8].H > size.Y*.22 {
t.Fatalf("%v: Timeline takes too much vertical space from the canvas: %+v", size, rects[8])
}
if w.Tree.FindPanel("art.layers") == w.Tree.FindPanel("art.palette") {
t.Fatal("layers and palette are still in one tab group")
}
if w.Tree.FindPanel("art.color") == w.Tree.FindPanel("art.palette") {
t.Fatal("color picker and palette must have separate docks")
}
if rects[3].X != rects[7].X || rects[3].W != rects[7].W || rects[3].Y+rects[3].H > rects[7].Y || rects[7].Y+rects[7].H > rects[5].Y {
t.Fatalf("%v: palette, color picker and preview must be stacked in that order: %+v", size, rects)
}
if w.Tree.Has("art.selection") || w.Tree.Has("art.info") || w.Tree.Has("art.asset_preview") {
t.Fatal("optional Art panels overcrowd the factory tab strip")
}
}
for _, id := range []string{"art.selection", "art.info", "art.asset_preview"} {
w.Show(id)
if !w.Tree.Has(id) {
t.Fatalf("optional panel %s cannot be opened", id)
}
}
}
func TestAssetClickKeepsSharedInspectorTabFromHidingAssets(t *testing.T) {
w := workspace.New(t.TempDir(), nil)
w.Tree = dock.Standard(nil, []string{workspace.Assets}, nil, []string{workspace.Inspector})
if !assetClickMayRevealInspector(w) {
t.Fatal("an independently docked inspector should still follow asset selection")
}
if !w.Tree.Undock(workspace.Inspector) || assetClickMayRevealInspector(w) {
t.Fatal("single-click inspection must not reopen a closed panel and move asset tiles")
}
w.Tree.DockPanel(workspace.Inspector, w.Tree.FindPanel(workspace.Assets), dock.SideRight)
w.Tree.DockPanel(workspace.Inspector, w.Tree.FindPanel(workspace.Assets), dock.SideCenter)
w.Tree.Activate(workspace.Assets)
if assetClickMayRevealInspector(w) {
t.Fatal("single-click inspection would hide Assets in the same tab group")
}
if !w.Tree.ToggleMaximize(workspace.Assets) || assetClickMayRevealInspector(w) {
t.Fatal("single-click inspection would disturb maximized Assets")
}
w.Tree.Restore()
w.Tree.DockPanel(workspace.Inspector, w.Tree.FindPanel(workspace.Assets), dock.SideRight)
if err := w.RegisterPreset(workspace.Preset{ID: workspace.Art}); err != nil {
t.Fatal(err)
}
if assetClickMayRevealInspector(w) {
t.Fatal("browsing Art should not force open or activate Inspector")
}
}
// The actual prompt must evaluate the destructive predicate after live text
// input, not only on focus loss. Art's filesystem predicate is tested in the
// render2d editor package; this tests its real modal presentation boundary.
func TestArtSavePromptUpdatesReplaceWhileTyping(t *testing.T) {
font := text.NewFont(14, 1, 16, 12)
for r := rune(32); r <= 0x04ff; r++ {
font.SetGlyph(r, text.FontGlyph{Advance: 8})
}
ui, state := core.New(font), &input.State{}
var submitted []string
q := context.Question{
Title: "Destination", Fields: []string{"Source path"}, Values: []string{"assets/new.aeart"},
Buttons: []string{"Save", "Cancel"}, DestructiveLabel: "Replace",
DestructiveWhen: func(values []string) bool { return len(values) == 1 && values[0] == "./assets/existing.aeart" },
Submit: func(values []string) { submitted = append([]string(nil), values...) },
}
a := &App{ui: ui, modals: []modal{{kind: modalAsk, ask: q, vals: append([]string(nil), q.Values...), focus: true}}}
id := core.IDOf("test.art.save.prompt")
draw := func() string {
ui.Begin(state, mathx.Vec2{X: 720, Y: 540}, 1)
ui.OpenModal(id)
a.drawPrompt(id)
list := ui.End()
// The list is the widget's real text command buffer. It is intentionally
// inspected only in this synthetic UI test, without a GPU or OS input.
return string(reflect.ValueOf(list).Elem().FieldByName("chars").Bytes())
}
state.BeginFrame()
if labels := draw(); !strings.Contains(labels, "Save") || strings.Contains(labels, "Replace") {
t.Fatalf("new destination did not display Save: %q", labels)
}
state.BeginFrame()
state.ApplyText([]byte("./assets/existing.aeart"))
if labels := draw(); !strings.Contains(labels, "Replace") || strings.Contains(labels, "Save") {
t.Fatalf("typed existing destination did not display Replace in the same frame: %q", labels)
}
state.BeginFrame()
state.ApplyKeyDown(input.KeyEnter, 0, 0, false)
draw()
if len(submitted) != 1 || submitted[0] != "./assets/existing.aeart" {
t.Fatalf("Enter submitted the wrong live path: %v", submitted)
}
}