@sungen/driver-ui 3.2.14 → 3.2.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-ui",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Sungen UI capability (Playwright) — step patterns, viewpoint gate, and phase hooks. Plugs into @sun-asterisk/sungen via the capability SPI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "author": "eqe team (engineer & quality) — Sun Asterisk",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@sun-asterisk/sungen": "3.2.14"
16
+ "@sun-asterisk/sungen": "3.2.15"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@playwright/test": "^1.57.0"
@@ -136,11 +136,35 @@ export const tablePatterns: StepPattern[] = [
136
136
  );
137
137
  const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
138
138
 
139
+ // Field report: the row label used to be DECORATIVE (comment-only) — a
140
+ // positional intent like `[First Contact Row]` silently degraded to an
141
+ // anywhere-in-the-table existence check, so ordering rules never failed.
142
+ // Now the label follows the same "resolve via selectors.yaml, fall back
143
+ // when absent" convention as every other [Ref]: a `type: locator` entry
144
+ // (RELATIVE to the table, e.g. `tbody tr:first-child`) positions the row,
145
+ // and the assertion becomes toContainText on that exact row. No entry →
146
+ // the historical filter-by-value behavior, byte-identical.
147
+ let rowSelector: string | undefined;
148
+ if (rowLabel && rowLabel !== tableName) {
149
+ try {
150
+ const rowResolved = context.selectorResolver.resolveSelector(
151
+ rowLabel, context.featureName, 'row', 0
152
+ );
153
+ if (rowResolved.strategy === 'locator' && rowResolved.value && rowResolved.value.trim()) {
154
+ rowSelector = rowResolved.value;
155
+ }
156
+ } catch {
157
+ // no selectors.yaml entry for the row label — display-only label (fallback)
158
+ }
159
+ }
160
+
139
161
  const isGiven = isGivenContext(context);
140
162
  return {
141
163
  templateName: 'table-row-exists',
142
- data: { ...resolved, filterValue, isGiven },
143
- comment: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table with ${step.dataRef}`,
164
+ data: { ...resolved, filterValue, isGiven, rowSelector },
165
+ comment: rowSelector
166
+ ? `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} (positioned row) in ${tableName} table contains ${step.dataRef}`
167
+ : `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table with ${step.dataRef}`,
144
168
  };
145
169
  },
146
170
  priority: 17,
@@ -310,27 +334,55 @@ export const tablePatterns: StepPattern[] = [
310
334
 
311
335
  const dataTable = step.dataTable!;
312
336
 
313
- // Resolve {{variable}} references in cell values
314
- const resolvedRows = dataTable.rows.map(row => ({
315
- cells: row.cells.map(cell => {
316
- const varMatch = cell.match(/^\{\{([a-z0-9\-\.\_]+)\}\}$/i);
317
- if (varMatch) {
318
- return context.dataResolver.resolveData(varMatch[1], context.featureName);
319
- }
320
- return cell;
321
- }),
322
- }));
323
-
324
- // Pre-compute filter-based assertion lines
325
- // For each expected row: chain .filter({ hasText }) for all cell values, then assert visible
337
+ // Resolve {{variable}} references in a cell (headers included — i18n headers are legal)
338
+ const resolveCell = (cell: string): string => {
339
+ const varMatch = cell.match(/^\{\{([a-z0-9\-\.\_]+)\}\}$/i);
340
+ if (varMatch) {
341
+ return context.dataResolver.resolveData(varMatch[1], context.featureName);
342
+ }
343
+ return cell;
344
+ };
345
+ const headers = dataTable.headers.map(resolveCell);
346
+ const resolvedRows = dataTable.rows.map(row => ({ cells: row.cells.map(resolveCell) }));
347
+ const esc = (s: string) => s.replace(/'/g, "\\'");
348
+
349
+ // Header → cell mapping (field report: the header row was parsed and then IGNORED —
350
+ // rows were asserted as chained row-level hasText filters, so column-swapped data and
351
+ // duplicate values like '-' still passed). Now:
352
+ // 1. span guard — rowspan/colspan shifts ordinal mapping; fail loud instead of
353
+ // asserting the wrong cells (span support is a documented follow-up);
354
+ // 2. resolve each DataTable header to its DOM column index AT RUNTIME by header text
355
+ // (exact after trim, else a UNIQUE contains-match to tolerate sort icons); a
356
+ // missing/ambiguous header fails with the actual header row in the message —
357
+ // robust to extra DOM columns (leading checkbox…) that break compile-time nth;
358
+ // 3. locate each expected row by the chained co-occurrence filters (kept — resilient
359
+ // to row order and extra rows), then prove EVERY cell under its own header with
360
+ // an exact toHaveText. `td, th` ordinals count all DOM cells on both the header
361
+ // and body sides, so indexes stay aligned even with hidden columns.
326
362
  const assertions: string[] = [];
363
+ assertions.push(
364
+ `await expect(table.locator('[rowspan]:not([rowspan="1"]), [colspan]:not([colspan="1"])'), 'table match data: rowspan/colspan tables are not supported yet — header-to-cell mapping would be wrong').toHaveCount(0);`
365
+ );
366
+ assertions.push(
367
+ `const __headers = (await table.locator('thead tr').first().locator('th, td').allTextContents()).map((t) => t.trim());`
368
+ );
369
+ assertions.push(
370
+ `const __colIdx = (h: string): number => { const i = __headers.indexOf(h); if (i !== -1) return i; const c = __headers.filter((t) => t.includes(h)); if (c.length === 1) return __headers.indexOf(c[0]); throw new Error(\`table match data: header "\${h}" \${c.length ? 'is ambiguous' : 'not found'} in table header row [\${__headers.join(' | ')}]\`); };`
371
+ );
327
372
  for (const row of resolvedRows) {
328
373
  const filters = row.cells
329
- .map(cell => `.filter({ hasText: '${cell.replace(/'/g, "\\'")}' })`)
374
+ .map(cell => `.filter({ hasText: '${esc(cell)}' })`)
330
375
  .join('');
331
- assertions.push(
332
- `await expect(rows${filters}).toBeVisible();`
333
- );
376
+ assertions.push(`{`);
377
+ assertions.push(` const row = rows${filters};`);
378
+ row.cells.forEach((cell, i) => {
379
+ const header = headers[i];
380
+ if (header === undefined) return;
381
+ assertions.push(
382
+ ` await expect(row.locator('td, th').nth(__colIdx('${esc(header)}'))).toHaveText('${esc(cell)}');`
383
+ );
384
+ });
385
+ assertions.push(`}`);
334
386
  }
335
387
 
336
388
  const isGiven = isGivenContext(context);