//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), ) }