Skip to content

Commit 5d752e3

Browse files
committed
Clipboard support for E2E tests
1 parent d5efd42 commit 5d752e3

File tree

8 files changed

+258
-24
lines changed

8 files changed

+258
-24
lines changed

.github/workflows/_test.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ jobs:
1717
run: npx playwright install --with-deps
1818
working-directory: packages/quill
1919
- name: Run Playwright tests
20-
run: npm run test:e2e
21-
working-directory: packages/quill
20+
uses: coactions/setup-xvfb@v1
21+
with:
22+
run: npm run test:e2e -- --headed
23+
working-directory: packages/quill
2224
fuzz:
2325
name: Fuzz Tests
2426
runs-on: ubuntu-latest

package-lock.json

Lines changed: 92 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/quill/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@babel/core": "^7.24.0",
1818
"@babel/preset-env": "^7.24.0",
1919
"@babel/preset-typescript": "^7.23.3",
20-
"@playwright/test": "1.38.1",
20+
"@playwright/test": "1.44.1",
2121
"@types/highlight.js": "^9.12.4",
2222
"@types/lodash-es": "^4.17.12",
2323
"@types/node": "^20.10.0",
@@ -36,6 +36,7 @@
3636
"eslint-plugin-jsx-a11y": "^6.8.0",
3737
"eslint-plugin-prettier": "^5.1.3",
3838
"eslint-plugin-require-extensions": "^0.1.3",
39+
"glob": "10.4.2",
3940
"highlight.js": "^9.18.1",
4041
"html-loader": "^4.2.0",
4142
"html-webpack-plugin": "^5.5.3",

packages/quill/playwright.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ export default defineConfig({
2121
ignoreHTTPSErrors: true,
2222
},
2323
projects: [
24-
{ name: 'Chrome', use: { ...devices['Desktop Chrome'] } },
24+
{
25+
name: 'Chrome',
26+
use: {
27+
...devices['Desktop Chrome'],
28+
contextOptions: {
29+
permissions: ['clipboard-read', 'clipboard-write'],
30+
},
31+
},
32+
},
2533
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
2634
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
2735
],
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Page } from '@playwright/test';
2+
import { SHORTKEY } from '../utils/index.js';
3+
4+
class Clipboard {
5+
constructor(private page: Page) {}
6+
7+
async copy() {
8+
await this.page.keyboard.press(`${SHORTKEY}+c`);
9+
}
10+
11+
async cut() {
12+
await this.page.keyboard.press(`${SHORTKEY}+x`);
13+
}
14+
15+
async paste() {
16+
await this.page.keyboard.press(`${SHORTKEY}+v`);
17+
}
18+
19+
async writeText(value: string) {
20+
// Playwright + Safari + Linux doesn't support async clipboard API
21+
// https://github.com/microsoft/playwright/issues/18901
22+
const hasFallbackWritten = await this.page.evaluate((value) => {
23+
if (navigator.clipboard) return false;
24+
const textArea = document.createElement('textarea');
25+
textArea.value = value;
26+
27+
textArea.style.top = '0';
28+
textArea.style.left = '0';
29+
textArea.style.position = 'fixed';
30+
31+
document.body.appendChild(textArea);
32+
textArea.focus();
33+
textArea.select();
34+
35+
const isSupported = document.execCommand('copy');
36+
textArea.remove();
37+
return isSupported;
38+
}, value);
39+
40+
if (!hasFallbackWritten) {
41+
await this.write(value, 'text/plain');
42+
}
43+
}
44+
45+
async writeHTML(value: string) {
46+
return this.write(value, 'text/html');
47+
}
48+
49+
async readText() {
50+
return this.read('text/plain');
51+
}
52+
53+
async readHTML() {
54+
const html = await this.read('text/html');
55+
return html.replace(/<meta[^>]*>/g, '');
56+
}
57+
58+
private async read(type: string) {
59+
const isHTML = type === 'text/html';
60+
await this.page.evaluate((isHTML) => {
61+
const dataContainer = document.createElement(isHTML ? 'div' : 'textarea');
62+
if (isHTML) dataContainer.setAttribute('contenteditable', 'true');
63+
dataContainer.id = '_readClipboard';
64+
document.body.appendChild(dataContainer);
65+
dataContainer.focus();
66+
return dataContainer;
67+
}, isHTML);
68+
await this.paste();
69+
const locator = this.page.locator('#_readClipboard');
70+
const data = await (isHTML ? locator.innerHTML() : locator.inputValue());
71+
await locator.evaluate((node) => node.remove());
72+
return data;
73+
}
74+
75+
private async write(data: string, type: string) {
76+
await this.page.evaluate(
77+
async ({ data, type }) => {
78+
if (type === 'text/html') {
79+
await navigator.clipboard.write([
80+
new ClipboardItem({
81+
'text/html': new Blob([data], { type: 'text/html' }),
82+
}),
83+
]);
84+
} else {
85+
await navigator.clipboard.writeText(data);
86+
}
87+
},
88+
{ data, type },
89+
);
90+
}
91+
}
92+
93+
export default Clipboard;

packages/quill/test/e2e/fixtures/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { test as base } from '@playwright/test';
22
import EditorPage from '../pageobjects/EditorPage.js';
33
import Composition from './Composition.js';
4+
import Locker from './utils/Locker.js';
5+
import Clipboard from './Clipboard.js';
46

57
export const test = base.extend<{
68
editorPage: EditorPage;
@@ -18,6 +20,15 @@ export const test = base.extend<{
1820

1921
use(new Composition(page, browserName));
2022
},
23+
clipboard: [
24+
async ({ page }, use) => {
25+
const locker = new Locker('clipboard');
26+
await locker.lock();
27+
await use(new Clipboard(page));
28+
await locker.release();
29+
},
30+
{ timeout: 30000 },
31+
],
2132
});
2233

2334
export const CHAPTER = 'Chapter 1. Loomings.';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { unlink, writeFile } from 'fs/promises';
2+
import { unlinkSync } from 'fs';
3+
import { tmpdir } from 'os';
4+
import { join } from 'path';
5+
import { globSync } from 'glob';
6+
7+
const sleep = (ms: number) =>
8+
new Promise((resolve) => {
9+
setTimeout(resolve, ms);
10+
});
11+
12+
const PREFIX = 'playwright_locker_';
13+
14+
class Locker {
15+
public static clearAll() {
16+
globSync(join(tmpdir(), `${PREFIX}*.txt`)).forEach(unlinkSync);
17+
}
18+
19+
constructor(private key: string) {}
20+
21+
private get filePath() {
22+
return join(tmpdir(), `${PREFIX}${this.key}.txt`);
23+
}
24+
25+
async lock() {
26+
try {
27+
await writeFile(this.filePath, '', { flag: 'wx' });
28+
} catch {
29+
await sleep(50);
30+
await this.lock();
31+
}
32+
}
33+
34+
async release() {
35+
await unlink(this.filePath);
36+
}
37+
}
38+
39+
export default Locker;

0 commit comments

Comments
 (0)