diff --git a/client/render.js b/client/render.js index 7fee3434..e137652f 100644 --- a/client/render.js +++ b/client/render.js @@ -5,7 +5,7 @@ import { anchorableElement } from './anchorableNode' import { generateCallback, generateSubject } from './events' import { ref } from './ref' -export default function render(node, options) { +export default function render(node, isSvg = false) { if (isFalse(node) || node.type === 'head') { node.element = document.createComment('') return node.element @@ -16,9 +16,8 @@ export default function render(node, options) { return node.element } - const svg = (options && options.svg) || node.type === 'svg' - - if (svg) { + isSvg = isSvg || node.type === 'svg' + if (isSvg) { node.element = document.createElementNS('http://www.w3.org/2000/svg', node.type) } else { node.element = document.createElement(node.type) @@ -58,7 +57,7 @@ export default function render(node, options) { if (!node.attributes.html) { for (let i = 0; i < node.children.length; i++) { - const child = render(node.children[i], { svg }) + const child = render(node.children[i], isSvg) node.element.appendChild(child) } diff --git a/client/rerender.js b/client/rerender.js index cd788f67..512f9d69 100644 --- a/client/rerender.js +++ b/client/rerender.js @@ -100,7 +100,7 @@ function updateHeadChildren(currentChildren, nextChildren) { } } -function _rerender(current, next) { +function _rerender(current, next, isParentSvg = false) { const selector = current.element next.element = current.element @@ -108,8 +108,10 @@ function _rerender(current, next) { return } + const isSvg = isParentSvg || next.type === 'svg' + if (current.type !== next.type) { - const nextSelector = render(next) + const nextSelector = render(next, isSvg) selector.replaceWith(nextSelector) return } @@ -132,22 +134,22 @@ function _rerender(current, next) { const limit = Math.max(current.children.length, next.children.length) if (next.children.length > current.children.length) { for (let i = 0; i < current.children.length; i++) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } for (let i = current.children.length; i < next.children.length; i++) { - const nextSelector = render(next.children[i]) + const nextSelector = render(next.children[i], isSvg) selector.appendChild(nextSelector) } } else if (current.children.length > next.children.length) { for (let i = 0; i < next.children.length; i++) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } for (let i = current.children.length - 1; i >= next.children.length; i--) { selector.childNodes[i].remove() } } else { for (let i = limit - 1; i > -1; i--) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } } } diff --git a/package.json b/package.json index 005279b0..970fece1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.20.1", + "version": "0.20.4", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", @@ -35,10 +35,8 @@ "swc-loader": "0.2.3", "swc-plugin-nullstack": "0.1.3", "terser-webpack-plugin": "5.3.6", - "webpack": "5.88.1", - "webpack-hot-middleware": "2.25.4" - }, - "devDependencies": { - "webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup" + "webpack": "5.88.2", + "webpack-hot-middleware": "2.25.4", + "webpack-dev-middleware": "6.1.1" } } \ No newline at end of file diff --git a/server/render.js b/server/render.js index e1b565d2..06e416cf 100644 --- a/server/render.js +++ b/server/render.js @@ -1,4 +1,4 @@ -import { isFalse } from '../shared/nodes' +import { isFalse, isText } from '../shared/nodes' import { sanitizeHtml } from '../shared/sanitizeString' import renderAttributes from './renderAttributes' @@ -25,7 +25,7 @@ function renderBody(node, scope, next) { if (isFalse(node)) { return '' } - if (node.type === 'text') { + if (isText(node)) { const text = node.text === '' ? ' ' : sanitizeHtml(node.text.toString()) return next && next.type === 'text' ? `${text}` : text } diff --git a/shared/nodes.js b/shared/nodes.js index f1d3363e..4da25648 100644 --- a/shared/nodes.js +++ b/shared/nodes.js @@ -18,5 +18,5 @@ export function isFunction(node) { } export function isText(node) { - return node.type === 'text' + return node.type === 'text' && node.attributes === undefined } diff --git a/tests/src/Application.njs b/tests/src/Application.njs index 58691b9e..5bd8201a 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -63,6 +63,7 @@ import LazyComponentLoader from './LazyComponentLoader' import NestedFolder from './nested/NestedFolder' import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions' import ObjectEventScope from './ObjectEventScope' +import SvgSupport from './SvgSupport.njs' import './Application.css' class Application extends Nullstack { @@ -156,6 +157,7 @@ class Application extends Nullstack { + ) diff --git a/tests/src/SvgSupport.njs b/tests/src/SvgSupport.njs new file mode 100644 index 00000000..9080bc9d --- /dev/null +++ b/tests/src/SvgSupport.njs @@ -0,0 +1,46 @@ +import Nullstack from 'nullstack'; + +function Close({ size }) { + return ( + + + + + ) +} + +function Hamburger({ size }) { + return ( + + + + + + ) +} + +class SvgSupport extends Nullstack { + + open = false + visible = false + + render() { + return ( +
+ + I + love + my + cat! + + {this.open ? : } + + {this.visible && } + +
+ ) + } + +} + +export default SvgSupport; \ No newline at end of file diff --git a/tests/src/SvgSupport.test.js b/tests/src/SvgSupport.test.js new file mode 100644 index 00000000..b6510611 --- /dev/null +++ b/tests/src/SvgSupport.test.js @@ -0,0 +1,110 @@ +describe('SvgSupport', () => { + beforeEach(async () => { + await page.goto('http://localhost:6969/svg-support') + await page.waitForSelector('[data-hydrated]') + }) + + test('svg can render text', async () => { + // Verifica se o SVG possui 4 elementos dentro dele + const svg = await page.$('svg'); + const texts = await svg.$$('text'); + expect(texts.length).toBe(4); + }) + + test('svg can add new paths while rerendering', async () => { + // Verifica se o ícone Hamburger está presente inicialmente (3 paths) + const hamburgerPaths = await page.$$('svg[width="30"] path') + expect(hamburgerPaths.length).toBe(3) // Hamburger has 3 paths + }) + + test('svg can render in short circuit statements', async () => { + // Verifica se o ícone de Hamburger está sendo exibido (3 paths) + const hamburgerPaths = await page.$$('svg[width="30"] path') + expect(hamburgerPaths.length).toBe(3) + }) + + test('svg can render in ternary statements', async () => { + let bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + + // Clica no segundo botão (show) + const buttons = await page.$$('button') + await buttons[1].click() + + // Aguarda o Hamburger grande aparecer + await page.waitForSelector('svg[width="69"]') + + // Verifica se o Hamburger foi renderizado no ternário + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeTruthy() + + }) + + test('icon toggle functionality works correctly', async () => { + // Primeiro verifica o estado inicial (deve ser Hamburger, 3 paths) + let iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths + + // Clica no primeiro botão (toggle) + const buttons = await page.$$('button') + await buttons[0].click() + + // Aguarda o ícone trocar (Close tem 2 paths) + await page.waitForFunction(() => { + const svg = document.querySelector('svg[width="30"]'); + return svg && svg.querySelectorAll('path').length === 2; + }); + + iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(2) // Close tem 2 paths + + // Clica novamente para voltar ao Hamburger + await buttons[0].click() + + // Aguarda o ícone trocar de volta (Hamburger tem 3 paths) + await page.waitForFunction(() => { + const svg = document.querySelector('svg[width="30"]'); + return svg && svg.querySelectorAll('path').length === 3; + }); + + iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths + }) + + test('icon visibility toggle works correctly', async () => { + + // Verifica que o ícone grande não está visível inicialmente + let bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + + // Clica no segundo botão (show) + const buttons = await page.$$('button') + await buttons[1].click() + + // Aguarda o Hamburger grande aparecer + await page.waitForSelector('svg[width="69"]') + + // Verifica se o Hamburger grande apareceu + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeTruthy() + + // Clica novamente no segundo botão (show) para esconder + await buttons[1].click() + + // Aguarda o Hamburger grande desaparecer do DOM + await page.waitForSelector('svg[width="69"]', { hidden: true }) + + // Verifica se o Hamburger grande desapareceu + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + }) + + test('svg attributes are correctly applied', async () => { + // Verifica se os atributos SVG estão sendo aplicados corretamente + const svgElement = await page.$('svg[viewBox="0 0 240 80"]') + expect(svgElement).toBeTruthy() + + const xmlns = await page.$eval('svg[viewBox="0 0 240 80"]', el => el.getAttribute('xmlns')) + expect(xmlns).toBe('http://www.w3.org/2000/svg') + }) +})