fix(mocks): keep a leading blank, and round a date the way the text branch does
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m14s
Build / Build-and-test-development (pull_request) Canceled after 1m16s

Three findings from review.

- viewdata trimmed both ends of a character cell. A leading blank is part of a
  SAS character value and the viewer renders it; only the trailing blanks SAS
  pads to are noise, so the cell is trimmed at the end and an all-blank cell is
  still SAS missing. viewdata-cells.test.js fails on the previous version.
- viewDate floored a stored day count while the ISO branch rounded, so the same
  instant could land on different days depending on how the row carried it. Both
  round now.
- the two label members stay different. The finding was that cols[].LABEL uses
  the data dictionary business label while $viewdata.vars[.].label uses the
  physical one; the recorded estate response does exactly that - cols DSN is
  "Dataset Name to be edited", $viewdata.vars.DSN.label is "DSN" - so unifying
  them would break parity. The comment says so, and a test asserts the split
  against the recorded answer so the next reader does not tidy it away.
This commit is contained in:
dc committed 2026-11-07 09:19:49 +00:00
1 parent 40550ca56b
commit 4b71cf5b03
2 files changed
+73 -3

No files matched your search

+18 -3
View File
@@ -51,7 +51,9 @@ function viewDateTime(seconds) {
}
function viewDate(seconds) {
const d = new Date(SAS_EPOCH_MS + Math.floor(seconds) * 86400000)
// whole days, rounded as the text branch rounds, so a stored number and a
// stored ISO string of the same instant land on the same day
const d = new Date(SAS_EPOCH_MS + Math.round(seconds) * 86400000)
// DATE9. is four digits wide, so a year below 1000 is zero-padded
return pad2(d.getUTCDate()) + VIEW_MONTHS[d.getUTCMonth()] + pad4(d.getUTCFullYear())
}
@@ -282,9 +284,15 @@ if (tableData && tableData.columns) {
if (val === undefined || val === null || val === '') {
row[col.name] = col.type === 'N' ? '.' : ' '
} else {
/*
Only the trailing blanks are noise - the width SAS pads to.
A leading blank is part of the value and the viewer shows it,
so the text is trimmed at the end only.
*/
const text = String(val).replace(/\s+$/, '')
row[col.name] = typeof val === 'number'
? String(val)
: (String(val).trim() === '' ? ' ' : String(val).trim())
? String(val)
: (text === '' ? ' ' : text)
}
}
return row
@@ -310,6 +318,13 @@ if (tableData && tableData.columns) {
bytes, DATETIME19.3. sasVarsEntry describes the *edit grid*, where the
temporal columns have been converted to character, so it is not used here.
*/
/*
The two label members are deliberately different, and the recorded
response proves it: cols[].LABEL for DSN is "Dataset Name to be edited"
(the data dictionary's business label, which viewdata.sas augments in as
it does for getdata) while $viewdata.vars.DSN.label is "DSN", the physical
label. Unifying them would look tidier and would stop matching the estate.
*/
vars = {}
for (const col of physicalColumns) {
const isTemporal = ['DATE', 'DATETIME', 'TIME'].indexOf(getDdType(col)) >= 0
+55
View File
@@ -0,0 +1,55 @@
/**
* Character cells and labels in the viewer's payload.
*
* Two rules the recorded response fixes, both of which look like bugs from the
* inside and are parity from the outside.
*/
const test = require('node:test')
const assert = require('node:assert')
const fs = require('node:fs')
const path = require('node:path')
const { seededEstate, runService } = require('./harness')
function viewOf(estate, libds) {
const body = JSON.parse(
runService(estate, 'public/viewdata', {
tableName: 'SASControlTable',
rows: [{ LIBDS: libds, FILTER_RK: -1 }]
})
)
assert.equal(body.sasjsAbort, undefined)
return body
}
test('a character cell keeps a leading blank and loses only the padding', () => {
const estate = seededEstate({ demo: false })
const table = {
columns: [{ name: 'C', type: 'char', length: 10 }],
rows: [{ C: ' ABC' }, { C: 'ABC ' }, { C: ' ' }]
}
fs.writeFileSync(
path.join(estate.appDir, 'data', 'DC_JSLIB', 'mpe_x_cells.json'),
JSON.stringify(table)
)
const rows = viewOf(estate, 'DC_JSLIB.MPE_X_CELLS').viewdata
// a leading blank is part of the value, and the viewer shows it
assert.equal(rows[0].C, ' ABC')
// trailing blanks are the width SAS pads to
assert.equal(rows[1].C, 'ABC')
// an all-blank cell is SAS missing
assert.equal(rows[2].C, ' ')
})
test('cols and $viewdata report the labels the estate reports', () => {
// the recorded response has cols[].LABEL for DSN as the data dictionary's
// business label and $viewdata.vars.DSN.label as the physical label - two
// members, two answers, and the estate is the authority
const body = viewOf(seededEstate({ demo: false }), 'DC_JSLIB.MPE_TABLES')
const dsn = body.cols.find((c) => c.NAME === 'DSN')
assert.equal(dsn.LABEL, 'Dataset Name to be edited')
assert.equal(body.$viewdata.vars.DSN.label, 'DSN')
})