Review fixes: ordered doc-snippet check, one page and doc per ID, fade test clock, listen-address docs, false placeholder gap, Today param
This commit is contained in:
10 files changed
+98
-69
No files matched your search
@@ -72,10 +72,10 @@ k := mujica.Tokens(c) // 当前窗口的颜色
|
||||
|
||||
```sh
|
||||
go run ./examples/gallery # 原生窗口
|
||||
go run ./examples/gallery -web :8080 # 浏览器镜像
|
||||
go run ./examples/gallery -web 127.0.0.1:8080 # 浏览器镜像,仅本机
|
||||
```
|
||||
|
||||
浏览器镜像不在浏览器里运行组件。Go 进程用 `ui.NewTester` 驱动同一个 Gallery 视图,把帧推给页面,再把指针、键盘、输入法和剪贴板事件送回 Go。
|
||||
`-web` 按传入的地址监听;写 `:8080` 会对所有网络接口开放。浏览器镜像不在浏览器里运行组件。Go 进程用 `ui.NewTester` 驱动同一个 Gallery 视图,把帧推给页面,再把指针、键盘、输入法和剪贴板事件送回 Go。
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
+1
-1
@@ -22,4 +22,4 @@
|
||||
|
||||
## 离线
|
||||
|
||||
库与 Gallery 不发起网络请求,无遥测、账号或远程字体。图标与角饰随代码嵌入。浏览器镜像只在本机监听调用方给定的地址。
|
||||
库与 Gallery 不发起网络请求,无遥测、账号或远程字体。图标与角饰随代码嵌入。浏览器镜像按 `-web` 传入的地址监听:`127.0.0.1:8080` 仅本机可达,`:8080` 对所有网络接口开放。
|
||||
@@ -57,5 +57,4 @@ ui.Render(view, 400, 300, 1)
|
||||
- 段顺序固定为“时:分:秒 上午/下午”,zh-CN 下上午/下午也位于末尾。
|
||||
- MyGo 的 `ui.TimeInput` 只有时分且无 12 小时制、步长和范围(ui/timeinput.go:18),因此本组件自绘分段。
|
||||
- 日期与时间值不带时区:`Date`、`YearMonth`、`TimeOfDay` 只保存日历字段,组件内部计算不做 UTC 转换。
|
||||
- `Today` 为零值时读取 `c.Now()`(系统本地时区);需要固定“今天”(测试、跨时区应用)时显式传入。
|
||||
- 无障碍:MyGo v0.2.10 没有公开设置弹出按钮展开状态(`expanded`)、数值范围(`hasRange`/`accRange`)与值(`accValue`)的接口,这些字段只在 MyGo 内部组件中赋值(ui/element.go:286-295,例如 ui/base.go:297、ui/indicators.go:61)。因此触发器只能声明为 `RolePopUpButton` 而无法报告展开状态;时间段以 `Description` 读出当前值与范围,错误通过 `Error()` 关联。
|
||||
@@ -53,5 +53,4 @@ ui.Render(view, 400, 300, 1)
|
||||
- 开始等于结束视为错误。
|
||||
- `TimeRange.Overnight()` 报告是否跨午夜。
|
||||
- 日期与时间值不带时区:`Date`、`YearMonth`、`TimeOfDay` 只保存日历字段,组件内部计算不做 UTC 转换。
|
||||
- `Today` 为零值时读取 `c.Now()`(系统本地时区);需要固定“今天”(测试、跨时区应用)时显式传入。
|
||||
- 无障碍:MyGo v0.2.10 没有公开设置弹出按钮展开状态(`expanded`)、数值范围(`hasRange`/`accRange`)与值(`accValue`)的接口,这些字段只在 MyGo 内部组件中赋值(ui/element.go:286-295,例如 ui/base.go:297、ui/indicators.go:61)。因此触发器只能声明为 `RolePopUpButton` 而无法报告展开状态;时间段以 `Description` 读出当前值与范围,错误通过 `Error()` 关联。
|
||||
@@ -7,11 +7,13 @@
|
||||
## 最小示例
|
||||
|
||||
```go
|
||||
mujica.Tabs(c, &tab, []mujica.Tab{
|
||||
if mujica.Tabs(c, &tab, []mujica.Tab{
|
||||
{Label: "General", Panel: func() { ui.Text(c, "General settings") }},
|
||||
{Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }},
|
||||
{Label: "Archive", Disabled: true},
|
||||
}, mujica.TabsOptions{Label: "Settings"})
|
||||
}, mujica.TabsOptions{Label: "Settings"}).Changed() {
|
||||
fmt.Println("tab", tab)
|
||||
}
|
||||
```
|
||||
|
||||
签名:`func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResult`
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
## 最小示例
|
||||
|
||||
```go
|
||||
mujica.SegmentedControl(c, &view, []mujica.Segment{
|
||||
if mujica.SegmentedControl(c, &view, []mujica.Segment{
|
||||
{Label: "List", Icon: icons.Must("list"), HideLabel: true},
|
||||
{Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true},
|
||||
}, mujica.SegmentedControlOptions{Label: "View"})
|
||||
}, mujica.SegmentedControlOptions{Label: "View"}).Changed() {
|
||||
fmt.Println("view", view)
|
||||
}
|
||||
```
|
||||
|
||||
签名:`func SegmentedControl(c *ui.Context, selected *int, segments []Segment, opts SegmentedControlOptions) *ui.Element`
|
||||
|
||||
@@ -61,7 +61,7 @@ if id, ok := res.Chosen(); ok {
|
||||
|
||||
## 限制
|
||||
|
||||
- MyGo `TextInputBase` 没有占位文字(`ui/editor.go:734`);为避免与输入法组合文字重叠,说明放在底部提示栏,搜索框以无障碍名称“搜索命令”标识。
|
||||
- 操作说明放在底部提示栏而不放进输入框,避免与输入法组合文字重叠;搜索框以无障碍名称“搜索命令”标识。
|
||||
- 搜索是子串匹配,不做模糊匹配或排序打分。
|
||||
- 焦点留在搜索框以便继续输入;方向键移动高亮时用 `c.Announce` 朗读当前命令,并写入搜索框的 `Description`(MyGo 的 active descendant 不公开)。
|
||||
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
|
||||
|
||||
+1
-1
@@ -33,6 +33,6 @@ PRD 第 2 节要求公开接口不足时向上游提交扩展。以下缺口在
|
||||
| `activeDescendant` 不公开 | `ui/element.go:331` | Menubar、CommandPalette |
|
||||
| `SliderBase` 方向键步长固定 1%,构建时处理指针 | `ui/base.go:111-117` | Slider |
|
||||
| `SelectBase` 高亮无设置接口 | `ui/base.go:260` | Select |
|
||||
| `TextInputBase` 无只读开关与占位符 | `ui/editor.go:139,734,811` | TextInput 只读模式、CommandPalette |
|
||||
| `TextInputBase` 无只读开关 | `ui/editor.go:139,811` | TextInput 只读模式 |
|
||||
| `ui.Stepper` 禁用时仍可步进,固定 9 位取整 | `ui/indicators.go:216`、`ui/widgets.go` `snap` | NumberInput(已改为自绘) |
|
||||
| 路径遮罩缓存按四分之一像素取键,进程内共享 | `ui/path.go:189-210` | 自绘路径以 `internal.Snap` 对齐;MyGo 自绘的箭头(Outline 等)无法对齐;快照测试因此在独立子进程中渲染,结果与测试顺序无关 |
|
||||
+79
-57
@@ -14,71 +14,93 @@ var (
|
||||
docsPageID = regexp.MustCompile(`ID: "(\d{3})"`)
|
||||
)
|
||||
|
||||
// TestDocSnippetsAreCompiledExamples keeps every doc's first Go block inside a compiled Example.
|
||||
// docsLines normalizes code into its non-empty, non-comment lines without whitespace.
|
||||
func docsLines(code string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(code, "\n") {
|
||||
s := strings.TrimSpace(l)
|
||||
if s != "" && !strings.HasPrefix(s, "//") {
|
||||
out = append(out, docsSpace.ReplaceAllString(s, ""))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// docsContains reports whether want appears in order inside have.
|
||||
func docsContains(have, want []string) bool {
|
||||
i := 0
|
||||
for _, h := range have {
|
||||
if i < len(want) && h == want[i] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i == len(want)
|
||||
}
|
||||
|
||||
func readGlob(t *testing.T, pattern string) map[string]string {
|
||||
t.Helper()
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, f := range files {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out[f] = string(b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestDocSnippetsAreCompiledExamples keeps each doc's first Go block, in order, inside one Example.
|
||||
func TestDocSnippetsAreCompiledExamples(t *testing.T) {
|
||||
examples, err := filepath.Glob("example_*_test.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var all strings.Builder
|
||||
for _, f := range examples {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all.Write(b)
|
||||
}
|
||||
compiled := docsSpace.ReplaceAllString(all.String(), "")
|
||||
docs, err := filepath.Glob("docs/components/*.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pages, err := filepath.Glob("examples/gallery/pages_*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := map[string]bool{}
|
||||
for _, f := range pages {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range docsPageID.FindAllSubmatch(b, -1) {
|
||||
ids[string(m[1])] = true
|
||||
var examples [][]string
|
||||
for _, src := range readGlob(t, "example_*_test.go") {
|
||||
for _, fn := range strings.Split(src, "\nfunc Example")[1:] {
|
||||
examples = append(examples, docsLines(fn))
|
||||
}
|
||||
}
|
||||
documented := map[string]bool{}
|
||||
for _, d := range docs {
|
||||
documented[filepath.Base(d)[:3]] = true
|
||||
}
|
||||
for id := range ids {
|
||||
if !documented[id] {
|
||||
t.Errorf("gallery page %s has no doc", id)
|
||||
}
|
||||
}
|
||||
for id := range documented {
|
||||
if !ids[id] {
|
||||
t.Errorf("doc %s has no gallery page", id)
|
||||
}
|
||||
}
|
||||
for _, d := range docs {
|
||||
b, err := os.ReadFile(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := docsGoBlock.FindSubmatch(b)
|
||||
for d, src := range readGlob(t, "docs/components/*.md") {
|
||||
m := docsGoBlock.FindStringSubmatch(src)
|
||||
if m == nil {
|
||||
t.Errorf("%s has no Go example", d)
|
||||
continue
|
||||
}
|
||||
for _, line := range strings.Split(string(m[1]), "\n") {
|
||||
s := strings.TrimSpace(line)
|
||||
if s == "" || strings.HasPrefix(s, "//") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(compiled, docsSpace.ReplaceAllString(s, "")) {
|
||||
t.Errorf("%s: %q is not in any compiled Example", d, s)
|
||||
want, found := docsLines(m[1]), false
|
||||
for _, ex := range examples {
|
||||
if docsContains(ex, want) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s: its Go block is not, in order, inside any compiled Example", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocsMatchGalleryPages wants exactly one doc and one gallery page per component ID.
|
||||
func TestDocsMatchGalleryPages(t *testing.T) {
|
||||
pages := map[string]int{}
|
||||
for _, src := range readGlob(t, "examples/gallery/pages_*.go") {
|
||||
for _, m := range docsPageID.FindAllStringSubmatch(src, -1) {
|
||||
pages[m[1]]++
|
||||
}
|
||||
}
|
||||
docs := map[string]int{}
|
||||
for d := range readGlob(t, "docs/components/*.md") {
|
||||
docs[filepath.Base(d)[:3]]++
|
||||
}
|
||||
for id, n := range pages {
|
||||
if n != 1 || docs[id] != 1 {
|
||||
t.Errorf("component %s: %d gallery pages, %d docs; want 1 and 1", id, n, docs[id])
|
||||
}
|
||||
}
|
||||
for id, n := range docs {
|
||||
if pages[id] == 0 {
|
||||
t.Errorf("doc %s (%d files) has no gallery page", id, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
|
||||
"mujicaui/icons"
|
||||
)
|
||||
|
||||
@@ -34,6 +36,7 @@ func TestNavigationPopupsFadeIn(t *testing.T) {
|
||||
CommandPalette(c, &open, commandPaletteSample(), CommandPaletteOptions{})
|
||||
}},
|
||||
} {
|
||||
opened := time.Now()
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{Mode: Light})
|
||||
ui.Column(c).Fill().Padding(16).AlignItems(ui.Start).Children(func() { tc.view(c) })
|
||||
@@ -49,9 +52,11 @@ func TestNavigationPopupsFadeIn(t *testing.T) {
|
||||
}
|
||||
x, y := int(r.X-3), int(r.Y+r.H/2)
|
||||
early := tt.Image().RGBAAt(x, y)
|
||||
mid := time.Since(opened) < theme.PopupDuration/2
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
tt.Frame()
|
||||
if late := tt.Image().RGBAAt(x, y); early == late {
|
||||
// A slow run may finish the fade before the first sample.
|
||||
if late := tt.Image().RGBAAt(x, y); mid && early == late {
|
||||
t.Errorf("%s: popup appeared at once (%v)", tc.name, late)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user