etaf-playground/scripts/task-workbench-gui-scenarios.el

631 lines
30 KiB
EmacsLisp

;;; task-workbench-gui-scenarios.el --- Workbench GUI acceptance -*- lexical-binding: t; -*-
;;; Commentary:
;; A concrete adapter for the existing temporal GUI verifier. Keyboard
;; actions traverse TAB and RET bindings; mouse actions use actual displayed
;; positions; text entry goes through the real minibuffer. The caller connects
;; to the existing GUI server and owns recording, screenshots, and visual review.
;; This adapter records observations; it does not certify visual quality or
;; turn action timings (which include user input) into performance benchmarks.
;;; Code:
(require 'cl-lib)
(require 'playground-gui-scenarios)
(require 'task-workbench
(expand-file-name "../examples/task-workbench.el"
(file-name-directory
(or load-file-name
(bound-and-true-p byte-compile-current-file)
buffer-file-name))))
(defconst wb-gui--primary-name "*ETAF GUI Task Workbench*"
"Dedicated primary buffer used throughout one Workbench verification run.")
(defconst wb-gui--secondary-name "*ETAF GUI Independent Workbench*"
"Dedicated buffer used to verify independent App state.")
(defvar wb-gui--prepared-buffer nil
"Fresh unmounted target prepared before the screen recorder starts.")
(defun wb-gui--configure-native ()
"Preserve the current backend, or verify an explicitly requested module."
(let* ((configured (getenv "EBOX_NATIVE_REFLOW_MODULE_PATH"))
(path
(when configured
(when (string-empty-p configured)
(error "EBOX_NATIVE_REFLOW_MODULE_PATH must not be empty"))
(expand-file-name configured))))
(when (and path (not (file-readable-p path)))
(error "Prepared Ebox native module path is unreadable: %s" path))
(when path
(setq ebox-native-reflow-module-path path))
(let* ((report (ebox-native-reflow-runtime-report))
(loaded (plist-get report :loaded-module-path)))
(when (and path (not (plist-get report :layout-ready-p)))
(error "Ebox native module is unavailable at %s: %S"
path (plist-get report :load-error)))
(when (and path
(not (and loaded
(file-equal-p
(if (file-directory-p path)
(file-name-directory loaded) loaded)
path))))
(error "Loaded Ebox module differs from configured path: %s / %s"
loaded path))
(when loaded
(message "Workbench GUI native module: %s (SHA256 %s)"
loaded (plist-get report :loaded-module-hash)))
report)))
(defun wb-gui--redisplay-for-geometry ()
"Clear this adapter's native diagnostic and paint before measuring geometry."
(when-let* ((diagnostic (current-message))
((string-prefix-p "Workbench GUI native module: " diagnostic)))
(message nil))
(redisplay t))
;;;###autoload
(defun task-workbench-gui-prepare (&optional foreground)
"Prepare a fresh target in the existing frame before recording starts.
Preserve application focus unless FOREGROUND is explicitly non-nil."
(unless (display-graphic-p) (error "Workbench acceptance needs GUI Emacs"))
(when (or (get-buffer wb-gui--primary-name)
(get-buffer wb-gui--secondary-name))
(error "Workbench verification requires fresh dedicated buffers"))
(wb-gui--configure-native)
(setq wb-gui--prepared-buffer (get-buffer-create wb-gui--primary-name))
(with-current-buffer wb-gui--prepared-buffer
(insert "任务工作台\n")
(goto-char (point-min)))
(switch-to-buffer wb-gui--prepared-buffer)
(delete-other-windows)
(when foreground (select-frame-set-input-focus (selected-frame)))
(wb-gui--redisplay-for-geometry)
wb-gui--prepared-buffer)
(defun wb-gui--runtime (context)
"Return the Runtime mounted in CONTEXT's target buffer."
(etaf-runtime-for-buffer (etaf-gui-verifier-context-target-buffer context)))
(defun wb-gui--data (context)
"Inspect CONTEXT's app-owned Data controller for result assertions."
(let ((instance
(cl-find-if
(lambda (instance)
(eq 'wb-app (etaf--component-spec-name
(etaf--component-instance-spec instance))))
(hash-table-values (etaf-runtime-instances (wb-gui--runtime context))))))
(plist-get (etaf--component-instance-state instance) :data)))
(defun wb-gui--ids (context)
"Return the currently loaded task identities in CONTEXT."
(mapcar #'wb-task-id (etaf-value (etaf-data-items (wb-gui--data context)))))
(defun wb-gui--selected-id (context)
"Return CONTEXT's selected task identity, or nil."
(when-let* ((item (etaf-data-selected-item (wb-gui--data context))))
(wb-task-id item)))
(defun wb-gui--text-p (context text)
"Return non-nil when CONTEXT's rendered output contains TEXT."
(string-match-p (regexp-quote text)
(etaf-playground-gui-scenarios--buffer-text context)))
(defun wb-gui--control (context label &optional row-id)
"Find CONTEXT control by aria LABEL, optionally within ROW-ID."
(let* ((runtime (wb-gui--runtime context))
(entries (etaf-runtime-host-props-entries runtime))
(row (and row-id
(cl-find-if
(lambda (entry)
(and (equal (plist-get (cdr entry) :key) row-id)
(member "etaf-table-row"
(split-string
(or (plist-get (cdr entry) :class) "")))))
entries)))
(matches (cl-remove-if-not
(lambda (entry)
(equal label (plist-get (cdr entry) :aria-label))) entries)))
(when row-id
(unless row (error "No task row %S" row-id))
(let* ((ancestries (etaf-runtime-host-ancestries
runtime (cons (car row) (mapcar #'car matches))))
(parent (car (gethash (car row) ancestries))))
(setq matches
(cl-remove-if-not
(lambda (entry) (memq parent (gethash (car entry) ancestries)))
matches))))
(unless (= (length matches) 1)
(error "Expected one control %S in row %S, got %d"
label row-id (length matches)))
(caar matches)))
(defun wb-gui--layout-snapshot (context)
"Observe painted control pixels and semantic row bounds in CONTEXT.
The viewport and font are part of the comparison; character offsets alone
cannot establish that Emacs painted a control on the same visual line."
(let* ((runtime (wb-gui--runtime context))
(window (selected-window))
(frame (window-frame window))
(entries (etaf-runtime-host-props-entries runtime))
(targets
(append
(list (cons "title" (wb-gui--find-text "任务工作台")))
(mapcar
(lambda (target)
(cons (format "%s%s" (car target) (or (cadr target) ""))
(etaf-host-ref-position
runtime (wb-gui--control context (car target) (cadr target)))))
(append
(mapcar #'list '("新增任务" "深色" "定位新增按钮" "全部" "未完成"
"已完成" "搜索" "每页条数 10 ✎" "显示说明"
"刷新 / 重试" "模拟失败"))
(mapcar (lambda (id) (list "完成" id)) (number-sequence 1 10)))))))
(unless (and (display-graphic-p frame)
(eq (window-buffer window) (etaf-runtime-buffer runtime)))
(error "Pixel geometry requires the displayed Workbench GUI target"))
(list
:viewport (list (window-inside-pixel-edges window)
(window-start window) (window-hscroll window))
:font (list (format "%s" (frame-parameter frame 'font))
(frame-char-width frame) (frame-char-height frame))
:controls
(mapcar
(lambda (target)
(let* ((position (cdr target))
(displayed (and position (pos-visible-in-window-p position window)))
(posn (and displayed (posn-at-point position window)))
(xy (and posn (posn-x-y posn))))
(unless (and (numberp (car xy)) (numberp (cdr xy)))
(error "Theme geometry target is not visibly painted: %s" (car target)))
(list (car target) (car xy) (cdr xy))))
targets)
:rows
(mapcar
(lambda (identity)
(let* ((entry
(cl-find-if
(lambda (entry)
(and (equal identity (plist-get (cdr entry) :key))
(member "etaf-table-row"
(split-string (or (plist-get (cdr entry) :class) "")))))
entries))
(bounds (and entry (etaf-host-ref-bounds runtime (car entry)))))
(unless bounds (error "Theme geometry row has no live bounds: %s" identity))
(list identity (car bounds) (cdr bounds))))
(number-sequence 1 10)))))
(defun wb-gui--panel-heights (context)
"Observe CONTEXT's four card identities and layout lines in source order."
(let ((runtime (wb-gui--runtime context))
panels)
(with-current-buffer (etaf-gui-verifier-context-target-buffer context)
(dolist (entry (etaf-runtime-host-props-entries runtime))
(let ((class (plist-get (cdr entry) :class)))
(when (member "etaf-panel" (if (stringp class) (split-string class) class))
(let* ((bounds (etaf-host-ref-bounds runtime (car entry)))
(start (car bounds))
(end (and bounds (1- (cdr bounds)))))
(unless (and start end)
(error "Workbench panel has no live bounds"))
(push (list :ref (car entry) :start start
:lines (1+ (- (line-number-at-pos end)
(line-number-at-pos start))))
panels))))))
(unless (= (length panels) 4)
(error "Expected four Workbench panels, got %d" (length panels)))
(sort panels (lambda (left right)
(< (plist-get left :start) (plist-get right :start))))))
(defun wb-gui--help-layout-preserved-p (context expanded-p)
"Check CONTEXT's disclosure state and unaffected card heights in layout lines.
EXPANDED-P also requires the detail card to recover its original height.
Native Emacs glyph rows may have different pixel heights as content moves."
(let* ((before (etaf-gui-verifier-context-get context 'help-panels-before))
(after (wb-gui--panel-heights context))
(height (lambda (panels index)
(plist-get (nth index panels) :lines))))
(etaf-gui-verifier-context-put context 'help-panels-current after)
(and before
(eq expanded-p (and (wb-gui--text-p context "点击行:选择任务") t))
(equal (mapcar (lambda (panel) (plist-get panel :ref)) before)
(mapcar (lambda (panel) (plist-get panel :ref)) after))
(cl-every (lambda (index)
(= (funcall height before index)
(funcall height after index)))
'(0 1 3))
(if expanded-p
(= (funcall height before 2) (funcall height after 2))
(< (funcall height after 2) (funcall height before 2))))))
(defun wb-gui--capture-theme-baseline (context)
"Capture CONTEXT's initial geometry once it survives another paint turn."
(when (etaf-gui-verifier-context-get context 'light-layout)
(error "Initial theme geometry must only be captured once"))
(wb-gui--redisplay-for-geometry)
(let ((previous (wb-gui--layout-snapshot context))
(deadline (+ (float-time) 5.0))
stable)
(while (not stable)
(when (>= (float-time) deadline)
(error "Initial Workbench geometry did not settle before theme input"))
(sit-for 0.05)
(redisplay t)
(let ((current (wb-gui--layout-snapshot context)))
(setq stable (equal previous current)
previous current)))
(etaf-gui-verifier-context-put context 'light-layout previous)))
(defun wb-gui--theme-layout-preserved-p (context)
"Compare CONTEXT's current painted geometry with its settled light theme."
(let ((current (wb-gui--layout-snapshot context)))
(etaf-gui-verifier-context-put context 'theme-layout-current current)
(equal (etaf-gui-verifier-context-get context 'light-layout) current)))
(defun wb-gui--root-background-p (context color)
"Check that CONTEXT's painted root uses COLOR in its buffer face remap."
(with-current-buffer (etaf-gui-verifier-context-target-buffer context)
(let* ((face (get-text-property (point-min) 'face))
(mapping (and (symbolp face) (assq face face-remapping-alist))))
(and mapping
(cl-some (lambda (attrs)
(and (listp attrs)
(equal color (plist-get attrs :background))))
(cdr mapping))))))
(defun wb-gui--tab-to (context ref)
"Reach REF in CONTEXT by executing actual TAB key bindings."
(let* ((runtime (wb-gui--runtime context))
(limit (1+ (length (etaf-runtime-host-props-entries runtime))))
(at-target
(lambda ()
(and (equal ref (etaf-focused-host-ref runtime))
(equal (point) (etaf-host-ref-position runtime ref))))))
(unless (eq (current-buffer) (etaf-runtime-buffer runtime))
(error "Keyboard input target drifted from the Workbench buffer"))
(while (and (> limit 0) (not (funcall at-target)))
(execute-kbd-macro (kbd "TAB"))
(cl-decf limit))
(unless (funcall at-target)
(error "TAB cannot reach the expected control"))))
(defun wb-gui--press (context label &optional row-id)
"Use TAB and RET to activate LABEL within CONTEXT's optional ROW-ID."
(wb-gui--tab-to context (wb-gui--control context label row-id))
(execute-kbd-macro (kbd "RET")))
(defun wb-gui--visit-next-page (context)
"Advance CONTEXT one page and retain the complete visited task sequence."
(let ((visited (etaf-gui-verifier-context-get
context 'visited-task-ids (wb-gui--ids context))))
(wb-gui--press context "Next page")
(setq visited (append visited (wb-gui--ids context)))
(etaf-gui-verifier-context-put context 'visited-task-ids visited)
(etaf-gui-verifier-context-put context 'visited-tasks (length visited))))
(defun wb-gui--enter (context label text)
"Activate LABEL and type TEXT through the actual minibuffer in CONTEXT."
(let (received)
(let ((minibuffer-exit-hook
(cons (lambda ()
(setq received (minibuffer-contents-no-properties)))
minibuffer-exit-hook)))
(minibuffer-with-setup-hook
(lambda ()
;; The recursive input loop starts after setup hooks return.
;; Queue real input events; RET during setup has no exit target.
(setq unread-command-events
(append (listify-key-sequence (vconcat text (kbd "RET")))
unread-command-events)))
(wb-gui--press context label)))
(unless (equal received text)
(error "Minibuffer did not receive the expected keyboard input"))))
(defun wb-gui--mouse-at (position)
"Click the actual visible buffer POSITION using its displayed event data."
(redisplay t)
(let ((position-data (posn-at-point position (selected-window))))
(unless (and position-data (pos-visible-in-window-p position))
(error "Mouse target is not currently displayed: %S" position))
(execute-kbd-macro (vector (list 'mouse-1 position-data)))))
(defun wb-gui--click (context label &optional row-id)
"Click LABEL at its rendered position within CONTEXT's optional ROW-ID."
(let* ((runtime (wb-gui--runtime context))
(ref (wb-gui--control context label row-id))
(position (etaf-host-ref-position runtime ref)))
(unless position (error "Control %S has no live bounds" label))
(wb-gui--mouse-at position)))
(defun wb-gui--find-text (text)
"Return a buffer position inside visible TEXT or signal a missing target."
(save-excursion
(goto-char (point-min))
(unless (search-forward text nil t)
(error "Rendered text target is absent: %S" text))
(- (point) (length text))))
(defun wb-gui--action (id execute predicate)
"Create action ID with EXECUTE and a persistent product PREDICATE."
(etaf-gui-verifier-action-create
:id id :execute execute :screenshot t
:settled-p
(lambda (context)
(and (etaf-playground-gui-scenarios--mounted-settled-p context)
(funcall predicate context)))
:assertions
(lambda (context)
(list (etaf-gui-verifier-assert id (funcall predicate context))))))
(defun wb-gui--adapter (context)
"Return primitive evidence for CONTEXT without copying private task text."
(append
(etaf-playground-gui-scenarios--adapter context)
(let ((native (ebox-native-reflow-runtime-report)))
(list (cons 'native_layout_ready (and (plist-get native :layout-ready-p) t))
(cons 'native_module_path (or (plist-get native :loaded-module-path) ""))
(cons 'native_module_hash (or (plist-get native :loaded-module-hash) ""))))
(mapcar
(lambda (entry)
(cons (car entry)
(vconcat
(mapcar #'vconcat
(plist-get (etaf-gui-verifier-context-get context (cdr entry))
:controls)))))
'((theme_pixels_before . light-layout)
(theme_pixels_after . theme-layout-current)))
(when (wb-gui--runtime context)
(let ((data (wb-gui--data context)))
(list (cons 'total (etaf-value (etaf-data-total data)))
(cons 'page (etaf-value (etaf-data-page data)))
(cons 'page_size (etaf-value (etaf-data-page-size data)))
(cons 'visited_tasks
(etaf-gui-verifier-context-get context 'visited-tasks 0))
(cons 'visible_rows (length (wb-gui--ids context)))
(cons 'selected_task (or (wb-gui--selected-id context) 0)))))))
(defun wb-gui--nested-scroll (context)
"Scroll CONTEXT's help box and retain both inner and outer observations."
(goto-char (wb-gui--find-text "点击行:选择任务"))
(let* ((regions (cl-remove-if-not #'ebox-scroll-state
(ebox--scroll-region-ids-at-point)))
(inner (car regions)))
(unless inner (error "Help text has no active scroll container"))
(etaf-gui-verifier-context-put context 'inner-region inner)
(etaf-gui-verifier-context-put
context 'inner-offset (plist-get (ebox-scroll-state inner) :scroll-offset))
(etaf-gui-verifier-context-put
context 'outer-offsets
(mapcar (lambda (id) (cons id (plist-get (ebox-scroll-state id) :scroll-offset)))
(cdr regions)))
(etaf-gui-verifier-context-put context 'outer-window-start (window-start))
(ebox-scroll-down 1)))
(defun wb-gui--nested-scrolled-p (context)
"Return non-nil when CONTEXT's inner help scrolled without its parent."
(let ((inner (ebox-scroll-state
(etaf-gui-verifier-context-get context 'inner-region))))
(and (> (plist-get inner :scroll-offset)
(etaf-gui-verifier-context-get context 'inner-offset))
(= (window-start)
(etaf-gui-verifier-context-get context 'outer-window-start))
(cl-every
(lambda (entry)
(equal (cdr entry) (plist-get (ebox-scroll-state (car entry))
:scroll-offset)))
(etaf-gui-verifier-context-get context 'outer-offsets)))))
(defun wb-gui--reset-outer-scroll (_context)
"Reset the outer Ebox viewport and its containing Emacs window."
(goto-char (point-min))
(condition-case nil (ebox-scroll-up 10000) (beginning-of-buffer nil))
(goto-char (point-min))
(set-window-start (selected-window) (point-min)))
(defun wb-gui--outer-reset-p (_context)
"Return non-nil when both the outer Ebox viewport and window are at top."
(and (= (window-start) (point-min))
(save-excursion
(goto-char (point-min))
(cl-every
(lambda (id)
(zerop (or (plist-get (ebox-scroll-state id) :scroll-offset) 0)))
(ebox--scroll-region-ids-at-point)))))
;;;###autoload
(defun task-workbench-gui-scenario ()
"Return the complete Workbench temporal GUI acceptance scenario."
(let ((primary wb-gui--primary-name)
(secondary wb-gui--secondary-name))
(etaf-gui-verifier-scenario-create
:name "task-workbench"
:claim "Workbench layout and public input, CRUD, resources, and isolated lifecycle"
:initialize
(lambda (context)
(unless (display-graphic-p) (error "Workbench acceptance needs GUI Emacs"))
(let ((buffer (get-buffer primary)))
(unless (and (buffer-live-p buffer)
(eq buffer wb-gui--prepared-buffer)
(not (etaf-runtime-for-buffer buffer))
(not (get-buffer secondary)))
(error "Workbench target is not the fresh prepared unmounted buffer"))
(etaf-gui-verifier-context-select-buffer context buffer)
(setq wb-gui--prepared-buffer nil)))
:invariants
(lambda (context)
(etaf-playground-gui-scenarios--invariants context nil))
:adapter #'wb-gui--adapter
:actions
(append
(list
(wb-gui--action
"mount"
(lambda (context)
(etaf-gui-verifier-context-put
context 'target-fullscreen (frame-parameter nil 'fullscreen))
(wb-open primary)
(etaf-gui-verifier-context-select-buffer context (get-buffer primary))
(etaf-gui-verifier-context-put context 'expects-mounted t))
(lambda (context)
(and (equal (wb-gui--ids context) (number-sequence 1 10))
(wb-gui--text-p context "共 100 条 · 当前页 10 条")
(wb-gui--text-p context "服务正常"))))
(wb-gui--action
"keyboard-theme"
(lambda (context)
(wb-gui--capture-theme-baseline context)
(wb-gui--press context "深色"))
(lambda (context)
(and (wb-gui--theme-layout-preserved-p context)
(wb-gui--root-background-p context "#111827"))))
(wb-gui--action
"keyboard-focus-add"
(lambda (context) (wb-gui--press context "定位新增按钮"))
(lambda (context)
(equal (etaf-focused-host-ref (wb-gui--runtime context))
(wb-gui--control context "新增任务"))))
(wb-gui--action
"minibuffer-add"
(lambda (context) (wb-gui--enter context "新增任务" "GUI 验收任务"))
(lambda (context)
(= (etaf-value (etaf-data-total (wb-gui--data context))) 101))))
(cl-loop for page from 2 to 11
collect
(let ((target page))
(wb-gui--action
(format "page-next-%d" target) #'wb-gui--visit-next-page
(lambda (c)
(and (equal (wb-gui--ids c)
(number-sequence (1+ (* 10 (1- target)))
(min 101 (* 10 target))))
(equal (etaf-gui-verifier-context-get c 'visited-task-ids)
(number-sequence 1 (min 101 (* 10 target))))
(or (< target 11)
(and (wb-gui--text-p c "GUI 验收任务")
(plist-get
(etaf-runtime-host-props-for
(wb-gui--runtime c) (wb-gui--control c "Next page"))
:disabled))))))))
(cl-loop for page from 10 downto 1
collect
(let ((target page))
(wb-gui--action
(format "page-previous-%d" target)
(lambda (c) (wb-gui--press c "Previous page"))
(lambda (c)
(equal (wb-gui--ids c)
(number-sequence (1+ (* 10 (1- target))) (* 10 target)))))))
(list
(wb-gui--action "disabled-cell-mouse" (lambda (c) (wb-gui--click c "删除" 1))
(lambda (c) (and (not (wb-gui--selected-id c))
(equal (wb-gui--ids c) (number-sequence 1 10)))))
(wb-gui--action
"row-mouse-select"
(lambda (_context) (wb-gui--mouse-at (wb-gui--find-text "设计组件接口")))
(lambda (c) (and (equal (wb-gui--selected-id c) 1)
(wb-gui--text-p c "进行中"))))
(wb-gui--action
"cell-mouse-toggle" (lambda (c) (wb-gui--click c "完成" 1))
(lambda (c) (and (plist-get (etaf-data-selected-item (wb-gui--data c)) :done)
(equal (wb-gui--selected-id c) 1))))
(wb-gui--action "cell-mouse-delete" (lambda (c) (wb-gui--click c "删除" 1))
(lambda (c) (and (equal (wb-gui--ids c) (number-sequence 2 11))
(= (etaf-value (etaf-data-total
(wb-gui--data c))) 100)
(not (wb-gui--selected-id c)))))
(wb-gui--action "filter-open" (lambda (c) (wb-gui--press c "未完成"))
(lambda (c)
(and (equal (wb-gui--ids c) '(2 4 6 7 9 10 11 13 14 15))
(= (etaf-value (etaf-data-total (wb-gui--data c))) 74))))
(wb-gui--action "filter-done" (lambda (c) (wb-gui--press c "已完成"))
(lambda (c)
(and (equal (wb-gui--ids c) '(3 5 8 12 16 20 24 28 32 36))
(= (etaf-value (etaf-data-total (wb-gui--data c))) 26))))
(wb-gui--action "minibuffer-search"
(lambda (c) (wb-gui--enter c "搜索" "键盘"))
(lambda (c) (equal (wb-gui--ids c) '(4))))
(wb-gui--action "filter-all" (lambda (c) (wb-gui--press c "全部"))
(lambda (c) (equal (wb-gui--ids c) (number-sequence 2 11))))
(wb-gui--action "minibuffer-page-size"
(lambda (c) (wb-gui--enter c "每页条数 10 ✎" "2"))
(lambda (c) (and (equal (wb-gui--ids c) '(2 3))
(wb-gui--text-p c "Page 1 / 50"))))
(wb-gui--action
"hide-help"
(lambda (c)
(etaf-gui-verifier-context-put
c 'help-panels-before (wb-gui--panel-heights c))
(wb-gui--press c "显示说明"))
(lambda (c) (wb-gui--help-layout-preserved-p c nil)))
(wb-gui--action "show-help" (lambda (c) (wb-gui--press c "显示说明"))
(lambda (c) (wb-gui--help-layout-preserved-p c t)))
(wb-gui--action "resource-failure" (lambda (c) (wb-gui--press c "模拟失败"))
(lambda (c) (wb-gui--text-p c "模拟加载失败")))
(wb-gui--action "resource-retry" (lambda (c) (wb-gui--press c "刷新 / 重试"))
(lambda (c) (and (wb-gui--text-p c "服务正常")
(not (wb-gui--text-p c "模拟加载失败")))))
(wb-gui--action "nested-scroll" #'wb-gui--nested-scroll
#'wb-gui--nested-scrolled-p)
(etaf-playground-gui-scenarios--windowed-action)
(etaf-playground-gui-scenarios--resize-action "resize-compact" 700 500)
(etaf-playground-gui-scenarios--scroll-action "outer-scroll" #'ebox-scroll-page-down)
(wb-gui--action "reset-scroll" #'wb-gui--reset-outer-scroll
#'wb-gui--outer-reset-p)
(etaf-playground-gui-scenarios--resize-action "resize-wide" 1300 750)
(wb-gui--action
"second-independent-app"
(lambda (context)
(etaf-gui-verifier-context-put context 'primary-data (wb-gui--data context))
(wb-open secondary)
(etaf-gui-verifier-context-select-buffer context (get-buffer secondary)))
(lambda (c)
(and (equal (wb-gui--ids c) (number-sequence 1 10))
(= (etaf-value (etaf-data-total (wb-gui--data c))) 100)
(not (eq (wb-gui--data c)
(etaf-gui-verifier-context-get c 'primary-data))))))
(wb-gui--action
"close-second-app"
(lambda (context)
(wb-close secondary)
(etaf-gui-verifier-context-select-buffer context (get-buffer primary)))
(lambda (c) (and (not (get-buffer secondary))
(equal (wb-gui--ids c) '(2 3)))))
(wb-gui--action
"unmount-remount"
(lambda (context)
(let ((old (wb-gui--runtime context)))
(etaf-gui-verifier-context-put context 'retired-runtime old)
(etaf-unmount old)
(wb-open primary)
(etaf-gui-verifier-context-select-buffer context (get-buffer primary))))
(lambda (c)
(and (not (etaf-runtime-mounted-p
(etaf-gui-verifier-context-get c 'retired-runtime)))
(equal (wb-gui--ids c) (number-sequence 1 10))
(= (etaf-value (etaf-data-total (wb-gui--data c))) 100))))))
:completion
(lambda (context)
(and (= (etaf-gui-verifier-context-action-count context)
(length (etaf-gui-verifier-scenario-actions
(etaf-gui-verifier-context-scenario context))))
(not (get-buffer secondary))
(= (etaf-gui-verifier-context-get context 'visited-tasks 0) 101)
(equal (wb-gui--ids context) (number-sequence 1 10)))))))
;;;###autoload
(defun task-workbench-gui-run (run-directory)
"Run Workbench GUI acceptance into prepared RUN-DIRECTORY."
(unless (buffer-live-p wb-gui--prepared-buffer)
(task-workbench-gui-prepare))
(etaf-gui-verifier-run (task-workbench-gui-scenario) run-directory))
;;;###autoload
(defun task-workbench-gui-run-from-environment ()
"Run Workbench acceptance in the generic runner's evidence directory."
(task-workbench-gui-run
(or (getenv "ETAF_GUI_RUN_DIR")
(error "ETAF_GUI_RUN_DIR is not configured"))))
(provide 'task-workbench-gui-scenarios)
;;; task-workbench-gui-scenarios.el ends here