Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,59 @@
*/

import 'sprintf-js'
// Match any whitespace control character we want to make visible in
// single-line displays. Kept as a module-level constant so the regex is
// compiled once and reused across every formatValueBase call.
const WHITESPACE_CONTROL_REGEX = /[\n\r\t]/
const WHITESPACE_CONTROL_REGEX_G = /[\n\r\t]/g
export default {
methods: {
formatValueBase(value, formatString) {
formatValueBase(value, formatString, options) {
if (this.isJsonString(value)) {
return this.formatJsonString(value)
}
const preserveWhitespace = options?.preserveWhitespace === true
if (Array.isArray(value)) {
return JSON.stringify(value).replace(/\\n/g, '')
return this.escapeWhitespace(JSON.stringify(value), preserveWhitespace)
}
if (this.isObject(value)) {
return JSON.stringify(value).replace(/\\n/g, '')
return this.escapeWhitespace(JSON.stringify(value), preserveWhitespace)
}
if (formatString && value) {
if (typeof value === 'bigint') {
return this.formatBigInt(value, formatString)
}
return sprintf(formatString, value)
return this.escapeWhitespace(
sprintf(formatString, value),
preserveWhitespace,
)
}
if (value === null || value === undefined) {
return 'null'
}
if (typeof value === 'string') {
return this.escapeWhitespace(value, preserveWhitespace)
}
return String(value)
},
// Make embedded newlines, carriage returns, and tabs visible in
// single-line displays (e.g. Packet Viewer) by replacing them with their
// escape sequences. Pass preserveWhitespace=true to return the value
// unchanged for multi-line displays such as the TEXTBOX widget.
escapeWhitespace(value, preserveWhitespace) {
if (preserveWhitespace || typeof value !== 'string') {
return value
}
// Cheap test first so we only allocate a new string when needed.
if (!WHITESPACE_CONTROL_REGEX.test(value)) {
return value
}
return value.replace(WHITESPACE_CONTROL_REGEX_G, (m) => {
if (m === '\n') return String.raw`\n`
if (m === '\r') return String.raw`\r`
return String.raw`\t`
})
},
// sprintf-js doesn't support BigInt values so we handle common
// integer format specifiers manually to preserve full precision
formatBigInt(value, formatString) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export default {
return {
width: 200,
height: 200,
// Let v-textarea render embedded newlines as actual line breaks
// rather than the escaped "\n" sequence used by single-line displays.
preserveWhitespace: true,
}
},
computed: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export default {
emits: ['addItem', 'deleteItem', 'open'],
data() {
return {
// Subclasses (e.g. TextboxWidget) set this true to let embedded
// newlines / tabs pass through to multi-line displays unchanged.
preserveWhitespace: false,
appliedTimeZone: 'local',
curValue: null,
prevValue: null,
Expand Down Expand Up @@ -214,7 +217,9 @@ export default {
this.appliedTimeZone,
)
}
return this.formatValueBase(value, this.formatString)
return this.formatValueBase(value, this.formatString, {
preserveWhitespace: this.preserveWhitespace,
})
} catch (e) {
// eslint-disable-next-line no-console
console.log(
Expand Down
52 changes: 52 additions & 0 deletions playwright/tests/packet-viewer.p.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,58 @@ test('gets details with right click', async ({ page, utils }) => {
)
})

// Regression for openc3#154: newlines embedded in a string telemetry value
// must be visible in the single-line Packet Viewer cell (escaped as \n) and
// not silently truncate everything after the first newline.
test('displays embedded newlines in string telemetry values', async ({
page,
utils,
}) => {
// 1) Override INST HEALTH_STATUS ASCIICMD with a multi-line string. We do
// this via Script Runner because the cmd-tlm API does not expose
// override_tlm directly over HTTP.
await page.goto('/tools/scriptrunner')
await expect(page.locator('.v-app-bar')).toContainText('Script Runner')
await page.locator('[data-test=script-runner-file]').click()
await page.locator('text=New File').click()
await expect(page.locator('textarea')).toHaveText('')
await page
.locator('textarea')
.fill(
String.raw`override_tlm("INST HEALTH_STATUS ASCIICMD = \"line1\nline2\nline3\"")`,
)
await page.locator('[data-test=start-button]').click()
await expect(page.locator('[data-test=state] input')).toHaveValue(
'completed',
{ timeout: 30000 },
)

// 2) Verify the Packet Viewer renders the embedded newlines as a literal
// `\n` sequence rather than chopping at the first newline.
await page.goto('/tools/packetviewer/INST/HEALTH_STATUS/')
await expect(page.locator('.v-app-bar')).toContainText('Packet Viewer')
await expect
.poll(
async () =>
await page.inputValue('tr:has(td div:text-is("ASCIICMD")) input'),
{ timeout: 10000 },
)
.toBe(String.raw`line1\nline2\nline3`)

// 3) Clear the override so we don't leak state into other tests.
await page.goto('/tools/scriptrunner')
await page.locator('[data-test=script-runner-file]').click()
await page.locator('text=New File').click()
await page
.locator('textarea')
.fill(`normalize_tlm("INST HEALTH_STATUS ASCIICMD")`)
await page.locator('[data-test=start-button]').click()
await expect(page.locator('[data-test=state] input')).toHaveValue(
'completed',
{ timeout: 30000 },
)
})

test('stops posting to the api after closing', async ({ page, utils }) => {
await page.goto('/tools/packetviewer/INST/ADCS/')
let requestCount = 0
Expand Down