mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: separate slug pages from tools
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TOOLS } from "../registry";
|
||||
import { PAGES } from "../registry";
|
||||
import { setLocale } from "./locale.svelte";
|
||||
import {
|
||||
searchTools,
|
||||
toolDescription,
|
||||
toolTitle,
|
||||
pageDescription,
|
||||
pageTitle,
|
||||
searchPages,
|
||||
verdictText,
|
||||
} from "./schema-tool-strings";
|
||||
|
||||
@@ -12,53 +12,41 @@ afterEach(() => {
|
||||
setLocale("ru");
|
||||
});
|
||||
|
||||
const ids = (tools: { id: string }[]) => tools.map((t) => t.id);
|
||||
const slugs = (pages: { slug: string }[]) => pages.map((page) => page.slug);
|
||||
|
||||
describe("searchTools (кросс-языковой поиск каталога)", () => {
|
||||
describe("searchPages (кросс-языковой поиск каталога)", () => {
|
||||
it("пустой запрос возвращает весь каталог", () => {
|
||||
expect(searchTools(TOOLS, "")).toHaveLength(TOOLS.length);
|
||||
expect(searchTools(TOOLS, " ")).toHaveLength(TOOLS.length);
|
||||
expect(searchPages(PAGES, "")).toHaveLength(PAGES.length);
|
||||
expect(searchPages(PAGES, " ")).toHaveLength(PAGES.length);
|
||||
});
|
||||
|
||||
it("находит ru-название, даже когда локаль en", () => {
|
||||
setLocale("en");
|
||||
expect(ids(searchTools(TOOLS, "обрез"))).toContain("crop-png");
|
||||
expect(ids(searchTools(TOOLS, "пустые поля"))).toContain(
|
||||
expect(slugs(searchPages(PAGES, "обрез"))).toContain("crop-png");
|
||||
expect(slugs(searchPages(PAGES, "пустые поля"))).toContain(
|
||||
"trim-empty-space-png",
|
||||
);
|
||||
});
|
||||
|
||||
it("находит en-название, даже когда локаль ru", () => {
|
||||
expect(ids(searchTools(TOOLS, "resize"))).toContain("resize-png");
|
||||
expect(ids(searchTools(TOOLS, "round corners"))).toContain(
|
||||
expect(slugs(searchPages(PAGES, "resize"))).toContain("resize-png");
|
||||
expect(slugs(searchPages(PAGES, "round corners"))).toContain(
|
||||
"round-corners-png",
|
||||
);
|
||||
});
|
||||
|
||||
it("находит по id", () => {
|
||||
expect(ids(searchTools(TOOLS, "crop png"))).toContain("crop-png");
|
||||
it("находит по slug (тексту адреса)", () => {
|
||||
expect(slugs(searchPages(PAGES, "crop png"))).toContain("crop-png");
|
||||
});
|
||||
|
||||
it("ё и е — один запрос", () => {
|
||||
const byYo = searchTools(TOOLS, "чёрно");
|
||||
const byYe = searchTools(TOOLS, "черно");
|
||||
expect(ids(byYo)).toEqual(ids(byYe));
|
||||
const byYo = searchPages(PAGES, "чёрно");
|
||||
const byYe = searchPages(PAGES, "черно");
|
||||
expect(slugs(byYo)).toEqual(slugs(byYe));
|
||||
});
|
||||
|
||||
it("нет совпадения — пустой список", () => {
|
||||
expect(searchTools(TOOLS, "квантовый тостер")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("metadata fallback", () => {
|
||||
it("uses registry title and description in the base locale", () => {
|
||||
setLocale("en");
|
||||
for (const tool of TOOLS) {
|
||||
expect(toolTitle(tool), `${tool.id}: title`).toBe(tool.title);
|
||||
expect(toolDescription(tool), `${tool.id}: description`).toBe(
|
||||
tool.description,
|
||||
);
|
||||
}
|
||||
expect(searchPages(PAGES, "квантовый тостер")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ToolEntry } from "$lib/registry";
|
||||
import type { Page } from "$lib/registry";
|
||||
import type { Field } from "$lib/registry-schema";
|
||||
import { getMergedDict } from "./locale.svelte";
|
||||
import { normalizeForSearch, scoreDoc, type SearchDoc } from "./matching";
|
||||
@@ -12,12 +12,12 @@ function labelOf(id: string): string {
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function toolTitle(tool: ToolEntry): string {
|
||||
return getMergedDict().tools[tool.id]?.title ?? tool.title;
|
||||
export function pageTitle(page: Page): string {
|
||||
return getMergedDict().tools[page.slug]?.title ?? page.title;
|
||||
}
|
||||
|
||||
export function toolDescription(tool: ToolEntry): string {
|
||||
return getMergedDict().tools[tool.id]?.description ?? tool.description;
|
||||
export function pageDescription(page: Page): string {
|
||||
return getMergedDict().tools[page.slug]?.description ?? page.description;
|
||||
}
|
||||
|
||||
export function fieldLabel(field: Field<unknown>, id: string): string {
|
||||
@@ -64,33 +64,30 @@ function dedupe(values: string[]): string[] {
|
||||
return [...new Set(values.filter((v) => v.length > 0))];
|
||||
}
|
||||
|
||||
export function toolSearchDoc(tool: ToolEntry): SearchDoc {
|
||||
const active = getMergedDict().tools[tool.id];
|
||||
export function pageSearchDoc(page: Page): SearchDoc {
|
||||
const active = getMergedDict().tools[page.slug];
|
||||
return {
|
||||
id: tool.id,
|
||||
id: page.slug,
|
||||
titles: dedupe([
|
||||
active?.title ?? "",
|
||||
ru.tools[tool.id]?.title ?? "",
|
||||
tool.title,
|
||||
ru.tools[page.slug]?.title ?? "",
|
||||
page.title,
|
||||
]),
|
||||
descriptions: dedupe([
|
||||
active?.description ?? "",
|
||||
ru.tools[tool.id]?.description ?? "",
|
||||
tool.description,
|
||||
ru.tools[page.slug]?.description ?? "",
|
||||
page.description,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Кросс-языковой поиск по каталогу: скор через `scoreDoc` на
|
||||
* `toolSearchDoc` (заголовки/описания всех локалей). Порядок каталога
|
||||
* `pageSearchDoc` (заголовки/описания всех локалей). Порядок каталога
|
||||
* сохраняется, элементы без совпадения отбрасываются.
|
||||
*/
|
||||
export function searchTools(
|
||||
tools: readonly ToolEntry[],
|
||||
query: string,
|
||||
): ToolEntry[] {
|
||||
export function searchPages(pages: readonly Page[], query: string): Page[] {
|
||||
const q = normalizeForSearch(query.trim());
|
||||
if (q.length === 0) return [...tools];
|
||||
return tools.filter((tool) => scoreDoc(toolSearchDoc(tool), q) !== null);
|
||||
if (q.length === 0) return [...pages];
|
||||
return pages.filter((page) => scoreDoc(pageSearchDoc(page), q) !== null);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ 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";
|
||||
import { getPageBySlug, PAGES } from "../registry";
|
||||
import { pageTitle } from "./schema-tool-strings";
|
||||
|
||||
afterEach(() => {
|
||||
setLocale("ru");
|
||||
@@ -52,11 +52,11 @@ describe("Смоук §6 i18n (новый UI)", () => {
|
||||
});
|
||||
|
||||
it("4. Заголовок инструмента переключается локалями (schema-tool-strings)", () => {
|
||||
const tool = getTool("crop-png") ?? TOOLS[0];
|
||||
const page = getPageBySlug("crop-png") ?? PAGES[0];
|
||||
setLocale("ru");
|
||||
expect(toolTitle(tool)).toContain("Обрезать");
|
||||
expect(pageTitle(page)).toContain("Обрезать");
|
||||
setLocale("en");
|
||||
expect(toolTitle(tool)).toBe(tool.title);
|
||||
expect(pageTitle(page)).toBe(page.title);
|
||||
});
|
||||
|
||||
it("5. Ошибки с vars локализуются на оба языка", () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS, type ToolEntry } from "../registry";
|
||||
import { PAGES, TOOLS, type Tool } from "../registry";
|
||||
import { en } from "./en";
|
||||
import { ru } from "./ru";
|
||||
|
||||
function toolKeys(tool: ToolEntry): { fields: string[]; groups: string[] } {
|
||||
function toolKeys(tool: Tool): { 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;
|
||||
@@ -18,7 +18,7 @@ function toolKeys(tool: ToolEntry): { fields: string[]; groups: string[] } {
|
||||
type SelectOption = { value: string; label: string };
|
||||
|
||||
function selectOptions(
|
||||
tool: ToolEntry,
|
||||
tool: Tool,
|
||||
): { fieldId: string; option: SelectOption }[] {
|
||||
const result: { fieldId: string; option: SelectOption }[] = [];
|
||||
for (const [fieldId, field] of Object.entries(tool.schema?.fields ?? {})) {
|
||||
@@ -32,12 +32,12 @@ function selectOptions(
|
||||
}
|
||||
|
||||
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("у каждой страницы есть перевод ru с непустыми title/description", () => {
|
||||
for (const page of PAGES) {
|
||||
const strings = ru.tools[page.slug];
|
||||
expect(strings, `нет перевода ru для ${page.slug}`).toBeDefined();
|
||||
expect(strings?.title, `${page.slug}: title`).toBeTruthy();
|
||||
expect(strings?.description, `${page.slug}: description`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,11 +75,14 @@ describe("полнота словарей для нового registry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("в словарях нет лишних инструментов", () => {
|
||||
const ids = new Set(TOOLS.map((tool) => tool.id));
|
||||
it("в словарях нет ключей без записи в реестре", () => {
|
||||
const known = new Set([
|
||||
...TOOLS.map((tool) => tool.id),
|
||||
...PAGES.map((page) => page.slug),
|
||||
]);
|
||||
for (const dict of [ru.tools, en.tools]) {
|
||||
for (const id of Object.keys(dict)) {
|
||||
expect(ids.has(id), `лишний инструмент в словаре: ${id}`).toBe(true);
|
||||
for (const key of Object.keys(dict)) {
|
||||
expect(known.has(key), `лишний ключ в словаре: ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user