package loki
import (
"os"
"strconv"
"strings"
"testing"
"time"
)
// TestCDPSmoke pilote un vrai Chrome de bout en bout : lancement, navigation,
// snapshot d'accessibilité, click, screenshot. Coûteux et dépendant d'un
// navigateur installé → ne tourne QUE si LOKI_CU_SMOKE=1.
func TestCDPSmoke(t *testing.T) {
if os.Getenv("LOKI_CU_SMOKE") != "1" {
t.Skip("smoke test désactivé (LOKI_CU_SMOKE=1 pour l'activer)")
}
if chromePath() == "" {
t.Skip("aucun navigateur détecté")
}
s, err := cdpGet()
if err != nil {
t.Fatalf("cdpGet: %v", err)
}
defer cdpShutdown()
if err := s.navigate("https://example.com"); err != nil {
t.Fatalf("navigate: %v", err)
}
snap, err := s.snapshot()
if err != nil {
t.Fatalf("snapshot: %v", err)
}
t.Logf("snapshot:\n%s", snap)
if !strings.Contains(strings.ToLower(snap), "example") {
t.Errorf("titre 'example' attendu dans le snapshot, absent")
}
if !strings.Contains(snap, "link") {
t.Errorf("le lien 'More information' devrait apparaître comme [n] link")
}
// browser_find doit retrouver le lien par son texte.
found, err := s.find("more")
if err != nil {
t.Fatalf("find: %v", err)
}
if !strings.Contains(strings.ToLower(found), "link") {
t.Errorf("browser_find('more') devrait trouver le lien, got: %s", found)
}
// Le screenshot doit produire des octets PNG non vides.
png, err := s.screenshot()
if err != nil {
t.Fatalf("screenshot: %v", err)
}
if len(png) < 100 {
t.Errorf("screenshot suspicieusement petit: %d octets", len(png))
}
t.Logf("screenshot: %d octets", len(png))
}
// TestCDPTypeReplacesField vérifie le fix du test IONOS #4 : browser_type(ref) cible le
// BON champ et REMPLACE son contenu, même avec deux champs et une valeur existante.
func TestCDPTypeReplacesField(t *testing.T) {
if os.Getenv("LOKI_CU_SMOKE") != "1" {
t.Skip("smoke test désactivé (LOKI_CU_SMOKE=1 pour l'activer)")
}
if chromePath() == "" {
t.Skip("aucun navigateur détecté")
}
s, err := cdpGet()
if err != nil {
t.Fatalf("cdpGet: %v", err)
}
defer cdpShutdown()
// Deux champs texte injectés dans la page courante ; le 2e contient « ancien ».
if _, err := s.eval(`document.body.innerHTML='';'ok'`); err != nil {
t.Fatalf("inject: %v", err)
}
if _, err := s.snapshot(); err != nil { // pose les data-loki-ref
t.Fatalf("snapshot: %v", err)
}
// Retrouve le numéro du 2e champ (celui à remplacer).
val, err := s.eval(`document.querySelector('#b').getAttribute('data-loki-ref')`)
if err != nil {
t.Fatalf("eval ref: %v", err)
}
ref, _ := strconv.Atoi(strings.Trim(string(val), `"`))
if ref == 0 {
t.Fatalf("pas de ref sur #b (snapshot: %s)", val)
}
if err := s.typeInto(ref, "nouveau"); err != nil {
t.Fatalf("typeInto: %v", err)
}
got, _ := s.eval(`document.querySelector('#b').value`)
if v := strings.Trim(string(got), `"`); v != "nouveau" {
t.Errorf("champ #b = %q, attendu \"nouveau\" (remplacement raté)", v)
}
// Le premier champ ne doit PAS avoir été touché.
got2, _ := s.eval(`document.querySelector('#a').value`)
if v := strings.Trim(string(got2), `"`); v != "" {
t.Errorf("champ #a = %q, attendu vide (mauvais ciblage)", v)
}
}
// TestCDPSnapshotHygiene vérifie que le snapshot (1) masque la valeur d'un champ
// mot de passe et (2) ne liste pas les conteneurs non-actionnables (un
/
).
func TestCDPSnapshotHygiene(t *testing.T) {
if os.Getenv("LOKI_CU_SMOKE") != "1" {
t.Skip("smoke test désactivé (LOKI_CU_SMOKE=1 pour l'activer)")
}
if chromePath() == "" {
t.Skip("aucun navigateur détecté")
}
s, err := cdpGet()
if err != nil {
t.Fatalf("cdpGet: %v", err)
}
defer cdpShutdown()
inject := `document.body.innerHTML='` +
`` +
`` +
`
bloc conteneur
` +
`un lien';'ok'`
if _, err := s.eval(inject); err != nil {
t.Fatalf("inject: %v", err)
}
snap, err := s.snapshot()
if err != nil {
t.Fatalf("snapshot: %v", err)
}
t.Logf("snapshot:\n%s", snap)
if strings.Contains(snap, "SECRET123") {
t.Error("la valeur du champ mot de passe ne doit PAS apparaître dans le snapshot")
}
if strings.Contains(snap, "bloc conteneur") {
t.Error("un
conteneur ne doit pas figurer dans la liste des éléments actionnables")
}
if !strings.Contains(snap, "un lien") {
t.Error("le lien actionnable devrait figurer dans le snapshot")
}
// Le champ mot de passe lui-même reste listé (par son label), juste sans valeur.
if !strings.Contains(strings.ToLower(snap), "mot de passe") {
t.Error("le champ mot de passe devrait apparaître par son label")
}
// Un champ texte non-secret DOIT montrer sa valeur (observabilité du formulaire).
if !strings.Contains(snap, "a@b.co") {
t.Error("la valeur d'un champ texte non-secret devrait apparaître dans le snapshot")
}
// Un doit être annoncé « textbox », pas « text » (qui ressemble
// à du texte statique et faisait croire au modèle que c'était une étiquette).
if !strings.Contains(snap, "textbox") {
t.Error("un champ de saisie devrait avoir le rôle 'textbox'")
}
}
// TestCDPRefUniqueness reproduit le bug du test IONOS #9 : un snapshot doit
// EFFACER les anciens data-loki-ref, sinon deux éléments partagent un numéro et
// browser_type/browser_click visent le mauvais champ (querySelector prend le 1er du DOM).
func TestCDPRefUniqueness(t *testing.T) {
if os.Getenv("LOKI_CU_SMOKE") != "1" {
t.Skip("smoke test désactivé (LOKI_CU_SMOKE=1 pour l'activer)")
}
if chromePath() == "" {
t.Skip("aucun navigateur détecté")
}
s, err := cdpGet()
if err != nil {
t.Fatalf("cdpGet: %v", err)
}
defer cdpShutdown()
if _, err := s.eval(`document.body.innerHTML='ABC';'ok'`); err != nil {
t.Fatalf("inject: %v", err)
}
if _, err := s.snapshot(); err != nil {
t.Fatalf("snapshot: %v", err)
}
// Simule un attribut périmé laissé par un snapshot antérieur sur un AUTRE élément.
if _, err := s.eval(`document.querySelectorAll('a')[2].setAttribute('data-loki-ref','1');'ok'`); err != nil {
t.Fatalf("stale: %v", err)
}
// Un nouveau snapshot doit repartir propre : chaque numéro sur UN SEUL élément.
if _, err := s.snapshot(); err != nil {
t.Fatalf("snapshot2: %v", err)
}
dup, _ := s.eval(`document.querySelectorAll('[data-loki-ref="1"]').length`)
if strings.TrimSpace(string(dup)) != "1" {
t.Errorf("data-loki-ref=\"1\" devrait être unique, trouvé %s occurrence(s)", dup)
}
}
// TestCDPSettleWaitsAsync vérifie que settle() attend le contenu injecté en
// asynchrone (cas du test #10 où le modèle devait « bash sleep » avant snapshot).
func TestCDPSettleWaitsAsync(t *testing.T) {
if os.Getenv("LOKI_CU_SMOKE") != "1" {
t.Skip("smoke test désactivé (LOKI_CU_SMOKE=1 pour l'activer)")
}
if chromePath() == "" {
t.Skip("aucun navigateur détecté")
}
s, err := cdpGet()
if err != nil {
t.Fatalf("cdpGet: %v", err)
}
defer cdpShutdown()
// Page réelle (réseau réel), puis on déclenche un fetch AJAX qui n'ajoute son
// lien qu'une fois la requête terminée. settle() doit attendre l'inactivité
// réseau et donc voir le lien.
if err := s.navigate("https://example.com"); err != nil {
t.Fatalf("navigate: %v", err)
}
if _, err := s.eval(`fetch(location.href,{cache:'no-store'}).then(function(){var a=document.createElement('a');a.href='#';a.textContent='AjaxDone';document.body.appendChild(a);});'ok'`); err != nil {
t.Fatalf("fetch: %v", err)
}
time.Sleep(60 * time.Millisecond) // laisse requestWillBeSent s'enregistrer
s.settle() // doit attendre la fin du fetch
snap, err := s.snapshot()
if err != nil {
t.Fatalf("snapshot: %v", err)
}
if !strings.Contains(snap, "AjaxDone") {
t.Errorf("settle() aurait dû attendre la fin du fetch AJAX; snapshot:\n%s", snap)
}
}
// TestCDPTypeSelectsOption vérifie le fix du test #14 : browser_type sur un