u
This commit is contained in:
1 parent
f572dce2f6
commit
e99a9fb274
356 files changed
+28877
-1055
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 数据展示类组件的交互回归。
|
||||
*
|
||||
* 注意:用 page.mouse.* 之前必须先 scrollIntoViewIfNeeded ——
|
||||
* mouse 事件按视口坐标派发,元素在视口外时事件会落到别的元素上
|
||||
* (表现为「拖拽/点击完全没反应」,很容易被误判成组件 bug)。
|
||||
*/
|
||||
|
||||
test.describe('Tabs', () => {
|
||||
test('面板懒渲染,切换后保留旧面板(表单状态不丢)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const tabs = page.getByTestId('tabs')
|
||||
|
||||
await expect(tabs.locator('.g3-tabs__pane')).toHaveCount(1)
|
||||
await tabs.locator('.g3-tabs__tab').nth(1).click()
|
||||
await expect(tabs.locator('.g3-tabs__pane')).toHaveCount(2)
|
||||
await expect(tabs.locator('.g3-tabs__pane').first()).toBeHidden()
|
||||
})
|
||||
|
||||
test('禁用标签不可点击(回归:空值布尔属性被 Boolean("") 判成 false)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const tabs = page.getByTestId('tabs')
|
||||
const disabled = tabs.locator('.g3-tabs__tab').nth(2)
|
||||
|
||||
await expect(disabled).toBeDisabled()
|
||||
await expect(disabled).toHaveAttribute('aria-disabled', 'true')
|
||||
await expect(tabs.locator('.g3-tabs__pane')).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Tree', () => {
|
||||
test('勾选父节点后子节点全选(父子联动)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const tree = page.getByTestId('tree')
|
||||
const checkboxes = tree.locator('.g3-tree-node__checkbox .g3-checkbox')
|
||||
|
||||
await expect(checkboxes).toHaveCount(6)
|
||||
await checkboxes.first().click()
|
||||
|
||||
const checkedCount = await tree.locator('.g3-tree-node__checkbox .g3-checkbox.is-checked').count()
|
||||
// 「前端」及其 3 个子孙(Vue / 组合式 API / React)
|
||||
expect(checkedCount).toBe(4)
|
||||
})
|
||||
|
||||
test('收起父节点后其子孙不再渲染(后端仍展开,所以剩下 3 个)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const tree = page.getByTestId('tree')
|
||||
|
||||
await expect(tree.locator('.g3-tree-node')).toHaveCount(6)
|
||||
await tree.locator('.g3-tree-node__switcher').first().click()
|
||||
await expect(tree.locator('.g3-tree-node')).toHaveCount(3)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Pagination', () => {
|
||||
test('省略号点击跳 5 页', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const pagination = page.getByTestId('pagination')
|
||||
|
||||
await expect(pagination.locator('[aria-current="page"]')).toHaveText('1')
|
||||
await pagination.locator('.g3-pagination__ellipsis').click()
|
||||
await expect(pagination.locator('[aria-current="page"]')).toHaveText('6')
|
||||
})
|
||||
|
||||
test('显示总数与跳页控件', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const pagination = page.getByTestId('pagination')
|
||||
|
||||
await expect(pagination.locator('.g3-pagination__total')).toContainText('126')
|
||||
await expect(pagination.locator('.g3-pagination__jumper')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Splitter', () => {
|
||||
test('拖拽分隔条同时改变分隔条位置与两侧尺寸', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const splitter = page.getByTestId('splitter')
|
||||
await splitter.scrollIntoViewIfNeeded()
|
||||
|
||||
const panels = splitter.locator('.g3-splitter-panel')
|
||||
const bar = splitter.locator('.g3-splitter__bar')
|
||||
|
||||
// 探针:区分「pointerdown 没落到分隔条上」与「落上了但逻辑没生效」
|
||||
await page.evaluate(() => {
|
||||
;(window as unknown as { __barDown: number }).__barDown = 0
|
||||
document.querySelector('.g3-splitter__bar')?.addEventListener('pointerdown', () => {
|
||||
;(window as unknown as { __barDown: number }).__barDown += 1
|
||||
})
|
||||
})
|
||||
|
||||
const before = (await panels.first().boundingBox())!.width
|
||||
const box = (await bar.boundingBox())!
|
||||
const centerX = box.x + box.width / 2
|
||||
const centerY = box.y + box.height / 2
|
||||
|
||||
await page.mouse.move(centerX, centerY)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(centerX + 40, centerY, { steps: 3 })
|
||||
await page.mouse.move(centerX + 80, centerY, { steps: 3 })
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
const barDown = await page.evaluate(() => (window as unknown as { __barDown: number }).__barDown)
|
||||
expect(barDown).toBe(1)
|
||||
expect((await panels.first().boundingBox())!.width).toBeGreaterThan(before + 20)
|
||||
})
|
||||
|
||||
test('分隔条可键盘调整(方向键 ±10px)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const splitter = page.getByTestId('splitter')
|
||||
await splitter.scrollIntoViewIfNeeded()
|
||||
|
||||
const panels = splitter.locator('.g3-splitter-panel')
|
||||
const before = (await panels.first().boundingBox())!.width
|
||||
|
||||
await splitter.locator('.g3-splitter__bar').focus()
|
||||
await page.keyboard.press('ArrowRight')
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
expect((await panels.first().boundingBox())!.width).toBeGreaterThan(before + 10)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Scrollbar', () => {
|
||||
test('点击轨道跳转滚动(clickScroll 默认开启)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const scrollbar = page.getByTestId('scrollbar')
|
||||
await scrollbar.scrollIntoViewIfNeeded()
|
||||
|
||||
const view = scrollbar.locator('.g3-scrollbar__view')
|
||||
expect(await view.evaluate((el) => el.scrollTop)).toBe(0)
|
||||
|
||||
const track = (await scrollbar.locator('.g3-scrollbar__track-v').boundingBox())!
|
||||
await page.mouse.click(track.x + track.width / 2, track.y + track.height * 0.7)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
expect(await view.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('拖动滑块同步滚动位置', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const scrollbar = page.getByTestId('scrollbar')
|
||||
await scrollbar.scrollIntoViewIfNeeded()
|
||||
|
||||
const view = scrollbar.locator('.g3-scrollbar__view')
|
||||
const box = (await scrollbar.locator('.g3-scrollbar__thumb-v').boundingBox())!
|
||||
const centerX = box.x + box.width / 2
|
||||
const centerY = box.y + box.height / 2
|
||||
|
||||
await page.mouse.move(centerX, centerY)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(centerX, centerY + 60, { steps: 5 })
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
expect(await view.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Segmented', () => {
|
||||
test('点击选项切换选中态(滑块跟随)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const segmented = page.getByTestId('segmented')
|
||||
|
||||
await expect(segmented.locator('.g3-segmented__item.is-selected')).toHaveText('日')
|
||||
await segmented.locator('.g3-segmented__item').filter({ hasText: '周' }).click()
|
||||
await expect(segmented.locator('.g3-segmented__item.is-selected')).toHaveText('周')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Drawer 四个方向的几何回归。
|
||||
*
|
||||
* 漏测教训:之前只验了默认的 `right`,导致「上下方向的抽屉宽度不是 100%」一直没被发现。
|
||||
* 根因是容器用 flex 的 justify-content / align-items 做吸附 —— 两条轴各管一个方向,
|
||||
* 上下面板在主轴(水平)方向没有约束,宽度缩到了内容宽。
|
||||
* 现在改为按方向定边(上下 `left/right: 0`、左右 `top/bottom: 0`),与 fms 一致。
|
||||
*
|
||||
* 断言以 `.g3-drawer__wrap`(定位容器)为参照,而不是写死视口尺寸:
|
||||
* 视口高度受 `devices['Desktop Chrome']` 与滚动条影响,写死会得到假失败。
|
||||
* 这类断言必须放 E2E:真实布局结果在 happy-dom 里量不出来。
|
||||
*/
|
||||
|
||||
/** 打开指定方向的抽屉,返回面板与定位容器的几何 */
|
||||
async function openDrawer(page: import('@playwright/test').Page, testId: string) {
|
||||
await page.getByTestId(testId).click()
|
||||
const drawer = page.locator('.g3-drawer')
|
||||
await expect(drawer).toBeVisible()
|
||||
// 等入场动画结束(transform 归零),否则量到的是滑入途中的位置
|
||||
await page.waitForTimeout(450)
|
||||
|
||||
const panel = (await drawer.boundingBox())!
|
||||
const wrap = (await page.locator('.g3-drawer__wrap').boundingBox())!
|
||||
return { drawer, panel, wrap }
|
||||
}
|
||||
|
||||
test.describe('Drawer 方向与尺寸', () => {
|
||||
test('右侧:贴右边缘、纵向撑满,宽度取 width', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const { panel, wrap } = await openDrawer(page, 'open-drawer')
|
||||
|
||||
expect(Math.round(panel.x + panel.width)).toBeCloseTo(Math.round(wrap.x + wrap.width), 0)
|
||||
expect(Math.round(panel.height)).toBeCloseTo(Math.round(wrap.height), 0)
|
||||
expect(Math.round(panel.width)).toBe(378)
|
||||
})
|
||||
|
||||
test('左侧:贴左边缘、纵向撑满', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const { panel, wrap } = await openDrawer(page, 'open-drawer-left')
|
||||
|
||||
expect(Math.round(panel.x)).toBeCloseTo(Math.round(wrap.x), 0)
|
||||
expect(Math.round(panel.height)).toBeCloseTo(Math.round(wrap.height), 0)
|
||||
expect(Math.round(panel.width)).toBe(378)
|
||||
})
|
||||
|
||||
test('上方:贴顶、横向撑满(回归:宽度曾是内容宽)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const { panel, wrap } = await openDrawer(page, 'open-drawer-top')
|
||||
|
||||
expect(Math.round(panel.y)).toBeCloseTo(Math.round(wrap.y), 0)
|
||||
expect(Math.round(panel.width)).toBeCloseTo(Math.round(wrap.width), 0)
|
||||
expect(Math.round(panel.height)).toBe(378)
|
||||
})
|
||||
|
||||
test('下方:贴底、横向撑满(回归:宽度曾是内容宽)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const { panel, wrap } = await openDrawer(page, 'open-drawer-bottom')
|
||||
|
||||
expect(Math.round(panel.y + panel.height)).toBeCloseTo(Math.round(wrap.y + wrap.height), 0)
|
||||
expect(Math.round(panel.width)).toBeCloseTo(Math.round(wrap.width), 0)
|
||||
expect(Math.round(panel.height)).toBe(378)
|
||||
})
|
||||
|
||||
test('四个方向都能撑满对应轴,且面板不被自身遮罩压住', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
for (const [testId, name, axis] of [
|
||||
['open-drawer', '右', 'height'],
|
||||
['open-drawer-left', '左', 'height'],
|
||||
['open-drawer-top', '上', 'width'],
|
||||
['open-drawer-bottom', '下', 'width'],
|
||||
] as const) {
|
||||
const { drawer, panel, wrap } = await openDrawer(page, testId)
|
||||
|
||||
if (axis === 'height') {
|
||||
expect(Math.round(panel.height), `${name}侧抽屉未撑满高度`).toBeCloseTo(
|
||||
Math.round(wrap.height),
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
expect(Math.round(panel.width), `${name}侧抽屉未撑满宽度`).toBeCloseTo(
|
||||
Math.round(wrap.width),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
const hit = await page.evaluate(
|
||||
([x, y]) => {
|
||||
const el = document.elementFromPoint(x as number, y as number)
|
||||
return el ? String(el.className || el.tagName) : null
|
||||
},
|
||||
[panel.x + panel.width / 2, panel.y + Math.min(20, panel.height / 2)],
|
||||
)
|
||||
expect(hit, `${name}侧抽屉面板被遮挡`).toContain('g3-drawer')
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(drawer).toBeHidden()
|
||||
}
|
||||
})
|
||||
|
||||
test('面板尺寸由组件自己决定,不受宿主页面全局样式污染', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// 模拟用户页面的「全局元素选择器」:面板本体是 <section>,header / footer 同理
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
section { margin: 16px; padding: 24px; border: 2px solid red; }
|
||||
header { margin: 12px; padding: 20px; }
|
||||
footer { margin: 12px; padding: 20px; }
|
||||
`,
|
||||
})
|
||||
|
||||
const { panel, wrap } = await openDrawer(page, 'open-drawer')
|
||||
|
||||
const styles = await page.$eval('.g3-drawer', (el) => {
|
||||
const cs = getComputedStyle(el)
|
||||
return {
|
||||
margin: [cs.marginTop, cs.marginRight, cs.marginBottom, cs.marginLeft],
|
||||
padding: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft],
|
||||
borderWidth: cs.borderTopWidth,
|
||||
}
|
||||
})
|
||||
|
||||
expect(styles.margin).toEqual(['0px', '0px', '0px', '0px'])
|
||||
expect(styles.padding).toEqual(['0px', '0px', '0px', '0px'])
|
||||
expect(styles.borderWidth).toBe('0px')
|
||||
// 仍然撑满:宿主 margin 不该从高度里扣掉 16px
|
||||
expect(Math.round(panel.height)).toBeCloseTo(Math.round(wrap.height), 0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 反馈类组件回归。
|
||||
*
|
||||
* 曾经踩过的坑:
|
||||
* - Message 的方位类名手写成 `g3-message-list--*`(样式表是 `g3-message__list--*`),
|
||||
* 9 个方位全部失效、消息一律堆在左上角;
|
||||
* - Modal 面板不是弹性布局,缩放后内容溢出面板而不是内部滚动。
|
||||
*/
|
||||
|
||||
test.describe('Message 方位', () => {
|
||||
test('默认方位是顶部居中', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('toast-default').click()
|
||||
|
||||
const message = page.locator('.g3-message').first()
|
||||
await expect(message).toBeVisible()
|
||||
|
||||
const messageBox = (await message.boundingBox())!
|
||||
const viewport = page.viewportSize()!
|
||||
const center = messageBox.x + messageBox.width / 2
|
||||
|
||||
expect(Math.abs(center - viewport.width / 2)).toBeLessThan(2)
|
||||
expect(messageBox.y).toBeLessThan(viewport.height * 0.25)
|
||||
})
|
||||
|
||||
test('指定方位生效:左上 / 右下', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const viewport = page.viewportSize()!
|
||||
|
||||
await page.getByTestId('toast-top-left').click()
|
||||
const topLeft = (await page.locator('.g3-message').first().boundingBox())!
|
||||
expect(topLeft.x).toBeLessThan(48)
|
||||
expect(topLeft.y).toBeLessThan(viewport.height * 0.25)
|
||||
|
||||
await page.waitForTimeout(3200)
|
||||
|
||||
await page.getByTestId('toast-bottom-right').click()
|
||||
const bottomRight = (await page.locator('.g3-message').first().boundingBox())!
|
||||
expect(bottomRight.x + bottomRight.width).toBeGreaterThan(viewport.width - 48)
|
||||
expect(bottomRight.y).toBeGreaterThan(viewport.height * 0.6)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Notification', () => {
|
||||
test('默认出现在右上角', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const viewport = page.viewportSize()!
|
||||
await page.getByTestId('notify').click()
|
||||
|
||||
const notification = page.locator('.g3-notification').first()
|
||||
await expect(notification).toBeVisible()
|
||||
|
||||
const box = (await notification.boundingBox())!
|
||||
expect(box.x + box.width).toBeGreaterThan(viewport.width - 48)
|
||||
expect(box.y).toBeLessThan(viewport.height * 0.3)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Modal', () => {
|
||||
test('缩放变矮后内容区内部滚动,而不是溢出面板', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-modal').click()
|
||||
|
||||
const modal = page.locator('.g3-modal')
|
||||
await expect(modal).toBeVisible()
|
||||
|
||||
// 把南侧手柄往上拖,压缩面板高度
|
||||
const handle = (await page.locator('.g3-modal__resizer-s').boundingBox())!
|
||||
const startX = handle.x + handle.width / 2
|
||||
await page.mouse.move(startX, handle.y + handle.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(startX, handle.y - 400, { steps: 8 })
|
||||
await page.mouse.up()
|
||||
|
||||
const panel = (await modal.boundingBox())!
|
||||
const body = await page.evaluate(() => {
|
||||
const el = document.querySelector('.g3-modal__body') as HTMLElement
|
||||
return { client: el.clientHeight, scroll: el.scrollHeight }
|
||||
})
|
||||
|
||||
// 面板被压矮,内容比可视区高 → 必须内部滚动
|
||||
expect(panel.height).toBeLessThan(700)
|
||||
expect(body.scroll).toBeGreaterThan(body.client)
|
||||
})
|
||||
|
||||
test('Esc 关闭,遮罩点击可关闭', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-modal').click()
|
||||
await expect(page.locator('.g3-modal')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(page.locator('.g3-modal')).toBeHidden()
|
||||
|
||||
await page.getByTestId('open-modal').click()
|
||||
await page.mouse.click(60, 400)
|
||||
await expect(page.locator('.g3-modal')).toBeHidden()
|
||||
})
|
||||
|
||||
test('焦点被限制在面板内(Tab 循环)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-modal').click()
|
||||
await expect(page.locator('.g3-modal')).toBeVisible()
|
||||
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
await page.keyboard.press('Tab')
|
||||
}
|
||||
|
||||
const inside = await page.evaluate(() => {
|
||||
const panel = document.querySelector('.g3-modal')
|
||||
return panel ? panel.contains(document.activeElement) : false
|
||||
})
|
||||
expect(inside).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Modal.confirm(命令式)', () => {
|
||||
test('点确定后按钮进入 loading,完成后自动关闭', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-confirm').click()
|
||||
|
||||
const modal = page.locator('.g3-modal')
|
||||
await expect(modal).toBeVisible()
|
||||
await expect(modal.locator('.g3-modal__title')).toHaveText('删除确认')
|
||||
|
||||
const confirmButton = modal.locator('.g3-modal__footer button').last()
|
||||
await expect(confirmButton).toHaveText('确定')
|
||||
await confirmButton.click()
|
||||
|
||||
// loading 期间按钮禁用且带 aria-busy
|
||||
await expect(confirmButton).toBeDisabled()
|
||||
await expect(confirmButton).toHaveAttribute('aria-busy', 'true')
|
||||
|
||||
// onConfirm 的 Promise resolve 后自动关闭
|
||||
await expect(modal).toBeHidden({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,113 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 表单类组件的真实布局回归。
|
||||
*
|
||||
* 曾经踩过的坑:
|
||||
* - textarea 的 footer(计数 + 拖拽手柄)在文档流里,被挤到框外;
|
||||
* - 日期时间面板上下排列而不是「左日期 / 右时间」;
|
||||
* - 日期面板宽度跟随输入框被拉伸,日历格子间距被撑散;
|
||||
* - 非法日期(2026-02-30)被原生解析静默顺延成 3 月 2 日。
|
||||
*/
|
||||
|
||||
test.describe('Input / textarea', () => {
|
||||
test('拖拽手柄落在输入框内部右下角(不再被挤到框外)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="textarea-count"]')
|
||||
const box = (await root.boundingBox())!
|
||||
const handle = (await root.locator('.g3-input__resize-handle').boundingBox())!
|
||||
|
||||
expect(handle.x + handle.width).toBeLessThanOrEqual(box.x + box.width + 1)
|
||||
expect(handle.y + handle.height).toBeLessThanOrEqual(box.y + box.height + 1)
|
||||
expect(handle.y).toBeGreaterThan(box.y + box.height / 2)
|
||||
})
|
||||
|
||||
test('字数统计与手柄同处一行,且有可访问语义', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="textarea-count"]')
|
||||
|
||||
await expect(root.locator('.g3-input__count')).toHaveText(/\d+ \/ 100/)
|
||||
await expect(root.locator('.g3-input__resize-handle')).toHaveAttribute('role', 'slider')
|
||||
await expect(root.locator('.g3-input__resize-handle')).toHaveAttribute('aria-orientation', 'vertical')
|
||||
})
|
||||
|
||||
test('number 输入实时净化,只保留一个小数点', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const input = page.locator('[data-testid="input-number"] input')
|
||||
|
||||
await input.fill('12.3.4')
|
||||
await expect(input).toHaveValue('12.34')
|
||||
})
|
||||
|
||||
test('password 可切换明文', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="input-password"]')
|
||||
|
||||
await expect(root.locator('input')).toHaveAttribute('type', 'password')
|
||||
await root.locator('.g3-input__password-toggle').click()
|
||||
await expect(root.locator('input')).toHaveAttribute('type', 'text')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DatePicker', () => {
|
||||
test('日期时间面板呈「左日历 / 右时间」,且日历定宽不被拉伸', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const trigger = page.locator('[data-testid="date-picker"] .g3-date-picker__trigger')
|
||||
const triggerWidth = (await trigger.boundingBox())!.width
|
||||
|
||||
await trigger.click()
|
||||
const panel = page.locator('.g3-date-picker__panel')
|
||||
await expect(panel).toBeVisible()
|
||||
|
||||
const calendar = panel.locator('.g3-calendar__main')
|
||||
const time = panel.locator('.g3-calendar__time')
|
||||
await expect(calendar).toBeVisible()
|
||||
await expect(time).toBeVisible()
|
||||
|
||||
const calendarBox = (await calendar.boundingBox())!
|
||||
const timeBox = (await time.boundingBox())!
|
||||
|
||||
// 日历在左、时间列在右,同一行(纵向重叠)
|
||||
expect(timeBox.x).toBeGreaterThan(calendarBox.x + calendarBox.width - 1)
|
||||
expect(timeBox.y).toBeLessThan(calendarBox.y + calendarBox.height)
|
||||
|
||||
// 日历定宽,面板不跟随输入框拉宽
|
||||
expect(calendarBox.width).toBeLessThan(260)
|
||||
const panelBox = (await panel.boundingBox())!
|
||||
expect(panelBox.width).toBeLessThan(triggerWidth)
|
||||
})
|
||||
|
||||
test('面板内的确定按钮只有点选后才能用', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.locator('[data-testid="date-picker"] .g3-date-picker__trigger').click()
|
||||
|
||||
const confirm = page.locator('.g3-calendar__footer button')
|
||||
await expect(confirm).toBeEnabled()
|
||||
})
|
||||
|
||||
test('非法日期不会被接受(不再静默顺延)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const input = page.locator('[data-testid="date-picker"] .g3-date-picker__input')
|
||||
|
||||
await input.click()
|
||||
await input.fill('2026-02-30')
|
||||
await input.press('Enter')
|
||||
|
||||
// 回退到原值,不能变成 2026-03-02
|
||||
await expect(input).not.toHaveValue('2026-03-02')
|
||||
await expect(page.locator('[data-testid="date-picker"]')).toHaveClass(/is-invalid/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('RangePicker', () => {
|
||||
test('区间面板打开且两端都有值', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="range-picker"]')
|
||||
|
||||
await expect(root.locator('input').first()).toHaveValue('2026-10-01')
|
||||
await expect(root.locator('input').nth(1)).toHaveValue('2026-10-08')
|
||||
|
||||
await root.locator('.g3-range-picker__trigger').click()
|
||||
await expect(page.locator('.g3-range-picker__panel')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 弹层层级回归 —— 这类问题单测覆盖不到(需要真实层叠上下文与命中测试)。
|
||||
*
|
||||
* 曾经踩过的坑:
|
||||
* 1. 档位顺序写反(popup < modal)→ 弹窗/抽屉内的下拉被遮罩盖住;
|
||||
* 2. 受控打开 / 挂载即展开的浮层根本没分配 z-index;
|
||||
* 3. Drawer 的 wrap 是 fixed 但没 z-index → 整个抽屉被自己的遮罩压住。
|
||||
*
|
||||
* 注意:playground 里有两个「挂载即展开」的常驻 Popover,
|
||||
* 所以定位浮层时要用 :has-text / filter 精确命中,不能直接 `.g3-popover__popup`。
|
||||
*/
|
||||
|
||||
/** 读取视口内某点最上层的元素类名(判断「谁盖住谁」的唯一可靠方式) */
|
||||
async function topElementAt(page: import('@playwright/test').Page, x: number, y: number) {
|
||||
return page.evaluate(
|
||||
([px, py]) => {
|
||||
const el = document.elementFromPoint(px as number, py as number)
|
||||
if (!el) return null
|
||||
return typeof el.className === 'string' ? el.className : el.tagName
|
||||
},
|
||||
[x, y],
|
||||
)
|
||||
}
|
||||
|
||||
test.describe('浮层层级', () => {
|
||||
test('挂载即展开的浮层同样有层级,且同档按打开顺序递增', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
const popupA = page.locator('.g3-popover__popup', { hasText: '浮层 A' })
|
||||
const popupB = page.locator('.g3-popover__popup', { hasText: '浮层 B' })
|
||||
await expect(popupA).toBeVisible()
|
||||
await expect(popupB).toBeVisible()
|
||||
|
||||
const zA = Number(await popupA.evaluate((el) => (el as HTMLElement).style.zIndex))
|
||||
const zB = Number(await popupB.evaluate((el) => (el as HTMLElement).style.zIndex))
|
||||
|
||||
expect(zA).toBeGreaterThan(3000)
|
||||
expect(zB).toBeGreaterThan(zA)
|
||||
})
|
||||
|
||||
test('抽屉面板不会被自身遮罩盖住,点遮罩仍可关闭', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-drawer').click()
|
||||
|
||||
const drawer = page.locator('.g3-drawer')
|
||||
await expect(drawer).toBeVisible()
|
||||
// 等入场动画结束(transform 归零)再做命中测试,否则面板还在视口外
|
||||
await page.waitForTimeout(450)
|
||||
|
||||
const panelBox = (await drawer.boundingBox())!
|
||||
const inside = await topElementAt(
|
||||
page,
|
||||
panelBox.x + panelBox.width / 2,
|
||||
panelBox.y + panelBox.height / 2,
|
||||
)
|
||||
expect(inside).not.toBeNull()
|
||||
expect(inside).toContain('g3-drawer')
|
||||
|
||||
// 遮罩与 wrap 同级(同一实例共用层级)
|
||||
const maskZ = Number(await page.$eval('.g3-drawer__mask', (el) => (el as HTMLElement).style.zIndex))
|
||||
const wrapZ = Number(await page.$eval('.g3-drawer__wrap', (el) => (el as HTMLElement).style.zIndex))
|
||||
expect(wrapZ).toBe(maskZ)
|
||||
|
||||
// 点面板外(遮罩区域)关闭
|
||||
const maskBox = (await page.locator('.g3-drawer__mask').boundingBox())!
|
||||
await page.mouse.click(maskBox.x + 40, maskBox.y + maskBox.height / 2)
|
||||
await expect(drawer).toBeHidden()
|
||||
})
|
||||
|
||||
test('弹窗内的下拉浮层高于遮罩,且可正常点选', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-modal').click()
|
||||
await expect(page.locator('.g3-modal')).toBeVisible()
|
||||
|
||||
const select = page.getByTestId('modal-select')
|
||||
await select.locator('.g3-select__trigger').click()
|
||||
|
||||
const popup = page.locator('.g3-popover__popup').filter({ has: page.locator('.g3-select__dropdown') })
|
||||
await expect(popup).toBeVisible()
|
||||
|
||||
const modalZ = Number(await page.$eval('.g3-modal__mask', (el) => (el as HTMLElement).style.zIndex))
|
||||
const popupZ = Number(await popup.evaluate((el) => (el as HTMLElement).style.zIndex))
|
||||
expect(popupZ).toBeGreaterThan(modalZ)
|
||||
|
||||
// 浮层区域最上层必须是选项本体,而不是遮罩
|
||||
const optionBox = (await page.locator('.g3-select__option').first().boundingBox())!
|
||||
const hit = await topElementAt(page, optionBox.x + 10, optionBox.y + optionBox.height / 2)
|
||||
expect(hit).toContain('g3-select__option')
|
||||
})
|
||||
|
||||
test('反馈类提示盖在浮层与弹窗之上', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('open-modal').click()
|
||||
const select = page.getByTestId('modal-select')
|
||||
await select.locator('.g3-select__trigger').click()
|
||||
await expect(
|
||||
page.locator('.g3-popover__popup').filter({ has: page.locator('.g3-select__dropdown') }),
|
||||
).toBeVisible()
|
||||
|
||||
// 触发按钮此时在弹窗遮罩之后:真实用户点不到它(这正是层级正确的表现),
|
||||
// 所以直接派发 click 到元素上,本用例只验证「提示出现后压在最上层」
|
||||
await page.getByTestId('toast-default').dispatchEvent('click')
|
||||
await expect(page.locator('.g3-message').first()).toBeVisible()
|
||||
|
||||
const messageZ = Number(
|
||||
await page.$eval('.g3-message__container', (el) => (el as HTMLElement).style.zIndex),
|
||||
)
|
||||
const popupZ = Number(
|
||||
await page
|
||||
.locator('.g3-popover__popup')
|
||||
.filter({ has: page.locator('.g3-select__dropdown') })
|
||||
.evaluate((el) => (el as HTMLElement).style.zIndex),
|
||||
)
|
||||
const modalZ = Number(await page.$eval('.g3-modal__mask', (el) => (el as HTMLElement).style.zIndex))
|
||||
|
||||
expect(messageZ).toBeGreaterThan(popupZ)
|
||||
expect(popupZ).toBeGreaterThan(modalZ)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Select 回归。
|
||||
*
|
||||
* 最严重的一次事故:unwrapped 触发器每次渲染都换新的组件对象,
|
||||
* `<component :is>` 判定类型变化 → 卸载重建触发器 DOM → 焦点丢失派发 focusout
|
||||
* → 处理器改状态 → 再重建……自我驱动成死循环,点击一下就把主线程打满。
|
||||
* 所以这里用 MutationObserver 数「DOM 节点增删次数」来锁住「触发器不被重建」。
|
||||
*/
|
||||
|
||||
test.describe('Select 交互', () => {
|
||||
test('打开下拉不会重建触发器 DOM(死循环回归)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
await page.evaluate(() => {
|
||||
;(window as unknown as { __mut: number }).__mut = 0
|
||||
new MutationObserver((records) => {
|
||||
const target = window as unknown as { __mut: number }
|
||||
for (const record of records) {
|
||||
target.__mut += record.addedNodes.length + record.removedNodes.length
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true })
|
||||
})
|
||||
|
||||
const trigger = page.locator('[data-testid="select-basic"] .g3-select__trigger')
|
||||
await trigger.click()
|
||||
await expect(page.locator('.g3-select__option').first()).toBeVisible()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
const mutations = await page.evaluate(() => (window as unknown as { __mut: number }).__mut)
|
||||
// 只有浮层挂载带来的少量节点变化;重建触发器会产生成百上千次
|
||||
expect(mutations).toBeLessThan(20)
|
||||
|
||||
// 焦点应稳定停留在触发器输入框上
|
||||
const focused = await page.evaluate(() => document.activeElement?.className ?? '')
|
||||
expect(focused).toContain('g3-select__input')
|
||||
})
|
||||
|
||||
test('选中选项后回填并关闭', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="select-basic"]')
|
||||
|
||||
await root.locator('.g3-select__trigger').click()
|
||||
await page.locator('.g3-select__option').filter({ hasText: '会员' }).click()
|
||||
|
||||
await expect(root.locator('input')).toHaveValue('会员')
|
||||
await expect(page.locator('.g3-select__dropdown')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('多选以「、」拼接回显,且选中后保持展开', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="select-multiple"]')
|
||||
|
||||
await root.locator('.g3-select__trigger').click()
|
||||
await page.locator('.g3-select__option').filter({ hasText: '管理员' }).click()
|
||||
await page.locator('.g3-select__option').filter({ hasText: '会员' }).click()
|
||||
|
||||
await expect(root.locator('input')).toHaveValue('管理员、会员')
|
||||
await expect(page.locator('.g3-select__dropdown')).toBeVisible()
|
||||
})
|
||||
|
||||
test('下拉宽度跟随触发框,且不超过上限', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const root = page.locator('[data-testid="select-basic"]')
|
||||
const triggerWidth = (await root.locator('.g3-select__trigger').boundingBox())!.width
|
||||
|
||||
await root.locator('.g3-select__trigger').click()
|
||||
// playground 里还有两个常驻 Popover,用内容过滤精确定位本用例的浮层
|
||||
const popup = page.locator('.g3-popover__popup').filter({ has: page.locator('.g3-select__dropdown') })
|
||||
await expect(popup).toBeVisible()
|
||||
|
||||
const popupWidth = (await popup.boundingBox())!.width
|
||||
// 下拉宽度 = max(触发框宽, 内容宽):触发框更宽时下拉跟着变宽(antd 行为),
|
||||
// 样式里的 max-width 只在触发框较窄、内容很长时起限制作用
|
||||
expect(popupWidth).toBeGreaterThanOrEqual(Math.floor(triggerWidth) - 1)
|
||||
|
||||
const viewport = page.viewportSize()!
|
||||
expect(popupWidth).toBeLessThanOrEqual(viewport.width)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,224 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 组件细节回归 —— 每一条都对应一个真实报障,之前全部漏测。
|
||||
*
|
||||
* 共同点:它们要么靠布局计算(滑块尺寸、等高、线性位移),要么靠伪类层叠
|
||||
* (:hover 的特异性、相邻边框的 z-index),要么靠 CSS 变量的实际解析结果
|
||||
* (color-mix 里的 var())。这些在 happy-dom 里量不出来,必须放 E2E。
|
||||
*/
|
||||
|
||||
test.describe('ButtonGroup', () => {
|
||||
test('横向:hover 的按钮浮到上层,被压住的那条边框仍可见', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const second = page.getByTestId('btn-group-plain').locator('.g3-button').nth(1)
|
||||
await second.hover()
|
||||
|
||||
// 相邻按钮用 -1px 负边距重叠,不抬 z-index 时后一个按钮的边框会盖住它
|
||||
await expect(second).toHaveCSS('z-index', '1')
|
||||
// 边框色有 0.2s 过渡:必须用会重试的断言,直接 evaluate 会读到过渡中间值
|
||||
await expect(second).toHaveCSS('border-top-color', 'rgb(24, 24, 27)')
|
||||
})
|
||||
|
||||
test('纵向:hover 的按钮同样抬升', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const second = page.getByTestId('btn-group-vertical').locator('.g3-button').nth(1)
|
||||
await second.hover()
|
||||
|
||||
await expect(second).toHaveCSS('z-index', '1')
|
||||
await expect(second).toHaveCSS('border-top-color', 'rgb(24, 24, 27)')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Segmented', () => {
|
||||
test('选中滑块与选中项同尺寸同位置(滑块曾塌成 0 高,看不出选中)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// 必须限定在同一个实例内:页面上有多组 Segmented,跨实例比较会拿到别人的滑块
|
||||
const group = page.locator('.g3-segmented__group').first()
|
||||
const thumb = group.locator('.g3-segmented__thumb')
|
||||
const item = group.locator('.g3-segmented__item.is-selected')
|
||||
|
||||
const t = (await thumb.boundingBox())!
|
||||
const i = (await item.boundingBox())!
|
||||
|
||||
// 滑块尺寸来自 offsetWidth/offsetHeight(整数),实际布局是亚像素,允许 1px 误差
|
||||
for (const [name, a, b] of [
|
||||
['width', t.width, i.width],
|
||||
['height', t.height, i.height],
|
||||
['x', t.x, i.x],
|
||||
['y', t.y, i.y],
|
||||
] as const) {
|
||||
expect(Math.abs(a - b), `滑块与选中项的 ${name} 不一致`).toBeLessThanOrEqual(1)
|
||||
}
|
||||
expect(t.height).toBeGreaterThan(0)
|
||||
// 滑块就是选中底色,不能是透明的
|
||||
expect(await thumb.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(
|
||||
'rgb(255, 255, 255)',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Pagination', () => {
|
||||
test('hover 当前页仍保持主色填充(曾被通用 hover 覆盖成白底白字)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const current = page.locator('.g3-pagination__page.is-page-current')
|
||||
await current.hover()
|
||||
|
||||
await expect(current).toHaveCSS('background-color', 'rgb(24, 24, 27)')
|
||||
await expect(current).toHaveCSS('color', 'rgb(250, 250, 250)')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Tooltip', () => {
|
||||
test('气泡没有描边(投影由 popup 的 drop-shadow 统一施加)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByTestId('tooltip-trigger').hover()
|
||||
|
||||
const content = page.locator('.g3-popover__content').first()
|
||||
await expect(content).toBeVisible()
|
||||
await expect(content).toHaveCSS('border-top-width', '0px')
|
||||
await expect(content).toHaveCSS('border-left-width', '0px')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Tabs 垂直方向', () => {
|
||||
for (const placement of ['left', 'right'] as const) {
|
||||
test(`${placement}:标签栏与内容区之间有竖向分隔线,选中项带侧边指示线与底色`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
const tabs = page.locator(`.g3-tabs--${placement}`).first()
|
||||
const nav = tabs.locator('.g3-tabs__nav')
|
||||
const active = tabs.locator('.g3-tabs__tab.is-tab-active').first()
|
||||
|
||||
if (placement === 'left') {
|
||||
await expect(nav).toHaveCSS('border-right-width', '1px')
|
||||
await expect(active).toHaveCSS('border-right-width', '2px')
|
||||
} else {
|
||||
await expect(nav).toHaveCSS('border-left-width', '1px')
|
||||
await expect(active).toHaveCSS('border-left-width', '2px')
|
||||
}
|
||||
// 选中项底色不再是 transparent
|
||||
const bg = await active.evaluate((el) => getComputedStyle(el).backgroundColor)
|
||||
expect(bg).not.toBe('rgba(0, 0, 0, 0)')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Tree', () => {
|
||||
test('点击节点有选中底色(token color-mix 曾解析失败导致背景透明)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const node = page.locator('.g3-tree-node', { hasText: '节点一' }).first()
|
||||
await node.click()
|
||||
|
||||
await expect(node).toHaveClass(/is-selected/)
|
||||
const bg = await node.evaluate((el) => getComputedStyle(el).backgroundColor)
|
||||
expect(bg, `选中底色不应为透明:${bg}`).not.toBe('rgba(0, 0, 0, 0)')
|
||||
})
|
||||
|
||||
test('整层都是叶子时不渲染展开箭头占位', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// 扁平树:两个节点都没有子节点
|
||||
const flatTree = page.locator('.g3-tree').filter({ hasText: '节点一(无子节点)' }).first()
|
||||
|
||||
await expect(flatTree.locator('.g3-tree-node')).toHaveCount(2)
|
||||
await expect(flatTree.locator('.g3-tree-node__switcher')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('拖拽需要长按武装:直接拖动不会产生落点', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const draggableTree = page.locator('.g3-tree').filter({ hasText: '节点一(无子节点)' })
|
||||
// 用带 draggable 的那棵树里的第一行
|
||||
const row = page.locator('.g3-tree-node').first()
|
||||
const box = (await row.boundingBox())!
|
||||
|
||||
await page.mouse.move(box.x + 40, box.y + box.height / 2)
|
||||
await page.mouse.down()
|
||||
// 立刻移动(未到 400ms 阈值):draggable 属性还没武装
|
||||
await page.mouse.move(box.x + 60, box.y + box.height / 2 + 30, { steps: 3 })
|
||||
expect(await row.getAttribute('draggable')).toBeNull()
|
||||
await page.mouse.up()
|
||||
expect(await draggableTree.count()).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DatePicker 时间列', () => {
|
||||
test('时/分/秒三列并排:定宽、列间分隔线、列表与日历等高且可滚动', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.locator('.g3-date-picker__input').first().click()
|
||||
|
||||
const calendar = page.locator('.g3-calendar')
|
||||
await expect(calendar).toBeVisible()
|
||||
|
||||
const cols = calendar.locator('.g3-time-select__col')
|
||||
await expect(cols).toHaveCount(3)
|
||||
await expect(cols.first()).toHaveCSS('width', '56px')
|
||||
await expect(cols.nth(1)).toHaveCSS('border-left-width', '1px')
|
||||
|
||||
const main = (await calendar.locator('.g3-calendar__main').boundingBox())!
|
||||
const time = (await calendar.locator('.g3-calendar__time').boundingBox())!
|
||||
expect(Math.round(main.height)).toBe(Math.round(time.height))
|
||||
|
||||
const list = calendar.locator('.g3-time-select__list').first()
|
||||
const listBox = (await list.boundingBox())!
|
||||
expect(Math.round(listBox.height)).toBe(Math.round(main.height))
|
||||
expect(
|
||||
await list.evaluate((el) => el.scrollHeight > el.clientHeight),
|
||||
'时间列应当内部滚动而不是撑开面板',
|
||||
).toBe(true)
|
||||
// 单个选项 28px 高
|
||||
await expect(calendar.locator('.g3-time-select__item').first()).toHaveCSS('height', '28px')
|
||||
})
|
||||
|
||||
test('年 / 月选择面板:3 列 × 4 行,标题显示年份区间,当前年有标记', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.locator('.g3-date-picker__input').first().click()
|
||||
|
||||
await page.locator('.g3-calendar__title-button').first().click()
|
||||
|
||||
const selector = page.locator('.g3-calendar__selector')
|
||||
await expect(selector).toBeVisible()
|
||||
await expect(selector.locator('.g3-calendar__selector-item')).toHaveCount(12)
|
||||
await expect(selector).toHaveCSS('grid-template-columns', /(\d+(\.\d+)?px ){2}/)
|
||||
|
||||
const title = await page.locator('.g3-calendar__title-button').first().textContent()
|
||||
expect(title).toMatch(/\d{4}\s*-\s*\d{4}/)
|
||||
await expect(selector.locator('.g3-calendar__selector-item.is-selected')).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Splitter 拖拽', () => {
|
||||
test('面板宽度随指针线性变化(曾经一拖就跳)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const bar = page.locator('.g3-splitter__bar').first()
|
||||
// mouse API 用视口坐标且不会自动滚动,元素不在视口内时拖拽会落空
|
||||
await bar.scrollIntoViewIfNeeded()
|
||||
const box = (await bar.boundingBox())!
|
||||
const widths = () =>
|
||||
page.$$eval('.g3-splitter-panel', (els) =>
|
||||
els.map((el) => Math.round(el.getBoundingClientRect().width)),
|
||||
)
|
||||
|
||||
const before = await widths()
|
||||
const startX = box.x + box.width / 2
|
||||
const y = box.y + box.height / 2
|
||||
|
||||
await page.mouse.move(startX, y)
|
||||
await page.mouse.down()
|
||||
|
||||
const samples: number[][] = []
|
||||
for (const dx of [20, 40, 60]) {
|
||||
await page.mouse.move(startX + dx, y)
|
||||
await page.waitForTimeout(60)
|
||||
samples.push(await widths())
|
||||
}
|
||||
await page.mouse.up()
|
||||
|
||||
// 每移动 20px,左侧面板就该宽 20px(严格 1:1,不是跳到某个值)
|
||||
samples.forEach((sample, index) => {
|
||||
const delta = sample[0] - before[0]
|
||||
expect(delta, `第 ${index + 1} 次移动后左侧宽度增量`).toBeGreaterThanOrEqual((index + 1) * 20 - 2)
|
||||
expect(delta).toBeLessThanOrEqual((index + 1) * 20 + 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user