@cspeach/cli 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/signing-public-key.js +1 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/smoke/app-driver-page.ts
|
|
2
|
+
//
|
|
3
|
+
// L1-5a — the IN-PAGE half of the RecordReplay AppDriver.
|
|
4
|
+
//
|
|
5
|
+
// One module constant: the source of a function that builds the `window.__csp`
|
|
6
|
+
// helper inside the app's page. The Node side (app-driver.ts) calls its methods
|
|
7
|
+
// with ONE JSON argument each. Nothing model-supplied is ever turned into code:
|
|
8
|
+
// the only source that runs in the page is this constant; selectors, values and
|
|
9
|
+
// URLs arrive as JSON data (same rule as L1-3's TABLE_SNAPSHOT_SRC).
|
|
10
|
+
//
|
|
11
|
+
// How the browser is driven (design §3.3): every UI5 interaction goes through
|
|
12
|
+
// the public `sap/ui/test/RecordReplay` API — `waitForUI5` before every read and
|
|
13
|
+
// after every action, `interactWithControl` with CONTROL selectors
|
|
14
|
+
// (controlType / id suffix / binding path) — never CSS class names. It is loaded
|
|
15
|
+
// with `sap.ui.require` from the app's own UI5 (`/resources/sap/ui/test/…`, the
|
|
16
|
+
// same origin the app's bootstrap uses), so no extra library ships with CSPeach.
|
|
17
|
+
//
|
|
18
|
+
// Written as ES5 in a string on purpose: it runs in whatever browser engine and
|
|
19
|
+
// UI5 version the app uses, and a transpiled closure could carry helpers the
|
|
20
|
+
// page does not have.
|
|
21
|
+
import { TABLE_SNAPSHOT_SRC } from './driver.js';
|
|
22
|
+
export const APP_DRIVER_PAGE_SRC = String.raw `function () {
|
|
23
|
+
var RR = null;
|
|
24
|
+
var WAIT = { timeout: 60000, interval: 400 };
|
|
25
|
+
|
|
26
|
+
function Element() {
|
|
27
|
+
return (sap.ui.require && sap.ui.require('sap/ui/core/Element')) || (sap.ui.core && sap.ui.core.Element);
|
|
28
|
+
}
|
|
29
|
+
function allControls() {
|
|
30
|
+
var E = Element();
|
|
31
|
+
return E && E.registry ? E.registry.all() : {};
|
|
32
|
+
}
|
|
33
|
+
function escapeRe(s) {
|
|
34
|
+
return String(s).replace(/[.*+?^$()|[\]{}\\]/g, '\\$&');
|
|
35
|
+
}
|
|
36
|
+
function toSelector(sel) {
|
|
37
|
+
var out = {};
|
|
38
|
+
if (sel.controlType) out.controlType = sel.controlType;
|
|
39
|
+
if (sel.id) out.id = sel.id;
|
|
40
|
+
if (sel.idSuffix) out.id = new RegExp(escapeRe(sel.idSuffix) + '$');
|
|
41
|
+
if (sel.bindingPath) out.bindingPath = { path: sel.bindingPath };
|
|
42
|
+
if (sel.properties) out.properties = sel.properties;
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function loadRR() {
|
|
46
|
+
if (RR) return Promise.resolve(RR);
|
|
47
|
+
return new Promise(function (resolve, reject) {
|
|
48
|
+
sap.ui.require(['sap/ui/test/RecordReplay'], function (m) { RR = m; resolve(m); }, reject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function wait() {
|
|
52
|
+
return loadRR().then(function (rr) { return rr.waitForUI5(WAIT); });
|
|
53
|
+
}
|
|
54
|
+
function visible(c) {
|
|
55
|
+
var d = c && c.getDomRef ? c.getDomRef() : null;
|
|
56
|
+
return !!(d && (d.offsetWidth || d.offsetHeight || d.getClientRects().length));
|
|
57
|
+
}
|
|
58
|
+
// UI5 1.84 collapses the DynamicPage header once a list report has loaded on
|
|
59
|
+
// open (1.120 keeps it expanded), hiding the filter bar — Go included. A user
|
|
60
|
+
// clicks the title to open it; so do we, before every press. No-op when the
|
|
61
|
+
// header is already expanded.
|
|
62
|
+
function expandHeaders() {
|
|
63
|
+
var reg = sap.ui.core.Element && sap.ui.core.Element.registry;
|
|
64
|
+
var n = 0;
|
|
65
|
+
if (reg && reg.filter) {
|
|
66
|
+
reg.filter(function (e) { return e.isA && e.isA('sap.f.DynamicPage'); }).forEach(function (dp) {
|
|
67
|
+
if (dp.getHeaderExpanded && dp.getHeaderExpanded() === false) { dp.setHeaderExpanded(true); n++; }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
// Refresh every visible list table's row binding (sap.ui.mdc.Table). Returns
|
|
73
|
+
// how many were refreshed.
|
|
74
|
+
function refreshLists() {
|
|
75
|
+
return wait().then(function () {
|
|
76
|
+
var n = 0;
|
|
77
|
+
var all = allControls();
|
|
78
|
+
for (var id in all) {
|
|
79
|
+
var t = all[id];
|
|
80
|
+
if (t && t.isA && t.isA('sap.ui.mdc.Table') && visible(t)) {
|
|
81
|
+
var b = t.getRowBinding ? t.getRowBinding() : null;
|
|
82
|
+
if (b && b.refresh) { b.refresh(); n++; }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return wait().then(function () { return n; });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function press(sel) {
|
|
89
|
+
return wait().then(function () {
|
|
90
|
+
if (expandHeaders() > 0) return wait();
|
|
91
|
+
}).then(function () {
|
|
92
|
+
return RR.interactWithControl({ selector: toSelector(sel), interactionType: RR.InteractionType.Press });
|
|
93
|
+
}).then(wait).then(function () { return true; });
|
|
94
|
+
}
|
|
95
|
+
function textOf(c) {
|
|
96
|
+
try {
|
|
97
|
+
if (c.getText) return String(c.getText());
|
|
98
|
+
if (c.getTitle) return String(c.getTitle());
|
|
99
|
+
} catch (e) { /* fall through */ }
|
|
100
|
+
var d = c.getDomRef && c.getDomRef();
|
|
101
|
+
return d ? String(d.innerText || d.textContent || '').replace(/\s+/g, ' ').trim() : '';
|
|
102
|
+
}
|
|
103
|
+
// No open error MessageBox and no message popover in error state (design §3.3).
|
|
104
|
+
function openErrors() {
|
|
105
|
+
return wait().then(function () {
|
|
106
|
+
var out = [];
|
|
107
|
+
var all = allControls();
|
|
108
|
+
for (var id in all) {
|
|
109
|
+
var c = all[id];
|
|
110
|
+
if (!c || !c.isA) continue;
|
|
111
|
+
if (c.isA('sap.m.Dialog') && c.isOpen && c.isOpen()) {
|
|
112
|
+
var st = c.getState ? c.getState() : '';
|
|
113
|
+
var isBox = c.hasStyleClass && (c.hasStyleClass('sapMMessageBoxError') || c.hasStyleClass('sapMMessageBoxWarning'));
|
|
114
|
+
if (st === 'Error' || isBox) {
|
|
115
|
+
var parts = [];
|
|
116
|
+
var content = c.getContent ? c.getContent() : [];
|
|
117
|
+
for (var i = 0; i < content.length; i++) parts.push(textOf(content[i]));
|
|
118
|
+
out.push('dialog: ' + (c.getTitle ? c.getTitle() : '') + ' — ' + parts.join(' ').replace(/\s+/g, ' ').trim());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if ((c.isA('sap.m.MessagePopover') || c.isA('sap.m.MessageView')) && visible(c)) {
|
|
122
|
+
var items = c.getItems ? c.getItems() : [];
|
|
123
|
+
for (var j = 0; j < items.length; j++) {
|
|
124
|
+
var it = items[j];
|
|
125
|
+
if (it.getType && it.getType() === 'Error') out.push('message: ' + (it.getTitle ? it.getTitle() : textOf(it)));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// UI5 1.84 surfaces a failed save only in the message model (the footer's
|
|
130
|
+
// message indicator), with no dialog — 1.120 also opens a dialog. An error
|
|
131
|
+
// message in the model is an error the user is shown, on every release.
|
|
132
|
+
try {
|
|
133
|
+
var msgs = sap.ui.getCore().getMessageManager().getMessageModel().getData() || [];
|
|
134
|
+
for (var m = 0; m < msgs.length; m++) {
|
|
135
|
+
if (msgs[m].getType && msgs[m].getType() === 'Error') out.push('message: ' + msgs[m].getMessage());
|
|
136
|
+
}
|
|
137
|
+
} catch (e) { /* no message manager — dialogs and popovers above still count */ }
|
|
138
|
+
return out;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function isBusy() {
|
|
142
|
+
var all = allControls();
|
|
143
|
+
for (var id in all) {
|
|
144
|
+
var c = all[id];
|
|
145
|
+
if (c && c.getBusy && c.getBusy() && visible(c)) return true;
|
|
146
|
+
}
|
|
147
|
+
var bi = document.getElementById('sapUiBusyIndicator');
|
|
148
|
+
return !!(bi && bi.offsetWidth);
|
|
149
|
+
}
|
|
150
|
+
// The FE stable id of an object-page field: …::DataField::<Property>::Field(-edit|-display)?
|
|
151
|
+
function findField(property) {
|
|
152
|
+
var re = new RegExp('::DataField::' + escapeRe(property) + '::Field(-edit)?$');
|
|
153
|
+
var all = allControls();
|
|
154
|
+
var hit = null;
|
|
155
|
+
for (var id in all) {
|
|
156
|
+
var c = all[id];
|
|
157
|
+
if (c && c.isA && c.isA('sap.ui.mdc.Field') && re.test(id) && visible(c)) {
|
|
158
|
+
if (!hit || /-edit$/.test(id)) hit = c;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return hit;
|
|
162
|
+
}
|
|
163
|
+
function valueHelpOf(field) {
|
|
164
|
+
// UI5 1.84 names the association fieldHelp (sap.ui.mdc.field.FieldValueHelp);
|
|
165
|
+
// newer releases name it valueHelp (sap.ui.mdc.ValueHelp).
|
|
166
|
+
var vhId = (field.getValueHelp && field.getValueHelp()) || (field.getFieldHelp && field.getFieldHelp());
|
|
167
|
+
if (!vhId) return null;
|
|
168
|
+
var E = Element();
|
|
169
|
+
return E.getElementById ? E.getElementById(vhId) : sap.ui.getCore().byId(vhId);
|
|
170
|
+
}
|
|
171
|
+
// The list the open value help shows: its content's list binding (MTable /
|
|
172
|
+
// FixedList — covers the fixed-values dropdown AND the dialog alike).
|
|
173
|
+
function valueHelpList(vh) {
|
|
174
|
+
var containers = [vh.getTypeahead && vh.getTypeahead(), vh.getDialog && vh.getDialog()];
|
|
175
|
+
for (var k = 0; k < containers.length; k++) {
|
|
176
|
+
var cont = containers[k];
|
|
177
|
+
if (!cont || !cont.isOpen || !cont.isOpen()) continue;
|
|
178
|
+
var contents = cont.getContent ? cont.getContent() : [];
|
|
179
|
+
for (var i = 0; i < contents.length; i++) {
|
|
180
|
+
var ct = contents[i];
|
|
181
|
+
var table = ct.getTable ? ct.getTable() : null;
|
|
182
|
+
var binding = table && table.getBinding ? (table.getBinding('items') || table.getBinding('rows')) : null;
|
|
183
|
+
if (!binding && ct.getListBinding) binding = ct.getListBinding();
|
|
184
|
+
// Rows with a binding context, wherever the content keeps them (the
|
|
185
|
+
// dropdown's sap.m.Table, or the dialog's table inside its layout).
|
|
186
|
+
var items = cont.findAggregatedObjects(true, function (x) {
|
|
187
|
+
return x.isA && (x.isA('sap.m.ListItemBase') || x.isA('sap.ui.table.Row')) && !!(x.getBindingContext && x.getBindingContext());
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
container: cont.isA('sap.ui.mdc.valuehelp.Dialog') ? 'dialog' : 'popover',
|
|
191
|
+
length: binding && binding.getLength ? binding.getLength() : items.length,
|
|
192
|
+
items: items,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// UI5 1.84: sap.ui.mdc.field.FieldValueHelp — one content wrapper holding the
|
|
197
|
+
// table, shown in a popover (typeahead / fixed values) or a dialog.
|
|
198
|
+
if (vh.isA && vh.isA('sap.ui.mdc.field.FieldValueHelp') && vh.isOpen && vh.isOpen()) {
|
|
199
|
+
// 1.84 has one content wrapper; 1.96 adds suggestContent (popover) and
|
|
200
|
+
// dialogContent (dialog). Take the wrapper of what is open, then content.
|
|
201
|
+
var okBtn = sap.ui.getCore().byId(vh.getId() + '-ok');
|
|
202
|
+
var inDialog = !!(okBtn && visible(okBtn));
|
|
203
|
+
var pick = inDialog
|
|
204
|
+
? [vh.getDialogContent && vh.getDialogContent(), vh.getContent && vh.getContent()]
|
|
205
|
+
: [vh.getSuggestContent && vh.getSuggestContent(), vh.getContent && vh.getContent()];
|
|
206
|
+
var wrapper = null;
|
|
207
|
+
for (var w = 0; w < pick.length; w++) { if (pick[w] && pick[w].getTable && pick[w].getTable()) { wrapper = pick[w]; break; } }
|
|
208
|
+
var tbl = wrapper ? wrapper.getTable() : null;
|
|
209
|
+
if (!tbl) return null;
|
|
210
|
+
var lb = wrapper.getListBinding ? wrapper.getListBinding() : (tbl.getBinding && (tbl.getBinding('items') || tbl.getBinding('rows')));
|
|
211
|
+
var rows = tbl.findAggregatedObjects(true, function (x) {
|
|
212
|
+
return x.isA && (x.isA('sap.m.ListItemBase') || x.isA('sap.ui.table.Row')) && !!(x.getBindingContext && x.getBindingContext());
|
|
213
|
+
});
|
|
214
|
+
return { container: inDialog ? 'dialog' : 'popover', length: lb && lb.getLength ? lb.getLength() : rows.length, items: rows };
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function openValueHelp(arg) {
|
|
219
|
+
var field;
|
|
220
|
+
return wait().then(function () {
|
|
221
|
+
field = findField(arg.property);
|
|
222
|
+
if (!field) throw new Error('no visible field for ' + arg.property + ' on the page');
|
|
223
|
+
if (!field.getEditMode || field.getEditMode() === 'Display' || field.getEditMode() === 'ReadOnly') {
|
|
224
|
+
throw new Error('field ' + arg.property + ' is not editable (' + (field.getEditMode && field.getEditMode()) + ')');
|
|
225
|
+
}
|
|
226
|
+
return RR.interactWithControl({
|
|
227
|
+
selector: { controlType: 'sap.ui.core.Icon', id: new RegExp(escapeRe(field.getId()) + '-inner-vhi$') },
|
|
228
|
+
interactionType: RR.InteractionType.Press,
|
|
229
|
+
});
|
|
230
|
+
}).then(wait).then(function () {
|
|
231
|
+
var vh = valueHelpOf(field);
|
|
232
|
+
var list = vh ? valueHelpList(vh) : null;
|
|
233
|
+
return {
|
|
234
|
+
valueHelpId: vh ? vh.getId() : null,
|
|
235
|
+
open: !!(vh && vh.isOpen && vh.isOpen()),
|
|
236
|
+
container: list ? list.container : null,
|
|
237
|
+
length: list ? list.length : 0,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function pickValueHelpValue(arg) {
|
|
242
|
+
var field = findField(arg.property);
|
|
243
|
+
var vh = field ? valueHelpOf(field) : null;
|
|
244
|
+
var list = vh ? valueHelpList(vh) : null;
|
|
245
|
+
if (!list) return Promise.reject(new Error('the value help of ' + arg.property + ' is not open'));
|
|
246
|
+
var target = null;
|
|
247
|
+
var seen = [];
|
|
248
|
+
for (var i = 0; i < list.items.length; i++) {
|
|
249
|
+
var ctx = list.items[i].getBindingContext && list.items[i].getBindingContext();
|
|
250
|
+
var v = ctx ? ctx.getProperty(arg.keyProperty) : undefined;
|
|
251
|
+
seen.push(String(v));
|
|
252
|
+
if (v !== undefined && String(v) === String(arg.value)) { target = list.items[i]; break; }
|
|
253
|
+
}
|
|
254
|
+
if (!target) return Promise.reject(new Error('value ' + arg.value + ' not in the value help of ' + arg.property + ' (has: ' + seen.join(', ') + ')'));
|
|
255
|
+
// A grid-table row (the dialog) is selected by a press on one of its cells.
|
|
256
|
+
var pressTarget = target.isA('sap.ui.table.Row') && target.getCells().length > 0 ? target.getCells()[0] : target;
|
|
257
|
+
return press({ id: pressTarget.getId() }).then(function () {
|
|
258
|
+
// UI5 1.84 FieldValueHelp dialog: a selection is only recorded; the value
|
|
259
|
+
// is taken when the user presses OK (button id <valueHelpId>-ok). The
|
|
260
|
+
// popover (typeahead / fixed values) applies it at once.
|
|
261
|
+
// Detect the dialog by its OK button being on screen: on 1.84 the table is
|
|
262
|
+
// not parented under the sap.m.Dialog, so walking up does not find it.
|
|
263
|
+
var ok = vh.isA && vh.isA('sap.ui.mdc.field.FieldValueHelp') ? sap.ui.getCore().byId(vh.getId() + '-ok') : null;
|
|
264
|
+
if (ok && visible(ok)) return press({ id: ok.getId() });
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// Values the open value help lists for its key property (to choose one).
|
|
269
|
+
function valueHelpValues(arg) {
|
|
270
|
+
var field = findField(arg.property);
|
|
271
|
+
var vh = field ? valueHelpOf(field) : null;
|
|
272
|
+
var list = vh ? valueHelpList(vh) : null;
|
|
273
|
+
if (!list) return [];
|
|
274
|
+
var out = [];
|
|
275
|
+
for (var i = 0; i < list.items.length; i++) {
|
|
276
|
+
var ctx = list.items[i].getBindingContext && list.items[i].getBindingContext();
|
|
277
|
+
if (!ctx) continue;
|
|
278
|
+
out.push({ key: String(ctx.getProperty(arg.keyProperty)), text: arg.textProperty ? String(ctx.getProperty(arg.textProperty) || '') : '' });
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
function fieldText(arg) {
|
|
283
|
+
return wait().then(function () {
|
|
284
|
+
var f = findField(arg.property);
|
|
285
|
+
if (!f) return null;
|
|
286
|
+
var d = f.getDomRef();
|
|
287
|
+
var input = d && d.querySelector('input');
|
|
288
|
+
return input ? String(input.value) : String(d.innerText || d.textContent || '').replace(/\s+/g, ' ').trim();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
// FE's "Discard Draft" opens a popover whose only button confirms (it has no
|
|
292
|
+
// stable id; its text is translated). Press the ONE button of the ONE open
|
|
293
|
+
// popover — anything else is refused rather than guessed.
|
|
294
|
+
function confirmPopover() {
|
|
295
|
+
return wait().then(function () {
|
|
296
|
+
var all = allControls();
|
|
297
|
+
var open = [];
|
|
298
|
+
for (var id in all) {
|
|
299
|
+
var c = all[id];
|
|
300
|
+
if (c && c.isA && c.isA('sap.m.Popover') && c.isOpen && c.isOpen()) open.push(c);
|
|
301
|
+
}
|
|
302
|
+
if (open.length !== 1) throw new Error('expected one open popover to confirm, found ' + open.length);
|
|
303
|
+
var buttons = open[0].findAggregatedObjects(true, function (x) { return x.isA && x.isA('sap.m.Button') && visible(x); });
|
|
304
|
+
if (buttons.length !== 1) throw new Error('expected one button in the open popover, found ' + buttons.length);
|
|
305
|
+
return press({ id: buttons[0].getId() });
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function back() {
|
|
309
|
+
window.history.back();
|
|
310
|
+
return wait().then(function () { return true; });
|
|
311
|
+
}
|
|
312
|
+
// One OData request from INSIDE the page (same origin, same session as the
|
|
313
|
+
// app). Mutating verbs carry the CSRF token fetched from the service root.
|
|
314
|
+
var tokens = {};
|
|
315
|
+
function csrf(serviceRoot) {
|
|
316
|
+
if (tokens[serviceRoot]) return Promise.resolve(tokens[serviceRoot]);
|
|
317
|
+
return fetch(serviceRoot, { method: 'GET', credentials: 'same-origin', headers: { 'X-CSRF-Token': 'Fetch', Accept: 'application/json', 'X-CSP-Smoke': '1' } })
|
|
318
|
+
.then(function (r) { tokens[serviceRoot] = r.headers.get('x-csrf-token') || ''; return tokens[serviceRoot]; });
|
|
319
|
+
}
|
|
320
|
+
// Same origin only: a path ('/sap/…'), never a URL, never a protocol-relative '//host'.
|
|
321
|
+
function isPath(u) {
|
|
322
|
+
return typeof u === 'string' && u.charAt(0) === '/' && u.charAt(1) !== '/' && u.indexOf('\\') < 0;
|
|
323
|
+
}
|
|
324
|
+
function request(arg) {
|
|
325
|
+
if (!arg || !isPath(arg.url) || !isPath(arg.serviceRoot)) {
|
|
326
|
+
return Promise.reject(new Error('in-page request refused: url and serviceRoot must be same-origin paths'));
|
|
327
|
+
}
|
|
328
|
+
var method = String(arg.method).toUpperCase();
|
|
329
|
+
if (['GET', 'POST', 'PATCH', 'DELETE'].indexOf(method) < 0) {
|
|
330
|
+
return Promise.reject(new Error('in-page request refused: method ' + method));
|
|
331
|
+
}
|
|
332
|
+
// X-CSP-Smoke marks the smoke's OWN calls: the driver leaves them out of the
|
|
333
|
+
// app's request log (each step judges its own answers).
|
|
334
|
+
var headers = { Accept: 'application/json', 'X-CSP-Smoke': '1' };
|
|
335
|
+
var p = method === 'GET' ? Promise.resolve('') : csrf(arg.serviceRoot);
|
|
336
|
+
return p.then(function (token) {
|
|
337
|
+
if (token) headers['X-CSRF-Token'] = token;
|
|
338
|
+
if (arg.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
339
|
+
if (arg.ifMatch) headers['If-Match'] = arg.ifMatch;
|
|
340
|
+
return fetch(arg.url, {
|
|
341
|
+
method: method,
|
|
342
|
+
credentials: 'same-origin',
|
|
343
|
+
headers: headers,
|
|
344
|
+
body: arg.body === undefined ? undefined : JSON.stringify(arg.body),
|
|
345
|
+
});
|
|
346
|
+
}).then(function (r) {
|
|
347
|
+
return r.text().then(function (text) {
|
|
348
|
+
var json = null;
|
|
349
|
+
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
|
350
|
+
return { status: r.status, body: json, text: json ? '' : text.slice(0, 500), etag: r.headers.get('etag') || '' };
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
wait: function () { return wait().then(function () { return true; }); },
|
|
356
|
+
press: press,
|
|
357
|
+
openErrors: openErrors,
|
|
358
|
+
isBusy: function () { return isBusy(); },
|
|
359
|
+
refreshLists: refreshLists,
|
|
360
|
+
openValueHelp: openValueHelp,
|
|
361
|
+
pickValueHelpValue: pickValueHelpValue,
|
|
362
|
+
valueHelpValues: valueHelpValues,
|
|
363
|
+
fieldText: fieldText,
|
|
364
|
+
confirmPopover: confirmPopover,
|
|
365
|
+
// L1-3's table reader (driver.ts), after waitForUI5.
|
|
366
|
+
tableSnapshot: function (entitySet) {
|
|
367
|
+
return wait().then(function () { return (${TABLE_SNAPSHOT_SRC})(entitySet); });
|
|
368
|
+
},
|
|
369
|
+
back: back,
|
|
370
|
+
request: request,
|
|
371
|
+
};
|
|
372
|
+
}`;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/smoke/app-driver.ts
|
|
2
|
+
//
|
|
3
|
+
// L1-5a — the `AppDriver` interface and its lane-1 implementation over
|
|
4
|
+
// `sap/ui/test/RecordReplay` (design §3.3, review F-8 / F-28).
|
|
5
|
+
//
|
|
6
|
+
// The checks that drive an app (fiori_draft_smoke today) are written ONCE,
|
|
7
|
+
// against this interface. Lane 2 implements the same interface for its React
|
|
8
|
+
// apps instead of refactoring the checks.
|
|
9
|
+
//
|
|
10
|
+
// The RecordReplay implementation runs every UI5 interaction inside the page
|
|
11
|
+
// through APP_DRIVER_PAGE_SRC: `waitForUI5` before every read and after every
|
|
12
|
+
// action, control selectors (control type + Fiori Elements stable-id suffix, a
|
|
13
|
+
// row's binding path) — never CSS class names. The only code that reaches the
|
|
14
|
+
// page is the module constant DRIVER_CALL_SRC; the method name (from this file)
|
|
15
|
+
// and its argument travel as JSON.
|
|
16
|
+
import { APP_DRIVER_PAGE_SRC } from './app-driver-page.js';
|
|
17
|
+
/** Fiori Elements V4 stable-id suffixes of the buttons the draft round trip presses. */
|
|
18
|
+
export const FE_ACTION_SUFFIX = {
|
|
19
|
+
/** Object page header: Edit (creates the draft). */
|
|
20
|
+
edit: '--fe::StandardAction::Edit',
|
|
21
|
+
/** Object page footer: Save (= Prepare + Activate). */
|
|
22
|
+
save: '--fe::FooterBar::StandardAction::Save',
|
|
23
|
+
/** Object page footer: "Discard Draft" — opens a confirmation popover. */
|
|
24
|
+
discard: '--fe::FooterBar::StandardAction::Cancel',
|
|
25
|
+
/** List report filter bar: Go. */
|
|
26
|
+
go: '-btnSearch',
|
|
27
|
+
};
|
|
28
|
+
/** The single in-page entry: build `window.__csp` once, then call one named method. */
|
|
29
|
+
export const DRIVER_CALL_SRC = 'async function (x) {' +
|
|
30
|
+
` if (!window.__csp) window.__csp = (${APP_DRIVER_PAGE_SRC})();` +
|
|
31
|
+
' var f = window.__csp[x.m];' +
|
|
32
|
+
" if (typeof f !== 'function') throw new Error('unknown driver call ' + x.m);" +
|
|
33
|
+
' return f(x.a);' +
|
|
34
|
+
' }';
|
|
35
|
+
/** Same origin only — the page-side guard says the same; this one stops it earlier. */
|
|
36
|
+
function isSameOriginPath(u) {
|
|
37
|
+
return typeof u === 'string' && u.startsWith('/') && !u.startsWith('//') && !u.includes('\\');
|
|
38
|
+
}
|
|
39
|
+
export function createRecordReplayDriver(page) {
|
|
40
|
+
const call = (m, a = null) => page.evalInPage(DRIVER_CALL_SRC, { m, a });
|
|
41
|
+
const pressSuffix = (idSuffix, controlType = 'sap.m.Button') => call('press', { controlType, idSuffix }).then(() => undefined);
|
|
42
|
+
return {
|
|
43
|
+
waitForUI5: () => call('wait').then(() => undefined),
|
|
44
|
+
isBusy: () => call('isBusy'),
|
|
45
|
+
openRow: (rowPath) => call('press', { controlType: 'sap.m.ColumnListItem', bindingPath: rowPath }).then(() => undefined),
|
|
46
|
+
pressAction: async (action) => {
|
|
47
|
+
if (typeof action === 'object')
|
|
48
|
+
return pressSuffix(action.idSuffix, action.controlType);
|
|
49
|
+
if (action === 'go') {
|
|
50
|
+
// A filter bar in live mode has no Go button (SAP's CAP sample on UI5
|
|
51
|
+
// 1.152); the row-binding refresh below reloads the list either way.
|
|
52
|
+
await pressSuffix(FE_ACTION_SUFFIX.go).catch((err) => {
|
|
53
|
+
if (!/No controls found/.test(err instanceof Error ? err.message : String(err)))
|
|
54
|
+
throw err;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await pressSuffix(FE_ACTION_SUFFIX[action]);
|
|
59
|
+
}
|
|
60
|
+
if (action === 'discard')
|
|
61
|
+
await call('confirmPopover');
|
|
62
|
+
// FE on UI5 1.108 does not search again when no filter changed, so Go
|
|
63
|
+
// alone can leave rows created by API off the list. Refresh the list's
|
|
64
|
+
// row binding too (1.120 already re-fetched on Go — one extra read).
|
|
65
|
+
if (action === 'go')
|
|
66
|
+
await call('refreshLists');
|
|
67
|
+
},
|
|
68
|
+
setFieldViaValueHelp: async (field, choose) => {
|
|
69
|
+
const opened = await call('openValueHelp', { property: field.property });
|
|
70
|
+
const values = await call('valueHelpValues', {
|
|
71
|
+
property: field.property,
|
|
72
|
+
keyProperty: field.keyProperty,
|
|
73
|
+
...(field.textProperty ? { textProperty: field.textProperty } : {}),
|
|
74
|
+
});
|
|
75
|
+
const obs = {
|
|
76
|
+
open: opened.open,
|
|
77
|
+
container: opened.container,
|
|
78
|
+
length: opened.length,
|
|
79
|
+
values,
|
|
80
|
+
};
|
|
81
|
+
if (!opened.open)
|
|
82
|
+
return obs;
|
|
83
|
+
const key = choose(values);
|
|
84
|
+
if (key === undefined)
|
|
85
|
+
return obs;
|
|
86
|
+
await call('pickValueHelpValue', { property: field.property, keyProperty: field.keyProperty, value: key });
|
|
87
|
+
obs.picked = values.find((v) => v.key === key) ?? { key, text: '' };
|
|
88
|
+
return obs;
|
|
89
|
+
},
|
|
90
|
+
cellTexts: (entitySet) => call('tableSnapshot', entitySet).then((r) => r ?? undefined),
|
|
91
|
+
openErrors: () => call('openErrors'),
|
|
92
|
+
back: () => call('back').then(() => undefined),
|
|
93
|
+
request: async (req) => {
|
|
94
|
+
if (!isSameOriginPath(req.url) || !isSameOriginPath(req.serviceRoot)) {
|
|
95
|
+
throw new Error(`in-page request refused: "${req.url}" is not a same-origin path`);
|
|
96
|
+
}
|
|
97
|
+
return call('request', req);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|