463 lines
20 KiB
TypeScript
463 lines
20 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
||
|
||
/**
|
||
* 富文本表格的样式 / 交互回归。
|
||
*
|
||
* 1. 抗宿主页面全局样式:文档站(VitePress 默认主题)会在 `.vp-doc` 下写全局表格样式 ——
|
||
* `table { display: block }` 让表格退化为块级收缩布局、网格宽度不再是 100%;
|
||
* `tr:nth-child(2n) { background-color: … }` 把斑马纹染到表体行。组件必须在自身样式里
|
||
* 显式复位这些属性(与 Drawer 的 `section { margin }` 是同一类坑)。
|
||
* 2. 空内容插入表格要与顶部留出间距(`.tiptap` 需有纵向内边距)。
|
||
* 3. 单元格单行行高不能被 `<p>` 的 UA 外边距撑高。
|
||
* 4. 列宽拖拽:光标为 col-resize、hover 不改变行高、表格不被拖出容器、列宽有下限。
|
||
*
|
||
* 这些都是布局结果,只能放 E2E(happy-dom 没有布局引擎)。
|
||
*/
|
||
|
||
/**
|
||
* 插入表格。
|
||
*
|
||
* 面板已从「行数 / 列数」两个数字输入框换成**网格选择器**(对齐 TinyMCE):
|
||
* 移入 (rows, cols) 那个格子即高亮对应矩形,点击插入。所以这里改成点格子,
|
||
* 不再有「插入」按钮需要按。
|
||
*/
|
||
async function insertTable(page: Page, cols = 3, rows = 3): Promise<void> {
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.scrollIntoViewIfNeeded()
|
||
await editor.locator('button[aria-label="插入表格"]').click()
|
||
const cell = page.locator(`.g3-rich-text__table-grid-cell[data-row="${rows}"][data-col="${cols}"]`)
|
||
await cell.hover()
|
||
await cell.click()
|
||
await expect(editor.locator('table')).toHaveCount(1)
|
||
await editor.locator('table').scrollIntoViewIfNeeded()
|
||
}
|
||
|
||
/** 表格几何 / 样式快照 */
|
||
function tableSnapshot(page: Page) {
|
||
return page.evaluate(() => {
|
||
const root = document.querySelector('[data-testid="rich-text"]')!
|
||
const content = root.querySelector('.g3-rich-text__content')!
|
||
const tiptap = root.querySelector('.tiptap')!
|
||
const table = root.querySelector('table')!
|
||
const wrapper = table.parentElement!
|
||
const headerCells = Array.from(table.querySelectorAll('tr:first-child th'))
|
||
const rows = Array.from(table.querySelectorAll('tr'))
|
||
const handle = table.querySelector('.column-resize-handle')
|
||
return {
|
||
// .tiptap 左右各 12px 内边距,减掉才是表格可用的内容宽度
|
||
containerWidth: Math.round(tiptap.getBoundingClientRect().width) - 24,
|
||
tableWidth: Math.round(table.getBoundingClientRect().width),
|
||
contentOverflowsX: content.scrollWidth > content.clientWidth,
|
||
wrapperScrollsX: wrapper.scrollWidth > wrapper.clientWidth,
|
||
columnWidths: headerCells.map((cell) => Math.round(cell.getBoundingClientRect().width)),
|
||
rowHeights: rows.map((row) => Math.round(row.getBoundingClientRect().height)),
|
||
cursor: getComputedStyle(tiptap).cursor,
|
||
handlePosition: handle ? getComputedStyle(handle).position : null,
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 第一列表头右边缘(列分隔线)的视口坐标 */
|
||
function dividerPoint(page: Page) {
|
||
return page.evaluate(() => {
|
||
const cell = document.querySelector('[data-testid="rich-text"] table tr:first-child th')!
|
||
const box = cell.getBoundingClientRect()
|
||
return { x: Math.round(box.right) - 2, y: Math.round(box.y + box.height / 2) }
|
||
})
|
||
}
|
||
|
||
/** 拖动第一列右侧的分隔线 dx 像素 */
|
||
async function dragDivider(page: Page, dx: number): Promise<void> {
|
||
const from = await dividerPoint(page)
|
||
await page.mouse.move(from.x, from.y)
|
||
await page.mouse.down()
|
||
await page.mouse.move(from.x + dx, from.y, { steps: 8 })
|
||
await page.mouse.up()
|
||
await page.waitForTimeout(150)
|
||
}
|
||
|
||
test.describe('RichText 表格', () => {
|
||
test('表格保持满宽,且仅表头行有底色', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
// 等价于 VitePress `.vp-doc` 的宿主全局表格样式
|
||
await page.addStyleTag({
|
||
content: [
|
||
'table { display: block; margin: 20px 0; overflow-x: auto; }',
|
||
'tr { border-top: 1px solid #e2e2e3; background-color: #fff; }',
|
||
'tr:nth-child(2n) { background-color: #f6f6f7; }',
|
||
].join('\n'),
|
||
})
|
||
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const table = editor.locator('table')
|
||
|
||
// 1) 布局:表格铺满容器(display:block 时网格会收缩到内容宽)
|
||
const tableBox = (await table.boundingBox())!
|
||
const wrapperBox = (await table.locator('xpath=..').boundingBox())!
|
||
expect(Math.round(tableBox.width)).toBeCloseTo(Math.round(wrapperBox.width), 0)
|
||
|
||
const headerCells = table.locator('tr').first().locator('th')
|
||
await expect(headerCells).toHaveCount(3)
|
||
const cellWidths = await headerCells.evaluateAll((els) =>
|
||
els.map((el) => el.getBoundingClientRect().width),
|
||
)
|
||
// 三列均分:每列约为 1/3 表宽(收缩布局下会远小于此)
|
||
for (const width of cellWidths) expect(width).toBeGreaterThan(tableBox.width / 3 - 4)
|
||
|
||
// 2) 背景:宿主斑马纹不得染到表体行,只有表头行(th)保留组件自身的底色
|
||
const rowBackgrounds = await table
|
||
.locator('tr')
|
||
.evaluateAll((rows) => rows.map((row) => getComputedStyle(row).backgroundColor))
|
||
expect(rowBackgrounds.slice(1).every((bg) => bg === 'rgba(0, 0, 0, 0)')).toBe(true)
|
||
|
||
const headerBg = await headerCells
|
||
.first()
|
||
.evaluate((el) => getComputedStyle(el).backgroundColor)
|
||
expect(headerBg).not.toBe('rgba(0, 0, 0, 0)')
|
||
})
|
||
|
||
test('空内容时插入的表格保留顶部间距', async ({ page }) => {
|
||
await page.goto('/')
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.scrollIntoViewIfNeeded()
|
||
|
||
// 清空内容,让表格成为首个块
|
||
await editor.locator('.tiptap').click()
|
||
await page.keyboard.press('Control+A')
|
||
await page.keyboard.press('Delete')
|
||
|
||
await insertTable(page)
|
||
|
||
// 表格(无 UA 外边距的块)作为首块时,必须与内容区顶部留出间距,不能贴顶
|
||
const gap = await page.evaluate(() => {
|
||
const root = document.querySelector('[data-testid="rich-text"]')!
|
||
const tiptap = root.querySelector('.tiptap')!
|
||
const table = root.querySelector('table')!
|
||
return Math.round(table.getBoundingClientRect().top - tiptap.getBoundingClientRect().top)
|
||
})
|
||
expect(gap).toBeGreaterThanOrEqual(8)
|
||
})
|
||
|
||
test('单元格单行行高不被段落默认外边距撑高', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
|
||
// 单元格内 <p> 的 UA 上下外边距(各 1em)曾把单行行高从 ~38px 撑到 ~66px
|
||
const { rowHeights } = await tableSnapshot(page)
|
||
for (const height of rowHeights) {
|
||
expect(height).toBeGreaterThan(30)
|
||
expect(height).toBeLessThan(50)
|
||
}
|
||
})
|
||
|
||
test('插入表格面板:网格选择器高亮矩形并实时显示尺寸', async ({ page }) => {
|
||
await page.goto('/')
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.scrollIntoViewIfNeeded()
|
||
await editor.locator('button[aria-label="插入表格"]').click()
|
||
|
||
// 8 行 × 10 列的网格
|
||
const cells = page.locator('.g3-rich-text__table-grid-cell')
|
||
await expect(cells).toHaveCount(80)
|
||
|
||
// 尺寸提示:未悬停时显示默认 3 × 3
|
||
const label = page.locator('.g3-rich-text__table-grid-label')
|
||
await expect(label).toHaveText('3 × 3')
|
||
|
||
// 悬停 4 行 6 列 → 高亮 24 格,提示同步
|
||
await page.locator('.g3-rich-text__table-grid-cell[data-row="4"][data-col="6"]').hover()
|
||
await expect(label).toHaveText('4 × 6')
|
||
await expect(page.locator('.g3-rich-text__table-grid-cell.is-table-grid-cell-active')).toHaveCount(
|
||
24,
|
||
)
|
||
|
||
// 悬停更小的区域 → 高亮随之收缩(不是只增不减)
|
||
await page.locator('.g3-rich-text__table-grid-cell[data-row="2"][data-col="2"]').hover()
|
||
await expect(label).toHaveText('2 × 2')
|
||
await expect(page.locator('.g3-rich-text__table-grid-cell.is-table-grid-cell-active')).toHaveCount(
|
||
4,
|
||
)
|
||
})
|
||
|
||
test('插入表格面板:不溢出容器,含表头行勾选可用', async ({ page }) => {
|
||
await page.goto('/')
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.scrollIntoViewIfNeeded()
|
||
await editor.locator('button[aria-label="插入表格"]').click()
|
||
|
||
const form = page.locator('.g3-rich-text__table-form')
|
||
const grid = page.locator('.g3-rich-text__table-grid')
|
||
const formBox = (await form.boundingBox())!
|
||
const gridBox = (await grid.boundingBox())!
|
||
|
||
// 网格必须落在面板内(早期手写样式曾让内容比容器宽 18px)
|
||
expect(gridBox.width).toBeLessThanOrEqual(formBox.width + 1)
|
||
expect(gridBox.x).toBeGreaterThanOrEqual(formBox.x - 1)
|
||
|
||
// 「包含表头行」默认选中;取消后插入的表首行应为 td 而非 th。
|
||
// 注意:点击**包裹用的 label**,不能对内部 input 调 uncheck() ——
|
||
// G3Checkbox 的原生 input 是 1px + opacity:0 + pointer-events:none(视觉由 span 承担),
|
||
// Playwright 认为它不可见 / 不可交互,uncheck() 会直接超时。
|
||
const toggleInput = page.locator('.g3-rich-text__table-checkbox input')
|
||
const toggleLabel = page.locator('.g3-rich-text__table-header-toggle')
|
||
await expect(toggleInput).toBeChecked()
|
||
await toggleLabel.click()
|
||
await expect(toggleInput).not.toBeChecked()
|
||
|
||
await page.locator('.g3-rich-text__table-grid-cell[data-row="2"][data-col="2"]').click()
|
||
await expect(editor.locator('table tr')).toHaveCount(2)
|
||
await expect(editor.locator('table')).toHaveCount(1)
|
||
await expect(editor.locator('table tr:first-child th')).toHaveCount(0)
|
||
})
|
||
})
|
||
|
||
test.describe('RichText 表格 · 列宽拖拽', () => {
|
||
// 用窄视口,更容易触及列宽的上下限
|
||
test.use({ viewport: { width: 720, height: 900 } })
|
||
|
||
test('悬停分隔线:光标为 col-resize,且行高不跳动', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
|
||
const before = await tableSnapshot(page)
|
||
expect(before.handlePosition).toBeNull()
|
||
|
||
const point = await dividerPoint(page)
|
||
await page.mouse.move(point.x, point.y)
|
||
await page.waitForTimeout(200)
|
||
|
||
const hover = await tableSnapshot(page)
|
||
expect(hover.cursor).toBe('col-resize')
|
||
// 拖拽手柄必须脱离文档流 —— 回归:它曾作为流内块把行高从 38px 撑到 45px
|
||
expect(hover.handlePosition).toBe('absolute')
|
||
expect(hover.rowHeights).toEqual(before.rowHeights)
|
||
|
||
// 手柄只在表头行显示 —— 回归:曾在每一行都画一段,看起来是一列断开的虚线
|
||
const handles = await page.evaluate(() => {
|
||
const all = Array.from(
|
||
document.querySelectorAll('[data-testid="rich-text"] table .column-resize-handle'),
|
||
)
|
||
return {
|
||
total: all.length,
|
||
visible: all.filter((el) => getComputedStyle(el).display !== 'none').length,
|
||
}
|
||
})
|
||
expect(handles.total).toBeGreaterThan(1)
|
||
expect(handles.visible).toBe(1)
|
||
})
|
||
|
||
test('拖拽列宽精确跟手,且溢出只发生在表格容器层', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
|
||
const before = await tableSnapshot(page)
|
||
await dragDivider(page, 150)
|
||
const after = await tableSnapshot(page)
|
||
|
||
// 拖宽第一列 150px:第一列精确变宽,其余列纹丝不动
|
||
expect(after.columnWidths[0]).toBeCloseTo(before.columnWidths[0] + 150, -1)
|
||
expect(after.columnWidths[1]).toBeCloseTo(before.columnWidths[1], -1)
|
||
expect(after.columnWidths[2]).toBeCloseTo(before.columnWidths[2], -1)
|
||
// 表格总宽超出容器时,横向滚动只发生在 .tableWrapper 层,不进编辑器内容区
|
||
expect(after.contentOverflowsX).toBe(false)
|
||
expect(after.wrapperScrollsX).toBe(true)
|
||
})
|
||
|
||
test('列宽被夹在最小宽度内,相邻列不会被挤没', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
|
||
// 向右拖到远超容器:被拖列变宽、其余列保持原宽,不被挤到看不见
|
||
await dragDivider(page, 430)
|
||
const wide = await tableSnapshot(page)
|
||
expect(wide.contentOverflowsX).toBe(false)
|
||
expect(wide.wrapperScrollsX).toBe(true)
|
||
// 其余列纹丝不动(既不被挤没,也不被拉伸)
|
||
expect(wide.columnWidths.slice(1).every((width) => width >= 9)).toBe(true)
|
||
|
||
// 再向左拖到极窄:应停在最小列宽,而不是细到看不见
|
||
await dragDivider(page, -400)
|
||
const narrow = await tableSnapshot(page)
|
||
expect(narrow.columnWidths[0]).toBeGreaterThanOrEqual(9)
|
||
})
|
||
|
||
test('拖拽后的列宽不会被随后输入的内容重排', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
await dragDivider(page, 150)
|
||
const dragged = await tableSnapshot(page)
|
||
|
||
// 在相邻单元格塞一个超长不可断词:fixed 布局下内容不会重算列宽
|
||
await page.locator('[data-testid="rich-text"] table tr:first-child th').nth(1).click()
|
||
await page.keyboard.type('A'.repeat(40))
|
||
await page.waitForTimeout(200)
|
||
|
||
const after = await tableSnapshot(page)
|
||
expect(after.columnWidths[0]).toBeCloseTo(dragged.columnWidths[0], -1)
|
||
expect(after.contentOverflowsX).toBe(false)
|
||
})
|
||
|
||
test('列宽总和超过容器时,横向滚动只发生在表格容器内,不进入编辑器内容区', async ({ page }) => {
|
||
await page.goto('/')
|
||
// 用较窄的视口 + 拖宽第一列,制造「总列宽 > 容器」的溢出场景
|
||
await insertTable(page)
|
||
|
||
// 拖宽第一列,让表格总宽超过容器
|
||
await dragDivider(page, 500)
|
||
const snapshot = await tableSnapshot(page)
|
||
expect(snapshot.contentOverflowsX).toBe(false)
|
||
expect(snapshot.wrapperScrollsX).toBe(true)
|
||
})
|
||
})
|
||
|
||
test.describe('RichText 表格 · 操作浮层', () => {
|
||
test.use({ viewport: { width: 900, height: 900 } })
|
||
|
||
async function openMenuInCell(page: Page, rowIndex: number): Promise<void> {
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.locator('table tr').nth(rowIndex).locator('td, th').first().click()
|
||
await expect(page.locator('.g3-rich-text-table-menu')).toBeVisible()
|
||
}
|
||
|
||
test('光标在表格内出现浮层,离开后隐藏', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const menu = page.locator('.g3-rich-text-table-menu')
|
||
|
||
// 初始(表格外 / 文档其它位置):不显示
|
||
await editor.locator('.tiptap').click({ position: { x: 10, y: 10 } })
|
||
await expect(menu).toBeHidden()
|
||
|
||
await openMenuInCell(page, 1)
|
||
// 点击编辑器内容区空白处,光标离开表格 → 浮层隐藏
|
||
await editor.locator('.tiptap').click({ position: { x: 10, y: 10 } })
|
||
await expect(menu).toBeHidden()
|
||
})
|
||
|
||
test('浮层操作:上方插入行 / 左侧插入列 / 删除表格', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const menu = page.locator('.g3-rich-text-table-menu')
|
||
|
||
await openMenuInCell(page, 1)
|
||
await menu.getByRole('button', { name: '上方插入行' }).click()
|
||
await expect(editor.locator('table tr')).toHaveCount(4)
|
||
|
||
await menu.getByRole('button', { name: '左侧插入列' }).click()
|
||
await expect(editor.locator('table tr:first-child th')).toHaveCount(4)
|
||
|
||
await menu.getByRole('button', { name: '删除表格' }).click()
|
||
await expect(editor.locator('table')).toHaveCount(0)
|
||
await expect(menu).toBeHidden()
|
||
})
|
||
|
||
test('合并单元格后置灰关系:合并 / 拆分各自独立', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const menu = page.locator('.g3-rich-text-table-menu')
|
||
const merge = () => menu.getByRole('button', { name: '合并单元格' })
|
||
const split = () => menu.getByRole('button', { name: '拆分单元格' })
|
||
|
||
// 单格选区:没有相邻格可合并、自身也没合并过 → 两个按钮都不可用
|
||
await openMenuInCell(page, 1)
|
||
await expect(merge()).toBeDisabled()
|
||
await expect(split()).toBeDisabled()
|
||
|
||
// 拖过同一行相邻两格(prosemirror-tables 在 mousemove 进另一格时建立 CellSelection)
|
||
const cells = editor.locator('table tr').nth(1).locator('td')
|
||
const first = (await cells.nth(0).boundingBox())!
|
||
const second = (await cells.nth(1).boundingBox())!
|
||
await page.mouse.move(first.x + 10, first.y + first.height / 2)
|
||
await page.mouse.down()
|
||
await page.mouse.move(second.x + second.width / 2, second.y + second.height / 2, { steps: 6 })
|
||
await page.mouse.up()
|
||
await expect(menu).toBeVisible()
|
||
|
||
await expect(merge()).toBeEnabled()
|
||
await expect(split()).toBeDisabled()
|
||
await merge().click()
|
||
await expect(editor.locator('table tr').nth(1).locator('td[colspan="2"]')).toHaveCount(1)
|
||
|
||
// 合并后选区落在跨列格里:拆分为可用、合并不可用
|
||
await expect(split()).toBeEnabled()
|
||
await expect(merge()).toBeDisabled()
|
||
await split().click()
|
||
await expect(editor.locator('table tr').nth(1).locator('td')).toHaveCount(3)
|
||
})
|
||
|
||
test('删除行 / 删除列与增删行列分组同排', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const menu = page.locator('.g3-rich-text-table-menu')
|
||
|
||
await openMenuInCell(page, 1)
|
||
|
||
// 操作栏里已不再出现单元格对齐按钮(对齐统一走顶部工具条)
|
||
await expect(menu.getByRole('button', { name: '单元格居中' })).toHaveCount(0)
|
||
await expect(menu.getByRole('button', { name: '取消单元格对齐' })).toHaveCount(0)
|
||
// 「表头」整组已移除:切换表头列 / 切换表头单元格都不再提供
|
||
await expect(menu.getByRole('button', { name: '切换表头行' })).toHaveCount(0)
|
||
await expect(menu.getByRole('button', { name: '切换表头列' })).toHaveCount(0)
|
||
await expect(menu.getByRole('button', { name: '切换表头单元格' })).toHaveCount(0)
|
||
|
||
// 删除行 / 删除列的图标是自绘 SVG(不再是拆分行列的 lucide 图标)
|
||
for (const name of ['删除行', '删除列']) {
|
||
const icon = menu.getByRole('button', { name })
|
||
await expect(icon).toBeVisible()
|
||
await expect(icon.locator('svg')).toHaveCount(1)
|
||
await expect(icon.locator('svg path')).toHaveCount(3)
|
||
}
|
||
|
||
await menu.getByRole('button', { name: '删除行' }).click()
|
||
await expect(editor.locator('table tr')).toHaveCount(2)
|
||
await menu.getByRole('button', { name: '删除列' }).click()
|
||
await expect(editor.locator('table tr:first-child th')).toHaveCount(2)
|
||
})
|
||
|
||
test('单元格内文字的对齐走顶部工具条', async ({ page }) => {
|
||
await page.goto('/')
|
||
await insertTable(page)
|
||
const editor = page.getByTestId('rich-text')
|
||
const cell = editor.locator('table tr').nth(1).locator('td').first()
|
||
|
||
await openMenuInCell(page, 1)
|
||
await cell.click()
|
||
await editor.locator('.g3-rich-text__toolbar').getByRole('button', { name: '居中' }).click()
|
||
|
||
// 单元格内容是 paragraph,对齐写在这层(不再是单元格自身的 align 属性)
|
||
await expect(cell.locator('p')).toHaveAttribute('style', /text-align:\s*center/)
|
||
})
|
||
|
||
/**
|
||
* 「超界被夹回」这条用例已随数字输入框一起移除:网格选择器的可选范围由网格本身
|
||
* 硬性限定(8 行 × 10 列),用户在结构上就填不出 0 行或 99 列 —— 也就没有「夹回」可言。
|
||
* 这里改为断言这个上限本身,防止有人无意中把网格尺寸改大或改小。
|
||
*/
|
||
test('网格选择器的上限为 8 行 × 10 列,且只能选到边界内的格子', async ({ page }) => {
|
||
await page.goto('/')
|
||
const editor = page.getByTestId('rich-text')
|
||
await editor.scrollIntoViewIfNeeded()
|
||
await editor.locator('button[aria-label="插入表格"]').click()
|
||
|
||
const cells = page.locator('.g3-rich-text__table-grid-cell')
|
||
await expect(cells).toHaveCount(80)
|
||
// 边界格子存在,越界的不存在
|
||
await expect(page.locator('.g3-rich-text__table-grid-cell[data-row="8"][data-col="10"]')).toHaveCount(1)
|
||
await expect(page.locator('.g3-rich-text__table-grid-cell[data-row="9"]')).toHaveCount(0)
|
||
await expect(page.locator('.g3-rich-text__table-grid-cell[data-col="11"]')).toHaveCount(0)
|
||
|
||
// 选边界值:8 行 10 列能正常插入
|
||
await page.locator('.g3-rich-text__table-grid-cell[data-row="8"][data-col="10"]').click()
|
||
const table = editor.locator('table')
|
||
await expect(table).toHaveCount(1)
|
||
await expect(table.locator('tr')).toHaveCount(8)
|
||
await expect(table.locator('tr:first-child th')).toHaveCount(10)
|
||
})
|
||
})
|