This commit is contained in:
oneao committed 2026-10-09 22:05:02 +08:00
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)
})
})