diff --git a/packages/quill/test/e2e/__dev_server__/iframe.html b/packages/quill/test/e2e/__dev_server__/iframe.html new file mode 100644 index 0000000000..1b44556495 --- /dev/null +++ b/packages/quill/test/e2e/__dev_server__/iframe.html @@ -0,0 +1,95 @@ + + + + + + + Quill E2E Tests - Iframe + + + + + + + + + + diff --git a/packages/quill/test/e2e/__dev_server__/webpack.config.cjs b/packages/quill/test/e2e/__dev_server__/webpack.config.cjs index bc1b18fab4..bca22f2597 100644 --- a/packages/quill/test/e2e/__dev_server__/webpack.config.cjs +++ b/packages/quill/test/e2e/__dev_server__/webpack.config.cjs @@ -6,8 +6,9 @@ const common = require('../../../webpack.common.cjs'); const { merge } = require('webpack-merge'); require('webpack-dev-server'); -module.exports = (env) => - merge(common, { +module.exports = (env) => { + console.log(env); + return merge(common, { plugins: [ new HtmlWebpackPlugin({ publicPath: '/', @@ -17,6 +18,14 @@ module.exports = (env) => inject: 'head', scriptLoading: 'blocking', }), + new HtmlWebpackPlugin({ + publicPath: '/', + filename: 'iframe.html', + template: path.resolve(__dirname, 'iframe.html'), + chunks: ['quill'], + inject: 'head', + scriptLoading: 'blocking', + }), ], devServer: { port: env.port, @@ -30,3 +39,4 @@ module.exports = (env) => webSocketServer: false, }, }); +}; diff --git a/packages/quill/test/e2e/fixtures/index.ts b/packages/quill/test/e2e/fixtures/index.ts index c4c006757f..ecb73e8920 100644 --- a/packages/quill/test/e2e/fixtures/index.ts +++ b/packages/quill/test/e2e/fixtures/index.ts @@ -4,32 +4,36 @@ import Composition from './Composition.js'; import Locker from './utils/Locker.js'; import Clipboard from './Clipboard.js'; -export const test = base.extend<{ - editorPage: EditorPage; - clipboard: Clipboard; - composition: Composition; -}>({ - editorPage: ({ page }, use) => { - use(new EditorPage(page)); - }, - composition: ({ page, browserName }, use) => { - test.fail( - browserName !== 'chromium', - 'CDPSession is only available in Chromium', - ); +export const testWithMode = function (mode: 'regular' | 'iframe') { + return base.extend<{ + editorPage: EditorPage; + clipboard: Clipboard; + composition: Composition; + }>({ + editorPage: ({ page }, use) => { + use(new EditorPage(page, mode)); + }, + composition: ({ page, browserName }, use) => { + test.fail( + browserName !== 'chromium', + 'CDPSession is only available in Chromium', + ); - 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(); + use(new Composition(page, browserName)); }, - { timeout: 30000 }, - ], -}); + clipboard: [ + async ({ page }, use) => { + const locker = new Locker('clipboard'); + await locker.lock(); + await use(new Clipboard(page)); + await locker.release(); + }, + { timeout: 30000 }, + ], + }); +}; + +export const test = testWithMode('regular'); export const CHAPTER = 'Chapter 1. Loomings.'; export const P1 = diff --git a/packages/quill/test/e2e/full.spec.ts b/packages/quill/test/e2e/full.spec.ts index 01e2b75a1b..1d9899772c 100644 --- a/packages/quill/test/e2e/full.spec.ts +++ b/packages/quill/test/e2e/full.spec.ts @@ -1,212 +1,220 @@ import { expect } from '@playwright/test'; import { getSelectionInTextNode, SHORTKEY } from './utils/index.js'; -import { test, CHAPTER, P1, P2 } from './fixtures/index.js'; +import { CHAPTER, P1, P2, testWithMode } from './fixtures/index.js'; -test('compose an epic', async ({ page, editorPage }) => { - await editorPage.open(); - await editorPage.root.pressSequentially('The Whale'); - expect(await editorPage.root.innerHTML()).toEqual('

The Whale

'); +function epicTest(mode: 'regular' | 'iframe' = 'regular') { + testWithMode(mode)( + `compose an epic (${mode})`, + async ({ page, editorPage }) => { + await editorPage.open(); + await editorPage.root.pressSequentially('The Whale'); + expect(await editorPage.root.innerHTML()).toEqual('

The Whale

'); - await page.keyboard.press('Enter'); - expect(await editorPage.root.innerHTML()).toEqual( - '

The Whale


', - ); + await page.keyboard.press('Enter'); + expect(await editorPage.root.innerHTML()).toEqual( + '

The Whale


', + ); - await page.keyboard.press('Enter'); - await page.keyboard.press('Tab'); - await editorPage.root.pressSequentially(P1); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); - await editorPage.root.pressSequentially(P2); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

The Whale

', - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + await page.keyboard.type(P1); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await editorPage.root.pressSequentially(P2); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

The Whale

', + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - // More than enough to get to top - await Promise.all( - Array(40) - .fill(0) - .map(() => page.keyboard.press('ArrowUp')), - ); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.type('.ql-editor', CHAPTER); - await page.keyboard.press('Enter'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

The Whale

', - '


', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + // More than enough to get to top + await Promise.all( + Array(40) + .fill(0) + .map(() => page.keyboard.press('ArrowUp')), + ); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await editorPage.root.pressSequentially(CHAPTER); + await page.keyboard.press('Enter'); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

The Whale

', + '


', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - // More than enough to get to top - await Promise.all( - Array(20) - .fill(0) - .map(() => page.keyboard.press('ArrowUp')), - ); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

Whale

', - '


', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + // More than enough to get to top + await Promise.all( + Array(20) + .fill(0) + .map(() => page.keyboard.press('ArrowUp')), + ); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

Whale

', + '


', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - await page.keyboard.press('Delete'); - await page.keyboard.press('Delete'); - await page.keyboard.press('Delete'); - await page.keyboard.press('Delete'); - await page.keyboard.press('Delete'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '


', - '


', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + await page.keyboard.press('Delete'); + await page.keyboard.press('Delete'); + await page.keyboard.press('Delete'); + await page.keyboard.press('Delete'); + await page.keyboard.press('Delete'); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '


', + '


', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - await page.keyboard.press('Delete'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '


', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + await page.keyboard.press('Delete'); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '


', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - await page.click('.ql-toolbar .ql-bold'); - await page.click('.ql-toolbar .ql-italic'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

\uFEFF

', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); - let bold = await page.$('.ql-toolbar .ql-bold.ql-active'); - let italic = await page.$('.ql-toolbar .ql-italic.ql-active'); - expect(bold).not.toBe(null); - expect(italic).not.toBe(null); + await editorPage.toolbar.locator('.ql-bold').click(); + await editorPage.toolbar.locator('.ql-italic').click(); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

\uFEFF

', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); + let bold = editorPage.toolbar.locator('.ql-bold'); + let italic = editorPage.toolbar.locator('.ql-italic'); + expect(bold).toHaveClass(/ql-active/); + expect(italic).toHaveClass(/ql-active/); - await editorPage.root.pressSequentially('Moby Dick'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

Moby Dick

', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); - bold = await page.$('.ql-toolbar .ql-bold.ql-active'); - italic = await page.$('.ql-toolbar .ql-italic.ql-active'); - expect(bold).not.toBe(null); - expect(italic).not.toBe(null); + await editorPage.root.pressSequentially('Moby Dick'); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

Moby Dick

', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); + bold = editorPage.toolbar.locator('.ql-bold'); + italic = editorPage.toolbar.locator('.ql-italic'); + expect(bold).toHaveClass(/ql-active/); + expect(italic).toHaveClass(/ql-active/); - await page.keyboard.press('ArrowRight'); - await page.keyboard.down('Shift'); - await Promise.all( - Array(CHAPTER.length) - .fill(0) - .map(() => page.keyboard.press('ArrowRight')), - ); - await page.keyboard.up('Shift'); - bold = await page.$('.ql-toolbar .ql-bold.ql-active'); - italic = await page.$('.ql-toolbar .ql-italic.ql-active'); - expect(bold).toBe(null); - expect(italic).toBe(null); + await page.keyboard.press('ArrowRight'); + await page.keyboard.down('Shift'); + await Promise.all( + Array(CHAPTER.length) + .fill(0) + .map(() => page.keyboard.press('ArrowRight')), + ); + await page.keyboard.up('Shift'); + bold = editorPage.toolbar.locator('.ql-bold'); + italic = editorPage.toolbar.locator('.ql-italic'); + expect(bold).not.toHaveClass(/ql-active/); + expect(italic).not.toHaveClass(/ql-active/); - await page.keyboard.down(SHORTKEY); - await page.keyboard.press('b'); - await page.keyboard.up(SHORTKEY); - bold = await page.$('.ql-toolbar .ql-bold.ql-active'); - expect(bold).not.toBe(null); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

Moby Dick

', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); + await page.keyboard.down(SHORTKEY); + await page.keyboard.press('b'); + await page.keyboard.up(SHORTKEY); + bold = editorPage.toolbar.locator('.ql-bold'); + expect(bold).toHaveClass(/ql-active/); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

Moby Dick

', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowUp'); - await page.click('.ql-toolbar .ql-header[value="1"]'); - expect(await editorPage.root.innerHTML()).toEqual( - [ - '

Moby Dick

', - `

${CHAPTER}

`, - '


', - `

\t${P1}

`, - '


', - `

${P2}

`, - ].join(''), - ); - const header = await page.$('.ql-toolbar .ql-header.ql-active[value="1"]'); - expect(header).not.toBe(null); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowUp'); + await editorPage.toolbar.locator('.ql-header[value="1"]').click(); + expect(await editorPage.root.innerHTML()).toEqual( + [ + '

Moby Dick

', + `

${CHAPTER}

`, + '


', + `

\t${P1}

`, + '


', + `

${P2}

`, + ].join(''), + ); + const header = editorPage.toolbar.locator('.ql-header[value="1"]'); + expect(header).toHaveClass(/ql-active/); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); - await page.keyboard.press('ArrowUp'); - await editorPage.root.pressSequentially('AA'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.down(SHORTKEY); - await page.keyboard.press('b'); - await page.keyboard.press('b'); - await page.keyboard.up(SHORTKEY); - await editorPage.root.pressSequentially('B'); - expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe('ABA'); - await page.keyboard.down(SHORTKEY); - await page.keyboard.press('b'); - await page.keyboard.up(SHORTKEY); - await editorPage.root.pressSequentially('C'); - await page.keyboard.down(SHORTKEY); - await page.keyboard.press('b'); - await page.keyboard.up(SHORTKEY); - await editorPage.root.pressSequentially('D'); - expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe( - 'ABCDA', + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.press('ArrowUp'); + await editorPage.root.pressSequentially('AA'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.down(SHORTKEY); + await page.keyboard.press('b'); + await page.keyboard.press('b'); + await page.keyboard.up(SHORTKEY); + await editorPage.root.pressSequentially('B'); + expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe('ABA'); + await page.keyboard.down(SHORTKEY); + await page.keyboard.press('b'); + await page.keyboard.up(SHORTKEY); + await editorPage.root.pressSequentially('C'); + await page.keyboard.down(SHORTKEY); + await page.keyboard.press('b'); + await page.keyboard.up(SHORTKEY); + await editorPage.root.pressSequentially('D'); + expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe( + 'ABCDA', + ); + const selection = await page.evaluate(getSelectionInTextNode); + expect(selection).toBe('["DA",1,"DA",1]'); + }, ); - const selection = await page.evaluate(getSelectionInTextNode); - expect(selection).toBe('["DA",1,"DA",1]'); -}); +} + +epicTest(); +epicTest('iframe'); diff --git a/packages/quill/test/e2e/pageobjects/EditorPage.ts b/packages/quill/test/e2e/pageobjects/EditorPage.ts index 0e770e0891..292cfbae2d 100644 --- a/packages/quill/test/e2e/pageobjects/EditorPage.ts +++ b/packages/quill/test/e2e/pageobjects/EditorPage.ts @@ -55,15 +55,33 @@ const updateSelectionDef = [ ]; export default class EditorPage { - constructor(protected readonly page: Page) {} + constructor( + protected readonly page: Page, + protected readonly mode: 'regular' | 'iframe' = 'regular', + ) {} get root() { + if (this.mode === 'iframe') { + return this.page.frameLocator('iframe').locator('.ql-editor'); + } return this.page.locator('.ql-editor'); } + get toolbar() { + if (this.mode === 'iframe') { + return this.page.frameLocator('iframe').locator('#toolbar-container'); + } + return this.page.locator('#toolbar-container'); + } + async open() { - await this.page.goto('/'); - await this.page.waitForSelector('.ql-editor', { timeout: 10000 }); + await this.page.goto(this.mode === 'iframe' ? '/iframe.html' : '/'); + await this.page.waitForSelector( + this.mode === 'iframe' ? 'iframe' : '.ql-editor', + { + timeout: 10000, + }, + ); } async html(content: string, title = '') { diff --git a/packages/quill/test/e2e/utils/index.ts b/packages/quill/test/e2e/utils/index.ts index dc2eb0f3b0..4607938651 100644 --- a/packages/quill/test/e2e/utils/index.ts +++ b/packages/quill/test/e2e/utils/index.ts @@ -2,7 +2,11 @@ export const isMac = process.platform === 'darwin'; export const SHORTKEY = isMac ? 'Meta' : 'Control'; export function getSelectionInTextNode() { - const selection = document.getSelection(); + const doc = + (document.activeElement?.tagName === 'IFRAME' + ? (document.activeElement as HTMLIFrameElement).contentDocument + : document) ?? document; + const selection = doc.getSelection(); if (!selection) { throw new Error('Selection is null'); } diff --git a/packages/quill/test/unit/core/quill.spec.ts b/packages/quill/test/unit/core/quill.spec.ts index c128c0dc12..9fc03c3296 100644 --- a/packages/quill/test/unit/core/quill.spec.ts +++ b/packages/quill/test/unit/core/quill.spec.ts @@ -1377,4 +1377,25 @@ describe('Quill', () => { ).toEqual(0); }); }); + + describe('iframe', () => { + const createDocument = (): Document => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + return iframe.contentDocument!; + }; + const createContainer = (html: string | { html: string } = '') => { + const doc = createDocument(); + const container = doc!.createElement('div'); + container.innerHTML = normalizeHTML(html); + doc!.body.appendChild(container); + return container; + }; + + test('initialize empty', () => { + const quill = new Quill(createContainer('0123')); + expect(quill.getContents()).toEqual(new Delta().insert('0123\n')); + expect(quill.root.innerHTML).toMatchInlineSnapshot('"

0123

"'); + }); + }); });