Files
workspace/code/g3soft-libs/packages/ui/e2e/rtl.spec.ts
T
2026-10-09 22:05:02 +08:00

152 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})