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
71 changes: 47 additions & 24 deletions cypress/e2e/query_error_structured_spec.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,20 @@ describe('Query error display — structured fields', () => {
})
}

it('exposes structured fields in pill title and panel body', () => {
// Type error — guaranteed to come back with code, line, column, description
it('surfaces a real failing query as an error pill with the cause', () => {
// Version-robust: works whether the bed's existdb-openapi returns the
// #71 QueryError shape or the current-release generic { error }. The
// structured-panel detail and the message/raw split are asserted
// deterministically by the cy.intercept test below.
setEditorContent('1 + "oops"')
cy.get('#run').click()

// Wait until the error pill is set
cy.get('#exide-err-pill.has-error', { timeout: 10000 }).should('exist')

// (1) Pill carries a `title` attribute with the structured dump
cy.get('#exide-err-pill')
.should('have.attr', 'title')
.and('include', 'Code:')
.and('include', 'err:XPTY0004')
.and('include', 'Location:')
.and('include', 'line 1')
.and('include', 'Description:')

// (2) The auto-opened panel body shows the structured rows.
cy.get('#exide-err-panel-body.ep-structured').should('exist')
cy.get('#exide-err-panel-body .ep-field-code .ep-field-value')
.should('contain.text', 'err:XPTY0004')
cy.get('#exide-err-panel-body .ep-field-loc .ep-field-value')
.should('contain.text', 'line 1')
.and('contain.text', 'column 3')
cy.get('#exide-err-panel-body .ep-field-desc .ep-field-value')
.should('contain.text', 'type error')

// (3) Pill label is still concise — shouldn't dump the whole thing.
// The pill shows the cause, concisely.
cy.get('#exide-err-pill-label')
.invoke('text')
.then((label) => {
expect(label.trim().length).to.be.greaterThan(0)
expect(label.length).to.be.at.most(60)
})

Expand All @@ -71,6 +54,46 @@ describe('Query error display — structured fields', () => {
cy.get('#exide-err-pill').should('not.have.class', 'has-error')
})

it('prefers message over raw and exposes raw in the hover dump (existdb-openapi#71 envelope)', () => {
// Stub a #71 envelope so this is deterministic regardless of the bed's
// existdb-openapi version: { code, message, line, column, raw }, HTTP 400.
// Exercises the full client path: runQueryCursor's coalesce →
// editor.evalError → the structured panel.
cy.intercept('POST', '**/existdb-openapi/api/query', {
statusCode: 400,
body: {
code: 'err:XPTY0004',
message: "'xs:string(oops)' can not be an operand for +",
line: 1,
column: 3,
raw: 'It is a type error if, during the static analysis phase, an expression is found to have a static type that is not appropriate.'
}
}).as('q71')

setEditorContent('1 + "oops"')
cy.get('#run').click()
cy.wait('@q71')

cy.get('#exide-err-pill.has-error', { timeout: 10000 }).should('exist')

// Panel Description shows the *concise* message, not the verbose raw boilerplate.
cy.get('#exide-err-panel-body .ep-field-desc .ep-field-value')
.should('contain.text', 'can not be an operand')
.and('not.contain.text', 'It is a type error if')
// Code and location still surface.
cy.get('#exide-err-panel-body .ep-field-code .ep-field-value').should('contain.text', 'err:XPTY0004')
cy.get('#exide-err-panel-body .ep-field-loc .ep-field-value')
.should('contain.text', 'line 1').and('contain.text', 'column 3')
// The verbose detail is preserved under Raw in the hover dump.
cy.get('#exide-err-pill')
.should('have.attr', 'title')
.and('include', 'Raw:')
.and('include', 'It is a type error if')

cy.get('#exide-err-panel-dismiss').click()
cy.get('#exide-err-pill').should('not.have.class', 'has-error')
})

it('falls back gracefully when the error has no structured payload', () => {
// Force a non-structured error: hit an endpoint that returns HTML/text
// by invoking validator-style flow. The simplest deterministic way is
Expand Down
49 changes: 23 additions & 26 deletions src/eXide.js
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,21 @@ eXide.app = (function(util) {
if (timingEl) timingEl.style.display = "none";
}

// Build a display message from a query-error envelope. existdb-openapi#71
// returns the QueryError shape { code, message, line, column, raw };
// current releases (≤ v0.9.7) return a generic { error: "..." }. Prefer
// the concise `message`, then `error`, then the verbose `raw`; fall back
// to a serialized payload so the cause is never silently lost.
function queryErrorMessage(err, fallback) {
if (err == null) { return fallback || "Query failed."; }
if (typeof err === "string") { return err; }
var msg = err.message || err.error || err.raw
|| fallback || JSON.stringify(err);
if (err.code) { msg = "[" + err.code + "] " + msg; }
if (err.line > 0) { msg = "line " + err.line + ": " + msg; }
return msg;
}

// Close previous cursor if any
if (app._cursorId) {
fetch("../existdb-openapi/api/query/" + app._cursorId, { method: "DELETE" }).catch(function() {});
Expand Down Expand Up @@ -936,27 +951,11 @@ eXide.app = (function(util) {
hideCancel();
if (!response.ok) {
return response.json().then(function(err) {
// existdb-openapi/cursor:eval errors come back as
// { code, description, line, column, module, value }
// (the standard XPathException → JSON shape). Older
// code paths used { error: "..." } or { message: "..." }.
// Coalesce so the user sees the real cause regardless
// of which shape the server returns; fall back to a
// serialized payload so the user can still copy/paste
// the response if all known fields are missing.
var msg = err.description || err.error || err.message
|| (typeof err === "string" ? err : JSON.stringify(err));
if (err.code) {
msg = "[" + err.code + "] " + msg;
}
if (err.line > 0) {
msg = "line " + err.line + ": " + msg;
}
// Pass the full structured error so the panel can
// surface code/line/column/module/value separately
// (request from @line-o on PR #794: the description
// alone isn't enough — need all the info).
editor.evalError(msg, !livePreview, err);
// Pass the full structured error so the panel can surface
// code/location/message/raw separately (request from
// @line-o on PR #794: the message alone isn't enough —
// need all the info).
editor.evalError(queryErrorMessage(err), !livePreview, err);
}, function () {
// Body wasn't JSON — try to surface whatever the
// server actually said (HTTP status + body text).
Expand All @@ -978,11 +977,9 @@ eXide.app = (function(util) {
// success path shows neither results nor an error. Coalesce the
// shapes the same way the !response.ok branch does.
if (data.error || !data.cursor) {
var emsg = data.description || data.error || data.message
|| "Query failed: no cursor returned.";
if (data.code) { emsg = "[" + data.code + "] " + emsg; }
if (data.line > 0) { emsg = "line " + data.line + ": " + emsg; }
editor.evalError(emsg, !livePreview, data);
editor.evalError(
queryErrorMessage(data, "Query failed: no cursor returned."),
!livePreview, data);
return;
}
app._cursorId = data.cursor;
Expand Down
61 changes: 35 additions & 26 deletions src/error-status-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
(function () {
'use strict';

function init() {
// ── Helpers ──────────────────────────────────────────────────────────────
/**
* Produce a short human-readable label from eXide's raw error string.
Expand Down Expand Up @@ -53,11 +52,12 @@
return s;
}
/**
* Format a structured error object (from existdb-openapi:
* { code, description, line, column, module, value }) as a multi-field
* panel body. Renders only the fields that are present so we don't
* surface noise like "module: null" or empty values. Falls back to
* formatPanelHtml(raw) when no structured data is available.
* Format a structured error object as a multi-field panel body. The query
* error envelope is existdb-openapi#71's { code, message, line, column, raw }
* (the `QueryError` schema in its api.json). Current existdb-openapi releases
* (\u2264 v0.9.7) instead return a generic { error: "..." }; for that \u2014 or any
* payload without recognizable structured fields \u2014 fall back to the
* plain-text formatter so the panel is never blank.
*/
function formatStructuredPanelHtml(errObj, raw) {
if (!errObj) return formatPanelHtml(raw);
Expand All @@ -78,16 +78,10 @@
if (errObj.column) loc += ', column ' + errObj.column;
}
if (loc) row('Location', loc, 'loc');
row('Description', errObj.description, 'desc');
if (errObj.module && errObj.module !== 'unknown' && !/^String\//.test(errObj.module)) {
// Skip synthetic module IDs from compile errors on inline source
// (e.g. "String/-3990248984871632423") \u2014 they're internal noise
// and confuse users; keep real module paths.
row('Module', errObj.module, 'module');
}
if (errObj.value !== null && errObj.value !== undefined && errObj.value !== '') {
row('Value', errObj.value, 'value');
}
row('Description', errObj.message, 'desc');
// Generic { error } (or anything without structured fields): show the
// plain text rather than an empty panel.
if (!rows.length) return formatPanelHtml(errObj.error || raw);
return rows.join('');
}
/**
Expand All @@ -105,13 +99,16 @@
if (errObj.column) l += ', column ' + errObj.column;
lines.push(l);
}
if (errObj.description) lines.push('Description: ' + errObj.description);
if (errObj.module) lines.push('Module: ' + errObj.module);
if (errObj.value !== null && errObj.value !== undefined && errObj.value !== '') {
lines.push('Value: ' + errObj.value);
if (errObj.message) lines.push('Description: ' + errObj.message);
// #71's `raw` is the full boilerplate behind the concise message; expose
// it in the hover dump when it adds detail beyond the message shown.
if (errObj.raw && errObj.raw !== errObj.message) {
lines.push('Raw: ' + errObj.raw);
}
return lines.length ? lines.join('\n') : raw;
return lines.length ? lines.join('\n') : (errObj.error || raw);
}

function init() {
// ── Element references ───────────────────────────────────────────────────
var errSource = document.getElementById('error-status'); // eXide writes here
var pill = document.getElementById('exide-err-pill');
Expand Down Expand Up @@ -265,10 +262,22 @@
}
} // end init()

// Script loads in <head>, so defer until DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
// Script loads in <head>, so defer until DOM is ready. Guarded so the
// module can be required in Node (no document) to unit-test the pure
// formatters exported below.
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = {
formatStructuredPanelHtml: formatStructuredPanelHtml,
formatTitleDump: formatTitleDump,
makeShortLabel: makeShortLabel
};
}
})();
67 changes: 67 additions & 0 deletions test/error-status-ui-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Tests for the error panel formatters in src/error-status-ui.js.
*
* Covers the existdb-openapi error-envelope parity: the panel/hover dump must
* prefer the concise `message` (existdb-openapi#71) over the verbose `raw`,
* while staying compatible with the older { code, description, module, value }
* shape and the oldest { error }-only / unstructured shapes.
*
* The envelope samples below are the real responses captured from running
* `1 + "oops"` against existdb-openapi at three versions.
*/
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { formatStructuredPanelHtml, formatTitleDump, makeShortLabel } =
require("../src/error-status-ui.js");

// existdb-openapi#71 envelope (HTTP 400): concise message + raw boilerplate.
const E71 = {
code: "err:XPTY0004",
message: "'xs:string(oops)' can not be an operand for +",
line: 1,
column: 3,
raw: "It is a type error if, during the static analysis phase, an expression is found to have a static type that is not appropriate."
};

// Current existdb-openapi release (≤ v0.9.7): a generic { error } string, no
// code/line/message. #71's clean QueryError shape is not yet released.
const EGENERIC = { error: "Invalid context-item: 'xs:string(oops)' can not be an operand for +" };

describe("error-status-ui formatters — envelope parity", () => {

it("#71: panel shows the concise message, not the raw boilerplate", () => {
const html = formatStructuredPanelHtml(E71, "");
assert.match(html, /err:XPTY0004/);
assert.match(html, /line 1, column 3/);
assert.match(html, /can not be an operand/); // the message
assert.doesNotMatch(html, /It is a type error if/); // boilerplate stays out of the panel
});

it("#71: hover dump exposes the raw boilerplate under Raw", () => {
const dump = formatTitleDump(E71, "");
assert.match(dump, /Code:\s+err:XPTY0004/);
assert.match(dump, /Location:\s+line 1, column 3/);
assert.match(dump, /Description: .*can not be an operand/);
assert.match(dump, /Raw:\s+It is a type error if/); // verbose detail preserved
});

it("generic { error } (current release) falls back to plain text, never blank", () => {
const html = formatStructuredPanelHtml(EGENERIC, "");
assert.notEqual(html, ""); // not an empty panel
assert.match(html, /can not be an operand/); // the error text is shown
const dump = formatTitleDump(EGENERIC, "");
assert.match(dump, /can not be an operand/);
assert.doesNotMatch(dump, /Raw:/); // no raw field on this shape
});

it("no structured object falls back to the raw-text formatter", () => {
const html = formatStructuredPanelHtml(null, "Cannot compile xquery: boom");
assert.match(html, /boom/);
assert.equal(formatTitleDump(null, "rawonly"), "rawonly");
});

it("makeShortLabel strips boilerplate and location", () => {
const label = makeShortLabel("err:XPST0017 Call to undeclared function: local:foo [at line 3, column 5]");
assert.equal(label, "Call to undeclared function: local:foo");
});
});
Loading