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:
2026-10-11 11:45:25 +02:00
co-authored by Cursor
parent afbd76445d
commit 623ada5f99
14 changed files with 1051 additions and 53 deletions
+182
View File
@@ -0,0 +1,182 @@
package gonexapp_test
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
"gitea.neitzel.de/konrad/go-nc-exapp"
)
const browserLanguage = "de-DE,de;q=0.9"
func TestUILanguageChrome(t *testing.T) {
german := chromeSeen{
lang: "de", open: "Navigation öffnen", close: "Navigation schließen",
fold: "Domains einklappen", cancel: "Abbrechen", ok: "OK",
}
english := chromeSeen{
lang: "en", open: "Open navigation", close: "Close navigation",
fold: "Fold Domains", cancel: "Cancel", ok: "OK",
}
tests := []struct {
name string
user bool
status int
ncLang string
want chromeSeen
}{
{name: "german", user: true, status: http.StatusOK, ncLang: "de", want: german},
{name: "english", user: true, status: http.StatusOK, ncLang: "en", want: english},
{name: "formal german", user: true, status: http.StatusOK, ncLang: "de_DE", want: german},
{name: "british english", user: true, status: http.StatusOK, ncLang: "en_GB", want: english},
{name: "french", user: true, status: http.StatusOK, ncLang: "fr", want: english},
{name: "failed read", user: true, status: http.StatusInternalServerError, ncLang: "de", want: english},
{name: "missing user", user: false, status: http.StatusOK, ncLang: "de", want: english},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nc := fakeLanguage(tt.status, tt.ncLang)
t.Cleanup(nc.Close)
page := languagePage(nc.URL)
t.Cleanup(page.Close)
headers := network.Headers{"Accept-Language": browserLanguage}
if tt.user {
headers["AUTHORIZATION-APP-API"] = base64.StdEncoding.EncodeToString([]byte("alice:secret"))
}
got := readChrome(t, page.URL+"/?item=domains", headers)
if !strings.Contains(got.page, "domains-page") {
t.Fatalf("page did not render: %+v", got)
}
if got.lang != tt.want.lang || got.open != tt.want.open || got.close != tt.want.close ||
got.fold != tt.want.fold || got.cancel != tt.want.cancel || got.ok != tt.want.ok || got.agree != "Remove" {
t.Fatalf("got %+v, want %+v with agree %q", got, tt.want, "Remove")
}
})
}
}
type chromeSeen struct {
lang string
open string
close string
fold string
cancel string
ok string
agree string
page string
}
func fakeLanguage(status int, language string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || !strings.Contains(r.URL.Path, "/cloud/users/") {
http.NotFound(w, r)
return
}
if status != http.StatusOK {
http.Error(w, "language unavailable", status)
return
}
// A read that drops the browser language must not look like a hit.
lang := language
if r.Header.Get("Accept-Language") != browserLanguage {
lang = "en"
}
// locale differs from language so a read of the wrong field fails the page.
locale := "fr"
if language == "fr" || language == "en_GB" {
locale = "de"
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ocs":{"meta":{"status":"ok","statuscode":200,"message":"OK"},"data":{"language":%q,"locale":%q}}}`, lang, locale)
}))
}
func languagePage(ncBase string) *httptest.Server {
nav := sampleNavigation()
nav.Cred = gonexapp.Credentials{
BaseURL: ncBase,
AppID: "exapp",
AppVersion: "1.0.0",
AAVersion: "1.0.0",
AppSecret: "secret",
}
nav.Page = func(_ *http.Request, id string) (string, bool) {
switch id {
case "domains":
return `<p>domains-page</p><button type="button" id="ask">Ask</button>
<script>
document.getElementById("ask").addEventListener("click", function () {
exappDialog.confirm({ text: "Sure?", agree: "Remove" });
});
</script>`, true
case "keys":
return "<p>keys-page</p>", true
case "example.com":
return "<p>zone-page</p>", true
default:
return "", false
}
}
return httptest.NewServer(nav.Handler())
}
func readChrome(t *testing.T, pageURL string, headers network.Headers) chromeSeen {
t.Helper()
ctx, cancel := chromedp.NewContext(allocCtx)
t.Cleanup(cancel)
ctx, cancel = context.WithTimeout(ctx, 25*time.Second)
t.Cleanup(cancel)
var raw, closeLabel, agree string
err := chromedp.Run(ctx,
network.Enable(),
network.SetExtraHTTPHeaders(headers),
chromedp.Navigate(pageURL),
chromedp.Evaluate(chromeProbeJS, &raw),
chromedp.Evaluate(`document.getElementById("nav-toggle").click()`, nil),
chromedp.Evaluate(`document.getElementById("nav-toggle").getAttribute("aria-label")`, &closeLabel),
chromedp.Evaluate(`document.getElementById("ask").click()`, nil),
chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &agree),
)
if err != nil {
t.Fatal(err)
}
var probe struct {
Lang string `json:"lang"`
Open string `json:"open"`
Fold string `json:"fold"`
Cancel string `json:"cancel"`
OK string `json:"ok"`
Page string `json:"page"`
}
if err := json.Unmarshal([]byte(raw), &probe); err != nil {
t.Fatal(err)
}
return chromeSeen{
lang: probe.Lang, open: probe.Open, close: closeLabel, fold: probe.Fold,
cancel: probe.Cancel, ok: probe.OK, agree: agree, page: probe.Page,
}
}
const chromeProbeJS = `(() => {
const fold = document.querySelector("button.fold");
return JSON.stringify({
lang: document.documentElement.lang,
open: document.getElementById("nav-toggle").getAttribute("aria-label"),
fold: fold ? fold.getAttribute("aria-label") : "",
cancel: document.getElementById("exapp-dialog-cancel").textContent,
ok: document.getElementById("exapp-dialog-agree").textContent,
page: document.querySelector("main").textContent
});
})()`