Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/playwright-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ jobs:
mkdir -p test-results/ci
suffix="${{ inputs.project-name }}${{ inputs.multisite && '-multisite' || '' }}"
: > "test-results/ci/playwright-${suffix}.log"
npm run test:playwright -- --project=${{ inputs.project-name }} 2>&1 | tee -a "test-results/ci/playwright-${suffix}.log"
# shellcheck disable=SC2086 -- project-name may hold several names.
npm run test:playwright -- --project ${{ inputs.project-name }} 2>&1 | tee -a "test-results/ci/playwright-${suffix}.log"

- name: Normalize Playwright report filenames
if: always()
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ jobs:
uses: ./.github/workflows/playwright-test.yml
with:
test-mode: 'default'
project-name: 'chromium-db-snippets'
# The right-to-left project runs after the default one on the same site;
# its own setup switches the test user's locale and its teardown restores it.
project-name: 'chromium-db-snippets chromium-rtl'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
multisite: ${{ inputs.multisite }}

playwright-file-based-execution:
Expand Down
30 changes: 28 additions & 2 deletions config/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const MILLISECONDS_IN_SECOND = 1000

const baseTestsDir = join(__dirname, '..', '..', 'tests')
const storageState = join(baseTestsDir, 'e2e/.auth/user.json')
const rtlSpecs = /rtl-layout\.spec\.ts/
const rtlStorageState = join(baseTestsDir, 'e2e/.auth/rtl-user.json')

