feat(FN-415): add locator-stability + hit-target verification tests

Commits merged:
- feat(FN-415): add locator-stability + hit-target verification tests

Files changed:
apps/api/src/telemetry/__tests__/telemetry.spec.ts |  8 +++
 .../src/routes/__tests__/dashboard-search.test.tsx | 67 ++++++++++++++++++++++
 2 files changed, 75 insertions(+)

Fusion-Task-Id: FN-415

Fusion-Task-Lineage: 01e6f3d2-cd2e-4023-b538-ea54926b4f87
This commit is contained in:
Fusion
2026-05-17 06:17:51 +00:00
parent 9b1fc48a54
commit 5118c8e5e9
2 changed files with 75 additions and 0 deletions

View File

@@ -1,5 +1,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock the heavy @opentelemetry/sdk-node meta-package to avoid slow/cascading imports
// that can cause the test to time out under system load.
vi.mock("@opentelemetry/sdk-node", () => ({
NodeSDK: class MockNodeSDK {
start() {}
},
}));
describe("Telemetry module", () => {
const originalEnv = process.env;

View File

@@ -170,3 +170,70 @@ test("retry suppressed for subscription block (abone olun)", async () => {
expect(screen.queryByRole("button", { name: /Tekrar Dene/i })).toBeNull();
});
// ─── FN-415: locator-stability + hit-target audit (AC 1, 3, 4) ────────────
test("locator stability: canonical Faro/ARIA hooks resolve and no data-testid is used", async () => {
const { ApiError: MockApiError } = await import("@/lib/api-client");
(api.post as any).mockRejectedValueOnce(
new (MockApiError as any)(503, "Servis geçici olarak kullanılamıyor"),
);
const { container } = renderSearch();
// (d) VIN input — getByRole('textbox') + canonical placeholder.
const vinInput = screen.getByPlaceholderText("Şase numarasını girin (17 karakter)");
expect(vinInput).toBe(screen.getByRole("textbox"));
// (e) Submit — getByRole('button', { name: 'Şase Çöz' }).
const submit = screen.getByRole("button", { name: "Şase Çöz" });
expect(submit).toBeInTheDocument();
typeVin(TEST_VIN);
await submitForm();
// (b) Error banner — role="alert" + aria-live="assertive".
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
const alert = screen.getByRole("alert");
expect(alert).toHaveAttribute("aria-live", "assertive");
expect(alert).toHaveTextContent("Şase çözümlenemedi");
// (a) Retry — [data-faro-user-action-name="vin-decode-retry"].
const retryByFaro = container.querySelector(
'[data-faro-user-action-name="vin-decode-retry"]',
) as HTMLButtonElement | null;
expect(retryByFaro).not.toBeNull();
// (c) Retry accessible-name fallback.
const retryByName = screen.getByRole("button", { name: "Tekrar Dene" });
expect(retryByName).toBe(retryByFaro);
// AC 4 (drift hardening) — no data-testid attributes anywhere in the render.
expect(container.querySelector("[data-testid]")).toBeNull();
});
test("hit-target audit: retry button meets the 44px Apple HIG / Material floor", async () => {
const { ApiError: MockApiError } = await import("@/lib/api-client");
(api.post as any).mockRejectedValueOnce(
new (MockApiError as any)(503, "Servis geçici olarak kullanılamıyor"),
);
renderSearch();
typeVin(TEST_VIN);
await submitForm();
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
const retry = screen.getByRole("button", { name: "Tekrar Dene" });
// The retry button uses Tailwind `h-11` (= 2.75rem = 44px at the default
// 16px root) and `w-full`, so it inherits the parent error-banner width on
// any viewport ≥ ~280px (well below the iPhone SE 375px baseline). Assert
// the class contract — jsdom does not run layout, so a getBoundingClientRect
// measurement would always be 0×0 and is intentionally avoided here.
expect(retry.className).toMatch(/(^|\s)h-11(\s|$)/);
expect(retry.className).toMatch(/(^|\s)w-full(\s|$)/);
});