Files
go-nc-exapp/phrase_ui_test.go
T

246 lines
8.1 KiB
Go

package gonexapp_test
import (
"context"
"encoding/base64"
"encoding/json"
"html"
"net/http"
"net/http/httptest"
"testing"
"testing/fstest"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
"gitea.neitzel.de/konrad/go-nc-exapp"
)
func TestExAppPhrasesOnThePage(t *testing.T) {
phrases := mergedPhrases(t)
tests := []struct {
name string
ncLang string
want phraseSeen
}{
{
name: "german",
ncLang: "de",
want: phraseSeen{
lang: "de", heading: "Spätere Überschrift", placeholder: "Platzhalter",
title: "Titel", aria: "Beschriftung", hole: "example.com fehlt",
openHole: "{zone} fehlt", fallback: "English only", unknown: "not.a.phrase",
htmlHole: "<b>x</b> fehlt", scripted: "Skript", chromeID: "dialog.cancel",
err: "<b>x</b> fehlt", nav: "Domänen", fold: "Domänen einklappen",
missing: "Dieser Eintrag fehlt",
},
},
{
name: "english",
ncLang: "en",
want: phraseSeen{
lang: "en", heading: "Later heading", placeholder: "Placeholder",
title: "Title", aria: "Aria", hole: "Zone example.com is missing",
openHole: "Zone {zone} is missing", fallback: "English only", unknown: "not.a.phrase",
htmlHole: "Zone <b>x</b> is missing", scripted: "Scripted", chromeID: "dialog.cancel",
err: "Missing <b>x</b>", nav: "Domains", fold: "Fold Domains",
missing: "that item is not in this Visit",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nc := fakeLanguage(http.StatusOK, tt.ncLang)
t.Cleanup(nc.Close)
page := phrasePage(nc.URL, phrases)
t.Cleanup(page.Close)
got := readPhrases(t, page.URL+"/?item=missing", network.Headers{
"Accept-Language": browserLanguage,
"AUTHORIZATION-APP-API": base64.StdEncoding.EncodeToString([]byte("alice:secret")),
})
if got != tt.want {
t.Fatalf("got %+v, want %+v", got, tt.want)
}
})
}
}
type phraseSeen struct {
lang string
heading string
placeholder string
title string
aria string
hole string
openHole string
fallback string
unknown string
htmlHole string
scripted string
chromeID string
err string
nav string
fold string
missing string
}
func mergedPhrases(t *testing.T) gonexapp.Phrases {
t.Helper()
base, err := gonexapp.PhrasesFromFS(fstest.MapFS{
"i18n/en.json": {Data: []byte(phraseEN)},
"i18n/de.json": {Data: []byte(phraseDE)},
})
if err != nil {
t.Fatal(err)
}
mod, err := gonexapp.PhrasesFromFS(fstest.MapFS{
"i18n/en.json": {Data: []byte(`{"page.heading":"Later heading"}`)},
"i18n/de.json": {Data: []byte(`{"page.heading":"Spätere Überschrift"}`)},
})
if err != nil {
t.Fatal(err)
}
return gonexapp.Merge(base, mod)
}
const phraseEN = `{
"page.heading": "Heading",
"page.placeholder": "Placeholder",
"page.title": "Title",
"page.aria": "Aria",
"page.hole": "Zone {zone} is missing",
"page.english-only": "English only",
"page.scripted": "Scripted",
"page.error": "Missing {name}",
"nav.domains": "Domains",
"nav.missing": "that item is not in this Visit"
}`
const phraseDE = `{
"page.placeholder": "Platzhalter",
"page.title": "Titel",
"page.aria": "Beschriftung",
"page.hole": "{zone} fehlt",
"page.scripted": "Skript",
"page.error": "{name} fehlt",
"nav.domains": "Domänen",
"nav.missing": "Dieser Eintrag fehlt"
}`
func phrasePage(ncBase string, phrases gonexapp.Phrases) *httptest.Server {
nav := sampleNavigation()
nav.Cred = gonexapp.Credentials{
BaseURL: ncBase, AppID: "exapp", AppVersion: "1.0.0", AAVersion: "1.0.0", AppSecret: "secret",
}
nav.Phrases = phrases
nav.MissingMessage = "that item is not in this Visit"
nav.MissingPhrase = "nav.missing"
nav.Items = func(*http.Request) []gonexapp.Item {
return []gonexapp.Item{{
ID: "domains", Label: "Domains", Phrase: "nav.domains",
Children: []gonexapp.Item{{ID: "example.com", Label: "example.com"}},
}}
}
nav.Page = func(r *http.Request, id string) (string, bool) {
if id != "domains" {
return "", false
}
msg := phrases.Sentence(nav.Cred.UILanguage(r), "page.error", map[string]string{"name": "<b>x</b>"})
return `<h1 id="heading" data-phrase="page.heading">Heading</h1>
<input id="field" data-phrase-placeholder="page.placeholder" placeholder="Placeholder" data-phrase-title="page.title" title="Title" data-phrase-aria-label="page.aria" aria-label="Aria">
<p id="hole" data-phrase="page.hole" data-phrase-holes='{"zone":"example.com"}'>Zone {zone} is missing</p>
<p id="open-hole" data-phrase="page.hole">Zone {zone} is missing</p>
<p id="fallback" data-phrase="page.english-only">English only</p>
<p id="unknown" data-phrase="not.a.phrase">English source</p>
<p id="htmlhole" data-phrase="page.hole" data-phrase-holes='{"zone":"&lt;b&gt;x&lt;/b&gt;"}'>Zone {zone} is missing</p>
<p id="scripted"></p>
<p id="chrome-id"></p>
<p id="err">` + html.EscapeString(msg) + `</p>
<script>
document.getElementById("scripted").textContent = exappPhrase("page.scripted");
document.getElementById("chrome-id").textContent = exappPhrase("dialog.cancel");
</script>`, true
}
return httptest.NewServer(nav.Handler())
}
func readPhrases(t *testing.T, pageURL string, headers network.Headers) phraseSeen {
t.Helper()
ctx, cancel := chromedp.NewContext(allocCtx)
t.Cleanup(cancel)
ctx, cancel = context.WithTimeout(ctx, 25*time.Second)
t.Cleanup(cancel)
var raw string
if err := chromedp.Run(ctx,
network.Enable(),
network.SetExtraHTTPHeaders(headers),
chromedp.Navigate(pageURL),
chromedp.Evaluate(phraseProbeJS, &raw),
); err != nil {
t.Fatal(err)
}
var probe struct {
Lang string `json:"lang"`
Heading string `json:"heading"`
Placeholder string `json:"placeholder"`
Title string `json:"title"`
Aria string `json:"aria"`
Hole string `json:"hole"`
OpenHole string `json:"openHole"`
Fallback string `json:"fallback"`
Unknown string `json:"unknown"`
HTMLHole string `json:"htmlHole"`
HTMLHasB bool `json:"htmlHasB"`
Scripted string `json:"scripted"`
ChromeID string `json:"chromeId"`
Err string `json:"err"`
ErrHasB bool `json:"errHasB"`
Nav string `json:"nav"`
Fold string `json:"fold"`
Missing string `json:"missing"`
}
if err := json.Unmarshal([]byte(raw), &probe); err != nil {
t.Fatalf("probe %s: %v", raw, err)
}
if probe.HTMLHasB || probe.ErrHasB {
t.Fatalf("hole value was parsed as HTML: %+v", probe)
}
return phraseSeen{
lang: probe.Lang, heading: probe.Heading, placeholder: probe.Placeholder,
title: probe.Title, aria: probe.Aria, hole: probe.Hole, openHole: probe.OpenHole,
fallback: probe.Fallback, unknown: probe.Unknown, htmlHole: probe.HTMLHole,
scripted: probe.Scripted, chromeID: probe.ChromeID, err: probe.Err,
nav: probe.Nav, fold: probe.Fold, missing: probe.Missing,
}
}
const phraseProbeJS = `(() => {
const field = document.getElementById("field");
const htmlHole = document.getElementById("htmlhole");
const err = document.getElementById("err");
const name = document.querySelector('[data-id="domains"] > .app-navigation-entry .app-navigation-entry__name');
const fold = document.querySelector('[data-id="domains"] button.fold');
const missing = document.getElementById("nav-missing");
return JSON.stringify({
lang: document.documentElement.lang,
heading: document.getElementById("heading").textContent,
placeholder: field.placeholder,
title: field.title,
aria: field.getAttribute("aria-label"),
hole: document.getElementById("hole").textContent,
openHole: document.getElementById("open-hole").textContent,
fallback: document.getElementById("fallback").textContent,
unknown: document.getElementById("unknown").textContent,
htmlHole: htmlHole.textContent,
htmlHasB: !!htmlHole.querySelector("b"),
scripted: document.getElementById("scripted").textContent,
chromeId: document.getElementById("chrome-id").textContent,
err: err.textContent,
errHasB: !!err.querySelector("b"),
nav: name ? name.textContent : "",
fold: fold ? fold.getAttribute("aria-label") : "",
missing: missing ? missing.textContent : ""
});
})()`