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
119 changes: 119 additions & 0 deletions cypress/e2e/xml_validate_spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
describe('XML validation (api/editor/validate)', () => {
beforeEach(() => {
cy.loginXHR('admin', '')
})

const validate = (xml) =>
cy.request({
method: 'POST',
url: '/eXide/api/editor/validate',
headers: {
'Content-Type': 'application/xml',
Accept: 'application/json'
},
body: xml
})

it('accepts a valid collection.xconf via native schema when available', () => {
const xml = `<?xml version="1.0"?>
<collection xmlns="http://exist-db.org/collection-config/1.0">
<index/>
</collection>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('schema', 'collection.xconf.xsd')
// Docker/CI with exist#6528 ships $EXIST_HOME/schema/; assert native path.
expect(res.body.grammar).to.equal('native')
})
})

it('rejects an invalid collection.xconf', () => {
// Empty collection fails the XSD 1.1 assert that at least one of
// index / triggers / validation must be present.
const xml = `<?xml version="1.0"?>
<collection xmlns="http://exist-db.org/collection-config/1.0"/>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'invalid')
expect(res.body).to.have.property('schema', 'collection.xconf.xsd')
expect(res.body.errors).to.be.an('array').that.is.not.empty
expect(res.body.errors[0].message).to.match(/assert|count\(\*\)/i)
})
})

it('honours xsi:schemaLocation for collection.xconf', () => {
const xml = `<?xml version="1.0"?>
<collection xmlns="http://exist-db.org/collection-config/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://exist-db.org/collection-config/1.0 collection.xconf.xsd">
<index/>
</collection>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('schema', 'collection.xconf.xsd')
expect(res.body.grammar).to.be.oneOf(['native', 'bundled'])
})
})

it('validates expath-pkg.xml', () => {
const xml = `<?xml version="1.0"?>
<package xmlns="http://expath.org/ns/pkg"
name="http://example.org/test-pkg"
abbrev="test-pkg"
version="1.0.0"
spec="1.0">
<title>Test package</title>
</package>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('schema', 'expath-pkg.xsd')
expect(res.body.grammar).to.equal('native')
})
})

it('validates no-namespace mime-types.xml', () => {
const xml = `<?xml version="1.0"?>
<mime-types>
<mime-type name="application/xml" type="xml">
<description>XML</description>
<extensions>.xml</extensions>
</mime-type>
</mime-types>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('schema', 'mime-types.xsd')
expect(res.body.grammar).to.equal('native')
})
})

it('rejects invalid no-namespace mime-types.xml', () => {
const xml = `<?xml version="1.0"?>
<mime-types>
<mime-type name="not-a-mime" type="xml">
<description>bad</description>
<extensions>.xml</extensions>
</mime-type>
</mime-types>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'invalid')
expect(res.body).to.have.property('schema', 'mime-types.xsd')
expect(res.body.errors).to.be.an('array').that.is.not.empty
})
})

