diff --git a/packages/testing/playwright/migration.md b/packages/testing/playwright/migration.md new file mode 100644 index 0000000000..e2bce57858 --- /dev/null +++ b/packages/testing/playwright/migration.md @@ -0,0 +1,56 @@ +# Cypress to Playwright Migration Guide + +## Migration Process + +### 1. Review Original Test +- Examine the Cypress test file to understand what functionality it's testing +- Note the test structure, assertions, and user interactions +- Identify any special setup or teardown requirements + +### 2. Scaffold New Test +- Create Playwright test file with exact same test names as Cypress +- Add commented descriptions of what each test is trying to achieve +- Keep the same test structure and organization + +### 3. Analyze Dependencies +- Review what page models, composables, and helpers the Cypress test uses +- Check existing Playwright infrastructure in `composables/`, `helpers/`, `pages/` +- Identify which components are already available vs need to be created + +### 4. Study Building Blocks +- Review existing Playwright tests to understand correct patterns +- Check how similar functionality is implemented in other migrated tests +- Follow established conventions for page objects and test structure + +### 5. Plan Migration Approach +- Determine the best strategy for translating Cypress commands to Playwright +- Plan any new page objects or helpers needed +- Consider test data requirements and setup + +### 6. Execute Migration +- Migrate tests one at a time +- Maintain original test intent and coverage +- Follow Playwright best practices and n8n conventions + +### 7. Verify Results +- Test using `pnpm test:local --grep "test-name" --reporter=line` +- Ensure all assertions pass and behavior matches original +- Validate test runs reliably and consistently + +## Key Differences: Cypress vs Playwright + +- **Selectors**: Cypress uses `cy.get()` → Playwright uses `page.locator()` +- **Assertions**: Cypress auto-waits → Playwright uses `expect()` with built-in waiting +- **Page Navigation**: `cy.visit()` → `page.goto()` +- **Element Interaction**: `cy.click()` → `locator.click()` +- **Text Content**: `cy.contains()` → `expect(locator).toContainText()` + +## Testing Commands + +```bash +# Run specific test with grep +pnpm --filter n8n-playwright test:local --grep "test-name" --reporter=line + +# Run full test suite in directory +pnpm --filter n8n-playwright test:local --reporter=line +``` diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index 21ad5b5463..27c486c03f 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -4,8 +4,11 @@ import { nanoid } from 'nanoid'; import { BasePage } from './BasePage'; import { ROUTES } from '../config/constants'; import { resolveFromRoot } from '../utils/path-helper'; +import { StickyComponent } from './components/StickyComponent'; export class CanvasPage extends BasePage { + readonly sticky = new StickyComponent(this.page); + saveWorkflowButton(): Locator { return this.page.getByRole('button', { name: 'Save' }); } diff --git a/packages/testing/playwright/pages/components/StickyComponent.ts b/packages/testing/playwright/pages/components/StickyComponent.ts new file mode 100644 index 0000000000..efc2561590 --- /dev/null +++ b/packages/testing/playwright/pages/components/StickyComponent.ts @@ -0,0 +1,50 @@ +import type { Locator, Page } from '@playwright/test'; + +import { BasePage } from '../BasePage'; + +/** + * Sticky note component for canvas interactions. + * Used within CanvasPage as `n8n.canvas.sticky.*` + * + * @example + * // Access via canvas page + * await n8n.canvas.sticky.addSticky(); + * await expect(n8n.canvas.sticky.getStickies()).toHaveCount(1); + */ +export class StickyComponent extends BasePage { + constructor(page: Page) { + super(page); + } + + getAddButton(): Locator { + return this.page.getByTestId('add-sticky-button'); + } + + getStickies(): Locator { + return this.page.getByTestId('sticky'); + } + + getStickyByIndex(index: number): Locator { + return this.getStickies().nth(index); + } + + async addSticky(): Promise { + await this.getAddButton().click(); + } + + /** + * Add a sticky from the context menu, targets top left corner of canvas, so could fail if it's covered + * @param canvasPane - The canvas pane locator + */ + async addFromContextMenu(canvasPane: Locator): Promise { + await canvasPane.click({ + button: 'right', + position: { x: 10, y: 10 }, + }); + await this.page.getByText('Add sticky note').click(); + } + + getDefaultStickyGuideLink(): Locator { + return this.getStickies().first().getByRole('link', { name: 'Guide' }); + } +} diff --git a/packages/testing/playwright/tests/ui/25-stickies.spec.ts b/packages/testing/playwright/tests/ui/25-stickies.spec.ts new file mode 100644 index 0000000000..c4174f0bb3 --- /dev/null +++ b/packages/testing/playwright/tests/ui/25-stickies.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../fixtures/base'; + +test.describe('Canvas Actions', () => { + test('adds sticky to canvas with default text and position', async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + await expect(n8n.canvas.sticky.getAddButton()).toBeVisible(); + + await n8n.canvas.sticky.addSticky(); + + const firstSticky = n8n.canvas.sticky.getStickies().first(); + await expect(firstSticky).toHaveCSS('height', '160px'); + await expect(firstSticky).toHaveCSS('width', '240px'); + + await n8n.canvas.deselectAll(); + await n8n.canvas.sticky.addFromContextMenu(n8n.canvas.canvasPane()); + await n8n.page.keyboard.press('Shift+s'); + + await expect(n8n.canvas.sticky.getStickies()).toHaveCount(3); + + await n8n.page.keyboard.press('ControlOrMeta+Shift+s'); + await expect(n8n.canvas.sticky.getStickies()).toHaveCount(3); + + await expect(n8n.canvas.sticky.getStickies().first()).toHaveText( + 'I’m a note\nDouble click to edit me. Guide\n', + ); + const guideLink = n8n.canvas.sticky.getDefaultStickyGuideLink(); + await expect(guideLink).toHaveAttribute('href'); + }); +});