@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
# Internationalization and Localization Testing
|
|
2
|
+
|
|
3
|
+
> **When to use**: Verifying your application works correctly across locales, languages, text directions, date/number formats, and timezones. Catches layout breaks, missing translations, and format errors before they reach international users.
|
|
4
|
+
> **Prerequisites**: [core/configuration.md](configuration.md), [core/locators.md](locators.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Set locale and timezone per context
|
|
10
|
+
const context = await browser.newContext({
|
|
11
|
+
locale: 'de-DE',
|
|
12
|
+
timezoneId: 'Europe/Berlin',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Or in playwright.config.ts for project-level locale testing
|
|
16
|
+
projects: [
|
|
17
|
+
{ name: 'english', use: { locale: 'en-US', timezoneId: 'America/New_York' } },
|
|
18
|
+
{ name: 'german', use: { locale: 'de-DE', timezoneId: 'Europe/Berlin' } },
|
|
19
|
+
{ name: 'arabic', use: { locale: 'ar-SA', timezoneId: 'Asia/Riyadh' } },
|
|
20
|
+
],
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Patterns
|
|
24
|
+
|
|
25
|
+
### Setting Browser Locale
|
|
26
|
+
|
|
27
|
+
**Use when**: Testing locale-dependent rendering -- date formats, number formatting, currency, sorting, and browser-level localization.
|
|
28
|
+
**Avoid when**: Your app does not use the browser locale and instead relies on a user preference stored server-side.
|
|
29
|
+
|
|
30
|
+
**TypeScript**
|
|
31
|
+
```typescript
|
|
32
|
+
import { test, expect } from '@playwright/test';
|
|
33
|
+
|
|
34
|
+
test.describe('locale-specific formatting', () => {
|
|
35
|
+
test('US locale formats dates as MM/DD/YYYY', async ({ browser }) => {
|
|
36
|
+
const context = await browser.newContext({ locale: 'en-US' });
|
|
37
|
+
const page = await context.newPage();
|
|
38
|
+
await page.goto('/dashboard');
|
|
39
|
+
|
|
40
|
+
// Verify date format matches US convention
|
|
41
|
+
await expect(page.getByTestId('last-updated')).toHaveText(/\d{1,2}\/\d{1,2}\/\d{4}/);
|
|
42
|
+
await context.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('German locale formats dates as DD.MM.YYYY', async ({ browser }) => {
|
|
46
|
+
const context = await browser.newContext({ locale: 'de-DE' });
|
|
47
|
+
const page = await context.newPage();
|
|
48
|
+
await page.goto('/dashboard');
|
|
49
|
+
|
|
50
|
+
await expect(page.getByTestId('last-updated')).toHaveText(/\d{1,2}\.\d{1,2}\.\d{4}/);
|
|
51
|
+
await context.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Japanese locale formats numbers with commas', async ({ browser }) => {
|
|
55
|
+
const context = await browser.newContext({ locale: 'ja-JP' });
|
|
56
|
+
const page = await context.newPage();
|
|
57
|
+
await page.goto('/pricing');
|
|
58
|
+
|
|
59
|
+
// Japanese yen: no decimal places, uses comma grouping
|
|
60
|
+
await expect(page.getByTestId('price')).toHaveText(/[\d,]+円/);
|
|
61
|
+
await context.close();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**JavaScript**
|
|
67
|
+
```javascript
|
|
68
|
+
const { test, expect } = require('@playwright/test');
|
|
69
|
+
|
|
70
|
+
test('German locale formats dates as DD.MM.YYYY', async ({ browser }) => {
|
|
71
|
+
const context = await browser.newContext({ locale: 'de-DE' });
|
|
72
|
+
const page = await context.newPage();
|
|
73
|
+
await page.goto('/dashboard');
|
|
74
|
+
|
|
75
|
+
await expect(page.getByTestId('last-updated')).toHaveText(/\d{1,2}\.\d{1,2}\.\d{4}/);
|
|
76
|
+
await context.close();
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Multi-Locale Project Configuration
|
|
81
|
+
|
|
82
|
+
**Use when**: Running the full test suite across multiple locales in CI.
|
|
83
|
+
**Avoid when**: You only need to test a single locale or the app does not vary by locale.
|
|
84
|
+
|
|
85
|
+
**TypeScript**
|
|
86
|
+
```typescript
|
|
87
|
+
// playwright.config.ts
|
|
88
|
+
import { defineConfig } from '@playwright/test';
|
|
89
|
+
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
projects: [
|
|
92
|
+
{
|
|
93
|
+
name: 'en-US',
|
|
94
|
+
use: {
|
|
95
|
+
locale: 'en-US',
|
|
96
|
+
timezoneId: 'America/New_York',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'de-DE',
|
|
101
|
+
use: {
|
|
102
|
+
locale: 'de-DE',
|
|
103
|
+
timezoneId: 'Europe/Berlin',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'ar-SA',
|
|
108
|
+
use: {
|
|
109
|
+
locale: 'ar-SA',
|
|
110
|
+
timezoneId: 'Asia/Riyadh',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'ja-JP',
|
|
115
|
+
use: {
|
|
116
|
+
locale: 'ja-JP',
|
|
117
|
+
timezoneId: 'Asia/Tokyo',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**JavaScript**
|
|
125
|
+
```javascript
|
|
126
|
+
// playwright.config.js
|
|
127
|
+
const { defineConfig } = require('@playwright/test');
|
|
128
|
+
|
|
129
|
+
module.exports = defineConfig({
|
|
130
|
+
projects: [
|
|
131
|
+
{ name: 'en-US', use: { locale: 'en-US', timezoneId: 'America/New_York' } },
|
|
132
|
+
{ name: 'de-DE', use: { locale: 'de-DE', timezoneId: 'Europe/Berlin' } },
|
|
133
|
+
{ name: 'ar-SA', use: { locale: 'ar-SA', timezoneId: 'Asia/Riyadh' } },
|
|
134
|
+
{ name: 'ja-JP', use: { locale: 'ja-JP', timezoneId: 'Asia/Tokyo' } },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### RTL Layout Testing
|
|
140
|
+
|
|
141
|
+
**Use when**: Your app supports right-to-left languages (Arabic, Hebrew, Persian, Urdu) and you need to verify layout direction, text alignment, and mirrored UI.
|
|
142
|
+
**Avoid when**: Your app has no RTL support.
|
|
143
|
+
|
|
144
|
+
**TypeScript**
|
|
145
|
+
```typescript
|
|
146
|
+
import { test, expect } from '@playwright/test';
|
|
147
|
+
|
|
148
|
+
test.describe('RTL layout', () => {
|
|
149
|
+
test('Arabic locale renders RTL layout', async ({ browser }) => {
|
|
150
|
+
const context = await browser.newContext({ locale: 'ar-SA' });
|
|
151
|
+
const page = await context.newPage();
|
|
152
|
+
await page.goto('/');
|
|
153
|
+
|
|
154
|
+
// Verify the document direction
|
|
155
|
+
const dir = await page.getAttribute('html', 'dir');
|
|
156
|
+
expect(dir).toBe('rtl');
|
|
157
|
+
|
|
158
|
+
// Verify navigation is right-aligned
|
|
159
|
+
const nav = page.getByRole('navigation', { name: 'Main' });
|
|
160
|
+
const navBox = await nav.boundingBox();
|
|
161
|
+
const viewportWidth = page.viewportSize()!.width;
|
|
162
|
+
// Nav should start from the right side
|
|
163
|
+
expect(navBox!.x + navBox!.width).toBeGreaterThan(viewportWidth * 0.5);
|
|
164
|
+
|
|
165
|
+
// Verify text alignment
|
|
166
|
+
const heading = page.getByRole('heading', { level: 1 });
|
|
167
|
+
const textAlign = await heading.evaluate((el) =>
|
|
168
|
+
window.getComputedStyle(el).textAlign
|
|
169
|
+
);
|
|
170
|
+
expect(textAlign).toMatch(/right|start/);
|
|
171
|
+
|
|
172
|
+
await context.close();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('RTL layout does not cause horizontal overflow', async ({ browser }) => {
|
|
176
|
+
const context = await browser.newContext({ locale: 'ar-SA' });
|
|
177
|
+
const page = await context.newPage();
|
|
178
|
+
await page.goto('/dashboard');
|
|
179
|
+
|
|
180
|
+
// Check for horizontal scrollbar (content overflow)
|
|
181
|
+
const hasHorizontalOverflow = await page.evaluate(() => {
|
|
182
|
+
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
183
|
+
});
|
|
184
|
+
expect(hasHorizontalOverflow).toBe(false);
|
|
185
|
+
|
|
186
|
+
await context.close();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('icons and directional elements are mirrored in RTL', async ({ browser }) => {
|
|
190
|
+
const context = await browser.newContext({ locale: 'ar-SA' });
|
|
191
|
+
const page = await context.newPage();
|
|
192
|
+
await page.goto('/dashboard');
|
|
193
|
+
|
|
194
|
+
// Back arrow should point right in RTL
|
|
195
|
+
const backButton = page.getByRole('button', { name: /back|رجوع/i });
|
|
196
|
+
const transform = await backButton.evaluate((el) =>
|
|
197
|
+
window.getComputedStyle(el).transform
|
|
198
|
+
);
|
|
199
|
+
// CSS transform for horizontal flip: matrix(-1, 0, 0, 1, 0, 0) or scaleX(-1)
|
|
200
|
+
// Or check the logical property direction
|
|
201
|
+
expect(transform).not.toBe('none');
|
|
202
|
+
|
|
203
|
+
await context.close();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**JavaScript**
|
|
209
|
+
```javascript
|
|
210
|
+
const { test, expect } = require('@playwright/test');
|
|
211
|
+
|
|
212
|
+
test('Arabic locale renders RTL layout', async ({ browser }) => {
|
|
213
|
+
const context = await browser.newContext({ locale: 'ar-SA' });
|
|
214
|
+
const page = await context.newPage();
|
|
215
|
+
await page.goto('/');
|
|
216
|
+
|
|
217
|
+
const dir = await page.getAttribute('html', 'dir');
|
|
218
|
+
expect(dir).toBe('rtl');
|
|
219
|
+
|
|
220
|
+
const hasHorizontalOverflow = await page.evaluate(() =>
|
|
221
|
+
document.documentElement.scrollWidth > document.documentElement.clientWidth
|
|
222
|
+
);
|
|
223
|
+
expect(hasHorizontalOverflow).toBe(false);
|
|
224
|
+
|
|
225
|
+
await context.close();
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Date, Number, and Currency Format Verification
|
|
230
|
+
|
|
231
|
+
**Use when**: Your app uses `Intl.DateTimeFormat`, `Intl.NumberFormat`, or similar locale-sensitive APIs.
|
|
232
|
+
**Avoid when**: Formats are hardcoded and do not depend on locale.
|
|
233
|
+
|
|
234
|
+
**TypeScript**
|
|
235
|
+
```typescript
|
|
236
|
+
import { test, expect } from '@playwright/test';
|
|
237
|
+
|
|
238
|
+
const FORMAT_EXPECTATIONS = {
|
|
239
|
+
'en-US': {
|
|
240
|
+
date: /\d{1,2}\/\d{1,2}\/\d{4}/, // 1/15/2025
|
|
241
|
+
number: /1,234,567\.89/, // 1,234,567.89
|
|
242
|
+
currency: /\$[\d,]+\.\d{2}/, // $1,234.56
|
|
243
|
+
},
|
|
244
|
+
'de-DE': {
|
|
245
|
+
date: /\d{1,2}\.\d{1,2}\.\d{4}/, // 15.1.2025
|
|
246
|
+
number: /1\.234\.567,89/, // 1.234.567,89
|
|
247
|
+
currency: /[\d.,]+\s?€/, // 1.234,56 €
|
|
248
|
+
},
|
|
249
|
+
'ja-JP': {
|
|
250
|
+
date: /\d{4}\/\d{1,2}\/\d{1,2}/, // 2025/1/15
|
|
251
|
+
number: /1,234,567\.89/, // 1,234,567.89
|
|
252
|
+
currency: /[¥¥][\d,]+/, // ¥1,235
|
|
253
|
+
},
|
|
254
|
+
} as const;
|
|
255
|
+
|
|
256
|
+
for (const [locale, expected] of Object.entries(FORMAT_EXPECTATIONS)) {
|
|
257
|
+
test.describe(`${locale} formatting`, () => {
|
|
258
|
+
test(`dates match ${locale} format`, async ({ browser }) => {
|
|
259
|
+
const context = await browser.newContext({ locale });
|
|
260
|
+
const page = await context.newPage();
|
|
261
|
+
await page.goto('/account');
|
|
262
|
+
|
|
263
|
+
const dateText = await page.getByTestId('member-since').textContent();
|
|
264
|
+
expect(dateText).toMatch(expected.date);
|
|
265
|
+
await context.close();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test(`currency matches ${locale} format`, async ({ browser }) => {
|
|
269
|
+
const context = await browser.newContext({ locale });
|
|
270
|
+
const page = await context.newPage();
|
|
271
|
+
await page.goto('/pricing');
|
|
272
|
+
|
|
273
|
+
const priceText = await page.getByTestId('monthly-price').textContent();
|
|
274
|
+
expect(priceText).toMatch(expected.currency);
|
|
275
|
+
await context.close();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**JavaScript**
|
|
282
|
+
```javascript
|
|
283
|
+
const { test, expect } = require('@playwright/test');
|
|
284
|
+
|
|
285
|
+
const locales = [
|
|
286
|
+
{ locale: 'en-US', datePattern: /\d{1,2}\/\d{1,2}\/\d{4}/, currencyPattern: /\$[\d,]+\.\d{2}/ },
|
|
287
|
+
{ locale: 'de-DE', datePattern: /\d{1,2}\.\d{1,2}\.\d{4}/, currencyPattern: /[\d.,]+\s?€/ },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
for (const { locale, datePattern, currencyPattern } of locales) {
|
|
291
|
+
test(`${locale} date format`, async ({ browser }) => {
|
|
292
|
+
const context = await browser.newContext({ locale });
|
|
293
|
+
const page = await context.newPage();
|
|
294
|
+
await page.goto('/account');
|
|
295
|
+
|
|
296
|
+
const dateText = await page.getByTestId('member-since').textContent();
|
|
297
|
+
expect(dateText).toMatch(datePattern);
|
|
298
|
+
await context.close();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Language Switcher Testing
|
|
304
|
+
|
|
305
|
+
**Use when**: Your app has an in-app language selector that changes the UI language without depending on browser locale.
|
|
306
|
+
**Avoid when**: Language is determined purely by browser locale with no user override.
|
|
307
|
+
|
|
308
|
+
**TypeScript**
|
|
309
|
+
```typescript
|
|
310
|
+
import { test, expect } from '@playwright/test';
|
|
311
|
+
|
|
312
|
+
test('language switcher changes UI language', async ({ page }) => {
|
|
313
|
+
await page.goto('/');
|
|
314
|
+
|
|
315
|
+
// Default language
|
|
316
|
+
await expect(page.getByRole('heading', { level: 1 })).toContainText('Welcome');
|
|
317
|
+
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
|
318
|
+
|
|
319
|
+
// Switch to French
|
|
320
|
+
await page.getByRole('combobox', { name: /language|langue/i }).selectOption('fr');
|
|
321
|
+
|
|
322
|
+
// UI should update to French
|
|
323
|
+
await expect(page.getByRole('heading', { level: 1 })).toContainText('Bienvenue');
|
|
324
|
+
await expect(page.getByRole('button', { name: 'Se connecter' })).toBeVisible();
|
|
325
|
+
|
|
326
|
+
// Verify language persists after navigation
|
|
327
|
+
await page.getByRole('link', { name: /about|à propos/i }).click();
|
|
328
|
+
await expect(page.getByRole('heading', { level: 1 })).not.toContainText('About');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('language preference persists across sessions', async ({ page, context }) => {
|
|
332
|
+
await page.goto('/');
|
|
333
|
+
await page.getByRole('combobox', { name: /language/i }).selectOption('es');
|
|
334
|
+
await expect(page.getByRole('button', { name: 'Iniciar sesión' })).toBeVisible();
|
|
335
|
+
|
|
336
|
+
// Reload page — language should persist (cookie/localStorage)
|
|
337
|
+
await page.reload();
|
|
338
|
+
await expect(page.getByRole('button', { name: 'Iniciar sesión' })).toBeVisible();
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**JavaScript**
|
|
343
|
+
```javascript
|
|
344
|
+
const { test, expect } = require('@playwright/test');
|
|
345
|
+
|
|
346
|
+
test('language switcher changes UI language', async ({ page }) => {
|
|
347
|
+
await page.goto('/');
|
|
348
|
+
|
|
349
|
+
await expect(page.getByRole('heading', { level: 1 })).toContainText('Welcome');
|
|
350
|
+
|
|
351
|
+
await page.getByRole('combobox', { name: /language/i }).selectOption('fr');
|
|
352
|
+
|
|
353
|
+
await expect(page.getByRole('heading', { level: 1 })).toContainText('Bienvenue');
|
|
354
|
+
await expect(page.getByRole('button', { name: 'Se connecter' })).toBeVisible();
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Translation Completeness Checks
|
|
359
|
+
|
|
360
|
+
**Use when**: Verifying that all visible UI strings are translated and no fallback keys leak into the UI.
|
|
361
|
+
**Avoid when**: You have a build-time translation validation step that already catches missing keys.
|
|
362
|
+
|
|
363
|
+
**TypeScript**
|
|
364
|
+
```typescript
|
|
365
|
+
import { test, expect } from '@playwright/test';
|
|
366
|
+
|
|
367
|
+
const LANGUAGES = ['en', 'fr', 'de', 'es', 'ja'];
|
|
368
|
+
const PAGES_TO_CHECK = ['/', '/login', '/dashboard', '/settings', '/pricing'];
|
|
369
|
+
|
|
370
|
+
for (const lang of LANGUAGES) {
|
|
371
|
+
test.describe(`${lang} translation completeness`, () => {
|
|
372
|
+
for (const pagePath of PAGES_TO_CHECK) {
|
|
373
|
+
test(`no missing translations on ${pagePath}`, async ({ page }) => {
|
|
374
|
+
// Set language via URL param, cookie, or language switcher
|
|
375
|
+
await page.goto(`${pagePath}?lang=${lang}`);
|
|
376
|
+
|
|
377
|
+
// Check for common translation key leak patterns
|
|
378
|
+
const pageText = await page.textContent('body');
|
|
379
|
+
|
|
380
|
+
// Translation keys typically look like: key.subkey, UPPER_SNAKE_CASE, or {{key}}
|
|
381
|
+
expect(pageText).not.toMatch(/\b[a-z]+\.[a-z]+\.[a-z]+\b/); // dot.notation.keys
|
|
382
|
+
expect(pageText).not.toContain('{{'); // Unresolved templates
|
|
383
|
+
expect(pageText).not.toContain('}}');
|
|
384
|
+
expect(pageText).not.toMatch(/\bTODO\b/i); // Placeholder text
|
|
385
|
+
|
|
386
|
+
// Check for untranslated English text when in non-English locale
|
|
387
|
+
if (lang !== 'en') {
|
|
388
|
+
// These common English strings should be translated
|
|
389
|
+
const untranslated = ['Sign in', 'Log out', 'Settings', 'Dashboard', 'Submit'];
|
|
390
|
+
for (const text of untranslated) {
|
|
391
|
+
const count = await page.getByText(text, { exact: true }).count();
|
|
392
|
+
// Allow 0 matches (element not on page) but flag exact English matches
|
|
393
|
+
if (count > 0) {
|
|
394
|
+
console.warn(`Possible untranslated text "${text}" found on ${pagePath} for ${lang}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**JavaScript**
|
|
405
|
+
```javascript
|
|
406
|
+
const { test, expect } = require('@playwright/test');
|
|
407
|
+
|
|
408
|
+
const LANGUAGES = ['en', 'fr', 'de'];
|
|
409
|
+
const PAGES = ['/', '/login', '/dashboard'];
|
|
410
|
+
|
|
411
|
+
for (const lang of LANGUAGES) {
|
|
412
|
+
for (const pagePath of PAGES) {
|
|
413
|
+
test(`${lang}: no missing translations on ${pagePath}`, async ({ page }) => {
|
|
414
|
+
await page.goto(`${pagePath}?lang=${lang}`);
|
|
415
|
+
|
|
416
|
+
const pageText = await page.textContent('body');
|
|
417
|
+
expect(pageText).not.toMatch(/\b[a-z]+\.[a-z]+\.[a-z]+\b/);
|
|
418
|
+
expect(pageText).not.toContain('{{');
|
|
419
|
+
expect(pageText).not.toContain('}}');
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Timezone Testing
|
|
426
|
+
|
|
427
|
+
**Use when**: Your app displays time-sensitive data (event times, deadlines, scheduling) and you need to verify correct timezone rendering.
|
|
428
|
+
**Avoid when**: All times are displayed in UTC with no local conversion.
|
|
429
|
+
|
|
430
|
+
**TypeScript**
|
|
431
|
+
```typescript
|
|
432
|
+
import { test, expect } from '@playwright/test';
|
|
433
|
+
|
|
434
|
+
test.describe('timezone rendering', () => {
|
|
435
|
+
test('event times adjust to user timezone', async ({ browser }) => {
|
|
436
|
+
// Event stored as 2025-03-15T14:00:00Z (2 PM UTC)
|
|
437
|
+
const contextNY = await browser.newContext({
|
|
438
|
+
locale: 'en-US',
|
|
439
|
+
timezoneId: 'America/New_York', // UTC-5 in March (EST)
|
|
440
|
+
});
|
|
441
|
+
const pageNY = await contextNY.newPage();
|
|
442
|
+
await pageNY.goto('/events/123');
|
|
443
|
+
// Should display 9:00 AM
|
|
444
|
+
await expect(pageNY.getByTestId('event-time')).toContainText('9:00 AM');
|
|
445
|
+
await contextNY.close();
|
|
446
|
+
|
|
447
|
+
const contextTokyo = await browser.newContext({
|
|
448
|
+
locale: 'en-US',
|
|
449
|
+
timezoneId: 'Asia/Tokyo', // UTC+9
|
|
450
|
+
});
|
|
451
|
+
const pageTokyo = await contextTokyo.newPage();
|
|
452
|
+
await pageTokyo.goto('/events/123');
|
|
453
|
+
// Should display 11:00 PM
|
|
454
|
+
await expect(pageTokyo.getByTestId('event-time')).toContainText('11:00 PM');
|
|
455
|
+
await contextTokyo.close();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('deadline displays correctly across DST boundary', async ({ browser }) => {
|
|
459
|
+
const context = await browser.newContext({
|
|
460
|
+
locale: 'en-US',
|
|
461
|
+
timezoneId: 'America/Los_Angeles',
|
|
462
|
+
});
|
|
463
|
+
const page = await context.newPage();
|
|
464
|
+
|
|
465
|
+
// Test a deadline that crosses DST (March second Sunday)
|
|
466
|
+
await page.goto('/tasks?deadline=2025-03-10T07:00:00Z');
|
|
467
|
+
// Before DST: UTC-8, so 11 PM on March 9
|
|
468
|
+
await expect(page.getByTestId('deadline')).toContainText('Mar 9');
|
|
469
|
+
|
|
470
|
+
await context.close();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**JavaScript**
|
|
476
|
+
```javascript
|
|
477
|
+
const { test, expect } = require('@playwright/test');
|
|
478
|
+
|
|
479
|
+
test('event times adjust to user timezone', async ({ browser }) => {
|
|
480
|
+
const context = await browser.newContext({
|
|
481
|
+
locale: 'en-US',
|
|
482
|
+
timezoneId: 'America/New_York',
|
|
483
|
+
});
|
|
484
|
+
const page = await context.newPage();
|
|
485
|
+
await page.goto('/events/123');
|
|
486
|
+
await expect(page.getByTestId('event-time')).toContainText('9:00 AM');
|
|
487
|
+
await context.close();
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Multi-Language Screenshot Comparison
|
|
492
|
+
|
|
493
|
+
**Use when**: Catching visual layout regressions caused by text expansion, RTL mirroring, or font rendering differences across locales.
|
|
494
|
+
**Avoid when**: Visual regression testing is handled separately and locale is not a layout risk.
|
|
495
|
+
|
|
496
|
+
**TypeScript**
|
|
497
|
+
```typescript
|
|
498
|
+
import { test, expect } from '@playwright/test';
|
|
499
|
+
|
|
500
|
+
const LOCALES_TO_SCREENSHOT = [
|
|
501
|
+
{ locale: 'en-US', name: 'english' },
|
|
502
|
+
{ locale: 'de-DE', name: 'german' }, // German text is ~30% longer than English
|
|
503
|
+
{ locale: 'ja-JP', name: 'japanese' }, // CJK characters, different font metrics
|
|
504
|
+
{ locale: 'ar-SA', name: 'arabic' }, // RTL layout
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
for (const { locale, name } of LOCALES_TO_SCREENSHOT) {
|
|
508
|
+
test(`visual snapshot for ${name} (${locale})`, async ({ browser }) => {
|
|
509
|
+
const context = await browser.newContext({
|
|
510
|
+
locale,
|
|
511
|
+
viewport: { width: 1280, height: 720 },
|
|
512
|
+
});
|
|
513
|
+
const page = await context.newPage();
|
|
514
|
+
await page.goto('/');
|
|
515
|
+
|
|
516
|
+
// Wait for fonts and images to load
|
|
517
|
+
await page.waitForLoadState('networkidle');
|
|
518
|
+
|
|
519
|
+
await expect(page).toHaveScreenshot(`homepage-${name}.png`, {
|
|
520
|
+
maxDiffPixelRatio: 0.02, // Allow 2% pixel difference for font rendering
|
|
521
|
+
fullPage: true,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await context.close();
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
test('long German text does not overflow buttons', async ({ browser }) => {
|
|
529
|
+
const context = await browser.newContext({ locale: 'de-DE' });
|
|
530
|
+
const page = await context.newPage();
|
|
531
|
+
await page.goto('/dashboard');
|
|
532
|
+
|
|
533
|
+
// Check that no button has overflowing text
|
|
534
|
+
const buttons = page.getByRole('button');
|
|
535
|
+
const count = await buttons.count();
|
|
536
|
+
for (let i = 0; i < count; i++) {
|
|
537
|
+
const button = buttons.nth(i);
|
|
538
|
+
const overflow = await button.evaluate((el) => {
|
|
539
|
+
const style = window.getComputedStyle(el);
|
|
540
|
+
return el.scrollWidth > el.clientWidth && style.overflow !== 'hidden';
|
|
541
|
+
});
|
|
542
|
+
if (overflow) {
|
|
543
|
+
const text = await button.textContent();
|
|
544
|
+
expect(overflow, `Button "${text}" overflows in German`).toBe(false);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
await context.close();
|
|
549
|
+
});
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**JavaScript**
|
|
553
|
+
```javascript
|
|
554
|
+
const { test, expect } = require('@playwright/test');
|
|
555
|
+
|
|
556
|
+
const LOCALES = [
|
|
557
|
+
{ locale: 'en-US', name: 'english' },
|
|
558
|
+
{ locale: 'de-DE', name: 'german' },
|
|
559
|
+
{ locale: 'ar-SA', name: 'arabic' },
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
for (const { locale, name } of LOCALES) {
|
|
563
|
+
test(`visual snapshot for ${name}`, async ({ browser }) => {
|
|
564
|
+
const context = await browser.newContext({ locale, viewport: { width: 1280, height: 720 } });
|
|
565
|
+
const page = await context.newPage();
|
|
566
|
+
await page.goto('/');
|
|
567
|
+
await page.waitForLoadState('networkidle');
|
|
568
|
+
|
|
569
|
+
await expect(page).toHaveScreenshot(`homepage-${name}.png`, {
|
|
570
|
+
maxDiffPixelRatio: 0.02,
|
|
571
|
+
fullPage: true,
|
|
572
|
+
});
|
|
573
|
+
await context.close();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Decision Guide
|
|
579
|
+
|
|
580
|
+
| Scenario | Approach | Why |
|
|
581
|
+
|---|---|---|
|
|
582
|
+
| Test locale-dependent formatting | Set `locale` in browser context | Playwright sets `navigator.language` and affects `Intl` APIs |
|
|
583
|
+
| Test app language switcher | Interact with the switcher UI directly | Tests the actual user workflow, not just browser locale |
|
|
584
|
+
| Test timezone rendering | Set `timezoneId` in browser context | Overrides `Date` and `Intl.DateTimeFormat` timezone |
|
|
585
|
+
| Catch text overflow from long translations | Visual regression + bounding box checks | German/Finnish text is 30-40% longer than English |
|
|
586
|
+
| Verify RTL layout | Set Arabic/Hebrew locale + assert `dir="rtl"` | Tests both browser signal and app response |
|
|
587
|
+
| Catch missing translations | Scan page text for key patterns (`{{`, dot notation) | Catches build/deploy issues where translation files are missing |
|
|
588
|
+
| Compare layouts across locales | `toHaveScreenshot` per locale with project-based config | Captures visual differences automatically |
|
|
589
|
+
| Test DST edge cases | Set specific `timezoneId` + known date boundaries | DST boundaries cause the most timezone bugs |
|
|
590
|
+
|
|
591
|
+
## Anti-Patterns
|
|
592
|
+
|
|
593
|
+
| Don't Do This | Problem | Do This Instead |
|
|
594
|
+
|---|---|---|
|
|
595
|
+
| Hardcoding translated text in assertions | Breaks when translations change | Use `getByRole` with `name` regex, or test IDs for locale-independent selection |
|
|
596
|
+
| Testing only `en-US` | Misses RTL, text overflow, and format bugs | Test at least one LTR, one RTL, and one CJK locale |
|
|
597
|
+
| Setting `locale` on the page instead of context | `locale` is a context-level option, not page-level | Set locale when creating the context or in project config |
|
|
598
|
+
| Ignoring text expansion for German/Finnish | Buttons and labels overflow in longer languages | Use visual regression or bounding box assertions |
|
|
599
|
+
| Using `toHaveScreenshot` without `maxDiffPixelRatio` | Font rendering differs slightly across OS/CI | Set `maxDiffPixelRatio: 0.02` or higher for cross-platform tolerance |
|
|
600
|
+
| Testing timezone only in UTC | Masks timezone conversion bugs | Test with at least 3 timezones: UTC-negative, UTC, UTC-positive |
|
|
601
|
+
| Relying on browser locale for app language | Many apps use server-side or cookie-based language | Test via the app's own language switching mechanism |
|
|
602
|
+
| Not testing DST boundaries | Time displayed can be off by 1 hour at transitions | Test dates near DST transitions for your target timezones |
|
|
603
|
+
|
|
604
|
+
## Troubleshooting
|
|
605
|
+
|
|
606
|
+
| Symptom | Likely Cause | Fix |
|
|
607
|
+
|---|---|---|
|
|
608
|
+
| `locale` option has no effect | App ignores `navigator.language` and uses server-side locale | Set locale through the app's own mechanism (cookie, URL param, API) |
|
|
609
|
+
| Date format does not change with locale | App hardcodes date format instead of using `Intl.DateTimeFormat` | This is an app bug to fix, not a test issue |
|
|
610
|
+
| RTL page still renders LTR | App checks locale but does not set `dir="rtl"` | Verify the app's RTL detection logic; may need to set `Accept-Language` header |
|
|
611
|
+
| Visual screenshots fail across OS | Font rendering differs between macOS, Linux, Windows | Run visual tests in Docker for consistency, or increase `maxDiffPixelRatio` |
|
|
612
|
+
| `timezoneId` does not affect page | App uses server time, not client `Date` | Timezone testing only works for client-side date rendering |
|
|
613
|
+
| Translation key appears briefly then disappears | Translation files load asynchronously | Wait for `networkidle` or a specific translated element before asserting |
|
|
614
|
+
| Character encoding issues (garbled text) | Incorrect charset in HTML or missing font | Verify `<meta charset="utf-8">` and that CJK/Arabic fonts are available |
|
|
615
|
+
|
|
616
|
+
## Related
|
|
617
|
+
|
|
618
|
+
- [core/locators.md](locators.md) -- locator strategies that work across locales
|
|
619
|
+
- [core/configuration.md](configuration.md) -- project-level locale and timezone configuration
|
|
620
|
+
- [core/visual-regression.md](visual-regression.md) -- screenshot comparison fundamentals
|
|
621
|
+
- [core/clock-and-time-mocking.md](clock-and-time-mocking.md) -- mocking time for date-dependent testing
|
|
622
|
+
- [ci/docker-and-containers.md](../ci/docker-and-containers.md) -- consistent font rendering in CI with Docker
|