Clipboard support for E2E tests

This commit is contained in:
Zihua Li 2024-06-21 10:47:32 +08:00 committed by Zihua Li
parent d5efd42de7
commit 5d752e3fa8
8 changed files with 258 additions and 24 deletions

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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'] } },
],

View 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;

View File

@ -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.';

View 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;

View File

@ -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');