mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
test: update i18n tests
This commit is contained in:
@@ -245,7 +245,7 @@
|
|||||||
|
|
||||||
### Фаза 6: Тесты
|
### Фаза 6: Тесты
|
||||||
|
|
||||||
- **6.1** `new-tools-i18n.test.ts` — полнота словарей для нового registry:
|
- **6.1** `tools-i18n.test.ts` — полнота словарей для текущего registry:
|
||||||
title/description ru для всех инструментов, каждый `label`-ключ схемы есть в
|
title/description ru для всех инструментов, каждый `label`-ключ схемы есть в
|
||||||
`fields`, каждый `groups`-ключ есть в `groups`, без лишних ключей.
|
`fields`, каждый `groups`-ключ есть в `groups`, без лишних ключей.
|
||||||
- **6.2** Фикс `known-issues.spec.ts` test.fixme — ошибки локализуются.
|
- **6.2** Фикс `known-issues.spec.ts` test.fixme — ошибки локализуются.
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { expect, test } from "playwright/test";
|
||||||
|
import { opaquePng } from "./helpers/fixtures";
|
||||||
|
import { openTool, uploadImage } from "./helpers/page";
|
||||||
|
|
||||||
|
test("RU/EN: переключение переводит заголовок инструмента", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openTool(page, "resize-png");
|
||||||
|
const ruBtn = page.getByRole("button", { name: "RU", exact: true });
|
||||||
|
const enBtn = page.getByRole("button", { name: "EN", exact: true });
|
||||||
|
await expect(async () => {
|
||||||
|
await ruBtn.click();
|
||||||
|
await expect(page.locator(".schema-tool h1")).toHaveText(
|
||||||
|
"Изменить размер PNG",
|
||||||
|
);
|
||||||
|
}).toPass();
|
||||||
|
await expect(ruBtn).toHaveAttribute("aria-pressed", "true");
|
||||||
|
await enBtn.click();
|
||||||
|
await expect(page.locator(".schema-tool h1")).toHaveText("Resize PNG");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Валится на известном баге resize-png (дефолтные 0-параметры дают ошибку при
|
||||||
|
// аплоаде, см. known-issues.spec.ts). Здесь он используется как детерминированная
|
||||||
|
// ошибка: важно, что её текст локализован, а не сырой ключ.
|
||||||
|
// TODO: найти другую ошибку, эта будет исправлена при фиксе resize-png
|
||||||
|
test("ошибка run'а локализуется, не сырой i18n-ключ", async ({ page }) => {
|
||||||
|
await openTool(page, "resize-png");
|
||||||
|
await uploadImage(page, opaquePng);
|
||||||
|
const alert = page.locator("[role='alert']");
|
||||||
|
await expect(alert).toBeVisible();
|
||||||
|
const text = (await alert.textContent()) ?? "";
|
||||||
|
expect(text).not.toMatch(/^errors\./);
|
||||||
|
expect(text.length).toBeGreaterThan(3);
|
||||||
|
});
|
||||||
@@ -2,10 +2,11 @@ import { expect, test } from "playwright/test";
|
|||||||
import { opaquePng } from "./helpers/fixtures";
|
import { opaquePng } from "./helpers/fixtures";
|
||||||
import { openTool, uploadImage } from "./helpers/page";
|
import { openTool, uploadImage } from "./helpers/page";
|
||||||
|
|
||||||
// FIXME: Весь файл — зарегистрированные баги preview (см. docs/checklist-manual-testing.md
|
// FIXME: В файле зарегистрированы баги preview (см. docs/checklist-manual-testing.md
|
||||||
// → «Известные баги preview»). Тела assert'ят ОЖИДАЕМОЕ поведение. Статус fixme
|
// → «Известные баги preview»). Тела assert'ят ОЖИДАЕМОЕ поведение. Статус fixme
|
||||||
// означает «мы знаем, что сейчас падает»; когда баг починят — убрать fixme и
|
// означает «мы знаем, что сейчас падает»; когда баг починят — убрать fixme и
|
||||||
// тест станет зелёным «сам по себе».
|
// тест станет зелёным «сам по себе». (Тест про локализацию ошибок переехал в
|
||||||
|
// i18n.spec.ts — он больше не issue.)
|
||||||
|
|
||||||
test.describe("known bugs — documented as fixme", () => {
|
test.describe("known bugs — documented as fixme", () => {
|
||||||
test.fixme("resize-png: upload produces a resized result (no alert)", async ({
|
test.fixme("resize-png: upload produces a resized result (no alert)", async ({
|
||||||
@@ -25,16 +26,4 @@ test.describe("known bugs — documented as fixme", () => {
|
|||||||
await expect(page.locator("[role='alert']")).toHaveCount(0);
|
await expect(page.locator("[role='alert']")).toHaveCount(0);
|
||||||
await expect(page.locator('img[alt="result"]')).toBeVisible();
|
await expect(page.locator('img[alt="result"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.fixme("error message is localized, not a raw i18n key", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await openTool(page, "resize-png");
|
|
||||||
await uploadImage(page, opaquePng);
|
|
||||||
const alert = page.locator("[role='alert']");
|
|
||||||
await expect(alert).toBeVisible();
|
|
||||||
const text = (await alert.textContent()) ?? "";
|
|
||||||
expect(text).not.toMatch(/^errors\./);
|
|
||||||
expect(text.length).toBeGreaterThan(3);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default tseslint.config(
|
|||||||
"e2e/tools-smoke.spec.ts",
|
"e2e/tools-smoke.spec.ts",
|
||||||
"e2e/generators.spec.ts",
|
"e2e/generators.spec.ts",
|
||||||
"e2e/known-issues.spec.ts",
|
"e2e/known-issues.spec.ts",
|
||||||
|
"e2e/i18n.spec.ts",
|
||||||
// Тесты и фикстуры кастомных линт-правил лежат вне src/ (не в
|
// Тесты и фикстуры кастомных линт-правил лежат вне src/ (не в
|
||||||
// tsconfig), поэтому для типизированного парсинга резолвятся
|
// tsconfig), поэтому для типизированного парсинга резолвятся
|
||||||
// через default-проект. Перечисляются точечно: `**` в
|
// через default-проект. Перечисляются точечно: `**` в
|
||||||
|
|||||||
+18
-30
@@ -525,64 +525,52 @@ export const ru: Dict = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-hsl": {
|
"png-to-hsl": {
|
||||||
|
title: "Разбить PNG на HSL",
|
||||||
|
description:
|
||||||
|
"Раскладывает изображение на компоненты тона, насыщенности и светлоты.",
|
||||||
options: {
|
options: {
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-hsv": {
|
"png-to-hsv": {
|
||||||
|
title: "Разбить PNG на HSV",
|
||||||
|
description:
|
||||||
|
"Раскладывает изображение на компоненты тона, насыщенности и значения (яркости).",
|
||||||
options: {
|
options: {
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-hsi": {
|
"png-to-hsi": {
|
||||||
|
title: "Разбить PNG на HSI",
|
||||||
|
description:
|
||||||
|
"Раскладывает изображение на компоненты тона, насыщенности и интенсивности.",
|
||||||
options: {
|
options: {
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-cmyk": {
|
"png-to-cmyk": {
|
||||||
|
title: "Конвертировать PNG в цвета CMYK",
|
||||||
|
description:
|
||||||
|
"Раскладывает изображение на печатные компоненты Cyan, Magenta, Yellow и Key (чёрный).",
|
||||||
options: {
|
options: {
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-ycbcr": {
|
"png-to-ycbcr": {
|
||||||
|
title: "Конвертировать PNG в цвета YCbCr",
|
||||||
|
description:
|
||||||
|
"Раскладывает изображение на яркость (Y) и цветоразностные компоненты Cb / Cr.",
|
||||||
options: {
|
options: {
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"png-to-lab": {
|
"png-to-lab": {
|
||||||
options: {
|
|
||||||
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cmyk: {
|
|
||||||
title: "Конвертировать PNG в цвета CMYK",
|
|
||||||
description:
|
|
||||||
"Раскладывает изображение на печатные компоненты Cyan, Magenta, Yellow и Key (чёрный).",
|
|
||||||
},
|
|
||||||
hsl: {
|
|
||||||
title: "Разбить PNG на HSL",
|
|
||||||
description:
|
|
||||||
"Раскладывает изображение на компоненты тона, насыщенности и светлоты.",
|
|
||||||
},
|
|
||||||
hsi: {
|
|
||||||
title: "Разбить PNG на HSI",
|
|
||||||
description:
|
|
||||||
"Раскладывает изображение на компоненты тона, насыщенности и интенсивности.",
|
|
||||||
},
|
|
||||||
hsv: {
|
|
||||||
title: "Разбить PNG на HSV",
|
|
||||||
description:
|
|
||||||
"Раскладывает изображение на компоненты тона, насыщенности и значения (яркости).",
|
|
||||||
},
|
|
||||||
lab: {
|
|
||||||
title: "Конвертировать PNG в цвета LAB",
|
title: "Конвертировать PNG в цвета LAB",
|
||||||
description:
|
description:
|
||||||
"Раскладывает изображение на перцепционную светлоту и оппонентные пары зелёный–пурпур / синий–жёлтый.",
|
"Раскладывает изображение на перцепционную светлоту и оппонентные пары зелёный–пурпур / синий–жёлтый.",
|
||||||
},
|
options: {
|
||||||
ycbcr: {
|
display: { gray: "Оттенки серого", color: "Пространство как RGB" },
|
||||||
title: "Конвертировать PNG в цвета YCbCr",
|
},
|
||||||
description:
|
|
||||||
"Раскладывает изображение на яркость (Y) и цветоразностные компоненты Cb / Cr.",
|
|
||||||
},
|
},
|
||||||
"black-and-white-png": {
|
"black-and-white-png": {
|
||||||
title: "Чёрно-белый PNG по порогу",
|
title: "Чёрно-белый PNG по порогу",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { getLocale, setLocale } from "./locale.svelte";
|
||||||
|
import { LOCALE_TAGS } from "./dict";
|
||||||
|
import { normalizeForSearch } from "./matching";
|
||||||
|
import { t } from "./t";
|
||||||
|
import { getTool, TOOLS } from "../registry";
|
||||||
|
import { toolTitle } from "./schema-tool-strings";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setLocale("ru");
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Смоук §6 i18n (новый UI)", () => {
|
||||||
|
it("1. html lang следует за локалью", () => {
|
||||||
|
const doc = { documentElement: { lang: "" } };
|
||||||
|
vi.stubGlobal("document", doc);
|
||||||
|
setLocale("en");
|
||||||
|
expect(doc.documentElement.lang).toBe("en");
|
||||||
|
setLocale("ru");
|
||||||
|
expect(doc.documentElement.lang).toBe("ru");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("2. getLocale отражает последний выбор", () => {
|
||||||
|
setLocale("en");
|
||||||
|
expect(getLocale()).toBe("en");
|
||||||
|
setLocale("ru");
|
||||||
|
expect(getLocale()).toBe("ru");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("3. Ключевые секции переведены без смеси языков", () => {
|
||||||
|
const samples: Array<[string, string, string]> = [
|
||||||
|
["header.workspace", "Рабочая область", "Workspace"],
|
||||||
|
["catalog.heading", "Каталог инструментов", "Tool catalog"],
|
||||||
|
[
|
||||||
|
"home.heroTitle",
|
||||||
|
"Что делаем с изображением?",
|
||||||
|
"What do you want to do",
|
||||||
|
],
|
||||||
|
["chain.inputLegend", "Вход", "Input"],
|
||||||
|
["resultCard.nextTool", "Следующий инструмент", "Next tool"],
|
||||||
|
["paramsCard.toolSettings", "Настройки инструмента", "Tool settings"],
|
||||||
|
["download.busy", "Готовим файл", "Preparing file"],
|
||||||
|
["dropZone.pickDefault", "Перетащите изображение", "Drop an image"],
|
||||||
|
];
|
||||||
|
for (const [key, ruPart, enPart] of samples) {
|
||||||
|
setLocale("ru");
|
||||||
|
expect(t(key), key + " @ru").toContain(ruPart);
|
||||||
|
setLocale("en");
|
||||||
|
expect(t(key), key + " @en").toContain(enPart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4. Заголовок инструмента переключается локалями (schema-tool-strings)", () => {
|
||||||
|
const tool = getTool("crop-png") ?? TOOLS[0];
|
||||||
|
setLocale("ru");
|
||||||
|
expect(toolTitle(tool)).toContain("Обрезать");
|
||||||
|
setLocale("en");
|
||||||
|
expect(toolTitle(tool)).toBe(tool.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("5. Ошибки с vars локализуются на оба языка", () => {
|
||||||
|
setLocale("ru");
|
||||||
|
expect(t("errors.badHex", { value: "#zz" })).toBe(
|
||||||
|
'Некорректный HEX-цвет: "#zz"',
|
||||||
|
);
|
||||||
|
expect(t("errors.toolNotFound", { id: "x" })).toContain("не найден");
|
||||||
|
setLocale("en");
|
||||||
|
expect(t("errors.badHex", { value: "#zz" })).toBe(
|
||||||
|
'Invalid HEX color: "#zz"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("6. Ё не мешает нормализации поискового запроса", () => {
|
||||||
|
expect(normalizeForSearch("ЧЁРНО") === normalizeForSearch("ЧЕРНО")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(normalizeForSearch("ёлка") === normalizeForSearch("елка")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("7. Теги локалей для форматирования чисел корректны", () => {
|
||||||
|
expect(LOCALE_TAGS.ru).toBe("ru-RU");
|
||||||
|
expect(LOCALE_TAGS.en).toBe("en-US");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TOOLS, type ToolEntry } from "../registry";
|
||||||
|
import { en } from "./en";
|
||||||
|
import { ru } from "./ru";
|
||||||
|
|
||||||
|
function toolKeys(tool: ToolEntry): { fields: string[]; groups: string[] } {
|
||||||
|
const fields: string[] = [];
|
||||||
|
for (const field of Object.values(tool.schema?.fields ?? {})) {
|
||||||
|
const label = (field as { spec?: { label?: string } }).spec?.label;
|
||||||
|
if (label) fields.push(label.replace(/^fields\./, ""));
|
||||||
|
}
|
||||||
|
const groups = (tool.schema?.layout?.groups ?? []).map((g) =>
|
||||||
|
(g.title ?? "").replace(/^groups\./, ""),
|
||||||
|
);
|
||||||
|
return { fields, groups };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("полнота словарей для нового registry", () => {
|
||||||
|
it("у каждого инструмента есть перевод ru с непустыми title/description", () => {
|
||||||
|
for (const tool of TOOLS) {
|
||||||
|
const strings = ru.tools[tool.id];
|
||||||
|
expect(strings, `нет перевода ru для ${tool.id}`).toBeDefined();
|
||||||
|
expect(strings?.title, `${tool.id}: title`).toBeTruthy();
|
||||||
|
expect(strings?.description, `${tool.id}: description`).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("каждый label-ключ поля схемы переведён в fields обоих словарей", () => {
|
||||||
|
for (const tool of TOOLS) {
|
||||||
|
for (const key of toolKeys(tool).fields) {
|
||||||
|
expect(en.fields?.[key], `${tool.id}: ${key} в en`).toBeTruthy();
|
||||||
|
expect(ru.fields?.[key], `${tool.id}: ${key} в ru`).toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("каждый groups-ключ схемы переведён в groups обоих словарей", () => {
|
||||||
|
for (const tool of TOOLS) {
|
||||||
|
for (const key of toolKeys(tool).groups) {
|
||||||
|
expect(en.groups?.[key], `${tool.id}: ${key} в en`).toBeTruthy();
|
||||||
|
expect(ru.groups?.[key], `${tool.id}: ${key} в ru`).toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("в словарях нет лишних инструментов", () => {
|
||||||
|
const ids = new Set(TOOLS.map((tool) => tool.id));
|
||||||
|
for (const dict of [ru.tools, en.tools]) {
|
||||||
|
for (const id of Object.keys(dict)) {
|
||||||
|
expect(ids.has(id), `лишний инструмент в словаре: ${id}`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("в fields и groups нет ключей, не используемых схемами", () => {
|
||||||
|
const usedFields = new Set<string>();
|
||||||
|
const usedGroups = new Set<string>();
|
||||||
|
for (const tool of TOOLS) {
|
||||||
|
const keys = toolKeys(tool);
|
||||||
|
keys.fields.forEach((k) => usedFields.add(k));
|
||||||
|
keys.groups.forEach((k) => usedGroups.add(k));
|
||||||
|
}
|
||||||
|
for (const dict of [en, ru]) {
|
||||||
|
for (const key of Object.keys(dict.fields ?? {})) {
|
||||||
|
expect(usedFields.has(key), `лишний fields-ключ: ${key}`).toBe(true);
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(dict.groups ?? {})) {
|
||||||
|
expect(usedGroups.has(key), `лишний groups-ключ: ${key}`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user