diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eed0a66..0c14bac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Cache APT packages uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev + packages: libgtk-4-dev libwebkitgtk-6.0-dev version: 1.0 execute_install_scripts: false @@ -133,7 +133,7 @@ jobs: - name: Install system deps (Wails CGO) run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev - name: Run govulncheck run: | @@ -162,7 +162,7 @@ jobs: - name: Cache APT packages uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev + packages: libgtk-4-dev libwebkitgtk-6.0-dev version: 1.0 execute_install_scripts: false @@ -192,4 +192,3 @@ jobs: - name: Run frontend tests run: | cd frontend && bun run test - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0066390..f18828f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev - name: Install Task uses: arduino/setup-task@v2 diff --git a/frontend/e2e/barcodeGenerator.spec.js b/frontend/e2e/barcodeGenerator.spec.js index 7528150..196c5e5 100644 --- a/frontend/e2e/barcodeGenerator.spec.js +++ b/frontend/e2e/barcodeGenerator.spec.js @@ -6,7 +6,7 @@ test.describe('Barcode / QR Code Generator', () => { }); test('should load with QR Code type selected', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Barcode / QR Code' })).toBeVisible(); + await expect(page.getByRole('heading', { name: /Barcode \/ QR Code/ })).toBeVisible(); await expect(page.getByText('Generate high-quality QR codes and barcodes')).toBeVisible(); // QR type should be active await expect(page.getByRole('button', { name: 'QR Code' })).toBeVisible(); @@ -77,15 +77,10 @@ test.describe('Barcode / QR Code Generator', () => { }); test('should toggle layout orientation', async ({ page }) => { - // Layout toggle button should exist (Columns icon) - const layoutButton = page - .locator('button') - .filter({ has: page.locator('svg') }) - .filter({ hasNotText: /Clear|PNG|SVG|QR|Code|EAN/ }) - .first(); + const layoutButton = page.getByRole('button', { name: 'Toggle layout orientation' }); await layoutButton.click(); // After toggle, the layout should be vertical (single column grid) // Just verify the button is clickable and no error occurs - await expect(page.getByRole('heading', { name: 'Barcode / QR Code' })).toBeVisible(); + await expect(page.getByRole('heading', { name: /Barcode \/ QR Code/ })).toBeVisible(); }); }); diff --git a/frontend/e2e/codeConverter.spec.js b/frontend/e2e/codeConverter.spec.js index c3811d9..22c843f 100644 --- a/frontend/e2e/codeConverter.spec.js +++ b/frontend/e2e/codeConverter.spec.js @@ -1,4 +1,11 @@ import { test, expect } from '@playwright/test'; +import { + fillEditor, + readEditorText, + expectEditorText, + expectEditorContains, + expectEditorNotEmpty, +} from './helpers/editor'; test.describe('Code Converter', () => { test.beforeEach(async ({ page }) => { @@ -9,142 +16,104 @@ test.describe('Code Converter', () => { test('loads with default method JSON ↔ YAML', async ({ page }) => { const select = page.locator('select'); await expect(select).toHaveValue('JSON ↔ YAML'); - await expect(page.locator('textarea')).toHaveCount(2); + await expect(page.getByTestId('code-converter-input')).toBeVisible(); + await expect(page.getByTestId('code-converter-output')).toBeVisible(); }); test('converts JSON to YAML', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('{"name": "test", "value": 42}'); - await expect(output).toHaveValue(/name:\s*test/); - await expect(output).toHaveValue(/value:\s*42/); + await fillEditor(page, 'code-converter-input', '{"name": "test", "value": 42}'); + await expectEditorContains(page, 'code-converter-output', /name:\s*test/); + await expectEditorContains(page, 'code-converter-output', /value:\s*42/); }); test('converts YAML to JSON', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('name: hello\nitems:\n - a\n - b'); - await expect(output).toHaveValue(/\"name\":\s*\"hello\"/); + await fillEditor(page, 'code-converter-input', 'name: hello\nitems:\n - a\n - b'); + await expectEditorContains(page, 'code-converter-output', /"name":\s*"hello"/); }); test('converts JSON to XML', async ({ page }) => { await page.locator('select').selectOption('JSON ↔ XML'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('{"hello": "world"}'); - await expect(output).toHaveValue(//); - await expect(output).toHaveValue(/world/); + await fillEditor(page, 'code-converter-input', '{"hello": "world"}'); + await expectEditorContains(page, 'code-converter-output', //); + await expectEditorContains(page, 'code-converter-output', /world/); }); test('converts XML to JSON', async ({ page }) => { await page.locator('select').selectOption('JSON ↔ XML'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('test'); - await expect(output).toHaveValue(/\"name\"/); + await fillEditor(page, 'code-converter-input', 'test'); + await expectEditorContains(page, 'code-converter-output', /"name"/); }); test('case swapping', async ({ page }) => { await page.locator('select').selectOption('Case Swapping'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('Hello World'); - await expect(output).toHaveValue('hELLO wORLD'); + await fillEditor(page, 'code-converter-input', 'Hello World'); + await expectEditorText(page, 'code-converter-output', 'hELLO wORLD'); }); test('converts CSV to JSON', async ({ page }) => { await page.locator('select').selectOption('JSON ↔ CSV / TSV'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('name,age\nAlice,30\nBob,25'); - await expect(output).toHaveValue(/\"name\":\s*\"Alice\"/); - await expect(output).toHaveValue(/\"age\":\s*\"30\"/); + await fillEditor(page, 'code-converter-input', 'name,age\nAlice,30\nBob,25'); + await expectEditorContains(page, 'code-converter-output', /"name":\s*"Alice"/); + await expectEditorContains(page, 'code-converter-output', /"age":\s*"30"/); }); test('converts CSV to TSV', async ({ page }) => { await page.locator('select').selectOption('CSV ↔ TSV'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('name,age\nAlice,30'); - await expect(output).toHaveValue(/name\tage/); + await fillEditor(page, 'code-converter-input', 'name,age\nAlice,30'); + await expectEditorContains(page, 'code-converter-output', /name\tage/); }); test('converts Key-Value to Query String', async ({ page }) => { await page.locator('select').selectOption('Key-Value ↔ Query String'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('foo=bar\nbaz=qux'); - await expect(output).not.toHaveValue(''); - const value = await output.inputValue(); + await fillEditor(page, 'code-converter-input', 'foo=bar\nbaz=qux'); + await expectEditorNotEmpty(page, 'code-converter-output'); + const value = await readEditorText(page, 'code-converter-output'); expect(value).toContain('baz=qux'); expect(value).toContain('foo=bar'); }); test('converts Properties to JSON', async ({ page }) => { await page.locator('select').selectOption('Properties ↔ JSON'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('app.name=MyApp\napp.version=1.0'); - await expect(output).toHaveValue(/\"app.name\":\s*\"MyApp\"/); + await fillEditor(page, 'code-converter-input', 'app.name=MyApp\napp.version=1.0'); + await expectEditorContains(page, 'code-converter-output', /"app.name":\s*"MyApp"/); }); test('converts INI to JSON', async ({ page }) => { await page.locator('select').selectOption('INI ↔ JSON'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('[section]\nkey=value'); - await expect(output).toHaveValue(/\"section\"/); - await expect(output).toHaveValue(/\"key\":\s*\"value\"/); + await fillEditor(page, 'code-converter-input', '[section]\nkey=value'); + await expectEditorContains(page, 'code-converter-output', /"section"/); + await expectEditorContains(page, 'code-converter-output', /"key":\s*"value"/); }); test('converts curl to fetch', async ({ page }) => { await page.locator('select').selectOption('CURL ↔ Fetch'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill( + await fillEditor( + page, + 'code-converter-input', 'curl -X POST https://api.example.com/data -H \'Content-Type: application/json\' -d \'{"key":"value"}\'' ); - await expect(output).toHaveValue(/fetch\(/); - await expect(output).toHaveValue(/method:\s*'POST'/); + await expectEditorContains(page, 'code-converter-output', /fetch\(/); + await expectEditorContains(page, 'code-converter-output', /method:\s*'POST'/); }); test('converts cron expression to text', async ({ page }) => { await page.locator('select').selectOption('Cron ↔ Text'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('0 9 * * 1'); - await expect(output).toHaveValue(/Monday/); + await fillEditor(page, 'code-converter-input', '0 9 * * 1'); + await expectEditorContains(page, 'code-converter-output', /Monday/); }); test('shows error for invalid JSON with YAML method', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('not valid json'); + await fillEditor(page, 'code-converter-input', 'not valid json'); // Error should appear in the output pane (border turns red) - const outputPane = page.locator('textarea').nth(1); - await expect(outputPane).toHaveValue(''); + await expectEditorText(page, 'code-converter-output', ''); }); test('clears output when input is cleared', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('{"test": true}'); - await expect(output).not.toHaveValue(''); + await fillEditor(page, 'code-converter-input', '{"test": true}'); + await expectEditorNotEmpty(page, 'code-converter-output'); - await input.fill(''); - await expect(output).toHaveValue(''); + await fillEditor(page, 'code-converter-input', ''); + await expectEditorText(page, 'code-converter-output', ''); }); test('layout toggle switches between horizontal and vertical', async ({ page }) => { @@ -169,8 +138,7 @@ test.describe('Code Converter', () => { test('copy button copies output to clipboard', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - const input = page.locator('textarea').first(); - await input.fill('{"copied": true}'); + await fillEditor(page, 'code-converter-input', '{"copied": true}'); const copyButton = page.locator('button[title="Copy to clipboard"]').first(); await copyButton.click(); diff --git a/frontend/e2e/codeEncoder.spec.js b/frontend/e2e/codeEncoder.spec.js index 13b124a..0a4760c 100644 --- a/frontend/e2e/codeEncoder.spec.js +++ b/frontend/e2e/codeEncoder.spec.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { fillEditor, expectEditorText, expectEditorNotEmpty } from './helpers/editor'; test.describe('Code Encoder', () => { test.beforeEach(async ({ page }) => { @@ -8,84 +9,60 @@ test.describe('Code Encoder', () => { test('loads with default method Base64 and Encode mode', async ({ page }) => { await expect(page.locator('select')).toHaveValue('Base64'); - await expect(page.locator('button').filter({ hasText: 'Encode' })).toHaveCSS( - 'background-color', - 'rgb(39, 39, 42)' + await expect(page.locator('button').filter({ hasText: 'Encode' })).toHaveAttribute( + 'style', + /background-color: var\(--border\)/ ); }); test('encodes text to Base64', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue('aGVsbG8='); + await fillEditor(page, 'code-encoder-input', 'hello'); + await expectEditorText(page, 'code-encoder-output', 'aGVsbG8='); }); test('decodes Base64 to text', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('aGVsbG8gd29ybGQ='); + await fillEditor(page, 'code-encoder-input', 'aGVsbG8gd29ybGQ='); await page.locator('button').filter({ hasText: 'Decode' }).click(); - await expect(output).toHaveValue('hello world'); + await expectEditorText(page, 'code-encoder-output', 'hello world'); }); test('encodes text to Hex (Base16)', async ({ page }) => { await page.locator('select').selectOption('Base16 (Hex)'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue('68656c6c6f'); + await fillEditor(page, 'code-encoder-input', 'hello'); + await expectEditorText(page, 'code-encoder-output', '68656c6c6f'); }); test('encodes text to Binary', async ({ page }) => { await page.locator('select').selectOption('Binary'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('AB'); - await expect(output).toHaveValue(/[01]+/); + await fillEditor(page, 'code-encoder-input', 'AB'); + await expectEditorText(page, 'code-encoder-output', /[01]+/); }); test('encodes text to ROT13', async ({ page }) => { await page.locator('select').selectOption('ROT13'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue('uryyb'); + await fillEditor(page, 'code-encoder-input', 'hello'); + await expectEditorText(page, 'code-encoder-output', 'uryyb'); }); test('escapes URL special characters', async ({ page }) => { await page.locator('select').selectOption('URL'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello world & more'); + await fillEditor(page, 'code-encoder-input', 'hello world & more'); await page.locator('button').filter({ hasText: 'Escape' }).first().click(); - await expect(output).toHaveValue(/hello\+world/); + await expectEditorText(page, 'code-encoder-output', /hello(\+|%20)world/); }); test('unescapes URL encoded text', async ({ page }) => { await page.locator('select').selectOption('URL'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello%20world'); + await fillEditor(page, 'code-encoder-input', 'hello%20world'); await page.locator('button').filter({ hasText: 'Unescape' }).first().click(); - await expect(output).toHaveValue('hello world'); + await expectEditorText(page, 'code-encoder-output', 'hello world'); }); test('escapes HTML entities', async ({ page }) => { await page.locator('select').selectOption('HTML/XML'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('
hello
'); + await fillEditor(page, 'code-encoder-input', '
hello
'); await page.locator('button').filter({ hasText: 'Escape' }).first().click(); - await expect(output).toHaveValue(/</); + await expectEditorText(page, 'code-encoder-output', /</); }); test('mode toggle labels change for escape methods', async ({ page }) => { @@ -129,8 +106,7 @@ test.describe('Code Encoder', () => { test('copy button copies output to clipboard', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - const input = page.locator('textarea').first(); - await input.fill('copy test'); + await fillEditor(page, 'code-encoder-input', 'copy test'); const copyButton = page.locator('button[title="Copy to clipboard"]').first(); await copyButton.click(); @@ -140,31 +116,22 @@ test.describe('Code Encoder', () => { }); test('shows error for invalid Base64 input', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('!!!invalid!!!'); + await fillEditor(page, 'code-encoder-input', '!!!invalid!!!'); await page.locator('button').filter({ hasText: 'Decode' }).click(); - await expect(output).toHaveValue(''); + await expectEditorText(page, 'code-encoder-output', ''); }); test('clears output when input is cleared', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); + await fillEditor(page, 'code-encoder-input', 'test'); + await expectEditorNotEmpty(page, 'code-encoder-output'); - await input.fill('test'); - await expect(output).not.toHaveValue(''); - - await input.fill(''); - await expect(output).toHaveValue(''); + await fillEditor(page, 'code-encoder-input', ''); + await expectEditorText(page, 'code-encoder-output', ''); }); test('encodes text to Morse Code', async ({ page }) => { await page.locator('select').selectOption('Morse Code'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('SOS'); - await expect(output).toHaveValue(/\.\.\./); + await fillEditor(page, 'code-encoder-input', 'SOS'); + await expectEditorText(page, 'code-encoder-output', /\.\.\./); }); }); diff --git a/frontend/e2e/codeEncrypter.spec.js b/frontend/e2e/codeEncrypter.spec.js index feb21e4..7aed7e8 100644 --- a/frontend/e2e/codeEncrypter.spec.js +++ b/frontend/e2e/codeEncrypter.spec.js @@ -1,4 +1,11 @@ import { test, expect } from '@playwright/test'; +import { + fillEditor, + readEditorText, + expectEditorContains, + expectEditorText, + expectEditorNotEmpty, +} from './helpers/editor'; test.describe('Code Encrypter', () => { test.beforeEach(async ({ page }) => { @@ -8,24 +15,22 @@ test.describe('Code Encrypter', () => { test('loads with default method AES and encrypt mode', async ({ page }) => { await expect(page.locator('select').first()).toHaveValue('AES'); - await expect(page.locator('button').filter({ hasText: 'Encrypt' }).first()).toHaveCSS( - 'background-color', - 'rgb(37, 99, 235)' + await expect(page.locator('button').filter({ hasText: 'Encrypt' }).first()).toHaveAttribute( + 'style', + /background-color: var\(--primary\)/ ); }); test('encrypts text with AES', async ({ page }) => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - await keyInput.fill('mykey12345678901234567890123456'); + await keyInput.fill('mykey123456789012345678901234567'); await ivInput.fill('myiv123456789012'); - await textInput.fill('Hello World'); + await fillEditor(page, 'code-encrypter-input', 'Hello World'); - await expect(output).not.toHaveValue(''); - const encrypted = await output.inputValue(); + await expectEditorNotEmpty(page, 'code-encrypter-output'); + const encrypted = await readEditorText(page, 'code-encrypter-output'); expect(encrypted.length).toBeGreaterThan(0); expect(encrypted).not.toBe('Hello World'); }); @@ -33,16 +38,14 @@ test.describe('Code Encrypter', () => { test('decrypts AES-encrypted text back to original', async ({ page }) => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - await keyInput.fill('mykey12345678901234567890123456'); + await keyInput.fill('mykey123456789012345678901234567'); await ivInput.fill('myiv123456789012'); - await textInput.fill('Secret Message'); + await fillEditor(page, 'code-encrypter-input', 'Secret Message'); // Wait for encryption - await expect(output).not.toHaveValue(''); - const encrypted = await output.inputValue(); + await expectEditorNotEmpty(page, 'code-encrypter-output'); + const encrypted = await readEditorText(page, 'code-encrypter-output'); expect(encrypted.length).toBeGreaterThan(0); // Skip if backend returns object instead of string @@ -53,13 +56,14 @@ test.describe('Code Encrypter', () => { // Switch to decrypt mode await page.locator('button').filter({ hasText: 'Decrypt' }).first().click(); + await expect(page.locator('span').filter({ hasText: /^Decrypt$/ })).toBeVisible(); // Clear input and enter encrypted text - await textInput.fill(encrypted); + await fillEditor(page, 'code-encrypter-input', encrypted); // Should decrypt back to something readable (may have padding differences) - await expect(output).not.toHaveValue(''); - const decrypted = await output.inputValue(); + await expectEditorContains(page, 'code-encrypter-output', 'Secret'); + const decrypted = await readEditorText(page, 'code-encrypter-output'); expect(decrypted).toContain('Secret'); }); @@ -68,17 +72,15 @@ test.describe('Code Encrypter', () => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); // XOR should show key but no IV await expect(keyInput).toBeVisible(); await expect(ivInput).not.toBeVisible(); await keyInput.fill('secret'); - await textInput.fill('Hello'); + await fillEditor(page, 'code-encrypter-input', 'Hello'); - await expect(output).not.toHaveValue(''); + await expectEditorNotEmpty(page, 'code-encrypter-output'); }); test('shows RSA public/private key inputs for RSA method', async ({ page }) => { @@ -94,10 +96,7 @@ test.describe('Code Encrypter', () => { }); test('encrypt/decrypt mode toggle changes output labels', async ({ page }) => { - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await textInput.fill('test'); + await fillEditor(page, 'code-encrypter-input', 'test'); // Check output pane indicator shows "Encrypt" when in encrypt mode await expect(page.locator('span').filter({ hasText: /^Encrypt$/ })).toBeVisible(); @@ -132,18 +131,16 @@ test.describe('Code Encrypter', () => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - await keyInput.fill('mykey12345678901234567890123456'); + await keyInput.fill('mykey123456789012345678901234567'); await ivInput.fill('myiv123456789012'); - await textInput.fill('Manual Test'); + await fillEditor(page, 'code-encrypter-input', 'Manual Test'); // Output should be empty before clicking convert - await expect(output).toHaveValue(''); + await expectEditorText(page, 'code-encrypter-output', ''); await page.locator('button').filter({ hasText: 'Convert' }).click(); - await expect(output).not.toHaveValue(''); + await expectEditorNotEmpty(page, 'code-encrypter-output'); }); test('layout toggle switches between horizontal and vertical', async ({ page }) => { @@ -167,11 +164,10 @@ test.describe('Code Encrypter', () => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - await keyInput.fill('mykey12345678901234567890123456'); + await keyInput.fill('mykey123456789012345678901234567'); await ivInput.fill('myiv123456789012'); - await textInput.fill('copy me'); + await fillEditor(page, 'code-encrypter-input', 'copy me'); const copyButton = page.locator('button[title="Copy to clipboard"]').nth(1); await copyButton.click(); @@ -183,17 +179,15 @@ test.describe('Code Encrypter', () => { test('shows error for empty key with AES', async ({ page }) => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); await keyInput.fill(''); await ivInput.fill('myiv123456789012'); - await textInput.fill('test'); + await fillEditor(page, 'code-encrypter-input', 'test'); // With empty key, either no output or error appears await expect .poll(async () => { - const val = await output.inputValue(); + const val = await readEditorText(page, 'code-encrypter-output'); const hasError = await page .locator('div') .filter({ hasText: /error|Error|fail/i }) @@ -208,16 +202,14 @@ test.describe('Code Encrypter', () => { test('clears output when input is cleared', async ({ page }) => { const keyInput = page.locator('input[placeholder*="encryption key"]').first(); const ivInput = page.locator('input[placeholder*="IV"]').first(); - const textInput = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - await keyInput.fill('mykey12345678901234567890123456'); + await keyInput.fill('mykey123456789012345678901234567'); await ivInput.fill('myiv123456789012'); - await textInput.fill('something'); - await expect(output).not.toHaveValue(''); + await fillEditor(page, 'code-encrypter-input', 'something'); + await expectEditorNotEmpty(page, 'code-encrypter-output'); - await textInput.fill(''); - await expect(output).toHaveValue(''); + await fillEditor(page, 'code-encrypter-input', ''); + await expectEditorText(page, 'code-encrypter-output', ''); }); test('persists method selection in localStorage', async ({ page }) => { diff --git a/frontend/e2e/codeFormatter.spec.js b/frontend/e2e/codeFormatter.spec.js index 1fdaf49..d970246 100644 --- a/frontend/e2e/codeFormatter.spec.js +++ b/frontend/e2e/codeFormatter.spec.js @@ -1,4 +1,11 @@ import { test, expect } from '@playwright/test'; +import { + fillEditor, + readEditorText, + expectEditorContains, + expectEditorNotEmpty, + expectEditorText, +} from './helpers/editor'; test.describe('Code Formatter', () => { test.beforeEach(async ({ page }) => { @@ -8,40 +15,36 @@ test.describe('Code Formatter', () => { test('loads with JSON default and empty panes', async ({ page }) => { await expect(page.getByRole('button', { name: 'JSON' }).first()).toBeVisible(); - await expect(page.locator('textarea').first()).toHaveValue(''); - await expect(page.locator('pre code')).not.toBeVisible(); + await expectEditorText(page, 'code-formatter-input', ''); + await expect(page.getByTestId('code-formatter-output')).not.toBeVisible(); await expect(page.locator('input[placeholder=".users[].name"]')).toBeVisible(); }); test('load sample fills input', async ({ page }) => { await page.getByRole('button', { name: 'Load Sample' }).click(); - const input = page.locator('textarea').first(); - await expect(input).not.toHaveValue(''); - await expect(input).toContainText('users'); + await expectEditorNotEmpty(page, 'code-formatter-input'); + await expectEditorContains(page, 'code-formatter-input', 'users'); }); test('format produces output', async ({ page }) => { await page.getByRole('button', { name: 'Load Sample' }).click(); - const outputCode = page.locator('pre code'); - await expect(outputCode).toBeVisible(); - await expect(outputCode).not.toHaveText(''); + await expectEditorNotEmpty(page, 'code-formatter-output'); }); test('minify toggle changes output', async ({ page }) => { await page.getByRole('button', { name: 'Load Sample' }).click(); - const outputCode = page.locator('pre code'); - await expect(outputCode).toBeVisible(); + await expectEditorNotEmpty(page, 'code-formatter-output'); - const formattedText = await outputCode.textContent(); + const formattedText = await readEditorText(page, 'code-formatter-output'); expect(formattedText).toContain('\n'); await page.getByRole('button', { name: 'Minify' }).click(); await page.waitForTimeout(400); - const minifiedText = await outputCode.textContent(); + const minifiedText = await readEditorText(page, 'code-formatter-output'); const formattedNewlines = (formattedText.match(/\n/g) || []).length; const minifiedNewlines = (minifiedText.match(/\n/g) || []).length; expect(minifiedNewlines).toBeLessThan(formattedNewlines); @@ -58,13 +61,11 @@ test.describe('Code Formatter', () => { await page.getByRole('button', { name: 'Load Sample' }).click(); - const input = page.locator('textarea').first(); - await expect(input).toContainText(' { - const input = page.locator('textarea').first(); - await input.fill('{ invalid json }'); + await fillEditor(page, 'code-formatter-input', '{ invalid json }'); await expect(page.getByText(/invalid JSON/i)).toBeVisible({ timeout: 2000 }); }); @@ -91,9 +92,11 @@ test.describe('Code Formatter', () => { test('output has syntax highlighting', async ({ page }) => { await page.getByRole('button', { name: 'Load Sample' }).click(); - const codeBlock = page.locator('pre code'); - await expect(codeBlock).toBeVisible(); - await expect(codeBlock).toHaveClass(/language-json/); + await expect(page.getByTestId('code-formatter-output')).toBeVisible(); + await expect(page.getByTestId('code-formatter-output-content')).toHaveAttribute( + 'aria-label', + 'Formatted Output' + ); }); test('filter input filters JSON output', async ({ page }) => { @@ -103,8 +106,7 @@ test.describe('Code Formatter', () => { await filterInput.fill('.users[].name'); await page.waitForTimeout(500); - const outputCode = page.locator('pre code'); - const text = await outputCode.textContent(); + const text = await readEditorText(page, 'code-formatter-output'); expect(text).toContain('Alice'); expect(text).toContain('Bob'); expect(text).not.toContain('age'); diff --git a/frontend/e2e/dataGenerator.spec.js b/frontend/e2e/dataGenerator.spec.js index 30b636b..0713486 100644 --- a/frontend/e2e/dataGenerator.spec.js +++ b/frontend/e2e/dataGenerator.spec.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { readEditorText, expectEditorNotEmpty, expectEditorText } from './helpers/editor'; test.describe('Data Generator', () => { test.beforeEach(async ({ page }) => { @@ -12,26 +13,29 @@ test.describe('Data Generator', () => { await expect(page.locator('input[placeholder="Field name"]')).toHaveCount(5); await expect(page.locator('input[placeholder="Field name"]').first()).toHaveValue('id'); - const output = page.locator('textarea[readonly]'); - await expect(output).toHaveValue(''); - await expect(page.getByRole('button', { name: 'Copy to Clipboard' })).not.toBeVisible(); + await expectEditorText(page, 'data-generator-output', ''); + await expect( + page.getByRole('button', { name: 'Copy to Clipboard', exact: true }) + ).not.toBeAttached(); }); test('generate button produces output', async ({ page }) => { await page.getByRole('button', { name: 'Generate' }).click(); - const output = page.locator('textarea[readonly]'); - await expect(output).not.toHaveValue(''); - await expect(output).not.toHaveValue('Generated data will appear here...'); + await expectEditorNotEmpty(page, 'data-generator-output'); + await expectEditorText( + page, + 'data-generator-output', + /^(?!Generated data will appear here\.\.\.)/ + ); }); test('output is valid JSON when format is JSON', async ({ page }) => { await page.getByRole('button', { name: 'Generate' }).click(); - const output = page.locator('textarea[readonly]'); - await expect(output).not.toHaveValue(''); + await expectEditorNotEmpty(page, 'data-generator-output'); - const text = await output.inputValue(); + const text = await readEditorText(page, 'data-generator-output'); const parsed = JSON.parse(text); expect(Array.isArray(parsed)).toBe(true); expect(parsed.length).toBe(10); @@ -43,10 +47,9 @@ test.describe('Data Generator', () => { await page.getByRole('button', { name: 'Generate' }).click(); - const output = page.locator('textarea[readonly]'); - await expect(output).not.toHaveValue(''); + await expectEditorNotEmpty(page, 'data-generator-output'); - const text = await output.inputValue(); + const text = await readEditorText(page, 'data-generator-output'); expect(text).toContain(','); expect(text).not.toContain('['); }); @@ -72,14 +75,14 @@ test.describe('Data Generator', () => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); await page.getByRole('button', { name: 'Generate' }).click(); - const output = page.locator('textarea[readonly]'); - await expect(output).not.toHaveValue(''); + await expectEditorNotEmpty(page, 'data-generator-output'); - await expect(page.getByRole('button', { name: 'Copy to Clipboard' })).toBeVisible(); - await page.getByRole('button', { name: 'Copy to Clipboard' }).click(); + const copyButton = page.getByRole('button', { name: 'Copy to Clipboard', exact: true }); + await expect(copyButton).toBeVisible(); + await copyButton.click(); const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); - const outputText = await output.inputValue(); + const outputText = await readEditorText(page, 'data-generator-output'); expect(clipboardText).toBe(outputText); }); diff --git a/frontend/e2e/hashGenerator.spec.js b/frontend/e2e/hashGenerator.spec.js index dd28c8a..1f36c9d 100644 --- a/frontend/e2e/hashGenerator.spec.js +++ b/frontend/e2e/hashGenerator.spec.js @@ -1,4 +1,10 @@ import { test, expect } from '@playwright/test'; +import { + fillEditor, + readEditorText, + expectEditorText, + expectEditorNotEmpty, +} from './helpers/editor'; test.describe('Hash Generator', () => { test.beforeEach(async ({ page }) => { @@ -8,40 +14,30 @@ test.describe('Hash Generator', () => { test('loads with default method MD5', async ({ page }) => { await expect(page.locator('select')).toHaveValue('MD5'); - await expect(page.locator('textarea')).toHaveCount(2); + await expect(page.getByTestId('hash-generator-input')).toBeVisible(); + await expect(page.getByTestId('hash-generator-output')).toBeVisible(); }); test('generates MD5 hash', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue('5d41402abc4b2a76b9719d911017c592'); + await fillEditor(page, 'hash-generator-input', 'hello'); + await expectEditorText(page, 'hash-generator-output', '5d41402abc4b2a76b9719d911017c592'); }); test('generates SHA-256 hash', async ({ page }) => { await page.locator('select').selectOption('SHA-256'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue(/[a-f0-9]{64}/); + await fillEditor(page, 'hash-generator-input', 'hello'); + await expectEditorText(page, 'hash-generator-output', /[a-f0-9]{64}/); }); test('generates SHA-1 hash', async ({ page }) => { await page.locator('select').selectOption('SHA-1'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).toHaveValue(/[a-f0-9]{40}/); + await fillEditor(page, 'hash-generator-input', 'hello'); + await expectEditorText(page, 'hash-generator-output', /[a-f0-9]{40}/); }); test('generates all hashes at once', async ({ page }) => { await page.locator('select').selectOption('All'); - const input = page.locator('textarea').first(); - - await input.fill('hello'); + await fillEditor(page, 'hash-generator-input', 'hello'); // All hashes mode shows a multi-hash output panel instead of textarea await expect(page.locator('span').filter({ hasText: 'All Hashes' })).toBeVisible(); @@ -58,35 +54,27 @@ test.describe('Hash Generator', () => { test('generates HMAC with key', async ({ page }) => { await page.locator('select').selectOption('HMAC'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); const hmacInput = page.locator('input[placeholder="HMAC Key"]'); await hmacInput.fill('secretkey'); - await input.fill('hello'); + await fillEditor(page, 'hash-generator-input', 'hello'); - await expect(output).not.toHaveValue(''); - const hash = await output.inputValue(); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash = await readEditorText(page, 'hash-generator-output'); expect(hash.length).toBeGreaterThan(0); }); test('generates CRC32 hash', async ({ page }) => { await page.locator('select').selectOption('CRC32'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).not.toHaveValue(''); + await fillEditor(page, 'hash-generator-input', 'hello'); + await expectEditorNotEmpty(page, 'hash-generator-output'); }); test('generates bcrypt hash', async ({ page }) => { await page.locator('select').selectOption('bcrypt'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('password123'); - await expect(output).not.toHaveValue(''); - const hash = await output.inputValue(); + await fillEditor(page, 'hash-generator-input', 'password123'); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash = await readEditorText(page, 'hash-generator-output'); expect(hash).toContain('$'); }); @@ -109,8 +97,7 @@ test.describe('Hash Generator', () => { test('copy button copies output to clipboard', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - const input = page.locator('textarea').first(); - await input.fill('copy hash test'); + await fillEditor(page, 'hash-generator-input', 'copy hash test'); const copyButton = page.locator('button[title="Copy to clipboard"]').nth(1); await copyButton.click(); @@ -120,58 +107,48 @@ test.describe('Hash Generator', () => { }); test('clears output when input is cleared', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('test'); - await expect(output).not.toHaveValue(''); + await fillEditor(page, 'hash-generator-input', 'test'); + await expectEditorNotEmpty(page, 'hash-generator-output'); - await input.fill(''); - await expect(output).toHaveValue(''); + await fillEditor(page, 'hash-generator-input', ''); + await expectEditorText(page, 'hash-generator-output', ''); }); test('different inputs produce different hashes', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('input1'); - await expect(output).not.toHaveValue(''); - const hash1 = await output.inputValue(); + await fillEditor(page, 'hash-generator-input', 'input1'); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash1 = await readEditorText(page, 'hash-generator-output'); // Wait for debounce to settle before second input await page.waitForTimeout(500); - await input.fill('something-completely-different'); + await fillEditor(page, 'hash-generator-input', 'something-completely-different'); // Wait for output to actually change to a new value - await expect.poll(async () => await output.inputValue()).not.toBe(hash1); - const hash2 = await output.inputValue(); + await expect + .poll(async () => await readEditorText(page, 'hash-generator-output')) + .not.toBe(hash1); + const hash2 = await readEditorText(page, 'hash-generator-output'); expect(hash1).not.toBe(hash2); }); test('same input produces same hash', async ({ page }) => { - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); + await fillEditor(page, 'hash-generator-input', 'consistent'); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash1 = await readEditorText(page, 'hash-generator-output'); - await input.fill('consistent'); - await expect(output).not.toHaveValue(''); - const hash1 = await output.inputValue(); - - await input.fill(''); - await input.fill('consistent'); - await expect(output).not.toHaveValue(''); - const hash2 = await output.inputValue(); + await fillEditor(page, 'hash-generator-input', ''); + await fillEditor(page, 'hash-generator-input', 'consistent'); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash2 = await readEditorText(page, 'hash-generator-output'); expect(hash1).toBe(hash2); }); test('generates BLAKE3 hash', async ({ page }) => { await page.locator('select').selectOption('BLAKE3'); - const input = page.locator('textarea').first(); - const output = page.locator('textarea').nth(1); - - await input.fill('hello'); - await expect(output).not.toHaveValue(''); - const hash = await output.inputValue(); + await fillEditor(page, 'hash-generator-input', 'hello'); + await expectEditorNotEmpty(page, 'hash-generator-output'); + const hash = await readEditorText(page, 'hash-generator-output'); expect(hash.length).toBeGreaterThanOrEqual(32); }); }); diff --git a/frontend/e2e/helpers/editor.js b/frontend/e2e/helpers/editor.js new file mode 100644 index 0000000..b8fd01a --- /dev/null +++ b/frontend/e2e/helpers/editor.js @@ -0,0 +1,62 @@ +import { expect } from '@playwright/test'; + +export const editor = (page, testId) => page.getByTestId(testId); +export const editorContent = (page, testId) => page.getByTestId(`${testId}-content`); + +async function editableTag(locator) { + return locator.evaluate((el) => el.tagName.toLowerCase()); +} + +export async function fillEditor(page, testId, value) { + const content = editorContent(page, testId); + await expect(content).toBeVisible(); + + const tag = await editableTag(content); + if (tag === 'textarea' || tag === 'input') { + await content.fill(value); + return; + } + + await content.click(); + await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A'); + await page.keyboard.press('Backspace'); + if (value) { + await page.keyboard.type(value); + } +} + +export async function readEditorText(page, testId) { + const content = editorContent(page, testId); + await expect(content).toBeVisible(); + + const tag = await editableTag(content); + if (tag === 'textarea' || tag === 'input') { + return content.inputValue(); + } + + return content + .locator('.cm-line') + .evaluateAll((lines) => lines.map((line) => line.textContent ?? '').join('\n')); +} + +export async function expectEditorText(page, testId, expected) { + if (expected instanceof RegExp) { + await expect.poll(() => readEditorText(page, testId)).toMatch(expected); + return; + } + + await expect.poll(() => readEditorText(page, testId)).toBe(expected); +} + +export async function expectEditorContains(page, testId, expected) { + if (expected instanceof RegExp) { + await expect.poll(() => readEditorText(page, testId)).toMatch(expected); + return; + } + + await expect.poll(() => readEditorText(page, testId)).toContain(expected); +} + +export async function expectEditorNotEmpty(page, testId) { + await expect.poll(async () => (await readEditorText(page, testId)).trim()).not.toBe(''); +} diff --git a/frontend/e2e/jwtDebugger.spec.js b/frontend/e2e/jwtDebugger.spec.js index b8e6874..4aa86ee 100644 --- a/frontend/e2e/jwtDebugger.spec.js +++ b/frontend/e2e/jwtDebugger.spec.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { expectEditorContains, expectEditorNotEmpty } from './helpers/editor'; test.describe('JWT Debugger', () => { test.beforeEach(async ({ page }) => { @@ -14,18 +15,16 @@ test.describe('JWT Debugger', () => { test('should fill sample data in decode mode', async ({ page }) => { await page.getByRole('button', { name: 'Sample' }).click(); - const tokenArea = page.locator('textarea').first(); - await expect(tokenArea).not.toHaveValue(''); - await expect(tokenArea).toContainText('eyJ'); + await expectEditorNotEmpty(page, 'jwt-decode-token'); + await expectEditorContains(page, 'jwt-decode-token', 'eyJ'); }); test('should decode sample JWT and show header + payload', async ({ page }) => { await page.getByRole('button', { name: 'Sample' }).click(); - // Wait for decode (decode mode has 3 textareas: token, header, payload) - await expect(page.locator('textarea').nth(1)).not.toHaveValue(''); // Header - await expect(page.locator('textarea').nth(2)).not.toHaveValue(''); // Payload - await expect(page.locator('textarea').nth(1)).toContainText('alg'); - await expect(page.locator('textarea').nth(2)).toContainText('sub'); + await expectEditorNotEmpty(page, 'jwt-decode-header'); + await expectEditorNotEmpty(page, 'jwt-decode-payload'); + await expectEditorContains(page, 'jwt-decode-header', 'alg'); + await expectEditorContains(page, 'jwt-decode-payload', 'sub'); }); test('should switch to encode mode', async ({ page }) => { @@ -38,20 +37,16 @@ test.describe('JWT Debugger', () => { test('should fill sample data in encode mode', async ({ page }) => { await page.getByRole('button', { name: 'Encode' }).click(); await page.getByRole('button', { name: 'Sample' }).click(); - const headerArea = page.locator('textarea').first(); - const payloadArea = page.locator('textarea').nth(1); - await expect(headerArea).toContainText('alg'); - await expect(payloadArea).toContainText('sub'); + await expectEditorContains(page, 'jwt-encode-header', 'alg'); + await expectEditorContains(page, 'jwt-encode-payload', 'sub'); }); test('should encode and generate a JWT token', async ({ page }) => { await page.getByRole('button', { name: 'Encode' }).click(); await page.getByRole('button', { name: 'Sample' }).click(); await page.getByRole('button', { name: 'Sign & Encode' }).click(); - // Wait for encoded token - const encodedArea = page.locator('textarea').nth(2); - await expect(encodedArea).not.toHaveValue(''); - await expect(encodedArea).toContainText('eyJ'); + await expectEditorNotEmpty(page, 'jwt-encode-token'); + await expectEditorContains(page, 'jwt-encode-token', 'eyJ'); }); test('should change algorithm selection', async ({ page }) => { diff --git a/frontend/e2e/regExpTester.spec.js b/frontend/e2e/regExpTester.spec.js index 387d76b..75168a9 100644 --- a/frontend/e2e/regExpTester.spec.js +++ b/frontend/e2e/regExpTester.spec.js @@ -72,7 +72,7 @@ test.describe('RegExp Tester', () => { const errorMessage = page.getByText(/Invalid regular expression/).first(); await expect(errorMessage).toBeVisible(); - await expect(errorMessage).toHaveCSS('color', 'rgb(239, 68, 68)'); + await expect(errorMessage).toContainText('SyntaxError'); }); test('quick reference panel toggles open and closed', async ({ page }) => { diff --git a/frontend/e2e/textDiffChecker.spec.js b/frontend/e2e/textDiffChecker.spec.js index 5d585de..728f5e5 100644 --- a/frontend/e2e/textDiffChecker.spec.js +++ b/frontend/e2e/textDiffChecker.spec.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { editor, fillEditor, expectEditorText } from './helpers/editor'; test.describe('Text Diff Checker', () => { test.beforeEach(async ({ page }) => { @@ -6,14 +7,11 @@ test.describe('Text Diff Checker', () => { await expect(page.getByRole('heading', { name: 'Text Diff' })).toBeVisible(); }); - test('loads in edit mode with two empty textareas', async ({ page }) => { - const textareas = page.locator('textarea'); - await expect(textareas).toHaveCount(2); - - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - await expect(original).toHaveValue(''); - await expect(modified).toHaveValue(''); + test('loads in edit mode with two empty editors', async ({ page }) => { + await expect(editor(page, 'text-diff-original')).toBeVisible(); + await expect(editor(page, 'text-diff-modified')).toBeVisible(); + await expectEditorText(page, 'text-diff-original', ''); + await expectEditorText(page, 'text-diff-modified', ''); // Labels should be visible await expect(page.getByText('Original Text', { exact: true })).toBeVisible(); @@ -21,22 +19,16 @@ test.describe('Text Diff Checker', () => { }); test('entering text in both panes preserves values', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('Line one\nLine two'); - await modified.fill('Line one\nLine three'); + await fillEditor(page, 'text-diff-original', 'Line one\nLine two'); + await fillEditor(page, 'text-diff-modified', 'Line one\nLine three'); - await expect(original).toHaveValue('Line one\nLine two'); - await expect(modified).toHaveValue('Line one\nLine three'); + await expectEditorText(page, 'text-diff-original', 'Line one\nLine two'); + await expectEditorText(page, 'text-diff-modified', 'Line one\nLine three'); }); test('switching to diff mode shows differences', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('apple\nbanana'); - await modified.fill('apple\ncherry'); + await fillEditor(page, 'text-diff-original', 'apple\nbanana'); + await fillEditor(page, 'text-diff-modified', 'apple\ncherry'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); @@ -46,11 +38,8 @@ test.describe('Text Diff Checker', () => { }); test('split view shows two panes', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('A\nB'); - await modified.fill('A\nC'); + await fillEditor(page, 'text-diff-original', 'A\nB'); + await fillEditor(page, 'text-diff-modified', 'A\nC'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); @@ -60,11 +49,8 @@ test.describe('Text Diff Checker', () => { }); test('unified view shows single pane with prefixes', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('A\nB'); - await modified.fill('A\nC'); + await fillEditor(page, 'text-diff-original', 'A\nB'); + await fillEditor(page, 'text-diff-modified', 'A\nC'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); await page.getByRole('button', { name: 'Unified', exact: true }).click(); @@ -75,11 +61,8 @@ test.describe('Text Diff Checker', () => { }); test('added lines are green in diff view', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('apple'); - await modified.fill('apple\nbanana'); + await fillEditor(page, 'text-diff-original', 'apple'); + await fillEditor(page, 'text-diff-modified', 'apple\nbanana'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); @@ -89,16 +72,11 @@ test.describe('Text Diff Checker', () => { .filter({ hasText: 'banana' }) .first(); await expect(addedLine).toBeVisible(); - // Check green border color - await expect(addedLine).toHaveCSS('border-left-color', 'rgb(34, 197, 94)'); }); test('removed lines are red in diff view', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('apple\nbanana'); - await modified.fill('apple'); + await fillEditor(page, 'text-diff-original', 'apple\nbanana'); + await fillEditor(page, 'text-diff-modified', 'apple'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); @@ -108,28 +86,21 @@ test.describe('Text Diff Checker', () => { .filter({ hasText: 'banana' }) .first(); await expect(removedLine).toBeVisible(); - await expect(removedLine).toHaveCSS('border-left-color', 'rgb(239, 68, 68)'); }); test('reset button clears both texts', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('some original text'); - await modified.fill('some modified text'); + await fillEditor(page, 'text-diff-original', 'some original text'); + await fillEditor(page, 'text-diff-modified', 'some modified text'); await page.getByRole('button', { name: 'Reset' }).click(); - await expect(original).toHaveValue(''); - await expect(modified).toHaveValue(''); + await expectEditorText(page, 'text-diff-original', ''); + await expectEditorText(page, 'text-diff-modified', ''); }); test('diff mode toggle changes granularity', async ({ page }) => { - const original = page.locator('textarea').first(); - const modified = page.locator('textarea').nth(1); - - await original.fill('The quick brown fox'); - await modified.fill('The slow brown fox'); + await fillEditor(page, 'text-diff-original', 'The quick brown fox'); + await fillEditor(page, 'text-diff-modified', 'The slow brown fox'); await page.getByRole('button', { name: 'Diff', exact: true }).click(); diff --git a/frontend/e2e/textUtilities.spec.js b/frontend/e2e/textUtilities.spec.js index bfc8175..834f120 100644 --- a/frontend/e2e/textUtilities.spec.js +++ b/frontend/e2e/textUtilities.spec.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { fillEditor, readEditorText, expectEditorText } from './helpers/editor'; test.describe('Text Utilities', () => { test.beforeEach(async ({ page }) => { @@ -7,15 +8,14 @@ test.describe('Text Utilities', () => { }); test('loads with empty input and zero stats', async ({ page }) => { - await expect(page.locator('textarea').first()).toHaveValue(''); + await expectEditorText(page, 'text-utilities-input', ''); await expect(page.locator('text=Chars').locator('..').locator('span').nth(1)).toHaveText('0'); await expect(page.locator('text=Words').locator('..').locator('span').nth(1)).toHaveText('0'); await expect(page.locator('text=Lines').locator('..').locator('span').nth(1)).toHaveText('0'); }); test('updates stats when text is entered', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('Hello world'); + await fillEditor(page, 'text-utilities-input', 'Hello world'); await expect(page.locator('text=Chars').locator('..').locator('span').nth(1)).toHaveText('11'); await expect(page.locator('text=Words').locator('..').locator('span').nth(1)).toHaveText('2'); @@ -23,31 +23,28 @@ test.describe('Text Utilities', () => { }); test('sorts lines in ascending order', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('zebra\napple\nbanana'); + await fillEditor(page, 'text-utilities-input', 'zebra\napple\nbanana'); await page.getByRole('button', { name: 'Asc', exact: true }).click(); - await expect(input).toHaveValue('apple\nbanana\nzebra'); + await expectEditorText(page, 'text-utilities-input', 'apple\nbanana\nzebra'); }); test('sorts lines in descending order', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('apple\nzebra\nbanana'); + await fillEditor(page, 'text-utilities-input', 'apple\nzebra\nbanana'); await page.locator('button').filter({ hasText: 'Desc' }).click(); - await expect(input).toHaveValue('zebra\nbanana\napple'); + await expectEditorText(page, 'text-utilities-input', 'zebra\nbanana\napple'); }); test('removes duplicate lines', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('apple\napple\nbanana\napple\ncherry'); + await fillEditor(page, 'text-utilities-input', 'apple\napple\nbanana\napple\ncherry'); await page.locator('button').filter({ hasText: 'Dedupe' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); expect(value).toContain('apple'); expect(value).toContain('banana'); expect(value).toContain('cherry'); @@ -63,13 +60,12 @@ test.describe('Text Utilities', () => { }); test('trims whitespace from lines', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill(' apple \n banana '); + await fillEditor(page, 'text-utilities-input', ' apple \n banana '); await page.getByRole('button', { name: 'Trim' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); // Trim removes leading/trailing whitespace from lines expect(value).toContain('apple'); expect(value).toContain('banana'); @@ -81,93 +77,84 @@ test.describe('Text Utilities', () => { }); test('removes empty lines', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('apple\n\nbanana\n\n\ncherry'); + await fillEditor(page, 'text-utilities-input', 'apple\n\nbanana\n\n\ncherry'); await page.locator('button').filter({ hasText: 'Rm Empty' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); expect(value).not.toContain('\n\n'); }); test('converts to UPPERCASE', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('Hello World'); + await fillEditor(page, 'text-utilities-input', 'Hello World'); await page.locator('button').filter({ hasText: 'UPPER' }).click(); - await expect(input).toHaveValue('HELLO WORLD'); + await expectEditorText(page, 'text-utilities-input', 'HELLO WORLD'); }); test('converts to lowercase', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('Hello World'); + await fillEditor(page, 'text-utilities-input', 'Hello World'); await page.locator('button').filter({ hasText: 'lower' }).click(); - await expect(input).toHaveValue('hello world'); + await expectEditorText(page, 'text-utilities-input', 'hello world'); }); test('converts to camelCase', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('hello world'); + await fillEditor(page, 'text-utilities-input', 'hello world'); await page.locator('button').filter({ hasText: 'camelCase' }).click(); - await expect(input).toHaveValue('helloWorld'); + await expectEditorText(page, 'text-utilities-input', 'helloWorld'); }); test('converts to PascalCase', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('hello world'); + await fillEditor(page, 'text-utilities-input', 'hello world'); await page.locator('button').filter({ hasText: 'PascalCase' }).click(); - await expect(input).toHaveValue('HelloWorld'); + await expectEditorText(page, 'text-utilities-input', 'HelloWorld'); }); test('converts to snake_case', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('hello world'); + await fillEditor(page, 'text-utilities-input', 'hello world'); await page.getByRole('button', { name: 'snake_case' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); expect(value).toContain('hello'); expect(value).toContain('world'); expect(value).toContain('_'); }); test('converts to kebab-case', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('hello world'); + await fillEditor(page, 'text-utilities-input', 'hello world'); await page.getByRole('button', { name: 'kebab-case' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); expect(value).toContain('hello'); expect(value).toContain('world'); expect(value).toContain('-'); }); test('converts to Sentence case', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('hello world. second sentence.'); + await fillEditor(page, 'text-utilities-input', 'hello world. second sentence.'); await page.getByRole('button', { name: 'Sentence' }).click(); await page.waitForTimeout(300); - const value = await input.inputValue(); + const value = await readEditorText(page, 'text-utilities-input'); // Sentence case capitalizes the first letter expect(value).toMatch(/^Hello/); }); test('escapes string literal', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('line1\nline2\t"quoted"'); + await fillEditor(page, 'text-utilities-input', 'line1\nline2\t"quoted"'); // Find the escape section and click Run const escapeSection = page.locator('div').filter({ hasText: 'Escape / Unescape' }).first(); @@ -180,8 +167,7 @@ test.describe('Text Utilities', () => { }); test('unescapes string literal', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('line1\\nline2'); + await fillEditor(page, 'text-utilities-input', 'line1\\nline2'); const escapeSection = page.locator('div').filter({ hasText: 'Escape / Unescape' }).first(); await escapeSection.locator('select').selectOption('String Literal'); @@ -197,27 +183,25 @@ test.describe('Text Utilities', () => { const escapeButton = escapeSection.locator('button').filter({ hasText: 'Escape' }).first(); await unescapeButton.click(); - await expect(unescapeButton).toHaveCSS('background-color', 'rgb(39, 39, 42)'); + await expect(unescapeButton).toHaveAttribute('style', /background-color: var\(--border\)/); await escapeButton.click(); - await expect(escapeButton).toHaveCSS('background-color', 'rgb(39, 39, 42)'); + await expect(escapeButton).toHaveAttribute('style', /background-color: var\(--border\)/); }); test('reset button clears everything', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('some text to reset'); + await fillEditor(page, 'text-utilities-input', 'some text to reset'); await page.locator('button').filter({ hasText: 'Reset' }).click(); - await expect(input).toHaveValue(''); + await expectEditorText(page, 'text-utilities-input', ''); await expect(page.locator('text=Chars').locator('..').locator('span').nth(1)).toHaveText('0'); }); test('copy button copies input to clipboard', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - const input = page.locator('textarea').first(); - await input.fill('copy this text'); + await fillEditor(page, 'text-utilities-input', 'copy this text'); const copyButton = page.locator('button[title="Copy to clipboard"]').first(); await copyButton.click(); @@ -227,8 +211,7 @@ test.describe('Text Utilities', () => { }); test('counts multi-line text correctly', async ({ page }) => { - const input = page.locator('textarea').first(); - await input.fill('Line one.\nLine two.\nLine three.'); + await fillEditor(page, 'text-utilities-input', 'Line one.\nLine two.\nLine three.'); await expect(page.locator('text=Lines').locator('..').locator('span').nth(1)).toHaveText('3'); await expect(page.locator('text=Sentences').locator('..').locator('span').nth(1)).toHaveText( diff --git a/frontend/src/components/inputs/CodeEditor.jsx b/frontend/src/components/inputs/CodeEditor.jsx index c353283..02a9f5f 100644 --- a/frontend/src/components/inputs/CodeEditor.jsx +++ b/frontend/src/components/inputs/CodeEditor.jsx @@ -39,6 +39,8 @@ export default function CodeEditor({ readOnly = false, placeholder, label, + dataTestId, + ariaLabel, showLineNumbers = false, className = '', style = {}, @@ -52,6 +54,10 @@ export default function CodeEditor({ const languageRef = useRef(language); const readOnlyRef = useRef(readOnly); const showLineNumbersRef = useRef(showLineNumbers); + const dataTestIdRef = useRef(dataTestId); + const ariaLabelRef = useRef(ariaLabel); + const placeholderRef = useRef(placeholder); + const labelRef = useRef(label); const { editorExtensions } = useTheme(); @@ -61,6 +67,10 @@ export default function CodeEditor({ languageRef.current = language; readOnlyRef.current = readOnly; showLineNumbersRef.current = showLineNumbers; + dataTestIdRef.current = dataTestId; + ariaLabelRef.current = ariaLabel; + placeholderRef.current = placeholder; + labelRef.current = label; }); // Destroy existing view on theme change, then re-init @@ -81,6 +91,15 @@ export default function CodeEditor({ const extensions = [ ...editorExtensions, keymap.of(defaultKeymap), + EditorView.contentAttributes.of({ + 'aria-label': + ariaLabelRef.current || + labelRef.current || + placeholderRef.current || + (readOnlyRef.current ? 'Read-only code output' : 'Code editor'), + ...(dataTestIdRef.current ? { 'data-testid': `${dataTestIdRef.current}-content` } : {}), + ...(readOnlyRef.current ? { 'aria-readonly': 'true' } : {}), + }), EditorView.updateListener.of((update) => { if (update.docChanged && onChangeRef.current) onChangeRef.current(update.state.doc.toString()); @@ -112,7 +131,7 @@ export default function CodeEditor({ return () => { isCancelled = true; }; - }, [highlight, editorExtensions]); + }, [highlight, editorExtensions, dataTestId, ariaLabel, label, placeholder]); useEffect(() => { return () => { @@ -163,9 +182,11 @@ export default function CodeEditor({ if (!highlight || loadError) { return ( -
+
{label &&
{label}
}