@cdmx/wappler_ag_grid 2.1.7 → 2.1.8
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 +71 -10
- package/app_connect/components.hjson +1 -1
- package/dmx-ag-grid.js +8 -0
- package/package.json +1 -1
- package/tests/01-basic-render.html +1 -1
- package/tests/02-themes-locale.html +1 -1
- package/tests/03-editing.html +1 -1
- package/tests/04-selection.html +1 -1
- package/tests/05-action-buttons.html +1 -1
- package/tests/06-export.html +1 -1
- package/tests/07-import.html +1 -1
- package/tests/08-formatting.html +1 -1
- package/tests/09-grouping.html +1 -1
- package/tests/10-state-persistence.html +1 -1
- package/tests/11-layout.html +1 -1
- package/tests/12-methods.html +1 -1
- package/tests/13-flags-styles.html +5 -1
- package/tests/14-compact-view.html +1 -1
- package/tests/15-csp.html +98 -0
- package/tests/FINDINGS.md +229 -0
- package/tests/README.md +6 -0
- package/tests/VERSIONS.json +29 -0
- package/tests/csp-hashes.cjs +191 -0
- package/tests/findings.json +892 -0
- package/tests/lib/common-head.html +1 -1
- package/tests/lib/version-banner.js +46 -0
- package/tests/run-all-puppeteer.cjs +2 -1
- package/tests/run-all.cjs +2 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# AG Grid Wappler · Test Run Findings
|
|
2
|
+
|
|
3
|
+
Latest run: **2026-05-27** · all 14 scenarios PASS against the unmodified
|
|
4
|
+
`dmx-ag-grid.js` v2.1.4 · driven by puppeteer-core against system Chrome
|
|
5
|
+
(`tests/run-all-puppeteer.cjs`). Scenario 13 now performs 11 colour-pixel
|
|
6
|
+
assertions that verify every `rstyles` condition form added in commit
|
|
7
|
+
[42ec4a8](../dmx-ag-grid.js) actually applies the expected number of coloured
|
|
8
|
+
rows.
|
|
9
|
+
|
|
10
|
+
The suite is intentionally tolerant: it verifies that each grid mounts, gets
|
|
11
|
+
data, and renders rows. The state-flag observability bug below means only one
|
|
12
|
+
of the three `onGrid*` flags is visible at a time, so the runner accepts any of
|
|
13
|
+
them as proof the grid mounted.
|
|
14
|
+
|
|
15
|
+
## Summary
|
|
16
|
+
|
|
17
|
+
| # | Scenario | Status | Grids verified |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| 01 | basic-render | PASS | 1 grid, 25 rows, 10 visible (paged) |
|
|
20
|
+
| 02 | themes-locale | PASS | 9 grids — Alpine/Balham/Material/Quartz/Custom/Dark/HE/RU/ES |
|
|
21
|
+
| 03 | editing | PASS | cell + row + static-select editors |
|
|
22
|
+
| 04 | selection | PASS | multi-row + single-row, checkbox + status events |
|
|
23
|
+
| 05 | action-buttons | PASS | edit/view/delete + button1..5 + condition |
|
|
24
|
+
| 06 | export | PASS | CSV/XLSX/PDF + no-export fallback |
|
|
25
|
+
| 07 | import | PASS | empty grid waiting for file upload (expectEmpty) |
|
|
26
|
+
| 08 | formatting | PASS | amount, date, ctypes, cnames, cwidths, wrap |
|
|
27
|
+
| 09 | grouping | PASS | group_config + columns_to_sum + columns_to_count |
|
|
28
|
+
| 10 | state-persistence | PASS | column state save + filter apply/clear |
|
|
29
|
+
| 11 | layout | PASS | autoHeight, fixed_header offset, horizontal scroll |
|
|
30
|
+
| 12 | methods | PASS | every imperative method exposed as a button |
|
|
31
|
+
| 13 | flags-styles | PASS | suppress_*, hide_filters, **rstyles ×6 forms (asserted)**, cstyles area:text/cell, data_changes |
|
|
32
|
+
| 14 | compact-view | PASS | compact_view + buttons 6–15 + class toggles |
|
|
33
|
+
|
|
34
|
+
Full per-grid state and DOM-row counts are in `tests/findings.json`.
|
|
35
|
+
|
|
36
|
+
## rstyles coverage for 2.1.4 (commit 42ec4a8)
|
|
37
|
+
|
|
38
|
+
Scenario 13 now exercises the full condition matrix introduced by
|
|
39
|
+
"feat: enhance row color formatting with dynamic conditions" and asserts the
|
|
40
|
+
expected number of coloured rows in the DOM:
|
|
41
|
+
|
|
42
|
+
| Form | Test grid | Sample config | Hand-derived count | Asserted? |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| Function — `myFn()` | `rsFn` | `{condition:'isRowVip()'}` (window-scoped) | 3 (rating≥4.7 & active) | ✓ |
|
|
45
|
+
| Value/operator `>` | `rsOp` | `{condition:'salary > 90000'}` | 7 | ✓ |
|
|
46
|
+
| Value/operator `<=` (2.1.4 parser fix) | `rsOp` | `{condition:'salary <= 65000'}` | 5 | ✓ |
|
|
47
|
+
| Compound `&&` | `rsCompound` | `{condition:'active == true && department == Engineering'}` | 7 | ✓ |
|
|
48
|
+
| Field + plain-value shorthand | `rsShort` | `{field:'department', condition:'Marketing'}` | 5 | ✓ |
|
|
49
|
+
| First match wins | `rsFirst` | `[{>95000→orange}, {>90000→yellow}]` | 5 orange / 2 yellow | ✓ |
|
|
50
|
+
| Legacy object form (backwards compat) | `rsLegacy` | `{sales:{…}, hr:{…}}` set programmatically | 5 / 4 | ✓ |
|
|
51
|
+
| cstyles area:text with `>=` | `csCell` | `{field:'salary', condition:'salary >= 90000', area:'text'}` | 7 | ✓ |
|
|
52
|
+
| cstyles area:cell with `<=` | `csCell` | `{field:'salary', condition:'salary <= 65000', area:'cell'}` | 5 | ✓ |
|
|
53
|
+
|
|
54
|
+
### Real finding from this work
|
|
55
|
+
|
|
56
|
+
**`rstyles` legacy object form is unreachable via `dmx-bind:`.** The 2.1.4 rewrite
|
|
57
|
+
explicitly added a backwards-compat branch:
|
|
58
|
+
```js
|
|
59
|
+
const styles = Array.isArray(rstyles)
|
|
60
|
+
? rstyles
|
|
61
|
+
: (rstyles && typeof rstyles === 'object' ? Object.values(rstyles) : []);
|
|
62
|
+
```
|
|
63
|
+
…but the schema is `rstyles: { type: Array, default: [] }`, and AppConnect's
|
|
64
|
+
type coercion drops every key of an object-shaped binding value, so the runtime
|
|
65
|
+
receives `[]` and the legacy branch is dead code. The branch only fires when
|
|
66
|
+
`rstyles` is set programmatically (e.g. `component.props.rstyles = {…}` then
|
|
67
|
+
`refreshGrid()`), which is what `rsLegacy` does to verify the branch still
|
|
68
|
+
works. Worth either changing the schema to `Object` to make the binding path
|
|
69
|
+
work, or removing the legacy branch.
|
|
70
|
+
|
|
71
|
+
## Bugs observed in dmx-ag-grid.js
|
|
72
|
+
|
|
73
|
+
The user reverted my proposed patches so the tests run against the production
|
|
74
|
+
file as-is. These are the issues the suite surfaces — listed for the maintainer
|
|
75
|
+
to decide whether to patch.
|
|
76
|
+
|
|
77
|
+
### 1. `set('state', ...)` overwrites previous state flags
|
|
78
|
+
|
|
79
|
+
**Where:** [dmx-ag-grid.js:1924-1930](../dmx-ag-grid.js#L1924-L1930)
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
onGridReady: () => this.set("state", { gridReady: true }),
|
|
83
|
+
onFirstDataRendered:() => this.set("state", { firstDataRendered: true }),
|
|
84
|
+
onRowDataUpdated: () => this.set("state", { rowDataUpdated: true }),
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Each call **replaces** the entire `state` object instead of merging. After all
|
|
88
|
+
three fire, only `{ rowDataUpdated: true }` (or whichever fired last) is
|
|
89
|
+
observable in the binding `grid.state.*`. The other two flags are lost.
|
|
90
|
+
|
|
91
|
+
**Visible in the test output:** every passing grid now shows
|
|
92
|
+
`state={"firstDataRendered":true}` — but the 09-grouping `aggGrid` shows
|
|
93
|
+
`state={"rowDataUpdated":true}` because its data refresh fires last. Wappler
|
|
94
|
+
templates that bind `dmx-show="grid.state.gridReady"` will flicker on and
|
|
95
|
+
disappear.
|
|
96
|
+
|
|
97
|
+
**Suggested fix (1 line × 3):**
|
|
98
|
+
```js
|
|
99
|
+
this.set("state", Object.assign({}, this.data.state, { gridReady: true }));
|
|
100
|
+
// …same pattern for firstDataRendered, rowDataUpdated
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 2. `cwidths[key]` with a non-object value freezes the page
|
|
106
|
+
|
|
107
|
+
**Where:** [dmx-ag-grid.js:1637-1640](../dmx-ag-grid.js#L1637-L1640)
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
...(cwidths.hasOwnProperty(key) && {
|
|
111
|
+
minWidth: parseInt(cwidths[key].min_width), // NaN if cwidths[key] is a number
|
|
112
|
+
maxWidth: parseInt(cwidths[key].max_width),
|
|
113
|
+
}),
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The expected shape is `{ field: { min_width: N, max_width: M } }`. A user
|
|
117
|
+
naturally typing `{ first_name: 120 }` (which matches the type declaration
|
|
118
|
+
`Object` and feels intuitive) makes `parseInt(undefined) === NaN`. AG Grid then
|
|
119
|
+
enters an infinite layout loop and **hangs the entire JS thread** — no console
|
|
120
|
+
error, no recovery, just a frozen page.
|
|
121
|
+
|
|
122
|
+
The 08-formatting scenario originally failed this way; the test page now uses
|
|
123
|
+
the documented shape so it passes. But any real user typing a number will hit
|
|
124
|
+
this.
|
|
125
|
+
|
|
126
|
+
**Suggested fix:**
|
|
127
|
+
```js
|
|
128
|
+
...(cwidths.hasOwnProperty(key) && cwidths[key] && typeof cwidths[key] === 'object' && {
|
|
129
|
+
...(Number.isFinite(parseInt(cwidths[key].min_width)) && { minWidth: parseInt(cwidths[key].min_width) }),
|
|
130
|
+
...(Number.isFinite(parseInt(cwidths[key].max_width)) && { maxWidth: parseInt(cwidths[key].max_width) })
|
|
131
|
+
}),
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### 3. `columns_to_count` entries assumed to be objects, crash on strings
|
|
137
|
+
|
|
138
|
+
**Where:** [dmx-ag-grid.js:2007-2009](../dmx-ag-grid.js#L2007-L2009)
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
columnsToCount.forEach(function (colObj) {
|
|
142
|
+
const col = colObj.field;
|
|
143
|
+
const uniqueValuesToCount = new Set(colObj.unique_values.split(',')); // crash if colObj is a string
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The attribute is declared `type: Array, default: []` which suggests an array of
|
|
147
|
+
column names — but each entry must actually be `{field, unique_values}`. Passing
|
|
148
|
+
`['customer']` throws "Cannot read properties of undefined (reading 'split')".
|
|
149
|
+
|
|
150
|
+
**Suggested fix:** validate + warn on malformed entries.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### 4. `action_button_class_toggles` entries crash on bad shape
|
|
155
|
+
|
|
156
|
+
**Where:** [dmx-ag-grid.js:782, 787](../dmx-ag-grid.js#L782)
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
if (toggle.btn_id.toLowerCase() === buttonConfig.id && ...) // crash if btn_id missing
|
|
160
|
+
button.classList.add(...toggle.class.split(' ')); // crash if class missing
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
No type guards on the toggle entries. The expected shape is
|
|
164
|
+
`{btn_id, condition, class}` but mis-shaped entries trigger
|
|
165
|
+
"Cannot read properties of undefined".
|
|
166
|
+
|
|
167
|
+
**Suggested fix:** validate `btn_id` and `class` are strings before using them.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### 5. No initial render when bound `data` is already populated at mount time
|
|
172
|
+
|
|
173
|
+
**Where:** [dmx-ag-grid.js:2985-3015](../dmx-ag-grid.js#L2985-L3015) — `requestUpdate`
|
|
174
|
+
|
|
175
|
+
`requestUpdate('data', oldValue)` is the only path that calls `refreshGrid()`.
|
|
176
|
+
If `data` is already at its final value when the component mounts (i.e. the
|
|
177
|
+
binding resolved synchronously before init), no `data` change event fires after
|
|
178
|
+
mount and `refreshGrid()` never runs — the grid stays empty.
|
|
179
|
+
|
|
180
|
+
In production Wappler apps this is masked because `serverConnect` data arrives
|
|
181
|
+
**after** mount. Static fixtures hit it.
|
|
182
|
+
|
|
183
|
+
**Workaround in the test suite:** `<dmx-test-data>` defers its `set('value', …)`
|
|
184
|
+
to `dmx.nextTick` so the grid sees a transition from `[]` → `[…]`. See
|
|
185
|
+
[tests/lib/test-helpers.js:24-31](lib/test-helpers.js#L24-L31).
|
|
186
|
+
|
|
187
|
+
**Suggested upstream fix:** in `init(node)`, if `this.props.data?.length`, call
|
|
188
|
+
`this.refreshGrid()` once. One line, in one place.
|
|
189
|
+
|
|
190
|
+
## Schema-shape gotchas (worth documenting in components.hjson help text)
|
|
191
|
+
|
|
192
|
+
- `cstatic_select_editors` — `{ field: { options: '<JSON-string>' } }` where
|
|
193
|
+
`options` is a **string** that parses to an object whose keys are the dropdown
|
|
194
|
+
values. Not an array.
|
|
195
|
+
- `cdynamic_select_editors` — `{ field: { options_field: 'someRowField' } }`.
|
|
196
|
+
- `cwidths` — `{ field: { min_width, max_width } }`. Plain numbers freeze the page.
|
|
197
|
+
- `columns_to_count` — array of `{ field, unique_values: csv-string }`. Not array of strings.
|
|
198
|
+
- `action_button_class_toggles` — array of `{ btn_id, condition, class }`. Not `button`/`class_when_true`.
|
|
199
|
+
|
|
200
|
+
## Test-helper observation (not a module bug)
|
|
201
|
+
|
|
202
|
+
`dmx.app.get(id)` in AppConnect 2.2.4 returns a **reactive-data proxy**, not the
|
|
203
|
+
component instance. Direct field access (`g.state`, `g.count`) works; methods
|
|
204
|
+
are exposed as `__methodName` (`__loadGrid`, `__reloadGrid`); calling `g.get(...)`
|
|
205
|
+
throws "g.get is not a function". `tests/lib/test-helpers.js` documents this
|
|
206
|
+
and exposes a friendlier `TestKit.callMethod('id', 'methodName', ...args)`.
|
|
207
|
+
|
|
208
|
+
## Test infrastructure
|
|
209
|
+
|
|
210
|
+
- [tests/run-all-puppeteer.cjs](run-all-puppeteer.cjs) — headless Chrome driver
|
|
211
|
+
via `puppeteer-core` against system Chrome. Recommended (~75 s for 14
|
|
212
|
+
scenarios, no daemon hassles).
|
|
213
|
+
- [tests/run-all.cjs](run-all.cjs) — agent-browser CLI driver. Works on Windows
|
|
214
|
+
+ Linux but its long-lived daemon can time out after ~7 heavy pages.
|
|
215
|
+
- [tests/serve.cjs](serve.cjs) — static server (port 8765).
|
|
216
|
+
|
|
217
|
+
## Files touched in this session
|
|
218
|
+
|
|
219
|
+
**Test pages updated to use correct schemas:**
|
|
220
|
+
- `tests/03-editing.html` — `cstatic_select_editors`
|
|
221
|
+
- `tests/08-formatting.html` — `cwidths`
|
|
222
|
+
- `tests/09-grouping.html` — `columns_to_count`
|
|
223
|
+
- `tests/14-compact-view.html` — `action_button_class_toggles`
|
|
224
|
+
|
|
225
|
+
**Test helpers:**
|
|
226
|
+
- `tests/lib/test-helpers.js` — defer `<dmx-test-data>` set, TestKit uses direct field access
|
|
227
|
+
- `tests/lib/page-template.js` — readiness signal accepts any state flag
|
|
228
|
+
|
|
229
|
+
**Module (`dmx-ag-grid.js`):** reverted by user — runs as-shipped.
|
package/tests/README.md
CHANGED
|
@@ -18,6 +18,10 @@ node tests/serve.cjs # http://localhost:8765/tests/index.html
|
|
|
18
18
|
node tests/run-all-puppeteer.cjs
|
|
19
19
|
# alternative — drives the agent-browser CLI (Windows + Linux):
|
|
20
20
|
node tests/run-all.cjs
|
|
21
|
+
|
|
22
|
+
# 3. CSP: print the style-src hash allow-list AG Grid + the module need, and
|
|
23
|
+
# (--verify) reload every scenario under that header. Starts its own server.
|
|
24
|
+
node tests/csp-hashes.cjs --verify
|
|
21
25
|
```
|
|
22
26
|
|
|
23
27
|
Both produce the same `tests/findings.json` and per-page status table.
|
|
@@ -50,9 +54,11 @@ tests/
|
|
|
50
54
|
├── 12-methods.html ← every imperative method as a button
|
|
51
55
|
├── 13-flags-styles.html ← suppress_*, rstyles, cstyles, data_changes
|
|
52
56
|
├── 14-compact-view.html ← compact_view + buttons 6–15
|
|
57
|
+
├── 15-csp.html ← nonce-only style-src, no 'unsafe-inline' (__cspCheck)
|
|
53
58
|
├── serve.cjs ← cross-platform static server
|
|
54
59
|
├── run-all.cjs ← agent-browser CLI driver (cross-platform)
|
|
55
60
|
├── run-all-puppeteer.cjs ← headless Chrome driver (recommended)
|
|
61
|
+
├── csp-hashes.cjs ← CSP style-src hash allow-list generator (+ --verify)
|
|
56
62
|
├── data/
|
|
57
63
|
│ └── sample-data.js ← employees + orders fixtures
|
|
58
64
|
├── dmxAppConnect/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Versions this test suite was written against. Bump these and re-run the suite when any dependency changes.",
|
|
3
|
+
"module": {
|
|
4
|
+
"name": "@cdmx/wappler_ag_grid",
|
|
5
|
+
"version": "2.1.3"
|
|
6
|
+
},
|
|
7
|
+
"appConnect": {
|
|
8
|
+
"expected": "2.2.4",
|
|
9
|
+
"build": "2026-01-19 14:03:35",
|
|
10
|
+
"file": "./dmxAppConnect/dmxAppConnect.js",
|
|
11
|
+
"detect": "window.dmx && dmx.version"
|
|
12
|
+
},
|
|
13
|
+
"vendor": {
|
|
14
|
+
"ag-grid-community": "~35.1.0",
|
|
15
|
+
"@ag-grid-community/locale": "~35.1.0",
|
|
16
|
+
"papaparse": "~5.5.2",
|
|
17
|
+
"pdfmake": "~0.2.18",
|
|
18
|
+
"read-excel-file": "~5.8.7",
|
|
19
|
+
"exceljs": "^4.4.0"
|
|
20
|
+
},
|
|
21
|
+
"compatibility": {
|
|
22
|
+
"policy": "If dmxAppConnect.js is replaced with a newer build, the suite should keep working as long as the dmx.Component API surface and dmx-bind:* attribute parsing remain backwards-compatible.",
|
|
23
|
+
"smokeCheck": "Open tests/index.html; the AppConnect version pill in the header should match expected. If it drifts, run all scenarios and update this manifest.",
|
|
24
|
+
"knownRisks": [
|
|
25
|
+
"performUpdate vs requestUpdate lifecycle — the grid still uses requestUpdate; AppConnect must keep supporting the older form.",
|
|
26
|
+
"dmx-bind:* attribute parser — any change to expression parsing would break data binding in every scenario."
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Prints the CSP `style-src` hash allow-list that AG Grid (the with-styles UMD bundle) and
|
|
2
|
+
// dmx-ag-grid.js need, so a page can drop 'unsafe-inline' without touching the module.
|
|
3
|
+
//
|
|
4
|
+
// node tests/csp-hashes.cjs → directive + helmet array form on stdout
|
|
5
|
+
// node tests/csp-hashes.cjs --verify → also reload every scenario page under that header;
|
|
6
|
+
// exit 1 on any AG Grid/module violation or broken grid
|
|
7
|
+
//
|
|
8
|
+
// Regenerate after ANY upgrade of ag-grid-community or dmx-ag-grid.js: the hashes are bound to
|
|
9
|
+
// the exact bytes of the injected CSS. Chrome auto-detection is the same as run-all-puppeteer.cjs
|
|
10
|
+
// (set PUPPETEER_EXECUTABLE_PATH to override).
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const puppeteer = require('puppeteer-core');
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
const PORT = Number(process.env.AG_TEST_PORT) || 8766;
|
|
19
|
+
const VERIFY = process.argv.includes('--verify');
|
|
20
|
+
// ponytail: coverage = whatever these pages (plus poke() below) make AG Grid inject. A feature
|
|
21
|
+
// outside the suite can need one more hash; add a scenario page for it and re-run.
|
|
22
|
+
const PAGES = fs.readdirSync(__dirname).filter(f => /^\d\d-.*\.html$/.test(f)).sort();
|
|
23
|
+
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.map': 'application/json' };
|
|
24
|
+
const sha = s => crypto.createHash('sha256').update(s, 'utf8').digest('base64');
|
|
25
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
26
|
+
|
|
27
|
+
function findChrome() {
|
|
28
|
+
if (process.env.PUPPETEER_EXECUTABLE_PATH) return process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
29
|
+
const candidates = process.platform === 'win32' ? [
|
|
30
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
31
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
32
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
33
|
+
'C:\\Users\\Roney\\.agent-browser\\browsers\\chrome-148.0.7778.97\\chrome.exe'
|
|
34
|
+
] : process.platform === 'darwin' ? [
|
|
35
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
36
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
37
|
+
] : [
|
|
38
|
+
'/usr/bin/google-chrome',
|
|
39
|
+
'/usr/bin/chromium',
|
|
40
|
+
'/usr/bin/chromium-browser',
|
|
41
|
+
'/snap/bin/chromium'
|
|
42
|
+
];
|
|
43
|
+
return candidates.find(p => fs.existsSync(p));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Static server over the repo root (serve.cjs cannot set headers). `csp` is sent on HTML responses.
|
|
47
|
+
let csp = null;
|
|
48
|
+
const server = http.createServer((req, res) => {
|
|
49
|
+
const file = path.normalize(path.join(ROOT, decodeURIComponent(req.url.split('?')[0])));
|
|
50
|
+
if (!file.startsWith(ROOT)) { res.writeHead(403); return res.end(); }
|
|
51
|
+
fs.readFile(file, (err, buf) => {
|
|
52
|
+
if (err) { res.writeHead(404); return res.end('404'); }
|
|
53
|
+
const headers = { 'content-type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream', 'cache-control': 'no-store' };
|
|
54
|
+
if (csp && file.endsWith('.html')) headers['content-security-policy'] = csp;
|
|
55
|
+
res.writeHead(200, headers);
|
|
56
|
+
res.end(buf);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// <style> blocks written into the scenario pages themselves are the pages' business, not the list's.
|
|
61
|
+
const pageOwned = new Set();
|
|
62
|
+
for (const f of PAGES) {
|
|
63
|
+
for (const m of fs.readFileSync(path.join(__dirname, f), 'utf8').matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
64
|
+
pageOwned.add(sha(m[1]));
|
|
65
|
+
pageOwned.add(sha(m[1].replace(/\r\n/g, '\n')));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The module's three static blocks, hashed from source (template literals normalise CRLF to LF),
|
|
70
|
+
// so they are listed even when no scenario switches the option on.
|
|
71
|
+
function moduleBlocks() {
|
|
72
|
+
const src = fs.readFileSync(path.join(ROOT, 'dmx-ag-grid.js'), 'utf8');
|
|
73
|
+
const wanted = [
|
|
74
|
+
['dmx-ag-grid.js cell centering', /styleElement\.textContent \+= `([\s\S]*?)`;/],
|
|
75
|
+
['dmx-ag-grid.js sticky h-scroll', /styleElement\.innerHTML = `([\s\S]*?)`;/],
|
|
76
|
+
['dmx-ag-grid.js pagination panel', /const paginationPanelCss = `([\s\S]*?)`;/]
|
|
77
|
+
];
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const [tag, re] of wanted) {
|
|
80
|
+
const m = src.match(re);
|
|
81
|
+
if (m) out.push({ hash: sha(m[1].replace(/\r\n/g, '\n')), tag, len: m[1].length });
|
|
82
|
+
else console.warn('warning: "' + tag + '" block not found in dmx-ag-grid.js — the module changed; check the runtime <style> blocks');
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Best-effort clicks so AG Grid also injects the CSS of lazily created parts (menus, popups, editors).
|
|
88
|
+
async function poke(page) {
|
|
89
|
+
const steps = [
|
|
90
|
+
async () => { await (await page.$('.ag-header-cell')).hover(); await sleep(150); await (await page.$('.ag-header-cell-menu-button')).click(); },
|
|
91
|
+
async () => { await (await page.$('.ag-floating-filter-button-button')).click(); },
|
|
92
|
+
async () => { await (await page.$('.ag-cell')).click({ clickCount: 2 }); },
|
|
93
|
+
async () => { await (await page.$('.ag-header-cell-label')).click(); },
|
|
94
|
+
async () => { await (await page.$('.ag-paging-button[data-ref="btNext"]')).click(); }
|
|
95
|
+
];
|
|
96
|
+
for (const step of steps) {
|
|
97
|
+
try { await step(); await sleep(300); await page.keyboard.press('Escape'); } catch (_) { /* element absent on this page */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function load(browser, file, policy) {
|
|
102
|
+
csp = policy;
|
|
103
|
+
const page = await browser.newPage();
|
|
104
|
+
page.setDefaultTimeout(30000);
|
|
105
|
+
const demanded = new Set(); // hashes Chrome names in its CSP console errors
|
|
106
|
+
page.on('console', m => {
|
|
107
|
+
const t = m.text();
|
|
108
|
+
if (/Content Security Policy/.test(t)) (t.match(/sha256-[A-Za-z0-9+/=]+/g) || []).forEach(h => demanded.add(h.slice(7)));
|
|
109
|
+
});
|
|
110
|
+
await page.evaluateOnNewDocument(() => {
|
|
111
|
+
window.__cspv = [];
|
|
112
|
+
window.__styles = new Map(); // text → origin
|
|
113
|
+
document.addEventListener('securitypolicyviolation', e => window.__cspv.push({ directive: e.violatedDirective, src: (e.sourceFile || '').split('/').pop() }));
|
|
114
|
+
const rec = el => {
|
|
115
|
+
if (!el || el.nodeType !== 1 || el.tagName !== 'STYLE' || window.__styles.has(el.textContent)) return;
|
|
116
|
+
window.__styles.set(el.textContent, el.getAttribute('data-ag-scope') ? 'ag-grid bundle (style-loader)' : el.dataset.agGlobalCss ? 'ag-grid theming api' : 'other');
|
|
117
|
+
};
|
|
118
|
+
new MutationObserver(ms => {
|
|
119
|
+
for (const m of ms) {
|
|
120
|
+
if (m.type === 'characterData') { rec(m.target.parentElement); continue; }
|
|
121
|
+
rec(m.target);
|
|
122
|
+
m.addedNodes.forEach(n => {
|
|
123
|
+
if (n.nodeType === 1) { rec(n); n.querySelectorAll && n.querySelectorAll('style').forEach(rec); }
|
|
124
|
+
else if (n.nodeType === 3) rec(n.parentElement);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}).observe(document, { childList: true, subtree: true, characterData: true });
|
|
128
|
+
});
|
|
129
|
+
await page.goto('http://127.0.0.1:' + PORT + '/tests/' + file, { waitUntil: 'domcontentloaded' });
|
|
130
|
+
try { await page.waitForFunction(() => document.body.dataset.gridsReady === 'true', { timeout: 8000 }); } catch (_) { /* empty-grid pages never set it */ }
|
|
131
|
+
await sleep(500);
|
|
132
|
+
await poke(page);
|
|
133
|
+
await sleep(400);
|
|
134
|
+
const result = await page.evaluate(() => {
|
|
135
|
+
document.querySelectorAll('style').forEach(s => { if (!window.__styles.has(s.textContent)) window.__styles.set(s.textContent, s.dataset.agGlobalCss ? 'ag-grid theming api' : 'other'); });
|
|
136
|
+
const grids = {};
|
|
137
|
+
document.querySelectorAll('dmx-ag-grid').forEach(g => { const w = document.querySelector('#' + g.id + '-grid .ag-root-wrapper'); grids[g.id] = w ? getComputedStyle(w).display : 'n/a'; });
|
|
138
|
+
return { styles: [...window.__styles.entries()], violations: window.__cspv, grids };
|
|
139
|
+
});
|
|
140
|
+
await page.close();
|
|
141
|
+
return { ...result, demanded: [...demanded] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
(async () => {
|
|
145
|
+
const exe = findChrome();
|
|
146
|
+
if (!exe) { console.error('No Chrome executable found — set PUPPETEER_EXECUTABLE_PATH'); process.exit(2); }
|
|
147
|
+
await new Promise(r => server.listen(PORT, '127.0.0.1', r));
|
|
148
|
+
const browser = await puppeteer.launch({ executablePath: exe, headless: 'new', args: ['--no-sandbox', '--disable-dev-shm-usage'] });
|
|
149
|
+
|
|
150
|
+
// 1. Harvest every <style> text the pages ever hold.
|
|
151
|
+
const blocks = new Map(); // hash → { tag, len }
|
|
152
|
+
for (const b of moduleBlocks()) blocks.set(b.hash, { tag: b.tag, len: b.len });
|
|
153
|
+
blocks.set(sha(''), { tag: 'empty <style> (style-loader inserts it empty, then fills it)', len: 0 });
|
|
154
|
+
for (const f of PAGES) {
|
|
155
|
+
for (const [text, tag] of (await load(browser, f, null)).styles) {
|
|
156
|
+
const h = sha(text);
|
|
157
|
+
if (!blocks.has(h) && !pageOwned.has(h)) blocks.set(h, { tag, len: text.length });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [h, b] of blocks) if (b.tag === 'other') console.warn('note: allowing an unattributed <style> block of ' + b.len + ' chars: ' + h);
|
|
161
|
+
|
|
162
|
+
const rank = t => /^dmx-ag-grid/.test(t) ? 0 : /^empty/.test(t) ? 1 : /bundle/.test(t) ? 2 : /theming/.test(t) ? 3 : 4;
|
|
163
|
+
const ordered = [...blocks.entries()].sort((a, b) => rank(a[1].tag) - rank(b[1].tag) || a[0].localeCompare(b[0]));
|
|
164
|
+
const sources = ordered.map(([h]) => "'sha256-" + h + "'");
|
|
165
|
+
const directive = "style-src 'self' " + sources.join(' ');
|
|
166
|
+
const groups = ['dmx-ag-grid.js', 'empty', 'ag-grid bundle', 'ag-grid theming api', 'other'];
|
|
167
|
+
const counts = {};
|
|
168
|
+
for (const [, b] of ordered) counts[groups[rank(b.tag)]] = (counts[groups[rank(b.tag)]] || 0) + 1;
|
|
169
|
+
console.log('# ' + blocks.size + ' hashes ' + JSON.stringify(counts) + ' — also needed: img-src \'self\' data:; font-src \'self\' data:');
|
|
170
|
+
console.log(directive + '\n');
|
|
171
|
+
console.log('# helmet: styleSrc: ' + JSON.stringify(["'self'", ...sources]) + '\n');
|
|
172
|
+
|
|
173
|
+
// 2. Verify: every page under the header, no AG Grid/module violation, every grid laid out.
|
|
174
|
+
let failed = 0;
|
|
175
|
+
if (VERIFY) {
|
|
176
|
+
const header = directive + "; font-src 'self' data:; img-src 'self' data:";
|
|
177
|
+
for (const f of PAGES) {
|
|
178
|
+
const r = await load(browser, f, header);
|
|
179
|
+
const ours = r.violations.filter(v => !/^\d\d-.*\.html$/.test(v.src)); // the pages' own style= / <style> are excluded
|
|
180
|
+
const broken = Object.entries(r.grids).filter(([, d]) => d !== 'flex' && d !== 'n/a').map(([id]) => id);
|
|
181
|
+
const ok = !ours.length && !broken.length;
|
|
182
|
+
if (!ok) failed++;
|
|
183
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + f.padEnd(28) + ' ag-grid/module violations=' + ours.length + ' broken grids=' + JSON.stringify(broken));
|
|
184
|
+
if (ours.length) console.log(' hashes Chrome asked for: ' + r.demanded.join(', '));
|
|
185
|
+
}
|
|
186
|
+
console.log(failed ? '\n' + failed + ' page(s) failed under the hash-only policy' : '\nall ' + PAGES.length + ' pages pass under the hash-only policy');
|
|
187
|
+
}
|
|
188
|
+
await browser.close();
|
|
189
|
+
server.close();
|
|
190
|
+
process.exit(failed ? 1 : 0);
|
|
191
|
+
})().catch(e => { console.error(e); process.exit(1); });
|