-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ux): customer-facing copy audit — hide implementation boundaries, add next-action guidance #1034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
fix(ux): customer-facing copy audit — hide implementation boundaries, add next-action guidance #1034
Changes from all commits
5ee3736
dce2203
3ef661c
638ad78
010e747
716035f
375e41f
13845c0
48ce16f
25065ca
fcf6e23
9cdbb79
f98f12c
81fb426
f8b0fef
ff6d61f
41cca66
537d267
a37583a
6f144f6
dba7f6b
6934dfa
8172227
65d4e6b
7cd1210
5ea3a4c
a07d0d7
042caed
9912540
c1a2488
ad82ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { App } from "./App"; | ||
|
|
||
| // App mounts the Score surface, whose pdf.js bridge depends on browser canvas | ||
| // globals such as DOMMatrix that jsdom does not provide. This regression only | ||
| // exercises source-selection localization, so isolate that unrelated boundary | ||
| // exactly as the canonical App suite does. | ||
| vi.mock("./features/score/pdfjs", () => ({ | ||
| configureScorePdfWorker: vi.fn(), | ||
| loadScorePdf: vi.fn(() => ({ | ||
| promise: Promise.resolve({ numPages: 1, getPage: vi.fn() }), | ||
| destroy: vi.fn(() => Promise.resolve()) | ||
| })) | ||
| })); | ||
|
|
||
| const originalLanguage = navigator.language; | ||
| const originalInternals = window.__TAURI_INTERNALS__; | ||
| const originalInvoke = window.__TAURI_INVOKE__; | ||
|
|
||
| function setNavigatorLanguage(language: string) { | ||
| Object.defineProperty(navigator, "language", { | ||
| configurable: true, | ||
| value: language | ||
| }); | ||
| } | ||
|
|
||
| describe("App localized source errors", () => { | ||
| afterEach(() => { | ||
| setNavigatorLanguage(originalLanguage); | ||
| window.__TAURI_INTERNALS__ = originalInternals; | ||
| window.__TAURI_INVOKE__ = originalInvoke; | ||
| }); | ||
|
|
||
| it("keeps the browser local-audio fallback in the selected Korean locale", async () => { | ||
| setNavigatorLanguage("ko-KR"); | ||
| window.__TAURI_INTERNALS__ = undefined; | ||
| window.__TAURI_INVOKE__ = undefined; | ||
|
|
||
| render(<App />); | ||
| fireEvent.click(screen.getByRole("button", { name: "로컬 오디오 선택" })); | ||
|
|
||
| expect(await screen.findByRole("alert")).toHaveTextContent( | ||
| "분석을 시작하려면 WAV, MP3, FLAC 또는 M4A 파일을 선택하세요." | ||
| ); | ||
| expect(screen.queryByText("Choose a WAV, MP3, FLAC, or M4A file to start analysis.")).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -210,6 +210,11 @@ function sectionCountDetail(t: ReturnType<typeof createTranslator>, sectionCount | |
| function ConfidenceMetric({ song, t }: { song: RehearsalSong | null; t: ReturnType<typeof createTranslator> }) { | ||
| const sectionCount = song?.sections.length ?? 0; | ||
| const confidenceOrder = { high: 3, medium: 2, low: 1 } as const; | ||
| const confidenceShortKeys = { | ||
| low: "confidenceShortLow", | ||
| medium: "confidenceShortMedium", | ||
| high: "confidenceShortHigh" | ||
| } as const satisfies Record<RehearsalSong["sections"][number]["confidence"]["level"], TranslationKey>; | ||
|
|
||
| // Performance: Avoid O(N) array scan with .reduce() to find minimum confidence. | ||
| // Instead use a for loop that can early exit (O(K)) as soon as the lowest bound ("low") is hit. | ||
|
|
@@ -224,7 +229,7 @@ function ConfidenceMetric({ song, t }: { song: RehearsalSong | null; t: ReturnTy | |
| } | ||
| } | ||
| } | ||
| const confidence = lowestConfidence ? `${lowestConfidence[0].toUpperCase()}${lowestConfidence.slice(1)}` : t("metricConfidenceReady"); | ||
| const confidence = lowestConfidence ? t(confidenceShortKeys[lowestConfidence]) : t("metricConfidenceReady"); | ||
| const detail = sectionCountDetail(t, sectionCount); | ||
|
|
||
| return ( | ||
|
|
@@ -435,13 +440,13 @@ export function App() { | |
| setSelectionErrorSource(null); | ||
| const normalizedUrl = youtubeUrl.trim(); | ||
| if (!normalizedUrl) { | ||
| setSelectionError(t("youtubeImportFailed")); | ||
| setSelectionError(t("youtubeLinkGuidance")); | ||
| setSelectionErrorSource("youtube"); | ||
| return; | ||
| } | ||
|
|
||
| if (!isSupportedYoutubeUrl(normalizedUrl)) { | ||
| setSelectionError(t("youtubeImportFailed")); | ||
| setSelectionError(t("youtubeLinkGuidance")); | ||
| setSelectionErrorSource("youtube"); | ||
| return; | ||
| } | ||
|
Comment on lines
442
to
452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Stale tests assert removed YouTube rejection copy
Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { App } from "./App"; | ||
|
|
||
| // App mounts the Score surface, whose pdf.js bridge depends on browser canvas | ||
| // globals such as DOMMatrix that jsdom does not provide. This regression only | ||
| // exercises pre-network YouTube URL admission guidance. | ||
| vi.mock("./features/score/pdfjs", () => ({ | ||
| configureScorePdfWorker: vi.fn(), | ||
| loadScorePdf: vi.fn(() => ({ | ||
| promise: Promise.resolve({ numPages: 1, getPage: vi.fn() }), | ||
| destroy: vi.fn(() => Promise.resolve()) | ||
| })) | ||
| })); | ||
|
|
||
| const originalLanguage = navigator.language; | ||
| const originalInternals = window.__TAURI_INTERNALS__; | ||
| const originalInvoke = window.__TAURI_INVOKE__; | ||
|
|
||
| function setNavigatorLanguage(language: string) { | ||
| Object.defineProperty(navigator, "language", { | ||
| configurable: true, | ||
| value: language | ||
| }); | ||
| } | ||
|
|
||
| describe("App YouTube URL admission guidance", () => { | ||
| afterEach(() => { | ||
| setNavigatorLanguage(originalLanguage); | ||
| window.__TAURI_INTERNALS__ = originalInternals; | ||
| window.__TAURI_INVOKE__ = originalInvoke; | ||
| }); | ||
|
|
||
| it("uses format guidance for a URL rejected before any import attempt", async () => { | ||
| setNavigatorLanguage("en-US"); | ||
| window.__TAURI_INTERNALS__ = undefined; | ||
| window.__TAURI_INVOKE__ = undefined; | ||
|
|
||
| render(<App />); | ||
| const input = screen.getByRole("textbox", { name: /YouTube URL/i }); | ||
| fireEvent.change(input, { target: { value: "https://example.com/watch?v=abc123DEF45" } }); | ||
| fireEvent.click(screen.getByRole("button", { name: /Import YouTube/i })); | ||
|
|
||
| const alert = await screen.findByRole("alert"); | ||
| expect(alert).toHaveTextContent("Use a standard YouTube video link (youtube.com/watch or youtu.be)."); | ||
| expect(alert).not.toHaveTextContent(/check your connection/i); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { afterEach, describe, expect, it } from "vitest"; | ||
| import { attachScorePdf } from "./scoreStorage"; | ||
|
|
||
| const originalLanguage = navigator.language; | ||
| const originalInternals = window.__TAURI_INTERNALS__; | ||
| const originalInvoke = window.__TAURI_INVOKE__; | ||
|
|
||
| function setNavigatorLanguage(language: string) { | ||
| Object.defineProperty(navigator, "language", { | ||
| configurable: true, | ||
| value: language | ||
| }); | ||
| } | ||
|
|
||
| describe("score storage buyer-visible error localization", () => { | ||
| afterEach(() => { | ||
| setNavigatorLanguage(originalLanguage); | ||
| window.__TAURI_INTERNALS__ = originalInternals; | ||
| window.__TAURI_INVOKE__ = originalInvoke; | ||
| }); | ||
|
|
||
| it("keeps the browser-only score message in Korean", async () => { | ||
| setNavigatorLanguage("ko-KR"); | ||
| window.__TAURI_INTERNALS__ = undefined; | ||
| window.__TAURI_INVOKE__ = undefined; | ||
|
|
||
| await expect(attachScorePdf("project-1", "song-1")).rejects.toThrow( | ||
| "악보 PDF는 BandScope 데스크톱 앱에서만 사용할 수 있습니다." | ||
| ); | ||
| }); | ||
|
|
||
| it("keeps an invalid bridge response in Korean", async () => { | ||
| setNavigatorLanguage("ko-KR"); | ||
| window.__TAURI_INTERNALS__ = undefined; | ||
| window.__TAURI_INVOKE__ = async () => ({}); | ||
|
|
||
| await expect(attachScorePdf("project-1", "song-1")).rejects.toThrow( | ||
| "악보를 준비할 수 없습니다. 다시 추가해 주세요." | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Localized import-failure copy survives safeErrorDetail redaction
The bridge-failure branch wraps
youtubeImportFailedinsafeErrorDetail(App.tsx:461), capped at 220 chars. Both locale strings are shorter and contain no URL/path/secret patterns, so redaction and truncation leave them intact.(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.