Ctrl K
ComunidadGuías…

frontend/e2e/i18n.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect, test, type Page } from "@playwright/test"
import { adminApi, expectNoHorizontalOverflow, newInvite, registerUser, uniqueName } from "./helpers"

const CJK = /[㐀-鿿＀-￯ -〿]/

/** Visible UI text (user content excluded: this account has none yet). */
async function visibleCjk(page: Page): Promise<string[]> {
  const text = await page.locator("body").innerText()
  // Language names are shown in their own script ("繁體中文") on purpose.
  return text.split("\n").filter((l) => CJK.test(l) && l.trim() !== "繁體中文")
}

test("English: pick it before signing in, whole UI in English, saved to the account", async ({ page }) => {
  // A fresh browser whose language is Chinese starts in Traditional Chinese …
  await page.goto("/login")
  await expect(page.getByRole("heading", { name: "登入" })).toBeVisible()
  // … the language switch on the sign-in page changes it instantly.
  await page.getByRole("radio", { name: "English" }).click()
  await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible()
  expect(await page.evaluate(() => document.documentElement.lang)).toBe("en")

  // Server errors come back in English too (the API follows the UI language).
  await page.getByLabel("Username").fill("nobody-" + Date.now())
  await page.getByLabel("Password").fill("wrong-password-123")
  await page.getByRole("button", { name: "Sign in" }).click()
  await expect(page.getByRole("alert")).toHaveText("Incorrect username or password")

  // Register in English.
  const token = await newInvite()
  await page.goto(`/register?token=${token}`)
  await expect(page.getByText("Valid invitation")).toBeVisible()
  const username = uniqueName("en")
  await page.getByLabel("Username").fill(username)
  await page.getByLabel("Display name").fill("Pat Tester")
  await page.getByLabel("Password", { exact: true }).fill("playwright-pass-123")
  await page.getByLabel("Confirm password").fill("playwright-pass-123")
  await page.getByRole("button", { name: "Create account" }).click()
  await expect(page.getByRole("heading", { level: 1, name: "Home" })).toBeVisible()

  // Every page: no Chinese left, no horizontal overflow with the (longer) English strings.
  for (const route of ["/", "/photos", "/photos/albums", "/videos", "/drive", "/shared", "/trash", "/settings"]) {
    await page.goto(route)
    await expect(page.locator("main h1").first()).toBeVisible()
    await page.waitForTimeout(300)
    expect(await visibleCjk(page), `Chinese text on ${route}`).toEqual([])
    await expectNoHorizontalOverflow(page)
  }

  // The choice made before signing in was saved to the account (it follows the user to other devices).
  const me = await page.request.get("/api/me")
  expect((await me.json()).user.locale).toBe("en")

  // Switching back in Settings: instant, and saved.
  await page.goto("/settings")
  await page.getByRole("radio", { name: "繁體中文" }).click()
  await expect(page.getByRole("heading", { level: 1, name: "設定" })).toBeVisible()
  await expect.poll(async () => (await (await page.request.get("/api/me")).json()).user.locale).toBe("zh-TW")
  await page.reload()
  await expect(page.getByRole("heading", { level: 1, name: "設定" })).toBeVisible()
})

test("account language applies after signing in on another device", async ({ browser }) => {
  const admin = await adminApi()
  const token = (await (await admin.post("/api/admin/invitations", { data: {} })).json()).token
  const username = uniqueName("dev")
  const reg = await admin.post("/api/auth/register", {
    data: { invite_token: token, username, display_name: "Dev", password: "playwright-pass-123" },
  })
  expect(reg.ok()).toBeTruthy()
  await admin.dispose()
  // "Device 1": save English to the account.
  const one = await browser.newContext({ ignoreHTTPSErrors: true, locale: "zh-TW" })
  const p1 = await one.newPage()
  await p1.goto("/login")
  await p1.getByLabel("使用者名稱").fill(username)
  await p1.getByLabel("密碼").fill("playwright-pass-123")
  await p1.getByRole("button", { name: "登入" }).click()
  await expect(p1.getByRole("heading", { level: 1, name: "首頁" })).toBeVisible()
  await p1.goto("/settings")
  await p1.getByRole("radio", { name: "English" }).click()
  await expect(p1.getByRole("heading", { level: 1, name: "Settings" })).toBeVisible()
  await expect.poll(async () => (await (await p1.request.get("/api/me")).json()).user.locale).toBe("en")
  // "Device 2" (Chinese browser, nothing stored): English right after signing in.
  const two = await browser.newContext({ ignoreHTTPSErrors: true, locale: "zh-TW" })
  const p2 = await two.newPage()
  await p2.goto("/login")
  await p2.getByLabel("使用者名稱").fill(username)
  await p2.getByLabel("密碼").fill("playwright-pass-123")
  await p2.getByRole("button", { name: "登入" }).click()
  await expect(p2.getByRole("heading", { level: 1, name: "Home" })).toBeVisible()
  await one.close()
  await two.close()
})

