@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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. 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