/**
* @see https://playwright.dev/docs/test-configuration
Expand Down Expand Up @@ -66,7 +68,7 @@ export default defineConfig({
storageState
},
dependencies: ['setup'],
testIgnore: /.*\.setup\.ts/
testIgnore: [/.*\.setup\.ts/, /.*\.teardown\.ts/, rtlSpecs]
},

{
Expand All @@ -76,7 +78,31 @@ export default defineConfig({
storageState
},
dependencies: ['setup', 'flat-files-setup'],
testIgnore: /.*\.setup\.ts/
testIgnore: [/.*\.setup\.ts/, /.*\.teardown\.ts/, rtlSpecs]
},

// The RTL specs sign in as a user of their own on a right-to-left locale
// and keep that session apart, so the other projects never see the site
// mirrored whatever order Playwright runs them in.
{
name: 'rtl-setup',
testMatch: /rtl\.setup\.ts/,
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup']
},
{
name: 'rtl-teardown',
testMatch: /rtl\.teardown\.ts/
},
{
name: 'chromium-rtl',
testMatch: rtlSpecs,
use: {
...devices['Desktop Chrome'],
storageState: rtlStorageState
},
dependencies: ['rtl-setup'],
teardown: 'rtl-teardown'
}
],

Expand Down
9 changes: 5 additions & 4 deletions src/css/common/list-table/_responsive.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
@use '../theme';

// Toolbar squeeze: above the mobile breakpoint the row never wraps —
// components shrink gracefully instead (flex-wrap would otherwise win
// before flex-shrink ever engages). Wrapping remains the small-screen
// fallback only.
// Toolbar squeeze: above the tablet collapse (see below) the row never wraps;
// components shrink instead (flex-wrap would otherwise win before flex-shrink
// ever engages). Between 782px and 1400px the pagination and view toggle, which
// cannot shrink, force the row onto two lines; below 782px wrapping is the
// small-screen fallback.
.snippets-list-view .tablenav {
flex-wrap: wrap;

Expand Down
20 changes: 20 additions & 0 deletions tests/e2e/editor-labels.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test'

// CodeMirror replaces the labelled textarea with an input of its own, so the
// name a screen reader hears comes from the attribute the plugin sets on that
// input after the editor starts. One check per editor the plugin creates.
test.describe('Code editor labels', () => {
test('the snippet editor input is named', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=add-snippet')
await page.waitForSelector('.CodeMirror')

await expect(page.locator('.snippet-editor .CodeMirror textarea')).toHaveAttribute('aria-label', 'Snippet code')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test('the settings preview input is named', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=snippets-settings&section=editing')
await page.waitForSelector('.CodeMirror')

await expect(page.locator('.CodeMirror textarea').first()).toHaveAttribute('aria-label', 'Code editor preview')
})
})
8 changes: 4 additions & 4 deletions tests/e2e/list-toolbar-fit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ const SNIPPETS_URL = '/wp-admin/admin.php?page=snippets'

// The toolbar's end group (pagination plus the view toggle) cannot shrink, so at
// widths where the row does not fit it must wrap rather than spill off the page.
// The row collapses onto two lines up to 1400px, so both sides of that boundary
// are covered; the right-to-left project checks the same screen mirrored.
test.describe('Snippets toolbar fit', () => {
for (const width of [1280, 1360, 1600]) {
for (const width of [1280, 1360, 1400, 1401, 1600]) {
test(`nothing spills out of the toolbar at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
await page.goto(SNIPPETS_URL)
Expand All @@ -15,18 +17,16 @@ test.describe('Snippets toolbar fit', () => {
const nav = document.querySelector('.snippets-list-view .tablenav.top')
const toggle = document.querySelector('.snippet-view-toggle')
if (!nav || !toggle) {
return { missing: true }
throw new Error('The snippets toolbar or its view toggle did not render.')
}
const n = nav.getBoundingClientRect()
const t = toggle.getBoundingClientRect()
return {
missing: false,
pageOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
toggleInsideNav: t.left >= n.left - 1 && t.right <= n.right + 1
}
})

expect(fit.missing).toBe(false)
expect(fit.pageOverflow).toBeLessThanOrEqual(0)
expect(fit.toggleInsideNav).toBe(true)
})
Expand Down
62 changes: 62 additions & 0 deletions tests/e2e/rtl-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, test } from '@playwright/test'

// The admin screens under a right-to-left locale. Mirroring is driven by the
// direction multiplier and logical properties; what can still go wrong is a
// control pushed past the page edge or a row that no longer fits, so every
// screen is checked for those rather than for how it looks.
const SCREENS: [string, string, string][] = [
['settings, editing tab', '/wp-admin/admin.php?page=snippets-settings&section=editing', '#settings-sections-tabs'],
['settings, advanced tab', '/wp-admin/admin.php?page=snippets-settings&section=advanced', '#settings-sections-tabs'],
['snippets list', '/wp-admin/admin.php?page=snippets', '.snippet-view-toggle'],
['new snippet', '/wp-admin/admin.php?page=add-snippet', '.CodeMirror'],
['import', '/wp-admin/admin.php?page=import-code-snippets', '#wpbody-content']
]

interface LayoutReport {
dir: string
bodyRtl: boolean
multiplier: string
pageOverflow: number
offscreen: string[]
}

const inspect = (): LayoutReport => {
const viewportWidth = document.documentElement.clientWidth
const selector = ['a', 'button', 'input', 'select'].map(tag => `#wpbody-content ${tag}`).join(', ')
const controls = Array.from(document.querySelectorAll<HTMLElement>(selector))

const offscreen = controls
.filter(element => null !== element.offsetParent)
.map(element => ({ element, rect: element.getBoundingClientRect() }))
.filter(({ rect }) => 0 < rect.width && (0 > rect.left || rect.right > viewportWidth + 1))
.map(({ element, rect }) => {
const name = `${element.tagName.toLowerCase()}.${element.className.split(' ')[0]}`
return `${name} left=${Math.round(rect.left)} right=${Math.round(rect.right)}`
})

return {
dir: document.documentElement.getAttribute('dir') ?? 'ltr',
bodyRtl: document.body.classList.contains('rtl'),
multiplier: getComputedStyle(document.documentElement).getPropertyValue('--cs-direction-multiplier').trim(),
pageOverflow: document.documentElement.scrollWidth - viewportWidth,
offscreen
}
}

test.describe('Right-to-left layout', () => {
for (const [name, url, ready] of SCREENS) {
test(`${name} mirrors without spilling off the page`, async ({ page }) => {
await page.setViewportSize({ width: 1360, height: 900 })
await page.goto(url)
await page.waitForSelector(ready)

const report = await page.evaluate(inspect)
test.skip('rtl' !== report.dir, 'The RTL locale is not available on this site, so there is nothing to check.')

expect(report.bodyRtl, 'WordPress marks the body as RTL').toBe(true)
expect(report.multiplier, 'the direction multiplier is flipped').toBe('-1')
expect(report.pageOverflow, 'the page does not scroll sideways').toBeLessThanOrEqual(0)
expect(report.offscreen, 'no control sits outside the viewport').toEqual([])
})
}
})
49 changes: 49 additions & 0 deletions tests/e2e/rtl.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { join } from 'path'
import { expect, test as setup } from '@playwright/test'
import { wpCli } from './helpers/wpCli'

// The RTL specs sign in as a user of their own whose locale is right-to-left,
// so the rest of the suite, which signs in as the usual admin, never sees the
// site mirrored, whatever order the projects run in. The language pack is
// fetched from wordpress.org when missing; if that is impossible (offline),
// the specs notice the page is still left-to-right and skip themselves.
const RTL_LOCALE = 'he_IL'
const RTL_USER = 'rtl-admin'
const SETUP_TIMEOUT_MS = 180000

export const rtlAuthFile = join(__dirname, '.auth/rtl-user.json')

setup('sign in as a right-to-left user', async ({ page }) => {
setup.setTimeout(SETUP_TIMEOUT_MS)

try {
await wpCli(['language', 'core', 'install', RTL_LOCALE])
} catch (error) {
console.warn(`Could not install the ${RTL_LOCALE} language pack; RTL specs will skip.`, error)
}

// `user create` takes no locale flag, so the locale is set by a second command.
try {
await wpCli(['user', 'get', RTL_USER, '--field=ID'])
} catch {
await wpCli(['user', 'create', RTL_USER, `${RTL_USER}@example.org`, '--role=administrator'])
}

await wpCli(['user', 'update', RTL_USER, '--user_pass=password', `--locale=${RTL_LOCALE}`])

await page.goto('/wp-login.php')
await page.waitForSelector('#user_login')
await page.fill('#user_login', RTL_USER)
await page.fill('#user_pass', 'password')
await Promise.all([
page.waitForLoadState('domcontentloaded'),
page.click('#wp-submit')
])
await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: 60000 })
await expect(page.locator('#adminmenu')).toBeVisible()

const dir = await page.evaluate(() => document.documentElement.getAttribute('dir'))
console.log(`RTL setup: ${RTL_USER} renders the admin with dir="${dir ?? 'ltr'}"`)

await page.context().storageState({ path: rtlAuthFile })
})
11 changes: 11 additions & 0 deletions tests/e2e/rtl.teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test as teardown } from '@playwright/test'
import { wpCli } from './helpers/wpCli'

// Remove the right-to-left user the setup created, so the site is left as found.
teardown('remove the right-to-left user', async () => {
try {
await wpCli(['user', 'delete', 'rtl-admin', '--yes'])
} catch {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Already gone, or never created because the setup failed early.
}
})
Loading