Clipboard support for E2E tests
This commit is contained in:
parent
d5efd42de7
commit
5d752e3fa8
6
.github/workflows/_test.yml
vendored
6
.github/workflows/_test.yml
vendored
@ -17,8 +17,10 @@ jobs:
|
||||
run: npx playwright install --with-deps
|
||||
working-directory: packages/quill
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:e2e
|
||||
working-directory: packages/quill
|
||||
uses: coactions/setup-xvfb@v1
|
||||
with:
|
||||
run: npm run test:e2e -- --headed
|
||||
working-directory: packages/quill
|
||||
fuzz:
|
||||
name: Fuzz Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
112
package-lock.json
generated
112
package-lock.json
generated
@ -4283,12 +4283,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
|
||||
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
|
||||
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.38.1"
|
||||
"playwright": "1.44.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -14271,9 +14271,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
|
||||
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@ -14824,6 +14824,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
||||
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/param-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||
@ -14956,15 +14962,15 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
|
||||
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@ -15167,12 +15173,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
|
||||
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
|
||||
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.38.1"
|
||||
"playwright-core": "1.44.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -15185,9 +15191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
|
||||
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
|
||||
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
@ -19243,7 +19249,7 @@
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@playwright/test": "1.44.1",
|
||||
"@types/highlight.js": "^9.12.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.0",
|
||||
@ -19262,6 +19268,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-require-extensions": "^0.1.3",
|
||||
"glob": "10.4.2",
|
||||
"highlight.js": "^9.18.1",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
@ -19288,6 +19295,71 @@
|
||||
"npm": ">=8.2.3"
|
||||
}
|
||||
},
|
||||
"packages/quill/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"packages/quill/node_modules/glob": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
|
||||
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/quill/node_modules/jackspeak": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz",
|
||||
"integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"packages/quill/node_modules/minimatch": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/website": {
|
||||
"version": "2.0.2",
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@playwright/test": "1.44.1",
|
||||
"@types/highlight.js": "^9.12.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.0",
|
||||
@ -36,6 +36,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-require-extensions": "^0.1.3",
|
||||
"glob": "10.4.2",
|
||||
"highlight.js": "^9.18.1",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
|
||||
@ -21,7 +21,15 @@ export default defineConfig({
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'Chrome', use: { ...devices['Desktop Chrome'] } },
|
||||
{
|
||||
name: 'Chrome',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
contextOptions: {
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
|
||||
],
|
||||
|
||||
93
packages/quill/test/e2e/fixtures/Clipboard.ts
Normal file
93
packages/quill/test/e2e/fixtures/Clipboard.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { SHORTKEY } from '../utils/index.js';
|
||||
|
||||
class Clipboard {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async copy() {
|
||||
await this.page.keyboard.press(`${SHORTKEY}+c`);
|
||||
}
|
||||
|
||||
async cut() {
|
||||
await this.page.keyboard.press(`${SHORTKEY}+x`);
|
||||
}
|
||||
|
||||
async paste() {
|
||||
await this.page.keyboard.press(`${SHORTKEY}+v`);
|
||||
}
|
||||
|
||||
async writeText(value: string) {
|
||||
// Playwright + Safari + Linux doesn't support async clipboard API
|
||||
// https://github.com/microsoft/playwright/issues/18901
|
||||
const hasFallbackWritten = await this.page.evaluate((value) => {
|
||||
if (navigator.clipboard) return false;
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = value;
|
||||
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const isSupported = document.execCommand('copy');
|
||||
textArea.remove();
|
||||
return isSupported;
|
||||
}, value);
|
||||
|
||||
if (!hasFallbackWritten) {
|
||||
await this.write(value, 'text/plain');
|
||||
}
|
||||
}
|
||||
|
||||
async writeHTML(value: string) {
|
||||
return this.write(value, 'text/html');
|
||||
}
|
||||
|
||||
async readText() {
|
||||
return this.read('text/plain');
|
||||
}
|
||||
|
||||
async readHTML() {
|
||||
const html = await this.read('text/html');
|
||||
return html.replace(/<meta[^>]*>/g, '');
|
||||
}
|
||||
|
||||
private async read(type: string) {
|
||||
const isHTML = type === 'text/html';
|
||||
await this.page.evaluate((isHTML) => {
|
||||
const dataContainer = document.createElement(isHTML ? 'div' : 'textarea');
|
||||
if (isHTML) dataContainer.setAttribute('contenteditable', 'true');
|
||||
dataContainer.id = '_readClipboard';
|
||||
document.body.appendChild(dataContainer);
|
||||
dataContainer.focus();
|
||||
return dataContainer;
|
||||
}, isHTML);
|
||||
await this.paste();
|
||||
const locator = this.page.locator('#_readClipboard');
|
||||
const data = await (isHTML ? locator.innerHTML() : locator.inputValue());
|
||||
await locator.evaluate((node) => node.remove());
|
||||
return data;
|
||||
}
|
||||
|
||||
private async write(data: string, type: string) {
|
||||
await this.page.evaluate(
|
||||
async ({ data, type }) => {
|
||||
if (type === 'text/html') {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/html': new Blob([data], { type: 'text/html' }),
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(data);
|
||||
}
|
||||
},
|
||||
{ data, type },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Clipboard;
|
||||
@ -1,6 +1,8 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
import EditorPage from '../pageobjects/EditorPage.js';
|
||||
import Composition from './Composition.js';
|
||||
import Locker from './utils/Locker.js';
|
||||
import Clipboard from './Clipboard.js';
|
||||
|
||||
export const test = base.extend<{
|
||||
editorPage: EditorPage;
|
||||
@ -18,6 +20,15 @@ export const test = base.extend<{
|
||||
|
||||
use(new Composition(page, browserName));
|
||||
},
|
||||
clipboard: [
|
||||
async ({ page }, use) => {
|
||||
const locker = new Locker('clipboard');
|
||||
await locker.lock();
|
||||
await use(new Clipboard(page));
|
||||
await locker.release();
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
],
|
||||
});
|
||||
|
||||
export const CHAPTER = 'Chapter 1. Loomings.';
|
||||
|
||||
39
packages/quill/test/e2e/fixtures/utils/Locker.ts
Normal file
39
packages/quill/test/e2e/fixtures/utils/Locker.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { unlink, writeFile } from 'fs/promises';
|
||||
import { unlinkSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { globSync } from 'glob';
|
||||
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
const PREFIX = 'playwright_locker_';
|
||||
|
||||
class Locker {
|
||||
public static clearAll() {
|
||||
globSync(join(tmpdir(), `${PREFIX}*.txt`)).forEach(unlinkSync);
|
||||
}
|
||||
|
||||
constructor(private key: string) {}
|
||||
|
||||
private get filePath() {
|
||||
return join(tmpdir(), `${PREFIX}${this.key}.txt`);
|
||||
}
|
||||
|
||||
async lock() {
|
||||
try {
|
||||
await writeFile(this.filePath, '', { flag: 'wx' });
|
||||
} catch {
|
||||
await sleep(50);
|
||||
await this.lock();
|
||||
}
|
||||
}
|
||||
|
||||
async release() {
|
||||
await unlink(this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
export default Locker;
|
||||
@ -38,6 +38,14 @@ test.describe('history', () => {
|
||||
expect(await editorPage.getContents()).toEqual([{ insert: '1234\n' }]);
|
||||
});
|
||||
|
||||
test('clipboard', async ({ clipboard, page, editorPage }) => {
|
||||
await editorPage.moveCursorAfterText('2');
|
||||
await clipboard.writeText('a');
|
||||
await clipboard.paste();
|
||||
await undo(page);
|
||||
expect(await editorPage.getContents()).toEqual([{ insert: '1234\n' }]);
|
||||
});
|
||||
|
||||
test.describe('selection', () => {
|
||||
test('typing', async ({ page, editorPage }) => {
|
||||
await editorPage.moveCursorAfterText('2');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user