u
This commit is contained in:
1 parent
0be0b0767a
commit
5f28a10d89
852 files changed
+8962
-111755
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import { expect, test, type Locator, type Page } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* RTL(L3,真浏览器)。
|
||||
*
|
||||
* 为什么必须在这一层:镜像本质是几何(谁挨着谁的哪条边、圆角跑到哪一侧),
|
||||
* happy-dom 没有布局引擎,单测只能测到 class / 逻辑属性名,测不到「真的翻过去了」。
|
||||
*
|
||||
* 进 RTL 统一走 `?dir=rtl`(playground 写到 `<html dir>`),不依赖点击开关的顺序;
|
||||
* 关键断言都在 LTR / RTL 各测一次 —— 单侧通过可能是巧合,两侧夹住才是「镜像」。
|
||||
*
|
||||
* 另外两类刻意不镜像的东西也在这里锁住(防止后人「顺手修」):
|
||||
* - Drawer / Modal / Popover 的 placement 是**物理**语义
|
||||
* - 日期面板 / 轮播类的居中定位(left: 50% + translateX)与方向无关
|
||||
*/
|
||||
|
||||
const url = (dir: 'ltr' | 'rtl') => `/?dir=${dir}`
|
||||
|
||||
/** 每个用例先确认 <html dir> 真的生效:假 RTL 会让所有结论失真 */
|
||||
async function gotoDir(page: Page, dir: 'ltr' | 'rtl') {
|
||||
await page.goto(url(dir))
|
||||
await expect(page.locator('html')).toHaveAttribute('dir', dir)
|
||||
}
|
||||
|
||||
/** 元素水平中心 */
|
||||
async function centerX(locator: Locator): Promise<number> {
|
||||
const box = await locator.boundingBox()
|
||||
if (!box) throw new Error('元素不可见,拿不到 boundingBox')
|
||||
return box.x + box.width / 2
|
||||
}
|
||||
|
||||
test.describe('RTL 镜像(几何)', () => {
|
||||
test('Switch:滑块从行内起始边起步,选中后朝行内结束方向走', async ({ page }) => {
|
||||
for (const dir of ['ltr', 'rtl'] as const) {
|
||||
await gotoDir(page, dir)
|
||||
const root = page.locator('[data-testid="switch"]')
|
||||
const track = root.locator('.g3-switch__track')
|
||||
const thumb = root.locator('.g3-switch__thumb')
|
||||
const trackCenter = await centerX(track)
|
||||
/** 滑块是否停在「行内结束」那一侧:LTR 是右、RTL 是左 */
|
||||
const onEndSide = async () =>
|
||||
dir === 'ltr' ? (await centerX(thumb)) > trackCenter : (await centerX(thumb)) < trackCenter
|
||||
|
||||
// playground 初始 agree = true(选中)→ 在行内结束侧
|
||||
// 用 poll:滑块有 200ms 过渡,直接读会拿到动画中间值
|
||||
await expect.poll(onEndSide).toBe(true)
|
||||
|
||||
// 取消选中(位移量乘了 --g3-dir-sign)→ 回到行内起始侧
|
||||
await root.click()
|
||||
await expect.poll(onEndSide).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
test('Scrollbar:竖直轨道贴行内结束边(RTL 下移到左侧,与原生滚动条一致)', async ({ page }) => {
|
||||
for (const dir of ['ltr', 'rtl'] as const) {
|
||||
await gotoDir(page, dir)
|
||||
const root = page.locator('[data-testid="scrollbar"]')
|
||||
const track = root.locator('.g3-scrollbar__track-v')
|
||||
await expect(track).toBeVisible()
|
||||
|
||||
const box = (await root.boundingBox())!
|
||||
const trackBox = (await track.boundingBox())!
|
||||
const trackCenter = trackBox.x + trackBox.width / 2
|
||||
const rootCenter = box.x + box.width / 2
|
||||
|
||||
if (dir === 'ltr') expect(trackCenter).toBeGreaterThan(rootCenter)
|
||||
else expect(trackCenter).toBeLessThan(rootCenter)
|
||||
}
|
||||
})
|
||||
|
||||
test('Tree:缩进与展开箭头的列在 RTL 下翻到右侧', async ({ page }) => {
|
||||
for (const dir of ['ltr', 'rtl'] as const) {
|
||||
await gotoDir(page, dir)
|
||||
const node = page.locator('[data-testid="tree"] .g3-tree-node').first()
|
||||
const switcher = node.locator('.g3-tree-node__switcher')
|
||||
await expect(switcher).toBeVisible()
|
||||
|
||||
const nodeBox = (await node.boundingBox())!
|
||||
const swBox = (await switcher.boundingBox())!
|
||||
const nodeCenter = nodeBox.x + nodeBox.width / 2
|
||||
const swCenter = swBox.x + swBox.width / 2
|
||||
|
||||
if (dir === 'ltr') expect(swCenter).toBeLessThan(nodeCenter)
|
||||
else expect(swCenter).toBeGreaterThan(nodeCenter)
|
||||
}
|
||||
})
|
||||
|
||||
test('ButtonGroup:首尾圆角镜像(逻辑半径属性真的落到了另一侧)', async ({ page }) => {
|
||||
for (const dir of ['ltr', 'rtl'] as const) {
|
||||
await gotoDir(page, dir)
|
||||
const first = page.locator('[data-testid="btn-group-plain"] .g3-button').first()
|
||||
const radii = await first.evaluate((el) => {
|
||||
const cs = getComputedStyle(el)
|
||||
return {
|
||||
topLeft: parseFloat(cs.borderTopLeftRadius),
|
||||
topRight: parseFloat(cs.borderTopRightRadius),
|
||||
}
|
||||
})
|
||||
|
||||
if (dir === 'ltr') {
|
||||
expect(radii.topLeft).toBeGreaterThan(0)
|
||||
expect(radii.topRight).toBe(0)
|
||||
} else {
|
||||
expect(radii.topRight).toBeGreaterThan(0)
|
||||
expect(radii.topLeft).toBe(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('浮层 bottom-start:RTL 下对齐触发器右边缘(position.ts 镜像了对齐轴)', async ({ page }) => {
|
||||
for (const dir of ['ltr', 'rtl'] as const) {
|
||||
await gotoDir(page, dir)
|
||||
// playground 里 popover-a 挂载即展开(placement="bottom-start")
|
||||
const trigger = page.getByRole('button', { name: '初始展开 A' })
|
||||
const popup = page.locator('.g3-popover__popup').filter({ hasText: '浮层 A' })
|
||||
await expect(popup).toBeVisible()
|
||||
|
||||
const t = (await trigger.boundingBox())!
|
||||
const p = (await popup.boundingBox())!
|
||||
// 容差 2px:浮层宽度可能因内容取整差 1px
|
||||
if (dir === 'ltr') expect(Math.abs(p.x - t.x)).toBeLessThanOrEqual(2)
|
||||
else expect(Math.abs(p.x + p.width - (t.x + t.width))).toBeLessThanOrEqual(2)
|
||||
}
|
||||
})
|
||||
|
||||
test('Tabs:RTL 下左右键语义反转(键盘走向跟视觉顺序)', async ({ page }) => {
|
||||
await gotoDir(page, 'rtl')
|
||||
const tabs = page.locator('[data-testid="tabs"]')
|
||||
|
||||
await tabs.locator('.g3-tabs__tab').first().focus()
|
||||
// RTL 里行内结束方向是「左」→ ArrowLeft 是下一个
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await expect(tabs.getByText('标签二的内容')).toBeVisible()
|
||||
|
||||
// 反向:ArrowRight 回到上一个
|
||||
await page.keyboard.press('ArrowRight')
|
||||
await expect(tabs.getByText('标签一的内容')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Drawer:placement 是物理语义,RTL 下右侧抽屉仍贴右边(有意不镜像)', async ({ page }) => {
|
||||
await gotoDir(page, 'rtl')
|
||||
await page.locator('[data-testid="open-drawer"]').click()
|
||||
|
||||
const panel = page.locator('.g3-drawer')
|
||||
await expect(panel).toBeVisible()
|
||||
|
||||
const viewport = page.viewportSize()!
|
||||
const box = (await panel.boundingBox())!
|
||||
expect(box.x + box.width).toBeGreaterThanOrEqual(viewport.width - 2)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user