Heim > Web-Frontend > js-Tutorial > Testen mit Playwright: Verwenden Sie iext-Übersetzungen in Tests, aber nicht „t(&#key&#)'.

Testen mit Playwright: Verwenden Sie iext-Übersetzungen in Tests, aber nicht „t(&#key&#)'.

Patricia Arquette
Freigeben: 2025-01-06 07:01:39
Original
644 Leute haben es durchsucht

Testing with Playwright: Use iext Translations in Tests, but not `t(

E2E-Tests lokalisierter Anwendungen können eine Herausforderung sein, Übersetzungen von Schlüsseln können das Lesen und Warten von Testcode erschweren. In diesem Artikel wird gezeigt, wie Sie i18next-Übersetzungen in der React-App mit Playwright testen, mit einem vereinfachten Ansatz, der Übersetzungsschlüssel vermeidet. Die Idee kann in jedem Projekt mit i18next oder ähnlichen Bibliotheken verwendet werden.

Dieser Ansatz baut auf Konzepten aus meinem vorherigen Artikel über die Verwendung von Playwright-Fixtures für die Autorisierung in RBAC-Anwendungen auf (Testen mit Playwright: Autorisierung weniger schmerzhaft und lesbarer machen).

Hier ein praktisches Beispiel, wie es im Test aussieht:

const LOCALES = ["en", "es", "zh"];

describe("Author page", () => {
  for (let locale of LOCALES) {
    test(`it has a link to articles. {locale: ${locale}}`, async ({ page, tkey }) => {
      await page.goto("/authors/123");
      const link = await page.getByRole("link").nth(0).textContent();
      expect(link).toBe(tkey("Mis articulos", "menu"));
    });
  }
});
Nach dem Login kopieren

Das Schlüsselkonzept hier ist die Verwendung der tatsächlichen Phrase anstelle eines i18n-Schlüssels für die Übersetzung. Herkömmliche Ansätze verwenden häufig Übersetzungsschlüssel, die schwer zu lesen sein können. Zum Beispiel: Expect(link).toBe(t("menu.current_user_articles_link"));. Dies widerspricht dem Grundsatz, gut lesbare Tests zu schreiben.

Implementierung: Austausch von Übersetzungsschlüsseln mit Phrasen

Die Implementierung dreht sich um die Verwendung von Phrasen, die Übersetzungsschlüsseln in Tests entsprechen. Hier ist ein Beispiel einer typischen Übersetzungsdatei:

{
  "en": {
    "menu": {
      "current_user_articles_link": "My articles"
    }
  },
  "es": {
    "menu": {
      "current_user_articles_link": "Mis articulos"
    }
  },
  "zh": {
    "menu": {
      "current_user_articles_link": "我的文章"
    }
  }
}
Nach dem Login kopieren

Wir wandeln dies in ein ausgetauschtes JSON-Format um, in dem Phrasen ihren entsprechenden Schlüsseln zugeordnet werden (in diesem Beispiel verwenden wir Spanisch als Primärsprache):

{
  "Mis articulos": "menu.current_user_articles_link"
}
Nach dem Login kopieren

Um diese Transformation zu erreichen, erstellen wir eine Dienstprogrammfunktion, die Schlüssel und Werte in den JSON-Dateien austauscht. Hier ist die Implementierung mit TypeScript und Deno:

import { readdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

const CONSTANTS = {
  DIRECTORY_PATH: 'app/public/locale/es', // Path to locale language files
  SWAPPED_FILE_PATH: 'e2e/fixtures/i18n/es_swapped.json', // Output path for swapped JSON
  FILE_ENCODING: 'utf8',
} as const;

interface TranslationObject {
  [key: string]: string | TranslationObject;
}
type SwappedTranslations = Record<string, Record<string, string>>;

function swapKeysAndValues(obj: TranslationObject): Record<string, string> {
  const swappedObject: Record<string, string> = {};

  function traverseObj(value: unknown, path = ''): void {
    if (value && typeof value === 'object') {
      for (const [key, val] of Object.entries(value)) {
        traverseObj(val, path ? `${path}.${key}` : key);
      }
    } else if (typeof value === 'string') {
      swappedObject[value] = path;
    }
  }

  traverseObj(obj);
  return swappedObject;
}

const JSON_EXTENSION_REGEX = /\.json$/;

async function buildJsonWithNamespaces(): Promise<void> {
  try {
    const files = await readdir(CONSTANTS.DIRECTORY_PATH);
    const result: SwappedTranslations = {};

    for (const file of files) {
      const filePath = join(CONSTANTS.DIRECTORY_PATH, file);
      const fileContent = await readFile(filePath, CONSTANTS.FILE_ENCODING);
      const parsedFileContent = JSON.parse(fileContent) as TranslationObject;
      const swappedContent = swapKeysAndValues(parsedFileContent);
      const key = file.replace(JSON_EXTENSION_REGEX, '');

      result[key] = swappedContent;
    }

    await writeFile(
      CONSTANTS.SWAPPED_FILE_PATH,
      JSON.stringify(result, null, 2),
      CONSTANTS.FILE_ENCODING,
    );

    console.info('✅ Successfully generated swapped translations');
  } catch (error) {
    console.error(
      '❌ Failed to generate swapped translations:',
      error instanceof Error ? error.message : error,
    );
    process.exit(1);
  }
}

console.info('? Converting locale to swapped JSON...');
buildJsonWithNamespaces();
Nach dem Login kopieren

Führen Sie dieses Skript aus mit: deno run --allow-read --allow-write path_to_swap.ts

Einrichten von i18n für Playwright

Jetzt erstellen wir in Playwright eine Vorrichtung für i18n. Diese Implementierung ist von der Playwright-i18next-Fixture-Bibliothek inspiriert, jedoch mit benutzerdefinierten Modifikationen für eine bessere Kontrolle:

import * as fs from "node:fs";
import path from "node:path";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { test as base } from "@playwright/test";
import { createInstance, type i18n, type TFunction } from "i18next";

const CONFIG = {
  TRANSLATIONS_PATH: "translations_path_to_files/or_api_endpoint",
  SWAPPED_TRANSLATIONS_PATH: "e2e/fixtures/i18n/es_swapped.json",
  LOCAL_STORAGE_KEY: "locale",
  SUPPORTED_LANGUAGES: ["es", "en", "zh"] as const,
  DEFAULT_LANGUAGE: "es",
  NAMESPACES: ["shared", "menu"] as const,
  DEFAULT_NS: "shared",
} as const;

const data = fs.existsSync(CONFIG.SWAPPED_TRANSLATIONS_PATH)
  ? JSON.parse(fs.readFileSync(CONFIG.SWAPPED_TRANSLATIONS_PATH, "utf8"))
  : {};

export function findTranslationByKey(
  key: string,
  namespace: string,
): string | undefined {
  const value = data[namespace][key];
  if (value && typeof value === "string") return value;

  throw new Error(
    `? Translation not found for the namespace "${namespace}" and key "${key}"`,
  );
}

type SupportedLanguage = (typeof CONFIG.SUPPORTED_LANGUAGES)[number];
type Namespace = (typeof CONFIG.NAMESPACES)[number];

export const i18nOptions = {
  plugins: [Backend, initReactI18next],
  options: {
    lng: CONFIG.DEFAULT_LANGUAGE,
    load: "languageOnly",
    ns: CONFIG.NAMESPACES,
    defaultNS: CONFIG.DEFAULT_NS,
    supportedLngs: CONFIG.SUPPORTED_LANGUAGES,
    backend: {
      allowMultiLoading: true,
      loadPath: CONFIG.TRANSLATIONS_PATH,
    },
    react: {
      useSuspense: true,
    },
  },
} as const;

let storedI18n: i18n | undefined;

async function initI18n({
  plugins,
  options,
}: typeof i18nOptions): Promise<i18n> {
  if (!storedI18n?.isInitialized) {
    const i18n = plugins.reduce(
      (i18n, plugin) => i18n.use(plugin),
      createInstance(),
    );
    await i18n.init(options);
    storedI18n = i18n;
    return i18n;
  }
  return storedI18n;
}

export type Tkey = (key: string, namespace?: Namespace) => string;

interface I18nFixtures {
  i18n: i18n;
  t: TFunction;
  tkey: Tkey;
}

/*
  Fixture for i18n functionality. It initializes the i18next instance and checks the language setting in the storageState created by Playwright. Similar technique was used in my RBAC example. For more details, see my article about authorization testing (https://dev.to/a-dev/testing-with-playwright-how-to-make-authorization-less-painful-and-more-readable-3344) and the official Playwright documentation (https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state).
*/

export const i18nFixture = base.extend<I18nFixtures>({
  i18n: async ({ storageState }, use) => {
    const i18nInstance = await initI18n(i18nOptions);

    if (storageState) {
      try {
        const data = JSON.parse(
          fs.readFileSync(path.resolve(storageState as string), "utf8"),
        );

        const localStorage = data?.origins?.[0]?.localStorage;
        const language = localStorage?.find(
          (i) => i.name === CONFIG.LOCAL_STORAGE_KEY,
        )?.value as SupportedLanguage | undefined;

        if (!language) {
          throw new Error(
            `No language setting found in localStorage (key: ${CONFIG.LOCAL_STORAGE_KEY})`,
          );
        }

        if (!CONFIG.SUPPORTED_LANGUAGES.includes(language)) {
          throw new Error(
            `Unsupported language: ${language}. Supported languages: ${CONFIG.SUPPORTED_LANGUAGES.join(", ")}`,
          );
        }

        // Change language if different from current
        if (i18nInstance.language !== language) {
          await i18nInstance.changeLanguage(language);
        }
      } catch (error) {
        throw new Error(`Failed to process storage state: ${error.message}`);
      }
    }

    await use(i18nInstance);
  },
  tkey: async ({ t }, use) => {
    await use((str?: string, namespace = "shared"): string => {
      if (!str) return "⭕ Error: no translation";
      const tkey = findTranslationByKey(str, namespace);
      return t(`${namespace}:${tkey}`);
    });
  },
  t: async ({ i18n }, use) => {
    await use(i18n.t);
  },
});
Nach dem Login kopieren

Schließlich können wir das i18nFixture in unseren Playwright-Tests verwenden, um Übersetzungen und Spracheinstellungen zu verwalten. Weitere Informationen zum Arbeiten mit Fixtures finden Sie in der offiziellen Playwright-Dokumentation. Ich empfehle, eine index.ts-Datei zu erstellen, die alle Fixtures exportiert, die dann in Ihre Testdateien importiert werden können.

import { mergeTests } from '@playwright/test';
import { i18nFixture } from './fixtures/i18n';

const test = mergeTests({
  ...i18nFixture,
});

export { test };
Nach dem Login kopieren

Ich wünsche Ihnen viel Spaß beim Testen mit Playwright und i18next! ?
Wenn Sie Fragen oder Vorschläge haben, zögern Sie nicht, unten einen Kommentar abzugeben.

Das obige ist der detaillierte Inhalt vonTesten mit Playwright: Verwenden Sie iext-Übersetzungen in Tests, aber nicht „t(&#key&#)'.. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage