Resolve the page UI language once and let each ExApp fill its own Phrases.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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":"<b>x</b>"}'>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 : ""
|
||||
});
|
||||
})()`
|
||||
Reference in New Issue
Block a user