152 lines
6.3 KiB
TypeScript
152 lines
6.3 KiB
TypeScript
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)
|
||
})
|
||
})
|