From ceac4b7d7860a3466b445c6db6972e0582f9046d Mon Sep 17 00:00:00 2001 From: rgdevment Date: Tue, 6 Oct 2026 04:03:31 -0300 Subject: [PATCH 1/6] docs(news): 3.0.1 tells about the shortcut reminder and the Mac fixes --- app/src/news.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/src/news.json b/app/src/news.json index b1932e8e..9057b4c0 100644 --- a/app/src/news.json +++ b/app/src/news.json @@ -1,4 +1,35 @@ [ + { + "version": "3.0.1", + "es": [ + { + "title": "Abrir la app te recuerda el atajo", + "said": "si abres CopyPaste cuando ya está funcionando, te muestra cómo llamar al panel y te deja probarlo." + }, + { + "title": "Mejor en Mac", + "said": "el panel se oculta al hacer clic fuera, Ajustes usa los botones de siempre de macOS y pegar es más fiable." + }, + { + "title": "Un atajo nuevo en Mac", + "said": "ahora es ⌃⌥V, igual que en Windows; si usabas ⌥⌘V, cambia solo." + } + ], + "en": [ + { + "title": "Opening the app reminds you of the shortcut", + "said": "open CopyPaste while it is already running and it shows you how to call the panel, and lets you try it." + }, + { + "title": "Better on a Mac", + "said": "the panel hides when you click elsewhere, Settings uses the usual macOS buttons, and pasting is more reliable." + }, + { + "title": "A new shortcut on a Mac", + "said": "it is now ⌃⌥V, the same as on Windows; if you used ⌥⌘V, it changes on its own." + } + ] + }, { "version": "3.0.0-rc2", "es": [ From 4dad0507d3bb17cc34afd9852ba5b528dc648b6a Mon Sep 17 00:00:00 2001 From: rgdevment Date: Tue, 6 Oct 2026 04:20:48 -0300 Subject: [PATCH 2/6] build(release): a patch release keeps the news of its minor release instead of writing its own --- CONTRIBUTING.md | 3 ++- app/src/news.json | 31 ------------------------------- scripts/news.sh | 9 +++++++++ 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1f28359..51f56ce2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,8 @@ tag it asks `scripts/news.sh` whether `app/src/news.json` says what changed in that version — the screen that tells a person what is new is the only place the app says it, and a tag is the last moment to notice it is empty. A candidate is exempt: the screen only shows versions at or below the one running, and `3.0.0` -is above `3.0.0-rc1`. Nothing slower goes in any of +is above `3.0.0-rc1`. So is a patch whose minor release already has its entry: +`3.0.1` fixes what `3.0.0` shipped, and the window keeps telling `3.0.0`'s news. Nothing slower goes in any of them: the suite, the build and the markdown lint are minutes, and they belong to CI. diff --git a/app/src/news.json b/app/src/news.json index 9057b4c0..b1932e8e 100644 --- a/app/src/news.json +++ b/app/src/news.json @@ -1,35 +1,4 @@ [ - { - "version": "3.0.1", - "es": [ - { - "title": "Abrir la app te recuerda el atajo", - "said": "si abres CopyPaste cuando ya está funcionando, te muestra cómo llamar al panel y te deja probarlo." - }, - { - "title": "Mejor en Mac", - "said": "el panel se oculta al hacer clic fuera, Ajustes usa los botones de siempre de macOS y pegar es más fiable." - }, - { - "title": "Un atajo nuevo en Mac", - "said": "ahora es ⌃⌥V, igual que en Windows; si usabas ⌥⌘V, cambia solo." - } - ], - "en": [ - { - "title": "Opening the app reminds you of the shortcut", - "said": "open CopyPaste while it is already running and it shows you how to call the panel, and lets you try it." - }, - { - "title": "Better on a Mac", - "said": "the panel hides when you click elsewhere, Settings uses the usual macOS buttons, and pasting is more reliable." - }, - { - "title": "A new shortcut on a Mac", - "said": "it is now ⌃⌥V, the same as on Windows; if you used ⌥⌘V, it changes on its own." - } - ] - }, { "version": "3.0.0-rc2", "es": [ diff --git a/scripts/news.sh b/scripts/news.sh index d97a793e..0c39d460 100755 --- a/scripts/news.sh +++ b/scripts/news.sh @@ -66,6 +66,15 @@ sys.exit(1) said=$(python3 -c "$reads" "$version" "$news" 2>&1) looked=$? +# a patch fixes what its minor release shipped, and the window already told that release's news +minor="${version%.*}.0" +if [ "$looked" = 1 ] && [ "$minor" != "$version" ] \ + && python3 -c "$reads" "$minor" "$news" > /dev/null 2>&1; then + printf 'ok %s is a patch of %s, whose entry in %s still says what it brings\n' \ + "$version" "$minor" "$news" + exit 0 +fi + case $looked in 0) printf 'ok %s is in %s, in both languages\n' "$version" "$news" From 0e06152f446f41faa884319e2c7cbac61f726f1e Mon Sep 17 00:00:00 2001 From: rgdevment Date: Tue, 6 Oct 2026 04:27:24 -0300 Subject: [PATCH 3/6] feat(gui): someone coming from 2.x ends the welcome with what the newest release brings --- app/src/tests/tour.test.tsx | 5 ++++- app/src/tests/welcome.test.ts | 10 +++++++++- app/src/ui/Welcome.tsx | 30 ++++++++++++++++++++++++++---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/src/tests/tour.test.tsx b/app/src/tests/tour.test.tsx index 40661e90..49faabdf 100644 --- a/app/src/tests/tour.test.tsx +++ b/app/src/tests/tour.test.tsx @@ -48,7 +48,10 @@ describe("la bienvenida de una instalación nueva", () => { await press("Siguiente"); expect(await screen.findByText("Lo esencial")).toBeInTheDocument(); - await press("Listo"); + await press("Siguiente"); + + expect(await screen.findByText("Hay novedades en CopyPaste")).toBeInTheDocument(); + await press("Entendido"); expect(theWindow.close).toHaveBeenCalled(); }); diff --git a/app/src/tests/welcome.test.ts b/app/src/tests/welcome.test.ts index ae022c9f..de47f115 100644 --- a/app/src/tests/welcome.test.ts +++ b/app/src/tests/welcome.test.ts @@ -2,12 +2,13 @@ import { describe, expect, it } from "vitest"; import { landing } from "../App"; import { asKeys, caps } from "../core"; import news from "../news.json"; -import { steps, toldFor } from "../ui/Welcome"; +import { newestStable, steps, toldFor } from "../ui/Welcome"; describe("la bienvenida", () => { it("lleva el historial de la 2.x antes de todo y el permiso antes del atajo", () => { expect(steps(true, true)).toEqual(["former", "hello", "trust", "keys", "where", "use"]); expect(steps(false, false)).toEqual(["hello", "keys", "where", "use"]); + expect(steps(true, false, true)).toEqual(["former", "hello", "keys", "where", "use", "news"]); }); it("dibuja el atajo tecla a tecla, con símbolos y en el orden del Mac", () => { @@ -73,4 +74,11 @@ describe("los ajustes abiertos desde la bienvenida", () => { expect(landing("#welcome")).toBe("general"); expect(landing("")).toBe("general"); }); + + it("a quien viene de la 2.x le cuenta lo nuevo de la última versión estable", () => { + const newest = newestStable(); + expect(newest).not.toBeNull(); + expect(newest).not.toContain("-"); + expect(news.some((one) => one.version === newest)).toBe(true); + }); }); diff --git a/app/src/ui/Welcome.tsx b/app/src/ui/Welcome.tsx index 62c7b9ff..ed8c25af 100644 --- a/app/src/ui/Welcome.tsx +++ b/app/src/ui/Welcome.tsx @@ -16,7 +16,7 @@ export type Greeting = | { kind: "keys" } | { kind: "nothing" }; -type Step = "former" | "hello" | "trust" | "keys" | "where" | "use"; +type Step = "former" | "hello" | "trust" | "keys" | "where" | "use" | "news"; type Former = { items: number; @@ -28,7 +28,7 @@ type Former = { type Told = { title: string; said: string }; -export function steps(former: boolean, asksTrust: boolean): Step[] { +export function steps(former: boolean, asksTrust: boolean, tellsNews = false): Step[] { return [ ...(former ? (["former"] as const) : []), "hello", @@ -36,9 +36,20 @@ export function steps(former: boolean, asksTrust: boolean): Step[] { "keys", "where", "use", + ...(tellsNews ? (["news"] as const) : []), ]; } +export function newestStable(): string | null { + const stable = news.map((one) => one.version).filter((one) => !one.includes("-")); + const parts = (one: string) => one.split(".").map(Number); + stable.sort((a, b) => { + const [x, y] = [parts(a), parts(b)]; + return y[0] - x[0] || y[1] - x[1] || y[2] - x[2]; + }); + return stable[0] ?? null; +} + export function toldFor( versions: string[], english: boolean, @@ -138,7 +149,11 @@ function Tour({ const worthBringing = !!old && !old.unreadable && old.items > 0 && (old.came ?? 0) === 0; const asksTrust = !!trustAtStart?.offered && !trustAtStart.pastes; - const all = useMemo(() => steps(worthBringing, asksTrust), [worthBringing, asksTrust]); + const newest = former ? newestStable() : null; + const all = useMemo( + () => steps(worthBringing, asksTrust, newest !== null), + [worthBringing, asksTrust, newest], + ); if (!looked || !trustLooked) { return null; @@ -172,6 +187,8 @@ function Tour({ ); case "trust": return ; + case "news": + return newest ? : null; case "keys": return ; case "where": @@ -214,7 +231,12 @@ function Tour({ }} /> } - right={} + right={ + + } > Date: Tue, 6 Oct 2026 04:31:03 -0300 Subject: [PATCH 4/6] test(gui): a fresh install still closes the welcome on its last step --- app/src/tests/tour.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/tests/tour.test.tsx b/app/src/tests/tour.test.tsx index 49faabdf..65992379 100644 --- a/app/src/tests/tour.test.tsx +++ b/app/src/tests/tour.test.tsx @@ -55,6 +55,22 @@ describe("la bienvenida de una instalación nueva", () => { expect(theWindow.close).toHaveBeenCalled(); }); + it("una instalación nueva termina en lo esencial y se cierra con Listo", async () => { + asked.greeting = { kind: "tour", former: false }; + render(); + + await press("Comenzar"); + const shown = await heardOn("panel-shown"); + act(() => shown({ payload: null })); + await press("Siguiente"); + await press("Siguiente"); + + expect(await screen.findByText("Lo esencial")).toBeInTheDocument(); + expect(screen.queryByText("Hay novedades en CopyPaste")).toBeNull(); + await press("Listo"); + expect(theWindow.close).toHaveBeenCalled(); + }); + it("ofrece los atajos libres cuando el suyo está ocupado", async () => { asked.greeting = { kind: "tour", former: false }; asked.bound = false; From ff9ea0bb94fcfe87d05ca9942201f3a6be2c268b Mon Sep 17 00:00:00 2001 From: rgdevment Date: Tue, 6 Oct 2026 04:35:19 -0300 Subject: [PATCH 5/6] test(gui): the window's tests are named in English, and a rule keeps them so --- CONTRIBUTING.md | 2 +- app/src/tests/bindings.test.ts | 18 ++++---- app/src/tests/kept.test.tsx | 6 +-- app/src/tests/keys.test.ts | 8 ++-- app/src/tests/links.test.ts | 6 +-- app/src/tests/notices.test.tsx | 22 +++++----- app/src/tests/prose.test.ts | 10 ++--- app/src/tests/rail.test.tsx | 6 +-- app/src/tests/refusals.test.tsx | 8 ++-- app/src/tests/theme.test.ts | 8 ++-- app/src/tests/tour.test.tsx | 36 +++++++-------- app/src/tests/welcome.test.ts | 20 ++++----- app/src/tests/window.test.tsx | 78 ++++++++++++++++----------------- scripts/rules.sh | 10 +++++ 14 files changed, 124 insertions(+), 114 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51f56ce2..6e6d8965 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,7 +121,7 @@ We keep the code simple and consistent: `scripts/rules.sh` holds the conventions a person can break in a second: a comment where the code should speak for itself, `unsafe` outside the `-sys` crates, the core printing to a terminal or reaching for a platform, Spanish in -an identifier, two crates naming an example the same, and a file grown past +an identifier or in the name of a test of the window, two crates naming an example the same, and a file grown past what anybody reads through. `scripts/commits.sh` holds the commit convention below. Both answer the same whether you run them or CI does, and they say every rule that broke diff --git a/app/src/tests/bindings.test.ts b/app/src/tests/bindings.test.ts index f333643a..7c64431d 100644 --- a/app/src/tests/bindings.test.ts +++ b/app/src/tests/bindings.test.ts @@ -157,8 +157,8 @@ function rowsOn(mac: boolean): Binding[] { }); } -describe("la tabla de atajos", () => { - it("promete solo teclas que el panel mira de verdad, en Windows y en un Mac", () => { +describe("the shortcut table", () => { + it("promises only keys the panel really watches, on Windows and on a Mac", () => { for (const mac of [false, true]) { const where = mac ? "macOS" : "Windows"; for (const row of rowsOn(mac)) { @@ -177,7 +177,7 @@ describe("la tabla de atajos", () => { } }); - it("dice en voz alta lo que en un Mac solo se dibuja", () => { + it("says out loud what a Mac only draws", () => { for (const tongue of ["es", "en"] as const) { adopt(tongue); for (const row of panelKeys(true)) { @@ -197,7 +197,7 @@ describe("la tabla de atajos", () => { expect(panelKeys(true).filter((one) => one.id === "remove")[0].said).toBe("Command Backspace"); }); - it("no ofrece en un Mac teclas que su teclado no tiene", () => { + it("does not offer a Mac keys its keyboard lacks", () => { const forbidden = [/\bF\d/, /\bSupr\b/, /\bDelete\b/, /\bCtrl\b/, /\bAlt\b/, /\bShift\b/]; for (const row of rowsOn(true)) { for (const one of forbidden) { @@ -208,7 +208,7 @@ describe("la tabla de atajos", () => { } }); - it("lo que solo responde en un Mac va detrás de la plataforma", () => { + it("what only answers on a Mac sits behind the platform", () => { const only = ['event.text == ","', "event.text == Key.Backspace && event.modifiers.control"]; for (const one of only) { const branch = BRANCHES.find((said) => said.includes(one)); @@ -224,7 +224,7 @@ describe("la tabla de atajos", () => { ); }); - it("dice lo mismo en los dos idiomas, fila por fila", () => { + it("says the same in both languages, row by row", () => { for (const mac of [false, true]) { adopt("es"); const es = panelKeys(mac); @@ -240,7 +240,7 @@ describe("la tabla de atajos", () => { } }); - it("un clic con modificador lo lee de verdad lo que se pincha", () => { + it("a click with a modifier really reads what is clicked", () => { const cards = ui("cards.slint"); // the layers row tells this one in words instead of naming a key, so nothing above reaches it expect(cards, "no clic suma tipos en vez de cambiarlos").toContain( @@ -265,7 +265,7 @@ describe("la tabla de atajos", () => { } }); - it("el botón que promete sumar tipos existe en el panel", () => { + it("the button that promises to add kinds exists in the panel", () => { for (const tongue of ["es", "en"] as const) { adopt(tongue); const row = panelKeys(false).find((one) => @@ -276,7 +276,7 @@ describe("la tabla de atajos", () => { expect(PANEL, "nothing in the panel toggles the mode").toContain("keep-toggled"); }); - it("filtra por tipos que el buscador conoce", () => { + it("filters by kinds the search box knows", () => { const view = readFileSync( join(__dirname, "..", "..", "..", "crates", "cp-panel", "src", "view.rs"), "utf8", diff --git a/app/src/tests/kept.test.tsx b/app/src/tests/kept.test.tsx index 73d5cd6e..c10f9f46 100644 --- a/app/src/tests/kept.test.tsx +++ b/app/src/tests/kept.test.tsx @@ -22,12 +22,12 @@ async function heardOn(name: string): Promise { return call[1]; } -describe("los ajustes guardados desde otra ventana", () => { +describe("settings saved from another window", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("se adoptan cuando esta ventana no tiene nada por guardar", async () => { + it("are adopted when this window has nothing left to save", async () => { const { result } = renderHook(() => useKept()); await waitFor(() => expect(result.current.kept).not.toBeNull()); @@ -36,7 +36,7 @@ describe("los ajustes guardados desde otra ventana", () => { expect(result.current.kept?.["keeps-days"]).toBe(7); }); - it("no pisan un cambio de esta ventana que todavía se está guardando", async () => { + it("do not overwrite a change of this window that is still being saved", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); let release: () => void = () => {}; diff --git a/app/src/tests/keys.test.ts b/app/src/tests/keys.test.ts index e981f55b..8c91fb05 100644 --- a/app/src/tests/keys.test.ts +++ b/app/src/tests/keys.test.ts @@ -11,20 +11,20 @@ function press(code: string, held: Partial { - it("se escribe como el sistema la espera", () => { +describe("the combination the user presses", () => { + it("is written the way the system expects it", () => { expect(combination(press("KeyV", { ctrl: true, alt: true }))).toBe("Ctrl+Alt+V"); expect(combination(press("F9", { ctrl: true, alt: true }))).toBe("Ctrl+Alt+F9"); expect(combination(press("Digit1", { ctrl: true, shift: true }))).toBe("Ctrl+Shift+1"); expect(combination(press("Space", { meta: true, alt: true }))).toBe("Alt+Cmd+Space"); }); - it("no acepta una tecla suelta, que secuestraría el teclado entero", () => { + it("refuses a lone key, which would hijack the whole keyboard", () => { expect(combination(press("KeyV", {}))).toBeNull(); expect(combination(press("F9", {}))).toBeNull(); }); - it("no acepta modificadores sin una tecla de verdad", () => { + it("refuses modifiers without a real key", () => { expect(combination(press("ControlLeft", { ctrl: true }))).toBeNull(); expect(combination(press("AltLeft", { ctrl: true, alt: true }))).toBeNull(); }); diff --git a/app/src/tests/links.test.ts b/app/src/tests/links.test.ts index e9f54928..a9cd6071 100644 --- a/app/src/tests/links.test.ts +++ b/app/src/tests/links.test.ts @@ -27,8 +27,8 @@ function covers(pattern: string, url: string): boolean { return url.startsWith(pattern.slice(0, star)) && url.endsWith(pattern.slice(star + 1)); } -describe("los enlaces del acerca de", () => { - it("todos están permitidos, así que ninguno nace muerto", () => { +describe("the links in About", () => { + it("are all allowed, so none is born dead", () => { const patterns = allowed(); for (const url of [...LINKS, ...TOOLS]) { expect( @@ -38,7 +38,7 @@ describe("los enlaces del acerca de", () => { } }); - it("las otras herramientas llevan a su propia página, no al repositorio", () => { + it("the other tools lead to their own page, not to the repository", () => { for (const url of TOOLS) { expect(url.startsWith("https://rgdevment.com/")).toBe(true); } diff --git a/app/src/tests/notices.test.tsx b/app/src/tests/notices.test.tsx index eda8cce4..7405e908 100644 --- a/app/src/tests/notices.test.tsx +++ b/app/src/tests/notices.test.tsx @@ -22,7 +22,7 @@ async function answering(notices: () => Promise) { }; } -describe("los avisos que cada licencia empaquetada pide", () => { +describe("the notices each bundled licence asks for", () => { const undo: (() => void)[] = []; afterEach(() => { @@ -31,7 +31,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { } }); - it("se muestran desde la ventana, y solo se piden cuando se abren", async () => { + it("are shown from the window, and only fetched when opened", async () => { const who = userEvent.setup(); const said = await answering(() => Promise.resolve("MIT License\n\nCopyright (c) alguien")); undo.push(said.undo); @@ -52,7 +52,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(button.getAttribute("aria-expanded")).toBe("false"); }); - it("se dibujan como texto, no como el markdown en que están escritos", async () => { + it("are drawn as text, not as the markdown they are written in", async () => { const who = userEvent.setup(); const said = await answering(() => Promise.resolve( @@ -71,7 +71,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(screen.queryByText(/Do not edit by hand/)).toBeNull(); }); - it("no ejecutan lo que traigan escrito como html", async () => { + it("do not run whatever html they carry", async () => { const who = userEvent.setup(); const said = await answering(() => Promise.resolve(" y nada más")); undo.push(said.undo); @@ -83,7 +83,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(container.querySelector(".notices img")).toBeNull(); }); - it("un enlace de dentro se abre fuera, sin llevarse la ventana", async () => { + it("a link inside opens outside, without taking the window with it", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); const said = await answering(() => Promise.resolve("Ver https://crates.io/crates/slint")); @@ -96,7 +96,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(invoke).toHaveBeenCalledWith("open_web", { url: "https://crates.io/crates/slint" }); }); - it("todo enlace del archivo de verdad lleva su sitio, porque uno relativo apunta a la ventana", () => { + it("every link in the real file names its site, because a relative one points at the window", () => { const holder = document.createElement("div"); holder.innerHTML = composed(readFileSync("../THIRD-PARTY-BUNDLED.md", "utf8")); const hrefs = Array.from(holder.querySelectorAll("a")).map((one) => one.getAttribute("href")); @@ -106,7 +106,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { } }); - it("un reintento que sale bien se lleva el aviso del fallo anterior", async () => { + it("a retry that works clears the notice of the earlier failure", async () => { const who = userEvent.setup(); let tries = 0; const said = await answering(() => { @@ -125,7 +125,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(screen.queryByText("No se pudieron leer los avisos de terceros")).toBeNull(); }); - it("los textos de las licencias tienen su propio botón, y solo se piden al abrirlo", async () => { + it("the licence texts have their own button, and are only fetched when it opens", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation() as Invoke; @@ -156,7 +156,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { }); }); - it("si los textos no se pueden leer, lo dice", async () => { + it("says so when the texts cannot be read", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation() as Invoke; @@ -172,7 +172,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { ).toBeDefined(); }); - it("todo enlace de los textos de verdad lleva su sitio", () => { + it("every link in the real texts names its site", () => { const holder = document.createElement("div"); holder.innerHTML = composed(readFileSync("../THIRD-PARTY-LICENSES.md", "utf8")); for (const href of Array.from(holder.querySelectorAll("a")).map((one) => @@ -183,7 +183,7 @@ describe("los avisos que cada licencia empaquetada pide", () => { expect(holder.querySelectorAll("pre").length).toBeGreaterThan(100); }); - it("si no se pueden leer, lo dice en vez de quedarse callado", async () => { + it("says so when they cannot be read instead of staying silent", async () => { const who = userEvent.setup(); const said = await answering(() => Promise.reject(new Error("no"))); undo.push(said.undo); diff --git a/app/src/tests/prose.test.ts b/app/src/tests/prose.test.ts index 133894d2..aaa1c121 100644 --- a/app/src/tests/prose.test.ts +++ b/app/src/tests/prose.test.ts @@ -3,24 +3,24 @@ import { t } from "../locales"; import { asProse } from "../ui/Bits"; describe("asProse", () => { - it("cierra cada frase, que sueltas se leen pegadas", () => { + it("closes every sentence, since loose ones read glued together", () => { expect(asProse(["una cosa", "otra cosa"])).toBe("una cosa. otra cosa."); }); - it("no dobla el punto de una frase que ya lo trae", () => { + it("does not double the full stop of a sentence that already has one", () => { expect(asProse(["ya termina.", "y esta no"])).toBe("ya termina. y esta no."); }); - it("respeta los dos puntos y los signos de cierre", () => { + it("respects colons and closing marks", () => { expect(asProse(["mira esto:", "¿seguro?", "¡claro!"])).toBe("mira esto: ¿seguro? ¡claro!"); }); - it("descarta lo vacio en vez de dejar un punto suelto", () => { + it("drops what is empty instead of leaving a stray full stop", () => { expect(asProse(["", " ", "solo esto"])).toBe("solo esto."); expect(asProse([])).toBe(""); }); - it("deja el resumen de CopyPaste 2 como un parrafo con puntos", () => { + it("leaves the CopyPaste 2 summary as one paragraph with full stops", () => { const said = asProse([ t("formerKeeps"), t("formerLosesPlain"), diff --git a/app/src/tests/rail.test.tsx b/app/src/tests/rail.test.tsx index 978f4d66..890bf462 100644 --- a/app/src/tests/rail.test.tsx +++ b/app/src/tests/rail.test.tsx @@ -13,12 +13,12 @@ async function heardOn(name: string): Promise { return (call as [string, Heard])[1]; } -describe("la ventana", () => { +describe("the window", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("aterriza en los atajos cuando el panel pide los ajustes", async () => { + it("lands on the shortcuts when the panel asks for settings", async () => { render(); await screen.findByLabelText("Idioma"); @@ -30,7 +30,7 @@ describe("la ventana", () => { expect(await screen.findByText("Atajo del panel")).toBeDefined(); }); - it("no se mueve si le piden una sección que no existe", async () => { + it("does not move when asked for a section that does not exist", async () => { render(); await screen.findByLabelText("Idioma"); diff --git a/app/src/tests/refusals.test.tsx b/app/src/tests/refusals.test.tsx index 27c93784..48ec3065 100644 --- a/app/src/tests/refusals.test.tsx +++ b/app/src/tests/refusals.test.tsx @@ -15,7 +15,7 @@ async function insteadOf(handle: (what: string, real?: Invoke) => Promise vi.mocked(invoke).mockImplementation(real as never); } -describe("cuando el backend dice que no", () => { +describe("when the backend says no", () => { const undo: (() => void)[] = []; afterEach(() => { @@ -24,7 +24,7 @@ describe("cuando el backend dice que no", () => { } }); - it("muestra el motivo y vuelve a leer lo que quedó guardado", async () => { + it("shows the reason and reads back what was saved", async () => { const who = userEvent.setup(); undo.push( await insteadOf((what) => @@ -46,7 +46,7 @@ describe("cuando el backend dice que no", () => { }); }); - it("calla sobre el atajo si ni siquiera puede preguntar", async () => { + it("says nothing about the shortcut if it cannot even ask", async () => { undo.push( await insteadOf((what) => (what === "keys" ? Promise.reject(new Error("no answer")) : null)), ); @@ -57,7 +57,7 @@ describe("cuando el backend dice que no", () => { expect(screen.queryByText(/Otro programa ya usa/)).toBeNull(); }); - it("avisa del atajo tomado aunque no consiga proponer otros", async () => { + it("warns the shortcut is taken even when it cannot offer others", async () => { undo.push( await insteadOf((what) => { if (what === "keys") { diff --git a/app/src/tests/theme.test.ts b/app/src/tests/theme.test.ts index 7e97c249..680bd75c 100644 --- a/app/src/tests/theme.test.ts +++ b/app/src/tests/theme.test.ts @@ -3,19 +3,19 @@ import { describe, expect, it } from "vitest"; const sheet = readFileSync("src/index.css", "utf8"); -describe("los tres temas", () => { - it("el claro es la base, así que sin elección no se cae al oscuro", () => { +describe("the three themes", () => { + it("light is the base, so with no choice it does not fall to dark", () => { const root = sheet.slice(sheet.indexOf(":root {"), sheet.indexOf("}")); expect(root).toContain("--panel: #f7f8fb"); expect(root).toContain("color-scheme: light"); }); - it("el del sistema sigue al sistema", () => { + it("the system one follows the system", () => { expect(sheet).toContain("@media (prefers-color-scheme: dark)"); expect(sheet).toContain(':root:not([data-theme="light"])'); }); - it("elegir uno gana sobre lo que diga el sistema, en los dos sentidos", () => { + it("choosing one wins over what the system says, both ways", () => { expect(sheet).toContain(':root[data-theme="dark"]'); const guarded = sheet.indexOf(':root:not([data-theme="light"])'); expect(guarded).toBeGreaterThan(-1); diff --git a/app/src/tests/tour.test.tsx b/app/src/tests/tour.test.tsx index 65992379..165871a8 100644 --- a/app/src/tests/tour.test.tsx +++ b/app/src/tests/tour.test.tsx @@ -18,12 +18,12 @@ async function press(name: string) { fireEvent.click(await screen.findByRole("button", { name })); } -describe("la bienvenida de una instalación nueva", () => { +describe("the welcome of a fresh install", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("trae la 2.x, enseña el atajo hasta que se prueba y se cierra al final", async () => { + it("brings the 2.x over, teaches the shortcut until it is tried and closes at the end", async () => { render(); expect(await screen.findByText("Tu historial anterior te espera")).toBeInTheDocument(); @@ -55,7 +55,7 @@ describe("la bienvenida de una instalación nueva", () => { expect(theWindow.close).toHaveBeenCalled(); }); - it("una instalación nueva termina en lo esencial y se cierra con Listo", async () => { + it("a fresh install ends on the essentials and closes with Done", async () => { asked.greeting = { kind: "tour", former: false }; render(); @@ -71,7 +71,7 @@ describe("la bienvenida de una instalación nueva", () => { expect(theWindow.close).toHaveBeenCalled(); }); - it("ofrece los atajos libres cuando el suyo está ocupado", async () => { + it("offers the free shortcuts when its own is taken", async () => { asked.greeting = { kind: "tour", former: false }; asked.bound = false; const { invoke } = await import("@tauri-apps/api/core"); @@ -88,7 +88,7 @@ describe("la bienvenida de una instalación nueva", () => { }); }); - it("omitir desde el principio cierra la ventana sin recorrer nada", async () => { + it("skipping from the start closes the window without going through anything", async () => { asked.greeting = { kind: "tour", former: false }; render(); @@ -96,7 +96,7 @@ describe("la bienvenida de una instalación nueva", () => { expect(theWindow.close).toHaveBeenCalled(); }); - it("no tiene botones de ventana: se sale con los suyos", async () => { + it("has no window buttons: you leave with its own", async () => { asked.greeting = { kind: "tour", former: false }; render(); @@ -117,7 +117,7 @@ function asAMac() { }; } -describe("la bienvenida en un Mac", () => { +describe("the welcome on a Mac", () => { let back = () => {}; beforeEach(() => { @@ -129,7 +129,7 @@ describe("la bienvenida en un Mac", () => { afterEach(() => back()); - it("pide el permiso antes del atajo y sigue sola cuando se concede", async () => { + it("asks for permission before the shortcut and moves on alone once it is granted", async () => { const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -157,7 +157,7 @@ describe("la bienvenida en un Mac", () => { expect(await screen.findByText("Pruébalo ahora", {}, { timeout: 4_000 })).toBeInTheDocument(); }, 20_000); - it("enseña la barra de menús y lo esencial con las teclas del Mac", async () => { + it("shows the menu bar and the essentials with the Mac keys", async () => { asked.trust = { offered: true, pastes: true, secureInput: false }; render(); @@ -177,7 +177,7 @@ describe("la bienvenida en un Mac", () => { expect(screen.queryByText("F1")).toBeNull(); }); - it("con el permiso ya concedido el paso no aparece", async () => { + it("with permission already granted the step does not appear", async () => { asked.trust = { offered: true, pastes: true, secureInput: false }; render(); @@ -187,12 +187,12 @@ describe("la bienvenida en un Mac", () => { }); }); -describe("lo que cambia con la ventana abierta", () => { +describe("what changes while the window is open", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("sin nada que mostrar se cierra sola en vez de quedar en blanco", async () => { + it("with nothing to show it closes itself instead of staying blank", async () => { asked.greeting = { kind: "nothing" }; render(); @@ -201,7 +201,7 @@ describe("lo que cambia con la ventana abierta", () => { }); }); - it("pasa de las novedades al recorrido cuando se pide desde Acerca de", async () => { + it("goes from the news to the tour when asked from About", async () => { asked.greeting = { kind: "news", versions: ["3.0.0"] }; render(); @@ -211,7 +211,7 @@ describe("lo que cambia con la ventana abierta", () => { expect(await screen.findByText("Tu portapapeles, con memoria")).toBeInTheDocument(); }); - it("sigue lo que otra ventana guardó, para no pisarlo después", async () => { + it("follows what another window saved, so as not to overwrite it later", async () => { asked.greeting = { kind: "tour", former: false }; render(); @@ -233,12 +233,12 @@ describe("lo que cambia con la ventana abierta", () => { }); }); -describe("las novedades tras una actualización", () => { +describe("the news after an update", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("dicen lo nuevo en el idioma elegido y se cierran con un botón", async () => { + it("tells what is new in the chosen language and closes with a button", async () => { asked.greeting = { kind: "news", versions: ["3.0.0"] }; asked.locale = "en"; render(); @@ -250,12 +250,12 @@ describe("las novedades tras una actualización", () => { }); }); -describe("abrir la app cuando ya está corriendo", () => { +describe("opening the app while it is already running", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("enseña solo el atajo, sin el recorrido, y se cierra con un botón", async () => { + it("teaches only the shortcut, without the tour, and closes with a button", async () => { asked.greeting = { kind: "keys" }; asked.locale = "en"; render(); diff --git a/app/src/tests/welcome.test.ts b/app/src/tests/welcome.test.ts index de47f115..a384bd47 100644 --- a/app/src/tests/welcome.test.ts +++ b/app/src/tests/welcome.test.ts @@ -4,21 +4,21 @@ import { asKeys, caps } from "../core"; import news from "../news.json"; import { newestStable, steps, toldFor } from "../ui/Welcome"; -describe("la bienvenida", () => { - it("lleva el historial de la 2.x antes de todo y el permiso antes del atajo", () => { +describe("the welcome", () => { + it("brings the 2.x history before anything else and the permission before the shortcut", () => { expect(steps(true, true)).toEqual(["former", "hello", "trust", "keys", "where", "use"]); expect(steps(false, false)).toEqual(["hello", "keys", "where", "use"]); expect(steps(true, false, true)).toEqual(["former", "hello", "keys", "where", "use", "news"]); }); - it("dibuja el atajo tecla a tecla, con símbolos y en el orden del Mac", () => { + it("draws the shortcut key by key, with symbols and in the Mac order", () => { expect(caps("Ctrl+Alt+V", false)).toEqual(["Ctrl", "Alt", "V"]); expect(caps("Cmd+Alt+V", true)).toEqual(["⌥", "⌘", "V"]); expect(caps("Shift+Ctrl+Space", true)).toEqual(["⌃", "⇧", "Space"]); expect(caps("", false)).toEqual([]); }); - it("junta las teclas sin pegar una palabra a un símbolo", () => { + it("joins the keys without gluing a word to a symbol", () => { expect(asKeys("Ctrl+Alt+V", false)).toBe("Ctrl + Alt + V"); expect(asKeys("Cmd+Alt+V", true)).toBe("⌥⌘V"); expect(asKeys("Shift+Cmd+Space", true)).toBe("⇧⌘ Space"); @@ -26,7 +26,7 @@ describe("la bienvenida", () => { expect(asKeys("Super+V", true), "what nothing maps is left as it was stored").toBe("Super V"); }); - it("cuenta solo las novedades de las versiones pedidas, en el idioma de la ventana", () => { + it("tells only the news of the versions asked for, in the window's language", () => { const [first] = news; expect(toldFor([first.version], false, false)[0].told.map((one) => one.title)).toEqual( first.es.map((one) => one.title), @@ -37,7 +37,7 @@ describe("la bienvenida", () => { expect(toldFor(["0.0.1"], false, false)).toEqual([]); }); - it("nombra la tecla de cada plataforma, y no deja un hueco sin rellenar", () => { + it("names each platform's key, and leaves no gap unfilled", () => { for (const one of news) { for (const mac of [false, true]) { for (const english of [false, true]) { @@ -56,7 +56,7 @@ describe("la bienvenida", () => { expect(onWindows.told.some((one) => one.said.includes("F2"))).toBe(true); }); - it("dice lo mismo en español y en inglés para cada versión", () => { + it("says the same in Spanish and in English for every version", () => { for (const one of news) { expect(one.es.length, one.version).toBe(one.en.length); for (const said of [...one.es, ...one.en]) { @@ -67,15 +67,15 @@ describe("la bienvenida", () => { }); }); -describe("los ajustes abiertos desde la bienvenida", () => { - it("aterrizan en la sección pedida, y en General si no existe", () => { +describe("settings opened from the welcome", () => { + it("land on the section asked for, and on General if it does not exist", () => { expect(landing("#keys")).toBe("keys"); expect(landing("#about")).toBe("about"); expect(landing("#welcome")).toBe("general"); expect(landing("")).toBe("general"); }); - it("a quien viene de la 2.x le cuenta lo nuevo de la última versión estable", () => { + it("tells someone coming from 2.x what the newest stable release brings", () => { const newest = newestStable(); expect(newest).not.toBeNull(); expect(newest).not.toContain("-"); diff --git a/app/src/tests/window.test.tsx b/app/src/tests/window.test.tsx index 93309fd0..3c5b92b4 100644 --- a/app/src/tests/window.test.tsx +++ b/app/src/tests/window.test.tsx @@ -4,19 +4,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "../App"; import { adopt } from "../locales"; -describe("la ventana", () => { +describe("the window", () => { beforeEach(() => { adopt("es"); document.documentElement.removeAttribute("data-theme"); }); - it("abre en General, que es donde está lo que se toca una vez", async () => { + it("opens on General, which is where the things touched once live", async () => { render(); expect(await screen.findByRole("heading", { level: 1 })).toHaveTextContent("General"); expect(await screen.findByLabelText("Idioma")).toBeDefined(); }); - it("el atajo y lo que responde el panel viven en su propia sección", async () => { + it("the shortcut and what the panel answers live in their own section", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Atajos de teclado" })); @@ -25,7 +25,7 @@ describe("la ventana", () => { expect(screen.getByText("Doble clic")).toBeDefined(); }); - it("ofrece cuatro secciones y el acerca de, y nada más", () => { + it("offers four sections and About, and nothing else", () => { render(); const rail = screen.getByRole("navigation", { name: "Secciones" }); const says = buttonsIn(rail).map((one) => one.textContent); @@ -38,7 +38,7 @@ describe("la ventana", () => { ]); }); - it("cambia de sección al elegirla", async () => { + it("switches section when one is chosen", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Historial" })); @@ -46,7 +46,7 @@ describe("la ventana", () => { expect(await screen.findByLabelText("Conservar")).toHaveValue("30"); }); - it("el tema elegido se escribe en la raíz, que es lo que lo pinta", async () => { + it("the chosen theme is written on the root, which is what paints it", async () => { const who = userEvent.setup(); render(); await who.selectOptions(await screen.findByLabelText("Tema"), "light"); @@ -55,7 +55,7 @@ describe("la ventana", () => { expect(document.documentElement.hasAttribute("data-theme")).toBe(false); }); - it("lo que se cambia se manda a guardar, no se queda en la ventana", async () => { + it("what changes is sent to be saved, it does not stay in the window", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -65,7 +65,7 @@ describe("la ventana", () => { }); }); - it("la barra propia minimiza y cierra de verdad, no solo lo dibuja", async () => { + it("its own title bar really minimizes and closes, it does not just draw them", async () => { const who = userEvent.setup(); const { theWindow } = await import("./setup"); render(); @@ -75,7 +75,7 @@ describe("la ventana", () => { expect(theWindow.close).toHaveBeenCalled(); }); - it("elegir «Siempre» se guarda como cero, que es lo que el archivo entiende", async () => { + it("choosing «Forever» is saved as zero, which is what the file understands", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -87,7 +87,7 @@ describe("la ventana", () => { expect(screen.queryByText(/invalid type/)).toBeNull(); }); - it("el arranque con la sesión lo decide el sistema, no el archivo", async () => { + it("starting with the session is decided by the system, not by the file", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -101,7 +101,7 @@ describe("la ventana", () => { ); }); - it("avisa cuando el panel no está funcionando, en vez de callarlo", async () => { + it("warns when the panel is not working, instead of hiding it", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation(((what: string, args?: never) => @@ -114,13 +114,13 @@ describe("la ventana", () => { vi.mocked(invoke).mockImplementation(real as never); }); - it("no inventa problemas del panel cuando todo va bien", async () => { + it("does not invent panel problems when all is well", async () => { render(); await screen.findByRole("button", { name: "General" }); expect(screen.queryByText(/no se está guardando/)).toBeNull(); }); - it("dice que el atajo no responde cuando otro programa lo tiene tomado", async () => { + it("says the shortcut does not answer when another program has taken it", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation(((what: string, args?: never) => @@ -134,7 +134,7 @@ describe("la ventana", () => { vi.mocked(invoke).mockImplementation(real as never); }); - it("ofrece las combinaciones libres y adopta la que se pulsa", async () => { + it("offers the free combinations and adopts the one pressed", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); @@ -157,7 +157,7 @@ describe("la ventana", () => { vi.mocked(invoke).mockImplementation(real as never); }); - it("calla sobre el atajo cuando el sistema sí lo cedió", async () => { + it("says nothing about the shortcut when the system did give it up", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Atajos de teclado" })); @@ -165,7 +165,7 @@ describe("la ventana", () => { expect(screen.queryByText(/Otro programa ya usa/)).toBeNull(); }); - it("un tope de imágenes que no está en la lista se muestra tal cual", async () => { + it("an image cap that is not in the list is shown as it is", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation(((what: string, args?: never) => @@ -187,7 +187,7 @@ describe("la ventana", () => { vi.mocked(invoke).mockImplementation(real as never); }); - it("exportar escribe el archivo que se elija y dice cuánto guardó", async () => { + it("exporting writes the chosen file and says how much it kept", async () => { const { invoke } = await import("@tauri-apps/api/core"); const { save } = await import("@tauri-apps/plugin-dialog"); render(); @@ -201,7 +201,7 @@ describe("la ventana", () => { expect(await screen.findByText("Guardado: 3 elementos")).toBeDefined(); }); - it("importar trae lo que falta y no duplica lo que ya estaba", async () => { + it("importing brings what is missing and does not duplicate what was there", async () => { const { invoke } = await import("@tauri-apps/api/core"); render(); await userEvent.click(await screen.findByRole("button", { name: "Copia de seguridad" })); @@ -215,7 +215,7 @@ describe("la ventana", () => { ).toBeDefined(); }); - it("dice lo que se pierde antes de traer el historial de la 2", async () => { + it("says what is lost before bringing the 2.x history over", async () => { render(); await userEvent.click(await screen.findByRole("button", { name: "Copia de seguridad" })); expect(await screen.findByText("1200 elementos guardados en CopyPaste 2")).toBeDefined(); @@ -225,7 +225,7 @@ describe("la ventana", () => { expect(screen.getByText(/Nada de CopyPaste 2 se toca ni se borra/)).toBeDefined(); }); - it("avisa de lo que el tiempo que guardas se llevará apenas llegue", async () => { + it("warns about what the time you keep will take as soon as it arrives", async () => { render(); await userEvent.click(await screen.findByRole("button", { name: "Copia de seguridad" })); expect( @@ -235,7 +235,7 @@ describe("la ventana", () => { expect(screen.getByText(/El panel se detiene mientras cruza/)).toBeDefined(); }); - it("trae el historial y cuenta lo que llegó y lo que no", async () => { + it("brings the history over and tells what arrived and what did not", async () => { const { invoke } = await import("@tauri-apps/api/core"); const who = userEvent.setup(); render(); @@ -249,7 +249,7 @@ describe("la ventana", () => { ).toBeDefined(); }); - it("una vez cruzada, la 2 deja de ofrecerse y dice cuándo fue", async () => { + it("once crossed, the 2.x stops being offered and says when it was", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation(async (what: string, args?: unknown) => { @@ -277,7 +277,7 @@ describe("la ventana", () => { } }); - it("borrar los datos de la 2 pide confirmación antes de tocar nada", async () => { + it("deleting the 2.x data asks for confirmation before touching anything", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -303,7 +303,7 @@ describe("la ventana", () => { } }); - it("cambiar el atajo guarda la combinación que se presiona", async () => { + it("changing the shortcut saves the combination pressed", async () => { const { invoke } = await import("@tauri-apps/api/core"); render(); await userEvent.click(screen.getByRole("button", { name: "Atajos de teclado" })); @@ -319,7 +319,7 @@ describe("la ventana", () => { expect(said?.config.shortcut).toBe("Ctrl+Alt+F9"); }); - it("vaciar el historial pide confirmación antes de hacerlo", async () => { + it("emptying the history asks for confirmation before doing it", async () => { const { invoke } = await import("@tauri-apps/api/core"); render(); await userEvent.click(await screen.findByRole("button", { name: "Historial" })); @@ -330,7 +330,7 @@ describe("la ventana", () => { expect(vi.mocked(invoke).mock.calls.some(([what]) => what === "empty")).toBe(true); }); - it("no afirma que está actualizada mientras nadie lo ha comprobado", async () => { + it("does not claim to be up to date while nobody has checked", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -350,7 +350,7 @@ describe("la ventana", () => { } }); - it("una copia de la Store no afirma nada: la Store se encarga", async () => { + it("a Store copy claims nothing: the Store takes care of it", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -371,7 +371,7 @@ describe("la ventana", () => { } }); - it("dice que estás al día solo después de haber mirado", async () => { + it("says you are up to date only after having looked", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Acerca de" })); @@ -379,7 +379,7 @@ describe("la ventana", () => { expect(document.querySelector(".pip.ok")).not.toBeNull(); }); - it("ofrece instalar la versión que encontró, y la instala", async () => { + it("offers to install the version it found, and installs it", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -404,7 +404,7 @@ describe("la ventana", () => { } }); - it("si la versión ya no está, se dice y deja de ofrecerla", async () => { + it("if the version is gone, it says so and stops offering it", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -432,7 +432,7 @@ describe("la ventana", () => { } }); - it("una comprobación que falla no deja el punto en verde", async () => { + it("a check that fails does not leave the dot green", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -450,7 +450,7 @@ describe("la ventana", () => { } }); - it("con brew no ofrece instalar: dice el comando", async () => { + it("with brew it does not offer to install: it says the command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); vi.mocked(invoke).mockImplementation((what: string, args?: unknown) => { @@ -473,14 +473,14 @@ describe("la ventana", () => { } }); - it("la versión sale del propio programa, no de un texto escrito a mano", async () => { + it("the version comes from the program itself, not from text written by hand", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Acerca de" })); expect(await screen.findByText("3.0.0")).toBeDefined(); }); - it("un enlace que no se puede abrir se dice, no se traga", async () => { + it("a link that cannot be opened is said, not swallowed", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); const real = vi.mocked(invoke).getMockImplementation(); @@ -498,7 +498,7 @@ describe("la ventana", () => { } }); - it("los enlaces salen por la misma puerta, la que sabe de LinkUnbound", async () => { + it("links go out through the same door, the one that knows about LinkUnbound", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -510,7 +510,7 @@ describe("la ventana", () => { ); }); - it("sin LinkUnbound se le recomienda, y con él solo se dice que ya abre por ahí", async () => { + it("without LinkUnbound it is recommended, and with it it only says links already open through it", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -534,7 +534,7 @@ describe("la ventana", () => { } }); - it("la copia de seguridad avisa de la 2 y de lo que se pierde al traerla", async () => { + it("the backup warns about the 2.x and what is lost by bringing it over", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Copia de seguridad" })); @@ -542,7 +542,7 @@ describe("la ventana", () => { expect(screen.getByText(/El resto llega en plano/)).toBeDefined(); }); - it("elegir English cambia la ventana entera, no solo la fila del idioma", async () => { + it("choosing English changes the whole window, not just the language row", async () => { const who = userEvent.setup(); const { invoke } = await import("@tauri-apps/api/core"); render(); @@ -554,7 +554,7 @@ describe("la ventana", () => { expect(invoke).toHaveBeenCalledWith("relabel", { locale: "en" }); }); - it("el acerca de dice qué es y que no sale de aquí", async () => { + it("About says what it is and that nothing leaves this machine", async () => { const who = userEvent.setup(); render(); await who.click(screen.getByRole("button", { name: "Acerca de" })); diff --git a/scripts/rules.sh b/scripts/rules.sh index 7661fb33..7f17f186 100755 --- a/scripts/rules.sh +++ b/scripts/rules.sh @@ -62,6 +62,15 @@ written_in_english() { && went_well "no Spanish identifiers" } +tests_named_in_english() { + grep -rnE '\b(describe|it|test)\("[^"]*(á|é|í|ó|ú|ñ|¿|¡|\b(el|la|los|las|que|una|del|con|sin|cuando|para)\b)' \ + app/src --include='*.test.ts' --include='*.test.tsx' + found_nothing \ + "the window's tests are named in English" $? \ + "the window's tests could not be looked through for their names" \ + && went_well "the window's tests are named in English" +} + no_comments_in_the_code() { grep -rnE '[/]{2}' crates app/src-tauri/src --include='*.rs' \ | grep -vE '[a-z]+:[/][/]' | grep -vE '"[^"]*[/]{2}' @@ -117,6 +126,7 @@ unsafe_only_in_the_sys_crates nothing_the_core_prints nothing_the_core_depends_on written_in_english +tests_named_in_english no_comments_in_the_code every_example_has_its_own_name what_python_measures "the tests live beside the file, not inside it" oversized.py --inline From a4b52aa6d8cce211f2087c3fbc845a709214b4c5 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Tue, 6 Oct 2026 04:37:56 -0300 Subject: [PATCH 6/6] test(gui): the newest stable release is picked from any list, and the news step has no dead branch --- app/src/tests/welcome.test.ts | 5 +++++ app/src/ui/Welcome.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/tests/welcome.test.ts b/app/src/tests/welcome.test.ts index a384bd47..4aefae54 100644 --- a/app/src/tests/welcome.test.ts +++ b/app/src/tests/welcome.test.ts @@ -80,5 +80,10 @@ describe("settings opened from the welcome", () => { expect(newest).not.toBeNull(); expect(newest).not.toContain("-"); expect(news.some((one) => one.version === newest)).toBe(true); + expect(newestStable(["3.0.0", "3.1.0-rc1", "2.9.9", "3.0.10", "3.0.2"])).toBe("3.0.10"); + expect(newestStable(["3.2.0", "4.0.0", "3.10.0"])).toBe("4.0.0"); + expect(newestStable(["3.1.0", "3.10.0", "3.9.0"])).toBe("3.10.0"); + expect(newestStable(["3.1.0-rc1"])).toBeNull(); + expect(newestStable([])).toBeNull(); }); }); diff --git a/app/src/ui/Welcome.tsx b/app/src/ui/Welcome.tsx index ed8c25af..fe0c67d3 100644 --- a/app/src/ui/Welcome.tsx +++ b/app/src/ui/Welcome.tsx @@ -40,8 +40,8 @@ export function steps(former: boolean, asksTrust: boolean, tellsNews = false): S ]; } -export function newestStable(): string | null { - const stable = news.map((one) => one.version).filter((one) => !one.includes("-")); +export function newestStable(versions: string[] = news.map((one) => one.version)): string | null { + const stable = versions.filter((one) => !one.includes("-")); const parts = (one: string) => one.split(".").map(Number); stable.sort((a, b) => { const [x, y] = [parts(a), parts(b)]; @@ -150,6 +150,7 @@ function Tour({ const worthBringing = !!old && !old.unreadable && old.items > 0 && (old.came ?? 0) === 0; const asksTrust = !!trustAtStart?.offered && !trustAtStart.pastes; const newest = former ? newestStable() : null; + const told = newest === null ? [] : [newest]; const all = useMemo( () => steps(worthBringing, asksTrust, newest !== null), [worthBringing, asksTrust, newest], @@ -188,7 +189,7 @@ function Tour({ case "trust": return ; case "news": - return newest ? : null; + return ; case "keys": return ; case "where":