it('marks plain XML without a known grammar as well-formed only', () => {
const xml = `<?xml version="1.0"?><note><text>hi</text></note>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('grammar', 'none')
})
})

it('does not force-match other documents in the shared eXist namespace', () => {
const xml = `<?xml version="1.0"?>
<query xmlns="http://exist.sourceforge.net/NS/exist"><text>1</text></query>`
validate(xml).then((res) => {
expect(res.body).to.have.property('status', 'valid')
expect(res.body).to.have.property('grammar', 'none')
})
})
})
62 changes: 62 additions & 0 deletions cypress/e2e/xml_validation_status_spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
describe('XML validation status UX', () => {
beforeEach(() => {
cy.loginXHR('admin', '')
cy.visit('/eXide/index.html', {
onBeforeLoad(win) { win.localStorage.setItem('eXide.firstTime', '0') }
})
cy.dismissDialog()
})

function openNewXMLFile() {
cy.get('#new').click()
cy.get('#dialog-templates').closest('.eXide-dialog', { timeout: 10000 }).should('have.attr', 'open')
cy.get('select.type-select').select('XML')
cy.get('#dialog-templates').closest('.eXide-dialog')
.find('.eXide-dialog-buttons button').contains('Create').click()
}

function validateNow(text) {
cy.window().then((win) => {
const editor = win.eXide.app.getEditor()
const doc = editor.getActiveDocument()
doc.setText(text)
doc.lastValidation = 0
return editor.validator.triggerNow(doc)
})
}

it('shows Validated against for collection.xconf without an error pill', () => {
openNewXMLFile()
validateNow(`<?xml version="1.0"?>
<collection xmlns="http://exist-db.org/collection-config/1.0">
<index/>
</collection>`)
cy.get('#exide-err-pill').should('not.have.class', 'has-error')
cy.get('#validation-status', { timeout: 10000 }).should('be.visible')
cy.get('#validation-status-label')
.invoke('text')
.should('match', /Validated against collection\.xconf\.xsd/i)
cy.get('#status-bar').screenshot('validation-status-schema-ok')
})

it('shows Well-formed only without an error pill', () => {
openNewXMLFile()
validateNow('<?xml version="1.0"?><note><text>hi</text></note>')
cy.get('#exide-err-pill').should('not.have.class', 'has-error')
cy.get('#validation-status', { timeout: 10000 }).should('be.visible')
cy.get('#validation-status-label')
.invoke('text')
.should('match', /Well-formed only/i)
cy.get('#status-bar').screenshot('validation-status-wellformed-only')
})

it('shows schema errors on the error pill and clears validation status', () => {
openNewXMLFile()
validateNow(`<?xml version="1.0"?>
<collection xmlns="http://exist-db.org/collection-config/1.0"/>`)
cy.get('#validation-status').should('not.be.visible')
cy.get('#exide-err-pill').should('have.class', 'has-error')
cy.get('#error-status').invoke('text').should('match', /assert|count/i)
cy.get('#status-bar').screenshot('validation-status-schema-error')
})
})
12 changes: 11 additions & 1 deletion cypress/e2e/xml_wellformed_spec.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,23 @@ describe('XML well-formedness reporting', () => {
.and('match', /oops|well-formed|terminated|error/i)
})

it('clears the error once the document is well-formed again', () => {
it('replaces the error with a well-formed status once the document is fixed', () => {
openNewXMLFile()

validateNow('<collection>\n <index>\n <oops>\n</collection>')
cy.get('#error-status', { timeout: 10000 }).invoke('text').should('not.be.empty')
cy.get('#exide-err-pill').should('have.class', 'has-error')

// Success uses the calm #validation-status segment, not the error pill.
// No matching grammar → "Well-formed only"; schema hits → "Validated against …".
validateNow('<collection>\n <index/>\n</collection>')
cy.get('#error-status', { timeout: 10000 }).invoke('text').should('be.empty')
cy.get('#exide-err-pill').should('not.have.class', 'has-error')
// Assert text on the element itself — .should('not.have.attr') yields
// undefined, so chaining .invoke('text') after it is unsafe.
cy.get('#validation-status', { timeout: 10000 }).should('be.visible')
cy.get('#validation-status-label')
.invoke('text')
.should('match', /Well-formed only/i)
})
})
5 changes: 5 additions & 0 deletions index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@
<span id="status-cursor">Ln 1, Col 1</span>
<span class="status-separator" id="status-scope-sep" style="display:none">|</span>
<span id="status-scope" style="display:none"></span>
<span class="status-separator" id="validation-status-sep" hidden="">|</span>
<span id="validation-status" class="validation-status" hidden="" aria-live="polite">
<span class="fa fa-check-circle" aria-hidden="true"></span>
<span id="validation-status-label"></span>
</span>
<a href="#" id="error-status"></a>
<span id="exide-err-pill" role="button" tabindex="0" aria-label="Error details">
<span class="fa fa-exclamation-circle"></span>
Expand Down
2 changes: 1 addition & 1 deletion modules/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@
},
"/api/editor/validate": {
"post": {
"summary": "Validate XML against schema catalog",
"summary": "Validate XML against native eXist grammars when available (XSD 1.1)",
"operationId": "editor:validate",
"tags": [
"editor"
Expand Down
Loading
Loading