//go:build browser
package slidestest
// The image drive (plan task 6): add an image block, describe it, pick
// a real PNG off disk, and watch it go browser → app → object store →
// back onto the stage as an the server rendered. Then reload, and
// find it still there.
//
// It runs against the store development binds (blobs.Dir), which has no
// presigning, so the PUT is the same-origin one this app accepts —
// exactly the path scripts/run-dev.sh takes. The presigned half is
// covered without a browser in assets_test.go, because a real bucket is
// not something a test can have.
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/chromedp/chromedp"
)
// TestBrowserUploadsAnImage is the whole §2.7 path in a real browser.
func TestBrowserUploadsAnImage(t *testing.T) {
rig, mailer := newRigWithStore(t)
browserSignIn(t, rig, mailer)
// A real file on disk: chromedp hands the a
// path, the same as a person picking one.
png := filepath.Join(t.TempDir(), "dot.png")
if err := os.WriteFile(png, onePixelPNG, 0o600); err != nil {
t.Fatalf("write the test PNG: %v", err)
}
var deck string
rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
const bound = `document.querySelector('[slides-stage] [data-block="heading"]')?.getAttribute('contenteditable') === 'true'`
const saved = `document.querySelector('[estilo-save-status]').dataset.state === 'saved'`
rig.Run(
chromedp.Navigate(rig.Origin+deck+"/edit"),
chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
waitFor(bound),
)
// With a store bound the page says so and the picker is live.
rig.Run(
waitFor(`document.querySelector('[slides-editor]').dataset.store === '1'`),
waitFor(`document.querySelector('[slides-asset-input]').disabled === false`),
)
// An image block, from the inspector's own Add block menu. The
// menu is a , so it is opened before its button is
// pressed — the same two moves a person makes.
rig.Run(
chromedp.Evaluate(`(() => {
document.querySelector('[slides-add-block]').open = true
document.querySelector('[data-add-block="image"]').click()
return true
})()`, nil),
waitFor(`document.querySelector('[slides-stage] [data-block="image"]') !== null`),
waitFor(saved),
)
// An empty frame until there is something in it (§2.4's
// data-missing-asset), which is what the person is about to fix.
rig.Run(waitFor(`document.querySelector('[slides-stage] [data-block="image"]').dataset.missingAsset === 'true'`))
rig.Screen("[slides-editor]", "an empty image block")
// Describe it first: the file input refuses to move bytes for an
// image with no alt text and no decorative mark, and says so.
rig.Run(
chromedp.SetUploadFiles(`[slides-asset-input]`, []string{png}, chromedp.ByQuery),
waitFor(`document.querySelector('[slides-asset-status]').dataset.state === 'error'`),
waitFor(`document.querySelector('[slides-asset-status]').textContent.trim().length > 0`),
)
var refused string
rig.Run(chromedp.Text(`[slides-asset-status]`, &refused, chromedp.ByQuery))
if refused == "" || strings.HasPrefix(refused, "slides.") {
t.Fatalf("the refusal reads %q, want a resolved catalog sentence", refused)
}
rig.Run(
setValue(`#rst-block-alt`, "A single red dot"),
waitFor(saved),
waitFor(`document.querySelector('[slides-stage] [data-block="image"]') !== null`),
)
// Now the upload. The stage's is the SERVER's rendering,
// re-fetched after the op saved — the browser never draws a slide.
rig.Run(
chromedp.SetUploadFiles(`[slides-asset-input]`, []string{png}, chromedp.ByQuery),
waitFor(`document.querySelector('[slides-stage] [data-block="image"] img') !== null`),
waitFor(saved),
)
rig.Screen("[slides-editor]", "the image on the stage")
var src, alt string
rig.Run(
chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "src", &src, nil, chromedp.ByQuery),
chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "alt", &alt, nil, chromedp.ByQuery),
)
wantPrefix := deck + "/assets/"
if !strings.HasPrefix(src, wantPrefix) {
t.Errorf("the image src is %q, want it under %q", src, wantPrefix)
}
if alt != "A single red dot" {
t.Errorf("alt = %q, want the description that was typed", alt)
}
// The ref in the src is the content hash the store keyed the bytes
// under, not the deck's own id for the asset.
if ref := strings.TrimPrefix(src, wantPrefix); ref != hashOf(onePixelPNG) {
t.Errorf("the src names %q, want the content hash %s", ref, hashOf(onePixelPNG))
}
// And the browser really fetched it: an that 404'd would have
// failed the rig, and a decoded one has a natural width.
var decoded bool
rig.Run(chromedp.Evaluate(`(() => {
const i = document.querySelector('[slides-stage] [data-block="image"] img')
return !!i && i.complete && i.naturalWidth > 0
})()`, &decoded))
if !decoded {
t.Error("the browser did not decode the image it was served")
}
// The status line says it finished, in the catalog's words.
var done string
rig.Run(chromedp.Text(`[slides-asset-status]`, &done, chromedp.ByQuery))
if done == "" || strings.HasPrefix(done, "slides.") {
t.Errorf("the status line reads %q after a finished upload", done)
}
// Nothing is unused while the block points at it.
var unused int
rig.Run(chromedp.Evaluate(`document.querySelectorAll('[slides-unused-list] > li').length`, &unused))
if unused != 0 {
t.Errorf("%d unused images while one block uses the only one", unused)
}
// A reload: the deck's own document carries it, so the server
// renders the same picture without the bundle having to.
rig.Run(
chromedp.Reload(),
chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
waitFor(bound),
waitFor(`document.querySelector('[slides-stage] [data-block="image"] img') !== null`),
)
rig.Screen("[slides-editor]", "the image after a reload")
var again string
rig.Run(chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "src", &again, nil, chromedp.ByQuery))
if again != src {
t.Errorf("after a reload the image src is %q, want %q", again, src)
}
// Delete the block and the registration stays (§2.7), which is
// what the Unused images list is for — and Remove clears it.
rig.Run(
chromedp.Click(`[slides-stage] [data-block="image"]`, chromedp.ByQuery),
waitFor(`document.querySelector('[slides-inspector-form][data-section="block"]').checkVisibility() === true`),
chromedp.Click(`[data-command="block-delete"]`, chromedp.ByQuery),
waitFor(`document.querySelector('[slides-stage] [data-block="image"]') === null`),
waitFor(saved),
waitFor(`document.querySelectorAll('[slides-unused-list] > li').length === 1`),
)
rig.Screen("[slides-editor]", "one unused image")
rig.Run(
chromedp.Click(`[slides-unused-list] [data-remove-asset]`, chromedp.ByQuery),
waitFor(`document.querySelectorAll('[slides-unused-list] > li').length === 0`),
waitFor(`document.querySelector('[slides-unused-none]').checkVisibility() === true`),
waitFor(saved),
)
}