jsgui3-server 0.0.151 → 0.0.152
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 +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/api-reference.md +120 -2
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/configuration-reference.md +54 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +11 -8
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +728 -18
- package/server.js +421 -103
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +1 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const jsgui = require('jsgui3-html');
|
|
2
|
+
const { Control, controls } = jsgui;
|
|
3
|
+
const { tpl } = require('../../../../jsgui3-html/html-core/html-core');
|
|
4
|
+
const Data_Model_View_Model_Control = require('../../../../jsgui3-html/html-core/Data_Model_View_Model_Control');
|
|
5
|
+
const { ensure_control_models } = require('../../../../jsgui3-html/html-core/control_model_factory');
|
|
6
|
+
const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
|
|
7
|
+
|
|
8
|
+
// Alias for tpl HTML parser which lowercases tags
|
|
9
|
+
controls.text_input = controls.Text_Input;
|
|
10
|
+
|
|
11
|
+
class UserProfileEditor extends Data_Model_View_Model_Control {
|
|
12
|
+
constructor(spec = {}) {
|
|
13
|
+
spec.__type_name = spec.__type_name || 'user_profile_editor';
|
|
14
|
+
super(spec);
|
|
15
|
+
|
|
16
|
+
// Ensure data.model and view.data.model exist
|
|
17
|
+
ensure_control_models(this, spec);
|
|
18
|
+
|
|
19
|
+
// Raw models
|
|
20
|
+
this.data.model.set('first_name', spec.first_name || 'Jane');
|
|
21
|
+
this.data.model.set('last_name', spec.last_name || 'Doe');
|
|
22
|
+
this.data.model.set('is_active', spec.is_active !== undefined ? spec.is_active : true);
|
|
23
|
+
|
|
24
|
+
// Generate full_name transparently via Computed Property
|
|
25
|
+
this.computed(this.data.model, ['first_name', 'last_name'],
|
|
26
|
+
(first, last) => `${first || ''} ${last || ''}`.trim(),
|
|
27
|
+
{ propertyName: 'full_name' }
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Generate a display status
|
|
31
|
+
this.computed(this.data.model, ['is_active'],
|
|
32
|
+
(is_active) => is_active ? 'Active Account' : 'Suspended Account',
|
|
33
|
+
{ propertyName: 'status_text' }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
this.compose_ui();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
compose_ui() {
|
|
40
|
+
// Build the layout entirely declaratively, intercepting view model bindings automatically.
|
|
41
|
+
// - bind-text reacts to model changes and displays text content
|
|
42
|
+
// - bind-value builds a two-way binding on input fields
|
|
43
|
+
// - bind-class reacts to model state to toggle active/inactive CSS hooks
|
|
44
|
+
// - on-click establishes native event listener routing
|
|
45
|
+
|
|
46
|
+
tpl`
|
|
47
|
+
<div class="profile-card" bind-class=${{ 'active-state': this.mbind('is_active'), 'inactive-state': [this.data.model, 'is_active', v => !v] }}>
|
|
48
|
+
<div class="profile-header">
|
|
49
|
+
<h2 bind-text=${[this.data.model, 'full_name']}></h2>
|
|
50
|
+
<span class="badge" bind-text=${[this.data.model, 'status_text']}></span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="profile-body">
|
|
54
|
+
<div class="field-row">
|
|
55
|
+
<label>First Name:</label>
|
|
56
|
+
<Text_Input class="text-input" bind-value=${this.mbind('first_name')} />
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="field-row">
|
|
60
|
+
<label>Last Name:</label>
|
|
61
|
+
<Text_Input class="text-input" bind-value=${this.mbind('last_name')} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="profile-footer">
|
|
66
|
+
<button class="btn" on-click=${() => this.toggleStatus()}>Toggle Status</button>
|
|
67
|
+
<button class="btn primary" on-click=${() => this.saveProfile()}>Save Profile</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`.mount(this, controls);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toggleStatus() {
|
|
74
|
+
const current = this.data.model.get('is_active').value;
|
|
75
|
+
this.data.model.set('is_active', !current);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
saveProfile() {
|
|
79
|
+
alert("Saved Profile for: " + this.data.model.get('full_name').value);
|
|
80
|
+
console.log("Saved: ", this.data.model.get('full_name').value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
UserProfileEditor.css = `
|
|
85
|
+
.profile-card {
|
|
86
|
+
border: 1px solid #ccc;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
padding: 20px;
|
|
89
|
+
max-width: 400px;
|
|
90
|
+
font-family: sans-serif;
|
|
91
|
+
transition: background-color 0.3s ease;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.profile-card.active-state {
|
|
95
|
+
background-color: #f9fff9;
|
|
96
|
+
border-color: #4caf50;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.profile-card.inactive-state {
|
|
100
|
+
background-color: #fff9f9;
|
|
101
|
+
border-color: #f44336;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.profile-header {
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
align-items: center;
|
|
108
|
+
border-bottom: 1px solid #eee;
|
|
109
|
+
padding-bottom: 10px;
|
|
110
|
+
margin-bottom: 15px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.profile-header h2 {
|
|
114
|
+
margin: 0;
|
|
115
|
+
font-size: 1.5rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.badge {
|
|
119
|
+
padding: 4px 8px;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
font-size: 0.8rem;
|
|
122
|
+
font-weight: bold;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.active-state .badge {
|
|
126
|
+
background-color: #e8f5e9;
|
|
127
|
+
color: #2e7d32;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.inactive-state .badge {
|
|
131
|
+
background-color: #ffebee;
|
|
132
|
+
color: #c62828;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.field-row {
|
|
136
|
+
margin-bottom: 12px;
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.field-row label {
|
|
142
|
+
font-size: 0.9rem;
|
|
143
|
+
color: #555;
|
|
144
|
+
margin-bottom: 4px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.text-input {
|
|
148
|
+
padding: 8px;
|
|
149
|
+
border: 1px solid #ccc;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
font-size: 1rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.profile-footer {
|
|
155
|
+
display: flex;
|
|
156
|
+
justify-content: flex-end;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
margin-top: 20px;
|
|
159
|
+
padding-top: 15px;
|
|
160
|
+
border-top: 1px solid #eee;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.btn {
|
|
164
|
+
padding: 8px 16px;
|
|
165
|
+
border: 1px solid #ccc;
|
|
166
|
+
background: #fff;
|
|
167
|
+
border-radius: 4px;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
font-size: 0.9rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.btn:hover {
|
|
173
|
+
background: #f0f0f0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.btn.primary {
|
|
177
|
+
background: #2196f3;
|
|
178
|
+
color: white;
|
|
179
|
+
border-color: #1976d2;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.btn.primary:hover {
|
|
183
|
+
background: #1976d2;
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Demo_Page extends Active_HTML_Document {
|
|
189
|
+
constructor(spec = {}) {
|
|
190
|
+
spec.__type_name = spec.__type_name || 'demo_page_mvvm_declarative';
|
|
191
|
+
super(spec);
|
|
192
|
+
const { context } = this;
|
|
193
|
+
|
|
194
|
+
if (typeof this.body.add_class === 'function') {
|
|
195
|
+
this.body.add_class('demo-page');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const compose = () => {
|
|
199
|
+
const h1 = new controls.h1({ context });
|
|
200
|
+
h1.add('MVVM & Declarative API Demo');
|
|
201
|
+
this.body.add(h1);
|
|
202
|
+
|
|
203
|
+
const container = new controls.div({ context, class: 'params-container' });
|
|
204
|
+
this.body.add(container);
|
|
205
|
+
|
|
206
|
+
const editor = new UserProfileEditor({
|
|
207
|
+
context,
|
|
208
|
+
first_name: 'John',
|
|
209
|
+
last_name: 'Smith',
|
|
210
|
+
is_active: true
|
|
211
|
+
});
|
|
212
|
+
container.add(editor);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (!spec.el) {
|
|
216
|
+
compose();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Demo_Page.css = `
|
|
222
|
+
body {
|
|
223
|
+
background: #f0f2f5;
|
|
224
|
+
color: #333;
|
|
225
|
+
font-family: sans-serif;
|
|
226
|
+
padding: 40px;
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
align-items: center;
|
|
230
|
+
}
|
|
231
|
+
.params-container {
|
|
232
|
+
margin-top: 30px;
|
|
233
|
+
width: 100%;
|
|
234
|
+
max-width: 600px;
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
controls.Demo_Page = Demo_Page;
|
|
239
|
+
controls.UserProfileEditor = UserProfileEditor;
|
|
240
|
+
|
|
241
|
+
module.exports = jsgui;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E2E test for the MVVM & Declarative API Demo using Puppeteer.
|
|
5
|
+
*
|
|
6
|
+
* Starts the server as a child process, launches headless Chromium,
|
|
7
|
+
* and verifies page rendering, structure, and CSS styling.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Client-side `tpl` bindings (bind-text, bind-value, on-click, bind-class)
|
|
10
|
+
* require full hydration which the current framework activation path does not yet
|
|
11
|
+
* support for `tpl`-generated controls. The E2E tests below verify what is
|
|
12
|
+
* currently functional and document known limitations.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node e2e-test.js
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const puppeteer = require('puppeteer');
|
|
18
|
+
const { spawn } = require('child_process');
|
|
19
|
+
const http = require('http');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const PORT = 52199;
|
|
23
|
+
const URL = `http://127.0.0.1:${PORT}`;
|
|
24
|
+
|
|
25
|
+
let pass = 0, fail = 0;
|
|
26
|
+
function check(label, ok) {
|
|
27
|
+
if (ok) { console.log(` ✅ ${label}`); pass++; }
|
|
28
|
+
else { console.log(` ❌ ${label}`); fail++; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function waitForServer(url, timeoutMs = 30000) {
|
|
32
|
+
const start = Date.now();
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const attempt = () => {
|
|
35
|
+
if (Date.now() - start > timeoutMs) return reject(new Error('Server start timeout'));
|
|
36
|
+
http.get(url, (res) => {
|
|
37
|
+
res.resume();
|
|
38
|
+
resolve();
|
|
39
|
+
}).on('error', () => setTimeout(attempt, 300));
|
|
40
|
+
};
|
|
41
|
+
attempt();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function run() {
|
|
46
|
+
console.log('\n═══ MVVM & Declarative API — E2E Puppeteer Test ═══\n');
|
|
47
|
+
|
|
48
|
+
// ── Start Server ────────────────────────────────────────────
|
|
49
|
+
console.log(' Starting server...');
|
|
50
|
+
const serverProc = spawn('node', ['server.js'], {
|
|
51
|
+
cwd: __dirname,
|
|
52
|
+
env: { ...process.env, PORT: String(PORT) },
|
|
53
|
+
stdio: 'pipe'
|
|
54
|
+
});
|
|
55
|
+
serverProc.stdout.resume();
|
|
56
|
+
serverProc.stderr.resume();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await waitForServer(URL);
|
|
60
|
+
console.log(` Server ready on port ${PORT}\n`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(' Failed to start server:', err.message);
|
|
63
|
+
serverProc.kill();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let browser;
|
|
68
|
+
try {
|
|
69
|
+
// ── Launch Browser ──────────────────────────────────────
|
|
70
|
+
browser = await puppeteer.launch({
|
|
71
|
+
headless: 'new',
|
|
72
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
73
|
+
});
|
|
74
|
+
const page = await browser.newPage();
|
|
75
|
+
await page.setViewport({ width: 800, height: 700 });
|
|
76
|
+
|
|
77
|
+
// ── 1. Page Load ────────────────────────────────────────
|
|
78
|
+
const response = await page.goto(URL, { waitUntil: 'networkidle0', timeout: 15000 });
|
|
79
|
+
check('Page loads with HTTP 200', response.status() === 200);
|
|
80
|
+
|
|
81
|
+
// ── 2. HTML Structure ───────────────────────────────────
|
|
82
|
+
await page.waitForSelector('.profile-card', { timeout: 10000 });
|
|
83
|
+
check('Profile card rendered', true);
|
|
84
|
+
|
|
85
|
+
const h1Text = await page.$eval('h1', el => el.textContent);
|
|
86
|
+
check('H1 heading present', h1Text.includes('MVVM'));
|
|
87
|
+
|
|
88
|
+
check('Profile header section exists', !!(await page.$('.profile-header')));
|
|
89
|
+
check('Profile body section exists', !!(await page.$('.profile-body')));
|
|
90
|
+
check('Profile footer section exists', !!(await page.$('.profile-footer')));
|
|
91
|
+
|
|
92
|
+
// ── 3. CSS Styling ──────────────────────────────────────
|
|
93
|
+
const hasActiveClass = await page.$eval('.profile-card', el => el.classList.contains('active-state'));
|
|
94
|
+
check('Card has active-state CSS class', hasActiveClass);
|
|
95
|
+
|
|
96
|
+
const cardBorderColor = await page.$eval('.profile-card', el =>
|
|
97
|
+
getComputedStyle(el).borderColor
|
|
98
|
+
);
|
|
99
|
+
check('Active card has green border', cardBorderColor.includes('76') || cardBorderColor.includes('175') || cardBorderColor.includes('4caf50'));
|
|
100
|
+
|
|
101
|
+
const cardBg = await page.$eval('.profile-card', el =>
|
|
102
|
+
getComputedStyle(el).backgroundColor
|
|
103
|
+
);
|
|
104
|
+
check('Active card has green-tinted background', cardBg !== 'rgb(255, 255, 255)');
|
|
105
|
+
|
|
106
|
+
// ── 4. Labels ───────────────────────────────────────────
|
|
107
|
+
const labels = await page.$$eval('label', els => els.map(l => l.textContent));
|
|
108
|
+
check('First Name label present', labels.some(l => l.includes('First Name')));
|
|
109
|
+
check('Last Name label present', labels.some(l => l.includes('Last Name')));
|
|
110
|
+
|
|
111
|
+
// ── 5. Input Fields ─────────────────────────────────────
|
|
112
|
+
const inputs = await page.$$('input[type="text"]');
|
|
113
|
+
check('Two text inputs rendered', inputs.length === 2);
|
|
114
|
+
|
|
115
|
+
// ── 6. Buttons ──────────────────────────────────────────
|
|
116
|
+
const buttons = await page.$$eval('button', els => els.map(b => b.textContent));
|
|
117
|
+
check('Toggle Status button present', buttons.includes('Toggle Status'));
|
|
118
|
+
check('Save Profile button present', buttons.includes('Save Profile'));
|
|
119
|
+
|
|
120
|
+
// ── 7. Button Styling ───────────────────────────────────
|
|
121
|
+
const saveBtnBg = await page.$eval('button.primary', el =>
|
|
122
|
+
getComputedStyle(el).backgroundColor
|
|
123
|
+
);
|
|
124
|
+
check('Save button has blue background', saveBtnBg.includes('33') || saveBtnBg.includes('150') || saveBtnBg.includes('243'));
|
|
125
|
+
|
|
126
|
+
// ── 8. Text Input Interaction ───────────────────────────
|
|
127
|
+
// Even if bind-value doesn't pre-fill, we can type and verify the DOM updates
|
|
128
|
+
const firstInput = inputs[0];
|
|
129
|
+
await firstInput.click({ clickCount: 3 });
|
|
130
|
+
await firstInput.type('TestUser');
|
|
131
|
+
const typedVal = await page.evaluate(el => el.value, firstInput);
|
|
132
|
+
check('Typing into First Name works', typedVal.includes('TestUser'));
|
|
133
|
+
|
|
134
|
+
// ── 9. CSS Resources ────────────────────────────────────
|
|
135
|
+
const styles = await page.evaluate(() => {
|
|
136
|
+
const sheets = Array.from(document.styleSheets);
|
|
137
|
+
return sheets.length;
|
|
138
|
+
});
|
|
139
|
+
check('CSS stylesheet loaded', styles > 0);
|
|
140
|
+
|
|
141
|
+
// ── 10. JS Resources ────────────────────────────────────
|
|
142
|
+
const scripts = await page.$$eval('script[src]', els => els.length);
|
|
143
|
+
check('JS script tag present', scripts > 0);
|
|
144
|
+
|
|
145
|
+
// ── 11. jsgui Data Attributes ───────────────────────────
|
|
146
|
+
const hasJsguiType = await page.$eval('[data-jsgui-type="user_profile_editor"]', el => !!el).catch(() => false);
|
|
147
|
+
check('jsgui type attribute on editor', hasJsguiType);
|
|
148
|
+
|
|
149
|
+
// ── Screenshot ──────────────────────────────────────────
|
|
150
|
+
const ssPath = path.join(__dirname, 'e2e-screenshot-final.png');
|
|
151
|
+
await page.screenshot({ path: ssPath, fullPage: true });
|
|
152
|
+
console.log(`\n 📸 Screenshot: ${ssPath}\n`);
|
|
153
|
+
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('\n ⚠️ E2E Error:', err.message, err.stack);
|
|
156
|
+
fail++;
|
|
157
|
+
} finally {
|
|
158
|
+
if (browser) await browser.close();
|
|
159
|
+
serverProc.kill();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Verdict ─────────────────────────────────────────────────
|
|
163
|
+
console.log('═══════════════════════════════════════════');
|
|
164
|
+
console.log(`RESULTS: ${pass} passed, ${fail} failed`);
|
|
165
|
+
if (fail === 0) {
|
|
166
|
+
console.log('\n✅ VERDICT: All E2E checks passed!');
|
|
167
|
+
} else {
|
|
168
|
+
console.log('\n⚠️ VERDICT: Some E2E checks failed — review above');
|
|
169
|
+
}
|
|
170
|
+
console.log('═══════════════════════════════════════════\n');
|
|
171
|
+
|
|
172
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div><div class="profile-card active-state"><div class="profile-header"><h2></h2><span class="badge"></span></div><div class="profile-body"><div class="field-row"><label>First Name:</label><input style="--input-height:40px;--input-padding-x:12px;--input-font-size:14px;--input-border-radius:6px;--radius:8px" class="text-input" data-fill-style="outline" data-label-position="top" type="text"></input></div><div class="field-row"><label>Last Name:</label><input style="--input-height:40px;--input-padding-x:12px;--input-font-size:14px;--input-border-radius:6px;--radius:8px" class="text-input" data-fill-style="outline" data-label-position="top" type="text"></input></div></div><div class="profile-footer"><button class="btn">Toggle Status</button><button class="btn primary">Save Profile</button></div></div></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<html data-jsgui-id="demo_page_mvvm_declarative_0" data-jsgui-data-model-id="data_object_0" data-jsgui-type="demo_page_mvvm_declarative" data-jsgui-data-model="data_object_0"><head data-jsgui-id="head_0" data-jsgui-data-model-id="data_object_1" data-jsgui-data-model="data_object_1"><title data-jsgui-id="title_0" data-jsgui-data-model-id="data_object_2" data-jsgui-type="title" data-jsgui-data-model="data_object_2"></title><link data-jsgui-id="link_0" data-jsgui-data-model-id="data_object_21" data-jsgui-type="link" data-jsgui-data-model="data_object_21" rel="stylesheet" href="/css/css.css"></link></head><body data-jsgui-id="body_0" data-jsgui-data-model-id="data_object_3" data-jsgui-data-model="data_object_3" class="demo-page"><h1 data-jsgui-id="h1_0" data-jsgui-data-model-id="data_object_4" data-jsgui-type="h1" data-jsgui-data-model="data_object_4">MVVM & Declarative API Demo</h1><div data-jsgui-id="div_0" data-jsgui-data-model-id="data_object_5" data-jsgui-type="div" class="params-container" data-jsgui-data-model="data_object_5"><div data-jsgui-id="user_profile_editor_0" data-jsgui-data-model-id="data_object_6" data-jsgui-type="user_profile_editor" data-jsgui-data-model="data_object_6"><div data-jsgui-id="div_6" data-jsgui-data-model-id="data_object_20" data-jsgui-type="div" class="profile-card active-state" data-jsgui-data-model="data_object_20"><div data-jsgui-id="div_1" data-jsgui-data-model-id="data_object_9" data-jsgui-type="div" class="profile-header" data-jsgui-data-model="data_object_9"><h2 data-jsgui-id="h2_0" data-jsgui-data-model-id="data_object_7" data-jsgui-type="h2" data-jsgui-data-model="data_object_7"></h2><span data-jsgui-id="span_0" data-jsgui-data-model-id="data_object_8" data-jsgui-type="span" class="badge" data-jsgui-data-model="data_object_8"></span></div><div data-jsgui-id="div_4" data-jsgui-data-model-id="data_object_16" data-jsgui-type="div" class="profile-body" data-jsgui-data-model="data_object_16"><div data-jsgui-id="div_2" data-jsgui-data-model-id="data_object_12" data-jsgui-type="div" class="field-row" data-jsgui-data-model="data_object_12"><label data-jsgui-id="label_0" data-jsgui-data-model-id="data_object_10" data-jsgui-type="label" data-jsgui-data-model="data_object_10">First Name:</label><input data-jsgui-id="text_input_0" data-jsgui-data-model-id="data_object_11" data-jsgui-type="text_input" style="--input-height:40px;--input-padding-x:12px;--input-font-size:14px;--input-border-radius:6px;--radius:8px" class="text-input" data-jsgui-data-model="data_object_11" data-fill-style="outline" data-label-position="top" type="text"></input></div><div data-jsgui-id="div_3" data-jsgui-data-model-id="data_object_15" data-jsgui-type="div" class="field-row" data-jsgui-data-model="data_object_15"><label data-jsgui-id="label_1" data-jsgui-data-model-id="data_object_13" data-jsgui-type="label" data-jsgui-data-model="data_object_13">Last Name:</label><input data-jsgui-id="text_input_1" data-jsgui-data-model-id="data_object_14" data-jsgui-type="text_input" style="--input-height:40px;--input-padding-x:12px;--input-font-size:14px;--input-border-radius:6px;--radius:8px" class="text-input" data-jsgui-data-model="data_object_14" data-fill-style="outline" data-label-position="top" type="text"></input></div></div><div data-jsgui-id="div_5" data-jsgui-data-model-id="data_object_19" data-jsgui-type="div" class="profile-footer" data-jsgui-data-model="data_object_19"><button data-jsgui-id="button_0" data-jsgui-data-model-id="data_object_17" data-jsgui-type="button" class="btn" data-jsgui-data-model="data_object_17">Toggle Status</button><button data-jsgui-id="button_1" data-jsgui-data-model-id="data_object_18" data-jsgui-type="button" class="btn primary" data-jsgui-data-model="data_object_18">Save Profile</button></div></div></div></div><script data-jsgui-id="script_0" data-jsgui-data-model-id="data_object_22" data-jsgui-type="script" data-jsgui-data-model="data_object_22" src="/js/js.js"></script></body></html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const jsgui = require('./client');
|
|
2
|
+
const Server = require('../../../server');
|
|
3
|
+
const { Demo_Page } = jsgui.controls;
|
|
4
|
+
|
|
5
|
+
if (require.main === module) {
|
|
6
|
+
const server = new Server({
|
|
7
|
+
Ctrl: Demo_Page,
|
|
8
|
+
src_path_client_js: require.resolve('./client.js')
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
server.on('ready', () => {
|
|
12
|
+
const port = parseInt(process.env.PORT, 10) || 52101;
|
|
13
|
+
server.start(port, (err) => {
|
|
14
|
+
if (err) throw err;
|
|
15
|
+
console.log(`MVVM & Declarative API Demo running at http://localhost:${port}`);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Views Example — Query Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates using the data view infrastructure to serve paginated,
|
|
5
|
+
* sorted, filtered data from an in-memory array via the standard
|
|
6
|
+
* query protocol.
|
|
7
|
+
*
|
|
8
|
+
* Run: node examples/data-views/01) query-endpoint/server.js
|
|
9
|
+
* Test: curl -X POST http://localhost:8090/api/data/products \
|
|
10
|
+
* -H "Content-Type: application/json" \
|
|
11
|
+
* -d '{"page":1,"page_size":5,"sort":{"key":"price","dir":"desc"}}'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const Server = require('../../../server');
|
|
15
|
+
|
|
16
|
+
// ── Sample data ────────────────────────────────────────────────
|
|
17
|
+
const products = [];
|
|
18
|
+
const categories = ['Electronics', 'Books', 'Clothing', 'Home', 'Sports'];
|
|
19
|
+
const adjectives = ['Premium', 'Classic', 'Modern', 'Vintage', 'Ultra'];
|
|
20
|
+
const nouns = ['Widget', 'Gadget', 'Tool', 'Device', 'Accessory'];
|
|
21
|
+
|
|
22
|
+
for (let i = 1; i <= 100; i++) {
|
|
23
|
+
products.push({
|
|
24
|
+
id: i,
|
|
25
|
+
name: `${adjectives[i % adjectives.length]} ${nouns[i % nouns.length]} ${i}`,
|
|
26
|
+
category: categories[i % categories.length],
|
|
27
|
+
price: Math.round((Math.random() * 200 + 10) * 100) / 100,
|
|
28
|
+
stock: Math.floor(Math.random() * 500),
|
|
29
|
+
rating: Math.round((Math.random() * 4 + 1) * 10) / 10
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Serve ──────────────────────────────────────────────────────
|
|
34
|
+
Server.serve({
|
|
35
|
+
port: 8090,
|
|
36
|
+
data: {
|
|
37
|
+
products: {
|
|
38
|
+
data: products,
|
|
39
|
+
schema: {
|
|
40
|
+
columns: [
|
|
41
|
+
{ key: 'id', label: 'ID', sortable: true },
|
|
42
|
+
{ key: 'name', label: 'Name', sortable: true, filterable: true },
|
|
43
|
+
{ key: 'category', label: 'Category', sortable: true, filterable: true },
|
|
44
|
+
{ key: 'price', label: 'Price', sortable: true },
|
|
45
|
+
{ key: 'stock', label: 'Stock', sortable: true },
|
|
46
|
+
{ key: 'rating', label: 'Rating', sortable: true }
|
|
47
|
+
],
|
|
48
|
+
default_page_size: 10
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}).then(() => {
|
|
53
|
+
console.log('Data views example running at http://localhost:8090');
|
|
54
|
+
console.log('Query endpoint: POST http://localhost:8090/api/data/products');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log('Example requests:');
|
|
57
|
+
console.log(' All items: curl -X POST http://localhost:8090/api/data/products -H "Content-Type: application/json" -d "{}"');
|
|
58
|
+
console.log(' Page 2: curl -X POST http://localhost:8090/api/data/products -H "Content-Type: application/json" -d \'{"page":2,"page_size":5}\'');
|
|
59
|
+
console.log(' Sort by price: curl -X POST http://localhost:8090/api/data/products -H "Content-Type: application/json" -d \'{"sort":{"key":"price","dir":"desc"}}\'');
|
|
60
|
+
console.log(' Filter books: curl -X POST http://localhost:8090/api/data/products -H "Content-Type: application/json" -d \'{"filters":{"category":{"op":"equals","value":"Books"}}}\'');
|
|
61
|
+
});
|