diff --git a/.github/actions/e2e-tests/action.yml b/.github/actions/e2e-tests/action.yml new file mode 100644 index 00000000..db848f0f --- /dev/null +++ b/.github/actions/e2e-tests/action.yml @@ -0,0 +1,36 @@ +name: E2E tests +description: This action runs end-to-end tests using Playwright +inputs: + working-directory: + description: 'Path to the ./packages/name_of_your_package_folder' + required: true + artifactory-report-name: + description: 'A full name of the artifact report' + required: true + +runs: + using: "composite" + steps: + - uses: actions/setup-node@v3 + with: + node-version-file: .nvmrc + - uses: ./.github/actions/setup + + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + working-directory: ${{ inputs.working-directory }} + shell: bash + + - name: Run Playwright tests + run: yarn playwright test + working-directory: ${{ inputs.working-directory }} + shell: bash + + - name: Upload Playwright report to GitHub artifacts + uses: actions/upload-artifact@v3 + if: always() + with: + name: ${{ inputs.artifactory-report-name }} + path: ${{ inputs.working-directory }}/playwright-report/ + retention-days: 7 + diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 4811f740..80d0c606 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -21,3 +21,13 @@ jobs: uses: ./.github/actions/build with: package-name: '@editorjs/document-playground' + + e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run E2E tests + uses: ./.github/actions/e2e-tests + with: + working-directory: packages/playground + artifactory-report-name: 'document-playground-e2e-report' diff --git a/packages/playground/.eslintrc.cjs b/packages/playground/.eslintrc.cjs index 7c4ace9f..c6541b81 100644 --- a/packages/playground/.eslintrc.cjs +++ b/packages/playground/.eslintrc.cjs @@ -10,6 +10,7 @@ module.exports = { project: [ './tsconfig.json', './tsconfig.eslint.json', + './tsconfig.playwright.json' ], ecmaVersion: 2022, extraFileExtensions: [ '.vue' ], diff --git a/packages/playground/.gitignore b/packages/playground/.gitignore index a547bf36..b88c8135 100644 --- a/packages/playground/.gitignore +++ b/packages/playground/.gitignore @@ -22,3 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/playground/package.json b/packages/playground/package.json index fb3124b7..6d1d7f8d 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -4,14 +4,17 @@ "type": "module", "packageManager": "yarn@4.0.1", "scripts": { - "dev": "concurrently -n \"TSC Watch\",Vite \"yarn dev:dependencies\" \"vite\"", + "dev": "concurrently -n \"TSC Watch\",Vite \"yarn dev:dependencies\" \"vite --port 3123\"", "build": "yarn build:dependencies && vue-tsc && vite build", "preview": "yarn build:dependencies && vite preview", "build:dependencies": "yarn workspaces foreach -Rpt --from $npm_package_name --exclude $npm_package_name run build", "dev:dependencies": "yarn workspaces foreach -Rp --from $npm_package_name --exclude $npm_package_name run dev", "lint": "eslint src --ext .ts,.vue", "lint:ci": "yarn lint --max-warnings 0", - "lint:fix": "yarn lint --fix" + "lint:fix": "yarn lint --fix", + "test": "playwright test", + "test:ui": "yarn test --ui", + "e2e-dev-server": "yarn build:dependencies && vite --port 3123" }, "dependencies": { "@editorjs/core": "workspace:^", @@ -20,7 +23,9 @@ "vue": "^3.3.4" }, "devDependencies": { + "@playwright/test": "^1.41.2", "@types/eslint": "^8", + "@types/node": "^20.11.17", "@vitejs/plugin-vue": "^4.2.3", "concurrently": "^8.2.2", "eslint": "^8.53.0", diff --git a/packages/playground/playwright.config.ts b/packages/playground/playwright.config.ts new file mode 100644 index 00000000..9cda4fee --- /dev/null +++ b/packages/playground/playwright.config.ts @@ -0,0 +1,81 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + // workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html'], + [process.env.CI ? 'github' : 'line'] + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3123', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'yarn e2e-dev-server', + url: 'http://localhost:3123', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/packages/playground/tests/contenteditable.spec.ts b/packages/playground/tests/contenteditable.spec.ts new file mode 100644 index 00000000..3ba7a2ae --- /dev/null +++ b/packages/playground/tests/contenteditable.spec.ts @@ -0,0 +1,31 @@ +import { expect } from '@playwright/test'; +import { test } from './utils/test'; + +test.describe('Contenteditable field', () => { + test('should be visible', async ({ playgroundPage }) => { + const contenteditable = playgroundPage.contenteditable; + + await expect(contenteditable).toBeVisible(); + }); + + test('should be focusable', async ({ playgroundPage }) => { + const contenteditable = playgroundPage.contenteditable; + + await contenteditable.click(); + + await expect(contenteditable).toBeFocused(); + }); + + test('should accept text', async ({ playgroundPage }) => { + const contenteditable = playgroundPage.contenteditable; + const initialText = await contenteditable.textContent(); + const inputText = 'Hello, World!'; + const expectedText = initialText + inputText; + + await contenteditable.click(); + await contenteditable.press('Control+ArrowRight'); + await contenteditable.pressSequentially(inputText); + + await expect(contenteditable).toHaveText(expectedText); + }); +}); diff --git a/packages/playground/tests/fixtures/PlaygroundPage.ts b/packages/playground/tests/fixtures/PlaygroundPage.ts new file mode 100644 index 00000000..44e95378 --- /dev/null +++ b/packages/playground/tests/fixtures/PlaygroundPage.ts @@ -0,0 +1,49 @@ +import type { Locator, Page } from '@playwright/test'; + +/** + * Playground page fixture to help access the page elements + */ +export class PlaygroundPage { + readonly #input: Locator; + readonly #textarea: Locator; + readonly #contenteditable: Locator; + + /** + * Sets locators for input, textarea and contenteditable elements + * + * @param page - Playwright page object + */ + constructor(public readonly page: Page) { + this.#input = page.locator('input'); + this.#textarea = page.locator('textarea'); + this.#contenteditable = page.locator('[contenteditable]').first(); // hack to get the first contenteditable element + } + + /** + * Navigates to the playground page + */ + public async goto(): Promise { + await this.page.goto('/'); + } + + /** + * Returns the native input locator + */ + public get input(): Locator { + return this.#input; + } + + /** + * Returns the textarea locator + */ + public get textarea(): Locator { + return this.#textarea; + } + + /** + * Returns the contenteditable locator + */ + public get contenteditable(): Locator { + return this.#contenteditable; + } +} diff --git a/packages/playground/tests/input.spec.ts b/packages/playground/tests/input.spec.ts new file mode 100644 index 00000000..13e6a588 --- /dev/null +++ b/packages/playground/tests/input.spec.ts @@ -0,0 +1,31 @@ +import { expect } from '@playwright/test'; +import { test } from './utils/test'; + +test.describe('Native input element', () => { + test('should be visible', async ({ playgroundPage }) => { + const input = playgroundPage.input; + + await expect(input).toBeVisible(); + }); + + test('should be focusable', async ({ playgroundPage }) => { + const input = playgroundPage.input; + + await input.click(); + + await expect(input).toBeFocused(); + }); + + test('should accept text', async ({ playgroundPage }) => { + const input = playgroundPage.input; + const initialText = await input.inputValue(); + const inputText = 'Hello, World!'; + const expectedText = initialText + inputText; + + await input.click(); + await input.press('Control+ArrowRight'); + await input.pressSequentially(inputText, { delay: 100 }); + + await expect(input).toHaveValue(expectedText); + }); +}); diff --git a/packages/playground/tests/textarea.spec.ts b/packages/playground/tests/textarea.spec.ts new file mode 100644 index 00000000..3231eb19 --- /dev/null +++ b/packages/playground/tests/textarea.spec.ts @@ -0,0 +1,31 @@ +import { expect } from '@playwright/test'; +import { test } from './utils/test'; + +test.describe('Textarea field', () => { + test('should be visible', async ({ playgroundPage }) => { + const textarea = playgroundPage.textarea; + + await expect(textarea).toBeVisible(); + }); + + test('should be focusable', async ({ playgroundPage }) => { + const textarea = playgroundPage.textarea; + + await textarea.click(); + + await expect(textarea).toBeFocused(); + }); + + test('should accept text', async ({ playgroundPage }) => { + const textarea = playgroundPage.textarea; + const initialText = await textarea.inputValue(); + const inputText = 'Hello, World!'; + const expectedText = initialText + inputText; + + await textarea.click(); + await textarea.press('Control+ArrowRight'); + await textarea.pressSequentially(inputText, { delay: 100 }); + + await expect(textarea).toHaveValue(expectedText); + }); +}); diff --git a/packages/playground/tests/utils/test.ts b/packages/playground/tests/utils/test.ts new file mode 100644 index 00000000..fe4f6dde --- /dev/null +++ b/packages/playground/tests/utils/test.ts @@ -0,0 +1,15 @@ +import { test as base } from '@playwright/test'; +import { PlaygroundPage } from '../fixtures/PlaygroundPage'; + +/** + * Custom test wrapper to extend the base test with the playground page fixture + */ +export const test = base.extend<{ playgroundPage: PlaygroundPage }>({ + playgroundPage: async ({ page }, use) => { + const playgroundPage = new PlaygroundPage(page); + + await playgroundPage.goto(); + + await use(playgroundPage); + }, +}); diff --git a/packages/playground/tsconfig.node.json b/packages/playground/tsconfig.node.json index b5a34318..4b497b39 100644 --- a/packages/playground/tsconfig.node.json +++ b/packages/playground/tsconfig.node.json @@ -7,6 +7,7 @@ "allowSyntheticDefaultImports": true }, "include": [ - "vite.config.ts" + "vite.config.ts", + "playwright.config.ts" ] } diff --git a/packages/playground/tsconfig.playwright.json b/packages/playground/tsconfig.playwright.json new file mode 100644 index 00000000..69fd1ad1 --- /dev/null +++ b/packages/playground/tsconfig.playwright.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "strict": true, + }, + "include": [ + "tests/**/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 97e43eaa..3ff92f60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -647,7 +647,9 @@ __metadata: "@editorjs/core": "workspace:^" "@editorjs/dom-adapters": "workspace:^" "@editorjs/model": "workspace:^" + "@playwright/test": "npm:^1.41.2" "@types/eslint": "npm:^8" + "@types/node": "npm:^20.11.17" "@vitejs/plugin-vue": "npm:^4.2.3" concurrently: "npm:^8.2.2" eslint: "npm:^8.53.0" @@ -1420,6 +1422,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.41.2": + version: 1.46.1 + resolution: "@playwright/test@npm:1.46.1" + dependencies: + playwright: "npm:1.46.1" + bin: + playwright: cli.js + checksum: 09e2c28574402f14e2d6f6843022c5778382dc7f703bae931dd531fc0fc1b725a862d3b52932bd6912cb13cbaed54822af33eb3d70134d93b0f1c10ec3fb0756 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1779,6 +1792,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.17": + version: 20.16.2 + resolution: "@types/node@npm:20.16.2" + dependencies: + undici-types: "npm:~6.19.2" + checksum: fcae2ffaa681c2947cd3dae67a6dcf83ef666fc0994281ad881b9e3bb542fec3a9206d9ce899c20e5cdddace2b96b42e32f247864de9baf95756c07b9eff15d7 + languageName: node + linkType: hard + "@types/semver@npm:^7.5.0": version: 7.5.5 resolution: "@types/semver@npm:7.5.5" @@ -4462,6 +4484,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -4472,6 +4504,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -6742,6 +6783,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.46.1": + version: 1.46.1 + resolution: "playwright-core@npm:1.46.1" + bin: + playwright-core: cli.js + checksum: 950aa935bba0b67ed289e07f31a52104c2b2ff9e39c46cda70b83f0b327e8114bcbcdeb4e8f94333ec941f9cd49cfac3af4cad91e247206ce927283482f24d91 + languageName: node + linkType: hard + +"playwright@npm:1.46.1": + version: 1.46.1 + resolution: "playwright@npm:1.46.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.46.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 17b0e7495a663dccbda4baf4953823a133af0b7cd4a5978bd2f40768a23e1a92d3659d7b48289a5160c9fa6269d8b9bbf5e2040aa4a63a3dd5f29475343ad3f2 + languageName: node + linkType: hard + "postcss-selector-parser@npm:^6.0.13": version: 6.0.13 resolution: "postcss-selector-parser@npm:6.0.13" @@ -7975,6 +8040,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0"