Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
307
e2e/tests/i18n.spec.ts
Normal file
307
e2e/tests/i18n.spec.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* i18n E2E Tests
|
||||
*
|
||||
* Tests the internationalization features in the admin UI:
|
||||
* - Locale column in content list
|
||||
* - Locale filter in content list
|
||||
* - Translations sidebar in content editor
|
||||
* - Creating translations via the admin UI
|
||||
* - Navigating between translations
|
||||
* - Slug correctness (no locale suffix accumulation)
|
||||
*
|
||||
* The e2e fixture has i18n configured with locales: en, fr, es
|
||||
* and defaultLocale: en.
|
||||
*
|
||||
* Seed data:
|
||||
* - posts: "First Post" (en, published), "Second Post" (en, published), "Draft Post" (en, draft)
|
||||
* - pages: "About" (en, published), "Contact" (en, draft)
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures";
|
||||
|
||||
const CONTENT_EDIT_URL_PATTERN = /\/content\/posts\/[A-Z0-9]+$/;
|
||||
|
||||
test.describe("i18n", () => {
|
||||
test.beforeEach(async ({ admin }) => {
|
||||
await admin.devBypassAuth();
|
||||
});
|
||||
|
||||
test.describe("Content List", () => {
|
||||
test("shows locale column when i18n is configured", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
// The table should have a "Locale" column header
|
||||
const localeHeader = admin.page.locator("th", { hasText: "Locale" });
|
||||
await expect(localeHeader).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays locale badges for each content item", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
// All seeded posts are English — should see EN badges
|
||||
const locales = await admin.getLocaleColumnValues();
|
||||
expect(locales.length).toBeGreaterThan(0);
|
||||
// All seeded content is "en"
|
||||
for (const locale of locales) {
|
||||
expect(locale.trim().toLowerCase()).toBe("en");
|
||||
}
|
||||
});
|
||||
|
||||
test("has a locale filter switcher", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should have a select element for locale filtering
|
||||
const select = admin.page.locator("select").first();
|
||||
await expect(select).toBeVisible();
|
||||
|
||||
// Should show available locale options
|
||||
const options = select.locator("option");
|
||||
const optionTexts = await options.allTextContents();
|
||||
// Expect EN, FR, ES options (may also have "All locales")
|
||||
expect(optionTexts.some((t) => t.includes("EN"))).toBe(true);
|
||||
expect(optionTexts.some((t) => t.includes("FR"))).toBe(true);
|
||||
expect(optionTexts.some((t) => t.includes("ES"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Content Editor", () => {
|
||||
test("shows translations sidebar for existing content", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Click on any post to edit (use first link in table body)
|
||||
await admin.page.locator("table tbody tr a").first().click();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should see the Translations sidebar heading
|
||||
const translationsHeading = admin.page.locator("h3", {
|
||||
hasText: "Translations",
|
||||
});
|
||||
await expect(translationsHeading).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows all configured locales in translations sidebar", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
await admin.page.locator("table tbody tr a").first().click();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should show en, fr, es in the sidebar
|
||||
const locales = await admin.getTranslationSidebarLocales();
|
||||
const normalized = locales.map((l) => l.trim().toLowerCase());
|
||||
expect(normalized).toContain("en");
|
||||
expect(normalized).toContain("fr");
|
||||
expect(normalized).toContain("es");
|
||||
});
|
||||
|
||||
test("marks current locale in translations sidebar", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
await admin.page.locator("table tbody tr a").first().click();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// The "current" marker should appear next to EN
|
||||
const currentMarker = admin.page.locator("span.text-kumo-brand", {
|
||||
hasText: "current",
|
||||
});
|
||||
await expect(currentMarker).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Translate buttons for missing locales", async ({ admin }) => {
|
||||
await admin.goToContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
await admin.page.locator("table tbody tr a").first().click();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// FR and ES should have "Translate" buttons since no translations exist yet
|
||||
expect(await admin.hasTranslateButton("fr")).toBe(true);
|
||||
expect(await admin.hasTranslateButton("es")).toBe(true);
|
||||
});
|
||||
|
||||
test("does not show translations sidebar for new content", async ({ admin }) => {
|
||||
await admin.goToNewContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
// The translations sidebar should NOT be visible for unsaved content
|
||||
const translationsHeading = admin.page.locator("h3", {
|
||||
hasText: "Translations",
|
||||
});
|
||||
await expect(translationsHeading).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Translation Flow", () => {
|
||||
test("creates a translation and navigates to it", async ({ admin }) => {
|
||||
// Create a fresh post so we have a clean translation group
|
||||
await admin.goToNewContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
const postTitle = `i18n Test Post ${Date.now()}`;
|
||||
await admin.fillField("title", postTitle);
|
||||
await admin.clickSave();
|
||||
|
||||
// Wait for redirect to edit page
|
||||
await expect(admin.page).toHaveURL(CONTENT_EDIT_URL_PATTERN, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Capture the original post URL
|
||||
const originalUrl = admin.page.url();
|
||||
|
||||
// Should see Translate buttons for FR and ES
|
||||
expect(await admin.hasTranslateButton("fr")).toBe(true);
|
||||
|
||||
// Click "Translate" for FR — wait for URL to change (SPA navigation)
|
||||
await admin.clickTranslate("fr");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== originalUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
// The title should be pre-filled from the original
|
||||
await expect(admin.page.locator("#field-title")).toHaveValue(postTitle);
|
||||
|
||||
// The slug should be the same as the original (no locale suffix)
|
||||
const slug = await admin.page.getByLabel("Slug").inputValue();
|
||||
expect(slug).not.toContain("-fr");
|
||||
expect(slug).not.toContain("-en");
|
||||
});
|
||||
|
||||
test("shows Edit link for existing translations", async ({ admin }) => {
|
||||
// Create a post and its FR translation
|
||||
await admin.goToNewContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
const postTitle = `Translation Edit Test ${Date.now()}`;
|
||||
await admin.fillField("title", postTitle);
|
||||
await admin.clickSave();
|
||||
|
||||
await expect(admin.page).toHaveURL(CONTENT_EDIT_URL_PATTERN, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await admin.waitForLoading();
|
||||
|
||||
const originalUrl = admin.page.url();
|
||||
|
||||
// Create FR translation and wait for navigation
|
||||
await admin.clickTranslate("fr");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== originalUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Now on the FR translation — EN should show "Edit" link, not "Translate"
|
||||
expect(await admin.hasEditTranslationLink("en")).toBe(true);
|
||||
// ES should still show "Translate"
|
||||
expect(await admin.hasTranslateButton("es")).toBe(true);
|
||||
});
|
||||
|
||||
test("can navigate between translations via Edit links", async ({ admin }) => {
|
||||
// Create a post and FR translation
|
||||
await admin.goToNewContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
const postTitle = `Navigation Test ${Date.now()}`;
|
||||
await admin.fillField("title", postTitle);
|
||||
await admin.clickSave();
|
||||
|
||||
await expect(admin.page).toHaveURL(CONTENT_EDIT_URL_PATTERN, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await admin.waitForLoading();
|
||||
|
||||
const originalUrl = admin.page.url();
|
||||
|
||||
// Create FR translation and wait for navigation
|
||||
await admin.clickTranslate("fr");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== originalUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
const frUrl = admin.page.url();
|
||||
|
||||
// Navigate back to EN via Edit link
|
||||
await admin.clickEditTranslation("en");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== frUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should be back on the original post
|
||||
await expect(admin.page).toHaveURL(originalUrl);
|
||||
await expect(admin.page.locator("#field-title")).toHaveValue(postTitle);
|
||||
});
|
||||
|
||||
test("creating multiple translations does not accumulate locale suffixes in slugs", async ({
|
||||
admin,
|
||||
}) => {
|
||||
// This is the regression test for the slug accumulation bug:
|
||||
// old code: slug = rawItem.slug + "-" + locale
|
||||
// Each translate would append more suffixes: post-fr-en-fr-en...
|
||||
|
||||
await admin.goToNewContent("posts");
|
||||
await admin.waitForLoading();
|
||||
|
||||
const postTitle = `Slug Accumulation Test ${Date.now()}`;
|
||||
await admin.fillField("title", postTitle);
|
||||
await admin.clickSave();
|
||||
|
||||
await expect(admin.page).toHaveURL(CONTENT_EDIT_URL_PATTERN, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await admin.waitForLoading();
|
||||
|
||||
const originalUrl = admin.page.url();
|
||||
const originalSlug = await admin.page.getByLabel("Slug").inputValue();
|
||||
expect(originalSlug).toBeTruthy();
|
||||
|
||||
// Create FR translation and wait for navigation
|
||||
await admin.clickTranslate("fr");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== originalUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
// FR slug should be the same as original (UNIQUE(slug, locale) allows this)
|
||||
const frSlug = await admin.page.getByLabel("Slug").inputValue();
|
||||
expect(frSlug).toBe(originalSlug);
|
||||
|
||||
const frUrl = admin.page.url();
|
||||
|
||||
// Navigate back to EN
|
||||
await admin.clickEditTranslation("en");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== frUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
const enUrl = admin.page.url();
|
||||
|
||||
// Create ES translation from EN
|
||||
await admin.clickTranslate("es");
|
||||
await admin.page.waitForURL(
|
||||
(url) => CONTENT_EDIT_URL_PATTERN.test(url.pathname) && url.href !== enUrl,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await admin.waitForLoading();
|
||||
|
||||
// ES slug should also be the same — no accumulation
|
||||
const esSlug = await admin.page.getByLabel("Slug").inputValue();
|
||||
expect(esSlug).toBe(originalSlug);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user