diff --git a/cypress/integration/component-data.js b/cypress/integration/v2/component-data.js similarity index 96% rename from cypress/integration/component-data.js rename to cypress/integration/v2/component-data.js index b62de3e7..4d4214d7 100644 --- a/cypress/integration/component-data.js +++ b/cypress/integration/v2/component-data.js @@ -1,4 +1,4 @@ -it('should display read-only function/HTMLElement attributes + allow editing of booleans, numbers and strings', () => { +it('v2 - should display read-only function/HTMLElement attributes + allow editing of booleans, numbers and strings', () => { cy.visit('/').get('[data-testid=component-name]').should('be.visible') cy.get('[data-testid=component-container]').first().click() @@ -73,7 +73,7 @@ it('should display read-only function/HTMLElement attributes + allow editing of cy.iframe('#target').contains('Str, type: "string", value: "devtools"') }) -it('should display nested arrays/object attributes and support editing', () => { +it('v2 - should display nested arrays/object attributes and support editing', () => { cy.visit('/').get('[data-testid=component-name]').should('be.visible') cy.get('[data-testid=component-name]').first().click().trigger('mouseleave') @@ -127,7 +127,7 @@ it('should display nested arrays/object attributes and support editing', () => { cy.get('[data-testid=data-property-name-0]').click() }) -it('should support x-model updates and editing values', () => { +it('v2 - should support x-model updates and editing values', () => { cy.visit('/').get('[data-testid=component-name]').should('be.visible') cy.get('[data-testid=component-name]').contains('model-no-render').click().trigger('mouseleave') @@ -168,7 +168,7 @@ it('should support x-model updates and editing values', () => { cy.iframe('#target').find('[data-testid=nested-model-no-render]').should('have.value', 'nested-from-devtools') }) -it('should reset component selection when changing page', () => { +it('v2 - should reset component selection when changing page', () => { cy.visit('/') cy.get('[data-testid=component-name]').first().click() diff --git a/cypress/integration/component-list.js b/cypress/integration/v2/component-list.js similarity index 92% rename from cypress/integration/component-list.js rename to cypress/integration/v2/component-list.js index 0d0a806c..8d088f98 100644 --- a/cypress/integration/component-list.js +++ b/cypress/integration/v2/component-list.js @@ -1,4 +1,4 @@ -it('should get names of components', () => { +it('v2 - should get names of components', () => { cy.visit('/') .get('[data-testid=component-name]') .should('be.visible') @@ -9,7 +9,7 @@ it('should get names of components', () => { .should('contain.text', 'combobox') }) -it('should create globals + add annotation for each component', () => { +it('v2 - should create globals + add annotation for each component', () => { cy.visit('/').get('[data-testid=component-name]').should('be.visible') let win @@ -38,7 +38,7 @@ it('should create globals + add annotation for each component', () => { }) }) -it('should handle adding and removing new components', () => { +it('v2 - should handle adding and removing new components', () => { cy.visit('/') .get('[data-testid=component-name]') .should('have.length.above', 0) @@ -53,7 +53,7 @@ it('should handle adding and removing new components', () => { }) }) -it('should handle replacing a component and keep its listed position', () => { +it('v2 - should handle replacing a component and keep its listed position', () => { let currentIndex = -1 cy.visit('/') .get('[data-testid=component-name]') @@ -68,7 +68,7 @@ it('should handle replacing a component and keep its listed position', () => { .then((index) => expect(currentIndex).to.equal(index)) }) -it('should add/remove hover overlay on component mouseenter/leave', () => { +it('v2 - should add/remove hover overlay on component mouseenter/leave', () => { cy.visit('/') // check overlay works for first component cy.get('[data-testid=component-container]').first().should('be.visible').trigger('mouseenter') @@ -132,7 +132,7 @@ it('should add/remove hover overlay on component mouseenter/leave', () => { cy.iframe('#target').find('[data-testid=hover-element]').should('not.exist') }) -it('should support selecting/unselecting a component', () => { +it('v2 - should support selecting/unselecting a component', () => { cy.visit('/') cy.get('[data-testid=component-container]').last().click().should('have.class', 'text-white bg-alpine-300') @@ -143,7 +143,7 @@ it('should support selecting/unselecting a component', () => { cy.get('[data-testid=component-container]').last().should('not.have.class', 'text-white bg-alpine-300') }) -it('should display message with number of components watched', () => { +it('v2 - should display message with number of components watched', () => { cy.visit('/') .get('[data-testid=component-name]') .should('have.length.above', 0) diff --git a/cypress/integration/v2/warning.js b/cypress/integration/v2/warning.js new file mode 100644 index 00000000..bd69a7f0 --- /dev/null +++ b/cypress/integration/v2/warning.js @@ -0,0 +1,84 @@ +it('v2 - should catch component initialisation errors', () => { + cy.visit('/').get('[data-testid=component-name]').should('have.length.above', 0) + cy.get('[data-testid=tab-link-warnings').should('be.visible').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('contain.text', 'No warnings found') + + cy.iframe('#target').find('[data-testid=inject-broken]').click() + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('1 warning') + }) + + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') + + cy.get('[data-testid=eval-error-div]') + .should('have.length', 1) + .should(($el) => { + const text = $el.text().replace(/\n/g, '') + expect(text).to.contain(`Error evaluating`) + expect(text).to.contain(`"{ foo: 'aaa' "`) + expect(text).to.contain(`SyntaxError: Unexpected token ')'`) + }) +}) +it('v2 - should catch x-on errors', () => { + cy.iframe('#target').find('[data-testid=broken-click]').click() + + cy.get('[data-testid=eval-error-button]').should('have.length', 1) + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('2 warnings') + }) + + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') + + cy.get('[data-testid=eval-error-button]').should(($el) => { + const text = $el.text().replace(/\n/g, '') + expect(text).to.contain(`Error evaluating`) + expect(text).to.contain(`"foo.bar.baz"`) + expect(text).to.contain(`ReferenceError: foo is not defined`) + }) +}) +it('v2 - should scroll to newest error when warnings tab is open', () => { + cy.iframe('#target').find('[data-testid=broken-click]').click() + cy.iframe('#target').find('[data-testid=broken-click]').click() + cy.iframe('#target').find('[data-testid=broken-click]').click() + + cy.get('[data-testid=eval-error-button').should('have.length', 4) + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('5 warnings') + }) + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) + +it('v2 - should scroll to newest error when switching from components to warnings tab', () => { + cy.get('[data-testid=warnings-scroll-container]').scrollTo('top') + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).to.equal(0) + }) + + cy.get('[data-testid=tab-link-components]').click() + cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') + + cy.get('[data-testid=tab-link-warnings]').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) + +it('v2 - should toggle using footer links', () => { + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + cy.get('[data-testid=footer-components-link').click() + cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') + cy.get('[data-testid=footer-warnings-link').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) diff --git a/cypress/integration/v3/component-data.js b/cypress/integration/v3/component-data.js new file mode 100644 index 00000000..fa6ed474 --- /dev/null +++ b/cypress/integration/v3/component-data.js @@ -0,0 +1,183 @@ +it('v3 - should display read-only function/HTMLElement attributes + allow editing of booleans, numbers and strings', () => { + cy.visit('/?target=v3.html').get('[data-testid=component-name]').should('be.visible') + + cy.get('[data-testid=component-container]').first().click() + + cy.get('[data-testid=data-property-name-myFunction]').should('be.visible').contains('myFunction') + + cy.get('[data-testid=data-property-value-myFunction]').should('contain.text', 'function') + + cy.get('[data-testid=data-property-name-el]').contains('el') + + cy.get('[data-testid=data-property-value-el]').should('contain.text', 'HTMLElement').as('elValue').click() + + // check nested attributes + cy.get('[data-testid=data-property-name-name]').contains('name').should('be.visible') + cy.get('[data-testid=data-property-value-name]').should('contain.text', 'div') + + cy.get('[data-testid=data-property-name-attributes]').contains('attributes').should('be.visible') + cy.get('[data-testid=data-property-value-attributes]').should('contain.text', 'Array[1]') + + cy.get('[data-testid=data-property-name-children]').contains('children').should('be.visible') + cy.get('[data-testid=data-property-value-children]').should('contain.text', 'Array[5]') + + // check they toggle off + cy.get('[data-testid=data-property-value-el]').click() + + cy.get('[data-testid=data-property-value-name]').should('not.exist') + + cy.get('[data-testid=data-property-name-attributes]').should('not.exist') + cy.get('[data-testid=data-property-name-children]').should('not.exist') + + // booleans + cy.get('[data-testid=data-property-name-bool]').should('be.visible').contains('bool') + cy.get('[data-testid=data-property-value-bool]').should('contain.text', 'true') + // checkbox is visibility is toggled using CSS click the hidden element + cy.get('[data-testid=data-property-value-bool] [type=checkbox]').click({ force: true }) + // check the edit worked + cy.get('[data-testid=data-property-name-bool]').should('be.visible').contains('bool') + + cy.get('[data-testid=data-property-value-bool]').should('contain.text', 'false') + + cy.iframe('#target').contains('Bool, type: "boolean", value: "false"') + + // numbers + cy.get('[data-testid=data-property-name-num]').should('be.visible').contains('num') + + cy.get('[data-testid=data-property-value-num]').should('contain.text', '5') + // edit icon visibility is toggled using CSS, force-click + cy.get('[data-testid=edit-icon-num]').click({ force: true }) + // editing toggles window.alpineState, causes issues with visibility/re-rendering + // force all interactions + cy.get('[data-testid=input-num]') + .clear({ force: true }) + .type('20', { force: true }) + .siblings('[data-testid=save-icon]') + .click({ force: true }) + + cy.iframe('#target').contains('Num, type: "number", value: "20"') + + // strings + cy.get('[data-testid=data-property-name-str]').should('be.visible').contains('str') + cy.get('[data-testid=data-property-value-str]').should('contain.text', 'string') + // edit icon visibility is toggled using CSS, force-click + cy.get('[data-testid=edit-icon-str]').click({ force: true }) + // editing toggles window.alpineState, causes issues with visibility/re-rendering + // force all interactions + cy.get('[data-testid=input-str]') + .clear({ force: true }) + .type('devtools', { force: true }) + .siblings('[data-testid=save-icon]') + .click({ force: true }) + + cy.iframe('#target').contains('Str, type: "string", value: "devtools"') +}) + +it('v3 - should display nested arrays/object attributes and support editing', () => { + cy.visit('/?target=v3.html').get('[data-testid=component-name]').should('be.visible') + + cy.get('[data-testid=component-name]').first().click().trigger('mouseleave') + + cy.get('[data-testid="data-property-name-nestedObjArr"]').contains('nestedObjArr') + cy.get('[data-testid="data-property-value-nestedObjArr"]').contains('Object') + + cy.get('[data-testid="data-property-value-nestedObjArr"]').click() + cy.get('[data-testid=data-property-name-array]').should('be.visible').contains('array') + cy.get('[data-testid=data-property-value-array]').should('be.visible').contains('Array[1]') + + cy.get('[data-testid=data-property-value-array]').click() + + cy.get('[data-testid=data-property-name-0]').should('be.visible').contains('0') + cy.get('[data-testid=data-property-value-0]').should('be.visible').contains('Object') + + cy.get('[data-testid=data-property-name-0]').click() + + cy.get('[data-testid=data-property-name-nested]').should('be.visible').contains('nested') + cy.get('[data-testid=data-property-value-nested]').should('be.visible').contains('property') + + // editing the nested array/object + cy.get('[data-testid=edit-icon-nested]').click({ force: true }) + cy.get('[data-testid=input-nested]') + .clear({ force: true }) + .type('from-devtools', { force: true }) + .siblings('[data-testid=save-icon]') + .click({ force: true }) + + cy.iframe('#target') + .find('[data-testid=nested-obj-arr]') + .should('have.text', JSON.stringify({ array: [{ nested: 'from-devtools' }] })) + + // check untoggling also works + cy.get('[data-testid=data-property-name-0]').click() + + cy.get('[data-testid=data-property-name-nested]').should('not.exist') + cy.get('[data-testid=data-property-value-nested]').should('not.exist') + + cy.get('[data-testid=data-property-value-array]').click() + + cy.get('[data-testid=data-property-name-0]').should('not.exist') + cy.get('[data-testid=data-property-value-0]').should('not.exist') + + cy.get('[data-testid="data-property-value-nestedObjArr"]').click() + cy.get('[data-testid=data-property-name-array]').should('not.exist') + cy.get('[data-testid=data-property-value-array]').should('not.exist') + + cy.get('[data-testid="data-property-value-nestedObjArr"]').click() + cy.get('[data-testid=data-property-value-array]').click() + cy.get('[data-testid=data-property-name-0]').click() +}) + +it('v3 - should support x-model updates and editing values', () => { + cy.visit('/?target=v3.html').get('[data-testid=component-name]').should('be.visible') + + cy.get('[data-testid=component-name]').contains('model-no-render').click().trigger('mouseleave') + + // check preloading doesn't cause issues with selected component tracking + cy.get('[data-testid=component-name]').last().trigger('mouseenter').trigger('mouseleave') + + cy.get('[data-testid=data-property-name-text]').should('be.visible').contains('text') + cy.get('[data-testid=data-property-value-text]').should('be.visible').contains('initial') + cy.iframe('#target').find('[data-testid=model-no-render]').should('be.visible').clear().type('updated') + cy.get('[data-testid=data-property-value-text]').should('be.visible').contains('updated') + + cy.get('[data-testid=edit-icon-text]').click({ force: true }) + cy.get('[data-testid=input-text]') + .clear({ force: true }) + .type('from-devtools', { force: true }) + .siblings('[data-testid=save-icon]') + .click({ force: true }) + + cy.iframe('#target').find('[data-testid=model-no-render]').should('have.value', 'from-devtools') + + // nested updates + cy.get('[data-testid=data-property-name-model]').click() + cy.get('[data-testid=data-property-name-nested').should('be.visible').contains('nested') + cy.get('[data-testid=data-property-value-nested').should('be.visible').contains('nested-initial') + + cy.iframe('#target').find('[data-testid=nested-model-no-render]').clear().type('nested-update') + cy.get('[data-testid=data-property-name-nested').should('be.visible').contains('nested') + cy.get('[data-testid=data-property-value-nested').should('be.visible').contains('nested-update') + + cy.get('[data-testid=edit-icon-nested]').click({ force: true }) + cy.get('[data-testid=input-nested]') + .clear({ force: true }) + .type('nested-from-devtools', { force: true }) + .siblings('[data-testid=save-icon]') + .click({ force: true }) + + cy.iframe('#target').find('[data-testid=nested-model-no-render]').should('have.value', 'nested-from-devtools') +}) + +it('v3 - should reset component selection when changing page', () => { + cy.visit('/?target=v3.html') + + cy.get('[data-testid=component-name]').first().click() + cy.get('[data-testid=component-container]').first().should('have.class', 'text-white bg-alpine-300') + + cy.get('[data-testid=data-property-name-myFunction]').should('be.visible').contains('myFunction') + + cy.iframe('#target').find('[data-testid=navigation-target]').click() + + cy.get('[data-testid=component-container]').first().should('not.have.class', 'text-white bg-alpine-300') + cy.get('[data-testid=data-property-name-myFunction]').should('not.exist') +}) diff --git a/cypress/integration/v3/component-list.js b/cypress/integration/v3/component-list.js new file mode 100644 index 00000000..690cd7f5 --- /dev/null +++ b/cypress/integration/v3/component-list.js @@ -0,0 +1,158 @@ +it('v3 - should get names of components', () => { + cy.visit('/?target=v3.html') + .get('[data-testid=component-name]') + .should('be.visible') + .should('contain.text', 'div') + .should('contain.text', 'app') + .should('contain.text', 'myFn') + .should('contain.text', 'component') + .should('contain.text', 'combobox') +}) + +it('v3 - should create globals + add annotation for each component', () => { + cy.visit('/?target=v3.html').get('[data-testid=component-name]').should('be.visible') + + let win + cy.frameLoaded('#target').then(() => { + win = cy.$$('#target').get(0).contentWindow + }) + + cy.iframe('#target') + .find('[x-data]') + .then((components) => { + components.each((i, component) => { + expect(win[`$x${i}`].$el).to.equal(component) + expect(win[`$x${i}`]).to.equal(component._x_dataStack[0]) + }) + return components.length + }) + .then((componentCount) => { + cy.get('[data-testid="console-global"]') + .should('contain.text', '= $x0') + .should('have.attr', 'title', 'Available as $x0 in the console') + .should('contain.text', '= $x1') + .should('contain.text', '= $x2') + .should('contain.text', '= $x3') + .should('contain.text', '= $x4') + .should('contain.text', `= $x${componentCount - 1}`) + }) +}) + +it('v3 - should handle adding and removing new components', () => { + cy.visit('/?target=v3.html') + .get('[data-testid=component-name]') + .should('have.length.above', 0) + .then((components) => { + const length = components.length + cy.iframe('#target').find('[data-testid=add-component-button]').click() + cy.get('[data-testid=component-name]').should('have.length', length + 1) + }) + .then((components) => { + cy.iframe('#target').find('[data-testid=delete-component-button]').click() + cy.get('[data-testid=component-name]').should('have.length', components.length - 1) + }) +}) + +it('v3 - should handle replacing a component and keep its listed position', () => { + let currentIndex = -1 + cy.visit('/?target=v3.html') + .get('[data-testid=component-name]') + .should('have.length.above', 0) + .then(() => cy.contains('Replaceable').invoke('index')) + .then((index) => (currentIndex = index)) + .then(() => { + cy.iframe('#target').find('[data-testid=replace-component-button]').click() + cy.get('[data-testid=component-name]') + }) + .then(() => cy.contains('Span').invoke('index')) + .then((index) => expect(currentIndex).to.equal(index)) +}) + +it('v3 - should add/remove hover overlay on component mouseenter/leave', () => { + cy.visit('/?target=v3.html') + // check overlay works for first component + cy.get('[data-testid=component-container]').first().should('be.visible').trigger('mouseenter') + + cy.iframe('#target').find('[data-testid=hover-element]').should('be.visible') + + cy.iframe('#target') + .find('[data-testid=hover-element]') + .should(($el) => { + expect($el.attr('style')).to.contain('position: absolute;') + expect($el.attr('style')).to.contain('background-color: rgba(104, 182, 255, 0.35);') + expect($el.attr('style')).to.contain('border-radius: 4px;') + expect($el.attr('style')).to.contain('z-index: 9999;') + }) + .then(($el) => { + cy.iframe('#target') + .find('[x-data]') + .first() + .then(($appEl) => { + const { left, top, width, height } = $appEl[0].getClientRects()[0] + + expect($el.attr('style')).to.contain(`width: ${width}px;`) + expect($el.attr('style')).to.contain(`height: ${height}px;`) + expect($el.attr('style')).to.contain(`top: ${top}px;`) + expect($el.attr('style')).to.contain(`left: ${left}px;`) + }) + }) + + // check overlay works for last component + cy.get('[data-testid=component-container]').last().trigger('mouseleave') + + cy.iframe('#target').find('[data-testid=hover-element]').should('not.exist') + + cy.get('[data-testid=component-container]').last().trigger('mouseenter') + + cy.iframe('#target').find('[data-testid=hover-element]').should('be.visible') + + cy.iframe('#target') + .find('[data-testid=hover-element]') + .should(($el) => { + expect($el.attr('style')).to.contain('position: absolute;') + expect($el.attr('style')).to.contain('background-color: rgba(104, 182, 255, 0.35);') + expect($el.attr('style')).to.contain('border-radius: 4px;') + expect($el.attr('style')).to.contain('z-index: 9999;') + }) + + cy.get('[data-testid=component-container]').last().trigger('mouseleave') + cy.iframe('#target').find('[data-testid=hover-element]').should('not.exist') + + // check overlay disappears on `shutdown` + cy.get('[data-testid=component-container]').first().trigger('mouseenter') + + cy.iframe('#target').find('[data-testid=hover-element]').should('be.visible') + + cy.window().then((win) => { + win.postMessage({ + source: 'alpineDevtool', + payload: 'shutdown', + }) + }) + cy.iframe('#target').find('[data-testid=hover-element]').should('not.exist') +}) + +it('v3 - should support selecting/unselecting a component', () => { + cy.visit('/?target=v3.html') + + cy.get('[data-testid=component-container]').last().click().should('have.class', 'text-white bg-alpine-300') + + cy.get('[data-testid=component-container]').first().click() + + cy.get('[data-testid=component-container]').first().should('have.class', 'text-white bg-alpine-300') + cy.get('[data-testid=component-container]').last().should('not.have.class', 'text-white bg-alpine-300') +}) + +it('v3 - should display message with number of components watched', () => { + cy.visit('/?target=v3.html') + .get('[data-testid=component-name]') + .should('have.length.above', 0) + .then((components) => { + cy.get('[data-testid=footer-line]').then(($el) => { + expect($el.text()).to.contain('Watching') + expect($el.text()).to.contain( + `${components.length} ${components.length > 1 ? 'components' : 'component'}`, + ) + }) + }) +}) diff --git a/cypress/integration/v3/warning.js b/cypress/integration/v3/warning.js new file mode 100644 index 00000000..f60ef753 --- /dev/null +++ b/cypress/integration/v3/warning.js @@ -0,0 +1,84 @@ +it.skip('v3 - should catch component initialisation errors', () => { + cy.visit('/?target=v3.html').get('[data-testid=component-name]').should('have.length.above', 0) + cy.get('[data-testid=tab-link-warnings').should('be.visible').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('contain.text', 'No warnings found') + + cy.iframe('#target').find('[data-testid=inject-broken]').click() + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('1 warning') + }) + + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') + + cy.get('[data-testid=eval-error-div]') + .should('have.length', 1) + .should(($el) => { + const text = $el.text().replace(/\n/g, '') + expect(text).to.contain(`Error evaluating`) + expect(text).to.contain(`"{ foo: 'aaa' "`) + expect(text).to.contain(`SyntaxError: Unexpected token ')'`) + }) +}) +it.skip('v3 - should catch x-on errors', () => { + cy.iframe('#target').find('[data-testid=broken-click]').click() + + cy.get('[data-testid=eval-error-button]').should('have.length', 1) + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('2 warnings') + }) + + cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') + + cy.get('[data-testid=eval-error-button]').should(($el) => { + const text = $el.text().replace(/\n/g, '') + expect(text).to.contain(`Error evaluating`) + expect(text).to.contain(`"foo.bar.baz"`) + expect(text).to.contain(`ReferenceError: foo is not defined`) + }) +}) +it.skip('v3 - should scroll to newest error when warnings tab is open', () => { + cy.iframe('#target').find('[data-testid=broken-click]').click() + cy.iframe('#target').find('[data-testid=broken-click]').click() + cy.iframe('#target').find('[data-testid=broken-click]').click() + + cy.get('[data-testid=eval-error-button').should('have.length', 4) + + cy.get('[data-testid=footer-line]').should(($el) => { + expect($el.text()).to.contain('5 warnings') + }) + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) + +it.skip('v3 - should scroll to newest error when switching from components to warnings tab', () => { + cy.get('[data-testid=warnings-scroll-container]').scrollTo('top') + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).to.equal(0) + }) + + cy.get('[data-testid=tab-link-components]').click() + cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') + + cy.get('[data-testid=tab-link-warnings]').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) + +it.skip('v3 - should toggle using footer links', () => { + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + cy.get('[data-testid=footer-components-link').click() + cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') + cy.get('[data-testid=footer-warnings-link').click() + cy.get('[data-testid=warnings-tab-content]').should('be.visible') + + cy.get('[data-testid=warnings-scroll-container]').should(($el) => { + expect($el.scrollTop()).not.to.equal(0) + }) +}) diff --git a/cypress/integration/warning.js b/cypress/integration/warning.js index f0ca4349..4ecdec91 100644 --- a/cypress/integration/warning.js +++ b/cypress/integration/warning.js @@ -9,83 +9,3 @@ it('should display "No Warnings" found', () => { cy.get('[data-testid=tab-link-warnings').should('be.visible').click() cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('contain.text', 'No warnings found') }) -it('should catch component initialisation errors', () => { - cy.iframe('#target').find('[data-testid=inject-broken]').click() - - cy.get('[data-testid=footer-line]').should(($el) => { - expect($el.text()).to.contain('1 warning') - }) - - cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') - - cy.get('[data-testid=eval-error-div]') - .should('have.length', 1) - .should(($el) => { - const text = $el.text().replace(/\n/g, '') - expect(text).to.contain(`Error evaluating`) - expect(text).to.contain(`"{ foo: 'aaa' "`) - expect(text).to.contain(`SyntaxError: Unexpected token ')'`) - }) -}) -it('should catch x-on errors', () => { - cy.iframe('#target').find('[data-testid=broken-click]').click() - - cy.get('[data-testid=eval-error-button]').should('have.length', 1) - - cy.get('[data-testid=footer-line]').should(($el) => { - expect($el.text()).to.contain('2 warnings') - }) - - cy.get('[data-testid=warnings-tab-content]').should('be.visible').should('not.contain.text', 'No warnings found') - - cy.get('[data-testid=eval-error-button]').should(($el) => { - const text = $el.text().replace(/\n/g, '') - expect(text).to.contain(`Error evaluating`) - expect(text).to.contain(`"foo.bar.baz"`) - expect(text).to.contain(`ReferenceError: foo is not defined`) - }) -}) -it('should scroll to newest error when warnings tab is open', () => { - cy.iframe('#target').find('[data-testid=broken-click]').click() - cy.iframe('#target').find('[data-testid=broken-click]').click() - cy.iframe('#target').find('[data-testid=broken-click]').click() - - cy.get('[data-testid=eval-error-button').should('have.length', 4) - - cy.get('[data-testid=footer-line]').should(($el) => { - expect($el.text()).to.contain('5 warnings') - }) - - cy.get('[data-testid=warnings-scroll-container]').should(($el) => { - expect($el.scrollTop()).not.to.equal(0) - }) -}) - -it('should scroll to newest error when switching from components to warnings tab', () => { - cy.get('[data-testid=warnings-scroll-container]').scrollTo('top') - cy.get('[data-testid=warnings-scroll-container]').should(($el) => { - expect($el.scrollTop()).to.equal(0) - }) - - cy.get('[data-testid=tab-link-components]').click() - cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') - - cy.get('[data-testid=tab-link-warnings]').click() - cy.get('[data-testid=warnings-tab-content]').should('be.visible') - - cy.get('[data-testid=warnings-scroll-container]').should(($el) => { - expect($el.scrollTop()).not.to.equal(0) - }) -}) - -it('footer links should toggle between components and warnings tab', () => { - cy.get('[data-testid=warnings-tab-content]').should('be.visible') - cy.get('[data-testid=footer-components-link').click() - cy.get('[data-testid=warnings-tab-content]').should('not.be.visible') - cy.get('[data-testid=footer-warnings-link').click() - cy.get('[data-testid=warnings-tab-content]').should('be.visible') - - cy.get('[data-testid=warnings-scroll-container]').should(($el) => { - expect($el.scrollTop()).not.to.equal(0) - }) -}) diff --git a/package.json b/package.json index 540cdfff..d546a13b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "homepage": "https://github.com/alpine-collective/alpinejs-devtools", "scripts": { "start": "rollup -c -w", + "dev": "npm start", "start:ci": "ROLLUP_SERVE=true rollup -c", "build:dev": "rollup -c", "build": "NODE_ENV=production rollup -c", diff --git a/packages/shell-chrome/src/backend.js b/packages/shell-chrome/src/backend.js index b965e539..8cade62d 100644 --- a/packages/shell-chrome/src/backend.js +++ b/packages/shell-chrome/src/backend.js @@ -45,6 +45,9 @@ function init() { this._stopMutationObserver = false this._lastComponentCrawl = Date.now() + + this.alpineVersion = window.Alpine.version + this.isV3 = isRequiredVersion('3.0.0', this.alpineVersion) } runWithMutationPaused(cb) { @@ -60,10 +63,31 @@ function init() { }, 10) } + getAlpineDataInstance(node) { + if (this.isV3) { + return node._x_dataStack[0] + } + return node.__x + } + + getReadOnlyAlpineData(node) { + const alpineDataInstance = this.getAlpineDataInstance(node) + if (this.isV3) { + // in v3 magics are registered on the data stack + return Object.fromEntries(Object.entries(alpineDataInstance).filter(([key]) => !key.startsWith('$'))) + } + return alpineDataInstance && alpineDataInstance.getUnobservedData() + } + + getWriteableAlpineData(node) { + const alpineDataInstance = this.getAlpineDataInstance(node) + return this.isV3 ? alpineDataInstance : alpineDataInstance.$data + } + start() { this.initAlpineErrorCollection() this.getAlpineVersion() - this.discoverComponents() + this.watchComponents() // Watch on the body for injected components. This is lightweight // as work is only done if there are components added/removed this.observeNode(document.querySelector('body')) @@ -77,10 +101,10 @@ function init() { } initAlpineErrorCollection() { - if (!isRequiredVersion('2.8.0', window.Alpine.version) || !window.Alpine.version) { + if (!isRequiredVersion('2.8.0', this.alpineVersion) || !this.alpineVersion) { return } - if (isRequiredVersion('2.8.1', window.Alpine.version)) { + if (isRequiredVersion('2.8.1', this.alpineVersion)) { window.addEventListener('error', (errorEvent) => { if (errorEvent.error && errorEvent.error.el && errorEvent.error.expression) { const { el, expression } = errorEvent.error @@ -131,7 +155,7 @@ function init() { }) } - discoverComponents() { + watchComponents() { const alpineRoots = Array.from(document.querySelectorAll('[x-data]')) const allComponentsInitialized = Object.values(alpineRoots).every((e) => e.__alpineDevtool) @@ -160,23 +184,42 @@ function init() { this.components = [] alpineRoots.forEach((rootEl, index) => { - if (!rootEl.__x) { + if (!this.getAlpineDataInstance(rootEl)) { // this component probably crashed during init return } if (!rootEl.__alpineDevtool) { - // add an attr to trigger the mutation observer and run this function - // that will send updated state to devtools - rootEl.setAttribute(DEVTOOLS_RENDER_BINDING_ATTR_NAME, 'Date.now()') + if (!this.isV3) { + // only necessary for Alpine v2 + // add an attr to trigger the mutation observer and run this function + // that will send updated state to devtools + rootEl.setAttribute(DEVTOOLS_RENDER_BINDING_ATTR_NAME, 'Date.now()') + } rootEl.__alpineDevtool = { id: this.uuid++, } - window[`$x${rootEl.__alpineDevtool.id - 1}`] = rootEl.__x + window[`$x${rootEl.__alpineDevtool.id - 1}`] = this.getAlpineDataInstance(rootEl) } if (rootEl.__alpineDevtool.id === this.selectedComponentId) { - this.getComponentData(this.selectedComponentId, rootEl) + this.sendComponentData(this.selectedComponentId, rootEl) + } + + if (this.isV3) { + const componentData = this.getAlpineDataInstance(rootEl) + Alpine.effect(() => { + Object.keys(componentData).forEach((key) => { + // since effects track which dependencies are accessed, + // run a fake component data access so that the effect runs + void componentData[key] + if (rootEl.__alpineDevtool.id === this.selectedComponentId) { + // this re-computes the whole component data + // with effect we could send only the key-value of the field that's changed + this.sendComponentData(this.selectedComponentId, rootEl) + } + }) + }) } const componentDepth = @@ -211,7 +254,7 @@ function init() { getAlpineVersion() { this._postMessage({ - version: window.Alpine.version, + version: this.alpineVersion, type: BACKEND_TO_PANEL_MESSAGES.SET_VERSION, }) } @@ -226,8 +269,9 @@ function init() { ) } - getComponentData(componentId, componentRoot) { - const data = Object.entries(componentRoot.__x.getUnobservedData()).reduce((acc, [key, value]) => { + sendComponentData(componentId, componentRoot) { + const componentData = this.getReadOnlyAlpineData(componentRoot) + const data = Object.entries(componentData).reduce((acc, [key, value]) => { acc[key] = serializeDataProperty(value) return acc @@ -247,9 +291,27 @@ function init() { } this.selectedComponentId = componentId this.runWithMutationPaused(() => { - Alpine.discoverComponents((component) => { + this.discoverComponents((component) => { if (component.__alpineDevtool.id === componentId) { - this.getComponentData(componentId, component) + this.sendComponentData(componentId, component) + } + }) + }) + } + + discoverComponents(cb) { + if (this.isV3) { + document.querySelectorAll('[x-data]').forEach(cb) + } else { + Alpine.discoverComponents(cb) + } + } + + handleSetComponentData(componentId, attributeSequence, attributeValue) { + devtoolsBackend.runWithMutationPaused(() => { + this.discoverComponents((component) => { + if (component.__alpineDevtool.id === componentId) { + set(this.getWriteableAlpineData(component), attributeSequence, attributeValue) } }) }) @@ -264,7 +326,7 @@ function init() { this.observer = new MutationObserver((_mutations) => { if (!this._stopMutationObserver) { - this.discoverComponents() + this.watchComponents() } }) @@ -353,9 +415,9 @@ function init() { } case PANEL_TO_BACKEND_MESSAGES.HOVER_COMPONENT: { devtoolsBackend.runWithMutationPaused(() => { - Alpine.discoverComponents((component) => { + devtoolsBackend.discoverComponents((component) => { if (component.__alpineDevtool && component.__alpineDevtool.id === e.data.payload.componentId) { - devtoolsBackend.addHoverElement(component.__x.$el) + devtoolsBackend.addHoverElement(component) } }) }) @@ -363,7 +425,7 @@ function init() { } case PANEL_TO_BACKEND_MESSAGES.HIDE_HOVER: { devtoolsBackend.runWithMutationPaused(() => { - Alpine.discoverComponents((component) => { + devtoolsBackend.discoverComponents((component) => { if (component.__alpineDevtool && component.__alpineDevtool.id === e.data.payload.componentId) { devtoolsBackend.cleanupHoverElement() } @@ -372,14 +434,11 @@ function init() { break } case PANEL_TO_BACKEND_MESSAGES.EDIT_ATTRIBUTE: { - devtoolsBackend.runWithMutationPaused(() => { - Alpine.discoverComponents((component) => { - if (component.__alpineDevtool.id === e.data.payload.componentId) { - const { attributeSequence, attributeValue } = e.data.payload - set(component.__x.$data, attributeSequence, attributeValue) - } - }) - }) + devtoolsBackend.handleSetComponentData( + e.data.payload.componentId, + e.data.payload.attributeSequence, + e.data.payload.attributeValue, + ) break } case PANEL_TO_BACKEND_MESSAGES.GET_DATA: { diff --git a/packages/shell-chrome/views/_components/tab-link.edge b/packages/shell-chrome/views/_components/tab-link.edge index 10bcc862..b745bfd2 100644 --- a/packages/shell-chrome/views/_components/tab-link.edge +++ b/packages/shell-chrome/views/_components/tab-link.edge @@ -9,6 +9,6 @@ class="px-4 border-b-3 xs:pl-2 xs:pr-3 capitalize" @click.prevent="activeTab = '{{ tab }}'" > - {{{ $slots.body() }}} + {{{ await $slots.body() }}} diff --git a/packages/simulator/v3.html b/packages/simulator/v3.html new file mode 100644 index 00000000..81a31ca2 --- /dev/null +++ b/packages/simulator/v3.html @@ -0,0 +1,125 @@ + + + + + Alpine.js Devtools v3 + + + + + + + +
+
Bool, type: "", value: ""
+
Num, type: "", value: ""
+
Str, type: "", value: ""
+
+ Arr, type: "", value (stringified): "" +
+
+ Nested serializable array/object value (stringified): "" +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+ Nesting 1 +
Nesting 2
+
+
+
+ + +
+ +
+ +
+ + + + + + +
+ Go to next page +
+ +