QA How-To
Test Right to Left Layouts With Playwright (2026)
Learn to test right to left layouts playwright projects with direction, geometry, keyboard, overflow, cross-browser, and visual regression checks in CI.
20 min read | 2,349 words
TL;DR
Test RTL layouts in layers: document semantics, computed direction, relative geometry, keyboard behavior, mobile overflow, and controlled screenshots. Run functional contracts across browser engines, while maintaining pixel baselines only in a pinned environment.
Key Takeaways
- Assert lang, dir, and computed direction before relying on image comparison.
- Compare relative element positions so layout tests survive harmless spacing changes.
- Keep keyboard focus in DOM order even when the visual inline flow mirrors.
- Stress narrow viewports with long natural Arabic text and explicit overflow metrics.
- Run functional RTL checks in Chromium, Firefox, and WebKit.
- Pin the browser, operating system, viewport, fonts, and data used for screenshot baselines.
The fastest way to test right to left layouts playwright teams can trust is to verify document direction, logical geometry, interaction order, overflow, and screenshots in real browsers. A page can contain correct Arabic text and still place icons, prices, navigation, or validation messages on the wrong physical side.
This tutorial builds a self-contained bilingual checkout fixture and tests it in Chromium, Firefox, and WebKit. You will assert semantics before pixels, compare left-to-right (LTR) and right-to-left (RTL) geometry, preserve keyboard order, stress a mobile viewport with long Arabic content, and add a controlled visual baseline.
The examples use CSS logical properties such as margin-inline and border-inline-start. If those concepts are new, read the localization testing fundamentals first. For a wider Playwright foundation, keep the Playwright beginner tutorial nearby.
What You Will Build
By the end, your project will contain:
- A deterministic English and Arabic checkout page rendered with
page.setContent(). - Semantic checks for
lang,dir, and computed CSS direction. - Geometry assertions that prove navigation, totals, icons, and actions mirror correctly.
- Keyboard and validation checks that catch interaction regressions without relying on coordinates.
- Mobile overflow coverage at 320, 390, and 768 CSS pixels.
- A Chromium screenshot baseline plus functional coverage in all three Playwright browser engines.
- A GitHub Actions job that runs the complete suite on every pull request.
This is not a translation test. It is a layout contract. The test data supplies readable Arabic content, while the assertions concentrate on browser behavior that changes when inline start moves from the left edge to the right edge.
Prerequisites
Use these exact versions for a repeatable 2026 setup:
- Node.js 24.18.0 LTS.
@playwright/test1.62.0.- Git 2.50 or newer.
- A terminal with about 2 GB free for the three browser binaries.
- Docker 27 or newer only if you want to create Linux screenshot baselines locally.
Check Node before creating the project:
node --version
npm --version
git --version
The first command must print v24.18.0. npm can vary with the Node distribution, so commit package-lock.json and use npm ci in automation. If your product is not yet internationalized, use this fixture as a learning harness, then adapt its contracts to your real components.
Step 1: Initialize Playwright and Define the Browser Matrix
Create a clean project, pin Playwright Test, install its managed browser builds, and add focused scripts:
mkdir rtl-playwright
cd rtl-playwright
npm init -y
npm install --save-dev @playwright/test@1.62.0
npx playwright install
npm pkg set scripts.test="playwright test"
npm pkg set scripts.test:rtl="playwright test tests/rtl-layout.spec.ts"
Create playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [
['list'],
['html', { open: 'never' }],
],
expect: {
timeout: 5_000,
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
},
},
snapshotPathTemplate:
'{testDir}/__screenshots__/{projectName}/{arg}{ext}',
use: {
headless: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Verify Step 1: Run npx playwright --version and expect Version 1.62.0. Then run npx playwright test --list --pass-with-no-tests. It should load the config, report zero tests, and exit successfully.
Step 2: Create a Fixture to test right to left layouts playwright
Create tests/support/rtl-demo.ts. This helper gives every test the same markup, styles, translations, and state change:
import type { Page } from '@playwright/test';
export type Direction = 'ltr' | 'rtl';
const copy = {
ltr: {
lang: 'en',
title: 'Review your order',
home: 'Home',
catalog: 'Catalog',
notice: 'Delivery requires a signature.',
email: 'Email address',
address: '21 Market Street, Bengaluru',
subtotal: 'Subtotal',
total: 'Total',
submit: 'Place order',
error: 'Enter a valid email address.',
},
rtl: {
lang: 'ar',
title: 'راجع طلبك',
home: 'الرئيسية',
catalog: 'المنتجات',
notice: 'يتطلب التسليم توقيعا عند الاستلام.',
email: 'البريد الإلكتروني',
address: '٢١ شارع السوق، بنغالورو',
subtotal: 'المجموع الفرعي',
total: 'الإجمالي',
submit: 'تأكيد الطلب',
error: 'أدخل عنوان بريد إلكتروني صحيحا.',
},
} as const;
export async function renderCheckout(
page: Page,
direction: Direction,
): Promise<void> {
const text = copy[direction];
await page.setContent(`
<!doctype html>
<html lang="${text.lang}" dir="${direction}">
<head>
<meta charset="utf-8">
<title>${text.title}</title>
<style>
* { box-sizing: border-box; }
html, body { min-height: 100%; }
body {
margin: 0;
color: #172033;
background: #f4f7fb;
direction: inherit;
font-family: Arial, "Noto Sans Arabic", sans-serif;
}
a, button, input { font: inherit; }
.app-shell {
max-width: 920px;
margin-inline: auto;
padding-inline: 24px;
overflow-wrap: anywhere;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding-block: 20px;
border-block-end: 1px solid #cbd5e1;
}
.brand { font-weight: 800; }
nav {
display: flex;
gap: 18px;
}
nav a { color: #4338ca; }
h1 {
margin-block: 32px 20px;
font-size: clamp(1.75rem, 5vw, 2.5rem);
}
.checkout-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 0.65fr);
gap: 24px;
}
.card {
min-inline-size: 0;
padding: 20px;
border: 1px solid #cbd5e1;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 18px rgb(15 23 42 / 8%);
}
.notice {
position: relative;
margin-block-end: 20px;
padding: 14px;
padding-inline-start: 48px;
border-inline-start: 4px solid #7c3aed;
border-radius: 8px;
background: #f5f3ff;
}
.notice-icon {
position: absolute;
inset-block-start: 14px;
inset-inline-start: 14px;
}
label {
display: block;
margin-block-end: 8px;
font-weight: 700;
}
input {
inline-size: 100%;
min-inline-size: 0;
padding-block: 10px;
padding-inline: 12px;
border: 1px solid #64748b;
border-radius: 8px;
}
.address {
margin-block: 18px 0;
line-height: 1.7;
}
.price-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding-block: 10px;
}
.price-row.total {
margin-block-start: 8px;
border-block-start: 1px solid #cbd5e1;
font-weight: 800;
}
.actions {
display: flex;
justify-content: flex-start;
margin-block-start: 18px;
}
button {
min-block-size: 44px;
padding-block: 10px;
padding-inline: 18px;
border: 0;
border-radius: 8px;
color: white;
background: #4338ca;
cursor: pointer;
}
.error {
margin-block: 8px 0;
color: #b42318;
font-weight: 700;
}
.clock {
display: block;
margin-block-start: 16px;
color: #64748b;
font-size: 0.875rem;
}
@media (max-width: 640px) {
.app-shell { padding-inline: 12px; }
.topbar { align-items: flex-start; }
.checkout-grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
</head>
<body>
<main class="app-shell" data-testid="app-shell">
<header class="topbar">
<span class="brand">QA Shop</span>
<nav aria-label="${text.catalog}">
<a data-testid="nav-home" href="#home">${text.home}</a>
<a data-testid="nav-catalog" href="#catalog">${text.catalog}</a>
</nav>
</header>
<h1>${text.title}</h1>
<section class="checkout-grid">
<div class="card">
<div class="notice" data-testid="notice">
<span class="notice-icon" data-testid="notice-icon"
aria-hidden="true">ⓘ</span>
<span>${text.notice}</span>
</div>
<label for="email">${text.email}</label>
<input id="email" name="email" type="email"
aria-describedby="email-error">
<p id="email-error" class="error"
data-testid="email-error" hidden>${text.error}</p>
<p class="address" data-testid="shipping-address">
${text.address}
</p>
</div>
<aside class="card" aria-label="${text.total}">
<div class="price-row">
<span data-testid="price-label">${text.subtotal}</span>
<span data-testid="price-value">₹4,200</span>
</div>
<div class="price-row total">
<span>${text.total}</span>
<span>₹4,200</span>
</div>
<div class="actions">
<button id="place-order" type="button">${text.submit}</button>
</div>
<time class="clock" data-testid="current-time"></time>
</aside>
</section>
</main>
<script>
document.querySelector('[data-testid="current-time"]').textContent =
new Date().toISOString();
document.querySelector('#place-order').addEventListener('click', () => {
const email = document.querySelector('#email');
const error = document.querySelector('#email-error');
error.hidden = email.value.includes('@');
});
</script>
</body>
</html>
`);
}
Create tests/rtl-layout.spec.ts with the first semantic test:
import { test, expect } from '@playwright/test';
import { renderCheckout } from './support/rtl-demo';
test('declares Arabic language and RTL direction', async ({ page }) => {
await renderCheckout(page, 'rtl');
await expect(page.locator('html')).toHaveAttribute('lang', 'ar');
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
await expect(page.getByRole('heading', { name: 'راجع طلبك' }))
.toBeVisible();
});
Verify Step 2: Run npm run test:rtl -- --project=chromium -g "declares Arabic". Expect one passing test. A missing dir or incorrect Arabic heading must fail independently, which makes the result more useful than a screenshot-only check.
Step 3: Assert Direction and Logical Geometry
Append this test to tests/rtl-layout.spec.ts:
test('computes RTL flow and mirrors inline-start styles', async ({ page }) => {
await renderCheckout(page, 'rtl');
const documentState = await page.locator('html').evaluate((element) => ({
lang: element.getAttribute('lang'),
dir: element.getAttribute('dir'),
computedDirection: getComputedStyle(element).direction,
}));
expect(documentState).toEqual({
lang: 'ar',
dir: 'rtl',
computedDirection: 'rtl',
});
const [homeBox, catalogBox] = await Promise.all([
page.getByTestId('nav-home').boundingBox(),
page.getByTestId('nav-catalog').boundingBox(),
]);
expect(homeBox).not.toBeNull();
expect(catalogBox).not.toBeNull();
expect(homeBox!.x).toBeGreaterThan(catalogBox!.x);
const borderWidths = await page.getByTestId('notice').evaluate((element) => {
const style = getComputedStyle(element);
return {
left: style.borderLeftWidth,
right: style.borderRightWidth,
};
});
expect(borderWidths).toEqual({ left: '0px', right: '4px' });
});
The three document values catch different bugs. The dir attribute checks markup, computed direction catches an overriding stylesheet, and geometry proves the browser placed the first navigation item at the RTL inline start. The notice test confirms that border-inline-start resolved to the physical right edge.
Do not assert that Arabic always means RTL. A page can contain Arabic inside an LTR shell, a user-entered email address should usually remain readable in its natural direction, and bidirectional strings can mix both scripts. Test the direction contract of each container rather than inferring direction from text.
Verify Step 3: Run npm run test:rtl -- --project=chromium -g "computes RTL". Expect one pass. Temporarily change border-inline-start to border-left if you want to prove the final assertion detects the physical-side mistake, then restore the logical property.
Step 4: Compare LTR and RTL Layout Contracts
A direction matrix exposes accidental one-sided CSS better than an RTL-only run:
| Contract | LTR expectation | RTL expectation | Best assertion |
|---|---|---|---|
| Document flow | direction: ltr |
direction: rtl |
Computed style |
| First price label | Left of value | Right of value | Relative x coordinates |
| Inline-start border | Physical left | Physical right | Computed border widths |
| Tab sequence | DOM order | Same DOM order | Focus assertions |
| Narrow page | No root overflow | No root overflow | scrollWidth comparison |
| Visual surface | LTR baseline | Mirrored RTL baseline | Controlled screenshot |
Append a parameterized price test:
for (const direction of ['ltr', 'rtl'] as const) {
test(`places the price label at ${direction} inline start`, async ({ page }) => {
await renderCheckout(page, direction);
const [labelBox, valueBox] = await Promise.all([
page.getByTestId('price-label').boundingBox(),
page.getByTestId('price-value').boundingBox(),
]);
expect(labelBox).not.toBeNull();
expect(valueBox).not.toBeNull();
if (direction === 'rtl') {
expect(labelBox!.x).toBeGreaterThan(valueBox!.x);
} else {
expect(labelBox!.x).toBeLessThan(valueBox!.x);
}
});
}
This pair uses the same DOM and CSS. Only copy and direction change, so a failure points to direction-dependent behavior instead of divergent fixture markup. It also checks the semantics of justify-content: space-between under the inherited writing direction.
Verify Step 4: Run npm run test:rtl -- --project=chromium -g "price label". Expect two passing tests, one for LTR and one for RTL. Change the price row to direction: ltr to confirm only the RTL case fails.
Step 5: Preserve Keyboard Order and Localized Validation
Visual order may mirror, but keyboard focus must remain logical and predictable. Append this behavior test:
test('keeps DOM focus order and exposes Arabic validation', async ({ page }) => {
await renderCheckout(page, 'rtl');
const email = page.getByRole('textbox', {
name: 'البريد الإلكتروني',
});
const placeOrder = page.getByRole('button', {
name: 'تأكيد الطلب',
});
const error = page.getByTestId('email-error');
await email.focus();
await expect(email).toBeFocused();
await page.keyboard.press('Tab');
await expect(placeOrder).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(email).toBeFocused();
await email.fill('invalid-address');
await placeOrder.click();
await expect(error).toBeVisible();
await expect(error).toHaveText('أدخل عنوان بريد إلكتروني صحيحا.');
await email.fill('qa@example.com');
await placeOrder.click();
await expect(error).toBeHidden();
});
CSS direction changes visual flow but should not reverse DOM focus order. Avoid positive tabindex values and CSS order tricks that make the screen layout disagree with assistive technology. The focused sequence here follows the input and then the button exactly as they appear in source.
RTL coverage intersects with accessibility but does not replace it. Add automated rules, accessible names, and screen reader review using the Playwright accessibility testing guide. Pay special attention to mixed Arabic and Latin values, focus indicators, error associations, and icons whose meaning depends on direction.
Verify Step 5: Run npm run test:rtl -- --project=chromium -g "focus order". Expect one pass. The trace should show input, button, input, invalid error, and cleared error in that order if you rerun with --trace on.
Step 6: Detect Narrow-Viewport Overflow and Clipping
Append a geometry reader and the responsive test:
async function readPageWidths(page: import('@playwright/test').Page) {
return page.evaluate(() => ({
viewportWidth: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
bodyWidth: document.body.scrollWidth,
}));
}
for (const width of [320, 390, 768]) {
test(`fits long Arabic content at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 });
await renderCheckout(page, 'rtl');
await page.getByTestId('shipping-address').evaluate((element) => {
element.textContent = (
'مبنى الاختبار، الطابق الرابع، بجانب محطة المترو، '
).repeat(8);
});
const shell = await page.getByTestId('app-shell').evaluate((element) => ({
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth,
}));
const pageWidths = await readPageWidths(page);
expect(
shell.scrollWidth,
`shell overflow: ${JSON.stringify(shell)}`,
).toBeLessThanOrEqual(shell.clientWidth + 1);
expect(
pageWidths.documentWidth,
`page overflow: ${JSON.stringify(pageWidths)}`,
).toBeLessThanOrEqual(pageWidths.viewportWidth + 1);
});
}
The one-pixel allowance absorbs fractional layout rounding without tolerating visible sideways movement. Keep the tolerance explicit. Raising it to hide a failing card converts a diagnostic into permission for clipping.
The shell metric and root metric answer different questions. The shell can overflow internally while the viewport still clips it; the document can overflow because of a portal or fixed element outside the shell. Record both values in the failure message so CI tells you which boundary broke.
Verify Step 6: Run npm run test:rtl -- --project=chromium -g "fits long Arabic". Expect three passes. Replace minmax(0, 1fr) with 1fr and give a child a large minimum width to see the narrow cases identify a genuine overflow.
Step 7: Add Snapshots to test right to left layouts playwright
Geometry catches known relationships. A screenshot can reveal an unknown regression such as a chevron pointing the wrong way, a clipped descender, a misplaced badge, or a card shadow cut at the inline edge.
Append the visual test:
for (const direction of ['ltr', 'rtl'] as const) {
test(`matches the ${direction} visual baseline`, async ({ page }) => {
test.skip(
test.info().project.name !== 'chromium',
'Visual baselines are maintained in Chromium.',
);
await page.setViewportSize({ width: 390, height: 844 });
await renderCheckout(page, direction);
await expect(page).toHaveScreenshot(`checkout-${direction}.png`, {
fullPage: true,
animations: 'disabled',
caret: 'hide',
mask: [page.getByTestId('current-time')],
maskColor: '#7c3aed',
});
});
}
Maintain visual baselines in one engine unless your product has a specific cross-engine rendering risk. Font rasterization differs by operating system and browser, while semantic and geometry checks remain portable. The suite therefore runs functional tests in all engines and screenshots only in Chromium.
Generate the first baseline in the same operating-system family used by CI. Review both images before committing them:
npx playwright test tests/rtl-layout.spec.ts \
--project=chromium \
-g "visual baseline" \
--update-snapshots
npx playwright test tests/rtl-layout.spec.ts \
--project=chromium \
-g "visual baseline"
Verify Step 7: Expect two passes on both commands. Open the LTR and RTL PNGs under tests/__screenshots__/chromium/. The Arabic navigation, notice icon, price label, and primary action should occupy opposite inline sides from their English counterparts.
Step 8: Run the Cross-Browser Suite in CI
Create .github/workflows/rtl-layouts.yml:
name: RTL layout tests
on:
pull_request:
push:
branches: [main]
jobs:
playwright:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.18.0
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:rtl
- uses: actions/upload-artifact@v4
if: always()
with:
name: rtl-playwright-report
path: |
playwright-report/
test-results/
retention-days: 14
npm ci enforces the committed lockfile, and playwright install --with-deps installs matching browsers plus Linux system dependencies. The job runs semantic, geometry, keyboard, overflow, and cross-browser tests. Chromium also evaluates the two committed screenshots.
Verify Step 8: Run CI=1 npm run test:rtl locally. Expect functional tests to pass in Chromium, Firefox, and WebKit, with the non-Chromium visual rows reported as skipped. After pushing a branch, download the HTML report artifact and confirm it contains results for all three projects.
Troubleshooting
Problem: dir="rtl" is present, but computed direction is ltr. -> Inspect ancestors and high-specificity rules for direction: ltr or an inline style. Place document direction on html, then scope intentional LTR islands such as email or code fields. Assert the container that owns the layout, not just a translated child.
Problem: Arabic text appears as empty boxes in screenshots. -> Install and preload a font with Arabic glyph coverage in the baseline environment. Wait on document.fonts.ready before the screenshot if the application loads web fonts. Keep the font package and Linux image pinned so baseline rendering does not drift.
Problem: The RTL screenshot fails by thousands of pixels. -> Compare viewport, browser project, operating system, and font versions before accepting a new image. Confirm the test did not capture a timestamp, caret, animation, skeleton, or unseeded record. Update only after reviewing the visual diff as an intended product change.
Problem: The page width passes, but an icon is clipped. -> Root scrollWidth cannot prove every child remains inside its component. Add bounding-box relationships for the icon and its card, then verify the icon's left and right edges fall within the card edges. Use a component screenshot for decorative details that do not affect root width.
Problem: Keyboard focus seems visually reversed. -> Check DOM order and remove positive tabindex values or CSS order used to fake mirroring. Direction-aware layout should place elements through logical CSS while focus continues through meaningful source order. Test both Tab and Shift+Tab around the affected controls.
Problem: Only WebKit fails a one-pixel geometry assertion. -> Log unrounded rectangles, computed styles, viewport size, and the Playwright browser revision. Prefer relational assertions over exact coordinates. If the product truly permits an engine-specific tolerance, isolate and document it instead of weakening every browser's contract.
Interview Questions and Answers
Q: What should an RTL Playwright test verify before taking a screenshot?
Verify the document lang and dir attributes, computed direction on the relevant container, localized accessible names, and one or two critical logical-position relationships. These checks distinguish semantic configuration errors from visual rendering changes. A screenshot then expands coverage to unanticipated details.
Q: Why are CSS logical properties important for testability?
Logical properties express intent as inline start, inline end, block start, and block end. One declaration can serve LTR and RTL modes, which lets a test compare the same component under two directions. Physical properties such as margin-left often produce a silent one-sided defect.
Q: Should an RTL layout reverse keyboard tab order?
No. Direction changes visual flow, but keyboard traversal should follow meaningful DOM order. Reversing it with positive tabindex or reordered markup can confuse keyboard and screen reader users. Assert focus transitions separately from x-coordinate relationships.
Q: How do geometry assertions complement visual regression?
Geometry assertions provide precise, low-noise contracts such as "the first nav item is farther right" or "document width is not larger than viewport width." Screenshots cover details you did not encode. When both fail, geometry often identifies the broken relationship while the image shows the user impact.
Q: How would you test mixed Arabic and Latin content?
Give the outer component its product direction, then test intentional islands such as emails, phone numbers, coupon codes, and URLs with realistic values. Assert readable ordering, focus behavior, and containment. Use dir="auto" only where the first strong character should legitimately select direction.
Q: Why run RTL functional tests in three browser engines?
The DOM contract is shared, but layout, font shaping, form controls, and focus behavior can differ across Chromium, Firefox, and WebKit. Cross-engine semantic and relational checks are usually stable. Restrict broad pixel baselines unless engine-specific screenshots provide enough value to justify their maintenance.
Best Practices
- Put
langanddiron the document root, then override direction only for deliberate bidi islands. - Prefer logical CSS properties and relative geometry comparisons over duplicated styles and fixed x coordinates.
- Test real Arabic strings, long addresses, numbers, currency, punctuation, email, and mixed-script content.
- Keep visual baselines deterministic by pinning browser, operating system, viewport, fonts, data, and animations.
- Use accessible roles and translated names for critical behavior, plus stable test IDs for structural measurements.
- Run at least one narrow viewport because mobile wrapping exposes many inline-size and minimum-width defects.
- Treat screenshot updates as reviewed product changes, never as an automatic repair for a red build.
- Store traces and failure screenshots, but keep the assertion message rich enough to diagnose overflow from plain logs.
Where To Go Next
Apply the same contracts to your production locale switcher and highest-risk components. Start with navigation, drawers, forms, tables, pagination, date pickers, toasts, and any control containing a directional icon.
Then deepen specific layers:
- Use localization testing fundamentals to expand the locale and bidi test matrix.
- Add Playwright accessibility automation for names, roles, focus, and automated rule checks.
- Extend the viewport set with responsive Playwright testing.
- Diagnose root-width failures with responsive overflow testing in Playwright.
- Maintain controlled image coverage with Playwright screenshot assertions.
- Organize locale and engine profiles using Playwright project configuration examples.
When you can explain why semantic direction, geometry, keyboard order, overflow, and visual evidence are separate signals, practice the explanation in the QA interview practice workspace.
Conclusion
To test right to left layouts playwright suites should begin with semantics, prove a few direction-sensitive geometry relationships, exercise keyboard behavior, stress narrow content, and reserve screenshots for visual evidence that structural assertions cannot express. The bilingual fixture keeps those signals isolated and reproducible.
Run the complete matrix before copying the pattern into your application. Replace fixture selectors and text with product equivalents, retain the relational checks, and add cases for the bidi content your users actually enter. That combination catches RTL failures early without turning every harmless pixel change into a release blocker.
Interview Questions and Answers
How would you design a Playwright test strategy for RTL layouts?
I would layer the strategy. First I would verify lang, dir, computed direction, and localized accessible names. Then I would add relative geometry, focus order, narrow-viewport overflow, and a small set of deterministic screenshots, with functional checks running across Chromium, Firefox, and WebKit.
Why should RTL tests compare relative coordinates instead of exact pixels?
Relative assertions encode the requirement, such as a label being to the right of its value in RTL. They tolerate harmless font, spacing, and container changes. Exact pixel coordinates are appropriate only when the product contract genuinely requires a fixed position.
What is the difference between dir and lang in an HTML RTL test?
lang identifies the content language for browsers and assistive technologies, while dir controls base text and layout direction. Arabic commonly uses lang="ar" with dir="rtl", but the properties answer different questions and should be asserted separately.
How do you test CSS logical properties with Playwright?
Render the same component in LTR and RTL, then read computed physical results or compare bounding boxes. For example, border-inline-start should become a left border in LTR and a right border in RTL. This proves browser resolution rather than merely checking the source stylesheet.
How do you detect horizontal overflow in an RTL page?
I compare document.documentElement.scrollWidth with window.innerWidth after the page reaches a known state, allowing only a documented rounding tolerance. I also record component scroll and client widths so the failure distinguishes root overflow from an intentional local scroller.
Would you maintain RTL screenshots for every browser?
Usually I would run semantic and geometry contracts across all supported engines and keep visual baselines in one pinned browser. I would add engine-specific images only for a known rendering risk because operating-system and font differences increase maintenance cost.
How do you validate mixed bidirectional content?
I use realistic Arabic text combined with emails, phone numbers, prices, IDs, URLs, and punctuation. I verify container direction, intentional LTR or auto-direction islands, readable visual ordering, keyboard access, and containment at narrow widths.
Frequently Asked Questions
How do I test right-to-left layout in Playwright?
Set or select the RTL locale, then assert the document dir and lang attributes, computed direction, and critical relative positions. Add keyboard, long-content overflow, and screenshot checks as separate tests so each failure identifies a specific contract.
Can Playwright emulate RTL without changing the application?
Playwright has no browser-context option that converts an LTR application into a correct RTL product. You can set dir through page content or application state for a fixture, but a real end-to-end test should activate the product's locale and direction path.
Should I use screenshots for every RTL assertion?
No. Use DOM attributes, computed styles, focus state, and relative geometry for known requirements because those checks produce focused diagnostics. Keep screenshots for visual details such as clipping, icon direction, spacing, and glyph rendering.
What viewport widths should RTL layout tests cover?
Choose widths around your actual breakpoints and include the narrowest supported phone. The tutorial uses 320, 390, and 768 CSS pixels as concrete stress points, but production coverage should reflect analytics, support policy, and component risk.
Why does Arabic text overflow when English does not?
Font fallback, glyph shaping, different word lengths, unbreakable mixed-script values, and flex or grid minimum sizes can expose width assumptions hidden by English. Test realistic long Arabic sentences plus separate IDs, URLs, emails, numbers, and currency cases.
Does RTL reverse keyboard tab order?
Direction alone should not reverse tab sequence. Keyboard focus follows DOM order unless tabindex or script changes it. Keep source order meaningful and test Tab and Shift+Tab independently from the visual position of controls.
How can I reduce flaky RTL screenshot tests?
Pin Playwright and the operating system, install the same fonts, fix the viewport and data, disable animations, hide the caret, and mask only truly dynamic regions. Generate and compare baselines in the same environment used by CI.
Related Guides
- How to Test Browser Permissions With Playwright TypeScript (2026)
- How to Test GraphQL Subscriptions with Playwright (2026)
- How to Test responsive layouts in Playwright (2026)
- How to Test Service Workers With Playwright (2026)
- How to Test WebAuthn Passkeys With Playwright TypeScript (2026)
- Automating Jira to test case with n8n (2026)