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() {
/>
-