test("drive: same-name upload offers replace / keep both / skip; trash has undo", async ({ page }) => {
  await registerUser(page)
  await page.goto("/drive")
  const input = page.locator('input[type="file"]')
  const file = (body: string) => ({ name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from(body) })
  await input.setInputFiles(file("v1"))
  await expect(page.getByRole("button", { name: "notes.txt", exact: true })).toBeVisible()

  // Keep both -> "notes (1).txt"
  await input.setInputFiles(file("v2"))
  const dialog = page.getByRole("dialog")
  await expect(dialog.getByText("這個位置已經有同名的檔案:")).toBeVisible()
  await dialog.getByRole("button", { name: "兩者都保留" }).click()
  await expect(page.getByRole("button", { name: "notes (1).txt", exact: true })).toBeVisible()

  // Skip -> nothing new
  await input.setInputFiles(file("v3"))
  await dialog.getByRole("button", { name: "略過" }).click()
  await expect(page.getByText("已略過同名檔案")).toBeVisible()

  // Replace -> still one "notes.txt", the old one is in the trash
  await input.setInputFiles(file("v4 replaced"))
  await dialog.getByRole("button", { name: "取代" }).click()
  await expect.poll(async () => {
    const r = await page.request.get("/api/drive/list")
    return (await r.json()).files.map((f: { name: string }) => f.name).sort().join("|")
  }).toBe("notes (1).txt|notes.txt")
  const trash = await (await page.request.get("/api/drive/trash")).json()
  expect(trash.files.map((f: { name: string }) => f.name)).toEqual(["notes.txt"])

  // Move to trash, then Undo from the toast.
  await page.getByRole("button", { name: "notes (1).txt", exact: true }).click({ button: "right" })
  await page.getByRole("menuitem", { name: "移到垃圾桶" }).click()
  await page.getByRole("dialog").getByRole("button", { name: "移到垃圾桶" }).click()
  await expect(page.getByRole("button", { name: "notes (1).txt", exact: true })).toHaveCount(0)
  await page.getByRole("button", { name: "復原" }).click()
  await expect(page.getByRole("button", { name: "notes (1).txt", exact: true })).toBeVisible()
})

test("admin: an expired confirmation asks for the password, then the action goes through", async ({ page }) => {
  await page.goto("/login")
  await page.getByLabel("使用者名稱").fill("admin")
  await page.getByLabel("密碼").fill(process.env.E2E_ADMIN_PASSWORD!)
  await page.getByRole("button", { name: "登入" }).click()
  await expect(page).not.toHaveURL(/\/login/)
  // Simulate the 15-minute window having passed: the server refuses the first two attempts.
  let refusals = 0
  await page.route("**/api/admin/invitations", async (route) => {
    if (route.request().method() === "POST" && refusals < 2) {
      refusals++
      await route.fulfill({ status: 403, contentType: "application/json", body: JSON.stringify({ detail: "x", code: "confirmation_required" }) })
      return
    }
    await route.continue()
  })
  await page.goto("/admin")
  await page.getByRole("radio", { name: "邀請" }).click()
  await page.getByLabel("備註(選填)").fill("needs confirmation")
  await page.getByRole("button", { name: "建立", exact: true }).click()
  const confirm = page.getByRole("dialog", { name: "確認密碼以繼續" })
  await expect(confirm).toBeVisible()
  await confirm.getByLabel("密碼").fill("definitely-wrong")
  await confirm.getByRole("button", { name: "確認" }).click()
  await expect(page.getByText("密碼不正確")).toBeVisible()
  // Again, with the right password: the original request is retried automatically.
  await page.getByRole("button", { name: "建立", exact: true }).click()
  await expect(confirm).toBeVisible()
  await confirm.getByLabel("密碼").fill(process.env.E2E_ADMIN_PASSWORD!)
  await confirm.getByRole("button", { name: "確認" }).click()
  await expect(page.locator("code")).toContainText("/register?token=")
  expect(refusals).toBe(2)
})

