Files
ds-galo/backend/internal/e2e/effects_edit_test.go
iu c953008a86 feat(photos): добавить эффекты и настройки панели редактора
- текстура, чёткость, дымка, виньетка и зерно в неразрушающем редакторе
- живой WebGL2-предпросмотр эффектов и CPU-фолбэк
- секции редактора свёрнуты по умолчанию, состояние в localStorage
- вкладка «Редактирование» доступна только из просмотрщика
2026-10-24 22:57:30 +03:00

143 lines
4.3 KiB
Go

package e2e
import (
"bytes"
"context"
"path/filepath"
"testing"
"time"
"github.com/disintegration/imaging"
"github.com/gen2brain/webp"
"galo/internal/albums"
"galo/internal/photos"
)
// TestEffectsEdit covers the effects round trip: the parameters are stored in
// the photo metadata, the background worker renders the edited version, the
// saved parameters carry the normalized effects, a reset removes everything,
// and re-rendering the same edit (including grain) is deterministic.
func TestEffectsEdit(t *testing.T) {
ctx := context.Background()
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
defer closeDB()
user, _, err := st.auth.Register(ctx, "effects@test.local", "", "password123", "E")
if err != nil {
t.Fatalf("register: %v", err)
}
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Effects"})
if err != nil {
t.Fatalf("create album: %v", err)
}
p, err := st.photos.Upload(ctx, photos.UploadInput{
AlbumID: album.ID, UserID: user.ID, Filename: "e.png", Data: makePNG(t),
})
if err != nil {
t.Fatalf("upload: %v", err)
}
w := photos.NewWorker(st.photos, time.Second, 10, time.Minute, 2)
w.ProcessOnce(ctx)
params := photos.EditParams{Effects: photos.EffectsParams{
Texture: 25,
Clarity: 20,
Dehaze: 15,
Vignette: photos.VignetteParams{
Mode: photos.VignetteColor, Amount: 40, Midpoint: 60, Feather: 40, Color: "#ff8800",
},
Grain: photos.GrainParams{Amount: 35, Size: 30, Roughness: 60},
}}
edited, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params)
if err != nil {
t.Fatalf("edit: %v", err)
}
if edited.EditParams == nil {
t.Fatal("edit returned no parameters")
}
if _, ok := edited.EditParams["effects"]; !ok {
t.Fatalf("effects params not returned: %+v", edited.EditParams)
}
w.ProcessOnce(ctx)
got, err := st.photos.Get(ctx, p.ID, user.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.EditPreviewURL == "" {
t.Fatalf("no edited preview url: %+v", got)
}
if got.EditPreviewKey == "" {
t.Fatalf("no edited preview key: %+v", got)
}
effects, ok := got.EditParams["effects"].(map[string]any)
if !ok {
t.Fatalf("effects params not stored: %+v", got.EditParams)
}
vignette, ok := effects["vignette"].(map[string]any)
if !ok {
t.Fatalf("vignette params not stored: %+v", effects)
}
if vignette["mode"] != photos.VignetteColor {
t.Fatalf("vignette mode = %v, want %q", vignette["mode"], photos.VignetteColor)
}
if vignette["color"] != "#ff8800" {
t.Fatalf("vignette color = %v, want #ff8800", vignette["color"])
}
if vignette["midpoint"] != float64(60) {
t.Fatalf("vignette midpoint = %v, want 60", vignette["midpoint"])
}
grain, ok := effects["grain"].(map[string]any)
if !ok {
t.Fatalf("grain params not stored: %+v", effects)
}
if grain["size"] != float64(30) || grain["roughness"] != float64(60) {
t.Fatalf("grain params = %+v, want size 30 roughness 60", grain)
}
first := previewPixels(t, st, got.EditPreviewKey)
reset, err := st.photos.ResetEdit(ctx, p.ID, user.ID)
if err != nil {
t.Fatalf("reset: %v", err)
}
if len(reset.EditParams) != 0 {
t.Fatalf("reset kept params: %+v", reset.EditParams)
}
if reset.EditURL != "" || reset.EditPreviewURL != "" || reset.EditPreviewKey != "" {
t.Fatalf("reset kept the edited version: %+v", reset)
}
// The same parameters and the same photo id must render the same grain.
if _, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params); err != nil {
t.Fatalf("edit again: %v", err)
}
w.ProcessOnce(ctx)
again, err := st.photos.Get(ctx, p.ID, user.ID)
if err != nil {
t.Fatalf("get after second edit: %v", err)
}
if again.EditPreviewURL == "" || again.EditPreviewKey == "" {
t.Fatalf("no edited preview after second edit: %+v", again)
}
second := previewPixels(t, st, again.EditPreviewKey)
if !bytes.Equal(first, second) {
t.Fatal("repeated render with the same grain parameters is not deterministic")
}
}
// previewPixels decodes an edited preview object into its pixel bytes.
func previewPixels(t *testing.T, st *stack, key string) []uint8 {
t.Helper()
data, err := st.storage.Get(context.Background(), key)
if err != nil {
t.Fatalf("read preview object: %v", err)
}
img, err := webp.Decode(bytes.NewReader(data))
if err != nil {
t.Fatalf("decode preview: %v", err)
}
return imaging.Clone(img).Pix
}