diff --git a/cypress/e2e/diagnostics_panel_spec.cy.js b/cypress/e2e/diagnostics_panel_spec.cy.js new file mode 100644 index 00000000..b1255605 --- /dev/null +++ b/cypress/e2e/diagnostics_panel_spec.cy.js @@ -0,0 +1,88 @@ +describe('Diagnostics panel', () => { + beforeEach(() => { + cy.intercept('POST', '**/api/query/compile').as('compile') + cy.loginXHR('admin', '') + cy.visit('/eXide/index.html') + cy.reload(true) + cy.get('.path', { timeout: 10000 }).should('contain', 'untitled-1') + cy.get('#user', { timeout: 15000 }).should('not.have.text', 'Login') + // Wait for initial compile to settle + cy.wait('@compile', { timeout: 10000 }) + }) + + function setEditorContent(text) { + cy.window().then((win) => { + var view = win.eXide.app.getEditor().editor + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: text } + }) + view.focus() + }) + } + + it('opens lint panel via Navigate menu', () => { + cy.get('.cm-panel-lint').should('not.exist') + + cy.get('#menu-navigate-diagnostics').click({ force: true }) + + cy.get('.cm-panel-lint', { timeout: 3000 }).should('be.visible') + }) + + it('toggles panel closed on second click', () => { + cy.get('#menu-navigate-diagnostics').click({ force: true }) + cy.get('.cm-panel-lint', { timeout: 3000 }).should('be.visible') + + cy.get('#menu-navigate-diagnostics').click({ force: true }) + cy.get('.cm-panel-lint').should('not.exist') + }) + + it('shows diagnostics for invalid XQuery', () => { + setEditorContent('declare function local:test() {\n $undefined\n};') + // Wait for validation + cy.wait('@compile', { timeout: 10000 }) + cy.wait(500) + + cy.get('#menu-navigate-diagnostics').click({ force: true }) + cy.get('.cm-panel-lint', { timeout: 3000 }).should('be.visible') + + // Lint panel should list at least one diagnostic + cy.get('.cm-panel-lint li').should('have.length.at.least', 1) + }) + + it('clears diagnostics when code is fixed', () => { + // First introduce an error + setEditorContent('declare function local:test() {\n $undefined\n};') + cy.wait('@compile', { timeout: 10000 }) + cy.wait(500) + + // Open panel and verify error exists + cy.get('#menu-navigate-diagnostics').click({ force: true }) + cy.get('.cm-panel-lint', { timeout: 3000 }).should('be.visible') + cy.get('.cm-panel-lint li').should('have.length.at.least', 1) + + // Fix the code + setEditorContent('1 + 1') + cy.wait('@compile', { timeout: 10000 }) + cy.wait(500) + + // Diagnostics should be cleared — panel shows "No diagnostics" text + cy.get('.cm-panel-lint').invoke('text').should('contain', 'No diagnostics') + }) + + it('error clearing: error pill disappears when code is fixed', () => { + // Trigger an error by setting invalid content + setEditorContent('declare function local:broken() { $x };\nlocal:broken()') + // Wait for compile to return the error + cy.wait('@compile', { timeout: 10000 }) + cy.wait(500) + + // Verify error appears + cy.get('#exide-err-pill', { timeout: 5000 }).should('have.class', 'has-error') + + // Fix the error with valid code + setEditorContent('1 + 1') + + // The error pill should eventually clear after re-validation + cy.get('#exide-err-pill', { timeout: 15000 }).should('not.have.class', 'has-error') + }) +}) diff --git a/cypress/e2e/native_autocomplete_spec.cy.js b/cypress/e2e/native_autocomplete_spec.cy.js new file mode 100644 index 00000000..e25ad796 --- /dev/null +++ b/cypress/e2e/native_autocomplete_spec.cy.js @@ -0,0 +1,119 @@ +describe('Native autocomplete for non-XQuery modes', () => { + beforeEach(() => { + cy.loginXHR('admin', '') + cy.visit('/eXide/index.html') + cy.reload(true) + cy.get('.cm-editor', { timeout: 10000 }).should('exist') + cy.get('#user', { timeout: 15000 }).should('not.have.text', 'Login') + }) + + /** + * Create a new document with the given file type and content. + * Types are: html, css, javascript, json, xml, less, markdown + */ + function newDocument(type, content) { + cy.window().then((win) => { + var editor = win.eXide.app.getEditor() + editor.newDocument(null, type) + }) + cy.wait(300) + if (content) { + cy.window().then((win) => { + var view = win.eXide.app.getEditor().editor + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: content }, + selection: { anchor: content.length } + }) + view.focus() + }) + } + } + + function triggerCompletion() { + cy.window().then((win) => { + var view = win.eXide.app.getEditor().editor + view.focus() + win.CM6.startCompletion(view) + }) + } + + function typeInEditor(text) { + cy.get('.cm-content').type(text, { delay: 50 }) + } + + describe('HTML completions', () => { + it('shows tag completions when typing <', () => { + newDocument('html', '') + typeInEditor(' { + newDocument('html', '
{ + it('shows property completions inside a rule', () => { + newDocument('css', 'body {\n co') + triggerCompletion() + + cy.get('.cm-tooltip-autocomplete', { timeout: 5000 }) + .should('be.visible') + .invoke('text') + .should('contain', 'color') + }) + + it('shows multiple property suggestions', () => { + newDocument('css', 'body {\n b') + triggerCompletion() + + cy.get('.cm-tooltip-autocomplete', { timeout: 5000 }) + .should('be.visible') + .invoke('text') + .should('match', /background|border|bottom/) + }) + }) + + describe('JavaScript completions', () => { + it('shows local variable completions', () => { + newDocument('javascript', 'var myLongVariable = 42;\nmy') + triggerCompletion() + + cy.get('.cm-tooltip-autocomplete', { timeout: 5000 }) + .should('be.visible') + .invoke('text') + .should('contain', 'myLongVariable') + }) + }) + + describe('JSON linting', () => { + it('shows parse error for invalid JSON', () => { + newDocument('json', '{ foo: bar }') + cy.wait(500) + + // Lint gutter should show an error marker + cy.get('.cm-lint-marker-error', { timeout: 5000 }) + .should('have.length.at.least', 1) + }) + + it('no errors for valid JSON', () => { + newDocument('json', '{\n "name": "test",\n "version": "1.0"\n}') + cy.wait(500) + + cy.get('.cm-lint-marker-error').should('have.length', 0) + }) + }) + +}) diff --git a/cypress/e2e/outline_modes_spec.cy.js b/cypress/e2e/outline_modes_spec.cy.js index 1cb37a97..0e8fc874 100644 --- a/cypress/e2e/outline_modes_spec.cy.js +++ b/cypress/e2e/outline_modes_spec.cy.js @@ -87,10 +87,10 @@ describe('Outline modes and filter', () => { // Type a filter that matches only some functions cy.get('#outline-filter').clear().type('access') - // The filter hides non-matching links via display:none on the + // The filter hides non-matching
  • elements via display:none // Some items should be hidden - cy.get('#outline li a').then(($links) => { - var visible = $links.filter(function () { + cy.get('#outline li').then(($items) => { + var visible = $items.filter(function () { return Cypress.$(this).css('display') !== 'none' }) expect(visible.length).to.be.below(totalCount) @@ -109,9 +109,9 @@ describe('Outline modes and filter', () => { // Clear the filter cy.get('#outline-filter').clear() - // All links should be visible again - cy.get('#outline li a').then(($links) => { - var visible = $links.filter(function () { + // All items should be visible again + cy.get('#outline li').then(($items) => { + var visible = $items.filter(function () { return Cypress.$(this).css('display') !== 'none' }) expect(visible.length).to.eq(totalCount) diff --git a/cypress/e2e/outline_navigation_spec.cy.js b/cypress/e2e/outline_navigation_spec.cy.js new file mode 100644 index 00000000..b8940035 --- /dev/null +++ b/cypress/e2e/outline_navigation_spec.cy.js @@ -0,0 +1,209 @@ +describe('Outline navigation', () => { + beforeEach(() => { + cy.loginXHR('admin', '') + cy.visit('/eXide/index.html') + cy.reload(true) + cy.get('.path', { timeout: 10000 }).should('contain', 'untitled-1') + cy.get('#user', { timeout: 15000 }).should('not.have.text', 'Login') + }) + + function setEditorContent(text) { + cy.window().then((win) => { + var view = win.eXide.app.getEditor().editor + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: text } + }) + view.focus() + }) + } + + function newDocument(type, content) { + cy.window().then((win) => { + var editor = win.eXide.app.getEditor() + editor.newDocument(null, type) + }) + cy.wait(300) + if (content) { + setEditorContent(content) + } + } + + function showOutline() { + cy.get('#tabs-outline').contains('outline').click() + cy.wait(300) + } + + describe('XML outline', () => { + var xml = [ + '', + '
    ', + ' Test', + '
    ', + ' ', + '
    ', + ' Hello', + '
    ', + '
    ', + ' World', + '
    ', + ' ', + '
    ' + ].join('\n') + + it('shows XML elements as nested tree', () => { + newDocument('xml', xml) + showOutline() + + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 5) + cy.get('#outline .outline-name').first().should('contain', 'root') + }) + + it('shows XPath signatures in tooltips', () => { + newDocument('xml', xml) + showOutline() + + // Outline items should have XPath-like title attributes + cy.get('#outline li a', { timeout: 5000 }).first() + .should('have.attr', 'title') + .and('match', /\/root/) + }) + + it('shows hint attributes for elements with id', () => { + newDocument('xml', xml) + showOutline() + + cy.get('#outline .outline-hint', { timeout: 5000 }) + .should('have.length.at.least', 1) + .first() + .invoke('text') + .should('contain', 'intro') + }) + + it('uses nested
      structure for hierarchy', () => { + newDocument('xml', xml) + showOutline() + + // Nested mode should produce
        children inside
      • elements + cy.get('#outline li ul', { timeout: 5000 }) + .should('have.length.at.least', 1) + }) + + it('navigates to element and centers viewport on click', () => { + newDocument('xml', xml) + showOutline() + + // Click the last element to trigger scroll + cy.get('#outline li a .outline-name').contains('section').first().parent().click() + + // Editor should exist and have the cursor on or near the section element + cy.get('#editor .cm-editor', { timeout: 5000 }).should('exist') + }) + }) + + describe('XML gotoSymbol', () => { + var xml = '\n \n \n' + + it('opens QuickPicker with XPath items', () => { + newDocument('xml', xml) + // Wait for outline to populate (needs ensureSyntaxTree + requestAnimationFrame) + showOutline() + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 1) + + cy.window().then((win) => { + win.eXide.app.getEditor().exec('gotoSymbol') + }) + + cy.get('.quick-picker', { timeout: 5000 }) + .should('be.visible') + + // Should show XPath-style items + cy.get('.quick-picker-list li', { timeout: 3000 }) + .should('have.length.at.least', 1) + .invoke('text') + .should('contain', '/') + }) + }) + + describe('JSON outline', () => { + var json = '{\n "name": "test",\n "version": "1.0",\n "scripts": {\n "build": "npm run build"\n }\n}' + + it('shows property keys in outline', () => { + newDocument('json', json) + showOutline() + + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 3) + cy.get('#outline .outline-name').invoke('text').should('contain', 'name') + }) + + it('opens QuickPicker via gotoSymbol', () => { + newDocument('json', json) + showOutline() + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 1) + + cy.window().then((win) => { + win.eXide.app.getEditor().exec('gotoSymbol') + }) + + cy.get('.quick-picker', { timeout: 5000 }).should('be.visible') + }) + }) + + describe('CSS outline', () => { + var css = 'body {\n color: red;\n}\n\n.header {\n display: flex;\n}\n\n#main {\n padding: 10px;\n}' + + it('shows selectors in outline', () => { + newDocument('css', css) + showOutline() + + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 2) + cy.get('#outline .outline-name').invoke('text').should('contain', 'body') + }) + }) + + describe('JavaScript outline', () => { + var js = 'function greet(name) {\n return "Hello " + name;\n}\n\nfunction add(a, b) {\n return a + b;\n}' + + it('shows functions in outline', () => { + newDocument('javascript', js) + showOutline() + + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 2) + cy.get('#outline .outline-name').invoke('text').should('contain', 'greet') + }) + }) + + describe('Markdown outline', () => { + var md = '# Title\n\n## Section 1\n\nContent\n\n## Section 2\n\nMore content\n\n### Subsection' + + it('shows headings in outline', () => { + newDocument('markdown', md) + showOutline() + + cy.get('#outline li', { timeout: 5000 }).should('have.length.at.least', 3) + cy.get('#outline .outline-name').invoke('text').should('contain', 'Title') + }) + }) + + describe('Navigation flash', () => { + it('flashes target line on gotoLine', () => { + // Open config.xqm which has enough lines + cy.get('#directory li').contains('span', 'apps', { timeout: 5000 }).click() + cy.get('#directory li').contains('span', 'eXide', { timeout: 5000 }).click() + cy.get('#directory li').contains('span', 'modules', { timeout: 5000 }).click() + cy.get('#directory li').contains('span', 'config.xqm', { timeout: 5000 }).click() + cy.get('.path', { timeout: 10000 }).should('contain', 'config.xqm') + + // Navigate to a line programmatically + cy.window().then((win) => { + var view = win.eXide.app.getEditor().editor + win.editorUtils.gotoLine(view, 10, 0, true) + }) + + // Flash decoration should appear briefly + cy.get('.cm-goto-flash', { timeout: 1000 }).should('exist') + + // Flash should fade and be removed + cy.get('.cm-goto-flash', { timeout: 2000 }).should('not.exist') + }) + }) +}) diff --git a/index.html.tmpl b/index.html.tmpl index 9f25031d..063c557b 100755 --- a/index.html.tmpl +++ b/index.html.tmpl @@ -7,6 +7,7 @@ +