test("drive: a large file goes through the resumable (tus) upload, including replace", async ({ page }) => {
  await registerUser(page)
  await page.goto("/drive")
  const tusCalls: string[] = []
  page.on("request", (r) => {
    if (r.url().includes("/api/uploads")) {
      tusCalls.push(r.method())
    }
  })
  const input = page.locator('input[type="file"]')
  const big = (fill: number) => ({ name: "big.bin", mimeType: "application/octet-stream", buffer: Buffer.alloc(9 * 1024 * 1024, fill) })
  await input.setInputFiles(big(1))
  await expect(page.getByRole("button", { name: "big.bin", exact: true })).toBeVisible({ timeout: 60_000 })
  expect(tusCalls).toContain("POST")
  expect(tusCalls).toContain("PATCH")

  // Same name again, "replace": the old one goes to the trash, the new content is served.
  await input.setInputFiles(big(2))
  await page.getByRole("dialog").getByRole("button", { name: "取代" }).click()
  await expect.poll(async () => (await (await page.request.get("/api/drive/trash")).json()).files.length, { timeout: 60_000 }).toBe(1)
  const files = (await (await page.request.get("/api/drive/list")).json()).files
  expect(files.map((f: { name: string }) => f.name)).toEqual(["big.bin"])
  const body = await (await page.request.get(`/api/files/${files[0].id}/content?download=1`)).body()
  expect(body.length).toBe(9 * 1024 * 1024)
  expect(body[0]).toBe(2)
})

test("admin: create and revoke a Nextcloud integration token", async ({ page }) => {
  await page.goto("/login")
  await page.getByLabel("使用者名稱").fill("admin")
  await page.getByLabel("密碼").fill(process.env.E2E_ADMIN_PASSWORD!)
  await page.getByRole("button", { name: "登入" }).click()
  await expect(page).not.toHaveURL(/\/login/)
  await page.goto("/storage")
  await page.getByRole("radio", { name: "整合" }).click()
  const name = `NC ${Date.now()}`
  await page.getByLabel("名稱").fill(name)
  await page.getByRole("button", { name: "建立權杖" }).click()
  const done = page.getByRole("status").filter({ hasText: name })
  await expect(done).toContainText("權杖只會顯示這一次")
  await expect(done.locator("pre").first()).toContainText("mhi_")
  await expect(done.locator("pre").nth(2)).toHaveText(/^[0-9a-f]{64}$/)
  await done.getByRole("button", { name: "我已經複製好了" }).click()
  const row = page.getByRole("listitem").filter({ hasText: name })
  await expect(row).toContainText("使用中")
  await row.getByRole("button", { name: "撤銷" }).click()
  await page.getByRole("dialog").getByRole("button", { name: "撤銷" }).click()
  await expect(row).toContainText("已撤銷")
  await expectNoHorizontalOverflow(page)
})

test("settings: sign in on the phone with a QR code", async ({ page }) => {
  await registerUser(page)
  await page.goto("/settings")
  const created = page.waitForResponse((r) => r.url().endsWith("/api/me/device-logins") && r.request().method() === "POST")
  await page.getByRole("button", { name: "顯示登入 QR Code" }).click()
  const payload = (await (await created).json()).payload
  await expect(page.getByRole("img", { name: "登入 QR Code" }).locator("svg")).toBeVisible()
  await expect(page.getByText("請勿把這個 QR Code 給別人看")).toBeVisible()
  // What the app does after scanning:
  const r = await page.request.post("/api/auth/device-login", {
    data: { code: payload.code, device_name: "Test Phone" },
    headers: { "X-Requested-With": "mediahub" },
  })
  expect(r.ok()).toBeTruthy()
  await expect(page.getByText("已在「Test Phone」登入。")).toBeVisible()
  await expect(page.getByText("Media Hub App · Test Phone")).toBeVisible()
  await expectNoHorizontalOverflow(page)
})