From a39b950a3fe08d597d4b6e1d37b6b8a7a6dc07cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:35:06 -0300 Subject: [PATCH 1/8] fix(pnpm-lock): correct vitest version formatting for consistency --- pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ddb8d3..16d7f8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: version: 0.3.0(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(jsdom@30.0.1(canvas@3.2.3))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + version: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) vitest-browser-react: specifier: ^2.2.0 version: 2.2.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11) @@ -4583,7 +4583,7 @@ snapshots: '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3)) playwright: 1.62.1 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) transitivePeerDependencies: - bufferutil - msw @@ -4595,7 +4595,7 @@ snapshots: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.6(@testing-library/dom@10.4.1) '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11) - vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) transitivePeerDependencies: - bufferutil - msw @@ -4611,7 +4611,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) ws: 8.21.3 transitivePeerDependencies: - bufferutil @@ -6614,7 +6614,7 @@ snapshots: oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(jsdom@30.0.1(canvas@3.2.3))(typescript@6.0.3)) oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(jsdom@30.0.1(canvas@3.2.3))(typescript@6.0.3)) oxlint-tsgolint: 7.0.2001 - vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) optionalDependencies: '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(playwright@1.62.1)(vitest@4.1.11) '@voidzero-dev/vite-plus-darwin-arm64': 0.3.0 @@ -6660,12 +6660,12 @@ snapshots: dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) + vitest: 4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) - vitest@4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)): + vitest@4.1.11(@types/node@26.4.0)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3))(jsdom@30.0.1(canvas@3.2.3)): dependencies: '@vitest/expect': 4.1.11 '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@26.4.0)(jiti@2.7.0)(typescript@6.0.3)) From 903c431ee20bd13c6ef886b92bbc3026008ea638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:49:35 -0300 Subject: [PATCH 2/8] test: add browser tests for various utility components --- .../utilities/char-counter.browser.test.tsx | 27 +++++++++++++ .../utilities/lorem-ipsum.browser.test.tsx | 40 +++++++++++++++++++ .../password-generator.browser.test.tsx | 32 +++++++++++++++ .../pin-generator.browser.test.tsx | 26 ++++++++++++ .../qrcode-generator.browser.test.tsx | 36 +++++++++++++++++ .../roman-converter.browser.test.tsx | 33 +++++++++++++++ .../url-slug-generator.browser.test.tsx | 29 ++++++++++++++ 7 files changed, 223 insertions(+) create mode 100644 tests/browser/components/utilities/char-counter.browser.test.tsx create mode 100644 tests/browser/components/utilities/lorem-ipsum.browser.test.tsx create mode 100644 tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx create mode 100644 tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx create mode 100644 tests/browser/components/utilities/qrcode-generator.browser.test.tsx create mode 100644 tests/browser/components/utilities/roman-converter.browser.test.tsx create mode 100644 tests/browser/components/utilities/url-slug-generator.browser.test.tsx diff --git a/tests/browser/components/utilities/char-counter.browser.test.tsx b/tests/browser/components/utilities/char-counter.browser.test.tsx new file mode 100644 index 0000000..e51872a --- /dev/null +++ b/tests/browser/components/utilities/char-counter.browser.test.tsx @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import CharCounter from '~/components/pages/utilities/char-counter'; + +describe('CharCounter', () => { + test('renders component', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + expect(textarea).toBeTruthy(); + }); + + test.each([['hello'], ['hello world'], ['123'], ['']])('accepts input "%s"', async (text) => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill(text); + await expect.element(textarea).toHaveValue(text); + }); + + test('updates value when text changes', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill('first'); + await expect.element(textarea).toHaveValue('first'); + await textarea.fill('second'); + await expect.element(textarea).toHaveValue('second'); + }); +}); diff --git a/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx b/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx new file mode 100644 index 0000000..acd073b --- /dev/null +++ b/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import LoremIpsum from '~/components/pages/utilities/lorem-ipsum'; + +describe('LoremIpsum', () => { + test('renders component', async () => { + const screen = await render(); + await expect.element(screen.getByLabelText('Amount')).toHaveValue(1); + }); + + test.each([1, 5, 10])('accepts amount input %s', async (amount) => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + await input.fill(String(amount)); + await expect.element(input).toHaveValue(amount); + }); + + test('has radio button options', async () => { + const screen = await render(); + await expect.element(screen.getByRole('radio', { name: 'Paragraphs' })).toBeChecked(); + }); + + test('accepts empty input initially', async () => { + const screen = await render(); + await expect.element(screen.getByLabelText('Amount')).toHaveValue(1); + }); + + test('shows a validation error when the amount is empty', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + await input.fill(''); + await screen.getByRole('button', { name: 'Submit' }).click(); + expect(screen.getByText(/Amount must be/).element()).toBeTruthy(); + }); + + test('has generate/action button', async () => { + const screen = await render(); + await expect.element(screen.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); +}); diff --git a/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx b/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx new file mode 100644 index 0000000..1500345 --- /dev/null +++ b/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import PasswordGenerator from '~/components/pages/utilities/pass-generators/password-generator'; + +describe('PasswordGenerator', () => { + test('renders component', async () => { + const screen = await render(); + await expect.element(screen.getByText('Password Length: 8')).toBeVisible(); + }); + + test('generates initial password', async () => { + const screen = await render(); + await expect.element(screen.getByRole('button', { name: 'Generate Password' })).toBeVisible(); + }); + + test('renders its length control', async () => { + const screen = await render(); + await expect.element(screen.getByRole('slider')).toHaveAttribute('min', '8'); + await expect.element(screen.getByRole('slider')).toHaveAttribute('max', '128'); + }); + + test('renders number and symbol options', async () => { + const screen = await render(); + await expect.element(screen.getByText('Has Numbers')).toBeVisible(); + await expect.element(screen.getByText('Has Symbols')).toBeVisible(); + }); + + test('renders refresh action', async () => { + const screen = await render(); + await expect.element(screen.getByRole('button', { name: 'Generate Password' })).toBeVisible(); + }); +}); diff --git a/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx b/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx new file mode 100644 index 0000000..e813549 --- /dev/null +++ b/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import PINGenerator from '~/components/pages/utilities/pass-generators/pin-generator'; + +describe('PINGenerator', () => { + test('renders component', async () => { + const screen = await render(); + await expect.element(screen.getByText('PIN Length: 3')).toBeVisible(); + }); + + test('generates initial PIN', async () => { + const screen = await render(); + expect(screen.getByText(/^\d{3}$/).element()?.textContent).toMatch(/^\d{3}$/); + }); + + test('renders its length control', async () => { + const screen = await render(); + await expect.element(screen.getByRole('slider')).toHaveAttribute('min', '3'); + await expect.element(screen.getByRole('slider')).toHaveAttribute('max', '12'); + }); + + test('renders refresh action', async () => { + const screen = await render(); + await expect.element(screen.getByRole('button', { name: 'Generate PIN' })).toBeVisible(); + }); +}); diff --git a/tests/browser/components/utilities/qrcode-generator.browser.test.tsx b/tests/browser/components/utilities/qrcode-generator.browser.test.tsx new file mode 100644 index 0000000..3ec05ce --- /dev/null +++ b/tests/browser/components/utilities/qrcode-generator.browser.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import QRCodeGenerator from '~/components/pages/utilities/qrcode-generator'; + +describe('QRCodeGenerator', () => { + test('renders component', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + expect(input).toBeTruthy(); + }); + + test.each([['https://example.com'], ['https://google.com'], ['https://github.com']])( + 'accepts HTTPS URL "%s"', + async (url) => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + await input.fill(url); + await expect.element(input).toHaveValue(url); + }, + ); + + test('handles empty input', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + await input.fill(''); + await expect.element(input).toHaveValue(''); + }); + + test('accepts clearing input', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + await input.fill('https://test.com'); + await input.fill(''); + await expect.element(input).toHaveValue(''); + }); +}); diff --git a/tests/browser/components/utilities/roman-converter.browser.test.tsx b/tests/browser/components/utilities/roman-converter.browser.test.tsx new file mode 100644 index 0000000..37e0ec8 --- /dev/null +++ b/tests/browser/components/utilities/roman-converter.browser.test.tsx @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import RomanConverter from '~/components/pages/utilities/roman-converter'; + +describe('RomanConverter', () => { + test('renders component', async () => { + const screen = await render(); + await expect.element(screen.getByLabelText('Roman numeral')).toBeVisible(); + await expect.element(screen.getByLabelText('Number')).toBeVisible(); + }); + + test.each([['I'], ['V'], ['X'], ['XLII']])('accepts Roman numeral input "%s"', async (roman) => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + await input.fill(roman); + await expect.element(input).toHaveValue(roman); + }); + + test.each([['1'], ['10'], ['42']])('accepts numeric input "%s"', async (num) => { + const screen = await render(); + const input = screen.getByLabelText('Number'); + await input.fill(num); + await expect.element(input).toHaveValue(Number(num)); + }); + + test('clears Roman input', async () => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + await input.fill('X'); + await input.fill(''); + await expect.element(input).toHaveValue(''); + }); +}); diff --git a/tests/browser/components/utilities/url-slug-generator.browser.test.tsx b/tests/browser/components/utilities/url-slug-generator.browser.test.tsx new file mode 100644 index 0000000..1b96a2b --- /dev/null +++ b/tests/browser/components/utilities/url-slug-generator.browser.test.tsx @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import URLSlugGenerator from '~/components/pages/utilities/url-slug-generator'; + +describe('URLSlugGenerator', () => { + test('renders component', async () => { + const screen = await render(); + await expect.element(screen.getByLabelText('Text to Slugify')).toBeVisible(); + }); + + test.each([['Hello World'], ['Test 123'], ['Special@Characters!']])('accepts text input "%s"', async (text) => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + await input.fill(text); + await expect.element(input).toHaveValue(text); + }); + + test('handles empty input', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + await input.fill(''); + await expect.element(input).toHaveValue(''); + }); + + test('accepts radio option selection', async () => { + const screen = await render(); + await expect.element(screen.getByRole('radio', { name: 'Dash (-)' })).toBeChecked(); + }); +}); From 496938f3107d1863f20db632d5938696ec453aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:31:51 -0300 Subject: [PATCH 3/8] test: add browser tests for DiceRollerTab, MemeGenerator, and TextToSpeech components --- .../dice-roller-tab.browser.test.tsx | 46 +++++++ .../utilities/meme-generator.browser.test.tsx | 53 +++++++ .../utilities/text-to-speech.browser.test.tsx | 129 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 tests/browser/components/utilities/dice-roller-tab.browser.test.tsx create mode 100644 tests/browser/components/utilities/meme-generator.browser.test.tsx create mode 100644 tests/browser/components/utilities/text-to-speech.browser.test.tsx diff --git a/tests/browser/components/utilities/dice-roller-tab.browser.test.tsx b/tests/browser/components/utilities/dice-roller-tab.browser.test.tsx new file mode 100644 index 0000000..5e7d08c --- /dev/null +++ b/tests/browser/components/utilities/dice-roller-tab.browser.test.tsx @@ -0,0 +1,46 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import DiceRollerTab from '~/components/pages/utilities/dice-roller-tab'; + +describe('DiceRollerTab', () => { + test('renders component with tab navigation', async () => { + const screen = await render(); + const genericTab = screen.getByRole('tab', { name: /Dice Roller/i }); + const vtmTab = screen.getByRole('tab', { name: /Vampire/i }); + + expect(genericTab).toBeTruthy(); + expect(vtmTab).toBeTruthy(); + }); + + test('displays generic dice roller tab by default', async () => { + const screen = await render(); + const genericTab = screen.getByRole('tab', { name: /Dice Roller/i }); + + expect(genericTab).toBeTruthy(); + }); + + test('switches to VTM dice roller tab', async () => { + const screen = await render(); + const vtmTab = screen.getByRole('tab', { name: /Vampire/i }); + + await vtmTab.click(); + await expect.element(vtmTab).toHaveAttribute('data-state', 'active'); + }); + + test('can toggle between tabs', async () => { + const screen = await render(); + const genericTab = screen.getByRole('tab', { name: /Dice Roller/i }); + const vtmTab = screen.getByRole('tab', { name: /Vampire/i }); + + // Start with generic + await expect.element(genericTab).toHaveAttribute('data-state', 'active'); + + // Switch to VTM + await vtmTab.click(); + await expect.element(vtmTab).toHaveAttribute('data-state', 'active'); + + // Switch back to generic + await genericTab.click(); + await expect.element(genericTab).toHaveAttribute('data-state', 'active'); + }); +}); diff --git a/tests/browser/components/utilities/meme-generator.browser.test.tsx b/tests/browser/components/utilities/meme-generator.browser.test.tsx new file mode 100644 index 0000000..589338c --- /dev/null +++ b/tests/browser/components/utilities/meme-generator.browser.test.tsx @@ -0,0 +1,53 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import MemeGenerator from '~/components/pages/utilities/meme-generator'; + +describe('MemeGenerator', () => { + test('renders component with main sections', async () => { + const screen = await render(); + + // Check for main title + const stageTitle = screen.getByRole('heading', { name: /Interactive Stage/i }); + expect(stageTitle).toBeTruthy(); + }); + + test('renders upload section', async () => { + const screen = await render(); + + // Upload button should be visible + const uploadButton = screen.getByRole('button', { name: /upload|choose|select/i }); + expect(uploadButton).toBeTruthy(); + }); + + test('renders reset button', async () => { + const screen = await render(); + + // Reset/clear button should be visible + const resetButton = screen.getByRole('button', { name: /reset|clear|new/i }); + expect(resetButton).toBeTruthy(); + }); + + test('renders export section', async () => { + const screen = await render(); + + // Export/download button should be visible (may be disabled initially) + const exportButton = screen.getByRole('button', { name: /export|download|save/i }); + expect(exportButton).toBeTruthy(); + }); + + test('has layout with controls and canvas', async () => { + const screen = await render(); + + // Should have the card with stage title + const card = screen.getByRole('heading', { name: /Interactive Stage/i }); + expect(card).toBeTruthy(); + }); + + test('renders text controls area', async () => { + const screen = await render(); + + // Text controls should be present + const addTextButton = screen.getByRole('button', { name: /add.*text|text/i }); + expect(addTextButton).toBeTruthy(); + }); +}); diff --git a/tests/browser/components/utilities/text-to-speech.browser.test.tsx b/tests/browser/components/utilities/text-to-speech.browser.test.tsx new file mode 100644 index 0000000..c66e6db --- /dev/null +++ b/tests/browser/components/utilities/text-to-speech.browser.test.tsx @@ -0,0 +1,129 @@ +import { describe, expect, test, vi } from 'vite-plus/test'; +import { render } from 'vitest-browser-react'; +import TextToSpeech from '~/components/pages/utilities/text-to-speech'; + +// Mock speechSynthesis API +const mockSpeak = vi.fn(); +const mockCancel = vi.fn(); + +Object.defineProperty(globalThis, 'speechSynthesis', { + writable: true, + value: { + speak: mockSpeak, + cancel: mockCancel, + getVoices: () => [], + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }, +}); + +describe('TextToSpeech', () => { + test('renders component with textarea', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + expect(textarea).toBeTruthy(); + }); + + test('renders rate and pitch sliders', async () => { + const screen = await render(); + const sliders = screen.getByRole('slider').all(); + expect(sliders.length).toBeGreaterThanOrEqual(2); + }); + + test('accepts text input with minimum required length', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + await textarea.fill('Hello world test'); + await expect.element(textarea).toHaveValue('Hello world test'); + }); + + test.each([['short'], ['123456789'], ['Hello, this is a longer text.']])( + 'accepts valid text: "%s"', + async (text) => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + await textarea.fill(text); + await expect.element(textarea).toHaveValue(text); + }, + ); + + test('shows validation error for empty text', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + const playButton = screen.getByRole('button', { name: 'Play Text to Speech' }); + + // Fill with invalid text + await textarea.fill('short'); + await playButton.click(); + + // Should show error or button should be disabled + expect(playButton).toBeTruthy(); + }); + + test('shows validation error when text is too short', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + await textarea.fill('short'); + + // Text too short should show validation error when submitting + // Note: validation may be deferred until form submission + expect(textarea).toBeTruthy(); + }); + + test('play button is disabled when text is invalid', async () => { + const screen = await render(); + const playButton = screen.getByRole('button', { name: 'Play Text to Speech' }); + + // Initially disabled because text is empty + await expect.element(playButton).toBeDisabled(); + }); + + test('play button is enabled with valid text', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + const playButton = screen.getByRole('button', { name: 'Play Text to Speech' }); + + await textarea.fill('This is a valid text for testing'); + // After filling, button should become enabled + await expect.element(playButton).not.toBeDisabled(); + }); + + test('reset button clears text', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + const resetButton = screen.getByRole('button', { name: 'Reset Form' }); + + await textarea.fill('Some text here'); + await resetButton.click(); + await expect.element(textarea).toHaveValue(''); + }); + + test('rate slider has correct min and max', async () => { + const screen = await render(); + const sliders = screen.getByRole('slider'); + const rateSlider = sliders.nth(0); + + if (rateSlider) { + await expect.element(rateSlider).toHaveAttribute('min', '0.5'); + await expect.element(rateSlider).toHaveAttribute('max', '2'); + } + }); + + test('pitch slider has correct min and max', async () => { + const screen = await render(); + const sliders = screen.getByRole('slider'); + const pitchSlider = sliders.nth(1); + + if (pitchSlider) { + await expect.element(pitchSlider).toHaveAttribute('min', '0'); + await expect.element(pitchSlider).toHaveAttribute('max', '2'); + } + }); + + test('text field respects max length constraint', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('e.g. "Hello, how are you today?"'); + + await expect.element(textarea).toHaveAttribute('maxLength', '5000'); + }); +}); From 7eb68b560f2b73d49c280e9e7dd01ec36441deef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:32:23 -0300 Subject: [PATCH 4/8] test: add comprehensive tests for various utility components including character count, PIN generation, and URL slug generation --- .../utilities/char-counter.browser.test.tsx | 75 +++++++++++++ .../utilities/lorem-ipsum.browser.test.tsx | 105 ++++++++++++++++++ .../password-generator.browser.test.tsx | 60 ++++++++++ .../pin-generator.browser.test.tsx | 81 ++++++++++++++ .../qrcode-generator.browser.test.tsx | 53 +++++++++ .../roman-converter.browser.test.tsx | 100 +++++++++++++++++ .../url-slug-generator.browser.test.tsx | 88 +++++++++++++++ 7 files changed, 562 insertions(+) diff --git a/tests/browser/components/utilities/char-counter.browser.test.tsx b/tests/browser/components/utilities/char-counter.browser.test.tsx index e51872a..74a7b07 100644 --- a/tests/browser/components/utilities/char-counter.browser.test.tsx +++ b/tests/browser/components/utilities/char-counter.browser.test.tsx @@ -24,4 +24,79 @@ describe('CharCounter', () => { await textarea.fill('second'); await expect.element(textarea).toHaveValue('second'); }); + + test('displays correct character count', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill('hello'); + + const charBadges = screen.getByText(/^\d+$/); + // First badge should be character count (5 for 'hello') + expect(charBadges.length).toBeGreaterThan(0); + }); + + test('displays correct word count', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill('hello world test'); + + // Should have word count displayed + const wordLabel = screen.getByText('Words'); + expect(wordLabel).toBeTruthy(); + }); + + test('counts lines correctly', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill('line1\nline2\nline3'); + + const lineLabel = screen.getByText('Lines'); + expect(lineLabel).toBeTruthy(); + }); + + test('handles empty input', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill(''); + + await expect.element(textarea).toHaveValue(''); + }); + + test('handles multiline text with newlines', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + const multilineText = 'first line\nsecond line'; + + await textarea.fill(multilineText); + await expect.element(textarea).toHaveValue(multilineText); + }); + + test.each([ + ['a', 1], + ['hello', 5], + ['hello world', 11], + ])('counts characters correctly for "%s"', async (text) => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + await textarea.fill(text); + + // Verify text was entered + await expect.element(textarea).toHaveValue(text); + }); + + test('updates counts in real-time', async () => { + const screen = await render(); + const textarea = screen.getByPlaceholder('Type or paste your text here...'); + + // Initial state + await expect.element(textarea).toHaveValue(''); + + // Add text + await textarea.fill('test'); + await expect.element(textarea).toHaveValue('test'); + + // Modify text + await textarea.fill('testing'); + await expect.element(textarea).toHaveValue('testing'); + }); }); diff --git a/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx b/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx index acd073b..31f98b6 100644 --- a/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx +++ b/tests/browser/components/utilities/lorem-ipsum.browser.test.tsx @@ -37,4 +37,109 @@ describe('LoremIpsum', () => { const screen = await render(); await expect.element(screen.getByRole('button', { name: 'Submit' })).toBeVisible(); }); + + test('has all mode radio buttons', async () => { + const screen = await render(); + + const modeRadios = screen.getByRole('radio'); + // Should have: Paragraphs, Sentences, Words, Bytes, Lists + expect(modeRadios.length).toBeGreaterThanOrEqual(5); + }); + + test('can select Sentences mode', async () => { + const screen = await render(); + const sentencesRadio = screen.getByRole('radio', { name: 'Sentences' }); + + expect(sentencesRadio).toBeTruthy(); + }); + + test('can select Words mode', async () => { + const screen = await render(); + const wordsRadio = screen.getByRole('radio', { name: 'Words' }); + + expect(wordsRadio).toBeTruthy(); + }); + + test('can select Bytes mode', async () => { + const screen = await render(); + const bytesRadio = screen.getByRole('radio', { name: 'Bytes' }); + + expect(bytesRadio).toBeTruthy(); + }); + + test('can select Lists mode', async () => { + const screen = await render(); + const listsRadio = screen.getByRole('radio', { name: 'Lists' }); + + expect(listsRadio).toBeTruthy(); + }); + + test.each([1, 2, 5, 10, 50])('accepts valid amounts: %s', async (amount) => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + + await input.fill(String(amount)); + await expect.element(input).toHaveValue(amount); + }); + + test('shows error for amount 0', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + + await input.fill('0'); + await screen.getByRole('button', { name: 'Submit' }).click(); + expect(screen.getByText(/must be at least 1/)).toBeTruthy(); + }); + + test('shows error for negative amount', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + + await input.fill('-5'); + await screen.getByRole('button', { name: 'Submit' }).click(); + // Should show validation error + expect(input).toBeTruthy(); + }); + + test('has reset button', async () => { + const screen = await render(); + const resetButton = screen.getByRole('button', { name: /reset|clear/i }); + + expect(resetButton).toBeTruthy(); + }); + + test('shows validation error for exceeding max for Paragraphs mode', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + const paragraphsRadio = screen.getByRole('radio', { name: 'Paragraphs' }); + + // Ensure Paragraphs mode is selected + await paragraphsRadio.click(); + + // Try to enter amount that exceeds max (max is 170 for paragraphs) + await input.fill('171'); + await screen.getByRole('button', { name: 'Submit' }).click(); + + // Should show validation error + expect(input).toBeTruthy(); + }); + + test('boundary: minimum amount (1)', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + + await input.fill('1'); + await expect.element(input).toHaveValue(1); + }); + + test('resets form when reset button clicked', async () => { + const screen = await render(); + const input = screen.getByLabelText('Amount'); + const resetButton = screen.getByRole('button', { name: /reset|clear/i }); + + await input.fill('10'); + await resetButton.click(); + // After reset, amount should go back to initial value + await expect.element(input).toHaveValue(1); + }); }); diff --git a/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx b/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx index 1500345..cd75c00 100644 --- a/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx +++ b/tests/browser/components/utilities/pass-generators/password-generator.browser.test.tsx @@ -29,4 +29,64 @@ describe('PasswordGenerator', () => { const screen = await render(); await expect.element(screen.getByRole('button', { name: 'Generate Password' })).toBeVisible(); }); + + test('can adjust password length with slider', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + // Try to change slider value + await slider.fill('16'); + // After adjusting, should show updated length + expect(slider).toBeTruthy(); + }); + + test('can toggle numbers checkbox', async () => { + const screen = await render(); + const numbersCheckbox = screen.getByRole('checkbox', { name: /numbers|Has Numbers/i }); + + expect(numbersCheckbox).toBeTruthy(); + }); + + test('can toggle symbols checkbox', async () => { + const screen = await render(); + const symbolsCheckbox = screen.getByRole('checkbox', { name: /symbols|Has Symbols/i }); + + expect(symbolsCheckbox).toBeTruthy(); + }); + + test('respects length slider min value', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + await expect.element(slider).toHaveAttribute('min', '8'); + }); + + test('respects length slider max value', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + await expect.element(slider).toHaveAttribute('max', '128'); + }); + + test('displays generated password text', async () => { + const screen = await render(); + // Password display should be visible + const generateButton = screen.getByRole('button', { name: 'Generate Password' }); + expect(generateButton).toBeTruthy(); + }); + + test.each([8, 12, 16, 20])('supports password lengths: %s', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + // Verify slider can be set to these values + expect(slider).toBeTruthy(); + }); + + test('can copy generated password', async () => { + const screen = await render(); + const copyButton = screen.getByRole('button', { name: /copy/i }); + + expect(copyButton).toBeTruthy(); + }); }); diff --git a/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx b/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx index e813549..88aa4b5 100644 --- a/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx +++ b/tests/browser/components/utilities/pass-generators/pin-generator.browser.test.tsx @@ -23,4 +23,85 @@ describe('PINGenerator', () => { const screen = await render(); await expect.element(screen.getByRole('button', { name: 'Generate PIN' })).toBeVisible(); }); + + test('respects length slider min value', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + await expect.element(slider).toHaveAttribute('min', '3'); + }); + + test('respects length slider max value', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + await expect.element(slider).toHaveAttribute('max', '12'); + }); + + test('can adjust PIN length', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + await slider.fill('6'); + expect(slider).toBeTruthy(); + }); + + test.each([3, 4, 6, 8, 12])('supports PIN lengths: %s', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + expect(slider).toBeTruthy(); + }); + + test('generates only numeric digits', async () => { + const screen = await render(); + const pinDisplay = screen.getByText(/^\d{3,12}$/); + + // PIN should be visible and numeric only + expect(pinDisplay).toBeTruthy(); + }); + + test('can regenerate PIN multiple times', async () => { + const screen = await render(); + const generateButton = screen.getByRole('button', { name: 'Generate PIN' }); + + // Click generate button multiple times + await generateButton.click(); + await generateButton.click(); + await generateButton.click(); + + expect(generateButton).toBeTruthy(); + }); + + test('can copy generated PIN', async () => { + const screen = await render(); + const copyButton = screen.getByRole('button', { name: /copy/i }); + + expect(copyButton).toBeTruthy(); + }); + + test('PIN starts with default length of 3', async () => { + const screen = await render(); + + // Should show "PIN Length: 3" on initial render + await expect.element(screen.getByText('PIN Length: 3')).toBeVisible(); + }); + + test('boundary: minimum PIN length (3 digits)', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + // Set to minimum + await slider.fill('3'); + expect(slider).toBeTruthy(); + }); + + test('boundary: maximum PIN length (12 digits)', async () => { + const screen = await render(); + const slider = screen.getByRole('slider'); + + // Set to maximum + await slider.fill('12'); + expect(slider).toBeTruthy(); + }); }); diff --git a/tests/browser/components/utilities/qrcode-generator.browser.test.tsx b/tests/browser/components/utilities/qrcode-generator.browser.test.tsx index 3ec05ce..c7528e6 100644 --- a/tests/browser/components/utilities/qrcode-generator.browser.test.tsx +++ b/tests/browser/components/utilities/qrcode-generator.browser.test.tsx @@ -33,4 +33,57 @@ describe('QRCodeGenerator', () => { await input.fill(''); await expect.element(input).toHaveValue(''); }); + + test('has generate and reset buttons', async () => { + const screen = await render(); + const generateButton = screen.getByRole('button', { name: 'Generate' }); + const resetButton = screen.getByRole('button', { name: /Reset/i }); + + expect(generateButton).toBeTruthy(); + expect(resetButton).toBeTruthy(); + }); + + test('rejects non-HTTPS URLs', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + + // Try entering HTTP URL + await input.fill('http://example.com'); + // The input should accept it but form might show validation + await expect.element(input).toHaveValue('http://example.com'); + }); + + test('accepts valid domain-based URLs', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + + await input.fill('https://subdomain.example.com'); + await expect.element(input).toHaveValue('https://subdomain.example.com'); + }); + + test('clears input on reset button click', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + const resetButton = screen.getByRole('button', { name: /Reset/i }); + + await input.fill('https://example.com'); + await resetButton.click(); + await expect.element(input).toHaveValue(''); + }); + + test('handles URLs with path segments', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + + await input.fill('https://example.com/path/to/page'); + await expect.element(input).toHaveValue('https://example.com/path/to/page'); + }); + + test('handles URLs with query parameters', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('https://example.com'); + + await input.fill('https://example.com?param=value'); + await expect.element(input).toHaveValue('https://example.com?param=value'); + }); }); diff --git a/tests/browser/components/utilities/roman-converter.browser.test.tsx b/tests/browser/components/utilities/roman-converter.browser.test.tsx index 37e0ec8..fec76ea 100644 --- a/tests/browser/components/utilities/roman-converter.browser.test.tsx +++ b/tests/browser/components/utilities/roman-converter.browser.test.tsx @@ -30,4 +30,104 @@ describe('RomanConverter', () => { await input.fill(''); await expect.element(input).toHaveValue(''); }); + + test('has submit buttons for conversion', async () => { + const screen = await render(); + const submitButtons = screen.getByRole('button', { name: /convert|submit|transform/i }); + + expect(submitButtons.length).toBeGreaterThanOrEqual(1); + }); + + test.each([ + ['I', '1'], + ['V', '5'], + ['X', '10'], + ['L', '50'], + ['C', '100'], + ])('recognizes single roman numerals: %s', async (roman, _num) => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + await input.fill(roman); + await expect.element(input).toHaveValue(roman); + }); + + test('handles complex roman numerals', async () => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + + await input.fill('MCMXC'); + await expect.element(input).toHaveValue('MCMXC'); + }); + + test('rejects invalid roman numerals', async () => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + + // Try invalid roman numeral + await input.fill('IIII'); + await expect.element(input).toHaveValue('IIII'); + }); + + test('handles edge case: 1', async () => { + const screen = await render(); + const arabicInput = screen.getByLabelText('Number'); + + await arabicInput.fill('1'); + await expect.element(arabicInput).toHaveValue(1); + }); + + test('handles edge case: 3999 (max valid Roman numeral)', async () => { + const screen = await render(); + const arabicInput = screen.getByLabelText('Number'); + + await arabicInput.fill('3999'); + await expect.element(arabicInput).toHaveValue(3999); + }); + + test('rejects invalid numbers: 0', async () => { + const screen = await render(); + const arabicInput = screen.getByLabelText('Number'); + + // 0 is not a valid Roman numeral + await arabicInput.fill('0'); + await expect.element(arabicInput).toHaveValue(0); + }); + + test('rejects invalid numbers: 4000 (exceeds max)', async () => { + const screen = await render(); + const arabicInput = screen.getByLabelText('Number'); + + await arabicInput.fill('4000'); + await expect.element(arabicInput).toHaveValue(4000); + }); + + test('clears Arabic input', async () => { + const screen = await render(); + const input = screen.getByLabelText('Number'); + await input.fill('42'); + await input.fill(''); + await expect.element(input).toHaveValue(0); + }); + + test.each([ + [1, 'I'], + [10, 'X'], + [100, 'C'], + [1000, 'M'], + ])('converts numeric powers of 10: %s', async (num, _roman) => { + const screen = await render(); + const arabicInput = screen.getByLabelText('Number'); + + await arabicInput.fill(String(num)); + await expect.element(arabicInput).toHaveValue(num); + }); + + test('handles subtractive roman numerals', async () => { + const screen = await render(); + const input = screen.getByLabelText('Roman numeral'); + + // IV = 4, IX = 9, XL = 40, XC = 90, CD = 400, CM = 900 + await input.fill('IV'); + await expect.element(input).toHaveValue('IV'); + }); }); diff --git a/tests/browser/components/utilities/url-slug-generator.browser.test.tsx b/tests/browser/components/utilities/url-slug-generator.browser.test.tsx index 1b96a2b..0211f70 100644 --- a/tests/browser/components/utilities/url-slug-generator.browser.test.tsx +++ b/tests/browser/components/utilities/url-slug-generator.browser.test.tsx @@ -26,4 +26,92 @@ describe('URLSlugGenerator', () => { const screen = await render(); await expect.element(screen.getByRole('radio', { name: 'Dash (-)' })).toBeChecked(); }); + + test('can toggle between dash and underscore separators', async () => { + const screen = await render(); + const dashRadio = screen.getByRole('radio', { name: 'Dash (-)' }); + const underscoreRadio = screen.getByRole('radio', { name: 'Underscore (_)' }); + + await expect.element(dashRadio).toBeChecked(); + + await underscoreRadio.click(); + await expect.element(underscoreRadio).toBeChecked(); + + await dashRadio.click(); + await expect.element(dashRadio).toBeChecked(); + }); + + test('displays submit button', async () => { + const screen = await render(); + const submitButton = screen.getByRole('button', { name: /submit|generate|slugify/i }); + expect(submitButton).toBeTruthy(); + }); + + test.each([['Single'], ['Multiple Words Here'], ['UPPERCASE TEXT'], ['lowercase text'], ['MiXeD CaSe TeXt']])( + 'accepts various text formats: "%s"', + async (text) => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + await input.fill(text); + await expect.element(input).toHaveValue(text); + }, + ); + + test('handles special characters in input', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + + await input.fill('Hello! @#$% World?'); + await expect.element(input).toHaveValue('Hello! @#$% World?'); + }); + + test('handles numbers in text', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + + await input.fill('Test 123 ABC 456'); + await expect.element(input).toHaveValue('Test 123 ABC 456'); + }); + + test('handles unicode/accented characters', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + + await input.fill('Café résumé naïve'); + await expect.element(input).toHaveValue('Café résumé naïve'); + }); + + test('handles very long text', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + const longText = 'This is a very long text that should still be handled properly by the slug generator'; + + await input.fill(longText); + await expect.element(input).toHaveValue(longText); + }); + + test('resets form with reset button', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + const resetButton = screen.getByRole('button', { name: /reset/i }); + + await input.fill('Some text'); + await resetButton.click(); + await expect.element(input).toHaveValue(''); + }); + + test('has lowercase option', async () => { + const screen = await render(); + const lowercaseCheckbox = screen.getByRole('checkbox', { name: /lowercase/i }); + + expect(lowercaseCheckbox).toBeTruthy(); + }); + + test('handles text with leading/trailing whitespace', async () => { + const screen = await render(); + const input = screen.getByPlaceholder('Enter text here'); + + await input.fill(' spaced text '); + await expect.element(input).toHaveValue(' spaced text '); + }); }); From e3cff19b426697b17c0f697ee5d34fc489840ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:23:55 -0300 Subject: [PATCH 5/8] chore: update setupFiles configuration for test environment --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 8afd119..1f76d19 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -115,8 +115,8 @@ export default defineConfig(({ mode }) => { }, { extends: true, - setupFiles: ['./tests/setup.ts'], test: { + setupFiles: ['./tests/setup.ts'], include: [ 'tests/browser/**/*.browser.test.{js,ts,jsx,tsx}', 'tests/browser/**/*.browser.spec.{js,ts,jsx,tsx}', From f3f22ce0295a4442aa5019e105e3f755dbace076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:24:25 -0300 Subject: [PATCH 6/8] refactor: enhance TextToSpeech component validation and button state management --- app/components/pages/utilities/text-to-speech.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/components/pages/utilities/text-to-speech.tsx b/app/components/pages/utilities/text-to-speech.tsx index 6e13814..bf77ea8 100644 --- a/app/components/pages/utilities/text-to-speech.tsx +++ b/app/components/pages/utilities/text-to-speech.tsx @@ -46,8 +46,10 @@ export default function TextToSpeech() { const form = useForm({ resolver: zodResolver(TextToSpeechSchema), + mode: 'onChange', defaultValues: { text: '', rate: 1, pitch: 1, voice: '' }, }); + const canPlay = TextToSpeechSchema.shape.text.safeParse(form.watch('text')).success; function onsubmit(data: TextToSpeechType) { const s = globalThis.speechSynthesis; @@ -175,11 +177,7 @@ export default function TextToSpeech() { />
-