From 07d4c9c96e25f81433156075c3e2bea22a4373cc Mon Sep 17 00:00:00 2001 From: Ohualtex <230626673+Ohualtex@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:18:17 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20openFieldDetail=20navigate=20ile=20t?= =?UTF-8?q?utarl=C4=B1=20+=20role-span=20gating'den=20hari=C3=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fields.js openFieldDetail: scrollTo(0,0) + focus preventScroll:true + mobil sidebar kapatma (navigate() ile tutarlı; tarla detayı artık tepeden açılır) - account.js _applyRoleVisibility: .user-badge-role HARİÇ (o span data-role'ü yalnız CSS renk kodu için taşır, gating'e girmemeli) + birim test için export Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/pages/account.js | 11 +++++++---- frontend/src/lib/pages/fields.js | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/pages/account.js b/frontend/src/lib/pages/account.js index c68675e..2736a4f 100644 --- a/frontend/src/lib/pages/account.js +++ b/frontend/src/lib/pages/account.js @@ -85,11 +85,14 @@ export function goToWelcome() { } /** - * Rol-aware nav görünürlüğü — `[data-role]` taşıyan nav item'ları yalnız - * eşleşen role gösterir (örn. admin "Kullanıcılar"). user null ise hepsi gizli. + * Rol-aware görünürlük — `[data-role]` taşıyan elemanları yalnız eşleşen role + * gösterir (örn. admin "Kullanıcılar"). user null ise hepsi gizli. + * `.user-badge-role` HARİÇ tutulur: o span data-role'ü SADECE CSS renk kodu için + * taşır, görünürlük gating'ine girmemeli (badge görünürken her zaman görünmeli). + * (export: vitest birim testi için.) */ -function _applyRoleVisibility(user) { - document.querySelectorAll('[data-role]').forEach(el => { +export function _applyRoleVisibility(user) { + document.querySelectorAll('[data-role]:not(.user-badge-role)').forEach(el => { // data-role tek rol ("admin") veya virgüllü çoklu rol ("admin,overseer,developer") const roles = (el.getAttribute('data-role') || '').split(',').map(r => r.trim()); el.style.display = (user && roles.includes(user.role)) ? '' : 'none'; diff --git a/frontend/src/lib/pages/fields.js b/frontend/src/lib/pages/fields.js index 1a1c603..3a30d9e 100644 --- a/frontend/src/lib/pages/fields.js +++ b/frontend/src/lib/pages/fields.js @@ -249,8 +249,14 @@ export function openFieldDetail(fieldId) { document.getElementById('pageTitle').textContent = pageTitles['field-detail'][0]; document.getElementById('pageSubtitle').textContent = pageTitles['field-detail'][1]; if (location.hash !== `#field/${fieldId}`) location.hash = `#field/${fieldId}`; + // navigate() ile tutarlı: sayfa tepeden açılsın, odak kaydırmasın, mobil sidebar kapansın. + window.scrollTo(0, 0); const main = document.getElementById('main-content'); - if (main) main.focus({ preventScroll: false }); + if (main) main.focus({ preventScroll: true }); + const sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.classList.remove('open'); + const hamburger = document.querySelector('.hamburger'); + if (hamburger) hamburger.setAttribute('aria-expanded', 'false'); loadFieldDetail(fieldId); } From cd56d1779391f69085effeddaa6c066854d20706 Mon Sep 17 00:00:00 2001 From: Ohualtex <230626673+Ohualtex@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:18:35 +0300 Subject: [PATCH 2/2] =?UTF-8?q?test:=20nav/tema/rol=20davran=C4=B1=C5=9Fla?= =?UTF-8?q?r=C4=B1=20i=C3=A7in=20vitest=20kapsam=C4=B1=20(+15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - charts_retheme.test.js (4): rethemeCharts renk güncelleme + amber/mavi accent korunması + grid-kapalı eksen - account_rolevis.test.js (5): rol-gating (admin/çiftçi/developer/null) + .user-badge-role hariç - router_nav.test.js (6): sayfa aktivasyon + hash senkron (replaceState) + scrollTo(0,0) + nav-item active + sidebar + handler Co-Authored-By: Claude Opus 4.8 --- frontend/tests/account_rolevis.test.js | 59 +++++++++++++++++++ frontend/tests/charts_retheme.test.js | 81 ++++++++++++++++++++++++++ frontend/tests/router_nav.test.js | 72 +++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 frontend/tests/account_rolevis.test.js create mode 100644 frontend/tests/charts_retheme.test.js create mode 100644 frontend/tests/router_nav.test.js diff --git a/frontend/tests/account_rolevis.test.js b/frontend/tests/account_rolevis.test.js new file mode 100644 index 0000000..28a105f --- /dev/null +++ b/frontend/tests/account_rolevis.test.js @@ -0,0 +1,59 @@ +/** + * Birim testler — _applyRoleVisibility() (frontend/src/lib/pages/account.js). + * + * Rol-aware görünürlük: [data-role] taşıyan elemanlar yalnız eşleşen role + * görünür; user null ise hepsi gizli. .user-badge-role span'i HARİÇ (yalnız + * CSS renk kodu için data-role taşır, gating'e girmemeli). + * jsdom environment vitest config'inden gelir. + */ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { _applyRoleVisibility } from "../src/lib/pages/account.js"; + +beforeEach(() => { + document.body.innerHTML = ` + Kullanıcılar + Raporlar + </> + yönetici`; +}); +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("_applyRoleVisibility", () => { + it("admin: tüm admin-içeren öğeler görünür", () => { + _applyRoleVisibility({ role: "admin" }); + expect(document.getElementById("usersNav").style.display).toBe(""); + expect(document.getElementById("analyticsNav").style.display).toBe(""); + expect(document.getElementById("devLink").style.display).toBe(""); + }); + + it("çiftçi: admin-only öğeler gizli", () => { + _applyRoleVisibility({ role: "farmer" }); + expect(document.getElementById("usersNav").style.display).toBe("none"); + expect(document.getElementById("devLink").style.display).toBe("none"); + expect(document.getElementById("analyticsNav").style.display).toBe("none"); + }); + + it("developer: görünür ama Kullanıcılar gizli", () => { + _applyRoleVisibility({ role: "developer" }); + expect(document.getElementById("devLink").style.display).toBe(""); + expect(document.getElementById("analyticsNav").style.display).toBe(""); + expect(document.getElementById("usersNav").style.display).toBe("none"); + }); + + it("user null → tüm gated öğeler gizli", () => { + _applyRoleVisibility(null); + expect(document.getElementById("usersNav").style.display).toBe("none"); + expect(document.getElementById("analyticsNav").style.display).toBe("none"); + expect(document.getElementById("devLink").style.display).toBe("none"); + }); + + it(".user-badge-role gating'e GİRMEZ — hiçbir durumda gizlenmez", () => { + const span = document.getElementById("roleSpan"); // data-role=admin + _applyRoleVisibility({ role: "farmer" }); // eşleşmeyen rol + expect(span.style.display).toBe(""); // hariç → dokunulmadı + _applyRoleVisibility(null); // user yok + expect(span.style.display).toBe(""); // yine dokunulmadı + }); +}); diff --git a/frontend/tests/charts_retheme.test.js b/frontend/tests/charts_retheme.test.js new file mode 100644 index 0000000..5f60af9 --- /dev/null +++ b/frontend/tests/charts_retheme.test.js @@ -0,0 +1,81 @@ +/** + * Birim testler — rethemeCharts() (frontend/src/lib/charts.js). + * + * Tema değişiminde kayıtlı Chart.js instance'larının canvas-içi renkleri + * YERİNDE güncellenmeli; amber (#f59e0b) / mavi (#3b82f6) accent eksen + * tick'leri KORUNMALI; grid:display=false eksenine dokunulmamalı. + * jsdom environment vitest config'inden gelir. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + charts, + rethemeCharts, + chartTick, + chartLegend, + chartGrid, +} from "../src/lib/charts.js"; + +function mockChart() { + return { + options: { + plugins: { legend: { labels: { color: "#000" } }, title: { color: "#000" } }, + scales: { + x: { ticks: { color: "#000" }, grid: { color: "#000" } }, + y: { ticks: { color: "#f59e0b" }, grid: { color: "#000" } }, // amber accent + y1: { ticks: { color: "#3b82f6" }, grid: { display: false } }, // mavi accent, grid kapalı + }, + }, + update: vi.fn(), + }; +} + +function clearRegistry() { + Object.keys(charts).forEach((k) => delete charts[k]); +} + +describe("rethemeCharts", () => { + beforeEach(() => { + clearRegistry(); + document.documentElement.dataset.theme = "light"; + }); + afterEach(() => { + clearRegistry(); + delete document.documentElement.dataset.theme; + }); + + it("gri tick/legend/grid renklerini güncel temaya çeker + update çağırır", () => { + const c = mockChart(); + charts.test = c; + document.documentElement.dataset.theme = "dark"; + rethemeCharts(); + expect(c.options.scales.x.ticks.color).toBe(chartTick()); + expect(c.options.plugins.legend.labels.color).toBe(chartLegend()); + expect(c.options.scales.x.grid.color).toBe(chartGrid()); + expect(c.update).toHaveBeenCalled(); + }); + + it("amber/mavi accent eksen tick'lerini KORUR", () => { + const c = mockChart(); + charts.test = c; + document.documentElement.dataset.theme = "dark"; + rethemeCharts(); + expect(c.options.scales.y.ticks.color).toBe("#f59e0b"); + expect(c.options.scales.y1.ticks.color).toBe("#3b82f6"); + }); + + it("grid:display=false ekseninin grid rengine dokunmaz", () => { + const c = mockChart(); + charts.test = c; + rethemeCharts(); + expect(c.options.scales.y1.grid.display).toBe(false); + expect(c.options.scales.y1.grid.color).toBeUndefined(); + }); + + it("light↔dark farklı renkler üretir (tema-duyarlı)", () => { + document.documentElement.dataset.theme = "light"; + const lightTick = chartTick(); + document.documentElement.dataset.theme = "dark"; + const darkTick = chartTick(); + expect(lightTick).not.toBe(darkTick); + }); +}); diff --git a/frontend/tests/router_nav.test.js b/frontend/tests/router_nav.test.js new file mode 100644 index 0000000..40ca64a --- /dev/null +++ b/frontend/tests/router_nav.test.js @@ -0,0 +1,72 @@ +/** + * Birim testler — navigate() (frontend/src/lib/router.js). + * + * Kapsam: hedef sayfa aktive + başlık güncelleme; hash senkronu (refresh + * kalıcılığı, replaceState); tepeden açılma (scrollTo 0,0); active highlight + * yalnız .nav-item'lara; handler tetikleme. + * jsdom environment vitest config'inden gelir. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { navigate } from "../src/lib/router.js"; + +beforeEach(() => { + document.body.innerHTML = ` + + +
+ Genel + Hava +

+

+
+
`; + history.replaceState(null, "", " "); + vi.spyOn(window, "scrollTo").mockImplementation(() => {}); + vi.spyOn(history, "replaceState"); +}); +afterEach(() => { + vi.restoreAllMocks(); + document.body.innerHTML = ""; +}); + +describe("navigate", () => { + it("hedef sayfayı aktive eder + öncekini kapatır + başlık günceller", () => { + navigate("weather", {}, null); + expect(document.getElementById("page-weather").classList.contains("active")).toBe(true); + expect(document.getElementById("page-dashboard").classList.contains("active")).toBe(false); + expect(document.getElementById("pageTitle").textContent).toBeTruthy(); + expect(document.getElementById("pageSubtitle").textContent).toBeTruthy(); + }); + + it("hash'i mevcut sayfaya senkronlar (refresh kalıcılığı)", () => { + navigate("weather", {}, null); + expect(history.replaceState).toHaveBeenCalledWith(null, "", "#weather"); + }); + + it("sayfayı tepeden açar (scrollTo 0,0)", () => { + navigate("weather", {}, null); + expect(window.scrollTo).toHaveBeenCalledWith(0, 0); + }); + + it("active highlight yalnız .nav-item'a — çip (nav-item değil) active almaz", () => { + document.body.insertAdjacentHTML( + "beforeend", + '', + ); + navigate("weather", {}, null); + expect(document.querySelector('.nav-item[href="#weather"]').classList.contains("active")).toBe(true); + expect(document.getElementById("chip").classList.contains("active")).toBe(false); + }); + + it("mobil sidebar'ı kapatır", () => { + navigate("weather", {}, null); + expect(document.getElementById("sidebar").classList.contains("open")).toBe(false); + expect(document.querySelector(".hamburger").getAttribute("aria-expanded")).toBe("false"); + }); + + it("sayfa handler'ını çağırır", () => { + const h = vi.fn(); + navigate("weather", { weather: h }, null); + expect(h).toHaveBeenCalledTimes(1); + }); +});