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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,7 @@ dmypy.json

# Pyre type checker
.pyre/

# Playwright
playwright-report/
test-results/
141 changes: 141 additions & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { test, expect } from "@playwright/test";

/**
* Smoke tests for Wordle Global.
*
* These tests verify core functionality works end-to-end.
* They should be fast and reliable.
*/

test.describe("Homepage", () => {
test("loads and displays title", async ({ page }) => {
await page.goto("/");

// Page title should be set
await expect(page).toHaveTitle(/Wordle/i);

// Header should be visible
const header = page.locator("h1");
await expect(header).toContainText("WORDLE");
});

test("has language data loaded", async ({ page }) => {
await page.goto("/");

// Wait for page to fully load
await page.waitForLoadState("domcontentloaded");

// Check that window.languages is populated (backend data - it's a dict)
const languageCount = await page.evaluate(() => {
const langs = (window as any).languages;
return langs ? Object.keys(langs).length : 0;
});
expect(languageCount).toBeGreaterThan(50);
});
});

test.describe("Game Page", () => {
test("loads English game page", async ({ page }) => {
await page.goto("/en");

// Page should have title
await expect(page).toHaveTitle(/Wordle/i);

// Should have game data loaded
const todaysWord = await page.evaluate(() => {
return (window as any).todays_word;
});
expect(todaysWord).toBeTruthy();
expect(todaysWord.length).toBe(5);
});

test("has keyboard data", async ({ page }) => {
await page.goto("/en");

// Check keyboard is in page HTML (server-rendered)
const keyboardHtml = await page.locator('[data-char="q"]').count();
expect(keyboardHtml).toBeGreaterThan(0);
});

test("has correct character set", async ({ page }) => {
await page.goto("/en");

const characters = await page.evaluate(() => {
return (window as any).characters;
});
expect(characters).toContain("a");
expect(characters).toContain("z");
});
});

test.describe("RTL Language", () => {
test("Hebrew game loads", async ({ page }) => {
await page.goto("/he");

// Should have Hebrew word data
const todaysWord = await page.evaluate(() => {
return (window as any).todays_word;
});
expect(todaysWord).toBeTruthy();
expect(todaysWord.length).toBe(5);
});

test("Hebrew has Hebrew keyboard keys", async ({ page }) => {
await page.goto("/he");

// Hebrew keyboard should have Hebrew letters in HTML
const hebrewKeyCount = await page
.locator('[data-char="א"], [data-char="ב"], [data-char="ג"]')
.count();
expect(hebrewKeyCount).toBeGreaterThan(0);
});
});

test.describe("Dark Mode", () => {
test("respects system dark mode preference", async ({ page }) => {
// Emulate dark mode preference
await page.emulateMedia({ colorScheme: "dark" });
await page.goto("/en");

// HTML element should have dark class (set by inline script)
const html = page.locator("html");
await expect(html).toHaveClass(/dark/);
});

test("light mode by default", async ({ page }) => {
await page.emulateMedia({ colorScheme: "light" });
await page.goto("/en");

// HTML should not have dark class
const htmlClass = await page.locator("html").getAttribute("class");
expect(htmlClass).not.toMatch(/dark/);
});
});

test.describe("Mobile Viewport", () => {
test("page loads on mobile", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/en");

// Keyboard keys should be present
const keyCount = await page.locator('[data-char="q"]').count();
expect(keyCount).toBeGreaterThan(0);
});
});

test.describe("Multiple Languages", () => {
const languages = ["en", "es", "fr", "de", "it", "ru", "he", "ar"];

for (const lang of languages) {
test(`${lang} game page loads`, async ({ page }) => {
await page.goto(`/${lang}`);

// Should have valid game data
const todaysWord = await page.evaluate(() => {
return (window as any).todays_word;
});
expect(todaysWord).toBeTruthy();
expect(todaysWord.length).toBe(5);
});
}
});
155 changes: 155 additions & 0 deletions frontend/src/__tests__/diacritics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { describe, it, expect } from 'vitest';
import {
buildNormalizeMap,
buildNormalizedWordMap,
normalizeWord,
normalizeChar,
charsMatch,
} from '../diacritics';

