diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e05c19..22f39bb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,8 +28,14 @@ jobs:
- name: Install
run: pnpm i
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
- name: Build
run: pnpm build
- - name: Typecheck
- run: pnpm test
+ - name: Unit Tests
+ run: pnpm test:unit
+
+ - name: Run Playwright tests
+ run: npx playwright test
diff --git a/.gitignore b/.gitignore
index 9132915..93c5f98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,9 @@ coverage
docs/*.css
docs/*umd.js
-docs/*umd.js.map
\ No newline at end of file
+docs/*umd.js.map
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/docs/test.html b/docs/test.html
new file mode 100644
index 0000000..ddf1a86
--- /dev/null
+++ b/docs/test.html
@@ -0,0 +1,60 @@
+
+
+
+
+ Quill Table Module Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/test.js b/docs/test.js
new file mode 100644
index 0000000..8470bb1
--- /dev/null
+++ b/docs/test.js
@@ -0,0 +1,127 @@
+/* eslint-disable no-undef */
+const Quill = window.Quill;
+const {
+ default: TableUp,
+ TableAlign,
+ TableVirtualScrollbar,
+ TableResizeLine,
+ TableResizeBox,
+ TableMenuContextmenu,
+ TableMenuSelect,
+ TableResizeScale,
+ defaultCustomSelect,
+ TableSelection,
+} = window.TableUp;
+
+Quill.register({
+ [`modules/${TableUp.moduleName}`]: TableUp,
+}, true);
+
+const toolbarConfig = [
+ ['bold', 'italic', 'underline', 'strike'],
+ ['blockquote', 'code-block', 'code'],
+ ['link', 'image', 'video', 'formula'],
+ [{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
+ [{ script: 'sub' }, { script: 'super' }],
+ [{ indent: '-1' }, { indent: '+1' }],
+ [{ direction: 'rtl' }],
+ [{ size: ['small', false, 'large', 'huge'] }],
+ [{ header: [1, 2, 3, 4, 5, 6, false] }],
+ [{ color: [] }, { background: [] }],
+ [{ font: [] }],
+ [{ align: [] }],
+ [{ [TableUp.toolName]: [] }],
+ ['clean'],
+];
+
+const quills = [
+ {
+ full: false,
+ scrollbar: TableVirtualScrollbar,
+ align: TableAlign,
+ resize: TableResizeLine,
+ resizeScale: TableResizeScale,
+ customSelect: defaultCustomSelect,
+ customBtn: true,
+ selection: TableSelection,
+ selectionOptions: {
+ tableMenu: TableMenuContextmenu,
+ },
+ },
+ {
+ full: false,
+ scrollbar: TableVirtualScrollbar,
+ align: TableAlign,
+ resize: TableResizeBox,
+ resizeScale: TableResizeScale,
+ customSelect: defaultCustomSelect,
+ customBtn: true,
+ selection: TableSelection,
+ selectionOptions: {
+ tableMenu: TableMenuSelect,
+ },
+ },
+ {
+ full: true,
+ scrollbar: TableVirtualScrollbar,
+ align: TableAlign,
+ resize: TableResizeLine,
+ resizeScale: TableResizeScale,
+ customSelect: defaultCustomSelect,
+ customBtn: true,
+ selection: TableSelection,
+ selectionOptions: {
+ tableMenu: TableMenuContextmenu,
+ },
+ },
+ {
+ full: true,
+ scrollbar: TableVirtualScrollbar,
+ align: TableAlign,
+ resize: TableResizeBox,
+ resizeScale: TableResizeScale,
+ customSelect: defaultCustomSelect,
+ customBtn: true,
+ selection: TableSelection,
+ selectionOptions: {
+ tableMenu: TableMenuSelect,
+ },
+ },
+].map((ops, i) => {
+ return new Quill(`#editor${i + 1}`, {
+ // debug: 'info',
+ theme: 'snow',
+ modules: {
+ toolbar: toolbarConfig,
+ [TableUp.moduleName]: ops,
+ },
+ });
+});
+
+window.quills = quills;
+
+const output = [
+ output1,
+ output2,
+ output3,
+ output4,
+];
+
+for (const [i, btn] of [
+ btn1,
+ btn2,
+ btn3,
+ btn4,
+].entries()) {
+ btn.addEventListener('click', () => {
+ const content = quills[i].getContents();
+ console.log(content);
+ output[i].innerHTML = '';
+ // eslint-disable-next-line unicorn/no-array-for-each
+ content.forEach((content) => {
+ const item = document.createElement('li');
+ item.textContent = `${JSON.stringify(content)},`;
+ output[i].appendChild(item);
+ });
+ });
+}
diff --git a/package.json b/package.json
index a04ac8c..7e388ae 100644
--- a/package.json
+++ b/package.json
@@ -33,9 +33,11 @@
"lint:fix": "eslint . --fix",
"build": "gulp --require @esbuild-kit/cjs-loader",
"dev": "gulp --require @esbuild-kit/cjs-loader dev",
- "test": "vitest",
- "test:ui": "vitest --ui",
- "test:cover": "vitest --coverage"
+ "server": "node ./server.js",
+ "test:unit": "vitest",
+ "test:unit-ui": "vitest --ui",
+ "test:e2e": "playwright test",
+ "test:e2e-ui": "playwright test --ui"
},
"peerDependencies": {
"quill": "^2.0.0"
@@ -45,6 +47,7 @@
},
"devDependencies": {
"@esbuild-kit/cjs-loader": "^2.4.4",
+ "@playwright/test": "^1.49.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..5abb602
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,56 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './src/__tests__/e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ trace: 'on-first-retry',
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ // {
+ // name: 'firefox',
+ // use: { ...devices['Desktop Firefox'] },
+ // },
+
+ // {
+ // name: 'webkit',
+ // use: { ...devices['Desktop Safari'] },
+ // },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+ webServer: {
+ command: 'npm run server',
+ port: 5500,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'ignore',
+ stderr: 'pipe',
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 18b9f84..79db937 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -18,6 +18,9 @@ importers:
'@esbuild-kit/cjs-loader':
specifier: ^2.4.4
version: 2.4.4
+ '@playwright/test':
+ specifier: ^1.49.1
+ version: 1.49.1
'@rollup/plugin-node-resolve':
specifier: ^15.3.0
version: 15.3.0(rollup@4.18.1)
@@ -682,6 +685,11 @@ packages:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.49.1':
+ resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
@@ -1963,6 +1971,11 @@ packages:
os: [darwin]
deprecated: Upgrade to fsevents v2 to mitigate potential security issues
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2875,6 +2888,16 @@ packages:
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'}
+ playwright-core@1.49.1:
+ resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.49.1:
+ resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
plugin-error@1.0.1:
resolution: {integrity: sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==}
engines: {node: '>= 0.10'}
@@ -4123,6 +4146,10 @@ snapshots:
'@pkgr/core@0.1.1': {}
+ '@playwright/test@1.49.1':
+ dependencies:
+ playwright: 1.49.1
+
'@polka/url@1.0.0-next.25': {}
'@rollup/plugin-node-resolve@15.3.0(rollup@4.18.1)':
@@ -5641,6 +5668,9 @@ snapshots:
nan: 2.20.0
optional: true
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -6608,6 +6638,14 @@ snapshots:
pinkie@2.0.4: {}
+ playwright-core@1.49.1: {}
+
+ playwright@1.49.1:
+ dependencies:
+ playwright-core: 1.49.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
plugin-error@1.0.1:
dependencies:
ansi-colors: 1.1.0
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..7d8b4d0
--- /dev/null
+++ b/server.js
@@ -0,0 +1,39 @@
+import fs from 'node:fs';
+import http from 'node:http';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const hostname = '127.0.0.1';
+const PORT = 5500;
+const server = http.createServer((req, res) => {
+ const filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url);
+ const extname = String(path.extname(filePath)).toLowerCase();
+ const mimeTypes = {
+ '.html': 'text/html',
+ '.js': 'text/javascript',
+ '.css': 'text/css',
+ };
+ const contentType = mimeTypes[extname] || 'application/octet-stream';
+ fs.readFile(filePath, (err, content) => {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ res.end('404 Not Found');
+ }
+ else {
+ res.writeHead(500);
+ res.end(`Server Error: ${err.code}`);
+ }
+ }
+ else {
+ res.writeHead(200, { 'Content-Type': contentType });
+ res.end(content, 'utf8');
+ }
+ });
+});
+
+server.listen(PORT, hostname, () => {
+ console.log(`Server is running on http://localhost:${PORT}`);
+});
diff --git a/src/__tests__/e2e/custom-creator.test.ts b/src/__tests__/e2e/custom-creator.test.ts
new file mode 100644
index 0000000..a956929
--- /dev/null
+++ b/src/__tests__/e2e/custom-creator.test.ts
@@ -0,0 +1,44 @@
+import { expect, test } from '@playwright/test';
+import { createTableBySelect } from './utils';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('http://127.0.0.1:5500/docs/test.html');
+ page.locator('#editor1.ql-container.ql-snow');
+});
+
+test('custom selecor should work', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 3, 3);
+
+ const isVisible = await page.locator('#editor1.ql-container .ql-table-wrapper').isVisible();
+ expect(isVisible).toBe(true);
+ const colCount = await page.locator('#editor1.ql-container .ql-table-wrapper col').count();
+ expect(colCount).toBe(3);
+ const rowCount = await page.locator('#editor1.ql-container .ql-table-wrapper tr').count();
+ expect(rowCount).toBe(3);
+ const cellCount = await page.locator('#editor1.ql-container .ql-table-wrapper td').count();
+ expect(cellCount).toBe(9);
+});
+
+test('custom button should work', async ({ page }) => {
+ await page.locator('#container1 .ql-toolbar .ql-table-up > .ql-picker-label').first().click();
+ await page.locator('#container1 .ql-toolbar .ql-table-up .ql-custom-select').getByText('Custom').click();
+
+ await page.locator('.table-up-dialog .table-up-button.confirm').click();
+ const rowInput = page.locator('.table-up-dialog .table-up-input__input').first();
+ expect(rowInput).toHaveClass(/error/);
+ const errorText = await page.locator('.table-up-dialog .table-up-input__input').first().locator('.table-up-input__error-tip').textContent();
+ expect(errorText).toBe('Please enter a positive integer');
+
+ await page.locator('.table-up-input__item').nth(0).locator('input').fill('3');
+ await page.locator('.table-up-input__item').nth(1).locator('input').fill('3');
+ await page.getByRole('button', { name: 'Confirm' }).click();
+
+ const isVisible = await page.locator('#editor1.ql-container .ql-table-wrapper').isVisible();
+ expect(isVisible).toBe(true);
+ const colCount = await page.locator('#editor1.ql-container .ql-table-wrapper col').count();
+ expect(colCount).toBe(3);
+ const rowCount = await page.locator('#editor1.ql-container .ql-table-wrapper tr').count();
+ expect(rowCount).toBe(3);
+ const cellCount = await page.locator('#editor1.ql-container .ql-table-wrapper td').count();
+ expect(cellCount).toBe(9);
+});
diff --git a/src/__tests__/e2e/table-align.test.ts b/src/__tests__/e2e/table-align.test.ts
new file mode 100644
index 0000000..260b3b4
--- /dev/null
+++ b/src/__tests__/e2e/table-align.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from '@playwright/test';
+import { createTableBySelect } from './utils';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('http://127.0.0.1:5500/docs/test.html');
+ page.locator('.ql-container.ql-snow');
+});
+
+test('test TableAlign', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 3, 3);
+ const centerCell = page.locator('#editor1').getByRole('cell').nth(4);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+ await page.mouse.move(cellBounding.x, cellBounding.y);
+ const colBoundingBox = (await page.locator('#editor1 .table-up-resize-line__col').boundingBox())!;
+ expect(colBoundingBox).not.toBeNull();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2 - 200, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.up();
+ await centerCell.click();
+
+ const isVisible = await page.locator('#editor1 .table-up-align.table-up-align--active').isVisible();
+ expect(isVisible).toBeTruthy();
+
+ const table = page.locator('#editor1 .ql-editor .ql-table');
+ await page.locator('#editor1 .table-up-align .table-up-align__item[data-align="center"]').click();
+ await expect(table).toHaveCSS('margin-left', `100px`);
+ await expect(table).toHaveCSS('margin-right', `100px`);
+
+ await page.locator('#editor1 .table-up-align .table-up-align__item[data-align="right"]').click();
+ await expect(table).toHaveCSS('margin-left', `200px`);
+ await expect(table).toHaveCSS('margin-right', '0px');
+
+ await page.locator('#editor1 .table-up-align .table-up-align__item[data-align="left"]').click();
+ await expect(table).toHaveCSS('margin-left', '0px');
+ await expect(table).toHaveCSS('margin-right', '200px');
+});
diff --git a/src/__tests__/e2e/table-resize.test.ts b/src/__tests__/e2e/table-resize.test.ts
new file mode 100644
index 0000000..36fe710
--- /dev/null
+++ b/src/__tests__/e2e/table-resize.test.ts
@@ -0,0 +1,138 @@
+import { expect, test } from '@playwright/test';
+import { createTableBySelect } from './utils';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('http://127.0.0.1:5500/docs/test.html');
+ page.locator('.ql-container.ql-snow');
+});
+
+test('test TableResizeLine fixed width', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 3, 3);
+ const centerCell = page.locator('#editor1').getByRole('cell').nth(4);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+
+ // trigger mousemove in table to set resize line position
+ await page.mouse.move(cellBounding.x, cellBounding.y);
+ // col
+ const colBoundingBox = (await page.locator('#editor1 .table-up-resize-line__col').boundingBox())!;
+ expect(colBoundingBox).not.toBeNull();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2 + 100, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.up();
+ expect(page.locator('#editor1 .ql-table-wrapper col').nth(1)).toHaveAttribute('width', `${Math.floor(cellBounding.width) + 100}px`);
+
+ await page.mouse.move(cellBounding.x, cellBounding.y);
+ // row
+ const rowBoundingBox = (await page.locator('#editor1 .table-up-resize-line__row').boundingBox())!;
+ expect(rowBoundingBox).not.toBeNull();
+ await page.mouse.move(rowBoundingBox.x + rowBoundingBox.width / 2, rowBoundingBox.y + rowBoundingBox.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(rowBoundingBox.x + rowBoundingBox.width / 2, rowBoundingBox.y + rowBoundingBox.height / 2 + 100);
+ await page.mouse.up();
+ const cells = await page.locator('#editor1 .ql-table-wrapper tr').nth(1).locator('td').all();
+
+ for (const cell of cells) {
+ await expect(cell).toHaveCSS('height', `${Math.floor(cellBounding.height) + 100}px`);
+ }
+});
+
+test('test TableResizeBox fixed width', async ({ page }) => {
+ await createTableBySelect(page, 'container2', 3, 3);
+ const centerCell = page.locator('#editor2').getByRole('cell').nth(4);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+
+ // col
+ const colBoundingBox = (await page.locator('#editor2 .table-up-resize-box__col-separator').nth(1).boundingBox())!;
+ expect(colBoundingBox).not.toBeNull();
+ // -4 for sure click on the separator
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width - 4, colBoundingBox.y + 4);
+ await page.mouse.down();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width - 4 + 100, colBoundingBox.y + 4);
+ await page.mouse.up();
+ expect(page.locator('#editor2 .ql-table-wrapper col').nth(1)).toHaveAttribute('width', `${Math.floor(cellBounding.width - 4) + 100}px`);
+
+ // row
+ const rowBoundingBox = (await page.locator('#editor2 .table-up-resize-box__row-separator').nth(1).boundingBox())!;
+ expect(rowBoundingBox).not.toBeNull();
+ // -4 for sure click on the separator
+ await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y + rowBoundingBox.height - 4);
+ await page.mouse.down();
+ await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y + rowBoundingBox.height - 4 + 100);
+ await page.mouse.up();
+ const cells = await page.locator('#editor2 .ql-table-wrapper tr').nth(1).locator('td').all();
+ expect(cells.length).toEqual(3);
+ for (const cell of cells) {
+ await expect(cell).toHaveCSS('height', `${Math.floor(cellBounding.height - 4) + 100}px`);
+ }
+});
+
+test('test TableResizeLine full width', async ({ page }) => {
+ await createTableBySelect(page, 'container3', 4, 4);
+ const centerCell = page.locator('#editor3').getByRole('cell').nth(1);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ const tableBounding = (await page.locator('#editor3 .ql-table').boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+ expect(tableBounding).not.toBeNull();
+
+ // trigger mousemove in table to set resize line position
+ await page.mouse.move(cellBounding.x, cellBounding.y);
+ const colBoundingBox = (await page.locator('#editor3 .table-up-resize-line__col').boundingBox())!;
+ expect(colBoundingBox).not.toBeNull();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width / 2 + tableBounding.width * 0.05, colBoundingBox.y + colBoundingBox.height / 2);
+ await page.mouse.up();
+ const cols = page.locator('#editor3 .ql-table-wrapper col');
+ await expect(cols.nth(1)).toHaveAttribute('width', '30%');
+ await expect(cols.nth(2)).toHaveAttribute('width', '20%');
+});
+
+test('test TableResizeBox full width', async ({ page }) => {
+ await createTableBySelect(page, 'container4', 4, 4);
+ const centerCell = page.locator('#editor4').getByRole('cell').nth(1);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ const tableBounding = (await page.locator('#editor4 .ql-table').boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+ expect(tableBounding).not.toBeNull();
+
+ const colBoundingBox = (await page.locator('#editor4 .table-up-resize-box__col-separator').nth(1).boundingBox())!;
+ expect(colBoundingBox).not.toBeNull();
+ // -4 for sure click on the separator
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width - 4, colBoundingBox.y);
+ await page.mouse.down();
+ await page.mouse.move(colBoundingBox.x + colBoundingBox.width - 4 + tableBounding.width * 0.05, colBoundingBox.y);
+ await page.mouse.up();
+ const cols = page.locator('#editor4 .ql-table-wrapper col');
+ await expect(cols.nth(1)).toHaveAttribute('width', '30%');
+ await expect(cols.nth(2)).toHaveAttribute('width', '20%');
+});
+
+test('test TableResizeScale', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 3, 3);
+ const centerCell = page.locator('#editor1').getByRole('cell').nth(4);
+ await centerCell.click();
+ const cellBounding = (await centerCell.boundingBox())!;
+ const scaleBtnBounding = (await page.locator('#editor1 .table-up-scale__block').boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+ expect(scaleBtnBounding).not.toBeNull();
+ await page.mouse.move(scaleBtnBounding.x + scaleBtnBounding.width / 2, scaleBtnBounding.y + scaleBtnBounding.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(scaleBtnBounding.x + scaleBtnBounding.width / 2 + 90, scaleBtnBounding.y + scaleBtnBounding.height / 2 + 90);
+ await page.mouse.up();
+ const cols = page.locator('#editor1 .ql-table-wrapper col');
+ for (const col of await cols.all()) {
+ await expect(col).toHaveAttribute('width', `${Math.floor(cellBounding.width + 30)}px`);
+ }
+
+ const cells = page.locator('#editor1 .ql-table-wrapper td');
+ for (const cell of await cells.all()) {
+ await expect(cell).toHaveCSS('height', `${Math.floor(cellBounding.height + 30)}px`);
+ }
+});
diff --git a/src/__tests__/e2e/table-selection.test.ts b/src/__tests__/e2e/table-selection.test.ts
new file mode 100644
index 0000000..80fb6e9
--- /dev/null
+++ b/src/__tests__/e2e/table-selection.test.ts
@@ -0,0 +1,83 @@
+import { expect, test } from '@playwright/test';
+import { createTableBySelect } from './utils';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('http://127.0.0.1:5500/docs/test.html');
+ page.locator('.ql-container.ql-snow');
+});
+
+test('test TableSelection horizontal', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 5, 5);
+ const cell = page.locator('#editor1 .ql-editor .ql-table td').nth(0);
+ await cell.click();
+ const cellBounding = (await cell.boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+
+ await cell.click();
+ const selectionLine = page.locator('#editor1 .table-up-selection__line');
+ await expect(selectionLine).toBeVisible();
+ await page.mouse.down();
+ await page.mouse.move(cellBounding.x + cellBounding.width * 3, cellBounding.y + cellBounding.height / 2);
+ await page.mouse.up();
+
+ await expect(selectionLine).toBeVisible();
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).width)),
+ ).toBeCloseTo(cellBounding.width * 3, -1);
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).height)),
+ ).toBeCloseTo(cellBounding.height, -1);
+
+ await page.locator('#editor1 .ql-editor .ql-table').click({ button: 'right' });
+ await page.locator('.table-up-menu.is-contextmenu .table-up-menu__item').filter({ hasText: 'Merge Cell' }).first().click();
+
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(3).click();
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(3).click();
+ await expect(selectionLine).toBeVisible();
+ await page.mouse.down();
+ await page.mouse.move(cellBounding.x, cellBounding.y);
+ await page.mouse.up();
+
+ const mergeCellBounding = (await page.locator('#editor1 .ql-editor .ql-table td').nth(0).boundingBox())!;
+ expect(mergeCellBounding).not.toBeNull();
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).width)),
+ ).toBeCloseTo(mergeCellBounding.width, -1);
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).height)),
+ ).toBeCloseTo(mergeCellBounding.height + cellBounding.height, -1);
+});
+
+test('test TableSelection vertical', async ({ page }) => {
+ await createTableBySelect(page, 'container1', 5, 5);
+ const cell = page.locator('#editor1 .ql-editor .ql-table td').nth(0);
+ const cellBounding = (await cell.boundingBox())!;
+ expect(cellBounding).not.toBeNull();
+ await cell.click();
+ await cell.click();
+ const selectionLine = page.locator('#editor1 .table-up-selection__line');
+ await expect(selectionLine).toBeVisible();
+
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(0).click();
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(0).click();
+ await expect(selectionLine).toBeVisible();
+ await page.mouse.down();
+ await page.mouse.move(cellBounding.x, cellBounding.y + cellBounding.height * 3);
+ await page.mouse.up();
+ await page.locator('#editor1 .ql-editor .ql-table').click({ button: 'right' });
+ await page.locator('.table-up-menu.is-contextmenu .table-up-menu__item').filter({ hasText: 'Merge Cell' }).first().click();
+
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(0).click();
+ await page.locator('#editor1 .ql-editor .ql-table td').nth(0).click();
+ await expect(selectionLine).toBeVisible();
+ await page.mouse.down();
+ await page.mouse.move(cellBounding.x + cellBounding.width * 2, cellBounding.y);
+ await page.mouse.up();
+
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).width)),
+ ).toBeCloseTo(cellBounding.width * 2, -1);
+ expect(
+ Number.parseFloat(await selectionLine.evaluate(el => getComputedStyle(el).height)),
+ ).toBeCloseTo(cellBounding.height * 3, -1);
+});
diff --git a/src/__tests__/e2e/utils.ts b/src/__tests__/e2e/utils.ts
new file mode 100644
index 0000000..3df8cd2
--- /dev/null
+++ b/src/__tests__/e2e/utils.ts
@@ -0,0 +1,6 @@
+import type { Page } from '@playwright/test';
+
+export const createTableBySelect = async (page: Page, container: string, row: number, col: number) => {
+ await page.locator(`#${container} .ql-toolbar .ql-table-up.ql-picker`).click();
+ await page.locator(`#${container} .ql-toolbar .ql-custom-select .table-up-select-box__item[data-row="${row}"][data-col="${col}"]`).click();
+};