feat: add executable ETAF best-practice examples
Add retained state, Data Controller, and Resource lifecycle applications under examples, with paired guidance and public-path interaction tests. Verified with make check (57 behavior tests and 5 docs tests), make load, byte compilation, checkdoc, and GUI width checks at 784px body width.
This commit is contained in:
parent
5b915c2ff0
commit
1d0a931583
82
DESIGN.md
Normal file
82
DESIGN.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Design
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
- Status: Active
|
||||||
|
- Last refreshed: 2026-08-05
|
||||||
|
- Primary product surfaces: Executable applications under `examples/`, especially the buffers opened by the three `etaf-*-example-open` commands.
|
||||||
|
- Evidence reviewed: ETAF public source and user guide, core/Data/Resource tests, `../etaf-playground/DESIGN.md`, the polished ETAF Showcase, and the original Ebox Flex reference.
|
||||||
|
|
||||||
|
## Brand
|
||||||
|
- Personality: Precise, calm, modern, technical, and intentionally composed.
|
||||||
|
- Trust signals: Stable geometry, readable contrast, explicit state ownership, public-only construction, and visible success/error/selection states.
|
||||||
|
- Avoid: Bare fixture text, low-contrast pastel text, decorative emoji, fake browser chrome, huge empty surfaces, and controls indistinguishable from copy.
|
||||||
|
|
||||||
|
## Product goals
|
||||||
|
- Goals: Make core ETAF practices copyable from small executable files; prove the installed Runtime interaction path; keep examples visually consistent with the Flex reference and ETAF Showcase.
|
||||||
|
- Non-goals: Replacing `etaf-playground`, simulating the missing `etaf-ui` catalog, introducing a second theme system, or demonstrating every public symbol in one application.
|
||||||
|
- Success signals: Each example teaches one ownership boundary, opens without side effects at load time, reacts through public events, cleans up its resources, and remains unclipped in a compact GUI frame.
|
||||||
|
|
||||||
|
## Personas and jobs
|
||||||
|
- Primary personas: ETAF application authors and framework maintainers.
|
||||||
|
- User jobs: Copy a correct state/data/resource pattern, inspect a real mounted application, and verify the visible result of an event.
|
||||||
|
- Key contexts of use: Source reading, GUI Emacs exploration, automated ERT, and framework regression review.
|
||||||
|
|
||||||
|
## Information architecture
|
||||||
|
- Primary navigation: No shared launcher; every file owns one explicit open and close command.
|
||||||
|
- Core routes/screens: Retained counter, task Data Controller, and Resource health.
|
||||||
|
- Content hierarchy: Capability eyebrow, application title and explanation, primary state surface, actions, then an ownership rule footer.
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
- Principle 1: One example teaches one lifecycle owner; avoid a mega-demo that hides where state and cleanup belong.
|
||||||
|
- Principle 2: Writes happen at Event, Action, lifecycle, Data, or Resource boundaries; render functions remain read-only.
|
||||||
|
- Principle 3: Use warm restrained semantic color families and one-pixel borders to make structure obvious without adding a design-system dependency.
|
||||||
|
- Tradeoffs: Prefer a fixed compact teaching canvas over viewport arithmetic in each standalone example; keep the responsive full-application showcase in `etaf-playground`.
|
||||||
|
|
||||||
|
## Visual language
|
||||||
|
- Color: Warm canvas `#F8F5EE`, paper `#FFFDF8`, ink `#252A2E`, muted ink `#66706A`, terracotta `#F1D4C9`, sage `#DCEBDD`, blue `#D9EAF2`, and violet `#E7E2F1`, always with explicit dark text.
|
||||||
|
- Typography: The configured Emacs monospace face; bold only for titles, actions, statuses, and important values.
|
||||||
|
- Spacing/layout rhythm: One-line vertical gaps, 10–12 px horizontal gaps, and 12–18 px surface padding.
|
||||||
|
- Shape/radius/elevation: Square text-native blocks and one-pixel borders; no shadows or fake radius.
|
||||||
|
- Motion: None; synchronous commits must settle without geometry shifts.
|
||||||
|
- Imagery/iconography: Text and restrained semantic marks such as selection circles only.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
- Existing components to reuse: Core Hosts, retained Components, refs, computed values, Actions, focusable Behaviors, Data Controllers, Resources, and Runtime lifecycle callbacks.
|
||||||
|
- New/changed components: Example-specific shell, metric, action, row, status, and footer Components/Hosts.
|
||||||
|
- Variants and states: Ready/active counter, selected/unselected task, open/done filter, idle/loading/success/error Resource.
|
||||||
|
- Token/component ownership: Each example owns its small static palette and composition; ETAF/Ebox own semantics, layout, and rendering.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
- Target standard: High-contrast, keyboard-addressable text UI.
|
||||||
|
- Keyboard/focus behavior: Every action has a stable ref, button role, and `etaf-focusable` Behavior.
|
||||||
|
- Contrast/readability: Tinted surfaces always set explicit foregrounds; status is expressed in text as well as color.
|
||||||
|
- Screen-reader semantics: Preserve textual labels and semantic roles; do not encode meaning only in decoration.
|
||||||
|
- Reduced motion and sensory considerations: No animation or flashing state.
|
||||||
|
|
||||||
|
## Responsive behavior
|
||||||
|
- Supported breakpoints/devices: GUI Emacs body widths of approximately 760 px and above.
|
||||||
|
- Layout adaptations: Examples use 680–720 px teaching canvases; action groups use wrapping Flex; the full viewport-responsive pattern remains in `etaf-playground`.
|
||||||
|
- Touch/hover differences: None; interaction uses the Runtime event boundary.
|
||||||
|
|
||||||
|
## Interaction states
|
||||||
|
- Loading: Explicit Resource/Data status rather than an invented fallback.
|
||||||
|
- Empty: Bounded explanatory row with no hidden placeholder data.
|
||||||
|
- Error: High-contrast visible error message that persists until the next action.
|
||||||
|
- Success: Explicit status text on a sage surface.
|
||||||
|
- Disabled: Remove the event/tab stop instead of presenting a misleading active control.
|
||||||
|
- Offline/slow network, if applicable: Not applicable to the synchronous core examples.
|
||||||
|
|
||||||
|
## Content voice
|
||||||
|
- Tone: Direct, technical, concise, and instructional.
|
||||||
|
- Terminology: Use the canonical names View, Component, Host, Runtime, Data Controller, Resource, Event, Action, Scope, and public API.
|
||||||
|
- Microcopy rules: Name the demonstrated ownership boundary and describe the visible effect; avoid promotional filler.
|
||||||
|
|
||||||
|
## Implementation constraints
|
||||||
|
- Framework/styling system: Emacs 29.1+, the public `etaf` facade, and public Ebox properties lowered through ETAF Hosts.
|
||||||
|
- Design-token constraints: Reuse the established warm palette directly; do not add a token framework for three examples.
|
||||||
|
- Performance constraints: Synchronous bounded data only; no timers, background work, or hidden repeated mounts.
|
||||||
|
- Compatibility constraints: Core examples must not require `etaf-ui`, `etaf-sqlite`, `etaf-playground`, or private `etaf--*` / `ebox--*` APIs.
|
||||||
|
- Test/screenshot expectations: `make check` byte-compiles examples and drives their mounted public event paths; GUI verification uses one target buffer window and rejects clipping or continuation indicators.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
- [ ] Add an asynchronous Resource example only after ETAF defines a public asynchronous completion contract / maintainer / avoids teaching a speculative API.
|
||||||
82
DESIGN.zh-CN.md
Normal file
82
DESIGN.zh-CN.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 设计规范
|
||||||
|
|
||||||
|
## 事实来源
|
||||||
|
- 状态:生效中
|
||||||
|
- 最近更新:2026-08-05
|
||||||
|
- 主要产品界面:`examples/` 下的可执行应用,尤其是三个 `etaf-*-example-open` 命令打开的缓冲区。
|
||||||
|
- 已审阅证据:ETAF 公开源码与用户指南、core/Data/Resource 测试、`../etaf-playground/DESIGN.md`、精修后的 ETAF Showcase,以及原始 Ebox Flex 参考示例。
|
||||||
|
|
||||||
|
## 品牌
|
||||||
|
- 气质:精确、克制、现代、技术化,并且经过明确构图。
|
||||||
|
- 可信信号:稳定的几何结构、清晰的对比度、明确的状态所有权、只使用公开 API,以及可见的成功/错误/选择状态。
|
||||||
|
- 避免:裸 fixture 文本、低对比度浅色文字、装饰性 emoji、假浏览器外壳、无意义的大面积留白,以及与正文无法区分的控件。
|
||||||
|
|
||||||
|
## 产品目标
|
||||||
|
- 目标:通过小型可执行文件提供可复制的 ETAF core 最佳实践;证明已安装 Runtime 的真实交互路径;让视觉质量与 Flex 参考和 ETAF Showcase 保持一致。
|
||||||
|
- 非目标:替代 `etaf-playground`、假装缺失的 `etaf-ui` 目录已经存在、引入第二套主题系统,或在一个应用里展示全部公开符号。
|
||||||
|
- 成功信号:每个示例只讲清一个所有权边界;加载文件时不产生应用副作用;通过公开事件响应;正确清理资源;在紧凑 GUI 窗口中不裁切。
|
||||||
|
|
||||||
|
## 用户与任务
|
||||||
|
- 主要用户:ETAF 应用作者和框架维护者。
|
||||||
|
- 用户任务:复制正确的状态/Data/Resource 模式、检查真实挂载应用,并验证事件产生的可见结果。
|
||||||
|
- 主要环境:源码阅读、GUI Emacs 探索、自动化 ERT 和框架回归审查。
|
||||||
|
|
||||||
|
## 信息架构
|
||||||
|
- 主导航:不增加共享 launcher;每个文件拥有一个明确的 open 与 close 命令。
|
||||||
|
- 核心界面:保留式计数器、任务 Data Controller、Resource 健康状态。
|
||||||
|
- 内容层级:能力 eyebrow、应用标题与说明、主要状态区域、操作区、所有权规则页脚。
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
- 原则一:一个示例只讲一个 lifecycle owner;避免用 mega-demo 隐藏状态与清理的归属。
|
||||||
|
- 原则二:写操作发生在 Event、Action、lifecycle、Data 或 Resource 边界;render 函数保持只读。
|
||||||
|
- 原则三:使用暖色、克制的语义色和一像素边框呈现结构,不增加设计系统依赖。
|
||||||
|
- 取舍:独立示例采用固定的紧凑教学画布;完整 viewport 响应式应用继续由 `etaf-playground` 展示。
|
||||||
|
|
||||||
|
## 视觉语言
|
||||||
|
- 颜色:暖色画布 `#F8F5EE`、纸张 `#FFFDF8`、正文 `#252A2E`、弱化正文 `#66706A`、陶土色 `#F1D4C9`、鼠尾草绿 `#DCEBDD`、蓝色 `#D9EAF2` 与紫色 `#E7E2F1`,始终搭配明确的深色文字。
|
||||||
|
- 字体:沿用 Emacs 当前等宽字体;粗体只用于标题、操作、状态和关键数值。
|
||||||
|
- 间距:一行纵向间隔、10–12 px 水平间隔、12–18 px 表面内边距。
|
||||||
|
- 形状与层级:方正的文本原生区块和一像素边框;不使用阴影或伪圆角。
|
||||||
|
- 动效:无;同步 commit 后几何结构必须稳定。
|
||||||
|
- 图像与图标:只使用文本和选择圆点等克制语义标记。
|
||||||
|
|
||||||
|
## 组件
|
||||||
|
- 复用能力:core Host、保留式 Component、ref、computed、Action、focusable Behavior、Data Controller、Resource 与 Runtime lifecycle callback。
|
||||||
|
- 新增或调整:示例专用 shell、metric、action、row、status 与 footer Component/Host。
|
||||||
|
- 状态:ready/active 计数器、selected/unselected 任务、open/done 筛选、idle/loading/success/error Resource。
|
||||||
|
- 所有权:每个示例拥有自己的小型静态配色和组合;ETAF/Ebox 拥有语义、布局和渲染。
|
||||||
|
|
||||||
|
## 可访问性
|
||||||
|
- 目标:高对比、可通过键盘寻址的文本 UI。
|
||||||
|
- 键盘与焦点:每个操作都具有稳定 ref、button role 和 `etaf-focusable` Behavior。
|
||||||
|
- 对比度:着色表面始终显式设置前景色;状态同时使用文字和颜色表达。
|
||||||
|
- 语义:保留文字标签和语义 role,不只用装饰表达含义。
|
||||||
|
- 动效:不使用动画或闪烁状态。
|
||||||
|
|
||||||
|
## 响应式行为
|
||||||
|
- 支持范围:正文宽度约 760 px 及以上的 GUI Emacs。
|
||||||
|
- 布局适配:示例使用 680–720 px 教学画布;操作组使用可换行 Flex;完整 viewport 响应式模式留在 `etaf-playground`。
|
||||||
|
- 触控/悬停:无;交互使用 Runtime event 边界。
|
||||||
|
|
||||||
|
## 交互状态
|
||||||
|
- Loading:明确显示 Resource/Data 状态,不伪造 fallback。
|
||||||
|
- Empty:有边界的解释行,不填充隐藏的占位数据。
|
||||||
|
- Error:高对比、持续可见的错误文案,直到下一次操作。
|
||||||
|
- Success:鼠尾草绿表面和明确状态文字。
|
||||||
|
- Disabled:移除 event/tab stop,不展示误导性的可用控件。
|
||||||
|
- 离线/慢网络:同步 core 示例不适用。
|
||||||
|
|
||||||
|
## 内容语气
|
||||||
|
- 风格:直接、技术化、简洁、适合教学。
|
||||||
|
- 术语:统一使用 View、Component、Host、Runtime、Data Controller、Resource、Event、Action、Scope 与 public API。
|
||||||
|
- 文案规则:点明展示的所有权边界与可见操作效果,避免宣传式填充文字。
|
||||||
|
|
||||||
|
## 实现约束
|
||||||
|
- 技术边界:Emacs 29.1+、公开 `etaf` facade,以及通过 ETAF Host 下沉的公开 Ebox 属性。
|
||||||
|
- Token:直接复用既有暖色配色;不为三个示例增加 token 框架。
|
||||||
|
- 性能:只使用同步、有界数据;不使用 timer、后台工作或隐藏的重复 mount。
|
||||||
|
- 兼容性:core 示例不得依赖 `etaf-ui`、`etaf-sqlite`、`etaf-playground`,也不得调用私有 `etaf--*` / `ebox--*` API。
|
||||||
|
- 验证:`make check` 会编译示例并驱动已挂载的公开事件路径;GUI 验证只使用一个目标缓冲区窗口,并拒绝裁切或续行标记。
|
||||||
|
|
||||||
|
## 待解决问题
|
||||||
|
- [ ] 只有 ETAF 定义公开异步完成契约后才增加异步 Resource 示例;维护者;避免教授臆造 API。
|
||||||
14
Makefile
14
Makefile
@ -1,18 +1,20 @@
|
|||||||
EMACS ?= emacs
|
EMACS ?= emacs
|
||||||
LOAD_PATH = -L . -L ../ebox
|
LOAD_PATH = -L . -L examples -L ../ebox
|
||||||
SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el
|
SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el
|
||||||
TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el
|
EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.el
|
||||||
|
TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-examples-tests.el
|
||||||
|
|
||||||
.PHONY: test compile load checkdoc docs-check check clean
|
.PHONY: test compile load checkdoc docs-check check clean
|
||||||
|
|
||||||
test: compile
|
test: compile
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" -l tests/etaf-tests.el -l tests/etaf-resource-tests.el -l tests/etaf-data-tests.el \
|
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" \
|
||||||
|
$(foreach test,$(TESTS),-l $(test)) \
|
||||||
-f ert-run-tests-batch-and-exit
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
compile: clean
|
compile: clean
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
--eval "(load-file \"etaf.el\")" \
|
--eval "(load-file \"etaf.el\")" \
|
||||||
--eval "(dolist (file '(\"etaf-view.el\" \"etaf-component.el\" \"etaf-reactive.el\" \"etaf-context.el\" \"etaf-resource.el\" \"etaf-data.el\" \"etaf-renderer.el\" \"etaf-runtime.el\" \"etaf-behavior.el\" \"etaf-actions.el\" \"etaf-events.el\" \"etaf.el\")) (byte-compile-file file))"
|
--eval "(dolist (file '($(foreach file,$(SOURCES) $(EXAMPLES),\"$(file)\"))) (byte-compile-file file))"
|
||||||
|
|
||||||
load: compile
|
load: compile
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" --eval "(require 'etaf)" --eval "(princ \"ETAF load OK\\n\")"
|
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" --eval "(require 'etaf)" --eval "(princ \"ETAF load OK\\n\")"
|
||||||
@ -22,9 +24,9 @@ docs-check:
|
|||||||
-f ert-run-tests-batch-and-exit
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
checkdoc:
|
checkdoc:
|
||||||
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (file (directory-files "." t)) (when (string-suffix-p ".el" file) (checkdoc-file file))))'
|
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (directory (list "." "examples")) (dolist (file (directory-files directory t)) (when (string-suffix-p ".el" file) (checkdoc-file file)))))'
|
||||||
|
|
||||||
check: checkdoc compile test docs-check
|
check: checkdoc compile test docs-check
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f *.elc tests/*.elc
|
rm -f *.elc examples/*.elc tests/*.elc
|
||||||
|
|||||||
11
README.md
11
README.md
@ -40,11 +40,22 @@ Define a Component:
|
|||||||
|
|
||||||
`etaf-view` is the single public View constructor. Structural forms do not use quote; quote remains ordinary Elisp data syntax, such as `'bold`. A View returned from ordinary Elisp is explicitly constructed with `(etaf-view ...)` inside `expr`.
|
`etaf-view` is the single public View constructor. Structural forms do not use quote; quote remains ordinary Elisp data syntax, such as `'bold`. A View returned from ordinary Elisp is explicitly constructed with `(etaf-view ...)` inside `expr`.
|
||||||
|
|
||||||
|
## Executable examples
|
||||||
|
|
||||||
|
The [`examples/`](examples/README.md) directory contains three core-only best-practice applications: retained state and Actions, Data Controller ownership, and Resource error/cleanup lifecycle. They are byte-compiled and driven through mounted public event paths by `make check`.
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Architecture](docs/architecture.en.md) · [中文架构](docs/architecture.zh.md)
|
- [Architecture](docs/architecture.en.md) · [中文架构](docs/architecture.zh.md)
|
||||||
- [User guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
|
- [User guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
|
||||||
- [Implementation plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
|
- [Implementation plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
|
||||||
|
- [Best-practice examples](examples/README.md) · [中文示例](examples/README.zh-CN.md)
|
||||||
|
|
||||||
## Independent packages
|
## Independent packages
|
||||||
|
|
||||||
|
|||||||
@ -40,11 +40,22 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
|
|||||||
|
|
||||||
`etaf-view` 是唯一的公共 View 构造入口。结构 form 不使用 quote;quote 仍然是普通 Elisp 数据语法,例如 `'bold`。普通 Elisp 返回 View 时,必须在 `expr` 中显式使用 `(etaf-view ...)` 构造它。
|
`etaf-view` 是唯一的公共 View 构造入口。结构 form 不使用 quote;quote 仍然是普通 Elisp 数据语法,例如 `'bold`。普通 Elisp 返回 View 时,必须在 `expr` 中显式使用 `(etaf-view ...)` 构造它。
|
||||||
|
|
||||||
|
## 可执行示例
|
||||||
|
|
||||||
|
[`examples/`](examples/README.zh-CN.md) 目录包含三个只依赖 core 的最佳实践应用:保留式状态与 Action、Data Controller 所有权,以及 Resource 错误/清理 lifecycle。`make check` 会编译它们,并通过已挂载的公开事件路径驱动交互。
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [English Architecture](docs/architecture.en.md) · [中文架构](docs/architecture.zh.md)
|
- [English Architecture](docs/architecture.en.md) · [中文架构](docs/architecture.zh.md)
|
||||||
- [English User Guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
|
- [English User Guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
|
||||||
- [English Implementation Plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
|
- [English Implementation Plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
|
||||||
|
- [English Best-practice Examples](examples/README.md) · [中文示例](examples/README.zh-CN.md)
|
||||||
|
|
||||||
## 独立包
|
## 独立包
|
||||||
|
|
||||||
|
|||||||
@ -519,6 +519,16 @@ Use it only when the normal Host and Component lowering path cannot express a re
|
|||||||
|
|
||||||
## 12. Playgrounds
|
## 12. Playgrounds
|
||||||
|
|
||||||
|
For small copyable core patterns, load one of the executable applications in `examples/`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
|
The counter example demonstrates retained state, computed values, Events, Actions, and focusable Hosts. `etaf-data-example-open` demonstrates mounted Data loading, query, selection, mutation, and explicit controller disposal. `etaf-resource-example-open` demonstrates deferred Resource loading, visible error state, reload cleanup, and Scope disposal. See [`examples/README.md`](../examples/README.md) for the ownership rules each example is designed to teach.
|
||||||
|
|
||||||
The independent ETAF playground is a complete ETAF application example:
|
The independent ETAF playground is a complete ETAF application example:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
|
|||||||
@ -519,6 +519,16 @@ Loader 错误会保存在 `etaf-resource-error`;cleanup/type 错误保持可
|
|||||||
|
|
||||||
## 12. Playgrounds
|
## 12. Playgrounds
|
||||||
|
|
||||||
|
如果需要小型、可复制的 core 模式,可以加载 `examples/` 中的可执行应用:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
|
计数器示例演示保留式状态、computed 值、Event、Action 和可聚焦 Host。`etaf-data-example-open` 演示 mounted 后的 Data 加载、查询、选择、变更和显式 controller 释放。`etaf-resource-example-open` 演示延迟 Resource 加载、可见错误状态、reload cleanup 与 Scope 释放。每个示例要教授的所有权规则见 [`examples/README.zh-CN.md`](../examples/README.zh-CN.md)。
|
||||||
|
|
||||||
独立的 ETAF Playground 是一个完整的 ETAF 应用示例:
|
独立的 ETAF Playground 是一个完整的 ETAF 应用示例:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
|
|||||||
32
examples/README.md
Normal file
32
examples/README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# ETAF best-practice examples
|
||||||
|
|
||||||
|
These examples are executable applications, not documentation fragments. They use only the public `etaf` facade and deliberately avoid `etaf-ui` so the core framework boundary remains independently runnable.
|
||||||
|
|
||||||
|
| Example | Public command | Practice demonstrated |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Retained counter | `M-x etaf-counter-example-open` | Component-owned refs, computed state, focusable Hosts, Event → Action → Runtime updates |
|
||||||
|
| Task controller | `M-x etaf-data-example-open` | Data Controller ownership, mounted loading, query, selection, mutation, and unmount cleanup |
|
||||||
|
| Resource health | `M-x etaf-resource-example-open` | Deferred Resource loading, visible error state, reload cleanup, and Scope disposal |
|
||||||
|
|
||||||
|
During checkout development, add Ebox, ETAF, and this directory to `load-path`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/ebox")
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf")
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the feature and open command with `etaf-data-example` / `etaf-data-example-open` or `etaf-resource-example` / `etaf-resource-example-open` for the other applications. Every example has a corresponding `-close` command that unmounts its Runtime before killing the buffer.
|
||||||
|
|
||||||
|
## Why these are the preferred patterns
|
||||||
|
|
||||||
|
- Keep writes out of render functions. Events and Actions mutate refs or Data Controllers; rendering only reads state.
|
||||||
|
- Create owned services during Component setup, start state-changing work from `etaf-on-mounted`, and release detached controllers from `etaf-on-unmounted`.
|
||||||
|
- Let Resource Scope cleanup own loaded values. Do not catch loader errors and invent plausible defaults; render the Resource error state explicitly.
|
||||||
|
- Use stable Host refs, semantic roles, and `etaf-focusable` so the same installed event path works for keyboard integration and tests.
|
||||||
|
- Keep examples compact and visually structured, but never introduce example-only rendering APIs or private `etaf--*` / `ebox--*` calls.
|
||||||
|
|
||||||
|
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` from the repository root. The gate byte-compiles these files, checks their docstrings and public boundaries, and drives their mounted interaction paths.
|
||||||
32
examples/README.zh-CN.md
Normal file
32
examples/README.zh-CN.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# ETAF 最佳实践示例
|
||||||
|
|
||||||
|
这些示例是可执行应用,而不是文档片段。它们只使用公开的 `etaf` facade,并刻意不依赖 `etaf-ui`,从而保证核心框架边界可以独立运行。
|
||||||
|
|
||||||
|
| 示例 | 公开命令 | 演示的实践 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 保留式计数器 | `M-x etaf-counter-example-open` | Component 自有 ref、computed 状态、可聚焦 Host、Event → Action → Runtime 更新 |
|
||||||
|
| 任务控制器 | `M-x etaf-data-example-open` | Data Controller 所有权、mounted 后加载、查询、选择、变更与 unmount 清理 |
|
||||||
|
| Resource 健康状态 | `M-x etaf-resource-example-open` | 延迟 Resource 加载、可见错误状态、reload 清理与 Scope 释放 |
|
||||||
|
|
||||||
|
在检出目录中开发时,把 Ebox、ETAF 和本目录加入 `load-path`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/ebox")
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf")
|
||||||
|
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||||||
|
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(etaf-counter-example-open)
|
||||||
|
```
|
||||||
|
|
||||||
|
另外两个应用分别替换为 `etaf-data-example` / `etaf-data-example-open` 和 `etaf-resource-example` / `etaf-resource-example-open`。每个示例都有对应的 `-close` 命令,会先卸载 Runtime,再销毁 buffer。
|
||||||
|
|
||||||
|
## 为什么推荐这些模式
|
||||||
|
|
||||||
|
- render 函数中不写状态。Event 与 Action 负责修改 ref 或 Data Controller;render 只读取状态。
|
||||||
|
- 在 Component setup 中创建自有服务,从 `etaf-on-mounted` 启动会修改状态的工作,并用 `etaf-on-unmounted` 释放 detached controller。
|
||||||
|
- 让 Resource Scope cleanup 管理已加载值。不要捕获加载错误后伪造一个看似合理的默认值;应明确渲染 Resource error 状态。
|
||||||
|
- 使用稳定的 Host ref、语义 role 和 `etaf-focusable`,让键盘集成与测试驱动同一条已安装事件路径。
|
||||||
|
- 示例应紧凑且有清晰视觉结构,但不能引入示例专用渲染 API,也不能调用私有 `etaf--*` / `ebox--*` 符号。
|
||||||
|
|
||||||
|
在仓库根目录运行 `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`。门禁会编译这些文件、检查 docstring 与公开边界,并驱动真实挂载后的交互路径。
|
||||||
140
examples/etaf-counter-example.el
Normal file
140
examples/etaf-counter-example.el
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
;;; etaf-counter-example.el --- Retained state example -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This example keeps state inside one retained Component, derives display
|
||||||
|
;; values with `etaf-computed', and routes named mutations through an Action.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'etaf)
|
||||||
|
|
||||||
|
(defconst etaf-counter-example-buffer-name "*ETAF Counter Example*"
|
||||||
|
"Buffer opened by `etaf-counter-example-open'.")
|
||||||
|
|
||||||
|
(etaf-action-define etaf-counter-example-update (runtime count operation)
|
||||||
|
"Apply OPERATION to COUNT inside RUNTIME."
|
||||||
|
(ignore runtime)
|
||||||
|
(etaf-set-value
|
||||||
|
count
|
||||||
|
(pcase operation
|
||||||
|
('increment (1+ (etaf-value count)))
|
||||||
|
('decrement (max 0 (1- (etaf-value count))))
|
||||||
|
('reset 0)
|
||||||
|
(_ (user-error "Unknown counter operation: %S" operation)))))
|
||||||
|
|
||||||
|
(defun etaf-counter-example--header (title)
|
||||||
|
"Return the counter header for TITLE."
|
||||||
|
(etaf-view
|
||||||
|
(column :class "hero"
|
||||||
|
(text :class "eyebrow" "BEST PRACTICE / RETAINED STATE")
|
||||||
|
(text :face 'bold (expr :value title))
|
||||||
|
(text :color "#66706A"
|
||||||
|
"State belongs to setup; rendering only reads it."))))
|
||||||
|
|
||||||
|
(defun etaf-counter-example--metrics (count double status)
|
||||||
|
"Return metric cards for COUNT, DOUBLE, and STATUS."
|
||||||
|
(etaf-view
|
||||||
|
(flex :width '(680) :flex-flow '(row wrap) :gap '(1 (12))
|
||||||
|
(text :class "metric"
|
||||||
|
(expr :value (format "COUNT %d" (etaf-value count))))
|
||||||
|
(text :class "metric"
|
||||||
|
(expr :value (format "DOUBLE %d" (etaf-value double))))
|
||||||
|
(text :class "metric"
|
||||||
|
(expr :value (format "STATE %s" (etaf-value status)))))))
|
||||||
|
|
||||||
|
(defun etaf-counter-example--action (count label host-ref operation)
|
||||||
|
"Return one COUNT action named LABEL using HOST-REF and OPERATION."
|
||||||
|
(etaf-view
|
||||||
|
(text :class "action" :ref host-ref :role 'button
|
||||||
|
:use (list (etaf-focusable))
|
||||||
|
:on-press (lambda ()
|
||||||
|
(etaf-dispatch 'etaf-counter-example-update
|
||||||
|
count operation))
|
||||||
|
(expr :value label))))
|
||||||
|
|
||||||
|
(defun etaf-counter-example--actions (count)
|
||||||
|
"Return the action group for COUNT."
|
||||||
|
(etaf-view
|
||||||
|
(flex :width '(680) :flex-flow '(row wrap) :gap '(1 (12))
|
||||||
|
(expr :value
|
||||||
|
(etaf-counter-example--action
|
||||||
|
count "− DECREMENT" 'counter-decrement 'decrement))
|
||||||
|
(expr :value
|
||||||
|
(etaf-counter-example--action
|
||||||
|
count "RESET" 'counter-reset 'reset))
|
||||||
|
(expr :value
|
||||||
|
(etaf-counter-example--action
|
||||||
|
count "+ INCREMENT" 'counter-increment 'increment)))))
|
||||||
|
|
||||||
|
(etaf-define-component etaf-counter-example-card (&key title initial-value)
|
||||||
|
"Render a retained counter named TITLE starting at INITIAL-VALUE."
|
||||||
|
:styles
|
||||||
|
(styles
|
||||||
|
("&" :width (680) :color "#252A2E" :bgcolor "#F8F5EE")
|
||||||
|
(".hero" :width (680) :padding (1 (18)) :border "#8F432F"
|
||||||
|
:bgcolor "#FFFDF8" :text-align center)
|
||||||
|
(".eyebrow" :color "#8F432F" :face bold)
|
||||||
|
(".metric" :flex-grow 1 :flex-shrink 1 :flex-basis (200)
|
||||||
|
:min-width (180) :padding (1 (14)) :border "#6D8A73"
|
||||||
|
:bgcolor "#DCEBDD" :text-align center)
|
||||||
|
(".action" :flex-grow 1 :flex-shrink 1 :flex-basis (160)
|
||||||
|
:min-width (140) :padding (1 (12)) :border "#4E7890"
|
||||||
|
:bgcolor "#D9EAF2" :text-align center :face bold)
|
||||||
|
(".note" :width (680) :padding (1 (16)) :border "#8D887F"
|
||||||
|
:color "#4D5651" :bgcolor "#EEEAE2"))
|
||||||
|
:setup
|
||||||
|
(let* ((count (etaf-ref (or initial-value 0) :name 'counter))
|
||||||
|
(double (etaf-computed
|
||||||
|
(lambda () (* 2 (etaf-value count)))
|
||||||
|
:name 'counter-double))
|
||||||
|
(status (etaf-computed
|
||||||
|
(lambda ()
|
||||||
|
(if (zerop (etaf-value count)) "READY" "ACTIVE"))
|
||||||
|
:name 'counter-status)))
|
||||||
|
(lambda ()
|
||||||
|
(etaf-view
|
||||||
|
(column
|
||||||
|
(expr :value (etaf-counter-example--header title))
|
||||||
|
(spacer :height 1)
|
||||||
|
(expr :value
|
||||||
|
(etaf-counter-example--metrics count double status))
|
||||||
|
(spacer :height 1)
|
||||||
|
(expr :value (etaf-counter-example--actions count))
|
||||||
|
(spacer :height 1)
|
||||||
|
(text :class "note"
|
||||||
|
"Public path: Event → Action → Ref → Computed → Runtime commit"))))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-counter-example-view ()
|
||||||
|
"Return the counter example View."
|
||||||
|
(etaf-view
|
||||||
|
(etaf-counter-example-card
|
||||||
|
:title "Counter workspace"
|
||||||
|
:initial-value 2)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-counter-example-open ()
|
||||||
|
"Mount the counter example and return its buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buffer (etaf-mount etaf-counter-example-buffer-name
|
||||||
|
(etaf-counter-example-view))))
|
||||||
|
(when (called-interactively-p 'interactive)
|
||||||
|
(pop-to-buffer buffer))
|
||||||
|
buffer))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-counter-example-close ()
|
||||||
|
"Unmount and kill the counter example buffer."
|
||||||
|
(interactive)
|
||||||
|
(when-let ((runtime
|
||||||
|
(etaf-runtime-for-buffer etaf-counter-example-buffer-name)))
|
||||||
|
(etaf-unmount runtime))
|
||||||
|
(when-let ((buffer (get-buffer etaf-counter-example-buffer-name)))
|
||||||
|
(kill-buffer buffer)))
|
||||||
|
|
||||||
|
(provide 'etaf-counter-example)
|
||||||
|
|
||||||
|
;;; etaf-counter-example.el ends here
|
||||||
190
examples/etaf-data-example.el
Normal file
190
examples/etaf-data-example.el
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
;;; etaf-data-example.el --- Data Controller example -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This example owns a Data Controller at the Component boundary, stops it on
|
||||||
|
;; unmount, and keeps query, selection, and mutation on public Data APIs.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'etaf)
|
||||||
|
|
||||||
|
(defconst etaf-data-example-buffer-name "*ETAF Data Example*"
|
||||||
|
"Buffer opened by `etaf-data-example-open'.")
|
||||||
|
|
||||||
|
(defconst etaf-data-example-tasks
|
||||||
|
'((:id 1 :title "Define the public boundary" :owner "Ada" :status open)
|
||||||
|
(:id 2 :title "Verify retained updates" :owner "Grace" :status done)
|
||||||
|
(:id 3 :title "Document cleanup ownership" :owner "Alan" :status open)
|
||||||
|
(:id 4 :title "Review visual hierarchy" :owner "Barbara" :status open)
|
||||||
|
(:id 5 :title "Run the complete gate" :owner "Edsger" :status done))
|
||||||
|
"Initial records used by the Data Controller example.")
|
||||||
|
|
||||||
|
(etaf-action-define etaf-data-example-add (runtime controller next-id)
|
||||||
|
"Insert one task into CONTROLLER using NEXT-ID inside RUNTIME."
|
||||||
|
(ignore runtime)
|
||||||
|
(let ((identity (etaf-value next-id)))
|
||||||
|
(etaf-data-mutate
|
||||||
|
controller 'insert
|
||||||
|
(list :id identity
|
||||||
|
:title (format "Review example #%d" identity)
|
||||||
|
:owner "Team"
|
||||||
|
:status 'open))
|
||||||
|
(etaf-set-value next-id (1+ identity))))
|
||||||
|
|
||||||
|
(etaf-action-define etaf-data-example-toggle (runtime controller identity)
|
||||||
|
"Toggle IDENTITY selection in CONTROLLER inside RUNTIME."
|
||||||
|
(ignore runtime)
|
||||||
|
(etaf-data-select controller identity
|
||||||
|
(not (etaf-data-selected-p controller identity))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--filter (controller query)
|
||||||
|
"Set CONTROLLER to QUERY and load its matching records."
|
||||||
|
(etaf-data-set-query controller query)
|
||||||
|
(etaf-data-load controller))
|
||||||
|
|
||||||
|
(defun etaf-data-example--row (controller task)
|
||||||
|
"Return one TASK row bound to CONTROLLER."
|
||||||
|
(let* ((identity (plist-get task :id))
|
||||||
|
(selected (etaf-data-selected-p controller identity))
|
||||||
|
(status (plist-get task :status))
|
||||||
|
(host-ref (intern (format "data-task-%d" identity))))
|
||||||
|
(etaf-view
|
||||||
|
(flex :width '(718) :flex-flow '(row nowrap) :gap '(0 (10))
|
||||||
|
:padding '(1 (12)) :border "#6D8A73"
|
||||||
|
:bgcolor (if selected "#DCEBDD" "#FFFDF8")
|
||||||
|
:ref host-ref :role 'button :use (list (etaf-focusable))
|
||||||
|
:on-press (lambda ()
|
||||||
|
(etaf-dispatch 'etaf-data-example-toggle
|
||||||
|
controller identity))
|
||||||
|
(text :width '(36) :face 'bold
|
||||||
|
:color (if selected "#2F6B43" "#8D887F")
|
||||||
|
(expr :value (if selected "●" "○")))
|
||||||
|
(text :flex-grow 1 :flex-shrink 1 :flex-basis '(390)
|
||||||
|
:min-width '(280)
|
||||||
|
(expr :value (plist-get task :title)))
|
||||||
|
(text :width '(96) :color "#66706A"
|
||||||
|
(expr :value (plist-get task :owner)))
|
||||||
|
(text :width '(84) :face 'bold :text-align 'right
|
||||||
|
:color (if (eq status 'done) "#2F6B43" "#9B4A34")
|
||||||
|
(expr :value (upcase (symbol-name status))))))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--header (controller)
|
||||||
|
"Return the summary header for CONTROLLER."
|
||||||
|
(let ((total (etaf-value (etaf-data-total controller)))
|
||||||
|
(selection (etaf-value (etaf-data-selection controller))))
|
||||||
|
(etaf-view
|
||||||
|
(column :width '(720) :padding '(1 (18)) :border "#8F432F"
|
||||||
|
:bgcolor "#FFFDF8" :text-align 'center
|
||||||
|
(text :color "#8F432F" :face 'bold
|
||||||
|
"BEST PRACTICE / DATA OWNERSHIP")
|
||||||
|
(text :face 'bold "Task controller")
|
||||||
|
(text :color "#66706A"
|
||||||
|
(expr :value
|
||||||
|
(format "%d records · %d selected" total
|
||||||
|
(length selection))))))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--filter-control
|
||||||
|
(controller label host-ref query border background)
|
||||||
|
"Return a filter LABEL for CONTROLLER using HOST-REF and QUERY.
|
||||||
|
Use BORDER and BACKGROUND for its semantic color family."
|
||||||
|
(etaf-view
|
||||||
|
(text :padding '(1 (12)) :border border :bgcolor background
|
||||||
|
:face 'bold :ref host-ref :role 'button
|
||||||
|
:use (list (etaf-focusable))
|
||||||
|
:on-press (lambda ()
|
||||||
|
(etaf-data-example--filter controller query))
|
||||||
|
(expr :value label))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--toolbar (controller next-id)
|
||||||
|
"Return the action toolbar for CONTROLLER and NEXT-ID."
|
||||||
|
(etaf-view
|
||||||
|
(flex :width '(720) :flex-flow '(row wrap) :gap '(1 (10))
|
||||||
|
(expr :value (etaf-data-example--filter-control
|
||||||
|
controller "ALL" 'data-filter-all nil
|
||||||
|
"#4E7890" "#D9EAF2"))
|
||||||
|
(expr :value (etaf-data-example--filter-control
|
||||||
|
controller "OPEN" 'data-filter-open '(:status open)
|
||||||
|
"#C97252" "#F1D4C9"))
|
||||||
|
(expr :value (etaf-data-example--filter-control
|
||||||
|
controller "DONE" 'data-filter-done '(:status done)
|
||||||
|
"#6D8A73" "#DCEBDD"))
|
||||||
|
(text :padding '(1 (12)) :border "#7A6B95" :bgcolor "#E7E2F1"
|
||||||
|
:face 'bold :ref 'data-add :role 'button
|
||||||
|
:use (list (etaf-focusable))
|
||||||
|
:on-press (lambda ()
|
||||||
|
(etaf-dispatch 'etaf-data-example-add
|
||||||
|
controller next-id))
|
||||||
|
"+ ADD TASK"))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--rows (controller)
|
||||||
|
"Return the loaded task rows for CONTROLLER."
|
||||||
|
(let ((items (etaf-value (etaf-data-items controller))))
|
||||||
|
(if items
|
||||||
|
(mapcar (lambda (task)
|
||||||
|
(etaf-data-example--row controller task))
|
||||||
|
items)
|
||||||
|
(etaf-view
|
||||||
|
(text :width '(720) :padding '(2 (16))
|
||||||
|
:border "#8D887F" :bgcolor "#EEEAE2"
|
||||||
|
:text-align 'center "No matching tasks.")))))
|
||||||
|
|
||||||
|
(defun etaf-data-example--view (controller next-id)
|
||||||
|
"Return the Data example View for CONTROLLER and NEXT-ID."
|
||||||
|
(etaf-view
|
||||||
|
(column :width '(720) :color "#252A2E" :bgcolor "#F8F5EE"
|
||||||
|
(expr :value (etaf-data-example--header controller))
|
||||||
|
(spacer :height 1)
|
||||||
|
(expr :value (etaf-data-example--toolbar controller next-id))
|
||||||
|
(spacer :height 1)
|
||||||
|
(column :width '(720)
|
||||||
|
(expr :value (etaf-data-example--rows controller)))
|
||||||
|
(spacer :height 1)
|
||||||
|
(text :width '(720) :padding '(1 (16)) :border "#8D887F"
|
||||||
|
:color "#4D5651" :bgcolor "#EEEAE2"
|
||||||
|
"Owner rule: create in setup, mutate through Data, stop on unmount"))))
|
||||||
|
|
||||||
|
(etaf-define-component etaf-data-example-app ()
|
||||||
|
"Render a memory-backed task application with owned cleanup."
|
||||||
|
:setup
|
||||||
|
(let* ((source (etaf-data-memory-source
|
||||||
|
etaf-data-example-tasks :id-key :id
|
||||||
|
:name 'etaf-data-example))
|
||||||
|
(controller (etaf-data-controller
|
||||||
|
source :page-size 20
|
||||||
|
:name 'etaf-data-example))
|
||||||
|
(next-id (etaf-ref 6 :name 'etaf-data-example-next-id)))
|
||||||
|
(etaf-on-mounted (lambda () (etaf-data-load controller)))
|
||||||
|
(etaf-on-unmounted (lambda () (etaf-data-stop controller)))
|
||||||
|
(lambda () (etaf-data-example--view controller next-id))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-data-example-view ()
|
||||||
|
"Return the Data Controller example View."
|
||||||
|
(etaf-view (etaf-data-example-app)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-data-example-open ()
|
||||||
|
"Mount the Data Controller example and return its buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buffer (etaf-mount etaf-data-example-buffer-name
|
||||||
|
(etaf-data-example-view))))
|
||||||
|
(when (called-interactively-p 'interactive)
|
||||||
|
(pop-to-buffer buffer))
|
||||||
|
buffer))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-data-example-close ()
|
||||||
|
"Unmount and kill the Data Controller example buffer."
|
||||||
|
(interactive)
|
||||||
|
(when-let ((runtime
|
||||||
|
(etaf-runtime-for-buffer etaf-data-example-buffer-name)))
|
||||||
|
(etaf-unmount runtime))
|
||||||
|
(when-let ((buffer (get-buffer etaf-data-example-buffer-name)))
|
||||||
|
(kill-buffer buffer)))
|
||||||
|
|
||||||
|
(provide 'etaf-data-example)
|
||||||
|
|
||||||
|
;;; etaf-data-example.el ends here
|
||||||
144
examples/etaf-resource-example.el
Normal file
144
examples/etaf-resource-example.el
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
;;; etaf-resource-example.el --- Resource lifecycle example -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This example keeps a synchronous Resource inside Component setup, exposes
|
||||||
|
;; success and error states, and lets Scope disposal own loader cleanup.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'etaf)
|
||||||
|
|
||||||
|
(defconst etaf-resource-example-buffer-name "*ETAF Resource Example*"
|
||||||
|
"Buffer opened by `etaf-resource-example-open'.")
|
||||||
|
|
||||||
|
(defun etaf-resource-example--message (resource)
|
||||||
|
"Return a display message for RESOURCE."
|
||||||
|
(pcase (etaf-resource-status resource)
|
||||||
|
('success (etaf-resource-value resource))
|
||||||
|
('error (error-message-string (etaf-resource-error resource)))
|
||||||
|
('loading "Loading service snapshot…")
|
||||||
|
(_ "Resource has not loaded.")))
|
||||||
|
|
||||||
|
(defun etaf-resource-example--header ()
|
||||||
|
"Return the Resource example header."
|
||||||
|
(etaf-view
|
||||||
|
(column :width '(680) :padding '(1 (18)) :border "#8F432F"
|
||||||
|
:bgcolor "#FFFDF8" :text-align 'center
|
||||||
|
(text :color "#8F432F" :face 'bold
|
||||||
|
"BEST PRACTICE / RESOURCE LIFECYCLE")
|
||||||
|
(text :face 'bold "Service health")
|
||||||
|
(text :color "#66706A"
|
||||||
|
"Loader errors become explicit state; cleanup stays scoped."))))
|
||||||
|
|
||||||
|
(defun etaf-resource-example--status (resource cleanup-count)
|
||||||
|
"Return the status card for RESOURCE and CLEANUP-COUNT."
|
||||||
|
(let* ((status (etaf-resource-status resource))
|
||||||
|
(success (eq status 'success))
|
||||||
|
(surface (if success "#DCEBDD" "#F1D4C9"))
|
||||||
|
(border (if success "#6D8A73" "#C97252"))
|
||||||
|
(ink (if success "#24422D" "#6B3020")))
|
||||||
|
(etaf-view
|
||||||
|
(column :width '(680) :padding '(2 (18)) :border border
|
||||||
|
:bgcolor surface :color ink :text-align 'center
|
||||||
|
(text :face 'bold
|
||||||
|
(expr :value (upcase (symbol-name status))))
|
||||||
|
(text (expr :value (etaf-resource-example--message resource)))
|
||||||
|
(text :color "#66706A"
|
||||||
|
(expr :value
|
||||||
|
(format "CLEANUPS %d" (etaf-value cleanup-count))))))))
|
||||||
|
|
||||||
|
(defun etaf-resource-example--actions (resource fail-next)
|
||||||
|
"Return action controls for RESOURCE and FAIL-NEXT."
|
||||||
|
(etaf-view
|
||||||
|
(flex :width '(680) :flex-flow '(row wrap) :gap '(1 (12))
|
||||||
|
(text :flex-grow 1 :flex-shrink 1 :flex-basis '(200)
|
||||||
|
:min-width '(180) :padding '(1 (12))
|
||||||
|
:border "#4E7890" :bgcolor "#D9EAF2" :face 'bold
|
||||||
|
:text-align 'center :ref 'resource-reload :role 'button
|
||||||
|
:use (list (etaf-focusable))
|
||||||
|
:on-press (lambda () (etaf-resource-load resource))
|
||||||
|
"RELOAD")
|
||||||
|
(text :flex-grow 1 :flex-shrink 1 :flex-basis '(200)
|
||||||
|
:min-width '(180) :padding '(1 (12))
|
||||||
|
:border "#C97252" :bgcolor "#F1D4C9" :face 'bold
|
||||||
|
:text-align 'center :ref 'resource-fail :role 'button
|
||||||
|
:use (list (etaf-focusable))
|
||||||
|
:on-press (lambda ()
|
||||||
|
(etaf-set-value fail-next t)
|
||||||
|
(etaf-resource-load resource))
|
||||||
|
"SIMULATE FAILURE"))))
|
||||||
|
|
||||||
|
(defun etaf-resource-example--view (resource fail-next cleanup-count)
|
||||||
|
"Return the example View for RESOURCE, FAIL-NEXT, and CLEANUP-COUNT."
|
||||||
|
(etaf-view
|
||||||
|
(column :width '(680) :color "#252A2E" :bgcolor "#F8F5EE"
|
||||||
|
(expr :value (etaf-resource-example--header))
|
||||||
|
(spacer :height 1)
|
||||||
|
(expr :value
|
||||||
|
(etaf-resource-example--status resource cleanup-count))
|
||||||
|
(spacer :height 1)
|
||||||
|
(expr :value (etaf-resource-example--actions resource fail-next))
|
||||||
|
(spacer :height 1)
|
||||||
|
(text :width '(680) :padding '(1 (16)) :border "#8D887F"
|
||||||
|
:color "#4D5651" :bgcolor "#EEEAE2"
|
||||||
|
"Scope rule: reload releases the old value; unmount releases the last one"))))
|
||||||
|
|
||||||
|
(etaf-define-component etaf-resource-example-app ()
|
||||||
|
"Render a reloadable Resource with visible cleanup and error state."
|
||||||
|
:setup
|
||||||
|
(let ((fail-next (etaf-ref nil :name 'etaf-resource-example-fail))
|
||||||
|
(cleanup-count (etaf-ref 0 :name 'etaf-resource-example-cleanups))
|
||||||
|
(generation 0)
|
||||||
|
resource)
|
||||||
|
(setq resource
|
||||||
|
(etaf-resource
|
||||||
|
(lambda ()
|
||||||
|
(cl-incf generation)
|
||||||
|
(if (etaf-value fail-next)
|
||||||
|
(progn
|
||||||
|
(etaf-set-value fail-next nil)
|
||||||
|
(error "Synthetic service failure %d" generation))
|
||||||
|
(etaf-resource-result
|
||||||
|
(format "Generation %d is healthy." generation)
|
||||||
|
:cleanup
|
||||||
|
(lambda ()
|
||||||
|
(etaf-set-value
|
||||||
|
cleanup-count
|
||||||
|
(1+ (etaf-value cleanup-count)))))))
|
||||||
|
:immediate nil
|
||||||
|
:name 'etaf-resource-example))
|
||||||
|
(etaf-on-mounted (lambda () (etaf-resource-load resource)))
|
||||||
|
(lambda ()
|
||||||
|
(etaf-resource-example--view resource fail-next cleanup-count))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-resource-example-view ()
|
||||||
|
"Return the Resource lifecycle example View."
|
||||||
|
(etaf-view (etaf-resource-example-app)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-resource-example-open ()
|
||||||
|
"Mount the Resource lifecycle example and return its buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((buffer (etaf-mount etaf-resource-example-buffer-name
|
||||||
|
(etaf-resource-example-view))))
|
||||||
|
(when (called-interactively-p 'interactive)
|
||||||
|
(pop-to-buffer buffer))
|
||||||
|
buffer))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun etaf-resource-example-close ()
|
||||||
|
"Unmount and kill the Resource lifecycle example buffer."
|
||||||
|
(interactive)
|
||||||
|
(when-let ((runtime
|
||||||
|
(etaf-runtime-for-buffer etaf-resource-example-buffer-name)))
|
||||||
|
(etaf-unmount runtime))
|
||||||
|
(when-let ((buffer (get-buffer etaf-resource-example-buffer-name)))
|
||||||
|
(kill-buffer buffer)))
|
||||||
|
|
||||||
|
(provide 'etaf-resource-example)
|
||||||
|
|
||||||
|
;;; etaf-resource-example.el ends here
|
||||||
21
postmortem/2026-08-05-executable-core-examples.en.md
Normal file
21
postmortem/2026-08-05-executable-core-examples.en.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Executable core examples
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Core best-practice examples are ordinary loadable Elisp files under `examples/`, with one application responsibility per file and a public open/close pair. They depend only on ETAF core and are compiled and interaction-tested by the normal repository gate.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
README fragments prove syntax but do not prove lifecycle ownership or the installed Runtime dispatch path. A single large playground proves breadth but makes it hard to identify which layer owns state, data loading, and cleanup. Small executable applications keep those boundaries visible and let tests fail when an example drifts from the framework contract.
|
||||||
|
|
||||||
|
Data Controllers and Resources are created during Component setup but start state-changing loads from the mounted lifecycle. This distinction matters because render is intentionally write-protected. Detached Data Controllers are stopped explicitly on unmount; Resource values remain owned by their Scope cleanup.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
- Depending on `etaf-ui` would make core examples inherit the incomplete catalog migration and would blur the independent package boundary.
|
||||||
|
- A shared example framework or theme module would add indirection without enough reuse; three small applications can own their own restrained presentation.
|
||||||
|
- Catching loader failures and returning display defaults would hide the Resource error contract instead of teaching it.
|
||||||
|
|
||||||
|
## Consequence
|
||||||
|
|
||||||
|
New core examples should be admitted only when they teach a distinct public ownership rule, include a real mounted interaction test, and remain free of private ETAF/Ebox calls. Broad application demonstrations continue to belong in `etaf-playground`.
|
||||||
21
postmortem/2026-08-05-executable-core-examples.zh.md
Normal file
21
postmortem/2026-08-05-executable-core-examples.zh.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 可执行 core 示例
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
Core 最佳实践示例作为普通可加载 Elisp 文件放在 `examples/` 中,每个文件只负责一种应用职责,并提供公开的 open/close 命令对。它们只依赖 ETAF core,并由仓库常规门禁编译和执行交互测试。
|
||||||
|
|
||||||
|
## 原因
|
||||||
|
|
||||||
|
README 片段只能证明语法,不能证明 lifecycle 所有权或已安装 Runtime 的 dispatch 路径。单个大型 playground 可以展示广度,却难以看清状态、数据加载和清理分别属于哪一层。小型可执行应用能让这些边界保持可见,并在示例偏离框架契约时让测试真正失败。
|
||||||
|
|
||||||
|
Data Controller 与 Resource 在 Component setup 中创建,但从 mounted lifecycle 启动会修改状态的加载。这个区别很重要,因为 render 阶段被明确设置为写保护。Detached Data Controller 在 unmount 时显式停止;Resource 值继续由所属 Scope cleanup 管理。
|
||||||
|
|
||||||
|
## 未采用方案
|
||||||
|
|
||||||
|
- 依赖 `etaf-ui` 会让 core 示例继承尚未完成的组件目录迁移,并模糊独立包边界。
|
||||||
|
- 增加共享示例框架或 theme 模块会在复用不足时制造间接层;三个小应用可以各自拥有克制的展示样式。
|
||||||
|
- 捕获 loader 错误并返回展示默认值会隐藏 Resource error 契约,而不是教授它。
|
||||||
|
|
||||||
|
## 后续影响
|
||||||
|
|
||||||
|
新的 core 示例只有在讲清一种独立的公开所有权规则、包含真实 mounted 交互测试,并且不调用 ETAF/Ebox 私有 API 时才应加入。更广泛的完整应用展示继续属于 `etaf-playground`。
|
||||||
@ -51,23 +51,32 @@
|
|||||||
"Keep the public architecture, guide, and plan in both languages."
|
"Keep the public architecture, guide, and plan in both languages."
|
||||||
(dolist (file '("README.md"
|
(dolist (file '("README.md"
|
||||||
"README.zh-CN.md"
|
"README.zh-CN.md"
|
||||||
|
"DESIGN.md"
|
||||||
|
"DESIGN.zh-CN.md"
|
||||||
|
"examples/README.md"
|
||||||
|
"examples/README.zh-CN.md"
|
||||||
"docs/architecture.en.md"
|
"docs/architecture.en.md"
|
||||||
"docs/architecture.zh.md"
|
"docs/architecture.zh.md"
|
||||||
"docs/user-guide.en.md"
|
"docs/user-guide.en.md"
|
||||||
"docs/user-guide.zh.md"
|
"docs/user-guide.zh.md"
|
||||||
"docs/implementation-plan.en.md"
|
"docs/implementation-plan.en.md"
|
||||||
"docs/implementation-plan.zh.md"))
|
"docs/implementation-plan.zh.md"
|
||||||
|
"postmortem/2026-08-05-executable-core-examples.en.md"
|
||||||
|
"postmortem/2026-08-05-executable-core-examples.zh.md"))
|
||||||
(should (file-exists-p (expand-file-name file etaf-docs-test--root)))))
|
(should (file-exists-p (expand-file-name file etaf-docs-test--root)))))
|
||||||
|
|
||||||
(ert-deftest etaf-docs-user-surface-has-one-current-vocabulary ()
|
(ert-deftest etaf-docs-user-surface-has-one-current-vocabulary ()
|
||||||
"Reject old public entry names and stale View examples in user docs."
|
"Reject old public entry names and stale View examples in user docs."
|
||||||
(dolist (file '("README.md" "README.zh-CN.md"
|
(dolist (file '("README.md" "README.zh-CN.md"
|
||||||
|
"examples/README.md" "examples/README.zh-CN.md"
|
||||||
"docs/architecture.en.md" "docs/architecture.zh.md"
|
"docs/architecture.en.md" "docs/architecture.zh.md"
|
||||||
"docs/user-guide.en.md" "docs/user-guide.zh.md"
|
"docs/user-guide.en.md" "docs/user-guide.zh.md"
|
||||||
"docs/implementation-plan.en.md"
|
"docs/implementation-plan.en.md"
|
||||||
"docs/implementation-plan.zh.md"))
|
"docs/implementation-plan.zh.md"))
|
||||||
(let ((contents (etaf-docs-test--read file)))
|
(let ((contents (etaf-docs-test--read file)))
|
||||||
(should-not (string-match-p "etaf-template\|etaf-create-app" contents))
|
(should-not
|
||||||
|
(string-match-p
|
||||||
|
(regexp-opt '("etaf-template" "etaf-create-app")) contents))
|
||||||
(should-not (string-match-p "(text \"[^\"]+\" :" contents))))
|
(should-not (string-match-p "(text \"[^\"]+\" :" contents))))
|
||||||
(let ((guide (etaf-docs-test--read "docs/user-guide.en.md")))
|
(let ((guide (etaf-docs-test--read "docs/user-guide.en.md")))
|
||||||
(dolist (token '("etaf-view" "etaf-mount" "etaf-define-component"
|
(dolist (token '("etaf-view" "etaf-mount" "etaf-define-component"
|
||||||
@ -77,6 +86,7 @@
|
|||||||
(ert-deftest etaf-docs-elisp-examples-are-readable ()
|
(ert-deftest etaf-docs-elisp-examples-are-readable ()
|
||||||
"Keep fenced Elisp examples syntactically readable by Emacs."
|
"Keep fenced Elisp examples syntactically readable by Emacs."
|
||||||
(dolist (file '("README.md" "README.zh-CN.md"
|
(dolist (file '("README.md" "README.zh-CN.md"
|
||||||
|
"examples/README.md" "examples/README.zh-CN.md"
|
||||||
"docs/architecture.en.md" "docs/architecture.zh.md"
|
"docs/architecture.en.md" "docs/architecture.zh.md"
|
||||||
"docs/user-guide.en.md" "docs/user-guide.zh.md"
|
"docs/user-guide.en.md" "docs/user-guide.zh.md"
|
||||||
"docs/implementation-plan.en.md"
|
"docs/implementation-plan.en.md"
|
||||||
@ -87,9 +97,25 @@
|
|||||||
|
|
||||||
(ert-deftest etaf-source-uses-only-public-ebox-names ()
|
(ert-deftest etaf-source-uses-only-public-ebox-names ()
|
||||||
"Keep the ETAF implementation independent of Ebox private functions."
|
"Keep the ETAF implementation independent of Ebox private functions."
|
||||||
(dolist (file (directory-files etaf-docs-test--root t "\\.el\\'"))
|
(dolist (file
|
||||||
|
(append
|
||||||
|
(directory-files etaf-docs-test--root t "\\.el\\'")
|
||||||
|
(directory-files
|
||||||
|
(expand-file-name "examples" etaf-docs-test--root)
|
||||||
|
t "\\.el\\'")))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(insert-file-contents file)
|
(insert-file-contents file)
|
||||||
(should-not (re-search-forward "ebox--" nil t)))))
|
(should-not (re-search-forward "ebox--" nil t)))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-examples-use-only-public-etaf-names ()
|
||||||
|
"Keep executable examples on the public ETAF contract."
|
||||||
|
(dolist (file
|
||||||
|
(directory-files
|
||||||
|
(expand-file-name "examples" etaf-docs-test--root)
|
||||||
|
t "\\.el\\'"))
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents file)
|
||||||
|
(should-not
|
||||||
|
(re-search-forward (regexp-opt '("etaf--" "ebox--")) nil t)))))
|
||||||
|
|
||||||
;;; etaf-docs-tests.el ends here
|
;;; etaf-docs-tests.el ends here
|
||||||
|
|||||||
83
tests/etaf-examples-tests.el
Normal file
83
tests/etaf-examples-tests.el
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
;;; etaf-examples-tests.el --- ETAF executable example tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'etaf-counter-example)
|
||||||
|
(require 'etaf-data-example)
|
||||||
|
(require 'etaf-resource-example)
|
||||||
|
|
||||||
|
(defun etaf-examples-test--text (buffer)
|
||||||
|
"Return plain rendered text from BUFFER."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(substring-no-properties (buffer-string))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-examples-load-without-opening-buffers ()
|
||||||
|
"Loading example libraries must not mount an application."
|
||||||
|
(dolist (name (list etaf-counter-example-buffer-name
|
||||||
|
etaf-data-example-buffer-name
|
||||||
|
etaf-resource-example-buffer-name))
|
||||||
|
(should-not (get-buffer name))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-counter-example-drives-event-action-and-state ()
|
||||||
|
"Drive the counter through its installed public event path."
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((buffer (etaf-counter-example-open))
|
||||||
|
(runtime (etaf-runtime-for-buffer buffer)))
|
||||||
|
(should (string-match-p "COUNT 2"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'counter-increment 'press)
|
||||||
|
(should (string-match-p "COUNT 3"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(should (string-match-p "DOUBLE 6"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'counter-reset 'press)
|
||||||
|
(should (string-match-p "STATE READY"
|
||||||
|
(etaf-examples-test--text buffer))))
|
||||||
|
(etaf-counter-example-close)))
|
||||||
|
|
||||||
|
(ert-deftest etaf-data-example-owns-query-selection-and-mutation ()
|
||||||
|
"Exercise Data query, selection, and mutation through mounted Hosts."
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((buffer (etaf-data-example-open))
|
||||||
|
(runtime (etaf-runtime-for-buffer buffer)))
|
||||||
|
(should (string-match-p "5 records · 0 selected"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'data-task-1 'press)
|
||||||
|
(should (string-match-p "5 records · 1 selected"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'data-add 'press)
|
||||||
|
(should (string-match-p "Review example #6"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'data-filter-done 'press)
|
||||||
|
(should (string-match-p "Verify retained updates"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(should-not (string-match-p "Define the public boundary"
|
||||||
|
(etaf-examples-test--text buffer))))
|
||||||
|
(etaf-data-example-close)))
|
||||||
|
|
||||||
|
(ert-deftest etaf-resource-example-shows-errors-recovery-and-cleanup ()
|
||||||
|
"Reload the Resource and keep failure state visible until recovery."
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((buffer (etaf-resource-example-open))
|
||||||
|
(runtime (etaf-runtime-for-buffer buffer)))
|
||||||
|
(should (string-match-p "Generation 1 is healthy"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'resource-fail 'press)
|
||||||
|
(should (string-match-p "Synthetic service failure 2"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(should (string-match-p "CLEANUPS 1"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'resource-reload 'press)
|
||||||
|
(should (string-match-p "Generation 3 is healthy"
|
||||||
|
(etaf-examples-test--text buffer)))
|
||||||
|
(etaf-dispatch-event runtime 'resource-reload 'press)
|
||||||
|
(should (string-match-p "CLEANUPS 2"
|
||||||
|
(etaf-examples-test--text buffer))))
|
||||||
|
(etaf-resource-example-close)))
|
||||||
|
|
||||||
|
(provide 'etaf-examples-tests)
|
||||||
|
|
||||||
|
;;; etaf-examples-tests.el ends here
|
||||||
Loading…
Reference in New Issue
Block a user