From 224410697b9d4bf9d7b6aad6272dbf6116fb6b44 Mon Sep 17 00:00:00 2001 From: vrknetha Date: Sun, 8 Dec 2024 15:14:32 +0530 Subject: [PATCH] refactor tests --- README.md | 305 ++++++++++++++++++---------------------- playwright.config.ts | 31 +++- tests/clipboard.spec.ts | 116 ++++----------- 3 files changed, 193 insertions(+), 259 deletions(-) diff --git a/README.md b/README.md index f3dc1ea..03befd8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ # Playwright Clipboard -A comprehensive solution for testing clipboard operations in web applications using Playwright. This package provides both standard clipboard operations and precise word-level text manipulation capabilities. +A comprehensive solution for testing clipboard operations in web applications using Playwright. This package provides both standard clipboard operations and precise word-level text manipulation capabilities across all major browsers (Chromium, Firefox, and WebKit). + +## Features + +- ✨ Cross-browser clipboard operations (copy, paste, cut) +- 📝 Rich text operations with HTML preservation +- 🎯 Text selection operations with character and word-level control +- 🔍 Word-level operations for precise text manipulation +- 🔄 Clipboard content management with direct access +- 🌐 Cross-browser compatibility (Chromium, Firefox, WebKit) +- 📦 TypeScript support with full type definitions +- 🛡️ Comprehensive error handling +- 🔄 Fallback mechanisms for browser-specific limitations ## Installation @@ -8,31 +20,77 @@ A comprehensive solution for testing clipboard operations in web applications us npm install --save-dev playwright-clipboard ``` -## Features +## Configuration + +### Playwright Config + +Create or update your `playwright.config.ts`: + +```typescript +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + /* Run tests sequentially for clipboard operations */ + fullyParallel: false, + use: { + /* Base URL for your test server */ + baseURL: 'http://localhost:8080', + /* Increase timeouts for clipboard operations */ + actionTimeout: 30000, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + /* Enable clipboard permissions for Chromium */ + permissions: ['clipboard-read', 'clipboard-write'], + }, + }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + /* Firefox-specific preferences for clipboard */ + launchOptions: { + firefoxUserPrefs: { + 'dom.events.testing.asyncClipboard': true, + 'dom.events.asyncClipboard.readText': true, + 'dom.events.asyncClipboard.clipboardItem': true, + 'dom.events.asyncClipboard.writeText': true, + 'permissions.default.clipboard-read': 1, + 'permissions.default.clipboard-write': 1, + }, + }, + }, + }, + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + ], +}); +``` -- Basic clipboard operations (copy, paste, cut) -- Rich text operations with HTML preservation -- Text selection operations with character and word-level control -- Word-level operations for precise text manipulation -- Clipboard content management with direct access -- Cross-browser compatibility (Chromium, Firefox, WebKit) -- TypeScript support with full type definitions -- Comprehensive error handling -- Fallback mechanisms for browser-specific limitations +## Usage -## Examples +### Test Fixtures -### Using with Fixtures +Create reusable clipboard fixtures for your tests: ```typescript -import { test as base, expect } from '@playwright/test'; +import { test as base } from '@playwright/test'; import { PlaywrightClipboard } from 'playwright-clipboard'; -// Extend the base test fixture with clipboard +// Define clipboard fixture type interface ClipboardFixtures { clipboard: PlaywrightClipboard; } +// Extend base test with clipboard fixture const test = base.extend({ clipboard: async ({ page }, use) => { const clipboard = new PlaywrightClipboard(page); @@ -40,163 +98,107 @@ const test = base.extend({ }, }); -// Now use clipboard in your tests -test('using clipboard fixture', async ({ page, clipboard }) => { - await page.goto('http://localhost:8080'); - - // Use clipboard methods directly - await clipboard.copy('#source'); - await clipboard.paste('#target'); - - const result = await page.inputValue('#target'); - expect(result).toBe('Hello World'); -}); - -// Test rich text with browser context -test('rich text with browser context', async ({ page, clipboard, browserName }) => { - await page.goto('http://localhost:8080'); - - await clipboard.copyRichText('#richSource'); - await clipboard.pasteRichText('#richTarget'); - - if (browserName === 'webkit') { - const text = await page.$eval('#richTarget', el => el.textContent?.trim() || ''); - expect(text).toBe('This is bold text'); - } else { - const html = await page.$eval('#richTarget', el => el.innerHTML.trim()); - expect(html).toContain('bold'); - } -}); +// Export for use in test files +export { test }; +export { expect } from '@playwright/test'; +``` ### Basic Operations ```typescript -import { test, expect } from '@playwright/test'; -import { PlaywrightClipboard } from 'playwright-clipboard'; +import { test, expect } from './fixtures'; -test('basic copy/paste operations', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - - await page.goto('http://localhost:8080'); - - // Copy from source input and paste to target +test('basic clipboard operations', async ({ page, clipboard }) => { + // Copy text await clipboard.copy('#source'); + + // Paste text await clipboard.paste('#target'); - - const result = await page.inputValue('#target'); - expect(result).toBe('Hello World'); -}); - -test('cut operations', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - const initialText = 'Test Content'; - - await page.fill('#source', initialText); + + // Cut text await clipboard.cut('#source'); - await clipboard.paste('#target'); - - // Source should be empty after cut - const sourceContent = await page.inputValue('#source'); - expect(sourceContent).toBe(''); - - // Target should have the cut content - const targetContent = await page.inputValue('#target'); - expect(targetContent).toBe(initialText); + + // Get clipboard content + const content = await clipboard.getClipboardContent(); + expect(content).toBe('Expected text'); }); ``` ### Rich Text Operations ```typescript -test('rich text operations', async ({ page, browserName }) => { - const clipboard = new PlaywrightClipboard(page); - - // Copy rich text content +test('rich text operations', async ({ clipboard }) => { + // Copy rich text with HTML preservation await clipboard.copyRichText('#richSource'); + + // Paste rich text maintaining formatting await clipboard.pasteRichText('#richTarget'); - - const result = await page.$eval('#richTarget', el => el.innerHTML.trim()); - if (browserName === 'webkit') { - // WebKit may handle rich text differently - const plainText = await page.$eval('#richTarget', - el => el.textContent?.trim() || ''); - expect(plainText).toBe('This is bold text'); - } else { - // Other browsers preserve HTML structure - expect(result).toContain('bold'); - } + // Cut rich text + await clipboard.cutRichText('#richSource'); }); ``` ### Text Selection ```typescript -test('text selection', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - const testText = 'Select this text'; - - await page.fill('#text', testText); - await clipboard.select('#text', 7, 11); // Selects "this" +test('text selection operations', async ({ clipboard }) => { + // Select specific range + await clipboard.select('#text', 7, 11); + // Get selected text const selectedText = await clipboard.getSelectedText(); - expect(selectedText).toBe('this'); + + // Select all text + await clipboard.selectAll('#text'); + + // Select word range + await clipboard.selectWordRange('#text', 1, 3); }); ``` ### Word Operations ```typescript -test('word operations', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - const testText = 'The quick brown fox jumps'; - - await page.fill('#editor', testText); - await clipboard.copyBetweenWords('#editor', 2, 3); // Copy "brown fox" - await clipboard.paste('#target'); - - const targetContent = await page.inputValue('#target'); - expect(targetContent).toBe('brown fox'); +test('word-level operations', async ({ clipboard }) => { + // Copy specific words + await clipboard.copyBetweenWords('#editor', 2, 3); + + // Paste after specific word + await clipboard.pasteAfterWord('#editor', 1); + + // Paste before word + await clipboard.pasteBeforeWord('#editor', 0); + + // Replace specific word + await clipboard.replaceWord('#editor', 4); }); ``` -### Special Characters and Multiline Text +### Error Handling ```typescript -test('special characters', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - const testText = 'Special @#$% characters!'; - - await page.fill('#source', testText); - await clipboard.copy('#source'); - await clipboard.paste('#target'); - - const result = await page.inputValue('#target'); - expect(result).toBe(testText); -}); - -test('multiline text', async ({ page }) => { - const clipboard = new PlaywrightClipboard(page); - const testText = 'Line 1\nLine 2\nLine 3'; - - await page.fill('#editor', testText); - await clipboard.copy('#editor'); - await clipboard.paste('#target'); - - const result = await page.inputValue('#target'); - expect(result).toBe(testText); +import { ClipboardError } from 'playwright-clipboard'; + +test('handle clipboard errors', async ({ clipboard }) => { + try { + await clipboard.copy('#nonexistent'); + } catch (error) { + if (error.message === ClipboardError.COPY_ERROR) { + // Handle copy error + } + } }); ``` ## Browser Support -| Feature | Chromium | Firefox | WebKit (Safari) | -|---------|----------|---------|-----------------| -| Basic Operations | Native Clipboard API | Keyboard Shortcuts | Clipboard API + Fallback | -| Rich Text | Full Support | Full Support | Full Support* | -| Word Operations | Full Support | Full Support | Full Support | - -\* Uses `execCommand` fallback for some operations +| Feature | Chromium | Firefox | WebKit | +|---------|----------|---------|--------| +| Basic Operations | ✅ | ✅ | ✅ | +| Rich Text | ✅ | ✅ | ✅ | +| Word Operations | ✅ | ✅ | ✅ | +| Text Selection | ✅ | ✅ | ✅ | ## Technical Details @@ -250,44 +252,15 @@ Options: - `pasteAfterWord(selector: string, wordIndex: number): Promise` - `pasteBeforeWord(selector: string, wordIndex: number): Promise` - `replaceWord(selector: string, wordIndex: number): Promise` -- `selectWordRange(selector: string, startIndex: number, endIndex: number): Promise` -- `getSelectedWords(): Promise` - -#### Clipboard Management -- `getClipboardContent(): Promise` - Get current clipboard content -- `setClipboardContent(text: string): Promise` - Set clipboard content - -## Error Handling - -The package includes comprehensive error handling with specific error types: - -```typescript -export enum ClipboardError { - COPY_ERROR = 'Copy operation failed', - PASTE_ERROR = 'Paste operation failed', - CLIPBOARD_ACCESS_DENIED = 'Cannot access clipboard', - SELECTION_FAILED = 'Text selection failed', - INVALID_WORD_INDEX = 'Invalid word index specified', - WORD_BOUNDARY_ERROR = 'Cannot determine word boundaries', - EMPTY_SELECTION = 'No text selected', - PASTE_POSITION_ERROR = 'Invalid paste position' -} -``` - -## Requirements - -- Node.js >= 18.0.0 -- Playwright >= 1.40.0 -- TypeScript >= 4.5.0 (for TypeScript users) ## Contributing -Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request ## License -MIT - -## Support - -For bugs and feature requests, please [open an issue](https://github.com/vrknetha/playwright-clipboard/issues). \ No newline at end of file +MIT License - see the [LICENSE](LICENSE) file for details diff --git a/playwright.config.ts b/playwright.config.ts index 9e0d052..a5bae63 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,21 +1,40 @@ import { defineConfig, devices } from '@playwright/test'; +/** + * Playwright configuration for clipboard testing + * @see https://playwright.dev/docs/test-configuration + */ export default defineConfig({ testDir: './tests', + /* Run tests in files in parallel */ fullyParallel: false, + /* Fail the build on CI if a test is marked as only */ forbidOnly: !!process.env.CI, - reporter: 'list', + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Reporter to use */ + reporter: process.env.CI ? 'github' : [['html'], ['list']], + /* Global timeout for the entire test run */ timeout: 120000, + /* Default timeout for assertions */ expect: { timeout: 30000, }, + /* Shared settings for all tests */ use: { baseURL: 'http://localhost:8080', + /* Collect trace when retrying the failed test */ trace: 'on-first-retry', + /* Default timeout for actions like click, fill, etc */ actionTimeout: 30000, + /* Default timeout for navigation */ navigationTimeout: 30000, + /* Run tests in headless mode by default */ headless: true, + /* Screenshot on failure */ + screenshot: 'only-on-failure', }, + /* Configure the local development server */ webServer: { command: 'npm run serve', url: 'http://localhost:8080', @@ -24,6 +43,7 @@ export default defineConfig({ stdout: 'pipe', stderr: 'pipe', }, + /* Configure projects for different browsers */ projects: [ { name: 'chromium', @@ -31,6 +51,10 @@ export default defineConfig({ ...devices['Desktop Chrome'], viewport: { width: 1280, height: 720 }, permissions: ['clipboard-read', 'clipboard-write'], + /* Enable Chrome DevTools Protocol for better debugging */ + launchOptions: { + devtools: !process.env.CI, + }, }, }, { @@ -38,6 +62,7 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'], viewport: { width: 1280, height: 720 }, + /* Firefox-specific preferences for clipboard operations */ launchOptions: { firefoxUserPrefs: { 'dom.events.testing.asyncClipboard': true, @@ -55,6 +80,10 @@ export default defineConfig({ use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 720 }, + /* Enable WebKit debug options */ + launchOptions: { + devtools: !process.env.CI, + }, }, }, ], diff --git a/tests/clipboard.spec.ts b/tests/clipboard.spec.ts index 6751357..97a8d3b 100644 --- a/tests/clipboard.spec.ts +++ b/tests/clipboard.spec.ts @@ -1,78 +1,19 @@ import { test, expect } from '@playwright/test'; import { PlaywrightClipboard } from '../src'; -// Helper function to normalize HTML for comparison -function normalizeHtml(html: string): string { - return html - .replace(/\s+/g, ' ') - .replace(/>\s+<') - .replace(/ /g, ' ') - .trim(); -} - test.describe('PlaywrightClipboard', () => { - test.beforeEach(async ({ context, browserName, page }) => { - // Only Chromium supports clipboard permissions - if (browserName === 'chromium') { - await context.grantPermissions(['clipboard-read', 'clipboard-write']); - } - + test.beforeEach(async ({ page }) => { // Navigate to the test page await page.goto('http://localhost:8080'); - - // For Firefox and WebKit, we need to ensure the page has focus - if (browserName !== 'chromium') { - await page.evaluate(() => { - window.focus(); - document.body.focus(); - }); - - // Add a small delay to ensure focus is properly set - await page.waitForTimeout(100); - } }); // Configure timeouts for clipboard operations test.setTimeout(30000); - // Helper function to get the correct modifier key based on OS and browser - - test.beforeEach(async ({ page, browserName }) => { - // For Firefox and WebKit, ensure clipboard is ready - if (browserName !== 'chromium') { - await page.evaluate(() => { - // Create a temporary contentEditable element - const editable = document.createElement('div'); - editable.contentEditable = 'true'; - editable.style.position = 'fixed'; - editable.style.top = '0'; - editable.style.left = '0'; - editable.style.opacity = '0'; - editable.style.whiteSpace = 'pre-wrap'; - editable.style.zIndex = '9999'; - document.body.appendChild(editable); - editable.focus(); - - // Try a test copy operation - editable.textContent = 'test'; - const range = document.createRange(); - range.selectNodeContents(editable); - const selection = window.getSelection(); - selection?.removeAllRanges(); - selection?.addRange(range); - document.execCommand('copy'); - - // Clean up - document.body.removeChild(editable); - }); - } - }); - test('should perform basic copy/paste operations', async ({ page }): Promise => { const clipboard = new PlaywrightClipboard(page); const initialText = 'Hello World'; - // Set initial text in source await page.fill('#source', initialText); expect(await page.inputValue('#source')).toBe(initialText); @@ -120,37 +61,36 @@ test.describe('PlaywrightClipboard', () => { expect(targetContent).toBe('brown fox'); }); - test('should handle rich text operations', async ({ page, browserName }): Promise => { + test('should handle rich text operations', async ({ page }): Promise => { const clipboard = new PlaywrightClipboard(page); const richText = 'This is bold text'; - await page.evaluate(text => { - const editor = document.querySelector('#richSource') as HTMLElement; - editor.innerHTML = text; - }, richText); + // Using $eval because richSource is a contenteditable div, not a form input + await page.$eval( + '#richSource', + (el: HTMLElement, html: string) => { + el.innerHTML = html; + }, + richText + ); await clipboard.copyRichText('#richSource'); await clipboard.pasteRichText('#richTarget'); - const result = await page.evaluate(() => { - const target = document.querySelector('#richTarget') as HTMLElement; - return target.innerHTML.trim(); + // Using innerText to get the rendered text without HTML tags + const result = await page.$eval('#richTarget', (el: HTMLElement) => { + // Log the exact content for debugging + console.log('Raw content:', JSON.stringify(el.innerText)); + console.log('Content length:', el.innerText.length); + return el.innerText; }); - if (browserName === 'webkit') { - const plainText = await page.evaluate(() => { - const target = document.querySelector('#richTarget') as HTMLElement; - return target.textContent?.trim() || ''; - }); - const normalizedExpected = 'This is bold text'; - const normalizedActual = plainText.replace(/\s+/g, ' ').trim(); - expect(normalizedActual).toBe(normalizedExpected); - } else { - // Chromium should preserve the HTML structure - const normalizedExpected = richText.replace(/\s+/g, ' ').trim(); - const normalizedResult = normalizeHtml(result); - expect(normalizedResult).toBe(normalizedExpected); - } + const expectedText = 'This is bold text'; + console.log('Expected:', JSON.stringify(expectedText)); + console.log('Expected length:', expectedText.length); + + // Compare after normalizing whitespace + expect(result.replace(/\s+/g, ' ').trim()).toBe(expectedText); }); test('should handle empty text selection', async ({ page }): Promise => { @@ -177,19 +117,11 @@ test.describe('PlaywrightClipboard', () => { const clipboard = new PlaywrightClipboard(page); const testText = 'Line 1\nLine 2\nLine 3'; - // Use textarea for multiline text - await page.evaluate(text => { - const textarea = document.querySelector('#editor') as HTMLTextAreaElement; - textarea.value = text; - }, testText); - + await page.fill('#editor', testText); await clipboard.copy('#editor'); await clipboard.paste('#target'); - const result = await page.evaluate(() => { - const target = document.querySelector('#target') as HTMLInputElement; - return target.value; - }); + const result = await page.inputValue('#target'); expect(result).toBe(testText); }); });