describe('Diacritics Module', () => {
// German diacritic configuration
const germanConfig = {
diacritic_map: {
a: ['ä'],
o: ['ö'],
u: ['ü'],
ss: ['ß'],
},
};

// Portuguese diacritic configuration
const portugueseConfig = {
diacritic_map: {
a: ['á', 'à', 'â', 'ã'],
e: ['é', 'ê'],
i: ['í'],
o: ['ó', 'ô', 'õ'],
u: ['ú'],
c: ['ç'],
},
};

// Norwegian - NO diacritic_map (æ, ø, å are distinct letters)
const norwegianConfig = {};

describe('buildNormalizeMap', () => {
it('should build reverse lookup map from diacritic config', () => {
const map = buildNormalizeMap(germanConfig);
expect(map.get('ä')).toBe('a');
expect(map.get('ö')).toBe('o');
expect(map.get('ü')).toBe('u');
expect(map.get('ß')).toBe('ss');
});

it('should return empty map when no diacritic_map present', () => {
const map = buildNormalizeMap(norwegianConfig);
expect(map.size).toBe(0);
});

it('should handle multiple variants per base character', () => {
const map = buildNormalizeMap(portugueseConfig);
expect(map.get('á')).toBe('a');
expect(map.get('à')).toBe('a');
expect(map.get('â')).toBe('a');
expect(map.get('ã')).toBe('a');
});
});

describe('normalizeChar', () => {
it('should normalize diacritic characters', () => {
const map = buildNormalizeMap(germanConfig);
expect(normalizeChar('ä', map)).toBe('a');
expect(normalizeChar('ö', map)).toBe('o');
});

it('should return unchanged for non-diacritic characters', () => {
const map = buildNormalizeMap(germanConfig);
expect(normalizeChar('a', map)).toBe('a');
expect(normalizeChar('b', map)).toBe('b');
});

it('should return unchanged when no normalization configured', () => {
const map = buildNormalizeMap(norwegianConfig);
expect(normalizeChar('æ', map)).toBe('æ');
expect(normalizeChar('ø', map)).toBe('ø');
expect(normalizeChar('å', map)).toBe('å');
});
});

describe('normalizeWord', () => {
it('should normalize entire word with German config', () => {
const map = buildNormalizeMap(germanConfig);
expect(normalizeWord('börde', map)).toBe('borde');
expect(normalizeWord('grün', map)).toBe('grun');
expect(normalizeWord('größe', map)).toBe('grosse');
});

it('should normalize entire word with Portuguese config', () => {
const map = buildNormalizeMap(portugueseConfig);
expect(normalizeWord('café', map)).toBe('cafe');
expect(normalizeWord('ação', map)).toBe('acao');
});

it('should NOT normalize Norwegian distinct letters', () => {
const map = buildNormalizeMap(norwegianConfig);
expect(normalizeWord('blåbær', map)).toBe('blåbær');
expect(normalizeWord('søster', map)).toBe('søster');
});
});

describe('charsMatch', () => {
it('should match diacritic variants in German', () => {
const map = buildNormalizeMap(germanConfig);
expect(charsMatch('a', 'ä', map)).toBe(true);
expect(charsMatch('ä', 'a', map)).toBe(true);
expect(charsMatch('o', 'ö', map)).toBe(true);
});

it('should NOT match different base characters', () => {
const map = buildNormalizeMap(germanConfig);
expect(charsMatch('a', 'o', map)).toBe(false);
expect(charsMatch('ä', 'ö', map)).toBe(false);
});

it('should NOT match Norwegian distinct letters to base', () => {
const map = buildNormalizeMap(norwegianConfig);
expect(charsMatch('æ', 'a', map)).toBe(false);
expect(charsMatch('ø', 'o', map)).toBe(false);
expect(charsMatch('å', 'a', map)).toBe(false);
});
});

describe('buildNormalizedWordMap', () => {
it('should build lookup map from word list', () => {
const map = buildNormalizeMap(germanConfig);
const wordList = ['börde', 'grün', 'apfel'];
const wordMap = buildNormalizedWordMap(wordList, map);

expect(wordMap.get('borde')).toBe('börde');
expect(wordMap.get('grun')).toBe('grün');
expect(wordMap.get('apfel')).toBe('apfel');
});

it('should preserve first word when conflicts occur', () => {
const map = buildNormalizeMap(germanConfig);
// Both normalize to same form, first one wins
const wordList = ['börde', 'borde'];
const wordMap = buildNormalizedWordMap(wordList, map);

expect(wordMap.get('borde')).toBe('börde');
});

it('should not normalize when no diacritic_map', () => {
const map = buildNormalizeMap(norwegianConfig);
const wordList = ['blåbær', 'søster'];
const wordMap = buildNormalizedWordMap(wordList, map);

// Words are their own keys (no normalization)
expect(wordMap.get('blåbær')).toBe('blåbær');
expect(wordMap.get('søster')).toBe('søster');
// Cannot look up without diacritics
expect(wordMap.get('blabaer')).toBeUndefined();
});
});
});
Loading
Loading