Skip to content

Commit

Permalink
Clipboard support for E2E tests
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Jun 21, 2024
1 parent d5efd42 commit f2e172b
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 22 deletions.
112 changes: 92 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/quill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion packages/quill/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] } },
],
Expand Down
93 changes: 93 additions & 0 deletions packages/quill/test/e2e/fixtures/Clipboard.ts
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions packages/quill/test/e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.';
Expand Down
39 changes: 39 additions & 0 deletions packages/quill/test/e2e/fixtures/utils/Locker.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions packages/quill/test/e2e/history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit f2e172b

Please sign in to comment.