import { expect, test, type Locator, type Page } from '@playwright/test' /** * RTL(L3,真浏览器)。 * * 为什么必须在这一层:镜像本质是几何(谁挨着谁的哪条边、圆角跑到哪一侧), * happy-dom 没有布局引擎,单测只能测到 class / 逻辑属性名,测不到「真的翻过去了」。 * * 进 RTL 统一走 `?dir=rtl`(playground 写到 ``),不依赖点击开关的顺序; * 关键断言都在 LTR / RTL 各测一次 —— 单侧通过可能是巧合,两侧夹住才是「镜像」。 * * 另外两类刻意不镜像的东西也在这里锁住(防止后人「顺手修」): * - Drawer / Modal / Popover 的 placement 是**物理**语义 * - 日期面板 / 轮播类的居中定位(left: 50% + translateX)与方向无关 */ const url = (dir: 'ltr' | 'rtl') => `/?dir=${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 { 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) }) })