mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Merge commit 'refs/pr12660' into takeover/12660
# Conflicts: # desktop/frontend-next/src/port/look.ts # desktop/frontend-next/src/styles/TOKENS.md # internal/ext/theme/theme.go
This commit is contained in:
commit
3bce33bff3
13 files changed
+348
-5
No files matched your search
@@ -61,6 +61,7 @@ node perf/concurrent.mjs # 多会话并发的守卫:9 个会话同时流式,
|
||||
node perf/wheel.mjs # 滚轮轨迹(只报告、不判红):向上滚时 scrollTop 不倒走、单步不超滚轮、scrollHeight 不缩水;WHEEL_STRICT=1 判红
|
||||
node perf/idle.mjs # 闲置的守卫:看不见的东西不许动,闲着的窗口不许烧 CPU
|
||||
node perf/effects.mjs # Effects guard: a sky pack idles quietly, a hidden window draws no sky, Reduced removes blur, sky and grain
|
||||
node perf/text-glow.mjs # Transcript glow, pack switching and prose contrast across schemes, wallpapers, widths, effects and motion
|
||||
node perf/shipped.mjs # 产物的守卫:dist 里的东西必须比源码新(构建可以成功而编的是别的树)
|
||||
node perf/reason.mjs # 内核拒绝的双语落地:同一个码,中英各说各的
|
||||
node perf/budget.mjs # 预算的守卫:输入框上下堆满东西时,转录还剩多少
|
||||
|
||||
@@ -44,6 +44,7 @@ let peakInFlight = 0;
|
||||
// ?pack=sky opens on a pack with the live sky; ?pack=photo on one with a
|
||||
// picture. Both are the states where the shell goes translucent over a backdrop.
|
||||
const PACK = new URLSearchParams(location.search).get("pack");
|
||||
const GLOW = new URLSearchParams(location.search).get("glow");
|
||||
|
||||
function benchPack(): ThemePack | null {
|
||||
const tokens = {
|
||||
@@ -59,7 +60,10 @@ function benchPack(): ThemePack | null {
|
||||
if (PACK === "photo") {
|
||||
return {
|
||||
id: "bench-photo", name: "Bench photo", active: true, tokens,
|
||||
background: { image: true, focusX: 0.5, focusY: 0.5, homeOpacity: 0.9, taskOpacity: 0.5, overlayStrength: 0.4 },
|
||||
background: {
|
||||
image: true, focusX: 0.5, focusY: 0.5, homeOpacity: 0.9, taskOpacity: 0.5, overlayStrength: 0.4,
|
||||
...(GLOW === "none" || GLOW === "default" ? { textGlow: GLOW } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const before = process.env.GLOW_BEFORE === "1";
|
||||
const shots = process.env.GLOW_SCREENSHOTS;
|
||||
if (shots) await mkdir(shots, { recursive: true });
|
||||
const browser = await chromium.launch();
|
||||
let checked = 0;
|
||||
const errors = [];
|
||||
|
||||
async function open(scheme, paper, width, effects, motion, glow) {
|
||||
const context = await browser.newContext({ viewport: { width, height: width === 390 ? 844 : 900 }, reducedMotion: motion });
|
||||
await context.addInitScript(({ scheme, effects }) => {
|
||||
localStorage.setItem("rx-theme", scheme);
|
||||
localStorage.setItem("rx-effects", effects);
|
||||
}, { scheme, effects });
|
||||
const page = await context.newPage();
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/themes/bench-photo/background", (route) => route.fulfill({
|
||||
contentType: "image/svg+xml",
|
||||
body: `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000"><rect width="1600" height="1000" fill="${paper === "bright" ? "#fff9e5" : "#081329"}"/><circle cx="1250" cy="300" r="280" fill="${paper === "bright" ? "#eebc7c" : "#253d69"}"/></svg>`,
|
||||
}));
|
||||
await page.goto(`${BASE}?pack=photo&glow=${glow}&turns=2&ws=1&sess=2`, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector('.flow:not([data-empty])');
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.waitForFunction(({ before, effects }) => {
|
||||
const value = getComputedStyle(document.querySelector('.flow:not([data-empty])')).textShadow;
|
||||
return before && effects === "full" ? value.includes("7px") : value === "none";
|
||||
}, { before, effects });
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
async function styles(page) {
|
||||
return page.locator('.flow:not([data-empty])').first().evaluate((el) => {
|
||||
const own = getComputedStyle(el);
|
||||
const backing = getComputedStyle(el, "::before");
|
||||
return {
|
||||
shadow: own.textShadow,
|
||||
blur: backing.backdropFilter || backing.webkitBackdropFilter,
|
||||
backing: backing.backgroundColor,
|
||||
content: backing.content,
|
||||
frameShadow: getComputedStyle(document.querySelector(".rail")).textShadow,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
for (const scheme of ["light", "dark"]) {
|
||||
for (const width of [1440, 390]) {
|
||||
for (const paper of before ? [scheme === "light" ? "bright" : "dark"] : ["bright", "dark"]) {
|
||||
for (const effects of before ? ["full"] : ["full", "reduced"]) {
|
||||
for (const motion of before ? ["no-preference"] : ["no-preference", "reduce"]) {
|
||||
const { context, page } = await open(scheme, paper, width, effects, motion, "none");
|
||||
const off = await styles(page);
|
||||
if (before) assert.notEqual(off.shadow, "none");
|
||||
else assert.equal(off.shadow, "none");
|
||||
assert.notEqual(off.content, "none");
|
||||
if (effects === "full") {
|
||||
assert.match(off.blur, /blur\(11px\)/);
|
||||
assert.notEqual(off.frameShadow, "none");
|
||||
} else {
|
||||
assert.equal(off.frameShadow, "none");
|
||||
}
|
||||
if (shots && paper === (scheme === "light" ? "bright" : "dark") && effects === "full" && motion === "no-preference") {
|
||||
await page.screenshot({ path: `${shots}/${scheme}-${width === 390 ? "phone" : "wide"}-${before ? "before" : "after"}.jpg`, type: "jpeg", quality: 88 });
|
||||
}
|
||||
if (!before) {
|
||||
const samples = await page.locator('.flow p').evaluateAll((els) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
return els.flatMap((el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 10 || r.height < 10 || r.top < 0 || r.bottom > innerHeight) return [];
|
||||
if (!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return [];
|
||||
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
||||
if (!hit || !el.contains(hit)) return [];
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
ctx.fillStyle = getComputedStyle(el).color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
return [{ x: Math.floor(r.left + r.width / 2), y: Math.floor(r.top + r.height / 2), text: el.textContent, fg: [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3) }];
|
||||
});
|
||||
});
|
||||
assert.ok(samples.length, "visible transcript prose must be measured");
|
||||
const hiding = await page.addStyleTag({ content: ".flow * { color: transparent !important; text-shadow: none !important }" });
|
||||
const blank = (await page.screenshot()).toString("base64");
|
||||
const ratios = await page.evaluate(async ({ blank, samples }) => {
|
||||
const img = new Image();
|
||||
img.src = `data:image/png;base64,${blank}`;
|
||||
await img.decode();
|
||||
const ctx = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
||||
ctx.canvas.width = img.width; ctx.canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const lum = (c) => c.map((v) => v / 255).map((v) => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4).reduce((sum, v, i) => sum + v * [.2126, .7152, .0722][i], 0);
|
||||
return samples.map(({ x, y, fg }) => {
|
||||
const bg = [...ctx.getImageData(x, y, 1, 1).data].slice(0, 3);
|
||||
const [hi, lo] = [lum(fg), lum(bg)].sort((a, b) => b - a);
|
||||
return (hi + .05) / (lo + .05);
|
||||
});
|
||||
}, { blank, samples });
|
||||
await hiding.evaluate((el) => el.remove());
|
||||
assert.ok(Math.min(...ratios) >= 4.5, `prose contrast ${Math.min(...ratios).toFixed(2)}:1 ${JSON.stringify(samples)}`);
|
||||
await page.evaluate(async () => {
|
||||
const { apply } = await import("/src/ui/theme.ts");
|
||||
const tokens = {
|
||||
light: { bg: "#F2F6F8", bgSoft: "#F9FCFD", panel: "#FFFFFF", border: "#CBD9E0", fg: "#12191C", fgDim: "#4E5D65", accent: "#0E6E82" },
|
||||
dark: { bg: "#080D10", bgSoft: "#0C1316", panel: "#131C21", border: "#23333A", fg: "#E4EEF2", fgDim: "#8298A2", accent: "#4FB6CE" },
|
||||
};
|
||||
const background = { image: true, focusX: .5, focusY: .5, homeOpacity: .9, taskOpacity: .5, overlayStrength: .4 };
|
||||
apply({ id: "bench-photo", name: "Old pack", tokens, background }, document.documentElement.dataset.theme);
|
||||
});
|
||||
if (effects === "full") await page.waitForFunction(() => getComputedStyle(document.querySelector('.flow:not([data-empty])')).textShadow.includes("7px"));
|
||||
const legacy = await styles(page);
|
||||
assert.equal(legacy.backing, off.backing);
|
||||
assert.equal(legacy.blur, off.blur);
|
||||
if (effects === "full") assert.match(legacy.shadow, /7px/);
|
||||
else assert.equal(legacy.shadow, "none");
|
||||
checked += 2;
|
||||
console.log(`ok ${scheme} ${width} ${paper} ${effects} ${motion}; prose min ${Math.min(...ratios).toFixed(2)}:1`);
|
||||
}
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(before ? "4 baseline views captured" : `${checked} glow/default scenarios passed; no page errors`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export interface ThemeBackground {
|
||||
homeOpacity: number;
|
||||
taskOpacity: number;
|
||||
overlayStrength: number;
|
||||
textGlow?: "default" | "none";
|
||||
}
|
||||
|
||||
// A live backdrop the window draws, rather than a picture it places. Absent on
|
||||
@@ -36,7 +37,7 @@ export interface ThemePack {
|
||||
sky?: ThemeSky;
|
||||
hasPreview?: boolean;
|
||||
removable?: boolean;
|
||||
// Tokens the kernel dropped and why: an unknown name, or a value that could
|
||||
// Values the kernel dropped and why: an unknown name, or a value that could
|
||||
// not be let into a stylesheet. The pack still loads without them.
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
@@ -244,6 +244,11 @@ raised with its ratios kept.
|
||||
- Default light Studio replies use `--w-read: 400` for prose only. Headings,
|
||||
emphasis and small UI text keep their existing weights. A chosen weight,
|
||||
installed pack or dark scheme falls back to `--w-reg`.
|
||||
- `background.textGlow: none` paints `--flow-text-shadow: none` through the
|
||||
pack sheet. The backdrop transcript rule defaults to its 2px shadow and 7px
|
||||
halo when this variable is absent. The choice affects text only; the frosted
|
||||
backing remains. Reduced effects overrides the text shadow.
|
||||
|
||||
- Stroke weight is an axis beside contrast, not part of it: however bright the
|
||||
colour, 400 Han at 11px is thin. Han strokes are dense and blur at a size
|
||||
Latin still holds, so the Chinese interface starts at medium.
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
}
|
||||
|
||||
:root[data-bg] .flow:not([data-empty]) {
|
||||
text-shadow: 0 1px 2px color-mix(in srgb, var(--page) 90%, transparent),
|
||||
0 0 7px color-mix(in srgb, var(--page) 72%, transparent);
|
||||
text-shadow: var(--flow-text-shadow, 0 1px 2px color-mix(in srgb, var(--page) 90%, transparent),
|
||||
0 0 7px color-mix(in srgb, var(--page) 72%, transparent));
|
||||
}
|
||||
:root[data-bg] .compose {
|
||||
background: color-mix(in srgb, var(--raised) 80%, transparent);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { apply } from "./theme";
|
||||
import type { ThemePack } from "../port/port";
|
||||
|
||||
const pack: ThemePack = {
|
||||
id: "glow-probe", name: "Glow probe",
|
||||
tokens: { light: { bg: "#ffffff", fg: "#111111" }, dark: { bg: "#111111", fg: "#eeeeee" } },
|
||||
background: { image: true, focusX: .5, focusY: .5, homeOpacity: 1, taskOpacity: .2, overlayStrength: .65 },
|
||||
};
|
||||
const withGlow = (textGlow: unknown) => ({ ...pack, background: { ...pack.background, textGlow } }) as ThemePack;
|
||||
const read = () => {
|
||||
const sheet = (document.getElementById("pack-theme") as HTMLStyleElement | null)?.sheet;
|
||||
return (sheet?.cssRules[0] as CSSStyleRule | undefined)?.style.getPropertyValue("--flow-text-shadow") ?? "";
|
||||
};
|
||||
afterEach(() => apply(null, "light"));
|
||||
|
||||
describe("wallpaper transcript glow", () => {
|
||||
it.each(["light", "dark"] as const)("turns off only the transcript glow in %s", (scheme) => {
|
||||
apply(withGlow("none"), scheme);
|
||||
expect(read()).toBe("none");
|
||||
expect(document.documentElement.dataset.bg).toBe("on");
|
||||
});
|
||||
|
||||
it.each([undefined, "default", "bright", "none; color: red", false, null])("keeps the stylesheet default for %s", (value) => {
|
||||
apply(withGlow("none"), "light");
|
||||
apply(withGlow(value), "light");
|
||||
expect(read()).toBe("");
|
||||
});
|
||||
|
||||
it("cleans up when switching to an old pack, a solid pack, or default", () => {
|
||||
for (const next of [{ ...pack, id: "legacy" }, { ...pack, id: "solid", background: undefined }, null]) {
|
||||
apply(withGlow("none"), "dark");
|
||||
expect(read()).toBe("none");
|
||||
apply(next, "light");
|
||||
expect(read()).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
it("retains the choice across scheme and running-state changes", () => {
|
||||
apply(withGlow("none"), "light", false);
|
||||
apply(withGlow("none"), "dark", true);
|
||||
expect(read()).toBe("none");
|
||||
});
|
||||
});
|
||||
@@ -161,6 +161,7 @@ function declarations(pack: ThemePack, scheme: "light" | "dark", busy: boolean,
|
||||
}
|
||||
|
||||
const bg = pack.background;
|
||||
if (bg?.textGlow === "none") out.push(["--flow-text-shadow", "none"]);
|
||||
if (!bg?.image) return out;
|
||||
// The server owns freshness for this mutable URL. encodeURIComponent keeps
|
||||
// a pack id out of the CSS url() grammar.
|
||||
|
||||
@@ -115,6 +115,33 @@ The current `background` block controls `focusX`, `focusY`, `safeArea`
|
||||
Use a low task opacity so the image does not compete with a transcript.
|
||||
Check your image licence and avoid including workspace screenshots or secrets.
|
||||
|
||||
### Transcript text glow
|
||||
|
||||
Set `background.textGlow` to `"none"` to disable the transcript's wallpaper
|
||||
text shadow. Omit it, or use `"default"`, to retain the two existing shadows
|
||||
(1px offset with 2px blur, plus a 7px halo). For example:
|
||||
|
||||
```json
|
||||
"background": { "textGlow": "none", "taskOpacity": 0.2 }
|
||||
```
|
||||
|
||||
This choice applies in both schemes whenever a backdrop is active, including
|
||||
a sky or the reader's own wallpaper. It also works without a packaged image.
|
||||
|
||||
The composer, navigation shadows and frosted transcript backing retain their
|
||||
own treatments. Reduced effects still overrides text shadows;
|
||||
reduced motion changes animation only.
|
||||
|
||||
Switching or clearing a pack
|
||||
removes its glow choice, so older packs retain the default.
|
||||
|
||||
Only `"default"` and `"none"` are accepted. An unknown string is dropped with
|
||||
a theme warning. Null behaves as omitted; booleans, numbers, arrays and objects
|
||||
fail manifest decoding. CSS values are not accepted.
|
||||
|
||||
Check ordinary and muted text over bright and dark images in
|
||||
both schemes and at phone width before sharing a pack.
|
||||
|
||||
## Disable and clean up
|
||||
|
||||
```bash
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"homeOpacity": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"taskOpacity": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"overlayStrength": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"textGlow": { "type": "string", "enum": ["default", "none"], "default": "default", "description": "Studio transcript text shadow when a backdrop is active; does not change the frosted backing." },
|
||||
"paneOpacity": { "type": "number", "minimum": 0, "maximum": 1 }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTextGlowSurvivesThemeLoading(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, field, want string
|
||||
warnings int
|
||||
}{
|
||||
{"legacy", ``, "", 0},
|
||||
{"null", `"textGlow":null`, "", 0},
|
||||
{"default", `"textGlow":"default"`, "default", 0},
|
||||
{"disabled", `"textGlow":"none"`, "none", 0},
|
||||
{"unknown", `"textGlow":"bright"`, "", 1},
|
||||
{"css", `"textGlow":"none; color: red"`, "", 1},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw := strings.TrimSuffix(minimal, "}") + `,"background":{` + tc.field + `}}`
|
||||
pack, err := decode([]byte(raw), "glow-probe")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := json.Marshal(pack.Background)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var bg map[string]any
|
||||
if err := json.Unmarshal(body, &bg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ := bg["textGlow"].(string)
|
||||
if got != tc.want || len(pack.Warnings) != tc.warnings {
|
||||
t.Fatalf("background=%s warnings=%v, want textGlow=%q and %d warnings", body, pack.Warnings, tc.want, tc.warnings)
|
||||
}
|
||||
if tc.warnings > 0 && !strings.Contains(pack.Warnings[0], "textGlow") {
|
||||
t.Fatalf("warning does not identify textGlow: %v", pack.Warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextGlowWithoutBackgroundStaysAbsent(t *testing.T) {
|
||||
pack, err := decode([]byte(minimal), "glow-probe")
|
||||
if err != nil || pack.Background != nil || len(pack.Warnings) != 0 {
|
||||
t.Fatalf("pack=%+v err=%v", pack, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextGlowWrongTypeRefusesManifest(t *testing.T) {
|
||||
for _, value := range []string{`false`, `7`, `{}`, `[]`} {
|
||||
raw := strings.TrimSuffix(minimal, "}") + `,"background":{"textGlow":` + value + `}}`
|
||||
if _, err := decode([]byte(raw), "glow-probe"); err == nil {
|
||||
t.Fatalf("accepted non-string textGlow %s", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ type Pack struct {
|
||||
// Removable is true for a pack that lives in the user's own library, which
|
||||
// is the only place Remove reaches.
|
||||
Removable bool `json:"removable,omitempty"`
|
||||
// Warnings names the tokens that were dropped and why. The pack still
|
||||
// Warnings names the values that were dropped and why. The pack still
|
||||
// loads: one bad value must not cost the author every good one.
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
@@ -70,6 +70,7 @@ type Background struct {
|
||||
HomeOpacity float64 `json:"homeOpacity"`
|
||||
TaskOpacity float64 `json:"taskOpacity"`
|
||||
OverlayStrength float64 `json:"overlayStrength"`
|
||||
TextGlow string `json:"textGlow,omitempty"`
|
||||
}
|
||||
|
||||
// Sky is a live backdrop rather than a picture: drifting cloud, a sun, and a
|
||||
@@ -115,6 +116,7 @@ type manifestBackground struct {
|
||||
HomeOpacity *float64 `json:"homeOpacity"`
|
||||
TaskOpacity *float64 `json:"taskOpacity"`
|
||||
OverlayStrength *float64 `json:"overlayStrength"`
|
||||
TextGlow string `json:"textGlow"`
|
||||
}
|
||||
|
||||
// Dir is where user-installed packs live: one directory per pack, each with a
|
||||
@@ -260,6 +262,14 @@ func decodeManifest(raw []byte, dirID string) (Pack, manifest, error) {
|
||||
}
|
||||
tokens[scheme] = copied
|
||||
}
|
||||
if bg := m.Background; bg != nil {
|
||||
switch bg.TextGlow {
|
||||
case "", "default", "none":
|
||||
default:
|
||||
warnings = append(warnings, fmt.Sprintf("background: %q is not a valid textGlow value", bg.TextGlow))
|
||||
bg.TextGlow = ""
|
||||
}
|
||||
}
|
||||
// Map iteration order would otherwise make the same pack report its
|
||||
// problems in a different order on every read.
|
||||
slices.Sort(warnings)
|
||||
@@ -309,6 +319,7 @@ func backgroundOf(m *manifestBackground, image bool) *Background {
|
||||
if m == nil {
|
||||
return out
|
||||
}
|
||||
out.TextGlow = m.TextGlow
|
||||
if m.FocusX != nil {
|
||||
out.FocusX = clamp01(*m.FocusX)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestThemeGlowImportListAndReload(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
srv := themeServer(t)
|
||||
const raw = `{"schemaVersion":1,"id":"glow-probe","name":"Glow probe","tokens":{"light":{"bg":"#fff"},"dark":{"bg":"#111"}},"background":{"textGlow":"none"}}`
|
||||
response := postJSON(t, srv.URL+"/themes/import", map[string]any{
|
||||
"files": map[string]string{"theme.json": base64.StdEncoding.EncodeToString([]byte(raw))},
|
||||
})
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("import status=%d", response.StatusCode)
|
||||
}
|
||||
var imported struct {
|
||||
Pack struct {
|
||||
Background struct {
|
||||
TextGlow string `json:"textGlow"`
|
||||
} `json:"background"`
|
||||
} `json:"pack"`
|
||||
}
|
||||
if err := json.NewDecoder(response.Body).Decode(&imported); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if imported.Pack.Background.TextGlow != "none" {
|
||||
t.Fatalf("import lost glow: %+v", imported)
|
||||
}
|
||||
activation := postJSON(t, srv.URL+"/themes", map[string]string{"id": "glow-probe"})
|
||||
activation.Body.Close()
|
||||
if activation.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("activate status=%d", activation.StatusCode)
|
||||
}
|
||||
for _, base := range []string{srv.URL, themeServer(t).URL} {
|
||||
row := find(t, listThemes(t, base), "glow-probe")
|
||||
body, err := json.Marshal(row.Background)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var bg map[string]any
|
||||
if err := json.Unmarshal(body, &bg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !row.Active || bg["textGlow"] != "none" {
|
||||
t.Fatalf("listing lost active glow after load: row=%+v background=%s", row, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user