From 73df30ccd507347b65b9e46a1dd39ee6cb7c728d Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Mon, 1 Sep 2025 15:31:50 -0300 Subject: [PATCH 1/2] :sparkles: svg support --- client/render.js | 9 ++++--- client/rerender.js | 14 ++++++----- shared/nodes.js | 2 +- tests/src/Application.njs | 2 ++ tests/src/SvgSupport.njs | 46 ++++++++++++++++++++++++++++++++++++ tests/src/SvgSupport.test.js | 22 +++++++++++++++++ 6 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 tests/src/SvgSupport.njs create mode 100644 tests/src/SvgSupport.test.js 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/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..6dcc98e5 --- /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..0a966a6b --- /dev/null +++ b/tests/src/SvgSupport.test.js @@ -0,0 +1,22 @@ +describe('SvgSupport', () => { + beforeEach(async () => { + await page.goto('http://localhost:6969/svg-support') + await page.waitForSelector('[data-hydrated]') + }) + + test('svg can render text', async () => { + expect(true).toBeTruthy() + }) + + test('svg can add new paths while rerendering', async () => { + expect(true).toBeTruthy() + }) + + test('svg can render in short circuit statements', async () => { + expect(true).toBeTruthy() + }) + + test('svg can render in ternary statements', async () => { + expect(true).toBeTruthy() + }) +}) From 0b51beb8097a7610645d1882bc6d2c1a621a814e Mon Sep 17 00:00:00 2001 From: Aylon Muramatsu Date: Mon, 6 Oct 2025 17:19:40 -0300 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20implementa=20testes=20e=20adiciona?= =?UTF-8?q?=20corre=C3=A7=C3=A3o=20para=20render=20do=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/render.js | 4 +- tests/src/SvgSupport.njs | 2 +- tests/src/SvgSupport.test.js | 96 ++++++++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 7 deletions(-) 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/tests/src/SvgSupport.njs b/tests/src/SvgSupport.njs index 6dcc98e5..9080bc9d 100644 --- a/tests/src/SvgSupport.njs +++ b/tests/src/SvgSupport.njs @@ -26,7 +26,7 @@ class SvgSupport extends Nullstack { render() { return ( -
+
I love diff --git a/tests/src/SvgSupport.test.js b/tests/src/SvgSupport.test.js index 0a966a6b..b6510611 100644 --- a/tests/src/SvgSupport.test.js +++ b/tests/src/SvgSupport.test.js @@ -5,18 +5,106 @@ describe('SvgSupport', () => { }) test('svg can render text', async () => { - expect(true).toBeTruthy() + // 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 () => { - expect(true).toBeTruthy() + // 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 () => { - expect(true).toBeTruthy() + // 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 () => { - expect(true).toBeTruthy() + 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') }) })